diff options
Diffstat (limited to 'src/client/views/collections')
47 files changed, 8397 insertions, 2310 deletions
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss new file mode 100644 index 000000000..5f8895c1f --- /dev/null +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -0,0 +1,107 @@ +.collectionCarousel3DView-outer { + height: 100%; + position: relative; + background-color: white; +} + +.carousel-wrapper { + display: flex; + position: absolute; + top: 15%; + align-items: center; + transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); + + .collectionCarousel3DView-item, + .collectionCarousel3DView-item-active { + flex: 1; + transition: opacity 0.3s linear, transform 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955); + pointer-events: none; + } + + .collectionCarousel3DView-item-active { + pointer-events: unset; + } +} + +.dot-bar { + display: flex; + position: absolute; + justify-content: center; + bottom: 5%; + width: 100%; + + .dot, + .dot-active { + height: 10px; + width: 10px; + border-radius: 50%; + margin: 3px; + display: inline-block; + background-color: lightgrey; + cursor: pointer; + } + + .dot-active { + background-color: grey; + } +} + +.carousel3DView-back, +.carousel3DView-fwd, +.carousel3DView-back-scroll, +.carousel3DView-fwd-scroll, +.carousel3DView-back-scroll-hidden, +.carousel3DView-fwd-scroll-hidden { + position: absolute; + display: flex; + width: 30; + height: 30; + align-items: center; + border-radius: 5px; + justify-content: center; + background: rgba(255, 255, 255, 0.46); + cursor: pointer; +} + +.carousel3DView-fwd, +.carousel3DView-back { + top: 50%; +} + +.carousel3DView-fwd-scroll, +.carousel3DView-back-scroll, +.carousel3DView-fwd-scroll-hidden, +.carousel3DView-back-scroll-hidden { + top: calc(50% - 30px); +} + +.carousel3DView-fwd, +.carousel3DView-fwd-scroll, +.carousel3DView-fwd-scroll-hidden { + right: 0; +} + +.carousel3DView-back, +.carousel3DView-back-scroll, +.carousel3DView-back-scroll-hidden { + left: 0; +} + +.carousel3DView-fwd-scroll-hidden, +.carousel3DView-back-scroll-hidden { + opacity: 0; + transition: opacity 0.5s linear; + pointer-events: none; +} + +.carousel3DView-fwd-scroll, +.carousel3DView-back-scroll { + opacity: 1; +} + +.carousel3DView-back:hover, +.carousel3DView-fwd:hover, +.carousel3DView-back-scroll:hover, +.carousel3DView-fwd-scroll:hover { + background: lightgray; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx new file mode 100644 index 000000000..0f3b6f212 --- /dev/null +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -0,0 +1,183 @@ +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { documentSchema, collectionSchema } from '../../../fields/documentSchemas'; +import { makeInterface } from '../../../fields/Schema'; +import { NumCast, StrCast, ScriptCast } from '../../../fields/Types'; +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); + +@observer +export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocument) { + @computed get scrollSpeed() { + return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; //default scroll speed + } + + private _dropDisposer?: DragManager.DragDropDisposer; + + componentWillUnmount() { this._dropDisposer?.(); } + + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this._dropDisposer?.(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); + } + } + + panelWidth = () => this.props.PanelWidth() / 3; + panelHeight = () => this.props.PanelHeight() * 0.6; + onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); + @computed get content() { + const currentIndex = NumCast(this.layoutDoc._itemIndex); + 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} + onDoubleClick={this.onChildDoubleClick} + onClick={onChildClick} + renderDepth={this.props.renderDepth + 1} + LayoutTemplate={this.props.ChildLayoutTemplate} + LayoutTemplateString={this.props.ChildLayoutString} + Document={childPair.layout} + DataDoc={childPair.data} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} + bringToFront={returnFalse} + parentActive={this.props.active} + />; + }; + + return (this.childLayoutPairs.map((childPair, index) => { + return ( + <div key={childPair.layout[Id]} + className={`collectionCarousel3DView-item${index === currentIndex ? "-active" : ""} ${index}`} + style={index === currentIndex ? + { opacity: '1', transform: 'scale(1.3)' } : + { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none' }}> + {displayDoc(childPair)} + </div>); + })); + } + + changeSlide = (direction: number) => { + this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length; + } + + onArrowClick = (e: React.MouseEvent, direction: number) => { + e.stopPropagation(); + this.changeSlide(direction); + !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = (direction === 1) ? "fwd" : "back"); // while autoscroll is on, keep the other autoscroll button hidden + !this.layoutDoc.autoScrollOn && this.fadeScrollButton(); // keep pause button visible while autoscroll is on + } + + interval?: number; + startAutoScroll = (direction: number) => { + this.interval = window.setInterval(() => { + this.changeSlide(direction); + }, this.scrollSpeed); + } + + stopAutoScroll = () => { + window.clearInterval(this.interval); + this.interval = undefined; + this.fadeScrollButton(); + } + + toggleAutoScroll = (direction: number) => { + this.layoutDoc.autoScrollOn = this.layoutDoc.autoScrollOn ? false : true; + this.layoutDoc.autoScrollOn ? this.startAutoScroll(direction) : this.stopAutoScroll(); + } + + fadeScrollButton = () => { + window.setTimeout(() => { + !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = "none"); //fade away after 1.5s if it's not clicked. + }, 1500); + } + + _downX = 0; + _downY = 0; + onPointerDown = (e: React.PointerEvent) => { + this._downX = e.clientX; + this._downY = e.clientY; + document.addEventListener("pointerup", this.onpointerup); + } + private _lastTap: number = 0; + private _doubleTap = false; + onpointerup = (e: PointerEvent) => { + this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); + this._lastTap = Date.now(); + } + + onClick = (e: React.MouseEvent) => { + if (this._doubleTap) { + e.stopPropagation(); + this.props.Document.isLightboxOpen = true; + } + } + + @computed get buttons() { + if (!this.props.active()) return null; + return <div className="arrow-buttons" > + <div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} + onClick={(e) => this.onArrowClick(e, -1)} + > + <FontAwesomeIcon icon={"angle-left"} size={"2x"} /> + </div> + <div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} + onClick={(e) => this.onArrowClick(e, 1)} + > + <FontAwesomeIcon icon={"angle-right"} size={"2x"} /> + </div> + {this.autoScrollButton} + </div>; + } + + @computed get autoScrollButton() { + const whichButton = this.layoutDoc.showScrollButton; + return <> + <div className={`carousel3DView-back-scroll${whichButton === "back" ? "" : "-hidden"}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} + onClick={() => this.toggleAutoScroll(-1)}> + {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={"pause"} size={"1x"} /> : <FontAwesomeIcon icon={"angle-double-left"} size={"1x"} />} + </div> + <div className={`carousel3DView-fwd-scroll${whichButton === "fwd" ? "" : "-hidden"}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} + onClick={() => this.toggleAutoScroll(1)}> + {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={"pause"} size={"1x"} /> : <FontAwesomeIcon icon={"angle-double-right"} size={"1x"} />} + </div> + </>; + } + + @computed get dots() { + return (this.childLayoutPairs.map((_child, index) => { + return <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? "-active" : ""}`} + onClick={() => this.layoutDoc._itemIndex = index} />; + })); + } + + render() { + const index = NumCast(this.layoutDoc._itemIndex); + const translateX = (1 - index) / this.childLayoutPairs.length * 100; + + return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> + <div className="carousel-wrapper" style={{ transform: `translateX(calc(${translateX}%` }}> + {this.content} + </div> + {this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)} + <div className="dot-bar"> + {this.dots} + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index f65a89422..27aea4b99 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -14,6 +14,7 @@ import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { ContextMenu } from '../ContextMenu'; import { ObjectField } from '../../../fields/ObjectField'; import { returnFalse } from '../../../Utils'; +import { ScriptField } from '../../../fields/ScriptField'; type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>; const CarouselDocument = makeInterface(documentSchema, collectionSchema); @@ -40,14 +41,16 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; } panelHeight = () => this.props.PanelHeight() - 50; + onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); + onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); @computed get content() { const index = NumCast(this.layoutDoc._itemIndex); return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) : <> <div className="collectionCarouselView-image" key="image"> <ContentFittingDocumentView {...this.props} - onDoubleClick={ScriptCast(this.layoutDoc.onChildDoubleClick)} - onClick={ScriptCast(this.layoutDoc.onChildClick)} + onDoubleClick={this.onContentDoubleClick} + onClick={this.onContentClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -83,30 +86,16 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) </>; } - - onContextMenu = (e: React.MouseEvent): void => { - // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped()) { - ContextMenu.Instance.addItem({ - description: "Make Hero Image", event: () => { - const index = NumCast(this.layoutDoc._itemIndex); - (this.dataDoc || Doc.GetProto(this.props.Document)).hero = ObjectField.MakeCopy(this.childLayoutPairs[index].layout.data as ObjectField); - }, icon: "plus" - }); - } - } _downX = 0; _downY = 0; onPointerDown = (e: React.PointerEvent) => { this._downX = e.clientX; this._downY = e.clientY; - console.log("CAROUSEL down"); document.addEventListener("pointerup", this.onpointerup); } private _lastTap: number = 0; private _doubleTap = false; onpointerup = (e: PointerEvent) => { - console.log("CAROUSEL up"); this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); this._lastTap = Date.now(); } @@ -119,7 +108,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument) } render() { - return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}> + return <div className="collectionCarouselView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> {this.content} {this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)} </div>; diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 2fafcecb2..4204ef5bb 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,34 +1,151 @@ @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; + } + } +} + +.miniPres:hover { + opacity: 1; +} + +.miniPres { + position: absolute; + overflow: hidden; + right: 10; + top: 10; + opacity: 0.1; + transition: all 0.4s; + /* border: solid 1px; */ + color: white; + /* box-shadow: black 0.4vw 0.4vw 0.8vw; */ + + .miniPresOverlay { + display: grid; + grid-template-columns: auto auto auto auto auto auto auto auto; + grid-template-rows: 100%; + height: 100%; + justify-items: center; + align-items: center; + + .miniPres-button-text { + display: flex; + height: 20; + font-weight: 400; + min-width: 100%; + border-radius: 5px; + align-items: center; + justify-content: center; + transition: all 0.3s; + } + + .miniPres-button-frame { + justify-self: center; + align-self: center; + align-items: center; + display: grid; + grid-template-columns: auto auto auto; + justify-content: space-around; + font-size: 11; + margin-left: 7; + width: 30; + height: 85%; + background-color: rgba(91, 157, 221, 0.4); + border-radius: 5px; + } + + .miniPres-divider { + width: 0.5px; + height: 80%; + border-right: solid 2px #5a5a5a; + } + + .miniPres-button { + display: flex; + height: 20; + min-width: 20; + border-radius: 100%; + align-items: center; + justify-content: center; + transition: all 0.3s; + } + + .miniPres-button:hover { + background-color: #5a5a5a; + } + + .miniPres-button-text:hover { + background-color: #5a5a5a; + } + } +} + + .lm_title { margin-top: 3px; - background: black; border-radius: 5px; border: solid 1px dimgray; border-width: 2px 2px 0px; height: 20px; transform: translate(0px, -3px); + cursor: grab; } + +.lm_title.focus-visible { + cursor: text; +} + .lm_title_wrap { overflow: hidden; height: 19px; - margin-top: -3px; - display:inline-block; + margin-top: -2px; + display: inline-block; } + .lm_active .lm_title { border: solid 1px lightgray; } + .lm_header .lm_tab .lm_close_tab { position: absolute; text-align: center; } .lm_header .lm_tab { - padding-right : 20px; + padding-right: 20px; + margin-top: -1px; + border-bottom: 1px black; + .collectionDockingView-gear { + display: none; + } +} + +.lm_header .lm_tab.lm_active { + padding-right: 20px; + margin-top: 1px; + border-bottom: unset; + .collectionDockingView-gear { + display: inline-block; + } } .lm_popout { - display:none; + display: none; } .messageCounter { @@ -51,14 +168,15 @@ position: absolute; top: 0; left: 0; + // overflow: hidden; // bcz: menus don't show up when this is on (e.g., the parentSelectorMenu) .collectionDockingView-gear { padding-left: 5px; height: 15px; width: 18px; - display: inline-block; margin: auto; } + .collectionDockingView-dragAsDocument { touch-action: none; position: absolute; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 6f5a3dfe4..7e096fa37 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,18 +1,16 @@ 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 } from "mobx"; +import { action, computed, Lambda, observable, reaction, runInAction, trace, IReactionDisposer } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; -import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; import { DateField } from '../../../fields/DateField'; import { Doc, DocListCast, Field, Opt, DataSym } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; -import { List } from '../../../fields/List'; import { FieldId } from "../../../fields/RefField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnTrue, Utils, returnZero } from "../../../Utils"; +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'; @@ -29,6 +27,13 @@ import { DockingViewButtonSelector } from './ParentDocumentSelector'; import React = require("react"); 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'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -44,7 +49,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp props: { documentId: document[Id], libraryPath: libraryPath?.map(d => d[Id]) - //collectionDockingView: CollectionDockingView.Instance } }; } @@ -392,6 +396,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp 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, () => { @@ -432,7 +442,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const cur = this._containerRef.current; // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed - this._goldenLayout && this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + this._goldenLayout?.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); } @action @@ -457,6 +467,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") { this._flush = true; } + if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { + return; + } else { + e.stopPropagation(); + } } updateDataField = async (json: string) => { @@ -465,7 +480,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (docids) { const docs = (await Promise.all(docids.map(id => DocServer.GetRefField(id)))).filter(f => f).map(f => f as Doc); - Doc.GetProto(this.props.Document)[this.props.fieldKey] = new List<Doc>(docs); + 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); } } @@ -497,7 +513,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc; if (doc instanceof Doc) { - //tab.titleElement[0].outerHTML = `<input class='lm_title' style="background:black" value='${doc.title}' />`; tab.titleElement[0].onclick = (e: any) => tab.titleElement[0].focus(); tab.titleElement[0].onchange = (e: any) => { tab.titleElement[0].size = e.currentTarget.value.length + 1; @@ -512,6 +527,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp 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; @@ -522,13 +541,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.setActive(true); }; const onDown = (e: React.PointerEvent) => { - if (!(e.nativeEvent as any).defaultPrevented) { - e.preventDefault(); - e.stopPropagation(); - const dragData = new DragManager.DocumentDragData([doc]); - dragData.dropAction = doc.dropAction as dropActionType; - DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY); - } + 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)), @@ -585,7 +606,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stackCreated = (stack: any) => { //stack.header.controlsContainer.find('.lm_popout').hide(); - stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; stack.header.element.on('mousedown', (e: any) => { if (e.target === stack.header.element[0] && e.button === 1) { this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" })); @@ -645,16 +665,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (this.props.renderDepth > 0) { return <div style={{ width: "100%", height: "100%" }}>Nested workspaces can't be rendered</div>; } - return ( - <Measure offset onResize={this.onResize}> - {({ measureRef }) => - <div ref={measureRef}> - <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} /> - </div> - } - </Measure> - ); + return <div className="collectiondockingview-container" id="menuContainer" + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />; } } @@ -663,7 +675,6 @@ interface DockedFrameProps { documentId: FieldId; glContainer: any; libraryPath: (FieldId[]); - backgroundColor?: (doc: Doc) => string | undefined; //collectionDockingView: CollectionDockingView } @observer @@ -674,10 +685,15 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @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)); @@ -738,9 +754,16 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { 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, "white") }), + (data) => { + const selected = data.views.some(v => Doc.AreProtosEqual(v.props.Document, this._document)); + 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); } @@ -749,6 +772,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { 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. } } @@ -768,10 +795,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { let scaling = 1; if (!this.layoutDoc?._fitWidth && (!nativeW || !nativeH)) { scaling = 1; - } else if ((this.layoutDoc?._fitWidth) || - this._panelHeight / NumCast(this.layoutDoc!._nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!._nativeWidth)) { + } 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 { + } 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)) { @@ -782,7 +809,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { // } const wscale = this.panelWidth() / nativeW; scaling = wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale; - } + } else scaling = 1; return scaling; } @@ -817,34 +844,135 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { } } + @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; + } + renderMiniPres() { + return ( + <div className="miniPres" + style={{ width: 250, height: 30, background: '#323232' }} + > + {<div className="miniPresOverlay"> + <div className="miniPres-button" onClick={PresBox.Instance.back}><FontAwesomeIcon icon={"arrow-left"} /></div> + <div className="miniPres-button" onClick={() => PresBox.Instance.startAutoPres(PresBox.Instance.itemIndex)}><FontAwesomeIcon icon={PresBox.Instance.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div> + <div className="miniPres-button" onClick={PresBox.Instance.next}><FontAwesomeIcon icon={"arrow-right"} /></div> + <div className="miniPres-divider"></div> + <div className="miniPres-button-text"> + Slide {PresBox.Instance.itemIndex + 1} / {PresBox.Instance.childDocs.length} + {PresBox.Instance.playButtonFrames} + </div> + <div className="miniPres-divider"></div> + <div className="miniPres-button-text" onClick={PresBox.Instance.updateMinimize}>EXIT</div> + </div>} + </div> + ); + } + 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!))), + }}> + <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> + </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;// document.layout instanceof Doc ? document : this._dataDoc; - 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} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} />; + 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)} + {document._viewType === CollectionViewType.Freeform && this._document?.miniPres ? this.renderMiniPres() : (null)} + </>; } render() { @@ -859,5 +987,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { </div >); } } -Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }); +Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); }, + "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); }); diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index 123a27deb..f5c4299a9 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/CollectionLinearView.scss @@ -1,12 +1,63 @@ @import "../globalCssVariables"; @import "../_nodeModuleOverrides"; -.collectionLinearView-outer{ - overflow: hidden; - height:100%; +.collectionLinearView-outer { + overflow: visible; + height: 100%; + .collectionLinearView { - display:flex; + display: flex; height: 100%; + + >span { + background: $dark-color; + color: $light-color; + border-radius: 18px; + margin-right: 6px; + cursor: pointer; + } + + .bottomPopup-background { + padding-right: 14px; + height: 35; + transform: translate3d(6px, 5px, 0px); + padding-top: 6.5px; + padding-bottom: 7px; + padding-left: 5px; + } + + .bottomPopup-text { + display: inline; + white-space: nowrap; + padding-left: 8px; + padding-right: 4px; + vertical-align: middle; + font-size: 12.5px; + } + + .bottomPopup-descriptions { + display: inline; + white-space: nowrap; + padding-left: 8px; + padding-right: 8px; + vertical-align: middle; + background-color: lightgrey; + border-radius: 5.5px; + color: black; + margin-right: 5px; + } + + .bottomPopup-exit { + display: inline; + white-space: nowrap; + padding-left: 8px; + padding-right: 8px; + vertical-align: middle; + background-color: lightgrey; + border-radius: 5.5px; + color: black; + } + >label { margin-top: "auto"; margin-bottom: "auto"; @@ -17,15 +68,15 @@ font-size: 12.5px; width: 18px; height: 18px; - margin-top:auto; - margin-bottom:auto; + margin-top: auto; + margin-bottom: auto; margin-right: 3px; cursor: pointer; transition: transform 0.2s; } label p { - padding-left:5px; + padding-left: 5px; } label:hover { @@ -36,6 +87,7 @@ >input { display: none; } + >input:not(:checked)~.collectionLinearView-content { display: none; } @@ -52,12 +104,14 @@ position: relative; margin-top: auto; - .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable { - position:relative; - margin:auto; + .collectionLinearView-docBtn, + .collectionLinearView-docBtn-scalable { + position: relative; + margin: auto; margin-left: 3px; transform-origin: center 80%; } + .collectionLinearView-docBtn-scalable:hover { transform: scale(1.15); } @@ -68,10 +122,10 @@ border-radius: 18px; font-size: 15px; } - + .collectionLinearView-round-button:hover { transform: scale(1.15); } } } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx index f1002044a..3cf46dbed 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -13,6 +13,10 @@ import { CollectionSubView } from './CollectionSubView'; import { DocumentView } from '../nodes/DocumentView'; import { documentSchema } from '../../../fields/documentSchemas'; import { Id } from '../../../fields/FieldSymbols'; +import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup'; +import { Tooltip } from '@material-ui/core'; type LinearDocument = makeInterface<[typeof documentSchema,]>; @@ -75,17 +79,54 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { return new Transform(-translateX, -translateY, 1); } + @action + exitLongLinks = () => { + if (DocumentLinksButton.StartLink) { + if (DocumentLinksButton.StartLink.Document) { + action((e: React.PointerEvent<HTMLDivElement>) => { + Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); + }); + } + } + DocumentLinksButton.StartLink = undefined; + } + + @action + changeDescriptionSetting = () => { + if (LinkDescriptionPopup.showDescriptions) { + if (LinkDescriptionPopup.showDescriptions === "ON") { + LinkDescriptionPopup.showDescriptions = "OFF"; + LinkDescriptionPopup.descriptionPopup = false; + } else { + LinkDescriptionPopup.showDescriptions = "ON"; + } + } else { + LinkDescriptionPopup.showDescriptions = "OFF"; + LinkDescriptionPopup.descriptionPopup = false; + } + } + render() { const guid = Utils.GenerateGuid(); const flexDir: any = StrCast(this.Document.flexDirection); const backgroundColor = StrCast(this.props.Document.backgroundColor, "black"); const color = StrCast(this.props.Document.color, "white"); + + 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)" + }} + onPointerDown={e => e.stopPropagation()} > + <p>+</p> + </label>; + return <div className="collectionLinearView-outer"> <div className="collectionLinearView" ref={this.createDashEventsTarget} > - <label htmlFor={`${guid}`} title="Close Menu" style={{ background: backgroundColor === color ? "black" : backgroundColor }} - onPointerDown={e => e.stopPropagation()} > - <p>+</p> - </label> + <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? "Close menu" : "Open menu"}</div></>} placement="top"> + {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)} /> @@ -120,15 +161,41 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { PanelHeight={nested ? pair.layout[HeightSym] : () => this.dimension()} renderDepth={this.props.renderDepth + 1} focus={emptyFunction} - backgroundColor={returnEmptyString} + backgroundColor={this.props.backgroundColor} parentActive={returnTrue} whenActiveChanged={emptyFunction} bringToFront={emptyFunction} + docFilters={this.props.docFilters} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} /> </div>; })} </div> + {DocumentLinksButton.StartLink ? <span className="bottomPopup-background" style={{ + background: backgroundColor === color ? "black" : backgroundColor + }} + onPointerDown={e => e.stopPropagation()} > + <span className="bottomPopup-text" > + Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in " : " "} {StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...'} + </span> + + <Tooltip title={<><div className="dash-tooltip">{LinkDescriptionPopup.showDescriptions ? "Turn off description pop-up" : + "Turn on description pop-up"} </div></>} placement="top"> + <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting}> + Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"} + </span> + </Tooltip> + + <Tooltip title={<><div className="dash-tooltip">Exit link clicking mode </div></>} placement="top"> + <span className="bottomPopup-exit" onClick={this.exitLongLinks}> + Clear + </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/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx index a0b7cd8a8..cfec3a6bc 100644 --- a/src/client/views/collections/CollectionMapView.tsx +++ b/src/client/views/collections/CollectionMapView.tsx @@ -42,7 +42,7 @@ const query = async (data: string | google.maps.LatLngLiteral) => { }; @observer -class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) { +export class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps> & { google: any }>(MapSchema) { private _cancelAddrReq = new Map<string, boolean>(); private _cancelLocReq = new Map<string, boolean>(); diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index cc7a9f5ac..c772dcfe7 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -43,6 +43,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @observable private heading: string = ""; @observable private color: string = "#f1efeb"; @observable private collapsed: boolean = false; + @observable private _paletteOn = false; private set _heading(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.heading = this.heading = value)); } private set _color(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.color = this.color = value)); } private set _collapsed(value: boolean) { runInAction(() => this.props.headingObject && (this.props.headingObject.collapsed = this.collapsed = value)); } @@ -83,7 +84,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @undoBatch rowDrop = action((e: Event, de: DragManager.DropEvent) => { - console.log("masronry row drop"); this._createAliasSelected = false; if (de.complete.docDragData) { (this.props.parent.Document.dropConverter instanceof ScriptField) && @@ -110,8 +110,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const key = StrCast(this.props.parent.props.Document._pivotField); const castedValue = this.getValue(value); if (castedValue) { - if (this.props.parent.sectionHeaders) { - if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) { + if (this.props.parent.columnHeaders) { + if (this.props.parent.columnHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) { return false; } } @@ -150,9 +150,9 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr this._createAliasSelected = false; const key = StrCast(this.props.parent.props.Document._pivotField); this.props.docList.forEach(d => d[key] = undefined); - if (this.props.parent.sectionHeaders && this.props.headingObject) { - const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); - this.props.parent.sectionHeaders.splice(index, 1); + 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); } })); @@ -238,7 +238,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr contents: "+ NEW", HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, - color: this.color }; const showChrome = (chromeStatus !== 'view-mode' && chromeStatus !== 'disabled'); const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `; @@ -278,7 +277,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr oneLine: true, HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, - color: this.color }; return this.props.parent.props.Document.miniHeaders ? <div className="collectionStackingView-miniHeader"> @@ -293,11 +291,10 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr {noChrome ? evContents : <EditableView {...headerEditableViewProps} />} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> - <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> - <button className="collectionStackingView-sectionColorButton"> - <FontAwesomeIcon icon="palette" size="lg" /> - </button> - </ Flyout > + <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> + <FontAwesomeIcon icon="palette" size="lg" /> + </button> + {this._paletteOn ? this.renderColorPicker() : (null)} </div> } {noChrome ? (null) : <button className="collectionStackingView-sectionDelete" onClick={noChrome ? undefined : this.collapseSection}> @@ -305,7 +302,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr </button>} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionOptions"> - <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> + <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderMenu()}> <button className="collectionStackingView-sectionOptionButton"> <FontAwesomeIcon icon="ellipsis-v" size="lg" /> </button> diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionMenu.scss index 03bd9a01a..b41cbe92d 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -1,41 +1,55 @@ @import "../globalCssVariables"; -@import '~js-datepicker/dist/datepicker.min.css'; -.collectionViewChrome-cont { - position: absolute; - width:100%; + +.collectionMenu-cont { + position: relative; + display: inline-flex; + width: 100%; opacity: 0.9; z-index: 9001; transition: top .5s; - background: lightgrey; + background: #323232; + color: white; transform-origin: top left; + top: 0; + width: 100%; + + .antimodeMenu-button { + padding: 0; + width: 30px; + display: flex; - .collectionViewChrome { + svg { + margin: auto; + } + } + + .collectionMenu { display: flex; padding-bottom: 1px; - height:32px; + height: 32px; border-bottom: .5px solid rgb(180, 180, 180); - overflow: hidden; + overflow: visible; + z-index: 9001; + border: unset; .collectionViewBaseChrome { display: flex; .collectionViewBaseChrome-viewPicker { font-size: 75%; - //text-transform: uppercase; - //letter-spacing: 2px; - background: rgb(238, 238, 238); - color: grey; + background: #323232; outline-color: black; + color: white; border: none; - //padding: 12px 10px 11px 10px; + border-right: solid gray 1px; } .collectionViewBaseChrome-viewPicker:active { outline-color: black; } - .collectionViewBaseChrome-button{ + .collectionViewBaseChrome-button { font-size: 75%; text-transform: uppercase; letter-spacing: 2px; @@ -46,23 +60,27 @@ padding: 12px 10px 11px 10px; margin-left: 10px; } + .collectionViewBaseChrome-cmdPicker { margin-left: 3px; margin-right: 0px; font-size: 75%; - background: rgb(238, 238, 238); + background: #323232; + color: white; border: none; - color: grey; + border-right: solid gray 1px; } + .commandEntry-outerDiv { pointer-events: all; - background-color: gray; + background-color: #323232; display: flex; flex-direction: row; - height:30px; + height: 30px; + .commandEntry-drop { - color:white; - width:25px; + color: white; + width: 30px; margin-top: auto; margin-bottom: auto; } @@ -71,38 +89,45 @@ .collectionViewBaseChrome-collapse { transition: all .5s, opacity 0.3s; position: absolute; - width: 40px; + width: 30px; transform-origin: top left; pointer-events: all; // margin-top: 10px; } + + @media only screen and (max-device-width: 480px) { + .collectionViewBaseChrome-collapse { + display: none; + } + } + .collectionViewBaseChrome-template, .collectionViewBaseChrome-viewModes { display: grid; background: rgb(238, 238, 238); - color:grey; - margin-top:auto; - margin-bottom:auto; - margin-left: 5px; - } - .collectionViewBaseChrome-viewModes { - margin-left: 25px; + color: grey; + margin-top: auto; + margin-bottom: auto; } .collectionViewBaseChrome-viewSpecs { margin-left: 5px; display: grid; - + border: none; + border-right: solid gray 1px; + .collectionViewBaseChrome-filterIcon { position: relative; display: flex; margin: auto; - background: gray; + background: #323232; color: white; width: 30px; height: 30px; align-items: center; justify-content: center; + border: none; + border-right: solid gray 1px; } .collectionViewBaseChrome-viewSpecsInput { @@ -163,13 +188,55 @@ } } - .collectionStackingViewChrome-cont, - .collectionTreeViewChrome-cont { + .collectionTreeViewChrome-cont, + .collection3DCarouselViewChrome-cont { display: flex; justify-content: space-between; } + .collectionGridViewChrome-cont { + display: flex; + margin-left: 10; + + .collectionGridViewChrome-viewPicker { + font-size: 75%; + //text-transform: uppercase; + //letter-spacing: 2px; + background: #121721; + color: white; + outline-color: black; + color: white; + border: none; + border-right: solid gray 1px; + } + + .collectionGridViewChrome-viewPicker:active { + outline-color: black; + } + + .grid-control { + align-self: center; + display: flex; + flex-direction: row; + margin-right: 5px; + + .grid-icon { + margin-right: 5px; + align-self: center; + } + + .flexLabel { + margin-bottom: 0; + } + } + + .collectionGridViewChrome-entryBox { + width: 50%; + } + } + + .collectionStackingViewChrome-sort, .collectionTreeViewChrome-sort { display: flex; @@ -189,23 +256,26 @@ .collectionStackingViewChrome-pivotField-cont, - .collectionTreeViewChrome-pivotField-cont { + .collectionTreeViewChrome-pivotField-cont, + .collection3DCarouselViewChrome-scrollSpeed-cont { justify-self: right; display: flex; font-size: 75%; letter-spacing: 2px; .collectionStackingViewChrome-pivotField-label, - .collectionTreeViewChrome-pivotField-label { + .collectionTreeViewChrome-pivotField-label, + .collection3DCarouselViewChrome-scrollSpeed-label { vertical-align: center; padding-left: 10px; - margin:auto; + margin: auto; } .collectionStackingViewChrome-pivotField, - .collectionTreeViewChrome-pivotField { + .collectionTreeViewChrome-pivotField, + .collection3DCarouselViewChrome-scrollSpeed { color: white; - width:100%; + width: 100%; min-width: 100px; display: flex; align-items: center; @@ -215,9 +285,9 @@ input, .editableView-container-editing-oneLine, .editableView-container-editing { - margin:auto; + margin: auto; border: 0px; - color: grey; + color: grey !important; text-align: center; letter-spacing: 2px; outline-color: black; @@ -233,47 +303,84 @@ } .collectionStackingViewChrome-pivotField:hover, - .collectionTreeViewChrome-pivotField:hover { + .collectionTreeViewChrome-pivotField:hover, + .collection3DCarouselViewChrome-scrollSpeed:hover { cursor: text; } + } } -.collectionFreeFormViewChrome-cont { - width: 60px; - display: flex; +.collectionFreeFormMenu-cont { + display: inline-flex; position: relative; align-items: center; - .fwdKeyframe, .numKeyframe, .backKeyframe { - cursor: pointer; + + .antimodeMenu-button { + text-align: center; + display: block; + } + + .color-previewI { + width: 80%; + height: 20%; + bottom: 0; position: absolute; + } + + .color-previewII { + width: 80%; + height: 80%; + margin-left: 10%; + } + + .btn-group { + display: grid; + grid-template-columns: auto auto auto auto; + margin: auto; + /* Make the buttons appear below each other */ + } + + .btn-draw { + display: inline-flex; + margin: auto; + /* Make the buttons appear below each other */ + } + + .fwdKeyframe, + .numKeyframe, + .backKeyframe { + cursor: pointer; + position: relative; width: 20; height: 30; bottom: 0; - background: gray; - display: flex; + background: #323232; + display: inline-flex; align-items: center; - color:white; + color: white; } + .backKeyframe { - left:0; svg { - display:block; - margin:auto; + display: block; + margin: auto; } } + + .numKeyframe { - left:20; - display: flex; flex-direction: column; - padding: 5px; + padding-top: 5px; } + .fwdKeyframe { - left:40; svg { - display:block; - margin:auto; + display: block; + margin: auto; } + + border-right: solid gray 1px; } } @@ -297,14 +404,14 @@ .collectionSchemaViewChrome-toggler { width: 100px; - height: 41px; + height: 35px; background-color: black; position: relative; } .collectionSchemaViewChrome-togglerButton { width: 47px; - height: 35px; + height: 30px; background-color: $light-color-secondary; // position: absolute; transition: all 0.5s ease; @@ -334,8 +441,9 @@ flex-direction: column; height: 40px; } + .commandEntry-inputArea { - display:flex; + display: flex; flex-direction: row; width: 150px; margin: auto auto auto auto; diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx new file mode 100644 index 000000000..53d2a136e --- /dev/null +++ b/src/client/views/collections/CollectionMenu.tsx @@ -0,0 +1,1081 @@ +import React = require("react"); +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; +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 { 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 { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { DragManager } from "../../util/DragManager"; +import { SelectionManager } from "../../util/SelectionManager"; +import { undoBatch } from "../../util/UndoManager"; +import AntimodeMenu from "../AntimodeMenu"; +import { EditableView } from "../EditableView"; +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 "./CollectionMenu.scss"; +import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView"; + +@observer +export default class CollectionMenu extends AntimodeMenu { + static Instance: CollectionMenu; + + @observable SelectedCollection: DocumentView | undefined; + @observable FieldKey: string; + + constructor(props: Readonly<{}>) { + super(props); + this.FieldKey = ""; + CollectionMenu.Instance = this; + this._canFade = false; // don't let the inking menu fade away + this.Pinned = Cast(Doc.UserDoc()["menuCollections-pinned"], "boolean", true); + this.jumpTo(300, 300); + } + + componentDidMount() { + reaction(() => SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0], + (doc) => doc && this.SetSelection(doc)); + } + + @action + SetSelection(view: DocumentView) { + this.SelectedCollection = view; + } + + @action + toggleMenuPin = (e: React.MouseEvent) => { + Doc.UserDoc()["menuCollections-pinned"] = this.Pinned = !this.Pinned; + if (!this.Pinned && this._left < 0) { + this.jumpTo(300, 300); + } + } + + @action + toggleProperties = () => { + if (CurrentUserUtils.propertiesWidth > 0) { + CurrentUserUtils.propertiesWidth = 0; + } else { + CurrentUserUtils.propertiesWidth = 250; + } + } + + render() { + const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom"> + <button className="antimodeMenu-button" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> + <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> + </button> + </Tooltip>; + + const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left"; + const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Properties Panel" : "Open Properties Panel"; + + const prop = <Tooltip title={<div className="dash-tooltip">{propTitle}</div>} key="properties" placement="bottom"> + <button className="antimodeMenu-button" key="properties" onPointerDown={this.toggleProperties}> + <FontAwesomeIcon icon={propIcon} size="lg" /> + </button> + </Tooltip>; + + return this.getElement(!this.SelectedCollection ? [button] : + [<CollectionViewBaseChrome key="chrome" + docView={this.SelectedCollection} + fieldKey={Doc.LayoutFieldKey(this.SelectedCollection?.props.Document)} + type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, + prop, + button]); + } +} + +interface CollectionMenuProps { + type: CollectionViewType; + fieldKey: string; + docView: DocumentView; +} + +const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); + +@observer +export class CollectionViewBaseChrome extends React.Component<CollectionMenuProps> { + //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) + + get document() { return this.props.docView?.props.Document; } + get target() { return this.document; } + _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]))), + initialize: emptyFunction, + }; + _narrativeCommand = { + params: ["target", "source"], title: "child click view", + script: "self.target.childClickedOpenTemplateView = getDocTemplate(self.source?.[0])", + immediate: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))), + initialize: emptyFunction, + }; + _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), + initialize: emptyFunction, + }; + _onClickCommand = { + params: ["target", "proxy"], title: "copy onClick", + script: `{ if (self.proxy?.[0]) { + getProto(self.proxy[0]).onClick = copyField(self.target.onClick); + getProto(self.proxy[0]).target = self.target.target; + getProto(self.proxy[0]).source = copyField(self.target.source); + }}`, + immediate: undoBatch((source: Doc[]) => { }), + initialize: emptyFunction, + }; + _openLinkInCommand = { + params: ["target", "container"], title: "link follow target", + script: `{ if (self.container?.length) { + getProto(self.target).linkContainer = self.container[0]; + getProto(self.target).isLinkButton = true; + getProto(self.target).onClick = makeScript("getProto(self.linkContainer).data = new List([self.links[0]?.anchor2])"); + }}`, + immediate: undoBatch((container: Doc[]) => { + if (container.length) { + Doc.GetProto(this.target).linkContainer = container[0]; + Doc.GetProto(this.target).isLinkButton = true; + Doc.GetProto(this.target).onClick = ScriptField.MakeScript("getProto(self.linkContainer).data = new List([self.links[0]?.anchor2])"); + } + }), + initialize: emptyFunction, + }; + _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; }, + }; + _clusterCommand = { + params: ["target"], title: "fit content", + script: "self.target._fitToBox = !self.target._fitToBox;", + immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox), + initialize: emptyFunction + }; + _fitContentCommand = { + params: ["target"], title: "toggle clusters", + 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) : ""; }, + }; + + @computed get _freeform_commands() { return Doc.UserDoc().noviceMode ? [this._viewCommand] : [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]; } + @computed get _doc_commands() { return Doc.UserDoc().noviceMode ? undefined : [this._openLinkInCommand, this._onClickCommand]; } + @computed get _tree_commands() { return undefined; } + private get _buttonizableCommands() { + switch (this.props.type) { + default: return this._doc_commands; + case CollectionViewType.Freeform: return this._freeform_commands; + case CollectionViewType.Tree: return this._tree_commands; + case CollectionViewType.Schema: return this._schema_commands; + case CollectionViewType.Stacking: return this._stacking_commands; + case CollectionViewType.Masonry: return this._stacking_commands; + case CollectionViewType.Time: return this._freeform_commands; + case CollectionViewType.Carousel: return this._freeform_commands; + case CollectionViewType.Carousel3D: return this._freeform_commands; + } + } + private _picker: any; + private _commandRef = React.createRef<HTMLInputElement>(); + private _viewRef = React.createRef<HTMLInputElement>(); + @observable private _currentKey: string = ""; + + componentDidMount = action(() => { + this._currentKey = this._currentKey || (this._buttonizableCommands?.length ? this._buttonizableCommands[0]?.title : ""); + }); + + @undoBatch + viewChanged = (e: React.ChangeEvent) => { + //@ts-ignore + this.document._viewType = e.target.selectedOptions[0].value; + } + + commandChanged = (e: React.ChangeEvent) => { + //@ts-ignore + 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) { + default: return this.otherSubChrome; + case CollectionViewType.Invalid: + case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} />); + case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" {...this.props} />); + case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" {...this.props} />); + case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" {...this.props} />); + case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" {...this.props} />); + case CollectionViewType.Carousel3D: return (<Collection3DCarouselViewChrome key="collchrome" {...this.props} />); + case CollectionViewType.Grid: return (<CollectionGridViewChrome key="collchrome" {...this.props} />); + case CollectionViewType.Docking: return (<CollectionDockingChrome key="collchrome" {...this.props} />); + } + } + + @computed get otherSubChrome() { + const docType = this.props.docView.Document.type; + switch (docType) { + default: return (null); + case DocumentType.IMG: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.PDF: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.INK: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.WEB: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.VID: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={false} isDoc={true} />); + case DocumentType.RTF: return (<CollectionFreeFormViewChrome key="collchrome" {...this.props} isOverlay={this.props.type === CollectionViewType.Invalid} isDoc={true} />); + } + } + + + private dropDisposer?: DragManager.DragDropDisposer; + protected createDropTarget = (ele: HTMLDivElement) => { + this.dropDisposer?.(); + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.document); + } + } + + @undoBatch + @action + protected drop(e: Event, de: DragManager.DropEvent): boolean { + const docDragData = de.complete.docDragData; + if (docDragData?.draggedDocuments.length) { + this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); + e.stopPropagation(); + } + return true; + } + + dragViewDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, (e, down, delta) => { + const vtype = this.props.type; + const c = { + params: ["target"], title: vtype, + script: `this.target._viewType = '${StrCast(this.props.type)}'`, + immediate: (source: Doc[]) => this.document._viewType = Doc.getDocTemplate(source?.[0]), + initialize: emptyFunction, + }; + DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), + { target: this.document }, c.params, c.initialize, e.clientX, e.clientY); + return true; + }, emptyFunction, emptyFunction); + } + dragCommandDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, (e, down, delta) => { + this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => + DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, + { target: this.document }, c.params, c.initialize, e.clientX, e.clientY)); + return true; + }, emptyFunction, () => { + this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate([])); + }); + } + + @computed get templateChrome() { + return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} > + <Tooltip title={<div className="dash-tooltip">drop document to apply or drag to create button</div>} placement="bottom"> + <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}> + <button className={"antimodeMenu-button"} > + <FontAwesomeIcon icon="bullseye" size="lg" /> + </button> + <select + className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey}> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> + {this._buttonizableCommands?.map(cmd => + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> + )} + </select> + </div> + </Tooltip> + </div>; + } + + @computed get viewModes() { + 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}> + <button className={"antimodeMenu-button"}> + <FontAwesomeIcon icon="bullseye" size="lg" /> + </button> + <select + className="collectionViewBaseChrome-viewPicker" + onPointerDown={stopPropagation} + onChange={this.viewChanged} + value={StrCast(this.props.type)}> + {Object.values(CollectionViewType).map(type => [CollectionViewType.Invalid, CollectionViewType.Docking].includes(type) ? (null) : ( + <option + key={Utils.GenerateGuid()} + className="collectionViewBaseChrome-viewOption" + onPointerDown={stopPropagation} + value={type}> + {type[0].toUpperCase() + type.substring(1)} + </option> + ))} + </select> + </div> + </Tooltip> + </div>; + } + + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get notACollection() { + if (this.selectedDoc) { + const layoutField = Doc.LayoutField(this.selectedDoc); + return this.props.type === CollectionViewType.Docking || + typeof (layoutField) === "string" && !layoutField?.includes("CollectionView"); + } + else return false; + } + + render() { + return ( + <div className="collectionMenu-cont" > + <div className="collectionMenu"> + <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" + style={{ backgroundColor: this.props.docView.layoutDoc.z ? "121212" : undefined, borderRight: "1px solid gray" }} + onClick={() => DocumentView.FloatDoc(this.props.docView)}> + <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> + </button> + </Tooltip>} + </div> + {this.subChrome} + </div> + </div> + ); + } +} + +@observer +export class CollectionDockingChrome extends React.Component<CollectionMenuProps> { + render() { + return (null); + } +} + +@observer +export class CollectionFreeFormViewChrome extends React.Component<CollectionMenuProps & { isOverlay: boolean, isDoc?: boolean }> { + public static Instance: CollectionFreeFormViewChrome; + constructor(props: any) { + super(props); + CollectionFreeFormViewChrome.Instance = this; + } + get document() { return this.props.docView.props.Document; } + @computed get dataField() { + return this.document[Doc.LayoutFieldKey(this.document)]; + } + @computed get childDocs() { + return DocListCast(this.dataField); + } + + @computed get selectedDocumentView() { + if (SelectionManager.SelectedDocuments().length) { + return SelectionManager.SelectedDocuments()[0]; + } else { return undefined; } + } + @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get isText() { + if (this.selectedDoc) { + return this.selectedDoc[Doc.LayoutFieldKey(this.selectedDoc)] instanceof RichTextField; + } + else return false; + } + + @undoBatch + @action + nextKeyframe = (): void => { + const currentFrame = Cast(this.document.currentFrame, "number", null); + if (currentFrame === undefined) { + 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)); + } + @undoBatch + @action + prevKeyframe = (): void => { + const currentFrame = Cast(this.document.currentFrame, "number", null); + if (currentFrame === undefined) { + 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; + } + + private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""]; + private _width = ["1", "5", "10", "100"]; + private _dotsize = [10, 20, 30, 40]; + private _draw = ["∿", "⎯", "→", "↔︎", "ãƒ", "O"]; + private _head = ["", "", "", "arrow", "", ""]; + private _end = ["", "", "arrow", "arrow", "", ""]; + private _shape = ["", "line", "line", "line", "rectangle", "circle"]; + private _title = ["pen", "line", "line with arrow", "line with double arrows", "square", "circle",]; + private _faName = ["pen-fancy", "minus", "long-arrow-alt-right", "arrows-alt-h", "square", "circle"]; + @observable _shapesNum = this._shape.length; + @observable _selected = this._shapesNum; + + @observable _keepMode = false; + + @observable _colorBtn = false; + @observable _widthBtn = false; + @observable _fillBtn = false; + + @action + clearKeep() { this._selected = this._shapesNum; } + + @action + changeColor = (color: string, type: string) => { + const col: ColorState = { + hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" }, + rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "", + }; + if (type === "color") { + SetActiveInkColor(Utils.colorString(col)); + } else if (type === "fill") { + SetActiveFillColor(Utils.colorString(col)); + } + } + + @action + editProperties = (value: any, field: string) => { + SelectionManager.SelectedDocuments().forEach(action((element: DocumentView) => { + const doc = Document(element.rootDoc); + if (doc.type === DocumentType.INK) { + switch (field) { + case "width": doc.strokeWidth = Number(value); break; + case "color": doc.color = String(value); break; + case "fill": doc.fillColor = String(value); break; + case "dash": doc.strokeDash = value; + } + } + })); + } + + @computed get drawButtons() { + const func = action((i: number, keep: boolean) => { + this._keepMode = keep; + if (this._selected !== i) { + this._selected = i; + Doc.SetSelectedTool(InkTool.Pen); + SetActiveArrowStart(this._head[i]); + SetActiveArrowEnd(this._end[i]); + SetActiveBezierApprox("300"); + + GestureOverlay.Instance.InkShape = this._shape[i]; + } else { + this._selected = this._shapesNum; + Doc.SetSelectedTool(InkTool.None); + SetActiveArrowStart(""); + SetActiveArrowEnd(""); + GestureOverlay.Instance.InkShape = ""; + SetActiveBezierApprox("0"); + } + }); + return <div className="btn-draw" key="draw"> + {this._draw.map((icon, i) => + <Tooltip key={icon} title={<div className="dash-tooltip">{this._title[i]}</div>} placement="bottom"> + <button className="antimodeMenu-button" onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)} + style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}> + <FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" /> + </button> + </Tooltip>)} + </div>; + } + + toggleButton = (key: string, value: boolean, setter: () => {}, icon: FontAwesomeIconProps["icon"], ele: JSX.Element | null) => { + return <Tooltip title={<div className="dash-tooltip">{key}</div>} placement="bottom"> + <button className="antimodeMenu-button" key={key} + onPointerDown={action(e => setter())} + style={{ backgroundColor: value ? "121212" : "" }}> + <FontAwesomeIcon icon={icon} size="lg" /> + {ele} + </button> + </Tooltip>; + } + + @computed get widthPicker() { + const widthPicker = this.toggleButton("stroke width", this._widthBtn, () => this._widthBtn = !this._widthBtn, "bars", null); + return !this._widthBtn ? widthPicker : + <div className="btn2-group" key="width"> + {widthPicker} + {this._width.map((wid, i) => + <Tooltip title={<div className="dash-tooltip">change width</div>} placement="bottom"> + <button className="antimodeMenu-button" key={wid} + onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} + style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001, fontSize: this._dotsize[i], padding: 0, textAlign: "center" }}> + • + </button> + </Tooltip>)} + </div>; + } + + @computed get colorPicker() { + const colorPicker = this.toggleButton("stroke color", this._colorBtn, () => this._colorBtn = !this._colorBtn, "pen-nib", + <div className="color-previewI" style={{ backgroundColor: ActiveInkColor() ?? "121212" }} />); + return !this._colorBtn ? colorPicker : + <div className="btn-group" key="color"> + {colorPicker} + {this._palette.map(color => + <button className="antimodeMenu-button" key={color} + onPointerDown={action(() => { this.changeColor(color, "color"); this._colorBtn = false; this.editProperties(color, "color"); })} + 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> : ""} + + </button>)} + </div>; + } + @computed get fillPicker() { + const fillPicker = this.toggleButton("shape fill color", this._fillBtn, () => this._fillBtn = !this._fillBtn, "fill-drip", + <div className="color-previewI" style={{ backgroundColor: ActiveFillColor() ?? "121212" }} />); + return !this._fillBtn ? fillPicker : + <div className="btn-group" key="fill" > + {fillPicker} + {this._palette.map(color => + <button className="antimodeMenu-button" key={color} + 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> : ""} + </div> + </button>)} + + </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"> + <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"> + <div className="numKeyframe" style={{ backgroundColor: this.document.editing ? "#759c75" : "#c56565" }} + onClick={action(() => this.document.editing = !this.document.editing)} > + {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"> + <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.isText ? + <> + {this.drawButtons} + {this.widthPicker} + {this.colorPicker} + {this.fillPicker} + </> : + (null) + } + {this.isText ? <RichTextMenu key="rich" /> : null} + </div>; + } +} +@observer +export class CollectionStackingViewChrome extends React.Component<CollectionMenuProps> { + @observable private _currentKey: string = ""; + @observable private suggestions: string[] = []; + + get document() { return this.props.docView.props.Document; } + + @computed private get descending() { return StrCast(this.document._columnsSort) === "descending"; } + @computed get pivotField() { return StrCast(this.document._pivotField); } + + getKeySuggestions = async (value: string): Promise<string[]> => { + value = value.toLowerCase(); + const docs = DocListCast(this.document[this.props.fieldKey]); + + if (Doc.UserDoc().noviceMode) { + 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] !== "_")); + return keys.filter(key => key.toLowerCase().indexOf(value.toLowerCase()) > -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] !== "_")); + return noviceKeys.filter(key => key.toLowerCase().indexOf(value.toLowerCase()) > -1); + } + } + + if (docs instanceof Doc) { + return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); + } else { + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); + } + } + + @action + onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { + this._currentKey = newValue; + } + + getSuggestionValue = (suggestion: string) => suggestion; + + renderSuggestion = (suggestion: string) => { + return <p>{suggestion}</p>; + } + + onSuggestionFetch = async ({ value }: { value: string }) => { + const sugg = await this.getKeySuggestions(value); + runInAction(() => { + this.suggestions = sugg; + }); + } + + @action + onSuggestionClear = () => { + this.suggestions = []; + } + + @action + setValue = (value: string) => { + this.document._pivotField = value; + return true; + } + + @action toggleSort = () => { + this.document._columnsSort = + this.document._columnsSort === "descending" ? "ascending" : + this.document._columnsSort === "ascending" ? undefined : "descending"; + } + @action resetValue = () => { this._currentKey = this.pivotField; }; + + render() { + return ( + <div className="collectionStackingViewChrome-cont"> + <div className="collectionStackingViewChrome-pivotField-cont"> + <div className="collectionStackingViewChrome-pivotField-label"> + GROUP BY: + </div> + <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}> + <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> + </div> + <div className="collectionStackingViewChrome-pivotField"> + <EditableView + GetValue={() => this.pivotField} + autosuggestProps={ + { + resetValue: this.resetValue, + value: this._currentKey, + onChange: this.onKeyChange, + autosuggestProps: { + inputProps: + { + value: this._currentKey, + onChange: this.onKeyChange + }, + getSuggestionValue: this.getSuggestionValue, + suggestions: this.suggestions, + alwaysRenderSuggestions: true, + renderSuggestion: this.renderSuggestion, + onSuggestionsFetchRequested: this.onSuggestionFetch, + onSuggestionsClearRequested: this.onSuggestionClear + } + }} + oneLine + SetValue={this.setValue} + contents={this.pivotField ? this.pivotField : "N/A"} + /> + </div> + </div> + </div> + ); + } +} + + +@observer +export class CollectionSchemaViewChrome extends React.Component<CollectionMenuProps> { + // private _textwrapAllRows: boolean = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0; + get document() { return this.props.docView.props.Document; } + + @undoBatch + togglePreview = () => { + const dividerWidth = 4; + const borderWidth = Number(COLLECTION_BORDER_WIDTH); + const panelWidth = this.props.docView.props.PanelWidth(); + const previewWidth = NumCast(this.document.schemaPreviewWidth); + const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth; + this.document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0; + } + + @undoBatch + @action + toggleTextwrap = async () => { + const textwrappedRows = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []); + if (textwrappedRows.length) { + this.document.textwrappedSchemaRows = new List<string>([]); + } else { + const docs = DocListCast(this.document[this.props.fieldKey]); + const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); + this.document.textwrappedSchemaRows = new List<string>(allRows); + } + } + + + render() { + const previewWidth = NumCast(this.document.schemaPreviewWidth); + const textWrapped = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0; + + return ( + <div className="collectionSchemaViewChrome-cont"> + <div className="collectionSchemaViewChrome-toggle"> + <div className="collectionSchemaViewChrome-label">Show Preview: </div> + <div className="collectionSchemaViewChrome-toggler" onClick={this.togglePreview}> + <div className={"collectionSchemaViewChrome-togglerButton" + (previewWidth !== 0 ? " on" : " off")}> + {previewWidth !== 0 ? "on" : "off"} + </div> + </div> + </div> + </div > + ); + } +} + +@observer +export class CollectionTreeViewChrome extends React.Component<CollectionMenuProps> { + + get document() { return this.props.docView.props.Document; } + get sortAscending() { + return this.document[this.props.fieldKey + "-sortAscending"]; + } + set sortAscending(value) { + this.document[this.props.fieldKey + "-sortAscending"] = value; + } + @computed private get ascending() { + return Cast(this.sortAscending, "boolean", null); + } + + @action toggleSort = () => { + if (this.sortAscending) this.sortAscending = undefined; + else if (this.sortAscending === undefined) this.sortAscending = false; + else this.sortAscending = true; + } + + render() { + return ( + <div className="collectionTreeViewChrome-cont"> + <button className="collectionTreeViewChrome-sort" onClick={this.toggleSort}> + <div className="collectionTreeViewChrome-sortLabel"> + Sort + </div> + <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.ascending === undefined ? "90" : this.ascending ? "180" : "0"}deg)` }}> + <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> + </div> + </button> + </div> + ); + } +} + +// Enter scroll speed for 3D Carousel +@observer +export class Collection3DCarouselViewChrome extends React.Component<CollectionMenuProps> { + get document() { return this.props.docView.props.Document; } + @computed get scrollSpeed() { + return this.document._autoScrollSpeed; + } + + @action + setValue = (value: string) => { + const numValue = Number(StrCast(value)); + if (numValue > 0) { + this.document._autoScrollSpeed = numValue; + return true; + } + return false; + } + + render() { + return ( + <div className="collection3DCarouselViewChrome-cont"> + <div className="collection3DCarouselViewChrome-scrollSpeed-cont"> + <div className="collectionStackingViewChrome-scrollSpeed-label"> + AUTOSCROLL SPEED: + </div> + <div className="collection3DCarouselViewChrome-scrollSpeed"> + <EditableView + GetValue={() => StrCast(this.scrollSpeed)} + oneLine + SetValue={this.setValue} + contents={this.scrollSpeed ? this.scrollSpeed : 1000} /> + </div> + </div> + </div> + ); + } +} + +/** + * Chrome for grid view. + */ +@observer +export class CollectionGridViewChrome extends React.Component<CollectionMenuProps> { + + private clicked: boolean = false; + private entered: boolean = false; + private decrementLimitReached: boolean = false; + @observable private resize = false; + private resizeListenerDisposer: Opt<Lambda>; + get document() { return this.props.docView.props.Document; } + + componentDidMount() { + + runInAction(() => this.resize = this.props.docView.props.PanelWidth() < 700); + + // listener to reduce text on chrome resize (panel resize) + this.resizeListenerDisposer = computed(() => this.props.docView.props.PanelWidth()).observe(({ newValue }) => { + runInAction(() => this.resize = newValue < 700); + }); + } + + componentWillUnmount() { + this.resizeListenerDisposer?.(); + } + + 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 `rowHeight` on the grid's Document to the value entered. + */ + // @undoBatch + // onRowHeightEnter = (e: React.KeyboardEvent<HTMLInputElement>) => { + // if (e.key === "Enter" || e.key === "Tab") { + // if (e.currentTarget.valueAsNumber > 0 && this.document.rowHeight as number !== e.currentTarget.valueAsNumber) { + // this.document.rowHeight = e.currentTarget.valueAsNumber; + // } + // } + // } + + /** + * Sets whether the grid is flexible or not on the grid's Document. + */ + @undoBatch + toggleFlex = () => { + this.document.gridFlex = !BoolCast(this.document.gridFlex, true); + } + + /** + * Increments the value of numCols on button click + */ + onIncrementButtonClick = () => { + this.clicked = true; + this.entered && (this.document.gridNumCols as number)--; + undoBatch(() => this.document.gridNumCols = this.numCols + 1)(); + this.entered = false; + } + + /** + * Decrements the value of numCols on button click + */ + onDecrementButtonClick = () => { + this.clicked = true; + if (!this.decrementLimitReached) { + this.entered && (this.document.gridNumCols as number)++; + undoBatch(() => this.document.gridNumCols = this.numCols - 1)(); + } + this.entered = false; + } + + /** + * Increments the value of numCols on button hover + */ + incrementValue = () => { + this.entered = true; + if (!this.clicked && !this.decrementLimitReached) { + this.document.gridNumCols = this.numCols + 1; + } + this.decrementLimitReached = false; + this.clicked = false; + } + + /** + * Decrements the value of numCols on button hover + */ + decrementValue = () => { + this.entered = true; + if (!this.clicked) { + if (this.numCols !== 1) { + this.document.gridNumCols = this.numCols - 1; + } + else { + this.decrementLimitReached = true; + } + } + + this.clicked = false; + } + + /** + * Toggles the value of preventCollision + */ + toggleCollisions = () => { + this.document.gridPreventCollision = !this.document.gridPreventCollision; + } + + /** + * Changes the value of the compactType + */ + changeCompactType = (e: React.ChangeEvent<HTMLSelectElement>) => { + // need to change startCompaction so that this operation will be undoable. + this.document.gridStartCompaction = e.target.selectedOptions[0].value; + } + + render() { + return ( + <div className="collectionGridViewChrome-cont" > + <span className="grid-control" style={{ width: this.resize ? "25%" : "30%" }}> + <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="columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" /> + <input className="columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" /> + </span> + {/* <span className="grid-control"> + <span className="grid-icon"> + <FontAwesomeIcon icon="text-height" size="1x" /> + </span> + <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.document.rowHeight as string} onKeyDown={this.onRowHeightEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} /> + </span> */} + <span className="grid-control" style={{ width: this.resize ? "12%" : "20%" }}> + <input type="checkbox" style={{ marginRight: 5 }} onChange={this.toggleCollisions} checked={!this.document.gridPreventCollision} /> + <label className="flexLabel">{this.resize ? "Coll" : "Collisions"}</label> + </span> + + <select className="collectionGridViewChrome-viewPicker" + style={{ marginRight: 5 }} + onPointerDown={stopPropagation} + onChange={this.changeCompactType} + value={StrCast(this.document.gridStartCompaction, StrCast(this.document.gridCompaction))}> + {["vertical", "horizontal", "none"].map(type => + <option className="collectionGridViewChrome-viewOption" + onPointerDown={stopPropagation} + value={type}> + {this.resize ? type[0].toUpperCase() + type.substring(1) : "Compact: " + type} + </option> + )} + </select> + + <span className="grid-control" style={{ width: this.resize ? "12%" : "20%" }}> + <input style={{ marginRight: 5 }} type="checkbox" onChange={this.toggleFlex} + checked={BoolCast(this.document.gridFlex, true)} /> + <label className="flexLabel">{this.resize ? "Flex" : "Flexible"}</label> + </span> + + <button onClick={() => this.document.gridResetLayout = true}> + {!this.resize ? "Reset" : + <FontAwesomeIcon icon="redo-alt" size="1x" />} + </button> + + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index fc48e0327..2e4055256 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -12,6 +12,7 @@ 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) { @@ -38,19 +39,27 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { @computed get contents() { return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" ? undefined : "none" }} > - <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} /> + <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} + addDocument={(doc: Doc | Doc[]) => { + (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d)); + return this.props.addDocument(doc); + }} + moveDocument={(doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { + (doc instanceof Doc ? [doc] : doc).map((d) => Doc.deiconifyView(d)); + return this.props.moveDocument(doc, targetCollection, addDoc); + }} /> </div>; } toggleStarburst = action(() => { if (this.layoutEngine() === 'starburst') { const defaultSize = 110; this.layoutDoc._overflow = undefined; - this.childDocs.forEach(d => Doc.iconify(d)); + this.childDocs.forEach(d => DocUtils.iconify(d)); this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2; this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2; this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize); this.layoutDoc._height = NumCast(this.layoutDoc._starburstPileHeight, defaultSize); - Doc.pileup(this.childDocs); + DocUtils.pileup(this.childDocs); this.layoutDoc._panX = 0; this.layoutDoc._panY = -10; this.props.Document._pileLayoutEngine = 'pass'; @@ -71,24 +80,13 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { } }); - @undoBatch - @action - onInternalDrop = (e: Event, de: DragManager.DropEvent) => { - if (super.onInternalDrop(e, de)) { - if (de.complete.docDragData) { - Doc.pileup(this.childDocs); - } - } - return true; - } - _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 && this.props.isSelected(true)) { + if (this.layoutEngine() === "pass" && this.childDocs.length && e.shiftKey) { dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]); if (dist > 100) { if (!this._undoBatch) { @@ -109,11 +107,11 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { if (!this.childDocs.length) { this.props.ContainingCollectionView?.removeDocument(this.props.Document); } - }, emptyFunction, false, this.layoutEngine() === "pass" && this.props.isSelected(true)); // this sets _doubleTap + }, emptyFunction, e.shiftKey && this.layoutEngine() === "pass", this.layoutEngine() === "pass" && e.shiftKey); // this sets _doubleTap } onClick = (e: React.MouseEvent) => { - if (e.button === 0 && this._doubleTap) { + if (e.button === 0) {//} && this._doubleTap) { SelectionManager.DeselectAll(); this.toggleStarburst(); e.stopPropagation(); @@ -123,7 +121,6 @@ export class CollectionPileView extends CollectionSubView(doc => doc) { render() { return <div className={"collectionPileView"} onClick={this.onClick} onPointerDown={this.pointerDown} - ref={this.createDashEventsTarget} 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 62aed67ed..d11d6a5ba 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -1,28 +1,39 @@ import React = require("react"); -import { action, observable } from "mobx"; +import { action, observable, trace, computed } from "mobx"; import { observer } from "mobx-react"; import { CellInfo } from "react-table"; import "react-table/react-table.css"; -import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils"; +import { emptyFunction, returnFalse, returnZero, returnOne, returnEmptyFilter, Utils, emptyPath } from "../../../Utils"; 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 } from '../globalCssVariables.scss'; +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 } from "./CollectionView"; -import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../fields/Types"; +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 { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; import { SnappingManager } from "../../util/SnappingManager"; +import { ComputedField } from "../../../fields/ScriptField"; +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"; +const path = require('path'); library.add(faExpand); @@ -46,6 +57,7 @@ export interface CellProps { setPreviewDoc: (doc: Doc) => void; setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean; getField: (row: number, col?: number) => void; + showDoc: (doc: Doc | undefined, dataDoc?: any, screenX?: number, screenY?: number) => void; } @observer @@ -53,11 +65,10 @@ export class CollectionSchemaCell extends React.Component<CellProps> { @observable protected _isEditing: boolean = false; protected _focusRef = React.createRef<HTMLDivElement>(); protected _document = this.props.rowProps.original; - private _dropDisposer?: DragManager.DragDropDisposer; + protected _dropDisposer?: DragManager.DragDropDisposer; componentDidMount() { document.addEventListener("keydown", this.onKeyDown); - } componentWillUnmount() { @@ -70,7 +81,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { document.removeEventListener("keydown", this.onKeyDown); this._isEditing = true; this.props.setIsEditing(true); - } } @@ -85,6 +95,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { @action onPointerDown = async (e: React.PointerEvent): Promise<void> => { + this.props.changeFocusedCellByIndex(this.props.row, this.props.col); this.props.setPreviewDoc(this.props.rowProps.original); @@ -130,7 +141,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } } - private dropRef = (ele: HTMLElement | null) => { + protected dropRef = (ele: HTMLElement | null) => { this._dropDisposer && this._dropDisposer(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)); @@ -141,7 +152,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // let field = this.props.rowProps.original[this.props.rowProps.column.id as string]; // let doc = FieldValue(Cast(field, Doc)); - // console.log("Expanding doc", StrCast(doc!.title)); // this.props.setPreviewDoc(doc!); // // this.props.changeFocusedCellByIndex(this.props.row, this.props.col); @@ -160,6 +170,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> { 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, @@ -183,7 +194,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const fieldIsDoc = (type === "document" && typeof field === "object") || (typeof field === "object" && doc); const onItemDown = (e: React.PointerEvent) => { - fieldIsDoc && SetupDrag(this._focusRef, + //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); @@ -206,6 +218,18 @@ export class CollectionSchemaCell extends React.Component<CellProps> { 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 className = "collectionSchemaView-cellWrapper"; if (this._isEditing) className += " editing"; @@ -217,37 +241,110 @@ export class CollectionSchemaCell extends React.Component<CellProps> { // <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} > // <FontAwesomeIcon icon="expand" size="sm" /> // </div> - // ); - + // ); + const positions = []; + if (StrCast(this.props.Document._searchString).toLowerCase() !== "") { + const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); + let term = ""; + if (cfield !== undefined) { + if (cfield.Text !== undefined) { + term = cfield.Text; + } + else if (StrCast(cfield)) { + term = StrCast(cfield); + } + else { + term = String(NumCast(cfield)); + } + } + term = term.toLowerCase(); + const search = StrCast(this.props.Document._searchString).toLowerCase(); + let start = term.indexOf(search); + let tally = 0; + if (start !== -1) { + positions.push(start); + } + while (start < contents.length && start !== -1) { + term = term.slice(start + search.length + 1); + tally += start + search.length + 1; + start = term.indexOf(search); + positions.push(tally + start); + } + if (positions.length > 1) { + positions.pop(); + } + } return ( - <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}> + <div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "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]}> <EditableView + positions={positions.length > 0 ? positions : undefined} + search={StrCast(this.props.Document._searchString) ? StrCast(this.props.Document._searchString) : undefined} editing={this._isEditing} isEditingCallback={this.isEditingCallback} display={"inline"} - contents={contents} + contents={contents ? contents : type === "number" ? "0" : "undefined"} + highlight={positions.length > 0 ? true : undefined} + //contents={StrCast(contents)} height={"auto"} maxHeight={Number(MAX_ROW_HEIGHT)} + placeholder={"enter value"} + bing={() => { + const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); + if (cfield !== undefined) { + console.log(typeof (cfield)); + // if (typeof(cfield)===RichTextField) + const a = cfield as RichTextField; + if (a.Text !== undefined) { + return (a.Text); + } + else if (StrCast(cfield)) { + return StrCast(cfield); + } + else { + return String(NumCast(cfield)); + } + } + }} GetValue={() => { - const field = props.Document[props.fieldKey]; - if (Field.IsField(field)) { - return Field.toScriptString(field); + 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; + } - return ""; - } - } - SetValue={(value: string) => { + + }} + SetValue={action((value: string) => { + let retVal = false; + if (value.startsWith(":=")) { - return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col); + retVal = this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, 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 script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - if (!script.compiled) { - return false; + 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 this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); - }} + 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) { @@ -258,6 +355,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } }} /> + + </div > {/* {fieldIsDoc ? docExpander : null} */} </div> @@ -292,12 +391,464 @@ export class CollectionSchemaStringCell extends CollectionSchemaCell { } @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(); + + @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; + //} + } + + render() { + return <DatePicker + selected={this._date} + onSelect={date => this.handleChange(date)} + onChange={date => this.handleChange(date)} + />; + } +} + +@observer 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(); } + + @action + onSetValue = (value: string) => { + this._docTitle = value; + //this.prop.Document[this.prop.fieldKey] = this._text; + + const script = CompileScript(value, { + addReturn: true, + typecheck: false, + transformer: DocumentIconContainer.getTransformer() + }); + + 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; + + return true; + } + return false; + } + + onFocus = () => { + this._overlayDisposer?.(); + 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, ""); + document.removeEventListener("keydown", this.onKeyDown); + isEditing && document.addEventListener("keydown", this.onKeyDown); + this._isEditing = isEditing; + this.props.setIsEditing(isEditing); + this.props.changeFocusedCellByIndex(this.props.row, this.props.col); + } + + 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={(e) => { this.onDown(e); }} + onPointerEnter={(e) => { this.showPreview(true, e); }} + onPointerLeave={(e) => { this.showPreview(false, e); }} + > + + <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> + </div> + ); + } else { + return this.renderCellWithType("document"); + } + } +} + +@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 + } + const ext = path.extname(url.href); + const _curSuffix = "_o"; + return url.href.replace(ext, _curSuffix + ext); + } + + 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 + }; + + 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(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"); + } + } +} + + + + + +@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>; + @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); + + } + + @action + onSelected = (element: string, index: number) => { + this._text = element; + this._selectedNum = index; + } + + onFocus = () => { + this._overlayDisposer?.(); + this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); + } + + render() { - return this.renderCellWithType("document"); + + const dragRef: React.RefObject<HTMLDivElement> = React.createRef(); + + let type = "list"; + + let link = false; + let doc = 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>; + } + }); + + 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]}> + <EditableView + editing={this._isEditing} + isEditingCallback={this.isEditingCallback} + display={"inline"} + contents={this._text} + height={"auto"} + maxHeight={Number(MAX_ROW_HEIGHT)} + GetValue={() => { + return this._text; + }} + SetValue={action((value: string) => { + + // add special for params + this.onSetValue(value); + return true; + })} + /> + </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-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> + <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div> + </div> + + {this._opened ? dropdown : null} + </div > + </div> + ); + } else { + 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; @@ -326,3 +877,69 @@ 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); + doc.searchIndex = NumCast(doc.searchIndex); + }} style={{ padding: 2, left: 77 }}> + <FontAwesomeIcon icon="arrow-up" size="sm" /> + </button> + <button onClick={() => { + { + doc.searchMatchAlt = false; + setTimeout(() => doc.searchMatchAlt = true, 0); + doc.searchIndex = NumCast(doc.searchIndex); + } + }} 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); + doc.searchIndex = NumCast(doc.searchIndex); + }}> + <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> + ); + } +} + diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx index dae0600b1..e65adcf76 100644 --- a/src/client/views/collections/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/CollectionSchemaHeaders.tsx @@ -2,18 +2,20 @@ import React = require("react"); import { action, observable } from "mobx"; import { observer } from "mobx-react"; import "./CollectionSchemaView.scss"; -import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faSortAmountDown, faSortAmountUp, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faSortAmountDown, faSortAmountUp, faTimes, faImage, faListUl, faCalendar } from '@fortawesome/free-solid-svg-icons'; import { library, IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { ColumnType } from "./CollectionSchemaView"; import { faFile } from "@fortawesome/free-regular-svg-icons"; import { SchemaHeaderField, PastelSchemaPalette } from "../../../fields/SchemaHeaderField"; import { undoBatch } from "../../util/UndoManager"; +import { Doc } from "../../../fields/Doc"; +import { StrCast } from "../../../fields/Types"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile as any, faSortAmountDown, faSortAmountUp, faTimes); +library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile as any, faSortAmountDown, faSortAmountUp, faTimes, faImage, faListUl, faCalendar); export interface HeaderProps { keyValue: SchemaHeaderField; @@ -33,7 +35,9 @@ export interface HeaderProps { 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 ? "file" : "align-justify"; + this.props.keyType === ColumnType.Boolean ? "check-square" : this.props.keyType === ColumnType.Doc ? "file" : + 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 @@ -72,6 +76,8 @@ export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHe } } + + export interface ColumnMenuProps { columnField: SchemaHeaderField; // keyValue: string; @@ -160,10 +166,22 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> <FontAwesomeIcon icon={"check-square"} size="sm" /> Checkbox </div> + <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.List)}> + <FontAwesomeIcon icon={"list-ul"} size="sm" /> + List + </div> <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc)}> <FontAwesomeIcon icon={"file"} size="sm" /> Document </div> + <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Image)}> + <FontAwesomeIcon icon={"image"} size="sm" /> + Image + </div> + <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Date)}> + <FontAwesomeIcon icon={"calendar"} size="sm" /> + Date + </div> </div> </div > ); @@ -258,17 +276,19 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> } -interface KeysDropdownProps { +export interface KeysDropdownProps { keyValue: string; possibleKeys: string[]; existingKeys: string[]; canAddNew: boolean; addNew: boolean; - onSelect: (oldKey: string, newKey: string, addnew: boolean) => void; + onSelect: (oldKey: string, newKey: string, addnew: boolean, filter?: string) => void; setIsEditing: (isEditing: boolean) => void; + width?: string; + docs?: Doc[]; } @observer -class KeysDropdown extends React.Component<KeysDropdownProps> { +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; @@ -281,10 +301,23 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { @action onSelect = (key: string): void => { - this.props.onSelect(this._key, key, this.props.addNew); - this.setKey(key); + if (key.slice(0, this._key.length) === this._key && this._key !== key) { + const filter = key.slice(this._key.length - key.length); + this.props.onSelect(this._key, this._key, this.props.addNew, filter); + } + else { + this.props.onSelect(this._key, key, this.props.addNew); + this.setKey(key); + this._isOpen = false; + this.props.setIsEditing(false); + } + } + + @action + onSelect2 = (key: string): void => { + this._searchTerm = this._searchTerm.slice(0, this._key.length) + key; this._isOpen = false; - this.props.setIsEditing(false); + } @undoBatch @@ -331,31 +364,81 @@ class KeysDropdown extends React.Component<KeysDropdownProps> { renderOptions = (): JSX.Element[] | JSX.Element => { if (!this._isOpen) return <></>; - const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1); + const searchTerm = this._searchTerm.trim() === "New field" ? "" : this._searchTerm; + + const 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 options = keyOptions.map(key => { - return <div key={key} className="key-option" onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>; + return <div key={key} className="key-option" style={{ + border: "1px solid lightgray", + width: this.props.width, maxWidth: this.props.width, overflowX: "hidden" + }} + 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 (!exactFound && this._searchTerm !== "" && this.props.canAddNew) { - options.push(<div key={""} className="key-option" - onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> - Create "{this._searchTerm}" key</div>); + if (this._key !== this._searchTerm.slice(0, this._key.length)) { + if (!exactFound && 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" + }} + onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}> + Create "{this._searchTerm}" key</div>); + } } return options; } + renderFilterOptions = (): JSX.Element[] | JSX.Element => { + if (!this._isOpen) return <></>; + const keyOptions: string[] = []; + const temp = this._searchTerm.slice(this._key.length); + this.props.docs?.forEach((doc) => { + const key = StrCast(doc[this._key]); + if (keyOptions.includes(key) === false && key.includes(temp)) { + keyOptions.push(key); + } + }); + + + const options = keyOptions.map(key => { + return <div key={key} className="key-option" style={{ + border: "1px solid lightgray", + width: this.props.width, maxWidth: this.props.width, overflowX: "hidden" + }} + onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect2(key); }}>{key}</div>; + }); + + return options; + } + + render() { return ( - <div className="keys-dropdown"> - <input className="keys-search" ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown} - onChange={e => this.onChange(e.target.value)} onFocus={this.onFocus} onBlur={this.onBlur}></input> - <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}> - {this.renderOptions()} + <div className="keys-dropdown" style={{ zIndex: 10, width: this.props.width, maxWidth: this.props.width }}> + {this._key === this._searchTerm.slice(0, this._key.length) ? + <div style={{ position: "absolute", marginLeft: "4px", marginTop: "3", color: "grey", pointerEvents: "none", lineHeight: 1.15 }}> + {this._key} + </div> + : undefined} + <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> + <div className="keys-options-wrapper" style={{ + backgroundColor: "white", + width: this.props.width, maxWidth: this.props.width, + }} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}> + {this._key === this._searchTerm.slice(0, this._key.length) ? + this.renderFilterOptions() : this.renderOptions()} </div> </div > ); diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx index 6f1e8ac1f..dade4f2f2 100644 --- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx +++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx @@ -66,8 +66,9 @@ export class MovableColumn extends React.Component<MovableColumnProps> { const rect = this._header!.current!.getBoundingClientRect(); const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top); const before = x[0] < bounds[0]; - if (de.complete.columnDragData) { - this.props.reorderColumns(de.complete.columnDragData.colKey, this.props.columnValue, before, this.props.allColumns); + const colDragData = de.complete.columnDragData; + if (colDragData) { + this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns); return true; } return false; @@ -136,6 +137,7 @@ export interface MovableRowProps { textWrapRow: (doc: Doc) => void; rowWrapped: boolean; dropAction: string; + addDocTab: any; } export class MovableRow extends React.Component<MovableRowProps> { @@ -164,13 +166,14 @@ export class MovableRow extends React.Component<MovableRowProps> { } createRowDropTarget = (ele: HTMLDivElement) => { - this._rowDropDisposer && this._rowDropDisposer(); + this._rowDropDisposer?.(); if (ele) { this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this)); } } rowDrop = (e: Event, de: DragManager.DropEvent) => { + this.onPointerLeave(e as any); const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc)); if (!rowDoc) return false; @@ -203,10 +206,15 @@ export class MovableRow extends React.Component<MovableRowProps> { @action move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => { const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection); - if (targetView && targetView.props.ContainingCollectionDoc) { - return doc !== targetCollection && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); + return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc); + } + + @action + onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + console.log("yes"); + if (e.key === "Backspace" || e.key === "Delete") { + undoBatch(() => this.props.removeDoc(this.props.rowInfo.original)); } - return doc !== targetCollection && this.props.removeDoc(doc) && addDoc(doc); } render() { @@ -227,13 +235,14 @@ export class MovableRow extends React.Component<MovableRowProps> { if (this.props.rowWrapped) className += " row-wrapped"; return ( - <div className={className} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}> - <div className="collectionSchema-row-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> - <ReactTableDefaults.TrComponent> - <div className="row-dragger"> + <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> + <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, "onRight")}><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 a24140b48..ba0a259c5 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -6,7 +6,7 @@ border-style: solid; border-radius: $border-radius; box-sizing: border-box; - position: absolute; + position: relative; top: 0; width: 100%; height: 100%; @@ -25,7 +25,6 @@ .collectionSchemaView-tableContainer { width: 100%; height: 100%; - overflow: scroll; } .collectionSchemaView-dividerDragger { @@ -59,13 +58,16 @@ } .rt-thead { - width: calc(100% - 52px); - margin-left: 50px; + width: 100%; + z-index: 100; + overflow-y: visible; &.-header { font-size: 12px; height: 30px; box-shadow: none; + z-index: 100; + overflow-y: visible; } .rt-resizable-header-content { @@ -91,7 +93,7 @@ } .rt-tbody { - width: calc(100% - 2px); + width: 100%; direction: rtl; overflow: visible; } @@ -159,40 +161,39 @@ .collectionSchema-col { height: 100%; - - .collectionSchema-col-wrapper { - &.col-before { - border-left: 2px solid red; - } - - &.col-after { - border-right: 2px solid red; - } - } } -.collectionSchemaView-header { +.collectionSchema-header-menu { height: 100%; - color: gray; + z-index: 100; + position: absolute; + background:white; - .collectionSchema-header-menu { + .collectionSchema-header-toggler { + z-index: 100; + width: 100%; height: 100%; + padding: 4px; + letter-spacing: 2px; + text-transform: uppercase; - .collectionSchema-header-toggler { - width: 100%; - height: 100%; - padding: 4px; - letter-spacing: 2px; - text-transform: uppercase; - - svg { - margin-right: 4px; - } + svg { + margin-right: 4px; } } } +.collectionSchemaView-header { + height: 100%; + color: gray; + z-index: 100; + overflow-y: visible; + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + button.add-column { width: 28px; } @@ -253,13 +254,16 @@ button.add-column { .keys-dropdown { position: relative; - width: 100%; + //width: 100%; + background-color: white; input { border: 2px solid $light-color-secondary; padding: 3px; height: 28px; font-weight: bold; + letter-spacing: "2px"; + text-transform: "uppercase"; &:focus { font-weight: normal; @@ -273,9 +277,11 @@ button.add-column { position: absolute; top: 28px; box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1); + background-color: white; .key-option { - background-color: $light-color; + //background-color: $light-color; + background-color: white; border: 1px solid lightgray; padding: 2px 3px; @@ -412,6 +418,56 @@ button.add-column { &:hover .collectionSchemaView-cellContents-docExpander { display: block; } + + + .collectionSchemaView-cellContents-document { + display: inline-block; + } + + .collectionSchemaView-cellContents-docButton { + float: right; + width: "15px"; + height: "15px"; + } + + .collectionSchemaView-dropdownWrapper { + + border: grey; + border-style: solid; + border-width: 1px; + height: 100%; + + .collectionSchemaView-dropdownButton { + + //display: inline-block; + float: left; + height: 100%; + + + } + + .collectionSchemaView-dropdownText { + display: inline-block; + //float: right; + height: 100%; + display: "flex"; + font-size: 13; + justify-content: "center"; + align-items: "center"; + } + + } + + .collectionSchemaView-dropdownContainer { + position: absolute; + border: 1px solid rgba(0, 0, 0, 0.04); + box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14); + + .collectionSchemaView-dropdownOption:hover { + background-color: rgba(0, 0, 0, 0.14); + cursor: pointer; + } + } } .collectionSchemaView-cellContents-docExpander { @@ -422,6 +478,7 @@ button.add-column { top: 0; right: 0; background-color: lightgray; + } .doc-drag-over { @@ -429,6 +486,10 @@ button.add-column { } .collectionSchemaView-toolbar { + z-index: 100; +} + +.collectionSchemaView-toolbar { height: 30px; display: flex; justify-content: flex-end; @@ -450,14 +511,21 @@ button.add-column { .collectionSchemaView-table { width: 100%; height: 100%; - overflow: visible; } .reactTable-sub { padding: 10px 30px; background-color: rgb(252, 252, 252); - width: calc(100% - 50px); - margin-left: 50px; + width: 100%; + + .rt-thead { + display:none; + } + .collectionSchemaView-table{ + border: solid 1px; + overflow: hidden; + } + .row-dragger { background-color: rgb(252, 252, 252); @@ -492,7 +560,6 @@ button.add-column { text-transform: uppercase; cursor: pointer; font-size: 10.5px; - padding: 10px; margin-left: 50px; margin-top: 10px; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 35f892d65..a003de0d3 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -4,31 +4,26 @@ import { faCog, faPlus, faSortDown, faSortUp, faTable } from '@fortawesome/free- import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, untracked } from "mobx"; import { observer } from "mobx-react"; -import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; +import Measure from "react-measure"; +import { Resize } from "react-table"; import "react-table/react-table.css"; -import { Doc, DocListCast, Field, Opt } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; +import { Doc } from "../../../fields/Doc"; 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, BoolCast } from "../../../fields/Types"; -import { Docs, DocumentOptions } from "../../documents/Documents"; -import { CompileScript, Transformer, ts } from "../../util/Scripting"; +import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; +import { Cast, NumCast } from "../../../fields/Types"; +import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, returnFalse, returnOne, returnZero, setupMoveUpEvents } from "../../../Utils"; +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 '../DocumentDecorations.scss'; -import { CellProps, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell, CollectionSchemaNumberCell, CollectionSchemaStringCell } from "./CollectionSchemaCells"; -import { CollectionSchemaAddColumnHeader, CollectionSchemaHeader } from "./CollectionSchemaHeaders"; -import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; +import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; +import { KeysDropdown } from "./CollectionSchemaHeaders"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { CollectionView } from "./CollectionView"; -import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; -import { setupMoveUpEvents, emptyFunction, returnZero, returnOne, returnFalse } from "../../../Utils"; -import { DocumentView } from "../nodes/DocumentView"; +import { SchemaTable } from "./SchemaTable"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); @@ -40,6 +35,9 @@ export enum ColumnType { String, Boolean, Doc, + Image, + List, + Date } // this map should be used for keys that should have a const type of value const columnTypes: Map<string, ColumnType> = new Map([ @@ -62,6 +60,365 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } + @observable _menuWidth = 0; + @observable _headerOpen = false; + @observable _isOpen = false; + @observable _node: HTMLDivElement | null = null; + @observable _headerIsEditing = false; + @observable _col: any = ""; + @observable _menuHeight = 0; + @observable _pointerX = 0; + @observable _pointerY = 0; + @observable _openTypes: boolean = false; + @computed get menuCoordinates() { + const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)); + const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)); + return this.props.ScreenToLocalTransform().transformPoint(x, y); + } + + @observable scale = this.props.ScreenToLocalTransform().Scale; + + @computed get columns() { + return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); + } + set columns(columns: SchemaHeaderField[]) { + this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns); + } + + get documentKeys() { + const docs = this.childDocs; + const keys: { [key: string]: boolean } = {}; + // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. + // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be + // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. + // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu + // is displayed (unlikely) it won't show up until something else changes. + //TODO Types + untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)))); + + this.columns.forEach(key => keys[key.heading] = true); + return Array.from(Object.keys(keys)); + } + @computed get possibleKeys() { return this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); } + + + componentDidMount() { + document.addEventListener("pointerdown", this.detectClick); + } + + componentWillUnmount() { + document.removeEventListener("pointerdown", this.detectClick); + } + + @action setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing; + + detectClick = (e: PointerEvent): void => { + if (this._node && this._node.contains(e.target as Node)) { + } else { + this._isOpen = false; + this.setHeaderIsEditing(false); + this.closeHeader(); + } + } + + @action + toggleIsOpen = (): void => { + this._isOpen = !this._isOpen; + this.setHeaderIsEditing(this._isOpen); + } + + @action + changeColumnType = (type: ColumnType, col: any): void => { + this._openTypes = false; + this.setColumnType(col, type); + } + + changeColumnSort = (desc: boolean | undefined, col: any): void => { + this.setColumnSort(col, desc); + } + + changeColumnColor = (color: string, col: any): void => { + this.setColumnColor(col, color); + } + + @undoBatch + setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => { + if (columnTypes.get(columnField.heading)) return; + + const columns = this.columns; + const index = columns.indexOf(columnField); + if (index > -1) { + columnField.setType(NumCast(type)); + columns[index] = columnField; + this.columns = columns; + } + } + + @undoBatch + setColumnColor = (columnField: SchemaHeaderField, color: string): void => { + const columns = this.columns; + const index = columns.indexOf(columnField); + if (index > -1) { + columnField.setColor(color); + columns[index] = columnField; + this.columns = columns; // need to set the columns to trigger rerender + } + } + + @undoBatch + @action + setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { + const columns = this.columns; + columns.forEach(col => col.setDesc(undefined)); + + const index = columns.findIndex(c => c.heading === columnField.heading); + const column = columns[index]; + column.setDesc(descending); + columns[index] = column; + this.columns = columns; + } + + @action + setNode = (node: HTMLDivElement): void => { + node && (this._node = node); + } + + @action + typesDropdownChange = (bool: boolean) => { + this._openTypes = bool; + } + + renderTypes = (col: any) => { + if (columnTypes.get(col.heading)) return (null); + + const type = col.type; + + const anyType = <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any, col)}> + <FontAwesomeIcon icon={"align-justify"} size="sm" /> + Any + </div>; + + const numType = <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number, col)}> + <FontAwesomeIcon icon={"hashtag"} size="sm" /> + Number + </div>; + + const textType = <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String, col)}> + <FontAwesomeIcon icon={"font"} size="sm" /> + Text + </div>; + + const boolType = <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean, col)}> + <FontAwesomeIcon icon={"check-square"} size="sm" /> + Checkbox + </div>; + + const listType = <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.List, col)}> + <FontAwesomeIcon icon={"list-ul"} size="sm" /> + List + </div>; + + const docType = <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc, col)}> + <FontAwesomeIcon icon={"file"} size="sm" /> + Document + </div>; + + const imageType = <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Image, col)}> + <FontAwesomeIcon icon={"image"} size="sm" /> + Image + </div>; + + const dateType = <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Date, col)}> + <FontAwesomeIcon icon={"calendar"} size="sm" /> + Date + </div>; + + + const allColumnTypes = <div className="columnMenu-types"> + {anyType} + {numType} + {textType} + {boolType} + {listType} + {docType} + {imageType} + {dateType} + </div>; + + const justColType = type === ColumnType.Any ? anyType : type === ColumnType.Number ? numType : + type === ColumnType.String ? textType : type === ColumnType.Boolean ? boolType : + type === ColumnType.List ? listType : type === ColumnType.Doc ? docType : + type === ColumnType.Date ? dateType : imageType; + + return ( + <div className="collectionSchema-headerMenu-group"> + <div onClick={() => this.typesDropdownChange(!this._openTypes)}> + <label>Column type:</label> + <FontAwesomeIcon icon={"caret-down"} size="lg" style={{ float: "right" }} /> + </div> + {this._openTypes ? allColumnTypes : justColType} + </div > + ); + } + + renderSorting = (col: any) => { + const sort = col.desc; + return ( + <div className="collectionSchema-headerMenu-group"> + <label>Sort by:</label> + <div className="columnMenu-sort"> + <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true, col)}> + <FontAwesomeIcon icon="sort-amount-down" size="sm" /> + Sort descending + </div> + <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false, col)}> + <FontAwesomeIcon icon="sort-amount-up" size="sm" /> + Sort ascending + </div> + <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined, col)}> + <FontAwesomeIcon icon="times" size="sm" /> + Clear sorting + </div> + </div> + </div> + ); + } + + renderColors = (col: any) => { + const selected = col.color; + + const pink = PastelSchemaPalette.get("pink2"); + const purple = PastelSchemaPalette.get("purple2"); + const blue = PastelSchemaPalette.get("bluegreen1"); + const yellow = PastelSchemaPalette.get("yellow4"); + const red = PastelSchemaPalette.get("red2"); + const gray = "#f1efeb"; + + return ( + <div className="collectionSchema-headerMenu-group"> + <label>Color:</label> + <div className="columnMenu-colors"> + <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!, col)}></div> + <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!, col)}></div> + <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!, col)}></div> + <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!, col)}></div> + <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!, col)}></div> + <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray, col)}></div> + </div> + </div> + ); + } + + @undoBatch + @action + changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => { + const columns = this.columns; + if (columns === undefined) { + this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]); + } else { + if (addNew) { + columns.push(new SchemaHeaderField(newKey, "f1efeb")); + this.columns = columns; + } else { + const index = columns.map(c => c.heading).indexOf(oldKey); + if (index > -1) { + const column = columns[index]; + column.setHeading(newKey); + columns[index] = column; + 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; + } + } + } + } + } + } + + @action + openHeader = (col: any, screenx: number, screeny: number) => { + this._col = col; + this._headerOpen = !this._headerOpen; + this._pointerX = screenx; + this._pointerY = screeny; + } + + @action + closeHeader = () => { this._headerOpen = false; } + + renderKeysDropDown = (col: any) => { + return <KeysDropdown + keyValue={col.heading} + possibleKeys={this.possibleKeys} + existingKeys={this.columns.map(c => c.heading)} + canAddNew={true} + addNew={false} + onSelect={this.changeColumns} + setIsEditing={this.setHeaderIsEditing} + />; + } + + @undoBatch + @action + deleteColumn = (key: string) => { + const columns = this.columns; + if (columns === undefined) { + this.columns = new List<SchemaHeaderField>([]); + } else { + const index = columns.map(c => c.heading).indexOf(key); + if (index > -1) { + columns.splice(index, 1); + this.columns = columns; + } + } + this.closeHeader(); + } + + getPreviewTransform = (): Transform => { + return this.props.ScreenToLocalTransform().translate(- this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, - this.borderWidth); + } + + @action + onHeaderClick = (e: React.PointerEvent) => { + this.props.active(true); + e.stopPropagation(); + } + + @action + onWheel(e: React.WheelEvent) { + const scale = this.props.ScreenToLocalTransform().Scale; + this.props.active(true) && e.stopPropagation(); + //this.menuCoordinates[0] -= e.screenX / scale; + //this.menuCoordinates[1] -= e.screenY / scale; + } + + @computed get renderMenuContent() { + TraceMobx(); + return <div className="collectionSchema-header-menuOptions"> + <div className="collectionSchema-headerMenu-group"> + <label>Key:</label> + {this.renderKeysDropDown(this._col)} + </div> + {this.renderTypes(this._col)} + {this.renderSorting(this._col)} + {this.renderColors(this._col)} + <div className="collectionSchema-headerMenu-group"> + <button onClick={() => { this.deleteColumn(this._col.heading); }} + >Delete Column</button> + </div> + </div>; + } + private createTarget = (ele: HTMLDivElement) => { this._previewCont = ele; super.CreateDropTarget(ele); @@ -105,14 +462,12 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get previewDocument(): Doc | undefined { return this.previewDoc; } - getPreviewTransform = (): Transform => { - return this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth); - } - @computed get dividerDragger() { return this.previewWidth() === 0 ? (null) : - <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; + <div className="collectionSchemaView-dividerDragger" + onPointerDown={this.onDividerDown} + style={{ width: `${this.DIVIDER_WIDTH}px` }} />; } @computed @@ -133,6 +488,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { PanelWidth={this.previewWidth} PanelHeight={this.previewHeight} ScreenToLocalTransform={this.getPreviewTransform} + docFilters={this.docFilters} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} moveDocument={this.props.moveDocument} @@ -173,6 +529,17 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { deleteDocument={this.props.removeDocument} addDocument={this.props.addDocument} dataDoc={this.props.DataDoc} + columns={this.columns} + documentKeys={this.documentKeys} + headerIsEditing={this._headerIsEditing} + openHeader={this.openHeader} + onPointerDown={this.onTablePointerDown} + onResizedChange={this.onResizedChange} + setColumns={this.setColumns} + reorderColumns={this.reorderColumns} + changeColumns={this.changeColumns} + setHeaderIsEditing={this.setHeaderIsEditing} + changeColumnSort={this.setColumnSort} />; } @@ -180,384 +547,33 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { public get schemaToolbar() { return <div className="collectionSchemaView-toolbar"> <div className="collectionSchemaView-toolbar-item"> - <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />Show Preview</div> - </div> - </div>; - } - - render() { - return <div className="collectionSchemaView-container"> - <div className="collectionSchemaView-tableContainer" style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> - {this.schemaTable} + <div id="preview-schema-checkbox-div"> + <input type="checkbox" + key={"Show Preview"} checked={this.previewWidth() !== 0} + onChange={this.toggleExpander} />Show Preview</div> </div> - {this.dividerDragger} - {!this.previewWidth() ? (null) : this.previewPanel} </div>; } -} - -export interface SchemaTableProps { - Document: Doc; // child doc - dataDoc?: Doc; - PanelHeight: () => number; - PanelWidth: () => number; - childDocs?: Doc[]; - CollectionView: Opt<CollectionView>; - ContainingCollectionView: Opt<CollectionView>; - ContainingCollectionDoc: Opt<Doc>; - fieldKey: string; - renderDepth: number; - deleteDocument: (document: Doc | Doc[]) => boolean; - addDocument: (document: Doc | Doc[]) => boolean; - moveDocument: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; - ScreenToLocalTransform: () => Transform; - active: (outsideReaction: boolean) => boolean; - onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; - addDocTab: (document: Doc, where: string) => boolean; - pinToPres: (document: Doc) => void; - isSelected: (outsideReaction?: boolean) => boolean; - isFocused: (document: Doc) => boolean; - setFocused: (document: Doc) => void; - setPreviewDoc: (document: Doc) => void; -} - -@observer -export class SchemaTable extends React.Component<SchemaTableProps> { - private DIVIDER_WIDTH = 4; - - @observable _headerIsEditing: boolean = false; - @observable _cellIsEditing: boolean = false; - @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; - @observable _openCollections: Array<string> = []; - - @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } - @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } - @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } - - @computed get columns() { - return Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField), []); - } - set columns(columns: SchemaHeaderField[]) { - this.props.Document.schemaColumns = new List<SchemaHeaderField>(columns); - } - - @computed get childDocs() { - if (this.props.childDocs) return this.props.childDocs; - - const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - return DocListCast(doc[this.props.fieldKey]); - } - set childDocs(docs: Doc[]) { - const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; - doc[this.props.fieldKey] = new List<Doc>(docs); - } - - @computed get textWrappedRows() { - return Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); - } - set textWrappedRows(textWrappedRows: string[]) { - this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows); - } - - @computed get resized(): { id: string, value: number }[] { - return this.columns.reduce((resized, shf) => { - (shf.width > -1) && resized.push({ id: shf.heading, value: shf.width }); - return resized; - }, [] as { id: string, value: number }[]); - } - @computed get sorted(): SortingRule[] { - return this.columns.reduce((sorted, shf) => { - shf.desc && sorted.push({ id: shf.heading, desc: shf.desc }); - return sorted; - }, [] as SortingRule[]); - } - - @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } - @computed get tableColumns(): Column<Doc>[] { - const possibleKeys = this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); - const columns: Column<Doc>[] = []; - const tableIsFocused = this.props.isFocused(this.props.Document); - const focusedRow = this._focusedCell.row; - const focusedCol = this._focusedCell.col; - const isEditable = !this._headerIsEditing; - - if (this.childDocs.reduce((found, doc) => found || doc.type === "collection", false)) { - columns.push( - { - expander: true, - Header: "", - width: 30, - Expander: (rowInfo) => { - if (rowInfo.original.type === "collection") { - if (rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onCloseCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-up"} size="sm" /></div>; - if (!rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onExpandCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-down"} size="sm" /></div>; - } else { - return null; - } - } - } - ); - } - - const cols = this.columns.map(col => { - const header = <CollectionSchemaHeader - keyValue={col} - possibleKeys={possibleKeys} - existingKeys={this.columns.map(c => c.heading)} - keyType={this.getColumnType(col)} - typeConst={columnTypes.get(col.heading) !== undefined} - onSelect={this.changeColumns} - setIsEditing={this.setHeaderIsEditing} - deleteColumn={this.deleteColumn} - setColumnType={this.setColumnType} - setColumnSort={this.setColumnSort} - setColumnColor={this.setColumnColor} - />; - - return { - Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.columns} reorderColumns={this.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />, - accessor: (doc: Doc) => doc ? doc[col.heading] : 0, - id: col.heading, - Cell: (rowProps: CellInfo) => { - const rowIndex = rowProps.index; - const columnIndex = this.columns.map(c => c.heading).indexOf(rowProps.column.id!); - const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused; - - const props: CellProps = { - row: rowIndex, - col: columnIndex, - rowProps: rowProps, - isFocused: isFocused, - changeFocusedCellByIndex: this.changeFocusedCellByIndex, - CollectionView: this.props.CollectionView, - ContainingCollection: this.props.ContainingCollectionView, - Document: this.props.Document, - fieldKey: this.props.fieldKey, - renderDepth: this.props.renderDepth, - addDocTab: this.props.addDocTab, - pinToPres: this.props.pinToPres, - moveDocument: this.props.moveDocument, - setIsEditing: this.setCellIsEditing, - isEditable: isEditable, - setPreviewDoc: this.props.setPreviewDoc, - setComputed: this.setComputed, - getField: this.getField, - }; - - 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} />; - return <CollectionSchemaCell {...props} />; - }, - minWidth: 200, - }; - }); - columns.push(...cols); - - columns.push({ - Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />, - accessor: (doc: Doc) => 0, - id: "add", - Cell: (rowProps: CellInfo) => <></>, - width: 28, - resizable: false - }); - return columns; - } - - constructor(props: SchemaTableProps) { - super(props); - // convert old schema columns (list of strings) into new schema columns (list of schema header fields) - const oldSchemaColumns = Cast(this.props.Document.schemaColumns, listSpec("string"), []); - if (oldSchemaColumns && oldSchemaColumns.length && typeof oldSchemaColumns[0] !== "object") { - const newSchemaColumns = oldSchemaColumns.map(i => typeof i === "string" ? new SchemaHeaderField(i, "#f1efeb") : i); - this.props.Document.schemaColumns = new List<SchemaHeaderField>(newSchemaColumns); - } - } - - componentDidMount() { - document.addEventListener("keydown", this.onKeyDown); - } - - componentWillUnmount() { - document.removeEventListener("keydown", this.onKeyDown); - } - - tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { - return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); - } - - private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { - return !rowInfo ? {} : { - ScreenToLocalTransform: this.props.ScreenToLocalTransform, - addDoc: this.tableAddDoc, - removeDoc: this.props.deleteDocument, - rowInfo, - rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document), - textWrapRow: this.toggleTextWrapRow, - rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1, - dropAction: StrCast(this.props.Document.childDropAction) - }; - } - - private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => { - if (!rowInfo || column) return {}; - - const row = rowInfo.index; - //@ts-ignore - const col = this.columns.map(c => c.heading).indexOf(column!.id); - const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document); - // TODO: editing border doesn't work :( - return { - style: { - border: !this._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 setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing; - - onPointerDown = (e: React.PointerEvent): void => { - this.props.setFocused(this.props.Document); + onTablePointerDown = (e: React.PointerEvent): void => { + this.setFocused(this.props.Document); if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) { e.stopPropagation(); } + this._pointerY = e.screenY; + this._pointerX = e.screenX; } - @action - onKeyDown = (e: KeyboardEvent): void => { - if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected(true)) { - const direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; - this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col); - - const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); - pdoc && this.props.setPreviewDoc(pdoc); - } - } - - changeFocusedCellByDirection = (direction: string, curRow: number, curCol: number) => { - switch (direction) { - case "tab": return { row: (curRow + 1 === this.childDocs.length ? 0 : curRow + 1), col: curCol + 1 === this.columns.length ? 0 : curCol + 1 }; - case "right": return { row: curRow, col: curCol + 1 === this.columns.length ? curCol : curCol + 1 }; - case "left": return { row: curRow, col: curCol === 0 ? curCol : curCol - 1 }; - case "up": return { row: curRow === 0 ? curRow : curRow - 1, col: curCol }; - case "down": return { row: curRow + 1 === this.childDocs.length ? curRow : curRow + 1, col: curCol }; - } - return this._focusedCell; - } - - @action - changeFocusedCellByIndex = (row: number, col: number): void => { - if (this._focusedCell.row !== row || this._focusedCell.col !== col) { - this._focusedCell = { row: row, col: col }; - } - this.props.setFocused(this.props.Document); - } - - @undoBatch - createRow = () => { - this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); - } - - @undoBatch - @action - createColumn = () => { - let index = 0; - let found = this.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1; - while (found) { - index++; - found = this.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; - } - this.columns.push(new SchemaHeaderField(`New field ${index ? "(" + index + ")" : ""}`, "#f1efeb")); - } - - @undoBatch - @action - deleteColumn = (key: string) => { - const columns = this.columns; - if (columns === undefined) { - this.columns = new List<SchemaHeaderField>([]); - } else { - const index = columns.map(c => c.heading).indexOf(key); - if (index > -1) { - columns.splice(index, 1); - this.columns = columns; - } - } - } - - @undoBatch - @action - changeColumns = (oldKey: string, newKey: string, addNew: boolean) => { - const columns = this.columns; - if (columns === undefined) { - this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, "f1efeb")]); - } else { - if (addNew) { - columns.push(new SchemaHeaderField(newKey, "f1efeb")); - this.columns = columns; - } else { - const index = columns.map(c => c.heading).indexOf(oldKey); - if (index > -1) { - const column = columns[index]; - column.setHeading(newKey); - columns[index] = column; - this.columns = columns; - } - } - } - } - - getColumnType = (column: SchemaHeaderField): ColumnType => { - // added functionality to convert old column type stuff to new column type stuff -syip - 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; - } - column.type = NumCast(typesDoc[column.heading]); - return NumCast(typesDoc[column.heading]); - } - - @undoBatch - setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => { - if (columnTypes.get(columnField.heading)) return; - - const columns = this.columns; - const index = columns.indexOf(columnField); - if (index > -1) { - columnField.setType(NumCast(type)); - columns[index] = columnField; - this.columns = columns; - } - } - - @undoBatch - setColumnColor = (columnField: SchemaHeaderField, color: string): void => { + onResizedChange = (newResized: Resize[], event: any) => { const columns = this.columns; - const index = columns.indexOf(columnField); - if (index > -1) { - columnField.setColor(color); - columns[index] = columnField; - this.columns = columns; // need to set the columns to trigger rerender - } + newResized.forEach(resized => { + const index = columns.findIndex(c => c.heading === resized.id); + const column = columns[index]; + column.setWidth(resized.value); + columns[index] = column; + }); + this.columns = columns; } @action @@ -576,180 +592,59 @@ export class SchemaTable extends React.Component<SchemaTableProps> { this.columns = columns; } - @undoBatch - @action - setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { - const columns = this.columns; - const index = columns.findIndex(c => c.heading === columnField.heading); - const column = columns[index]; - column.setDesc(descending); - columns[index] = column; - this.columns = columns; - } - - get documentKeys() { - const docs = this.childDocs; - const keys: { [key: string]: boolean } = {}; - // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. - // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be - // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. - // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu - // is displayed (unlikely) it won't show up until something else changes. - //TODO Types - untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)))); - - this.columns.forEach(key => keys[key.heading] = true); - return Array.from(Object.keys(keys)); - } - - @undoBatch - @action - toggleTextwrap = async () => { - const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); - if (textwrappedRows.length) { - this.props.Document.textwrappedSchemaRows = new List<string>([]); + onZoomMenu = (e: React.WheelEvent) => { + this.props.active(true) && e.stopPropagation(); + if (this.menuCoordinates[0] > e.screenX) { + this.menuCoordinates[0] -= e.screenX; //* this.scale; } else { - const docs = DocListCast(this.props.Document[this.props.fieldKey]); - const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); - this.props.Document.textwrappedSchemaRows = new List<string>(allRows); + this.menuCoordinates[0] += e.screenX; //* this.scale; } - } - - @action - toggleTextWrapRow = (doc: Doc): void => { - const textWrapped = this.textWrappedRows; - const index = textWrapped.findIndex(id => doc[Id] === id); - - index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]); - - this.textWrappedRows = textWrapped; - } - - @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 rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( - - return <ReactTable - style={{ position: "relative" }} - data={children} - page={0} - pageSize={children.length} - showPagination={false} - columns={this.tableColumns} - getTrProps={this.getTrProps} - getTdProps={this.getTdProps} - sortable={false} - TrComponent={MovableRow} - sorted={this.sorted} - expanded={expanded} - resized={this.resized} - onResizedChange={this.onResizedChange} - SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== "collection") ? (null) : - <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} childDocs={undefined} /></div>} - - />; - } - - onResizedChange = (newResized: Resize[], event: any) => { - const columns = this.columns; - newResized.forEach(resized => { - const index = columns.findIndex(c => c.heading === resized.id); - const column = columns[index]; - column.setWidth(resized.value); - columns[index] = column; - }); - this.columns = columns; - } - - 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" }); + if (this.menuCoordinates[1] > e.screenY) { + this.menuCoordinates[1] -= e.screenY; //* this.scale; + } else { + this.menuCoordinates[1] += e.screenY; //* this.scale; } } - getField = (row: number, col?: number) => { - const docs = this.childDocs; - - row = row % docs.length; - while (row < 0) row += docs.length; - const columns = this.columns; - const doc = docs[row]; - if (col === undefined) { - return doc; - } - if (col >= 0 && col < columns.length) { - const column = this.columns[col].heading; - return doc[column]; - } - return undefined; - } - - createTransformer = (row: number, col: number): Transformer => { - const self = this; - const captures: { [name: string]: Field } = {}; - - const transformer: ts.TransformerFactory<ts.SourceFile> = context => { - return root => { - function visit(node: ts.Node) { - node = ts.visitEachChild(node, visit, context); - if (ts.isIdentifier(node)) { - const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; - const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; - if (isntPropAccess && isntPropAssign) { - if (node.text === "$r") { - return ts.createNumericLiteral(row.toString()); - } else if (node.text === "$c") { - return ts.createNumericLiteral(col.toString()); - } else if (node.text === "$") { - if (ts.isCallExpression(node.parent)) { - // captures.doc = self.props.Document; - // captures.key = self.props.fieldKey; - } - } - } - } - - return node; - } - return ts.visitNode(root, visit); - }; - }; - - // const getVars = () => { - // return { capturedVariables: captures }; - // }; - return { transformer, /*getVars*/ }; - } - 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).schemaColumns[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) }); - if (compiled.compiled) { - doc[field] = new ComputedField(compiled); - return true; - } - return false; + onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { } - render() { - return <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > - {this.reactTable} - <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div> + TraceMobx(); + const menuContent = this.renderMenuContent; + const menu = <div className="collectionSchema-header-menu" ref={this.setNode} + onWheel={e => this.onZoomMenu(e)} + onPointerDown={e => this.onHeaderClick(e)} + style={{ + position: "fixed", background: "white", + transform: `translate(${this.menuCoordinates[0] / this.scale}px, ${this.menuCoordinates[1] / this.scale}px)` + }}> + <Measure offset onResize={action((r: any) => { + const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); + this._menuWidth = dim[0]; this._menuHeight = dim[1]; + })}> + {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>} + </Measure> + </div>; + return <div className="collectionSchemaView-container" + style={{ + overflow: this.props.overflow === true ? "auto" : undefined, + pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined, + width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%", position: "relative", + }} > + <div className="collectionSchemaView-tableContainer" + style={{ backgroundColor: "white", width: `calc(100% - ${this.previewWidth()}px)` }} + onKeyPress={this.onKeyPress} + onPointerDown={this.onPointerDown} + onWheel={e => this.props.active(true) && e.stopPropagation()} + onDrop={e => this.onExternalDrop(e, {})} + ref={this.createTarget}> + {this.schemaTable} + </div> + {this.dividerDragger} + {!this.previewWidth() ? (null) : this.previewPanel} + {this._headerOpen ? menu : null} </div>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 203c51163..8fc74a9c6 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -33,8 +33,9 @@ .collectionStackingViewFieldColumn { height: max-content; } + .collectionStackingViewFieldColumnDragging { - height:100%; + height: 100%; } .collectionSchemaView-previewDoc { @@ -240,11 +241,15 @@ } .collectionStackingView-sectionColorButton { - height: 35px; + height: 30px; + display: inherit; } .collectionStackingView-colorPicker { width: 78px; + z-index: 10; + position: relative; + background: white; .colorOptions { display: flex; @@ -278,7 +283,7 @@ } .collectionStackingView-sectionOptionButton { - height: 35px; + height: 30px; } .collectionStackingView-optionPicker { @@ -421,4 +426,15 @@ .rc-switch-checked .rc-switch-inner { left: 8px; } +} + +@media only screen and (max-device-width: 480px) { + + .collectionStackingView .collectionStackingView-columnDragger, + .collectionMasonryView .collectionStackingView-columnDragger { + width: 0.1; + height: 0.1; + opacity: 0; + font-size: 0; + } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 7fd19a23c..fe3d57bdb 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -27,6 +27,7 @@ import { CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from "./CollectionView"; import { SnappingManager } from "../../util/SnappingManager"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { DocUtils } from "../../documents/Documents"; const _global = (window /* browser */ || global /* node */) as any; type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>; @@ -42,27 +43,26 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) @observable _heightMap = new Map<string, number>(); @observable _cursor: CursorProperty = "grab"; @observable _scroll = 0; // used to force the document decoration to update when scrolling - @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); } - @computed get pivotField() { return StrCast(this.props.Document._pivotField); } - @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); } - @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } - @computed get yMargin() { return Math.max(this.props.Document._showTitle && !this.props.Document._showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 2 * this.gridGap)); } - @computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); } - @computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); } + @computed get 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, 0)); } // 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.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); } + @computed get showAddAGroup() { return (this.pivotField && (this.layoutDoc._chromeStatus !== 'view-mode' && this.layoutDoc._chromeStatus !== 'disabled')); } @computed get columnWidth() { - TraceMobx(); - return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin, - this.isStackingView ? Number.MAX_VALUE : this.props.Document.columnWidth === -1 ? this.props.PanelWidth() - 2 * this.xMargin : NumCast(this.props.Document.columnWidth, 250)); + 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)); } @computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; } constructor(props: any) { super(props); - if (this.sectionHeaders === undefined) { - this.props.Document.sectionHeaders = new List<SchemaHeaderField>(); + if (this.columnHeaders === undefined) { + this.layoutDoc._columnHeaders = new List<SchemaHeaderField>(); } } @@ -88,14 +88,14 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } get Sections() { - if (!this.pivotField || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); + if (!this.pivotField || this.columnHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); - if (this.sectionHeaders === undefined) { - setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0); + if (this.columnHeaders === undefined) { + setTimeout(() => this.layoutDoc._columnHeaders = new List<SchemaHeaderField>(), 0); return new Map<SchemaHeaderField, Doc[]>(); } - const sectionHeaders = Array.from(this.sectionHeaders); - const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); + const columnHeaders = Array.from(this.columnHeaders); + const fields = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); let changed = false; this.filteredChildren.map(d => { const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object; @@ -104,26 +104,26 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue; // look for if header exists already - const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`)); + const existingHeader = columnHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`)); if (existingHeader) { fields.get(existingHeader)!.push(d); } else { const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`); fields.set(newSchemaHeader, [d]); - sectionHeaders.push(newSchemaHeader); + columnHeaders.push(newSchemaHeader); changed = true; } }); // remove all empty columns if hideHeadings is set - if (this.props.Document.hideHeadings) { + if (this.layoutDoc._columnsHideIfEmpty) { Array.from(fields.keys()).filter(key => !fields.get(key)!.length).map(header => { fields.delete(header); - sectionHeaders.splice(sectionHeaders.indexOf(header), 1); + columnHeaders.splice(columnHeaders.indexOf(header), 1); changed = true; }); } - changed && setTimeout(action(() => { if (this.sectionHeaders) { this.sectionHeaders.length = 0; this.sectionHeaders.push(...sectionHeaders); } }), 0); + changed && setTimeout(action(() => this.columnHeaders?.splice(0, this.columnHeaders.length, ...columnHeaders)), 0); return fields; } @@ -135,7 +135,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); if (!layoutDoc._fitWidth && nw && nh) { const aspect = nw && nh ? nh / nw : 1; - if (!(this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); + 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](); @@ -146,7 +146,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) // reset section headers when a new filter is inputted this._pivotFieldDisposer = reaction( () => this.pivotField, - () => this.props.Document.sectionHeaders = new List() + () => this.layoutDoc._columnHeaders = new List() ); } componentWillUnmount() { @@ -163,8 +163,8 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) this.createDashEventsTarget(ele!); //so the whole grid is the drop target? } - @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } + @computed get onChildClickHandler() { return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); } + @computed get onChildDoubleClickHandler() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } addDocTab = (doc: Doc, where: string) => { if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { @@ -184,7 +184,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) if (found) { const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); - smoothScroll(500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + smoothScroll(doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); } afterFocus && setTimeout(() => { if (afterFocus?.()) { } @@ -208,14 +208,15 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) NativeHeight={returnZero} NativeWidth={returnZero} fitToBox={false} - dontRegisterView={this.props.dontRegisterView} + dontRegisterView={BoolCast(this.layoutDoc.dontRegisterChildViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={dxf} opacity={opacity} focus={this.focusDocument} + docFilters={this.docFilters} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} @@ -226,6 +227,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) addDocTab={this.addDocTab} bringToFront={returnFalse} ContentScaling={returnOne} + scriptContext={this.props.scriptContext} pinToPres={this.props.pinToPres} />; } @@ -234,7 +236,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) if (!d) return 0; const layoutDoc = Doc.Layout(d, this.props.ChildLayoutTemplate?.()); const nw = NumCast(layoutDoc._nativeWidth); - return Math.min(nw && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); + return Math.min(nw && !this.layoutDoc._columnsFill ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); } getDocHeight(d?: Doc) { if (!d) return 0; @@ -244,11 +246,11 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); if (!layoutDoc._fitWidth && nw && nh) { const aspect = nw && nh ? nh / nw : 1; - if (!(this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid); + if (!(this.layoutDoc._columnsFill)) wid = Math.min(layoutDoc[WidthSym](), wid); return 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) : layoutDoc[HeightSym](); + Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : Math.max(20, layoutDoc[HeightSym]()); } columnDividerDown = (e: React.PointerEvent) => { @@ -257,7 +259,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } @action onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { - this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta[0]); + this.layoutDoc._columnWidth = Math.max(10, this.columnWidth + delta[0]); return false; } @@ -285,19 +287,31 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) } }); if (super.onInternalDrop(e, de)) { - const newDoc = de.complete.docDragData.droppedDocuments[0]; + const newDocs = de.complete.docDragData.droppedDocuments; const docs = this.childDocList; if (docs) { - if (targInd === -1) targInd = docs.length; - else targInd = docs.indexOf(this.filteredChildren[targInd]); - const srcInd = docs.indexOf(newDoc); - docs.splice(srcInd, 1); - docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, newDoc); + newDocs.map((doc, i) => { + console.log(doc.title); + if (i === 0) { + if (targInd === -1) targInd = docs.length; + else targInd = docs.indexOf(this.filteredChildren[targInd]); + const srcInd = docs.indexOf(doc); + docs.splice(srcInd, 1); + docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, doc); + } else if (i < (newDocs.length / 2)) { //glr: for some reason dragged documents are duplicated + if (targInd === -1) targInd = docs.length; + else targInd = docs.indexOf(newDocs[0]) + 1; + const srcInd = docs.indexOf(doc); + docs.splice(srcInd, 1); + docs.splice((targInd > srcInd ? targInd - 1 : targInd) + plusOne, 0, doc); + } + }); } } } return false; } + @undoBatch @action onExternalDrop = async (e: React.DragEvent): Promise<void> => { @@ -339,7 +353,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) this.refList.push(ref); const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { - if (this.props.Document._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { + 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", ""))))); } })); @@ -386,7 +400,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) this.refList.push(ref); const doc = this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc; this.observer = new _global.ResizeObserver(action((entries: any) => { - if (this.props.Document._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { + 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); } })); @@ -409,32 +423,32 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) @action addGroup = (value: string) => { - if (value && this.sectionHeaders) { + if (value && this.columnHeaders) { const schemaHdrField = new SchemaHeaderField(value); - this.sectionHeaders.push(schemaHdrField); - Doc.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: schemaHdrField.color }]); + this.columnHeaders.push(schemaHdrField); + DocUtils.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: schemaHdrField.color }]); return true; } return false; } sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => { - const descending = BoolCast(this.props.Document.stackingHeadersSortDescending); + const descending = StrCast(this.layoutDoc._columnsSort) === "descending"; const firstEntry = descending ? b : a; const secondEntry = descending ? a : b; return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; } onToggle = (checked: Boolean) => { - this.props.Document._chromeStatus = checked ? "collapsed" : "view-mode"; + this.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.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" }); - subItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); + 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" }); ContextMenu.Instance.addItem({ description: "Options...", subitems: subItems, icon: "eye" }); } } @@ -444,7 +458,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; if (this.pivotField) { const entries = Array.from(this.Sections.entries()); - sections = entries.sort(this.sortFunc); + sections = this.layoutDoc._columnsSort ? entries.sort(this.sortFunc) : entries; } return sections.map((section, i) => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1], i === 0)); } @@ -474,24 +488,27 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) width: `${1 / this.scaling * 100}%`, transformOrigin: "top left", }} - onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} + onScroll={action(e => { + if (!this.props.isSelected() && this.props.renderDepth) e.currentTarget.scrollTop = this._scroll; + else this._scroll = e.currentTarget.scrollTop; + })} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} - onWheel={e => this.props.active() && e.stopPropagation()} > + onWheel={e => this.props.active(true) && e.stopPropagation()} > {this.renderedSections} {!this.showAddAGroup ? (null) : <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> <EditableView {...editableViewProps} /> </div>} - {this.props.Document._chromeStatus !== 'disabled' && this.props.isSelected() ? <Switch + {this.layoutDoc._chromeStatus !== 'disabled' && this.props.isSelected() ? <Switch onChange={this.onToggle} onClick={this.onToggle} - defaultChecked={this.props.Document._chromeStatus !== 'view-mode'} + defaultChecked={this.layoutDoc._chromeStatus !== 'view-mode'} checkedChildren="edit" unCheckedChildren="view" /> : null} </div> </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index a269b21f5..f193a9787 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { library } from '@fortawesome/fontawesome-svg-core'; import { faPalette } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, observable, runInAction } from "mobx"; +import { action, observable, runInAction, computed } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast } from "../../../fields/Doc"; import { RichTextField } from "../../../fields/RichTextField"; @@ -12,6 +12,7 @@ import { NumCast, StrCast, Cast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; import { Docs, DocUtils } from "../../documents/Documents"; +import { DocumentType } from "../../documents/DocumentTypes"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; @@ -50,6 +51,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC private dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); + @observable _paletteOn = false; @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; _ele: HTMLElement | null = null; @@ -95,8 +97,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const key = StrCast(this.props.parent.props.Document._pivotField); const castedValue = this.getValue(value); if (castedValue) { - if (this.props.parent.sectionHeaders) { - if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) { + if (this.props.parent.columnHeaders) { + if (this.props.parent.columnHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) { return false; } } @@ -147,9 +149,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC deleteColumn = () => { const key = StrCast(this.props.parent.props.Document._pivotField); this.props.docList.forEach(d => d[key] = undefined); - if (this.props.parent.sectionHeaders && this.props.headingObject) { - const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); - this.props.parent.sectionHeaders.splice(index, 1); + 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); } } @@ -167,7 +169,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC startDrag = (e: PointerEvent, down: number[], delta: number[]) => { const alias = Doc.MakeAlias(this.props.parent.props.Document); - alias._width = this.props.parent.props.PanelWidth() / (Cast(this.props.parent.props.Document.sectionHeaders, listSpec(SchemaHeaderField))?.length || 1); + alias._width = this.props.parent.props.PanelWidth() / (Cast(this.props.parent.columnHeaders, listSpec(SchemaHeaderField))?.length || 1); alias._pivotField = undefined; const key = StrCast(this.props.parent.props.Document._pivotField); let value = this.getValue(this._heading); @@ -235,7 +237,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey => docItems.push({ description: ":" + fieldKey, event: () => { - const created = Docs.Get.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.parent.props.Document)); + const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.parent.props.Document)); if (created) { if (this.props.parent.Document.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document); @@ -258,8 +260,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, _LODdisable: true })), icon: "compress-arrows-alt" }); - layoutItems.push({ description: ":carousel", event: () => this.props.parent.props.addDocument(Docs.Create.CarouselDocument([], { _width: 400, _height: 200, _LODdisable: true })), icon: "compress-arrows-alt" }); + layoutItems.push({ description: ":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" }); @@ -278,8 +280,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y); ContextMenu.Instance.displayMenu(x, y); } - - render() { + @computed get innards() { TraceMobx(); const cols = this.props.cols(); const key = StrCast(this.props.parent.props.Document._pivotField); @@ -298,7 +299,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC oneLine: true, HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, - color: this._color }; const newEditableViewProps = { GetValue: () => "", @@ -306,7 +306,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC contents: "+ NEW", HeadingObject: this.props.headingObject, toggle: this.toggleVisibility, - color: this._color }; const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} @@ -326,18 +325,15 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <EditableView {...headerEditableViewProps} /> {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> - <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> - <button className="collectionStackingView-sectionColorButton"> - <FontAwesomeIcon icon="palette" size="lg" /> - </button> - </ Flyout > + <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> + <FontAwesomeIcon icon="palette" size="lg" /> + </button> + {this._paletteOn ? this.renderColorPicker() : (null)} </div> } - {evContents === `NO ${key.toUpperCase()} VALUE` ? - (null) : - <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> - <FontAwesomeIcon icon="trash" size="lg" /> - </button>} + {<button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> + <FontAwesomeIcon icon="trash" size="lg" /> + </button>} {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionOptions"> <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> @@ -351,6 +347,43 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; const chromeStatus = this.props.parent.props.Document._chromeStatus; + const type = this.props.parent.props.Document.type; + return <> + {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} + { + this.collapsed ? (null) : + <div> + <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} + style={{ + padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`, + margin: "auto", + width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, + height: 'max-content', + position: "relative", + gridGap: style.gridGap, + gridTemplateColumns: singleColumn ? undefined : templatecols, + gridAutoRows: singleColumn ? undefined : "0px" + }}> + {this.props.parent.children(this.props.docList, uniqueHeadings.length)} + {singleColumn ? (null) : this.props.parent.columnDragger} + </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 }}> + <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> + </div> : null} + </div> + } + </>; + } + + + render() { + TraceMobx(); + const headings = this.props.headings(); + const heading = this._heading; + const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); + const chromeStatus = this.props.parent.props.Document._chromeStatus; return ( <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading} style={{ @@ -359,31 +392,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> - {this.props.parent.Document.hideHeadings ? (null) : headingView} - { - this.collapsed ? (null) : - <div> - <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} - style={{ - padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`, - margin: "auto", - width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, - height: 'max-content', - position: "relative", - gridGap: style.gridGap, - gridTemplateColumns: singleColumn ? undefined : templatecols, - gridAutoRows: singleColumn ? undefined : "0px" - }}> - {this.props.parent.children(this.props.docList, uniqueHeadings.length)} - {singleColumn ? (null) : this.props.parent.columnDragger} - </div> - {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> - </div> : null} - </div> - } + {this.innards} </div > ); } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 423eb1d90..72aece284 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,32 +1,25 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; import { basename } from 'path'; import CursorField from "../../../fields/CursorField"; -import { Doc, Opt } from "../../../fields/Doc"; +import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; -import { Cast, ScriptCast, NumCast } from "../../../fields/Types"; +import { WebField } from "../../../fields/URLField"; +import { Cast, ScriptCast, NumCast, StrCast } from "../../../fields/Types"; import { GestureUtils } from "../../../pen-gestures/GestureUtils"; -import { CurrentUserUtils } from "../../util/CurrentUserUtils"; -import { Upload } from "../../../server/SharedMediaTypes"; -import { Utils } from "../../../Utils"; -import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; +import { Utils, returnFalse, returnEmptyFilter } from "../../../Utils"; import { DocServer } from "../../DocServer"; -import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; -import { DocumentType } from "../../documents/DocumentTypes"; import { Networking } from "../../Network"; -import { DragManager, dropActionType } from "../../util/DragManager"; import { ImageUtils } from "../../util/Import & Export/ImageUtils"; import { InteractionUtils } from "../../util/InteractionUtils"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; -import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox"; -import { CollectionView } from "./CollectionView"; import React = require("react"); -import { SelectionManager } from "../../util/SelectionManager"; -import { WebField } from "../../../fields/URLField"; +import * as rp from 'request-promise'; +import ReactLoading from 'react-loading'; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc | Doc[]) => boolean; @@ -62,17 +55,17 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: class CollectionSubView extends DocComponent<X & SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; private gestureDisposer?: GestureUtils.GestureEventDisposer; - protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _mainCont?: HTMLDivElement; protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this.dropDisposer?.(); this.gestureDisposer?.(); - this.multiTouchDisposer?.(); + this._multiTouchDisposer?.(); if (ele) { this._mainCont = ele; this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc, this.onInternalPreDrop.bind(this)); this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this)); - this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this)); + this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this)); } } protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view @@ -81,7 +74,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: componentWillUnmount() { this.gestureDisposer?.(); - this.multiTouchDisposer?.(); + this._multiTouchDisposer?.(); } @computed get dataDoc() { @@ -98,6 +91,11 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: // 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() { + // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly + // setTimeout changes it outside of the @computed section + setTimeout(() => { + if (!this.dataDoc[this.props.annotationsKey || this.props.fieldKey]) this.dataDoc[this.props.annotationsKey || this.props.fieldKey] = new List<Doc>(); + }, 1000); return this.dataDoc[this.props.annotationsKey || this.props.fieldKey]; } @@ -109,20 +107,15 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: get childDocList() { return Cast(this.dataField, listSpec(Doc)); } + docFilters = () => { + return this.props.ignoreFields?.includes("_docFilters") ? [] : + [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; + } @computed get childDocs() { - const docFilters = this.props.ignoreFields?.includes("_docFilters") ? [] : Cast(this.props.Document._docFilters, listSpec("string"), []); - const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - for (let i = 0; i < docFilters.length; i += 3) { - const [key, value, modifiers] = docFilters.slice(i, i + 3); - if (!filterFacets[key]) { - filterFacets[key] = {}; - } - filterFacets[key][value] = modifiers; - } + let rawdocs: (Doc | Promise<Doc>)[] = DocListCast(this.props.Document._searchDocs); - let rawdocs: (Doc | Promise<Doc>)[] = []; - if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; + if (rawdocs.length !== 0) { + } else if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; rawdocs = [this.dataField]; } else if (Cast(this.dataField, listSpec(Doc), null)) { // otherwise, if the collection data is a list, then use it. rawdocs = Cast(this.dataField, listSpec(Doc), null); @@ -132,34 +125,68 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const rootDoc = Cast(this.props.Document.rootDocument, Doc, null); rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : []; } + const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc); const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); - const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - - const filteredDocs = docFilters.length && !this.props.dontRegisterView ? childDocs.filter(d => { - for (const facetKey of Object.keys(filterFacets)) { - const facet = filterFacets[facetKey]; - const satisfiesFacet = Object.keys(facet).some(value => - (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value)); - if (!satisfiesFacet) { - return false; + let childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + + const searchDocs = DocListCast(this.props.Document._searchDocs); + // if (searchDocs !== undefined && searchDocs.length > 0) { + // let newdocs: Doc[] = []; + // childDocs.forEach((el) => { + // searchDocs.includes(el) ? newdocs.push(el) : undefined; + // }); + // childDocs = newdocs; + // } + + let docsforFilter: Doc[] = childDocs; + if (searchDocs !== undefined && searchDocs.length > 0) { + docsforFilter = []; + // let newdocs: Doc[] = []; + // let newarray: Doc[] = []; + //while (childDocs.length > 0) { + //newarray = []; + childDocs.forEach((d) => { + if (d.data !== undefined) { + console.log(d); + let newdocs = DocListCast(d.data); + if (newdocs.length > 0) { + let vibecheck: 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)) { + vibecheck = true; + } + }); + newdocs = newarray; + } + if (vibecheck === true) { + docsforFilter.push(d); + } + } } - } - return true; - }) : childDocs; - const rangeFilteredDocs = filteredDocs.filter(d => { - for (let i = 0; i < docRangeFilters.length; i += 3) { - const key = docRangeFilters[i]; - const min = Number(docRangeFilters[i + 1]); - const max = Number(docRangeFilters[i + 2]); - const val = Cast(d[key], "number", null); - if (val !== undefined && (val < min || val > max)) { - return false; + if (searchDocs.includes(d)) { + docsforFilter.push(d); } - } - return true; - }); - return rangeFilteredDocs; + }); + //childDocs = newarray; + //} + } + childDocs = docsforFilter; + + + const docFilters = this.docFilters(); + const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); + + return this.props.Document.dontRegisterView ? docs : DocUtils.FilterDocs(docs, this.docFilters(), docRangeFilters, viewSpecScript); } @action @@ -211,25 +238,26 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: addDocument = (doc: Doc | Doc[]) => this.props.addDocument(doc); - @undoBatch @action protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; - ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); if (docDragData) { let added = false; - const dropaction = docDragData.dropAction || docDragData.userDropAction; - if (dropaction && dropaction !== "move") { - added = this.addDocument(docDragData.droppedDocuments); - } else if (docDragData.moveDocument) { + const dropAction = docDragData.dropAction || docDragData.userDropAction; + if ((!dropAction || dropAction === "move") && 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; - added = movedDocs.length ? docDragData.moveDocument(movedDocs, this.props.Document, this.addDocument) : res; + 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; } else { + ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); added = this.addDocument(docDragData.droppedDocuments); } - e.stopPropagation(); + added && e.stopPropagation(); return added; } else if (de.complete.annoDragData) { @@ -238,21 +266,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } return false; } - readUploadedFileAsText = (inputFile: File) => { - const temporaryFileReader = new FileReader(); - - return new Promise((resolve, reject) => { - temporaryFileReader.onerror = () => { - temporaryFileReader.abort(); - reject(new DOMException("Problem parsing input file.")); - }; - - temporaryFileReader.onload = () => { - resolve(temporaryFileReader.result); - }; - temporaryFileReader.readAsText(inputFile); - }); - } + @undoBatch @action protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) { @@ -264,6 +278,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const { dataTransfer } = e; const html = dataTransfer.getData("text/html"); const text = dataTransfer.getData("text/plain"); + const uriList = dataTransfer.getData("text/uri-list"); if (text && text.startsWith("<div")) { return; @@ -271,8 +286,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: e.stopPropagation(); e.preventDefault(); - const { addDocument } = this; - if (!addDocument) { + if (!this.addDocument) { alert("this.props.addDocument does not exist. Aborting drop operation."); return; } @@ -286,14 +300,14 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: DocServer.GetRefField(docid).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView - (f instanceof Doc) && addDocument(f); + (f instanceof Doc) && this.addDocument(f); } }); } else { - addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); + this.addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { - addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); + this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 })); } return; } @@ -313,7 +327,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: if (source.startsWith("http")) { const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 }); ImageUtils.ExtractExif(doc); - addDocument(doc); + this.addDocument(doc); } return; } else { @@ -336,15 +350,15 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const reg = new RegExp(Utils.prepend(""), "g"); const modHtml = srcUrl ? html.replace(reg, srcUrl) : html; const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: "-web page-", _width: 300, _height: 300 }); - Doc.GetProto(htmlDoc)["data-text"] = text; + 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); if (focusNode) { const rect = "getBoundingClientRect" in focusNode ? focusNode.getBoundingClientRect() : focusNode?.parentElement.getBoundingClientRect(); const x = (rect?.x || 0); - const y = NumCast(srcWeb.scrollTop) + (rect?.y || 0); - const anchor = Docs.Create.FreeformDocument([], { _LODdisable: true, _backgroundColor: "transparent", _width: 25, _height: 25, x, y, annotationOn: srcWeb }); + const y = NumCast(srcWeb._scrollTop) + (rect?.y || 0); + const anchor = Docs.Create.FreeformDocument([], { _backgroundColor: "transparent", _width: 75, _height: 40, x, y, annotationOn: srcWeb }); anchor.context = srcWeb; const key = Doc.LayoutFieldKey(srcWeb); Doc.AddDocToList(srcWeb, key + "-annotations", anchor); @@ -357,10 +371,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } } - if (text) { - if (text.includes("www.youtube.com/watch")) { - const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0]; - addDocument(Docs.Create.VideoDocument(url, { + 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, { ...options, title: url, _width: 400, @@ -384,10 +398,34 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: // if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) { // const albumId = matches[3]; // const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId); - // console.log(mediaItems); // return; // } } + if (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._width = 400; + this.addDocument(alias); + } else { + const newDoc = Docs.Create.WebDocument(uriList, { + ...options, + title: uriList.split("#annotations:")[0], + _width: 400, + _height: 315, + _nativeWidth: 850, + _nativeHeight: 962, + UseCors: true + }); + 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); + } + return; + } const { items } = e.dataTransfer; const { length } = items; @@ -403,9 +441,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.includes("uri")) { const stringContents = await new Promise<string>(resolve => item.getAsString(resolve)); - const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; + const type = (await rp.head(Utils.CorsProxy(stringContents)))["content-type"]; if (type) { - const doc = await Docs.Get.DocumentFromType(type, stringContents, options); + const doc = await DocUtils.DocumentFromType(type, Utils.CorsProxy(stringContents), options); doc && generatedDocuments.push(doc); } } @@ -413,10 +451,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const file = item.getAsFile(); file?.type && files.push(file); - file?.type === "application/json" && this.readUploadedFileAsText(file).then(result => { - console.log(result); + file?.type === "application/json" && Utils.readUploadedFileAsText(file).then(result => { const json = JSON.parse(result as string); - addDocument(Docs.Create.TreeDocument( + this.addDocument(Docs.Create.TreeDocument( json["rectangular-puzzle"].crossword.clues[0].clue.map((c: any) => { const label = Docs.Create.LabelDocument({ title: c["#text"], _width: 120, _height: 20 }); const proto = Doc.GetProto(label); @@ -428,44 +465,46 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: }); } } - for (const { source: { name, type }, result } of await Networking.UploadFilesToServer(files)) { - if (result instanceof Error) { - alert(`Upload failed: ${result.message}`); - return; - } - const full = { ...options, _width: 400, title: name }; - const pathname = Utils.prepend(result.accessPaths.agnostic.client); - const doc = await Docs.Get.DocumentFromType(type, pathname, full); - if (!doc) { - continue; - } - const proto = Doc.GetProto(doc); - proto.text = result.rawText; - proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); - if (Upload.isImageInformation(result)) { - proto["data-nativeWidth"] = (result.nativeWidth > result.nativeHeight) ? 400 * result.nativeWidth / result.nativeHeight : 400; - proto["data-nativeHeight"] = (result.nativeWidth > result.nativeHeight) ? 400 : 400 / (result.nativeWidth / result.nativeHeight); - proto.contentSize = result.contentSize; - } - generatedDocuments.push(doc); - } + this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY); + batch.end(); + } + slowLoadDocuments = async (files: File[], options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: (() => void) | undefined, clientX: number, clientY: number) => { + runInAction(() => CollectionSubViewLoader.Waiting = "block"); + const disposer = OverlayView.Instance.addElement( + <ReactLoading type={"spinningBubbles"} color={"green"} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 }); + generatedDocuments.push(...await DocUtils.uploadFilesToDocs(files, options)); if (generatedDocuments.length) { - const set = generatedDocuments.length > 1 && generatedDocuments.map(d => Doc.iconify(d)); + const set = generatedDocuments.length > 1 && generatedDocuments.map(d => DocUtils.iconify(d)); if (set) { - addDocument(Doc.pileup(generatedDocuments, options.x!, options.y!)!); + UndoManager.RunInBatch(() => this.addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!), "drop"); } else { - generatedDocuments.forEach(addDocument); + UndoManager.RunInBatch(() => generatedDocuments.forEach(this.addDocument), "drop"); } completed?.(); } else { if (text && !text.includes("https://")) { - addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); + this.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 })); } } - batch.end(); + disposer(); } } return CollectionSubView; } +export class CollectionSubViewLoader { + @observable public static Waiting = "none"; +} + +import { DragManager, dropActionType } from "../../util/DragManager"; +import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox"; +import { CollectionView, CollectionViewType } from "./CollectionView"; +import { SelectionManager } from "../../util/SelectionManager"; +import { OverlayView } from "../OverlayView"; +import { setTimeout } from "timers"; +import { Hypothesis } from "../../util/HypothesisUtils"; + diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 15bc0bfd5..c2d682361 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -19,6 +19,7 @@ const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; import React = require("react"); +import { DocUtils } from "../../documents/Documents"; @observer export class CollectionTimeView extends CollectionSubView(doc => doc) { @@ -28,7 +29,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { @observable _childClickedScript: Opt<ScriptField>; @observable _viewDefDivClick: Opt<ScriptField>; async componentDidMount() { - const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || Doc.findTemplate("detailView", StrCast(this.props.Document.type), ""); + const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.props.Document.type), ""); const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; runInAction(() => { this._childClickedScript = ScriptField.MakeScript(childText, { this: Doc.name, shiftKey: "boolean" }, { detailView: detailView! }); diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 2aac81146..c9bf82406 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -22,7 +22,7 @@ ul { list-style: none; padding-left: 20px; - margin-bottom: 1px;// otherwise vertical scrollbars may pop up for no apparent reason.... + margin-bottom: 1px; // otherwise vertical scrollbars may pop up for no apparent reason.... } @@ -35,7 +35,7 @@ width: 15px; color: $intermediate-color; margin-top: 3px; - transform: scale(1.3, 1.3); + transform: scale(1.3, 1.3); border: #80808030 1px solid; border-radius: 4px; } @@ -67,8 +67,10 @@ margin-left: 3px; display: none; } + .collectionTreeView-keyHeader:hover { background: #797777; + cursor: pointer; } .collectionTreeView-subtitle { @@ -82,8 +84,6 @@ text-overflow: ellipsis; white-space: pre-wrap; min-width: 10px; - // width:100%;//width: max-content; - } .treeViewItem-openRight { @@ -91,8 +91,10 @@ height: 17px; width: 15px; } + .treeViewItem-openRight:hover { background: #797777; + cursor: pointer; } .treeViewItem-border { @@ -100,6 +102,7 @@ border-left: dashed 1px #00000042; } +.treeViewItem-header-editing, .treeViewItem-header { border: transparent 1px solid; display: flex; @@ -107,10 +110,12 @@ .editableView-container-editing-oneLine { min-width: 15px; } + .documentView-node-topmost { width: unset; } - > svg { + + >svg { display: none; } @@ -120,7 +125,8 @@ .collectionTreeView-keyHeader { display: inherit; } - > svg { + + >svg { display: inherit; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index b2e1c0f73..3c7471d7c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -8,12 +8,12 @@ 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 } from '../../../Utils'; +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 { DragManager, dropActionType, SetupDrag } from "../../util/DragManager"; +import { DragManager, dropActionType } from "../../util/DragManager"; import { Scripting } from '../../util/Scripting'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; @@ -55,14 +55,14 @@ export interface TreeViewProps { ScreenToLocalTransform: () => Transform; backgroundColor?: (doc: Doc) => string | undefined; outerXf: () => { translateX: number, translateY: number }; - treeViewId: Doc; + 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; + onCheckedClick?: () => ScriptField; + onChildClick?: () => ScriptField; ignoreFields?: string[]; } @@ -76,30 +76,35 @@ export interface TreeViewProps { * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree */ class TreeView extends React.Component<TreeViewProps> { - static _editTitleScript: ScriptField | undefined; + 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 displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive - get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); } + 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 ? 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.props.document.treeViewOpen = this._overrideTreeViewOpen = c; } - @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && !this.props.document.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; } - @computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); } - @computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containingCollection.maxEmbedHeight, 200); } - @computed get dataDoc() { return this.props.document[DataSym]; } - @computed get fieldKey() { - const splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\'"); - return splits.length > 1 ? splits[1].split("\'")[0] : "data"; + 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.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined; + 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 ? Cast(layout[field], listSpec(Doc)) : undefined) || // else if there's a layout doc, display it's fields - Cast(this.props.document[field], listSpec(Doc))) as Doc[]; // otherwise use the document'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"); } @@ -108,22 +113,28 @@ class TreeView extends React.Component<TreeViewProps> { Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey])); } - @undoBatch openRight = () => this.props.addDocTab(this.props.document, "onRight", this.props.libraryPath); + @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.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc); + 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); + 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}'); selectDoc(self);} `); + 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)), this.props.document); + ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.doc); } onPointerEnter = (e: React.PointerEvent): void => { @@ -135,7 +146,9 @@ class TreeView extends React.Component<TreeViewProps> { } onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.dataDoc); - this._header!.current!.className = "treeViewItem-header"; + if (this._header?.current?.className !== "treeViewItem-header-editing") { + this._header!.current!.className = "treeViewItem-header"; + } document.removeEventListener("pointermove", this.onDragMove, true); } onDragMove = (e: PointerEvent): void => { @@ -143,7 +156,7 @@ class TreeView extends React.Component<TreeViewProps> { const pt = [e.clientX, e.clientY]; const rect = this._header!.current!.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; - const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length); + 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"; @@ -154,99 +167,91 @@ class TreeView extends React.Component<TreeViewProps> { editableView = (key: string, style?: string) => (<EditableView oneLine={true} display={"inline-block"} - editing={true /*this.dataDoc[Id] === EditableView.loadId*/} - contents={StrCast(this.props.document[key])} + editing={true} + contents={StrCast(this.doc[key])} height={12} + sizeToContent={true} fontStyle={style} fontSize={12} - GetValue={() => StrCast(this.props.document[key])} + GetValue={() => StrCast(this.doc[key])} SetValue={undoBatch((value: string) => { - Doc.SetInPlace(this.props.document, key, value, false) || true; - Doc.SetInPlace(this.props.document, "editTitle", undefined, false); + Doc.SetInPlace(this.doc, key, value, false) || true; + Doc.SetInPlace(this.doc, "editTitle", undefined, false); })} OnFillDown={undoBatch((value: string) => { - Doc.SetInPlace(this.props.document, key, value, false); - const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) }); - Doc.SetInPlace(this.props.document, "editTitle", undefined, false); - Doc.SetInPlace(doc, "editTitle", true, false); + 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.props.document]); + Doc.UserDoc().activeSelection = new List([this.doc]); return false; }} OnTab={undoBatch((shift?: boolean) => { - EditableView.loadId = this.dataDoc[Id]; shift ? this.props.outdentDocument?.() : this.props.indentDocument?.(); - setTimeout(() => { // unsetting/setting brushing for this doc will recreate & refocus this editableView after all other treeview changes have been made to the Dom (which may remove focus from this document). - Doc.UnBrushDoc(this.props.document); - Doc.BrushDoc(this.props.document); - EditableView.loadId = ""; - }, 0); + 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 && DocListCast(this.dataDoc[this.fieldKey]).length); - if (de.complete.linkDragData) { - const sourceDoc = de.complete.linkDragData.linkSourceDocument; - const destDoc = this.props.document; - DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link"); + 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(); } - if (de.complete.docDragData) { + const docDragData = complete.docDragData; + if (docDragData) { e.stopPropagation(); - if (de.complete.docDragData.draggedDocuments[0] === this.props.document) return true; - let addDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); + 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) || addDoc(doc); + (flg: boolean, doc) => flg && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc), true) || parentAddDoc(doc); } - const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments); - const move = de.complete.docDragData.dropAction === "move" || de.complete.docDragData.dropAction; - return ((!move && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ? - de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false) - : de.complete.docDragData.moveDocument ? - movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false) - : de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d), false); + 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; } - docTransform = () => { - const { scale, translateX, translateY } = Utils.GetScreenTransform(this._dref.current!); - const outerXf = this.props.outerXf(); - const offset = this.props.ScreenToLocalTransform().transformDirection((outerXf.translateX - translateX), outerXf.translateY - translateY); - const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); - - return finalXf; - } - getTransform = () => { - const { scale, translateX, translateY } = Utils.GetScreenTransform(this._tref.current!); + 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); - const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); - return finalXf; + return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]); } + docTransform = () => this.refTransform(this._dref.current!); + getTransform = () => this.refTransform(this._tref.current!); docWidth = () => { - const layoutDoc = Doc.Layout(this.props.document); + 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 = Doc.Layout(this.props.document); + 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.props.document.nativeHeight ? NumCast(this.props.containingCollection._height) : + 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; @@ -255,7 +260,7 @@ class TreeView extends React.Component<TreeViewProps> { @computed get expandedField() { const ids: { [key: string]: string } = {}; - const doc = this.props.document; + const doc = this.doc; doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)); const rows: JSX.Element[] = []; @@ -269,13 +274,12 @@ class TreeView extends React.Component<TreeViewProps> { 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.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, + 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" + contentElement = <EditableView key="editableView" contents={contents !== undefined ? Field.toString(contents as Field) : "null"} height={13} fontSize={12} @@ -303,44 +307,45 @@ class TreeView extends React.Component<TreeViewProps> { return rows; } - rtfWidth = () => Math.min(Doc.Layout(this.props.document)?.[WidthSym](), this.props.panelWidth() - 20); - rtfHeight = () => this.rtfWidth() < Doc.Layout(this.props.document)?.[WidthSym]() ? Math.min(Doc.Layout(this.props.document)?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; + 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 === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined; - if (expandKey !== undefined) { + const expandKey = this.treeViewExpandedView; + if (["links", 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 : this.childDocs; const sortKey = `${this.fieldKey}-sortAscending`; return <ul key={expandKey + "more"} onClick={(e) => { - this.props.document[sortKey] = (this.props.document[sortKey] ? false : (this.props.document[sortKey] === false ? undefined : true)); + this.doc[sortKey] = (this.doc[sortKey] ? false : (this.doc[sortKey] === false ? undefined : true)); e.stopPropagation(); }}> {!docs ? (null) : - TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document), + TreeView.GetChildElements(docs, this.props.treeViewDoc, this.layoutDoc, this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - StrCast(this.props.document.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, + 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.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)} + [...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><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}> + return <ul key={this.doc[Id] + this.doc.title}><div ref={this._dref} style={{ display: "inline-block" }} > {this.expandedField} </div></ul>; } else { - const layoutDoc = Doc.Layout(this.props.document); - const panelHeight = layoutDoc.type === DocumentType.RTF ? this.rtfHeight : this.docHeight; - const panelWidth = layoutDoc.type === DocumentType.RTF ? this.rtfWidth : this.docWidth; - return <div ref={this._dref} style={{ display: "inline-block", height: panelHeight() }} key={this.props.document[Id] + this.props.document.title}> + 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} @@ -350,6 +355,7 @@ class TreeView extends React.Component<TreeViewProps> { PanelHeight={panelHeight} focus={returnFalse} ScreenToLocalTransform={this.docTransform} + docFilters={returnEmptyFilter} ContainingCollectionDoc={this.props.containingCollection} ContainingCollectionView={undefined} addDocument={returnFalse} @@ -366,17 +372,17 @@ class TreeView extends React.Component<TreeViewProps> { } } - get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.props.document.onCheckedClick); } + get onCheckedClick() { return this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } @action bulletClick = (e: React.MouseEvent) => { - if (this.onCheckedClick && this.props.document.type !== DocumentType.COL) { + 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.props.document.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.props.document, + this.onCheckedClick?.script.run({ + this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, heading: this.props.containingCollection.title, - checked: this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check", - containingTreeView: this.props.treeViewId, + checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", + containingTreeView: this.props.treeViewDoc, }, console.log); } else { this.treeViewOpen = !this.treeViewOpen; @@ -384,108 +390,117 @@ class TreeView extends React.Component<TreeViewProps> { e.stopPropagation(); } - @computed - get renderBullet() { - const checked = this.props.document.type === DocumentType.COL ? undefined : this.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined; + @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.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}> + 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 ? "caret-square-right" : "caret-right") : (this.childDocs ? "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); - e.stopPropagation(); } focusOnDoc = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(doc)?.props.focus(doc, true); - contextMenuItems = () => { - const focusScript = ScriptField.MakeFunction(`DocFocus(self)`); - return [{ script: focusScript!, label: "Focus" }]; - } + 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(); - (!TreeView._editTitleScript) && (TreeView._editTitleScript = ScriptField.MakeFunction("setInPlace(self, 'editTitle', true)")); - const headerElements = ( + const headerElements = this.props.treeViewHideHeaderFields() ? (null) : <> - <FontAwesomeIcon icon="cog" size="sm" onClick={e => this.showContextMenu(e)}></FontAwesomeIcon> + <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.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" : - this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" : - this.treeViewExpandedView === "layout" && this.props.document.links ? "links" : - this.childDocs ? this.fieldKey : "fields"; + 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.childDocs ? this.fieldKey : (Doc.UserDoc().noviceMode ? "layout" : "fields"); } this.treeViewOpen = true; })}> {this.treeViewExpandedView} </span> - </>); - const openRight = (<div className="treeViewItem-openRight" onClick={this.openRight}> - <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" /> - </div>); + </>; + 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} + 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.props.document.searchMatch ? "bold" : undefined, - textDecoration: Doc.GetT(this.props.document, "title", "string", true) ? "underline" : undefined, - outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined, + 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" }} > - {Doc.GetT(this.props.document, "editTitle", "boolean", true) ? - this.editableView("title") : - <DocumentView - ref={this._docRef} - Document={this.props.document} - DataDoc={undefined} - treeViewId={this.props.treeViewId[Id]} - LibraryPath={this.props.libraryPath || emptyPath} - addDocument={undefined} - addDocTab={this.props.addDocTab} - rootSelected={returnTrue} - pinToPres={emptyFunction} - onClick={this.props.onChildClick || TreeView._editTitleScript} - dropAction={this.props.dropAction} - moveDocument={this.move} - removeDocument={this.removeDoc} - ScreenToLocalTransform={this.getTransform} - ContentScaling={returnOne} - PanelWidth={returnZero} - PanelHeight={returnZero} - NativeHeight={returnZero} - NativeWidth={returnZero} - contextMenuItems={this.contextMenuItems} - renderDepth={1} - focus={returnTrue} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildViews)} - ContainingCollectionView={undefined} - ContainingCollectionDoc={this.props.containingCollection} - />} + {view} </div > - {this.props.treeViewHideHeaderFields() ? (null) : headerElements} - {openRight} + {headerElements} + <div className="treeViewItem-openRight" onClick={this.openRight}> + <FontAwesomeIcon title="open in pane on right" icon="external-link-alt" size="sm" /> + </div> </>; } render() { TraceMobx(); - const sorting = this.props.document[`${this.fieldKey}-sortAscending`]; - //setTimeout(() => runInAction(() => untracked(() => this._overrideTreeViewOpen = this.treeViewOpen)), 0); - return <div className="treeViewItem-container" ref={this.createTreeDropTarget}> + 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() && SelectionManager.DeselectAll()}> <li className="collection-child"> - <div className="treeViewItem-header" ref={this._header} onClick={e => { + <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 => { @@ -499,14 +514,14 @@ class TreeView extends React.Component<TreeViewProps> { {this.renderTitle} </div> <div className="treeViewItem-border" style={{ borderColor: sorting === undefined ? undefined : sorting ? "crimson" : "blue" }}> - {!this.treeViewOpen || this.props.renderedIds.indexOf(this.props.document[Id]) !== -1 ? (null) : this.renderContent} + {!this.treeViewOpen || this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (null) : this.renderContent} </div> </li> </div>; } public static GetChildElements( childDocs: Doc[], - treeViewId: Doc, + treeViewDoc: Doc, containingCollection: Doc, dataDoc: Doc | undefined, key: string, @@ -529,8 +544,8 @@ class TreeView extends React.Component<TreeViewProps> { treeViewPreventOpen: boolean, renderedIds: string[], libraryPath: Doc[] | undefined, - onCheckedClick: ScriptField | undefined, - onChildClick: ScriptField | undefined, + onCheckedClick: undefined | (() => ScriptField), + onChildClick: undefined | (() => ScriptField), ignoreFields: string[] | undefined ) { const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField); @@ -616,7 +631,7 @@ class TreeView extends React.Component<TreeViewProps> { libraryPath={libraryPath ? [...libraryPath, containingCollection] : undefined} containingCollection={containingCollection} prevSibling={docs[i]} - treeViewId={treeViewId} + treeViewDoc={treeViewDoc} key={child[Id]} indentDocument={indent} outdentDocument={outdent} @@ -648,8 +663,8 @@ class TreeView extends React.Component<TreeViewProps> { export type collectionTreeViewProps = { treeViewHideTitle?: boolean; treeViewHideHeaderFields?: boolean; - onCheckedClick?: ScriptField; - onChildClick?: ScriptField; + onCheckedClick?: () => ScriptField; + onChildClick?: () => ScriptField; }; @observer @@ -657,12 +672,22 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; - @computed get dataDoc() { return this.props.DataDoc || this.props.Document; } + @computed get doc() { 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.props.Document); + this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); + } + } + + protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + const dragData = de.complete.docDragData; + if (dragData) { + if (targetAction && !dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d))) { + dragData.dropAction = targetAction; + } else dragData.dropAction = this.doc === dragData?.treeViewDoc ? "same" : dragData.dropAction; } } @@ -674,7 +699,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll @action remove = (doc: Doc | Doc[]): boolean => { const docs = doc instanceof Doc ? [doc] : doc; - const targetDataDoc = this.props.Document[DataSym]; + const targetDataDoc = this.doc[DataSym]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const result = value.filter(v => !docs.includes(v)); if (result.length !== value.length) { @@ -687,9 +712,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll addDoc = (doc: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { const doAddDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => - flg && Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false), true); - if (this.props.Document.resolvedDataDoc instanceof Promise) { - this.props.Document.resolvedDataDoc.then((resolved: any) => doAddDoc(doc)); + 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 { doAddDoc(doc); } @@ -697,25 +722,26 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.props.Document === Doc.UserDoc().myWorkspaces) { + 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.props.Document), icon: "minus" }); + 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.props.Document === Doc.UserDoc().myRecentlyClosed) { + } 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 { const layoutItems: ContextMenuProps[] = []; - layoutItems.push({ description: (this.props.Document.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.treeViewPreventOpen = !this.props.Document.treeViewPreventOpen, icon: "paint-brush" }); - layoutItems.push({ description: (this.props.Document.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.treeViewHideHeaderFields = !this.props.Document.treeViewHideHeaderFields, icon: "paint-brush" }); - layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" }); + 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" }); } - ContextMenu.Instance.addItem({ + !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: "Buxton Layout", icon: "eye", event: () => { const { ImageDocument, PdfDocument } = Docs.Create; const { Document } = this.props; @@ -728,7 +754,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll const fallback = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target let pdfContent: string; - DocListCast(this.dataDoc[this.props.fieldKey]).map(d => { + this.childDocs?.map(d => { DocListCast(d.data).map((img, i) => { const caption = (d.captions as any)[i]; if (caption) { @@ -757,9 +783,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ - description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => Doc.makeCustomViewClicked(this.props.Document, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" + description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + !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, {}); @@ -767,60 +793,61 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll @computed get renderClearButton() { return <div id="toolbar" key="toolbar"> <button className="toolbar-button round-button" title="Empty" - onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}> + onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}> <FontAwesomeIcon icon={"trash"} size="sm" /> </button> </div >; } - onKeyPress = (e: React.KeyboardEvent) => { - console.log(e); + onChildClick = () => { + return this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); } render() { - if (!(this.props.Document instanceof Doc)) return (null); - const dropAction = StrCast(this.props.Document.childDropAction) as dropActionType; + 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) : ( - <div className="collectionTreeView-dropTarget" id="body" - style={{ - background: this.props.backgroundColor?.(this.props.Document), - paddingLeft: `${NumCast(this.props.Document._xPadding, 10)}px`, - paddingRight: `${NumCast(this.props.Document._xPadding, 10)}px`, - paddingTop: `${NumCast(this.props.Document._yPadding, 20)}px` - }} - onKeyPress={this.onKeyPress} - onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} - onDrop={this.onTreeDrop} - ref={this.createTreeDropTarget}> - {(this.props.treeViewHideTitle || this.props.Document.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, _LODdisable: true, templates: new List<string>([Templates.Title.Layout]) }); - EditableView.loadId = doc[Id]; - Doc.SetInPlace(doc, "editTitle", true, false); - this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true); - })} />)} - {this.props.Document.allowClear ? this.renderClearButton : (null)} - <ul className="no-indent" style={{ width: "max-content" }} > - { - TreeView.GetChildElements(childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, - this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.props.Document.treeViewHideHeaderFields), - BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick, - this.props.onChildClick || ScriptCast(this.props.Document.onChildClick), this.props.ignoreFields) - } - </ul> - </div > + <div className="collectionTreeView-container" onContextMenu={this.onContextMenu}> + <div className="collectionTreeView-dropTarget" id="body" + style={{ + background: this.props.backgroundColor?.(this.doc), + 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 + }} + 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) + } + </ul> + </div > + </div> ); } } diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index 7877fe155..a5aef86de 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -11,17 +11,20 @@ height: 100%; overflow: hidden; // bcz: used to be 'auto' which would create scrollbars when there's a floating doc that's not visible. not sure if that's better, but the scrollbars are annoying... - .collectionTimeView-dragger { - background-color: lightgray; + .collectionView-filterDragger { + background-color: rgb(140, 139, 139); height: 40px; - width: 20px; + width: 10px; position: absolute; - border-radius: 10px; top: 55%; border: 1px black solid; + border-radius: 0; + border-top-left-radius: 20px; + border-bottom-left-radius: 20px; + border-right: unset; z-index: 2; - right: -10px; } + .collectionTimeView-treeView { display: flex; flex-direction: column; @@ -30,6 +33,8 @@ position: absolute; right: 0; top: 0; + border-left: solid 1px; + z-index: 1; .collectionTimeView-addfacet { display: inline-block; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index fecba32c5..837ae7e86 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,32 +1,43 @@ import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEye, faEdit } from '@fortawesome/free-regular-svg-icons'; +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 { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree, faGlobeAmericas } from '@fortawesome/free-solid-svg-icons'; -import { action, observable, computed } from 'mobx'; +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 { DataSym, Doc, DocListCast, Field, Opt, AclSym, AclAddonly, AclReadonly } from '../../../fields/Doc'; +import { AclAddonly, AclReadonly, DataSym, Doc, DocListCast, Field, Opt, AclEdit, AclSym, AclPrivate, AclAdmin } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; -import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Types'; +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 { ImageField } from '../../../fields/URLField'; -import { TraceMobx } from '../../../fields/util'; -import { Utils, setupMoveUpEvents, returnFalse, returnZero, emptyPath, emptyFunction, returnOne } from '../../../Utils'; +import { TraceMobx, GetEffectiveAcl, SharingPermissions, distributeAcls } from '../../../fields/util'; +import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; +import { InteractionUtils } from '../../util/InteractionUtils'; +import { UndoManager } from '../../util/UndoManager'; import { ContextMenu } from "../ContextMenu"; import { FieldView, FieldViewProps } from '../nodes/FieldView'; -import { ScriptBox } from '../ScriptBox'; import { Touchable } from '../Touchable'; +import { CollectionCarousel3DView } from './CollectionCarousel3DView'; import { CollectionCarouselView } from './CollectionCarouselView'; import { CollectionDockingView } from "./CollectionDockingView"; -import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { CollectionGridView } from './collectionGrid/CollectionGridView'; import { CollectionLinearView } from './CollectionLinearView'; +import CollectionMapView from './CollectionMapView'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; +import { CollectionPileView } from './CollectionPileView'; import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionStaffView } from './CollectionStaffView'; @@ -34,21 +45,13 @@ import { SubCollectionViewProps } from './CollectionSubView'; import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; -import { CollectionViewBaseChrome } from './CollectionViewChromes'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; -import { Id } from '../../../fields/FieldSymbols'; -import { listSpec } from '../../../fields/Schema'; -import { Docs } from '../../documents/Documents'; -import { ScriptField, ComputedField } from '../../../fields/ScriptField'; -import { InteractionUtils } from '../../util/InteractionUtils'; -import { ObjectField } from '../../../fields/ObjectField'; -import CollectionMapView from './CollectionMapView'; -import { CollectionPileView } from './CollectionPileView'; +import { ContextMenuProps } from '../ContextMenuItem'; 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 { @@ -63,16 +66,19 @@ export enum CollectionViewType { Multirow = "multirow", Time = "time", Carousel = "carousel", + Carousel3D = "3D Carousel", Linear = "linear", Staff = "staff", Map = "map", + Grid = "grid", Pile = "pileup" } 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) + 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; } export interface CollectionRenderProps { @@ -82,6 +88,7 @@ export interface CollectionRenderProps { active: () => boolean; whenActiveChanged: (isActive: boolean) => void; PanelWidth: () => number; + PanelHeight: () => number; ChildLayoutTemplate?: () => Doc; ChildLayoutString?: string; } @@ -90,19 +97,27 @@ export interface CollectionRenderProps { export class CollectionView extends Touchable<FieldViewProps & CollectionViewCustomProps> { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } - private _isChildActive = false; //TODO should this be observable? + _isChildActive = false; //TODO should this be observable? get _isLightboxOpen() { return BoolCast(this.props.Document.isLightboxOpen); } set _isLightboxOpen(value) { this.props.Document.isLightboxOpen = value; } @observable private _curLightboxImg = 0; @observable private static _safeMode = false; public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } - protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + + private AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit], + [AclAdmin, SharingPermissions.Admin] + ]); get collectionViewType(): CollectionViewType | undefined { const viewField = StrCast(this.props.Document._viewType); if (CollectionView._safeMode) { - if (viewField === CollectionViewType.Freeform) { + if (viewField === CollectionViewType.Freeform || viewField === CollectionViewType.Schema) { return CollectionViewType.Tree; } if (viewField === CollectionViewType.Invalid) { @@ -121,20 +136,53 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus if (this.props.filterAddDocument?.(doc) === false) { return false; } + const docs = doc instanceof Doc ? [doc] : doc; const targetDataDoc = this.props.Document[DataSym]; const docList = DocListCast(targetDataDoc[this.props.fieldKey]); const added = docs.filter(d => !docList.includes(d)); + const effectiveAcl = GetEffectiveAcl(this.props.Document); + if (added.length) { - if (this.dataDoc[AclSym] === AclReadonly) { + if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { return false; - } else if (this.dataDoc[AclSym] === AclAddonly) { - added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc)); - } else { - added.map(doc => 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 + "-lastModified"] = new DateField(new Date(Date.now())); + } + else { + if (this.props.Document[AclSym]) { + added.forEach(d => { + for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + if (d.author === Doc.CurrentUserEmail && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, 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)); + } + else { + added.map(doc => { + const context = Cast(doc.context, Doc, null); + if (context && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { + const pushpin = Docs.Create.FontIconDocument({ + title: "pushpin", label: "", + icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), _backgroundColor: "#0000003d", color: "#ACCEF7", + _width: 15, _height: 15, _xPadding: 0, isLinkButton: true, displayTimecode: Cast(doc.displayTimecode, "number", null) + }); + pushpin.isPushpin = true; + Doc.GetProto(pushpin).annotationOn = doc.annotationOn; + Doc.SetInPlace(doc, "annotationOn", undefined, true); + Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); + const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); + doc.displayTimecode = 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())); + } } } return true; @@ -142,13 +190,18 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @action.bound removeDocument = (doc: any): boolean => { - const docs = doc instanceof Doc ? [doc] : doc as Doc[]; - const targetDataDoc = this.props.Document[DataSym]; - const value = DocListCast(targetDataDoc[this.props.fieldKey]); - const result = value.filter(v => !docs.includes(v)); - if (result.length !== value.length) { - targetDataDoc[this.props.fieldKey] = new List<Doc>(result); - return true; + const collectionEffectiveAcl = GetEffectiveAcl(this.props.Document); + const docEffectiveAcl = GetEffectiveAcl(doc); + // you can remove the document if you either have Admin/Edit access to the collection or to the specific document + if (collectionEffectiveAcl === AclEdit || collectionEffectiveAcl === AclAdmin || docEffectiveAcl === AclAdmin || docEffectiveAcl === AclEdit) { + 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) { + toRemove.forEach(doc => Doc.RemoveDocFromList(targetDataDoc, this.props.fieldKey, doc)); + return true; + } } return false; } @@ -156,18 +209,29 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus // this is called with the document that was dragged and the collection to move it into. // if the target collection is the same as this collection, then the move will be allowed. // otherwise, the document being moved must be able to be removed from its container before - // moving it into the target. + // moving it into the target. @action.bound moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => { if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { return true; } - return this.removeDocument(doc) ? addDocument(doc) : false; + const first = doc instanceof Doc ? doc : doc[0]; + if (!first?.stayInCollection && addDocument !== returnFalse) { + if (UndoManager.RunInTempBatch(() => this.removeDocument(doc))) { + const added = addDocument(doc); + if (!added) UndoManager.UndoTempBatch(); + else UndoManager.ClearTempBatch(); + + return added; + } + UndoManager.ClearTempBatch(); + } + return false; } showIsTagged = () => { return (null); - // this section would display an icon in the bototm right of a collection to indicate that all + // this section would display an icon in the bototm right of a collection to indicate that all // photos had been processed through Google's content analysis API and Google's tags had been // assigned to the documents googlePhotosTags field. // const children = DocListCast(this.props.Document[this.props.fieldKey]); @@ -176,8 +240,9 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus // return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />; } + screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - const props: SubCollectionViewProps = { ...this.props, ...renderProps, CollectionView: this, annotationsKey: "" }; + const props: SubCollectionViewProps = { ...this.props, ...renderProps, ScreenToLocalTransform: this.screenToLocalTransform, CollectionView: this, annotationsKey: "" }; switch (type) { case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />); case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />); @@ -188,32 +253,25 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); } case CollectionViewType.Pile: { return (<CollectionPileView key="collview" {...props} />); } case CollectionViewType.Carousel: { return (<CollectionCarouselView key="collview" {...props} />); } - case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); } - case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); } + case CollectionViewType.Carousel3D: { return (<CollectionCarousel3DView key="collview" {...props} />); } + case CollectionViewType.Stacking: { this.props.Document._columnsStack = true; return (<CollectionStackingView key="collview" {...props} />); } + case CollectionViewType.Masonry: { this.props.Document._columnsStack = false; return (<CollectionStackingView key="collview" {...props} />); } case CollectionViewType.Time: { return (<CollectionTimeView key="collview" {...props} />); } 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} />); } } } - @action - private collapse = (value: boolean) => { - this.props.Document._chromeStatus = value ? "collapsed" : "enabled"; - } - private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => { - // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip - const chrome = this.props.Document._chromeStatus === "disabled" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) : - <CollectionViewBaseChrome key="chrome" CollectionView={this} PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />; - return <>{chrome} {this.SubViewHelper(type, renderProps)}</>; + return this.SubViewHelper(type, renderProps); } setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc, addExtras: boolean) { - const existingVm = ContextMenu.Instance.findByDescription(category); - const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; + const subItems: ContextMenuProps[] = []; subItems.push({ description: "Freeform", event: () => func(CollectionViewType.Freeform), icon: "signature" }); if (addExtras && CollectionView._safeMode) { ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => func(CollectionViewType.Invalid), icon: "project-diagram" }); @@ -227,60 +285,67 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus 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" }); - if (addExtras && this.props.Document._viewType === CollectionViewType.Freeform) { - subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); - } + subItems.push({ description: "Grid", event: () => func(CollectionViewType.Grid), icon: "th-list" }); addExtras && subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" }); - !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: subItems, 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" }); } onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - - this.setupViewTypes("Add a Perspective...", vtype => { + const cm = ContextMenu.Instance; + if (cm && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + this.setupViewTypes("UI Controls...", vtype => { const newRendition = Doc.MakeAlias(this.props.Document); newRendition._viewType = vtype; this.props.addDocTab(newRendition, "onRight"); return newRendition; }, false); - const existing = ContextMenu.Instance.findByDescription("Options..."); - const layoutItems = existing && "subitems" in existing ? existing.subitems : []; - layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); + const options = cm.findByDescription("Options..."); + 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) { - layoutItems.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, "onRight"), icon: "project-diagram" }); } if (this.props.Document.childClickedOpenTemplateView instanceof Doc) { - layoutItems.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, "onRight"), icon: "project-diagram" }); } - layoutItems.push({ description: `${this.props.Document.isInPlaceContainer ? "Unset" : "Set"} inPlace Container`, event: () => this.props.Document.isInPlaceContainer = !this.props.Document.isInPlaceContainer, icon: "project-diagram" }); + !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" }); - !existing && ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "hand-point-right" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" }); - const existingOnClick = ContextMenu.Instance.findByDescription("OnClick..."); + 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) => { - ScriptBox.EditButtonScript(func.name + "...", this.props.Document, func.key, obj.x, obj.y, { thisContainer: Doc.name }); + 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: () => this.props.Document[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), + event: () => Doc.GetProto(this.props.Document)[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)), })); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + !existingOnClick && cm.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); - const more = ContextMenu.Instance.findByDescription("More..."); - const moreItems = more && "subitems" in more ? more.subitems : []; - moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); - !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + if (!Doc.UserDoc().noviceMode) { + const more = cm.findByDescription("More..."); + const moreItems = more && "subitems" in more ? more.subitems : []; + moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); + !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + } } } @@ -303,7 +368,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus get _facetWidth() { return NumCast(this.props.Document._facetWidth); } set _facetWidth(value) { this.props.Document._facetWidth = value; } - bodyPanelWidth = () => this.props.PanelWidth() - this.facetWidth(); + bodyPanelWidth = () => this.props.PanelWidth(); facetWidth = () => Math.max(0, Math.min(this.props.PanelWidth() - 25, this._facetWidth)); @computed get dataDoc() { @@ -334,10 +399,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; } @computed get _allFacets() { - const facets = new Set<string>(); - this.childDocs.filter(child => child).forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key))); + 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); + return Array.from(facets).filter(f => !f.startsWith("_") && !["proto", "zIndex", "isPrototype", "context", "text-noTemplate"].includes(f)).sort(); } /** @@ -364,8 +430,13 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } } else { const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]); - const facetValues = Array.from(allCollectionDocs.reduce((set, child) => - set.add(Field.toString(child[facetHeader] as Field)), new Set<string>())); + 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; @@ -379,13 +450,18 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } }); let newFacet: Opt<Doc>; - if (nonNumbers / allCollectionDocs.length < .1) { - newFacet = Docs.Create.SliderDocument({ title: facetHeader }); + 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 - newFacet.treeViewExpandedView = "layout"; - newFacet.treeViewOpen = true; 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]; @@ -395,7 +471,6 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus 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(); @@ -413,15 +488,18 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus 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)); + }), 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)"; - return ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name }); + 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()}> @@ -444,10 +522,12 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus </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} @@ -470,7 +550,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus ContentScaling={returnOne} focus={returnFalse} treeViewHideHeaderFields={true} - onCheckedClick={this.scriptField!} + onCheckedClick={this.scriptField} ignoreFields={this.ignoreFields} annotationsKey={""} dontRegisterView={true} @@ -481,6 +561,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus </div> </div>; } + childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null); childLayoutString = this.props.childLayoutString || StrCast(this.props.Document.childLayoutString); @@ -493,18 +574,16 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus active: this.active, whenActiveChanged: this.whenActiveChanged, PanelWidth: this.bodyPanelWidth, + PanelHeight: this.props.PanelHeight, ChildLayoutTemplate: this.childLayoutTemplate, ChildLayoutString: this.childLayoutString, }; - return (<div className={"collectionView"} - style={{ - pointerEvents: this.props.Document.isBackground ? "none" : undefined, - 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")}` - }} - onContextMenu={this.onContextMenu}> + 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")}`; + return (<div className={"collectionView"} onContextMenu={this.onContextMenu} + style={{ pointerEvents: this.props.Document.isBackground ? "none" : undefined, boxShadow }}> {this.showIsTagged()} - <div className="collectionView-facetCont" style={{ width: `calc(100% - ${this.facetWidth()}px)` }}> + <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 => @@ -513,10 +592,11 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href : ""))} - {!this.props.isSelected() || this.props.PanelHeight() < 100 || this.props.Document.hideFilterView ? (null) : - <div className="collectionTimeView-dragger" title="library View Dragger" onPointerDown={this.onPointerDown} style={{ right: this.facetWidth() - 10 }} /> + {(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%" }} /> } - {this.facetWidth() < 10 ? (null) : this.filterView} + {Doc.UserDoc()?.noviceMode || this.facetWidth() < 10 ? (null) : this.filterView} </div>); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx deleted file mode 100644 index 29a3e559a..000000000 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ /dev/null @@ -1,564 +0,0 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc, DocListCast } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { List } from "../../../fields/List"; -import { listSpec } from "../../../fields/Schema"; -import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { Utils, emptyFunction, setupMoveUpEvents } from "../../../Utils"; -import { DragManager } from "../../util/DragManager"; -import { undoBatch } from "../../util/UndoManager"; -import { EditableView } from "../EditableView"; -import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss"; -import { CollectionViewType } from "./CollectionView"; -import { CollectionView } from "./CollectionView"; -import "./CollectionViewChromes.scss"; -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -const datepicker = require('js-datepicker'); - -interface CollectionViewChromeProps { - CollectionView: CollectionView; - type: CollectionViewType; - collapse?: (value: boolean) => any; - PanelWidth: () => number; -} - -interface Filter { - key: string; - value: string; - contains: boolean; -} - -const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); - -@observer -export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> { - //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) - - get target() { return this.props.CollectionView.props.Document; } - _templateCommand = { - params: ["target", "source"], title: "=> item view", - script: "this.target.childLayout = getDocTemplate(this.source?.[0])", - immediate: (source: Doc[]) => this.target.childLayout = Doc.getDocTemplate(source?.[0]), - initialize: emptyFunction, - }; - _narrativeCommand = { - params: ["target", "source"], title: "=> child click view", - script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])", - immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]), - initialize: emptyFunction, - }; - _contentCommand = { - params: ["target", "source"], title: "=> content", - script: "getProto(this.target).data = copyField(this.source);", - immediate: (source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source), // Doc.aliasDocs(source), - initialize: emptyFunction, - }; - _viewCommand = { - params: ["target"], title: "=> saved view", - script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", - immediate: (source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; }, - initialize: (button: Doc) => { button.restoredPanX = this.target._panX; button.restoredPanY = this.target._panY; button.restoredScale = this.target.scale; }, - }; - _freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand]; - _stacking_commands = [this._contentCommand, this._templateCommand]; - _masonry_commands = [this._contentCommand, this._templateCommand]; - _schema_commands = [this._templateCommand, this._narrativeCommand]; - _tree_commands = []; - private get _buttonizableCommands() { - switch (this.props.type) { - case CollectionViewType.Tree: return this._tree_commands; - case CollectionViewType.Schema: return this._schema_commands; - case CollectionViewType.Stacking: return this._stacking_commands; - case CollectionViewType.Masonry: return this._stacking_commands; - case CollectionViewType.Freeform: return this._freeform_commands; - case CollectionViewType.Time: return this._freeform_commands; - case CollectionViewType.Carousel: return this._freeform_commands; - } - return []; - } - private _picker: any; - private _commandRef = React.createRef<HTMLInputElement>(); - private _viewRef = React.createRef<HTMLInputElement>(); - @observable private _currentKey: string = ""; - - componentDidMount = action(() => { - // chrome status is one of disabled, collapsed, or visible. this determines initial state from document - switch (this.props.CollectionView.props.Document._chromeStatus) { - case "disabled": - throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!"); - case "collapsed": - this.props.collapse?.(true); - break; - } - }) - - @undoBatch - viewChanged = (e: React.ChangeEvent) => { - //@ts-ignore - this.document._viewType = e.target.selectedOptions[0].value; - } - - commandChanged = (e: React.ChangeEvent) => { - //@ts-ignore - 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; - } - - // @action - // openDatePicker = (e: React.PointerEvent) => { - // if (this._picker) { - // this._picker.alwaysShow = true; - // this._picker.show(); - // // TODO: calendar is offset when zoomed in/out - // // this._picker.calendar.style.position = "absolute"; - // // let transform = this.props.CollectionView.props.ScreenToLocalTransform(); - // // let x = parseInt(this._picker.calendar.style.left) / transform.Scale; - // // let y = parseInt(this._picker.calendar.style.top) / transform.Scale; - // // this._picker.calendar.style.left = x; - // // this._picker.calendar.style.top = y; - - // e.stopPropagation(); - // } - // } - - // <input className="collectionViewBaseChrome-viewSpecsMenu-rowRight" - // id={Utils.GenerateGuid()} - // ref={this.datePickerRef} - // value={this._dateValue instanceof Date ? this._dateValue.toLocaleDateString() : this._dateValue} - // onChange={(e) => runInAction(() => this._dateValue = e.target.value)} - // onPointerDown={this.openDatePicker} - // placeholder="Value" /> - // @action.bound - // applyFilter = (e: React.MouseEvent) => { - // const keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")"; - // const yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0; - // const monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0; - // const weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0; - // const dayOffset = (this._dateWithinValue[1] === 'd' ? parseInt(this._dateWithinValue[0]) : 0) + weekOffset * 7; - // let dateRestrictionScript = ""; - // if (this._dateValue instanceof Date) { - // const lowerBound = new Date(this._dateValue.getFullYear() - yearOffset, this._dateValue.getMonth() - monthOffset, this._dateValue.getDate() - dayOffset); - // const upperBound = new Date(this._dateValue.getFullYear() + yearOffset, this._dateValue.getMonth() + monthOffset, this._dateValue.getDate() + dayOffset + 1); - // dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; - // } - // else { - // const createdDate = new Date(this._dateValue); - // if (!isNaN(createdDate.getTime())) { - // const lowerBound = new Date(createdDate.getFullYear() - yearOffset, createdDate.getMonth() - monthOffset, createdDate.getDate() - dayOffset); - // const upperBound = new Date(createdDate.getFullYear() + yearOffset, createdDate.getMonth() + monthOffset, createdDate.getDate() + dayOffset + 1); - // dateRestrictionScript = `((doc.creationDate as any).date >= ${lowerBound.valueOf()} && (doc.creationDate as any).date <= ${upperBound.valueOf()})`; - // } - // } - // const fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ? - // `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` : - // `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` : - // "true"; - - // this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name }); - // } - - // datePickerRef = (node: HTMLInputElement) => { - // if (node) { - // try { - // this._picker = datepicker("#" + node.id, { - // disabler: (date: Date) => date > new Date(), - // onSelect: (instance: any, date: Date) => runInAction(() => {}), // this._dateValue = date), - // dateSelected: new Date() - // }); - // } catch (e) { - // console.log("date picker exception:" + e); - // } - // } - // } - - - @action - toggleCollapse = () => { - this.document._chromeStatus = this.document._chromeStatus === "enabled" ? "collapsed" : "enabled"; - if (this.props.collapse) { - this.props.collapse(this.props.CollectionView.props.Document._chromeStatus !== "enabled"); - } - } - - @computed get subChrome() { - const collapsed = this.document._chromeStatus !== "enabled"; - if (collapsed) return null; - switch (this.props.type) { - case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - default: return null; - } - } - - private get document() { - return this.props.CollectionView.props.Document; - } - - private dropDisposer?: DragManager.DragDropDisposer; - protected createDropTarget = (ele: HTMLDivElement) => { - this.dropDisposer?.(); - if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.document); - } - } - - @undoBatch - @action - protected drop(e: Event, de: DragManager.DropEvent): boolean { - if (de.complete.docDragData && de.complete.docDragData.draggedDocuments.length) { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData?.draggedDocuments || [])); - e.stopPropagation(); - } - return true; - } - - dragViewDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e, down, delta) => { - const vtype = this.props.CollectionView.collectionViewType; - const c = { - params: ["target"], title: vtype, - script: `this.target._viewType = '${StrCast(this.props.CollectionView.props.Document._viewType)}'`, - immediate: (source: Doc[]) => this.props.CollectionView.props.Document._viewType = Doc.getDocTemplate(source?.[0]), - initialize: emptyFunction, - }; - DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), - { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY); - return true; - }, emptyFunction, emptyFunction); - } - dragCommandDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, (e, down, delta) => { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => - DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title, - { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY)); - return true; - }, emptyFunction, emptyFunction); - } - - @computed get templateChrome() { - const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; - return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}> - <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}> - <div className="commandEntry-drop"> - <FontAwesomeIcon icon="bullseye" size="2x" /> - </div> - <select - className="collectionViewBaseChrome-cmdPicker" - onPointerDown={stopPropagation} - onChange={this.commandChanged} - value={this._currentKey}> - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} /> - {this._buttonizableCommands.map(cmd => - <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option> - )} - </select> - </div> - </div>; - } - - @computed get viewModes() { - const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; - return <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}> - <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}> - <div className="commandEntry-drop"> - <FontAwesomeIcon icon="bullseye" size="2x" /> - </div> - <select - className="collectionViewBaseChrome-viewPicker" - onPointerDown={stopPropagation} - onChange={this.viewChanged} - value={StrCast(this.props.CollectionView.props.Document._viewType)}> - {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : ( - <option - key={Utils.GenerateGuid()} - className="collectionViewBaseChrome-viewOption" - onPointerDown={stopPropagation} - value={type}> - {type[0].toUpperCase() + type.substring(1)} - </option> - ))} - </select> - </div> - </div>; - } - - render() { - const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; - const scale = Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale); - return ( - <div className="collectionViewChrome-cont" style={{ - top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined, - transform: collapsed ? "" : `scale(${scale})`, - width: `${this.props.PanelWidth() / scale}px` - }}> - <div className="collectionViewChrome" style={{ border: "unset", pointerEvents: collapsed ? "none" : undefined }}> - <div className="collectionViewBaseChrome"> - <button className="collectionViewBaseChrome-collapse" - style={{ - top: collapsed ? 70 : 10, - transform: `rotate(${collapsed ? 180 : 0}deg) scale(0.5) translate(${collapsed ? "-100%, -100%" : "0, 0"})`, - opacity: 0.9, - display: (collapsed && !this.props.CollectionView.props.isSelected()) ? "none" : undefined, - left: (collapsed ? 0 : "unset"), - }} - title="Collapse collection chrome" onClick={this.toggleCollapse}> - <FontAwesomeIcon icon="caret-up" size="2x" /> - </button> - {this.viewModes} - <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}> - <div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.toggleViewSpecs} > - <FontAwesomeIcon icon="filter" size="2x" /> - </div> - </div> - {this.templateChrome} - </div> - {this.subChrome} - </div> - </div> - ); - } -} - -@observer -export class CollectionFreeFormViewChrome extends React.Component<CollectionViewChromeProps> { - - get Document() { return this.props.CollectionView.props.Document; } - @computed get dataField() { - return this.props.CollectionView.props.Document[Doc.LayoutFieldKey(this.props.CollectionView.props.Document)]; - } - @computed get childDocs() { - return DocListCast(this.dataField); - } - @undoBatch - @action - nextKeyframe = (): void => { - const currentFrame = NumCast(this.Document.currentFrame); - if (currentFrame === undefined) { - 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)); - } - @undoBatch - @action - prevKeyframe = (): void => { - const currentFrame = NumCast(this.Document.currentFrame); - if (currentFrame === undefined) { - this.Document.currentFrame = 0; - CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); - } - CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice()); - this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1); - } - render() { - return this.Document.isAnnotationOverlay ? (null) : - <div className="collectionFreeFormViewChrome-cont"> - <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}> - <FontAwesomeIcon icon={"caret-left"} size={"lg"} /> - </div> - <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }} - onClick={action(() => this.Document.editing = !this.Document.editing)} > - {NumCast(this.Document.currentFrame)} - </div> - <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}> - <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> - </div> - </div>; - } -} - -@observer -export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> { - @observable private _currentKey: string = ""; - @observable private suggestions: string[] = []; - - @computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); } - @computed get pivotField() { return StrCast(this.props.CollectionView.props.Document._pivotField); } - - getKeySuggestions = async (value: string): Promise<string[]> => { - value = value.toLowerCase(); - const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); - if (docs instanceof Doc) { - return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value)); - } else { - const keys = new Set<string>(); - docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); - } - } - - @action - onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => { - this._currentKey = newValue; - } - - getSuggestionValue = (suggestion: string) => suggestion; - - renderSuggestion = (suggestion: string) => { - return <p>{suggestion}</p>; - } - - onSuggestionFetch = async ({ value }: { value: string }) => { - const sugg = await this.getKeySuggestions(value); - runInAction(() => { - this.suggestions = sugg; - }); - } - - @action - onSuggestionClear = () => { - this.suggestions = []; - } - - @action - setValue = (value: string) => { - this.props.CollectionView.props.Document._pivotField = value; - return true; - } - - @action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; }; - @action resetValue = () => { this._currentKey = this.pivotField; }; - - render() { - return ( - <div className="collectionStackingViewChrome-cont"> - <div className="collectionStackingViewChrome-pivotField-cont"> - <div className="collectionStackingViewChrome-pivotField-label"> - GROUP BY: - </div> - <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}> - <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> - </div> - <div className="collectionStackingViewChrome-pivotField"> - <EditableView - GetValue={() => this.pivotField} - autosuggestProps={ - { - resetValue: this.resetValue, - value: this._currentKey, - onChange: this.onKeyChange, - autosuggestProps: { - inputProps: - { - value: this._currentKey, - onChange: this.onKeyChange - }, - getSuggestionValue: this.getSuggestionValue, - suggestions: this.suggestions, - alwaysRenderSuggestions: true, - renderSuggestion: this.renderSuggestion, - onSuggestionsFetchRequested: this.onSuggestionFetch, - onSuggestionsClearRequested: this.onSuggestionClear - } - }} - oneLine - SetValue={this.setValue} - contents={this.pivotField ? this.pivotField : "N/A"} - /> - </div> - </div> - </div> - ); - } -} - - -@observer -export class CollectionSchemaViewChrome extends React.Component<CollectionViewChromeProps> { - // private _textwrapAllRows: boolean = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; - - @undoBatch - togglePreview = () => { - const dividerWidth = 4; - const borderWidth = Number(COLLECTION_BORDER_WIDTH); - const panelWidth = this.props.CollectionView.props.PanelWidth(); - const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); - const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth; - this.props.CollectionView.props.Document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0; - } - - @undoBatch - @action - toggleTextwrap = async () => { - const textwrappedRows = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []); - if (textwrappedRows.length) { - this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]); - } else { - const docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]); - const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); - this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows); - } - } - - - render() { - const previewWidth = NumCast(this.props.CollectionView.props.Document.schemaPreviewWidth); - const textWrapped = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; - - return ( - <div className="collectionSchemaViewChrome-cont"> - <div className="collectionSchemaViewChrome-toggle"> - <div className="collectionSchemaViewChrome-label">Show Preview: </div> - <div className="collectionSchemaViewChrome-toggler" onClick={this.togglePreview}> - <div className={"collectionSchemaViewChrome-togglerButton" + (previewWidth !== 0 ? " on" : " off")}> - {previewWidth !== 0 ? "on" : "off"} - </div> - </div> - </div> - </div > - ); - } -} - -@observer -export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> { - - get sortAscending() { - return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"]; - } - set sortAscending(value) { - this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"] = value; - } - @computed private get ascending() { - return Cast(this.sortAscending, "boolean", null); - } - - @action toggleSort = () => { - if (this.sortAscending) this.sortAscending = undefined; - else if (this.sortAscending === undefined) this.sortAscending = false; - else this.sortAscending = true; - } - - render() { - return ( - <div className="collectionTreeViewChrome-cont"> - <button className="collectionTreeViewChrome-sort" onClick={this.toggleSort}> - <div className="collectionTreeViewChrome-sortLabel"> - Sort - </div> - <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.ascending === undefined ? "90" : this.ascending ? "180" : "0"}deg)` }}> - <FontAwesomeIcon icon="caret-up" size="2x" color="white" /> - </div> - </button> - </div> - ); - } -} - diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss index 4e704b58f..bc9cf4848 100644 --- a/src/client/views/collections/ParentDocumentSelector.scss +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -2,11 +2,13 @@ div { overflow: visible !important; } + .metadataEntry-outerDiv { overflow: hidden !important; pointer-events: all; } } + .parentDocumentSelector-flyout { position: relative; z-index: 9999; @@ -31,26 +33,31 @@ border-left: 0px; } } + .parentDocumentSelector-button { - pointer-events: all; + pointer-events: all; position: relative; display: inline-block; + svg { - width:20px !important; - height:20px; + // 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; + + display: inline-block; width:100%; height:100%; }
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 649406e6c..4c8cac3ed 100644 --- a/src/client/views/collections/ParentDocumentSelector.tsx +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -40,15 +40,21 @@ export class SelectorContextMenu extends React.Component<SelectorProps> { this._reaction?.(); } async fetchDocuments() { - const aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document); - const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${this.props.Document[Id]}"` }); - const map: Map<Doc, Doc> = new Map; - const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs))); - allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index]))); - docs.forEach(doc => map.delete(doc)); + 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 = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document })); - this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target })); + this._docs = doclayouts.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document })); + this._otherDocs = []; }); } @@ -123,7 +129,7 @@ export class DockingViewButtonSelector extends React.Component<{ views: () => Do this.props.views()[0]?.select(false); }} className="buttonSelector"> <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}> - <FontAwesomeIcon icon={"cog"} size={"sm"} /> + <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} /> </Flyout> </span>; } diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx new file mode 100644 index 000000000..a974c5496 --- /dev/null +++ b/src/client/views/collections/SchemaTable.tsx @@ -0,0 +1,663 @@ +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 ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; +import "react-table/react-table.css"; +import { Doc, DocListCast, Field, Opt } 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 { 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"; +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 { CollectionSchemaAddColumnHeader, KeysDropdown } from "./CollectionSchemaHeaders"; +import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; +import "./CollectionSchemaView.scss"; +import { CollectionView } from "./CollectionView"; + + +enum ColumnType { + Any, + Number, + String, + Boolean, + Doc, + Image, + List, + Date +} + +// this map should be used for keys that should have a const type of value +const columnTypes: Map<string, ColumnType> = new Map([ + ["title", ColumnType.String], + ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], + ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], + ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] +]); + +export interface SchemaTableProps { + Document: Doc; // child doc + dataDoc?: Doc; + PanelHeight: () => number; + PanelWidth: () => number; + childDocs?: Doc[]; + CollectionView: Opt<CollectionView>; + ContainingCollectionView: Opt<CollectionView>; + ContainingCollectionDoc: Opt<Doc>; + fieldKey: string; + renderDepth: number; + deleteDocument: (document: Doc | Doc[]) => boolean; + addDocument: (document: Doc | Doc[]) => boolean; + moveDocument: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; + ScreenToLocalTransform: () => Transform; + active: (outsideReaction: boolean) => boolean; + onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; + addDocTab: (document: Doc, where: string) => boolean; + pinToPres: (document: Doc) => void; + isSelected: (outsideReaction?: boolean) => boolean; + isFocused: (document: Doc) => boolean; + setFocused: (document: Doc) => void; + setPreviewDoc: (document: Doc) => void; + columns: SchemaHeaderField[]; + documentKeys: any[]; + headerIsEditing: boolean; + openHeader: (column: any, screenx: number, screeny: number) => void; + onPointerDown: (e: React.PointerEvent) => void; + onResizedChange: (newResized: Resize[], event: any) => void; + setColumns: (columns: SchemaHeaderField[]) => void; + reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => void; + changeColumns: (oldKey: string, newKey: string, addNew: boolean) => void; + setHeaderIsEditing: (isEditing: boolean) => void; + changeColumnSort: (columnField: SchemaHeaderField, descending: boolean | undefined) => void; +} + +@observer +export class SchemaTable extends React.Component<SchemaTableProps> { + private DIVIDER_WIDTH = 4; + + @observable _cellIsEditing: boolean = false; + @observable _focusedCell: { row: number, col: number } = { row: 0, col: 0 }; + @observable _openCollections: Array<string> = []; + + @observable _showDoc: Doc | undefined; + @observable _showDataDoc: any = ""; + @observable _showDocPos: number[] = []; + + @observable _showTitleDropdown: boolean = false; + + @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } + @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } + @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } + + @computed get childDocs() { + if (this.props.childDocs) return this.props.childDocs; + + const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + return DocListCast(doc[this.props.fieldKey]); + } + set childDocs(docs: Doc[]) { + const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document; + doc[this.props.fieldKey] = new List<Doc>(docs); + } + + @computed get textWrappedRows() { + return Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); + } + set textWrappedRows(textWrappedRows: string[]) { + this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows); + } + + @computed get resized(): { id: string, value: number }[] { + return this.props.columns.reduce((resized, shf) => { + (shf.width > -1) && resized.push({ id: shf.heading, value: shf.width }); + return resized; + }, [] as { id: string, value: number }[]); + } + @computed get sorted(): SortingRule[] { + return this.props.columns.reduce((sorted, shf) => { + shf.desc !== undefined && sorted.push({ id: shf.heading, desc: shf.desc }); + return sorted; + }, [] as SortingRule[]); + } + + @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); + } + } + + @action + changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown + + @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); + const focusedRow = this._focusedCell.row; + 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") { + if (rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onCloseCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-up"} size="sm" /></div>; + if (!rowInfo.isExpanded) return <div className="collectionSchemaView-expander" onClick={() => this.onExpandCollection(rowInfo.original)}><FontAwesomeIcon icon={"sort-down"} size="sm" /></div>; + } else { + return null; + } + } + } + ); + } + + const cols = this.props.columns.map(col => { + + const keysDropdown = <KeysDropdown + keyValue={col.heading} + possibleKeys={possibleKeys} + existingKeys={this.props.columns.map(c => c.heading)} + canAddNew={true} + addNew={false} + onSelect={this.props.changeColumns} + setIsEditing={this.props.setHeaderIsEditing} + docs={this.props.childDocs} + // try commenting this out + width={"100%"} + />; + + 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" : + this.getColumnType(col) === ColumnType.Date ? "calendar" : "align-justify"; + + const headerText = this._showTitleDropdown ? keysDropdown : <div + onClick={this.changeTitleMode} + style={{ + background: col.color, padding: "2px", + letterSpacing: "2px", + textTransform: "uppercase", + display: "flex" + }}> + {col.heading}</div>; + + 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 icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px" }} /> + {/* <div className="keys-dropdown" + style={{ display: "inline", zIndex: 1000 }}> */} + {keysDropdown} + {/* </div> */} + <div onClick={e => this.changeSorting(col)} + style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit" }}> + <FontAwesomeIcon icon={sortIcon} size="lg" /> + </div> + {/* <div onClick={e => this.props.openHeader(col, e.clientX, e.clientY)} + style={{ float: "right", paddingRight: "6px", zIndex: 1, background: "inherit" }}> + <FontAwesomeIcon icon={"compass"} size="sm" /> + </div> */} + </div>; + + return { + Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.props.columns} reorderColumns={this.props.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />, + accessor: (doc: Doc) => doc ? doc[col.heading] : 0, + id: col.heading, + Cell: (rowProps: CellInfo) => { + 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, + col: columnIndex, + rowProps: rowProps, + isFocused: isFocused, + changeFocusedCellByIndex: this.changeFocusedCellByIndex, + CollectionView: this.props.CollectionView, + ContainingCollection: this.props.ContainingCollectionView, + Document: this.props.Document, + fieldKey: this.props.fieldKey, + renderDepth: this.props.renderDepth, + addDocTab: this.props.addDocTab, + pinToPres: this.props.pinToPres, + moveDocument: this.props.moveDocument, + setIsEditing: this.setCellIsEditing, + isEditable: isEditable, + setPreviewDoc: this.props.setPreviewDoc, + setComputed: this.setComputed, + getField: this.getField, + 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} />; + }, + minWidth: 200, + }; + }); + columns.push(...cols); + + columns.push({ + Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />, + accessor: (doc: Doc) => 0, + id: "add", + Cell: (rowProps: CellInfo) => { + 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, + col: columnIndex, + rowProps: rowProps, + isFocused: isFocused, + changeFocusedCellByIndex: this.changeFocusedCellByIndex, + CollectionView: this.props.CollectionView, + ContainingCollection: this.props.ContainingCollectionView, + Document: this.props.Document, + fieldKey: this.props.fieldKey, + renderDepth: this.props.renderDepth, + addDocTab: this.props.addDocTab, + pinToPres: this.props.pinToPres, + moveDocument: this.props.moveDocument, + setIsEditing: this.setCellIsEditing, + isEditable: isEditable, + setPreviewDoc: this.props.setPreviewDoc, + setComputed: this.setComputed, + getField: this.getField, + showDoc: this.showDoc, + }; + + return <CollectionSchemaButtons {...props} />; + }, + width: 28, + resizable: false + }); + return columns; + } + + + + @action + nextHighlight = (e: React.MouseEvent, doc: Doc) => { + e.preventDefault(); + e.stopPropagation(); + doc.searchMatch = false; + console.log(doc.searchMatch); + setTimeout(() => doc.searchMatch = true, 0); + console.log(doc.searchMatch); + + doc.searchIndex = NumCast(doc.searchIndex); + } + + @action + nextHighlight2 = (doc: Doc) => { + + doc.searchMatchAlt = false; + setTimeout(() => doc.searchMatchAlt = true, 0); + doc.searchIndex = NumCast(doc.searchIndex); + } + + 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) { + this.props.Document._schemaHeaders = new List<SchemaHeaderField>([new SchemaHeaderField("title", "#f1efeb")]); + } + } + + componentDidMount() { + document.addEventListener("keydown", this.onKeyDown); + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.onKeyDown); + } + + tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => { + return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before); + } + + private getTrProps: ComponentPropsGetterR = (state, rowInfo) => { + return !rowInfo ? {} : { + ScreenToLocalTransform: this.props.ScreenToLocalTransform, + addDoc: this.tableAddDoc, + removeDoc: this.props.deleteDocument, + rowInfo, + rowFocused: !this.props.headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document), + textWrapRow: this.toggleTextWrapRow, + rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1, + dropAction: StrCast(this.props.Document.childDropAction), + addDocTab: this.props.addDocTab + }; + } + + private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => { + if (!rowInfo || column) return {}; + + const row = rowInfo.index; + //@ts-ignore + const col = this.columns.map(c => c.heading).indexOf(column!.id); + const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document); + // TODO: editing border doesn't work :( + return { + style: { + 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 + onKeyDown = (e: KeyboardEvent): void => { + if (!this._cellIsEditing && !this.props.headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected(true)) { + const direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : ""; + this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col); + + const pdoc = FieldValue(this.childDocs[this._focusedCell.row]); + pdoc && this.props.setPreviewDoc(pdoc); + } + } + + changeFocusedCellByDirection = (direction: string, curRow: number, curCol: number) => { + switch (direction) { + case "tab": return { row: (curRow + 1 === this.childDocs.length ? 0 : curRow + 1), col: curCol + 1 === this.props.columns.length ? 0 : curCol + 1 }; + case "right": return { row: curRow, col: curCol + 1 === this.props.columns.length ? curCol : curCol + 1 }; + case "left": return { row: curRow, col: curCol === 0 ? curCol : curCol - 1 }; + case "up": return { row: curRow === 0 ? curRow : curRow - 1, col: curCol }; + case "down": return { row: curRow + 1 === this.childDocs.length ? curRow : curRow + 1, col: curCol }; + } + return this._focusedCell; + } + + @action + changeFocusedCellByIndex = (row: number, col: number): void => { + if (this._focusedCell.row !== row || this._focusedCell.col !== col) { + this._focusedCell = { row: row, col: col }; + } + this.props.setFocused(this.props.Document); + } + + @undoBatch + createRow = () => { + this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 })); + } + + @undoBatch + @action + createColumn = () => { + let index = 0; + let found = this.props.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1; + while (found) { + index++; + found = this.props.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1; + } + this.props.columns.push(new SchemaHeaderField(`New field ${index ? "(" + index + ")" : ""}`, "#f1efeb")); + } + + @action + getColumnType = (column: SchemaHeaderField): ColumnType => { + // added functionality to convert old column type stuff to new column type stuff -syip + 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; + } + column.type = NumCast(typesDoc[column.heading]); + return NumCast(typesDoc[column.heading]); + } + + @undoBatch + @action + toggleTextwrap = async () => { + const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec("string"), []); + if (textwrappedRows.length) { + this.props.Document.textwrappedSchemaRows = new List<string>([]); + } else { + const docs = DocListCast(this.props.Document[this.props.fieldKey]); + const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]); + this.props.Document.textwrappedSchemaRows = new List<string>(allRows); + } + } + + @action + toggleTextWrapRow = (doc: Doc): void => { + const textWrapped = this.textWrappedRows; + const index = textWrapped.findIndex(id => doc[Id] === id); + + index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]); + + this.textWrappedRows = textWrapped; + } + + @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 rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :(((( + + return <ReactTable + style={{ position: "relative" }} + data={children} + page={0} + pageSize={children.length} + showPagination={false} + columns={this.tableColumns} + getTrProps={this.getTrProps} + getTdProps={this.getTdProps} + sortable={false} + TrComponent={MovableRow} + sorted={this.sorted} + expanded={expanded} + resized={this.resized} + onResizedChange={this.props.onResizedChange} + SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== "collection") ? (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" }); + } + } + + getField = (row: number, col?: number) => { + const docs = this.childDocs; + + row = row % docs.length; + while (row < 0) row += docs.length; + const columns = this.props.columns; + const doc = docs[row]; + if (col === undefined) { + return doc; + } + if (col >= 0 && col < columns.length) { + const column = this.props.columns[col].heading; + return doc[column]; + } + return undefined; + } + + createTransformer = (row: number, col: number): Transformer => { + const self = this; + const captures: { [name: string]: Field } = {}; + + const transformer: ts.TransformerFactory<ts.SourceFile> = context => { + return root => { + function visit(node: ts.Node) { + node = ts.visitEachChild(node, visit, context); + if (ts.isIdentifier(node)) { + const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node; + const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node; + if (isntPropAccess && isntPropAssign) { + if (node.text === "$r") { + return ts.createNumericLiteral(row.toString()); + } else if (node.text === "$c") { + return ts.createNumericLiteral(col.toString()); + } else if (node.text === "$") { + if (ts.isCallExpression(node.parent)) { + // captures.doc = self.props.Document; + // captures.key = self.props.fieldKey; + } + } + } + } + + return node; + } + return ts.visitNode(root, visit); + }; + }; + + // const getVars = () => { + // return { capturedVariables: captures }; + // }; + + return { transformer, /*getVars*/ }; + } + + 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]; + } + 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) }); + if (compiled.compiled) { + doc[field] = new ComputedField(compiled); + return true; + } + return false; + } + + @action + showDoc = (doc: Doc | undefined, dataDoc?: Doc, screenX?: number, screenY?: number) => { + this._showDoc = doc; + if (dataDoc && screenX && screenY) { + this._showDocPos = this.props.ScreenToLocalTransform().transformPoint(screenX, screenY); + } + } + + onOpenClick = () => { + if (this._showDoc) { + this.props.addDocTab(this._showDoc, "onRight"); + } + } + + getPreviewTransform = (): Transform => { + return this.props.ScreenToLocalTransform().translate(- this.borderWidth - 4 - this.tableWidth, - this.borderWidth); + } + + render() { + const preview = ""; + return <div className="collectionSchemaView-table" onPointerDown={this.props.onPointerDown} 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> + : undefined} + {!this._showDoc ? (null) : + <div className="collectionSchemaView-documentPreview" //onClick={() => { this.onOpenClick(); }} + style={{ + position: "absolute", width: 150, height: 150, + background: "dimGray", display: "block", top: 0, left: 0, + transform: `translate(${this._showDocPos[0]}px, ${this._showDocPos[1] - 180}px)` + }} + ref="overlay"><ContentFittingDocumentView + Document={this._showDoc} + DataDoc={this._showDataDoc} + NativeHeight={returnZero} + NativeWidth={returnZero} + fitToBox={true} + FreezeDimensions={true} + focus={emptyFunction} + LibraryPath={emptyPath} + renderDepth={this.props.renderDepth} + rootSelected={() => false} + PanelWidth={() => 150} + PanelHeight={() => 150} + ScreenToLocalTransform={this.getPreviewTransform} + docFilters={returnEmptyFilter} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + moveDocument={this.props.moveDocument} + parentActive={this.props.active} + whenActiveChanged={emptyFunction} + addDocTab={this.props.addDocTab} + pinToPres={this.props.pinToPres} + bringToFront={returnFalse} + ContentScaling={returnOne}> + </ContentFittingDocumentView> + </div>} + </div>; + } +}
\ 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 a4fd5384f..b00074cc6 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -142,9 +142,16 @@ export function computePivotLayout( const fieldKey = "data"; const pivotColumnGroups = new Map<FieldResult<Field>, PivotColumn>(); + let nonNumbers = 0; const pivotFieldKey = toLabel(pivotDoc._pivotField); childPairs.map(pair => { - const lval = Cast(pair.layout[pivotFieldKey], listSpec("string"), null); + const lval = pivotFieldKey === "#" ? Array.from(Object.keys(Doc.GetProto(pair.layout))).filter(k => k.startsWith("#")).map(k => k.substring(1)) : + Cast(pair.layout[pivotFieldKey], listSpec("string"), null); + + const num = toNumber(pair.layout[pivotFieldKey]); + if (num === undefined || Number.isNaN(num)) { + nonNumbers++; + } const val = Field.toString(pair.layout[pivotFieldKey] as Field); if (lval) { lval.forEach((val, i) => { @@ -168,13 +175,6 @@ export function computePivotLayout( }); } }); - let nonNumbers = 0; - childPairs.map(pair => { - const num = toNumber(pair.layout[pivotFieldKey]); - if (num === undefined || Number.isNaN(num)) { - nonNumbers++; - } - }); const pivotNumbers = nonNumbers / childPairs.length < .1; if (pivotColumnGroups.size > 10) { const arrayofKeys = Array.from(pivotColumnGroups.keys()); @@ -434,27 +434,3 @@ function normalizeResults( payload: gname.payload }))); } - -export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void { - return () => { - const addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { - let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox - const scriptField = Cast(doc[key], ScriptField); - const scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript} - // tslint:disable-next-line: no-unnecessary-callback-wrapper - onCancel={() => overlayDisposer()} // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below - onSave={(text, onError) => { - const script = CompileScript(text, { params, requiredType, typecheck: false }); - if (!script.compiled) { - onError(script.errors.map(error => error.messageText).join("\n")); - } else { - doc[key] = new ScriptField(script); - overlayDisposer(); - } - }} />; - overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options); - }; - addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined); - addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}"); - }; -} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss index 05111adb4..8cbda310a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss @@ -16,4 +16,5 @@ stroke: rgb(0,0,0); opacity: 0.5; pointer-events: all; + cursor: move; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index f3fc04752..3a2979696 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,13 +1,12 @@ import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; -import { Utils } from '../../../../Utils'; +import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from '../../../../Utils'; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); -import v5 = require("uuid/v5"); import { DocumentType } from "../../../documents/DocumentTypes"; -import { observable, action, reaction, IReactionDisposer } from "mobx"; -import { StrCast, Cast } from "../../../../fields/Types"; +import { observable, action, reaction, IReactionDisposer, trace, computed } from "mobx"; +import { StrCast, Cast, NumCast } from "../../../../fields/Types"; import { Id } from "../../../../fields/FieldSymbols"; import { SnappingManager } from "../../../util/SnappingManager"; @@ -20,18 +19,27 @@ export interface CollectionFreeFormLinkViewProps { @observer export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> { @observable _opacity: number = 0; + @observable _start = 0; _anchorDisposer: IReactionDisposer | undefined; + _timeout: NodeJS.Timeout | undefined; + componentWillUnmount() { + this._anchorDisposer?.(); + } @action + timeout = () => (Date.now() < this._start++ + 1000) && 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(() => { - if (SnappingManager.GetIsDragging()) return; + this._start = Date.now(); + this._timeout && clearTimeout(this._timeout); + this._timeout = setTimeout(this.timeout, 25); + if (SnappingManager.GetIsDragging() || !this.props.A.ContentDiv || !this.props.B.ContentDiv) return; setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() setTimeout(action(() => (!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. - const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; - const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("linkAnchorBox-cont") : []; - const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv!); - const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv!); + const acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; + const bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; + const adiv = (acont.length ? acont[0] : this.props.A.ContentDiv); + const bdiv = (bcont.length ? bcont[0] : this.props.B.ContentDiv); const a = adiv.getBoundingClientRect(); const b = bdiv.getBoundingClientRect(); const abounds = adiv.parentElement!.getBoundingClientRect(); @@ -46,48 +54,67 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const bfield = afield === "anchor1" ? "anchor2" : "anchor1"; // really hacky stuff to make the LinkAnchorBox display where we want it to: - // if there's an element in the DOM with the id of the opposite anchor, then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right + // if there's an element in the DOM with a classname containing the link's id and a data-targetids attribute containing the other end of the link, + // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right // otherwise, we just use the computed nearest point on the document boundary to the target Document - const targetAhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][afield] as Doc)[Id]); - const targetBhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][bfield] as Doc)[Id]); + const linkId = this.props.LinkDocs[0][Id]; // this link's Id + const AanchorId = (this.props.LinkDocs[0][afield] as Doc)[Id]; // anchor a's id + const BanchorId = (this.props.LinkDocs[0][bfield] as Doc)[Id]; // anchor b's id + const linkEles = Array.from(window.document.getElementsByClassName(linkId)); + const targetAhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes(AanchorId)); + const targetBhyperlink = linkEles.find((ele: any) => ele.dataset.targetids?.includes(BanchorId)); if (!targetBhyperlink) { - this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; - this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; + this.props.A.rootDoc[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100; + this.props.A.rootDoc[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100; } else { setTimeout(() => { - (this.props.A.props.Document[(this.props.A.props as any).fieldKey] as Doc); + (this.props.A.rootDoc[(this.props.A.props as any).fieldKey] as Doc); const m = targetBhyperlink.getBoundingClientRect(); const mp = this.props.A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.A.props.Document[afield + "_x"] = mp[0] / this.props.A.props.PanelWidth() * 100; - this.props.A.props.Document[afield + "_y"] = mp[1] / this.props.A.props.PanelHeight() * 100; + this.props.A.rootDoc[afield + "_x"] = Math.min(1, mp[0] / this.props.A.props.PanelWidth()) * 100; + this.props.A.rootDoc[afield + "_y"] = Math.min(1, mp[1] / this.props.A.props.PanelHeight()) * 100; }, 0); } if (!targetAhyperlink) { - this.props.A.props.Document[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; - this.props.A.props.Document[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; + this.props.A.rootDoc[bfield + "_x"] = (bpt.point.x - bbounds.left) / bbounds.width * 100; + this.props.A.rootDoc[bfield + "_y"] = (bpt.point.y - bbounds.top) / bbounds.height * 100; } else { setTimeout(() => { - (this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc); + (this.props.B.rootDoc[(this.props.B.props as any).fieldKey] as Doc); const m = targetAhyperlink.getBoundingClientRect(); const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - this.props.B.props.Document[bfield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100; - this.props.B.props.Document[bfield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100; + this.props.B.rootDoc[bfield + "_x"] = Math.min(1, mp[0] / this.props.B.props.PanelWidth()) * 100; + this.props.B.rootDoc[bfield + "_y"] = Math.min(1, mp[1] / this.props.B.props.PanelHeight()) * 100; }, 0); } }) , { fireImmediately: true }); } - @action - componentWillUnmount() { - this._anchorDisposer?.(); + + + pointerDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, (e, down, delta) => { + this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0]; + this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1]; + return false; + }, emptyFunction, () => { + // OverlayView.Instance.addElement( + // <LinkEditor sourceDoc={this.props.A.props.Document} linkDoc={this.props.LinkDocs[0]} + // showLinks={action(() => { })} + // />, { x: 300, y: 300 }); + }); } - render() { - if (SnappingManager.GetIsDragging()) return null; + + @computed get renderData() { + this._start; + if (SnappingManager.GetIsDragging() || !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 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, a.left + a.width / 2, a.top + a.height / 2); @@ -100,18 +127,26 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const pt2vec = [pt2[0] - (b.left + b.width / 2), pt2[1] - (b.top + 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])) / 3; + const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; const pt1norm = [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen]; 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.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document); - const text = StrCast(this.props.A.props.Document.linkRelationship); - return !a.width || !b.width || ((!this.props.LinkDocs.length || !this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> - <text x={(Math.min(pt1[0], pt2[0]) * 2 + Math.max(pt1[0], pt2[0])) / 3} y={(pt1[1] + pt2[1]) / 2}> - {text !== "-ungrouped-" ? text : ""} - </text> + const bActive = this.props.B.isSelected() || Doc.IsBrushed(this.props.B.props.Document); + + 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); + return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 }; + } + + render() { + if (!this.renderData) return (null); + const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData; + 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} > + {StrCast(this.props.LinkDocs[0].description)} + </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 ae81b4b36..1a2421bfd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -10,6 +10,7 @@ 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 { @@ -30,8 +31,8 @@ export class CollectionFreeFormLinksView extends React.Component { return drawnPairs; }, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]); return connections.filter(c => - c.a.props.Document.type === DocumentType.LINK && - c.a.props.pinToPres !== emptyFunction && c.b.props.pinToPres !== emptyFunction // bcz: this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed + c.a.props.Document.type === DocumentType.LINK + && !c.a.props.treeViewDoc?.treeViewHideLinkLines && !c.b.props.treeViewDoc?.treeViewHideLinkLines ).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index d9011c9d3..2b07c4efb 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,7 +1,6 @@ @import "../../globalCssVariables"; -.collectionfreeformview-none, -.collectionfreeformview-ease { +.collectionfreeformview-none { position: inherit; top: 0; left: 0; @@ -14,20 +13,153 @@ } .collectionfreeformview-viewdef { - > .collectionFreeFormDocumentView-container { + >.collectionFreeFormDocumentView-container { pointer-events: none; + .contentFittingDocumentDocumentView-previewDoc { pointer-events: all; } } -} -.collectionfreeformview-ease { - transition: transform 500ms; + svg.presPaths { + position: absolute; + z-index: 100000; + overflow: visible; + } + + svg.presPaths-hidden { + display: none; + } } .collectionfreeformview-none { touch-action: none; + + svg.presPaths { + position: absolute; + z-index: 100000; + overflow: visible; + } + + svg.presPaths-hidden { + display: none; + } +} + +.pathOrder { + position: absolute; + z-index: 200000; + + .pathOrder-frame { + position: absolute; + width: 40; + text-align: center; + font-size: 30; + background-color: #69a6db; + font-family: Roboto; + font-weight: 300; + } +} + +.progressivizeButton { + position: absolute; + display: grid; + grid-template-columns: auto 20px auto; + transform: translate(-105%, 0); + align-items: center; + border: black solid 1px; + border-radius: 3px; + justify-content: center; + width: 40; + z-index: 30000; + height: 20; + overflow: hidden; + background-color: #d5dce2; + transition: all 1s; + + .progressivizeButton-prev:hover { + color: #5a9edd; + } + + .progressivizeButton-frame { + justify-self: center; + text-align: center; + width: 15px; + } + + .progressivizeButton-next:hover { + color: #5a9edd; + } +} + +.resizable { + background: rgba(0, 0, 0, 0.2); + width: 100px; + height: 100px; + position: absolute; + top: 100px; + left: 100px; + + .resizers { + width: 100%; + height: 100%; + border: 3px solid #69a6db; + box-sizing: border-box; + + .resizer { + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + /*magic to turn square into circle*/ + background: white; + border: 3px solid #69a6db; + } + + .resizer.top-left { + left: -3px; + top: -3px; + cursor: nwse-resize; + /*resizer cursor*/ + } + + .resizer.top-right { + right: -3px; + top: -3px; + cursor: nesw-resize; + } + + .resizer.bottom-left { + left: -3px; + bottom: -3px; + cursor: nesw-resize; + } + + .resizer.bottom-right { + right: -3px; + bottom: -3px; + cursor: nwse-resize; + } + } +} + +.progressivizeMove-frame { + width: 20px; + border-radius: 2px; + z-index: 100000; + color: white; + text-align: center; + background-color: #5a9edd; + transform: translate(-110%, 110%); +} + +.progressivizeButton:hover { + box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5); + + .progressivizeButton-frame { + background-color: #5a9edd; + color: white; + } } .collectionFreeform-customText { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index b54d0e266..509d7cda8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,69 +1,69 @@ import { library } from "@fortawesome/fontawesome-svg-core"; -import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons"; +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, _allowStateChangesInsideComputed, trace } from "mobx"; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; -import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../fields/Doc"; -import { documentSchema, collectionSchema } from "../../../../fields/documentSchemas"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; +import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; -import { InkData, InkField, InkTool, PointData } from "../../../../fields/InkField"; +import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; -import { createSchema, listSpec, makeInterface } from "../../../../fields/Schema"; -import { ScriptField, ComputedField } from "../../../../fields/ScriptField"; +import { createSchema, makeInterface } from "../../../../fields/Schema"; +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, returnOne, Utils, returnZero, returnFalse, numberRange } from "../../../../Utils"; +import { aggregateBounds, intersectRect, returnFalse, returnOne, returnZero, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; +import { DocumentType } from "../../../documents/DocumentTypes"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; import { HistoryUtil } from "../../../util/History"; import { InteractionUtils } from "../../../util/InteractionUtils"; import { SelectionManager } from "../../../util/SelectionManager"; +import { SnappingManager } from "../../../util/SnappingManager"; import { Transform } from "../../../util/Transform"; import { undoBatch, UndoManager } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; +import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; -import { ContextMenuProps } from "../../ContextMenuItem"; -import { InkingControl } from "../../InkingControl"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; -import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView"; +import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; +import { DocumentViewProps } from "../../nodes/DocumentView"; import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox"; import { pageSchema } from "../../nodes/ImageBox"; -import PDFMenu from "../../pdf/PDFMenu"; import { CollectionDockingView } from "../CollectionDockingView"; import { CollectionSubView } from "../CollectionSubView"; -import { computePivotLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult, computerStarburstLayout, computerPassLayout } from "./CollectionFreeFormLayoutEngines"; +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 { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { CollectionViewType } from "../CollectionView"; -import { Timeline } from "../../animationtimeline/Timeline"; -import { SnappingManager } from "../../../util/SnappingManager"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +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", - scale: "number", currentTimecode: "number", displayTimecode: "number", currentFrame: "number", - arrangeScript: ScriptField, arrangeInit: ScriptField, useClusters: "boolean", fitToBox: "boolean", _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - panTransformType: "string", + _viewTransition: "string", scrollHeight: "number", fitX: "number", fitY: "number", @@ -77,6 +77,8 @@ export type collectionFreeformViewProps = { forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; childPointerEvents?: boolean; + scaleField?: string; + noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) }; @observer @@ -102,6 +104,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @observable _clusterSets: (Doc[])[] = []; @observable _timelineRef = React.createRef<Timeline>(); + @observable _marqueeRef = React.createRef<HTMLDivElement>(); + @observable canPanX: boolean = true; + @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; } @@ -109,20 +115,22 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @computed get nativeWidth() { return this.fitToContent ? 0 : NumCast(this.Document._nativeWidth, this.props.NativeWidth()); } @computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight, this.props.NativeHeight()); } private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } + private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private easing = () => this.props.Document.panTransformType === "Ease"; private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; private 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)) : - this.Document.scale || 1) + NumCast(this.Document[this.scaleFieldKey], 1)) @computed get cachedCenteringShiftX(): number { - return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0; // shift so pan position is at center of window for non-overlay collections + 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 } @computed get cachedCenteringShiftY(): number { - return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 0;// shift so pan position is at center of window for non-overlay collections + 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 } @computed get cachedGetLocalTransform(): Transform { return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); @@ -156,8 +164,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } if (retVal) { const newBoxes = (newBox instanceof Doc) ? [newBox] : newBox; - for (let i = 0; i < newBoxes.length; i++) { - const newBox = newBoxes[i]; + for (const newBox of newBoxes) { if (newBox.activeFrame !== undefined) { const x = newBox.x; const y = newBox.y; @@ -176,11 +183,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } return retVal; - }) + }); private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); + 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); } @@ -188,79 +195,80 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); } + onExternalDrop = (e: React.DragEvent) => { + return (pt => super.onExternalDrop(e, { x: pt[0], y: pt[1] }))(this.getTransform().transformPoint(e.pageX, e.pageY)); + } + @action - onExternalDrop = (e: React.DragEvent): Promise<void> => { - const pt = this.getTransform().transformPoint(e.pageX, e.pageY); - return super.onExternalDrop(e, { x: pt[0], y: pt[1] }); + internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { + if (!super.onInternalDrop(e, de)) return false; + const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y); + const z = NumCast(docDragData.droppedDocuments[0].z); + const x = (z ? xpo : xp) - docDragData.offset[0]; + const y = (z ? ypo : yp) - docDragData.offset[1]; + const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); + zsorted.forEach((doc, index) => doc.zIndex = doc.isInkMask ? 5000 : index + 1); + const dropPos = [NumCast(docDragData.droppedDocuments[0].x), NumCast(docDragData.droppedDocuments[0].y)]; + for (let i = 0; i < docDragData.droppedDocuments.length; i++) { + const d = docDragData.droppedDocuments[i]; + const layoutDoc = Doc.Layout(d); + 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); + } else { + d.x = x + NumCast(d.x) - dropPos[0]; + d.y = y + NumCast(d.y) - dropPos[1]; + } + 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 + } + + (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); + return true; } @undoBatch @action - onInternalDrop = (e: Event, de: DragManager.DropEvent) => { - // if (this.props.Document.isBackground) return false; - const xf = this.getTransform(); - const xfo = this.getTransformOverlay(); - const [xp, yp] = xf.transformPoint(de.x, de.y); - const [xpo, ypo] = xfo.transformPoint(de.x, de.y); - const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - if (!this.isAnnotationOverlay && de.complete.linkDragData && de.complete.linkDragData.linkSourceDocument !== this.props.Document) { + internalPdfAnnoDrop(e: Event, annoDragData: DragManager.PdfAnnoDragData, xp: number, yp: number) { + const dragDoc = annoDragData.dropDocument; + const dropPos = [NumCast(dragDoc.x), NumCast(dragDoc.y)]; + dragDoc.x = xp - annoDragData.offset[0] + (NumCast(dragDoc.x) - dropPos[0]); + dragDoc.y = yp - annoDragData.offset[1] + (NumCast(dragDoc.y) - dropPos[1]); + annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation + this.bringToFront(dragDoc); + return true; + } + + @undoBatch + @action + internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { + if (linkDragData.linkSourceDocument === this.props.Document || this.props.Document.annotationOn) return false; + if (!linkDragData.linkSourceDocument.context || StrCast(Cast(linkDragData.linkSourceDocument.context, Doc, null)?.type) === DocumentType.COL) { + // const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); + // this.props.addDocument(source); + // linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation"); // TODODO this is where in text links get passed + return false; + } else { const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" }); this.props.addDocument(source); - (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceDocument }, - "doc annotation")); // TODODO this is where in text links get passed + linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation", ""); // TODODO this is where in text links get passed e.stopPropagation(); return true; } - if (super.onInternalDrop(e, de)) { - if (de.complete.docDragData) { - if (de.complete.docDragData.droppedDocuments.length) { - const firstDoc = de.complete.docDragData.droppedDocuments[0]; - const z = NumCast(firstDoc.z); - const x = (z ? xpo : xp) - de.complete.docDragData.offset[0]; - const y = (z ? ypo : yp) - de.complete.docDragData.offset[1]; - const dropX = NumCast(firstDoc.x); - const dropY = NumCast(firstDoc.y); - const droppedDocs = de.complete.docDragData.droppedDocuments; - runInAction(() => { - zsorted.forEach((doc, index) => doc.zIndex = index + 1); - for (let i = 0; i < droppedDocs.length; i++) { - const d = droppedDocs[i]; - const layoutDoc = Doc.Layout(d); - if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) { - const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); - CollectionFreeFormDocumentView.setValues(this.Document.currentFrame, d, x + vals.x - dropX, y + vals.y - dropY, vals.opacity); - } else { - d.x = x + NumCast(d.x) - dropX; - d.y = y + NumCast(d.y) - dropY; - } - if (!NumCast(layoutDoc._width)) { - layoutDoc._width = 300; - } - if (!NumCast(layoutDoc._height)) { - const nw = NumCast(layoutDoc._nativeWidth); - const nh = NumCast(layoutDoc._nativeHeight); - layoutDoc._height = nw && nh ? nh / nw * NumCast(layoutDoc._width) : 300; - } - d.isBackground === undefined && (d.zIndex = zsorted.length + 1 + i); // bringToFront - } - }); + } - (de.complete.docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(de.complete.docDragData.droppedDocuments); - } - } - else if (de.complete.annoDragData) { - if (de.complete.annoDragData.dropDocument) { - const dragDoc = de.complete.annoDragData.dropDocument; - const x = xp - de.complete.annoDragData.offset[0]; - const y = yp - de.complete.annoDragData.offset[1]; - const dropX = NumCast(dragDoc.x); - const dropY = NumCast(dragDoc.y); - dragDoc.x = x + NumCast(dragDoc.x) - dropX; - dragDoc.y = y + NumCast(dragDoc.y) - dropY; - de.complete.annoDragData.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation - this.bringToFront(dragDoc); - } - } + @action + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + // 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); + } else if (de.complete.annoDragData?.dropDocument && super.onInternalDrop(e, de)) { + return this.internalPdfAnnoDrop(e, de.complete.annoDragData, xp, yp); + } else if (de.complete.docDragData?.droppedDocuments.length && this.internalDocDrop(e, de, de.complete.docDragData, xp, yp)) { + return true; } return false; } @@ -391,7 +399,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action onPointerDown = (e: React.PointerEvent): void => { - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) { + if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (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; @@ -408,7 +416,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P document.addEventListener("pointermove", this.onPointerMove); document.addEventListener("pointerup", this.onPointerUp); // if not using a pen and in no ink mode - if (InkingControl.Instance.selectedTool === InkTool.None) { + if (Doc.GetSelectedTool() === InkTool.None) { this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; } @@ -432,13 +440,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.addMoveListeners(); this.removeEndListeners(); this.addEndListeners(); - // if (InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen) { + // if (Doc.SelectedTool() === InkTool.Highlighter || Doc.SelectedTool() === InkTool.Pen) { // e.stopPropagation(); // e.preventDefault(); // const point = this.getTransform().transformPoint(pt.pageX, pt.pageY); // this._points.push({ X: point[0], Y: point[1] }); // } - if (InkingControl.Instance.selectedTool === InkTool.None) { + if (Doc.GetSelectedTool() === InkTool.None) { this._lastX = pt.pageX; this._lastY = pt.pageY; e.preventDefault(); @@ -458,7 +466,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P case GestureUtils.Gestures.Stroke: const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - const inkDoc = Docs.Create.InkDocument(InkingControl.Instance.selectedColor, InkingControl.Instance.selectedTool, InkingControl.Instance.selectedWidth, points, { title: "ink stroke", x: B.x, y: B.y, _width: B.width, _height: B.height }); + const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), Doc.GetSelectedTool(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, + { title: "ink stroke", x: B.x - Number(ActiveInkWidth()) / 2, y: B.y - Number(ActiveInkWidth()) / 2, _width: B.width + Number(ActiveInkWidth()), _height: B.height + Number(ActiveInkWidth()) }); this.addDocument(inkDoc); e.stopPropagation(); break; @@ -488,10 +497,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); this._inkToTextStartX = start[0]; this._inkToTextStartY = start[1]; - console.log("start"); break; case GestureUtils.Gestures.EndBracket: - console.log("end"); if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "rtf" && s.color); @@ -532,9 +539,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } }); - console.log(this._wordPalette) CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { - console.log(results); const wordResults = results.filter((r: any) => r.category === "inkWord"); for (const word of wordResults) { const indices: number[] = word.strokeIds; @@ -586,6 +591,7 @@ 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); const docpt = this.getTransform().transformPoint(e.clientX, e.clientY); this.scaleAtPt(docpt, 1); e.stopPropagation(); @@ -619,8 +625,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return; } if (!e.cancelBubble) { - const selectedTool = InkingControl.Instance.selectedTool; - if (selectedTool === InkTool.None) { + if (Doc.GetSelectedTool() === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); @@ -641,7 +646,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); const pt = myTouches[0]; if (pt) { - if (InkingControl.Instance.selectedTool === InkTool.None) { + if (Doc.GetSelectedTool() === InkTool.None) { if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); @@ -784,7 +789,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); - this.props.Document.scale = Math.abs(safeScale); + this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); } } @@ -800,7 +805,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true); else this.zoom(e.clientX, e.clientY, e.deltaY); } - this.props.Document.targetScale = NumCast(this.props.Document.scale); + this.props.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]); } @action @@ -828,7 +833,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } if (!this.layoutDoc._lockedTransform || this.Document.inOverlay) { - this.Document.panTransformType = panType; + this.Document._viewTransition = panType; const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); @@ -840,8 +845,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P bringToFront = action((doc: Doc, sendToBack?: boolean) => { if (sendToBack || doc.isBackground) { doc.zIndex = 0; - } - else { + } 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; @@ -855,8 +861,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P scaleAtPt(docpt: number[], scale: number) { const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); - this.Document.panTransformType = "Ease"; - this.layoutDoc.scale = scale; + this.Document._viewTransition = "transform 500ms"; + this.layoutDoc[this.scaleFieldKey] = scale; const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] }; const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y); @@ -867,7 +873,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { const state = HistoryUtil.getState(); - // TODO This technically isn't correct if type !== "doc", as + // TODO This technically isn't correct if type !== "doc", as // currently nothing is done, but we should probably push a new state if (state.type === "doc" && this.Document._panX !== undefined && this.Document._panY !== undefined) { const init = state.initializers![this.Document[Id]]; @@ -887,8 +893,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.props.focus(doc); } else { const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height); - const offset = annotOn && (contextHgt / 2 * 96 / 72); - this.props.Document.scrollY = NumCast(doc.y) - offset; + const offset = annotOn && (contextHgt / 2); + this.props.Document._scrollY = NumCast(doc.y) - offset; } afterFocus && setTimeout(afterFocus, 1000); @@ -900,14 +906,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY }; HistoryUtil.pushState(newState); - const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType }; + const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[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)) { - if (!doc.z) this.setPan(newPanX, newPanY, "Ease", 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 + // 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 } Doc.BrushDoc(this.props.Document); this.props.focus(this.props.Document); @@ -919,8 +926,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (afterFocus?.()) { this.Document._panX = savedState.px; this.Document._panY = savedState.py; - this.Document.scale = savedState.s; - this.Document.panTransformType = savedState.pt; + this.Document[this.scaleFieldKey] = savedState.s; + this.Document._viewTransition = savedState.pt; } }, 500); } @@ -928,26 +935,30 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } setScaleToZoom = (doc: Doc, scale: number = 0.75) => { - this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); + this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); } @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } - @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } @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; getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { - ...this.props, + addDocument: this.props.addDocument, + removeDocument: this.props.removeDocument, + moveDocument: this.props.moveDocument, + pinToPres: this.props.pinToPres, + whenActiveChanged: this.props.whenActiveChanged, NativeHeight: returnZero, NativeWidth: returnZero, fitToBox: false, DataDoc: childData, Document: childLayout, LibraryPath: this.libraryPath, - LayoutTemplate: this.props.ChildLayoutTemplate, - LayoutTemplateString: this.props.ChildLayoutString, + LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate, + LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString, FreezeDimensions: this.props.freezeChildDimensions, layoutKey: undefined, setupDragLines: this.setupDragLines, @@ -963,6 +974,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ContentScaling: returnOne, ContainingCollectionView: this.props.CollectionView, ContainingCollectionDoc: this.props.Document, + docFilters: this.docFilters, focus: this.focusDocument, backgroundColor: this.getClusterColor, backgroundHalo: this.backgroundHalo, @@ -995,17 +1007,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return this.props.addDocTab(doc, where); }); getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { - const result = this.Document.arrangeScript?.script.run(params, console.log); - if (result?.success) { - return { x: 0, y: 0, transition: "transform 1s", ...result, pair: params.pair, replica: "" }; - } 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 { z, color, zIndex } = params.pair.layout; return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), - transition: StrCast(layoutDoc.transition), opacity: this.Document.editing ? 1 : Cast(opacity, "number", null), + transition: StrCast(layoutDoc.dataTransition), opacity: this.Document.editing ? 1 : Cast(opacity, "number", null), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number"), pair: params.pair, replica: "" }; } @@ -1026,7 +1034,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const transform = `translate(${x}px, ${y}px)`; if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below - const fontSize = Cast(viewDef.fontSize, "number"); + const fontSize = Cast(viewDef.fontSize, "string"); return [text, x, y].some(val => val === undefined) ? undefined : { ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z + color} style={{ width, height, color, fontSize, transform }}> @@ -1064,7 +1072,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doFreeformLayout(poolData: Map<string, PoolData>) { const layoutDocs = this.childLayoutPairs.map(pair => pair.layout); - const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log); + const initResult = this.Document.arrangeInit?.script.run({ docs: layoutDocs, collection: this.Document }, console.log); const state = initResult?.success ? initResult.result.scriptState : undefined; const elements = initResult?.success ? this.viewDefsToJSX(initResult.result.views) : []; @@ -1129,9 +1137,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P })); if (this.props.isAnnotationOverlay) { - this.props.Document.scale = Math.max(1, NumCast(this.props.Document.scale)); + 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); return elements; } @@ -1141,10 +1150,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this._layoutComputeReaction = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); + + this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } + componentWillUnmount() { this._layoutComputeReaction?.(); + this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } + @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } elementFunc = () => this._layoutElements; @@ -1153,6 +1167,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + + // <div ref={this._marqueeRef}> + + @action + onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => { + if ((e as any).handlePan || this.props.isAnnotationOverlay) return; + (e as any).handlePan = true; + + if (this._marqueeRef?.current) { + const dragX = e.detail.clientX; + const dragY = e.detail.clientY; + const bounds = this._marqueeRef.current?.getBoundingClientRect(); + + const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0; + const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0; + if (deltaX !== 0 || deltaY !== 0) { + this.Document._panY = NumCast(this.Document._panY) + deltaY / 2; + this.Document._panX = NumCast(this.Document._panX) + deltaX / 2; + } + } + e.stopPropagation(); + } + promoteCollection = undoBatch(action(() => { const childDocs = this.childDocs.slice(); childDocs.forEach(doc => { @@ -1199,59 +1236,76 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private thumbIdentifier?: number; onContextMenu = (e: React.MouseEvent) => { - if (this.props.annotationsKey) return; + if (this.props.annotationsKey || !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" }); + 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" }); + + const viewctrls = ContextMenu.Instance.findByDescription("UI Controls..."); + const viewCtrlItems = viewctrls && "subitems" in viewctrls ? viewctrls.subitems : []; - ContextMenu.Instance.addItem({ - description: (this._timelineVisible ? "Close" : "Open") + " Animation Timeline", event: action(() => { - this._timelineVisible = !this._timelineVisible; - }), icon: this._timelineVisible ? faEyeSlash : faEye - }); + + !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; + !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); const options = ContextMenu.Instance.findByDescription("Options..."); - const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - - optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" }); - optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); - optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" }); - optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); - optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); - optionItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); - this.props.ContainingCollectionView && optionItems.push({ description: "Promote Collection", event: this.promoteCollection, icon: "table" }); + 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 }); + this.props.ContainingCollectionView && + optionItems.push({ description: "Promote 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: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); - // layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" }); - optionItems.push({ - description: "Import document", icon: "upload", event: ({ x, y }) => { - const input = document.createElement("input"); - input.type = "file"; - input.accept = ".zip"; - input.onchange = async _e => { - const upload = Utils.prepend("/uploadDoc"); - const formData = new FormData(); - const file = input.files && input.files[0]; - if (file) { - formData.append('file', file); - formData.append('remap', "true"); - const response = await fetch(upload, { method: "POST", body: formData }); - const json = await response.json(); - if (json !== "error") { - const doc = await DocServer.GetRefField(json); - if (doc instanceof Doc) { - const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); - doc.x = xx, doc.y = yy; - this.props.addDocument?.(doc); - } - } + 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" }); + + } + !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) }); + !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" }); + } + + importDocument = (x: number, y: number) => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".zip"; + input.onchange = async _e => { + const upload = Utils.prepend("/uploadDoc"); + const formData = new FormData(); + const file = input.files && input.files[0]; + if (file) { + formData.append('file', file); + formData.append('remap', "true"); + const response = await fetch(upload, { method: "POST", body: formData }); + const json = await response.json(); + if (json !== "error") { + const doc = await DocServer.GetRefField(json); + if (doc instanceof Doc) { + const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); + doc.x = xx, doc.y = yy; + this.props.addDocument?.(doc); + setTimeout(() => { + SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => { + docs.docs.forEach(d => LinkManager.Instance.addLink(d)); + }); + }, 2000); // need to give solr some time to update so that this query will find any link docs we've added. } - }; - input.click(); + } } - }); - optionItems.push({ description: `${this.Document._LODdisable ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._LODdisable = !this.Document._LODdisable, icon: "table" }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); - + }; + input.click(); } - @observable _timelineVisible = false; + + + @observable showTimeline = false; intersectRect(r1: { left: number, top: number, width: number, height: number }, r2: { left: number, top: number, width: number, height: number }) { @@ -1325,15 +1379,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P nudge = action((x: number, y: number) => { if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform) { // 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(), "Ease", true); + NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "transform 500ms", true); this._nudgeTime = Date.now(); - setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document.panTransformType = undefined), 500); + setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document._viewTransition = undefined), 500); return true; } return false; }); @computed get marqueeView() { - return <MarqueeView {...this.props} + return <MarqueeView + {...this.props} nudge={this.nudge} addDocTab={this.addDocTab} activeDocuments={this.getActiveDocuments} @@ -1343,17 +1398,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <CollectionFreeFormViewPannableContents - centeringShiftX={this.centeringShiftX} - centeringShiftY={this.centeringShiftY} - shifted={!this.nativeHeight && !this.isAnnotationOverlay} - easing={this.easing} - transition={Cast(this.layoutDoc.transition, "string", null)} - viewDefDivClick={this.props.viewDefDivClick} - zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> - {this.children} - </CollectionFreeFormViewPannableContents> - {this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} + <div ref={this._marqueeRef}> + <CollectionFreeFormViewPannableContents + centeringShiftX={this.centeringShiftX} + centeringShiftY={this.centeringShiftY} + presPaths={BoolCast(this.Document.presPathView)} + progressivize={BoolCast(this.Document.editProgressivize)} + zoomProgressivize={BoolCast(this.Document.editZoomProgressivize)} + transition={Cast(this.layoutDoc._viewTransition, "string", null)} + viewDefDivClick={this.props.viewDefDivClick} + zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + {this.children} + </CollectionFreeFormViewPannableContents></div> + {this.showTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)} </MarqueeView>; } @@ -1369,6 +1426,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); + !this.fitToContent && this._layoutElements?.length && setTimeout(() => this.Document._renderContentBounds = new List<number>([this.contentBounds.x, this.contentBounds.y, this.contentBounds.r, this.contentBounds.b]), 0); return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget} onPointerOver={this.onPointerOver} onWheel={this.onPointerWheel} @@ -1376,9 +1434,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} - onDragOver={e => { - e.preventDefault(); - }} + onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ pointerEvents: this.backgroundEvents ? "all" : undefined, @@ -1387,9 +1443,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P width: this.contentScaling ? `${100 / this.contentScaling}%` : "", height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> - {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? + {this.Document._freeformLOD && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} - <CollectionFreeFormOverlayView elements={this.elementFunc} /> + {!this.props.noOverlay ? <CollectionFreeFormOverlayView elements={this.elementFunc} /> : (null)} <div className={"pullpane-indicator"} style={{ @@ -1429,17 +1485,55 @@ interface CollectionFreeFormViewPannableContentsProps { panX: () => number; panY: () => number; zoomScaling: () => number; - easing: () => boolean; viewDefDivClick?: ScriptField; children: () => JSX.Element[]; - shifted: boolean; transition?: string; + presPaths?: boolean; + progressivize?: boolean; + zoomProgressivize?: boolean; } @observer class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{ + @computed get zoomProgressivize() { + return PresBox.Instance && this.props.zoomProgressivize ? PresBox.Instance.zoomProgressivizeContainer : (null); + } + + @computed get progressivize() { + return PresBox.Instance && this.props.progressivize ? PresBox.Instance.progressivizeChildDocs : (null); + } + + @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></>} + </>); + } + render() { - const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : (this.props.easing() ? "-ease" : "-none")); + // trace(); + const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none"); const cenx = this.props.centeringShiftX(); const ceny = this.props.centeringShiftY(); const panx = -this.props.panX(); @@ -1447,11 +1541,13 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const zoom = this.props.zoomScaling(); return <div className={freeformclass} style={{ - width: this.props.shifted ? 0 : undefined, height: this.props.shifted ? 0 : undefined, transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`, transition: this.props.transition }}> {this.props.children()} + {this.presPaths} + {this.progressivize} + {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 new file mode 100644 index 000000000..d49ab27fb --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss @@ -0,0 +1,68 @@ +.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 new file mode 100644 index 000000000..6263be261 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx @@ -0,0 +1,486 @@ +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 from "../../AntimodeMenu"; +import "./FormatShapePane.scss"; +import { undoBatch } from "../../../util/UndoManager"; +import { ColorState, SketchPicker } from 'react-color'; + +@observer +export default class FormatShapePane extends AntimodeMenu { + 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 + 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/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index a811dd15a..62510ce9d 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -28,6 +28,6 @@ white-space:nowrap; } .marquee-legend::after { - content: "Press: c (collection), s (summary), or Delete" + content: "Press <space> for lasso" } }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index cdfeeaa6b..858f33291 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,7 +1,8 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt } from "../../../../fields/Doc"; -import { InkData, InkField } from "../../../../fields/InkField"; +import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; +import { GetEffectiveAcl } from "../../../../fields/util"; +import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField"; @@ -25,7 +26,7 @@ interface MarqueeViewProps { getContainerTransform: () => Transform; getTransform: () => Transform; activeDocuments: () => Doc[]; - selectDocuments: (docs: Doc[], ink: { Document: Doc, Ink: Map<any, any> }[]) => void; + selectDocuments: (docs: Doc[]) => void; addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; nudge: (x: number, y: number) => boolean; @@ -42,6 +43,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @observable _downY: number = 0; @observable _visible: boolean = false; _commandExecuted = false; + @observable _pointsX: number[] = []; + @observable _pointsY: number[] = []; + @observable _freeHand: boolean = false; componentDidMount() { this.props.setPreviewCursor?.(this.setPreviewCursor); @@ -57,6 +61,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (hideMarquee) { this._visible = false; } + this._pointsX = []; + this._pointsY = []; } @undoBatch @@ -68,18 +74,24 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (e.key === "?") { ContextMenu.Instance.setDefaultItem("?", (str: string) => { const textDoc = Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { - _width: 200, x, y, _nativeHeight: 962, _nativeWidth: 800, isAnnotating: false, + _width: 200, x, y, _nativeHeight: 962, _nativeWidth: 850, isAnnotating: false, title: "bing", UseCors: true }); this.props.addDocTab(textDoc, "onRight"); }); ContextMenu.Instance.displayMenu(this._downX, this._downY); + e.stopPropagation(); } else if (e.key === ":") { DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument, x, y); ContextMenu.Instance.displayMenu(this._downX, this._downY); + e.stopPropagation(); + } else if (e.key === "a" && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.props.selectDocuments(this.props.activeDocuments()); + e.stopPropagation(); } else if (e.key === "q" && e.ctrlKey) { e.preventDefault(); (async () => { @@ -103,6 +115,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque y += 40 * this.props.getTransform().Scale; }); })(); + e.stopPropagation(); } else if (e.key === "b" && e.ctrlKey) { e.preventDefault(); navigator.clipboard.readText().then(text => { @@ -113,11 +126,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.pasteTable(ns, x, y); } }); - } else if (!e.ctrlKey) { + 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: NumCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily), _backgroundColor: StrCast(Doc.UserDoc().backgroundColor), + _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; @@ -127,8 +141,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; } this.props.addLiveTextDocument(tbox); + e.stopPropagation(); } - e.stopPropagation(); } //heuristically converts pasted text into a table. // assumes each entry is separated by a tab @@ -175,15 +189,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.clientX; this._downY = this._lastY = e.clientY; - // allow marquee if right click OR alt+left click OR space bar + left click - if (e.button === 2 || (e.button === 0 && (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))))) { - // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { - this.setPreviewCursor(e.clientX, e.clientY, true); - // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. - e.preventDefault(); - // } - // bcz: do we need this? it kills the context menu on the main collection if !altKey - // e.stopPropagation(); + if (!(e.nativeEvent as any).marqueeHit) { + (e.nativeEvent as any).marqueeHit = true; + // allow marquee if right click OR alt+left click OR space bar + left click + if (e.button === 2 || (e.button === 0 && (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))))) { + // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { + this.setPreviewCursor(e.clientX, e.clientY, true); + // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. + e.preventDefault(); + // } + // bcz: do we need this? it kills the context menu on the main collection if !altKey + // e.stopPropagation(); + } } } @@ -191,6 +208,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque onPointerMove = (e: PointerEvent): void => { this._lastX = e.pageX; this._lastY = e.pageY; + this._pointsX.push(e.clientX); + this._pointsY.push(e.clientY); if (!e.cancelBubble) { if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { @@ -218,7 +237,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // let inkselect = this.ink ? this.marqueeInkSelect(this.ink.inkData) : new Map(); // let inks = inkselect.size ? [{ Document: this.inkDoc, Ink: inkselect }] : []; const docs = mselect.length ? mselect : [this.props.Document]; - this.props.selectDocuments(docs, []); + this.props.selectDocuments(docs); } const hideMarquee = () => { this.hideMarquee(); @@ -243,6 +262,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque e.preventDefault(); } } + clearSelection() { + if (window.getSelection) { window.getSelection()?.removeAllRanges(); } + else if (document.getSelection()) { document.getSelection()?.empty(); } + } setPreviewCursor = action((x: number, y: number, drag: boolean) => { if (drag) { @@ -257,16 +280,22 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else { this._downX = x; this._downY = y; - PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + const effectiveAcl = GetEffectiveAcl(this.props.Document); + if ([AclAdmin, AclEdit, AclAddonly].includes(effectiveAcl)) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); + this.clearSelection(); } }); @action onClick = (e: React.MouseEvent): void => { - if ( - Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false); + if (Doc.GetSelectedTool() === InkTool.None) { + if (!(e.nativeEvent as any).marqueeHit) { + (e.nativeEvent as any).marqueeHit = true; + !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false); + } + } // let the DocumentView stopPropagation of this event when it selects this document } else { // why do we get a click event when the cursor have moved a big distance? // let's cut it off here so no one else has to deal with it. @@ -310,10 +339,21 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._visible = false; } + @undoBatch @action delete = () => { - this.props.removeDocument(this.marqueeSelect(false)); + const recent = Cast(Doc.UserDoc().myRecentlyClosed, Doc) as Doc; + const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); + + selected.map(doc => { + const effectiveAcl = GetEffectiveAcl(doc); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) { // deletes whatever you have the right to delete + recent && Doc.AddDocToList(recent, "data", doc, undefined, true, true); + this.props.removeDocument(doc); + } + }); + this.cleanupInteractions(false); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); @@ -331,7 +371,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque backgroundColor: this.props.isAnnotationOverlay ? "#00000015" : isBackground ? "cyan" : undefined, _width: bounds.width, _height: bounds.height, - _LODdisable: true, title: "a nested collection", }); selected.forEach(d => d.context = newCollection); @@ -344,9 +383,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const selected = this.marqueeSelect(false); SelectionManager.DeselectAll(); selected.forEach(d => this.props.removeDocument(d)); - const newCollection = Doc.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2); + const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2); this.props.addDocument(newCollection!); - this.props.selectDocuments([newCollection!], []); + this.props.selectDocuments([newCollection!]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -371,7 +410,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined); this.props.addDocument(newCollection); - this.props.selectDocuments([newCollection], []); + this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } @@ -406,8 +445,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }); CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => { // const wordResults = results.filter((r: any) => r.category === "inkWord"); - // console.log(wordResults); - // console.log(results); // for (const word of wordResults) { // const indices: number[] = word.strokeIds; // indices.forEach(i => { @@ -448,7 +485,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // }); // } const lines = results.filter((r: any) => r.category === "line"); - console.log(lines); const text = lines.map((l: any) => l.recognizedText).join("\r\n"); this.props.addDocument(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); }); @@ -473,7 +509,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque summary._backgroundColor = "#e2ad32"; portal.layoutKey = "layout_portal"; portal.title = "document collection"; - DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing"); + DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing", ""); this.props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); @@ -484,7 +520,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.props.addDocument(newCollection); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); - setTimeout(() => this.props.selectDocuments([newCollection], []), 0); + setTimeout(() => this.props.selectDocuments([newCollection]), 0); } @undoBatch @@ -519,6 +555,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } this.cleanupInteractions(false); } + if (e.key === "r" || e.key === " ") { + this._commandExecuted = true; + e.stopPropagation(); + e.preventDefault(); + this.changeFreeHand(true); + } + } + + @action + changeFreeHand = (x: boolean) => { + this._freeHand = !this._freeHand; } // @action // marqueeInkSelect(ink: Map<any, any>) { @@ -559,7 +606,51 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // this.ink = new InkField(idata); // } // } + touchesLine(r1: { left: number, top: number, width: number, height: number }) { + for (var i = 0; i < this._pointsX.length; i++) { + const topLeft = this.props.getTransform().transformPoint(this._pointsX[i], this._pointsY[i]); + if (topLeft[0] > r1.left && + topLeft[0] < r1.left + r1.width && + topLeft[1] > r1.top && + topLeft[1] < r1.top + r1.height) { + return true; + } + } + return false; + } + boundingShape(r1: { left: number, top: number, width: number, height: number }) { + const trueLeft = this.props.getTransform().transformPoint(Math.min(...this._pointsX), Math.min(...this._pointsY))[0]; + const trueTop = this.props.getTransform().transformPoint(Math.min(...this._pointsX), Math.min(...this._pointsY))[1]; + const trueRight = this.props.getTransform().transformPoint(Math.max(...this._pointsX), Math.max(...this._pointsY))[0]; + const trueBottom = this.props.getTransform().transformPoint(Math.max(...this._pointsX), Math.max(...this._pointsY))[1]; + + if (r1.left > trueLeft && r1.top > trueTop && r1.left + r1.width < trueRight && r1.top + r1.height < trueBottom) { + var hasTop = false; + var hasLeft = false; + var hasBottom = false; + var hasRight = false; + for (var i = 0; i < this._pointsX.length; i++) { + const truePoint = this.props.getTransform().transformPoint(this._pointsX[i], this._pointsY[i]); + if (!hasLeft && (truePoint[0] > trueLeft && truePoint[0] < r1.left) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height)) { + hasLeft = true; + } + if (!hasTop && (truePoint[1] > trueTop && truePoint[1] < r1.top) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width)) { + hasTop = true; + } + if (!hasRight && (truePoint[0] < trueRight && truePoint[0] > r1.left + r1.width) && (truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height)) { + hasRight = true; + } + if (!hasBottom && (truePoint[1] < trueBottom && truePoint[1] > r1.top + r1.height) && (truePoint[0] > r1.left && truePoint[0] < r1.left + r1.width)) { + hasBottom = true; + } + if (hasTop && hasLeft && hasBottom && hasRight) { + return true; + } + } + } + return false; + } marqueeSelect(selectBackgrounds: boolean = true) { const selRect = this.Bounds; const selection: Doc[] = []; @@ -569,8 +660,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const y = NumCast(doc.y); const w = NumCast(layoutDoc._width); const h = NumCast(layoutDoc._height); - if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { - selection.push(doc); + if (this._freeHand === false) { + if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { + selection.push(doc); + } + } else { + if (this.touchesLine({ left: x, top: y, width: w, height: h }) || + this.boundingShape({ left: x, top: y, width: w, height: h })) { + selection.push(doc); + } } }); if (!selection.length && selectBackgrounds) { @@ -597,8 +695,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const y = NumCast(doc.y); const w = NumCast(layoutDoc._width); const h = NumCast(layoutDoc._height); - if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) { - selection.push(doc); + if (this._freeHand === false) { + if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) { + selection.push(doc); + } + } else { + if (this.touchesLine({ left: x, top: y, width: w, height: h }) || + this.boundingShape({ left: x, top: y, width: w, height: h })) { + selection.push(doc); + } } }); } @@ -607,20 +712,47 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @computed get marqueeDiv() { - const p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; + const p = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; const v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); /** * @RE - The commented out span below * This contains the "C for collection, ..." text on marquees. * Commented out by syip2 when the marquee menu was added. */ - return <div className="marquee" style={{ - transform: `translate(${p[0]}px, ${p[1]}px)`, - width: `${Math.abs(v[0])}`, - height: `${Math.abs(v[1])}`, zIndex: 2000 - }} > - {/* <span className="marquee-legend" /> */} - </div>; + if (!this._freeHand) { + return <div className="marquee" style={{ + transform: `translate(${p[0]}px, ${p[1]}px)`, + width: `${Math.abs(v[0])}`, + height: `${Math.abs(v[1])}`, zIndex: 2000 + }} > + <span className="marquee-legend"></span> + </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(); + str += ","; + str += this._pointsY[i].toString(); + str += (" "); + } + + //hardcoded height and width. + return <div className="marquee" style={{ zIndex: 2000 }}> + <svg height={2000} width={2000}> + <polyline + points={str} + fill="none" + stroke="black" + strokeWidth="1" + strokeDasharray="3" + /> + </svg> + </div>; + } } render() { diff --git a/src/client/views/collections/collectionFreeForm/PropertiesView.scss b/src/client/views/collections/collectionFreeForm/PropertiesView.scss new file mode 100644 index 000000000..aee28366a --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.scss @@ -0,0 +1,731 @@ +.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; + + .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; + + .propertiesView-sharingTable-item { + + display: flex; + // padding: 5px; + padding: 3px; + align-items: center; + border-bottom: 0.5px solid grey; + cursor: pointer; + + &: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 new file mode 100644 index 000000000..b1c3d3dc5 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/PropertiesView.tsx @@ -0,0 +1,1048 @@ +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 } 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; + + //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" + defaultValue={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 effectiveAcl = GetEffectiveAcl(this.selectedDoc!); + const tableEntries = []; + + // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { + if (this.selectedDoc![AclSym]) { + for (const [key, value] of Object.entries(this.selectedDoc![AclSym])) { + const name = key.substring(4).replace("_", "."); + if (name !== Doc.CurrentUserEmail && name !== this.selectedDoc!.author/* && sidebarUsersDisplayed![name] !== false*/) { + tableEntries.push(this.sharingItem(name, effectiveAcl, AclMap.get(value)!)); + } + } + } + + // 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 and the owner to the top of the doc. + tableEntries.unshift(this.sharingItem("Me", effectiveAcl, Doc.CurrentUserEmail === this.selectedDoc!.author ? "Owner" : StrCast(this.selectedDoc![`ACL-${Doc.CurrentUserEmail.replace(".", "_")}`]))); + if (Doc.CurrentUserEmail !== this.selectedDoc!.author) tableEntries.unshift(this.sharingItem(StrCast(this.selectedDoc!.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"> + {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"> + <div className="propertiesView-title"> + 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={() => runInAction(() => { 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 new file mode 100644 index 000000000..4d8473be9 --- /dev/null +++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss @@ -0,0 +1,159 @@ +.collectionGridView-contents { + display: flex; + overflow: hidden; + width: 100%; + height: 100%; + flex-direction: column; + + .collectionGridView-gridContainer { + height: 100%; + overflow-y: auto; + overflow-x: hidden; + + display: flex; + flex-direction: row; + + .imageBox-cont img { + height: auto; + width: auto; + max-height: 100%; + max-width: 100%; + } + + .react-grid-layout { + width: 100%; + } + + .react-grid-item>.react-resizable-handle { + z-index: 4; // doesn't work on prezi otherwise + } + + .react-grid-item>.react-resizable-handle::after { + // grey so it can be seen on the audiobox + border-right: 2px solid slategrey; + border-bottom: 2px solid slategrey; + } + + .rowHeightSlider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 15px; + background: #d3d3d3; + + position: absolute; + height: 3; + left: 5; + top: 30; + transform-origin: left; + transform: rotate(90deg); + outline: none; + opacity: 0.7; + } + + .rowHeightSlider:hover { + opacity: 1; + } + + .rowHeightSlider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 10px; + height: 10px; + border-radius: 50%; + background: darkgrey; + opacity: 1; + } + + .rowHeightSlider::-moz-range-thumb { + width: 10px; + height: 10px; + border-radius: 50%; + background: darkgrey; + opacity: 1; + } + } + + .collectionGridView-addDocumentButton { + display: flex; + overflow: hidden; + margin: auto; + width: 90%; + cursor: text; + min-height: 30px; + max-height: 30px; + font-size: 75%; + letter-spacing: 2px; + + .editableView-input { + outline-color: black; + letter-spacing: 2px; + color: grey; + border: 0px; + padding: 12px 10px 11px 10px; + } + + .editableView-container-editing, + .editableView-container-editing-oneLine { + display: flex; + align-items: center; + flex-direction: row; + height: 20px; + + width: 100%; + color: grey; + padding: 10px; + + span::before, + span::after { + content: ""; + width: 50%; + position: relative; + display: inline-block; + } + + span::before { + margin-right: 10px; + } + + span::after { + margin-left: 10px; + } + + span { + position: relative; + text-align: center; + white-space: nowrap; + overflow: visible; + display: flex; + color: gray; + align-items: center; + } + } + } + +} + +// .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, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +input[type=number] { + -moz-appearance: textfield; +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx new file mode 100644 index 000000000..e6ac7021a --- /dev/null +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -0,0 +1,355 @@ +import { action, computed, Lambda, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from "react"; +import { Doc, Opt, WidthSym } from '../../../../fields/Doc'; +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 { Docs } from '../../../documents/Documents'; +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 { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; +import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; +import { CollectionSubView } from '../CollectionSubView'; +import "./CollectionGridView.scss"; +import Grid, { Layout } from "./Grid"; + +type GridSchema = makeInterface<[typeof documentSchema]>; +const GridSchema = makeInterface(documentSchema); + +@observer +export class CollectionGridView extends CollectionSubView(GridSchema) { + private _containerRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _changeListenerDisposer: Opt<Lambda>; // listens for changes in this.childLayoutPairs + private _resetListenerDisposer: Opt<Lambda>; // listens for when the reset button is clicked + @observable private _rowHeight: Opt<number>; // temporary store of row height to make change undoable + @observable private _scroll: number = 0; // required to make sure the decorations box container updates on scroll + private dropLocation: object = {}; // sets the drop location for external drops + + onChildClickHandler = () => ScriptCast(this.Document.onChildClick); + + @computed get numCols() { return NumCast(this.props.Document.gridNumCols, 10); } + @computed get rowHeight() { return this._rowHeight === undefined ? NumCast(this.props.Document.gridRowHeight, 100) : this._rowHeight; } + // sets the default width and height of the grid nodes + @computed get defaultW() { return NumCast(this.props.Document.gridDefaultW, 2); } + @computed get defaultH() { return NumCast(this.props.Document.gridDefaultH, 2); } + + @computed get colWidthPlusGap() { return (this.props.PanelWidth() - this.margin) / this.numCols; } + @computed get rowHeightPlusGap() { return this.rowHeight + this.margin; } + + @computed get margin() { return NumCast(this.props.Document.margin, 10); } // sets the margin between grid nodes + + @computed get flexGrid() { return BoolCast(this.props.Document.gridFlex, true); } // is grid static/flexible i.e. whether nodes be moved around and resized + @computed get compaction() { return StrCast(this.props.Document.gridStartCompaction, StrCast(this.props.Document.gridCompaction, "vertical")); } // is grid static/flexible i.e. whether nodes be moved around and resized + + /** + * Sets up the listeners for the list of documents and the reset button. + */ + componentDidMount() { + this._changeListenerDisposer = reaction(() => this.childLayoutPairs, (pairs) => { + const newLayouts: Layout[] = []; + const oldLayouts = this.savedLayoutList; + pairs.forEach((pair, i) => { + const existing = oldLayouts.find(l => l.i === pair.layout[Id]); + if (existing) newLayouts.push(existing); + else { + if (Object.keys(this.dropLocation).length) { // external drop + this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.dropLocation as { x: number, y: number }, !this.flexGrid)); + this.dropLocation = {}; + } + else { // internal drop + this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid)); + } + } + }); + pairs?.length && this.setLayoutList(newLayouts); + }, { fireImmediately: true }); + + // updates the layouts if the reset button has been clicked + this._resetListenerDisposer = reaction(() => this.props.Document.gridResetLayout, (reset) => { + if (reset && this.flexGrid) { + this.setLayout(this.childLayoutPairs.map((pair, index) => this.makeLayoutItem(pair.layout, this.unflexedPosition(index)))); + } + this.props.Document.gridResetLayout = false; + }); + } + + /** + * Disposes the listeners. + */ + componentWillUnmount() { + this._changeListenerDisposer?.(); + this._resetListenerDisposer?.(); + } + + /** + * @returns the default location of the grid node (i.e. when the grid is static) + * @param index + */ + 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, + w: this.defaultW, + h: this.defaultH, + static: true + }; + } + + /** + * Maps the x- and y- coordinates of the event to a grid cell. + */ + screenToCell(sx: number, sy: number) { + const pt = this.props.ScreenToLocalTransform().transformPoint(sx, sy); + const x = Math.floor(pt[0] / this.colWidthPlusGap); + const y = Math.floor((pt[1] + this._scroll) / this.rowHeight); + return { x, y }; + } + + /** + * Creates a layout object for a grid item + */ + makeLayoutItem = (doc: Doc, pos: { x: number, y: number }, Static: boolean = false, w: number = this.defaultW, h: number = this.defaultH) => { + return ({ i: doc[Id], w, h, x: pos.x, y: pos.y, static: Static }); + } + + /** + * Adds a layout to the list of layouts. + */ + addLayoutItem = (layouts: Layout[], layout: Layout) => { + const f = layouts.findIndex(l => l.i === layout.i); + f !== -1 && layouts.splice(f, 1); + layouts.push(layout); + return layouts; + } + /** + * @returns the transform that will correctly place the document decorations box. + */ + private lookupIndividualTransform = (layout: Layout) => { + const xypos = this.flexGrid ? layout : this.unflexedPosition(this.renderedLayoutList.findIndex(l => l.i === layout.i)); + const pos = { x: xypos.x * this.colWidthPlusGap + this.margin, y: xypos.y * this.rowHeightPlusGap + this.margin - this._scroll }; + + return this.props.ScreenToLocalTransform().translate(-pos.x, -pos.y); + } + + /** + * @returns the layout list converted from JSON + */ + get savedLayoutList() { + return (this.props.Document.gridLayoutString ? JSON.parse(StrCast(this.props.Document.gridLayoutString)) : []) as Layout[]; + } + + /** + * Stores the layout list on the Document as JSON + */ + setLayoutList(layouts: Layout[]) { + this.props.Document.gridLayoutString = JSON.stringify(layouts); + } + + /** + * + * @param layout + * @param dxf the x- and y-translations of the decorations box as a transform i.e. this.lookupIndividualTransform + * @param width + * @param height + * @returns the `ContentFittingDocumentView` of the node + */ + getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { + return <ContentFittingDocumentView + {...this.props} + Document={layout} + DataDoc={layout.resolvedDataDoc as Doc} + NativeHeight={returnZero} + NativeWidth={returnZero} + backgroundColor={this.props.backgroundColor} + ContainingCollectionDoc={this.props.Document} + PanelWidth={width} + PanelHeight={height} + ScreenToLocalTransform={dxf} + 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 + />; + } + + /** + * Saves the layouts received from the Grid to the Document. + * @param layouts `Layout[]` + */ + @action + setLayout = (layoutArray: Layout[]) => { + // for every child in the collection, check to see if there's a corresponding grid layout object and + // updated layout object. If both exist, which they should, update the grid layout object from the updated object + if (this.flexGrid) { + const savedLayouts = this.savedLayoutList; + this.childLayoutPairs.forEach(({ layout: doc }) => { + const gridLayout = savedLayouts.find(gridLayout => gridLayout.i === doc[Id]); + if (gridLayout) Object.assign(gridLayout, layoutArray.find(layout => layout.i === doc[Id]) || gridLayout); + }); + + if (this.props.Document.gridStartCompaction) { + undoBatch(() => { + this.props.Document.gridCompaction = this.props.Document.gridStartCompaction; + this.setLayoutList(savedLayouts); + })(); + this.props.Document.gridStartCompaction = undefined; + } else { + undoBatch(() => this.setLayoutList(savedLayouts))(); + } + } + } + + /** + * @returns a list of `ContentFittingDocumentView`s inside wrapper divs. + * The key of the wrapper div must be the same as the `i` value of the corresponding layout. + */ + @computed + private get contents(): JSX.Element[] { + const collector: JSX.Element[] = []; + if (this.renderedLayoutList.length === this.childLayoutPairs.length) { + this.renderedLayoutList.forEach(l => { + const child = this.childLayoutPairs.find(c => c.layout[Id] === l.i); + const dxf = () => this.lookupIndividualTransform(l); + const width = () => (this.flexGrid ? l.w : this.defaultW) * this.colWidthPlusGap - this.margin; + const height = () => (this.flexGrid ? l.h : this.defaultH) * this.rowHeightPlusGap - this.margin; + child && collector.push( + <div key={child.layout[Id]} className={"document-wrapper" + (this.flexGrid && this.props.isSelected() ? "" : " static")} > + {this.getDisplayDoc(child.layout, dxf, width, height)} + </div > + ); + }); + } + return collector; + } + + /** + * @returns a list of `Layout` objects with attributes depending on whether the grid is flexible or static + */ + @computed get renderedLayoutList(): Layout[] { + return this.flexGrid ? + this.savedLayoutList.map(({ i, x, y, w, h }) => ({ + i, y, h, + x: x + w > this.numCols ? 0 : x, // handles wrapping around of nodes when numCols decreases + w: Math.min(w, this.numCols), // reduces width if greater than numCols + static: BoolCast(this.childLayoutPairs.find(({ layout }) => layout[Id] === i)?.layout.lockedPosition, false) // checks if the lock position item has been selected in the context menu + })) : + this.savedLayoutList.map((layout, index) => { Object.assign(layout, this.unflexedPosition(index)); return layout; }); + } + + /** + * Handles internal drop of Dash documents. + */ + @action + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + const savedLayouts = this.savedLayoutList; + const dropped = de.complete.docDragData?.droppedDocuments; + if (dropped && super.onInternalDrop(e, de) && savedLayouts.length !== this.childDocs.length) { + dropped.forEach(doc => this.addLayoutItem(savedLayouts, this.makeLayoutItem(doc, this.screenToCell(de.x, de.y)))); // shouldn't place all docs in the same cell; + this.setLayoutList(savedLayouts); + return true; + } + return false; + } + + /** + * Handles external drop of images/PDFs etc from outside Dash. + */ + @action + onExternalDrop = async (e: React.DragEvent): Promise<void> => { + this.dropLocation = this.screenToCell(e.clientX, e.clientY); + super.onExternalDrop(e, {}); + } + + /** + * Handles the change in the value of the rowHeight slider. + */ + @action + onSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => { + this._rowHeight = event.currentTarget.valueAsNumber; + } + /** + * Handles the user clicking on the slider. + */ + @action + onSliderDown = (e: React.PointerEvent) => { + this._rowHeight = this.rowHeight; // uses _rowHeight during dragging and sets doc's rowHeight when finished so that operation is undoable + setupMoveUpEvents(this, e, returnFalse, action(() => { + undoBatch(() => this.props.Document.gridRowHeight = this._rowHeight)(); + this._rowHeight = undefined; + }), emptyFunction, false, false); + e.stopPropagation(); + } + /** + * Adds the display option to change the css display attribute of the `ContentFittingDocumentView`s + */ + onContextMenu = () => { + const displayOptionsMenu: ContextMenuProps[] = []; + displayOptionsMenu.push({ description: "Toggle Content Display Style", event: () => this.props.Document.display = this.props.Document.display ? undefined : "contents", icon: "copy" }); + ContextMenu.Instance.addItem({ description: "Display", subitems: displayOptionsMenu, icon: "tv" }); + } + + /** + * Handles text document creation on double click. + */ + onPointerDown = (e: React.PointerEvent) => { + if (this.props.active(true)) { + setupMoveUpEvents(this, e, returnFalse, returnFalse, + (e: PointerEvent, doubleTap?: boolean) => { + if (doubleTap) { + undoBatch(action(() => { + const text = Docs.Create.TextDocument("", { _width: 150, _height: 50 }); + FormattedTextBox.SelectOnLoad = text[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed + Doc.AddDocToList(this.props.Document, this.props.fieldKey, text); + this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(e.clientX, e.clientY)))); + }))(); + } + }, + false); + if (this.props.isSelected(true)) e.stopPropagation(); + } + } + + render() { + return ( + <div className="collectionGridView-contents" ref={this.createDashEventsTarget} + style={{ pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }} + onContextMenu={this.onContextMenu} + onPointerDown={this.onPointerDown} + onDrop={this.onExternalDrop} + > + <div className="collectionGridView-gridContainer" ref={this._containerRef} + style={{ backgroundColor: StrCast(this.layoutDoc._backgroundColor, "white") }} + onWheel={e => e.stopPropagation()} + onScroll={action(e => { + if (!this.props.isSelected()) e.currentTarget.scrollTop = this._scroll; + else this._scroll = e.currentTarget.scrollTop; + })} > + <Grid + width={this.props.PanelWidth()} + nodeList={this.contents.length ? this.contents : null} + layout={this.contents.length ? this.renderedLayoutList : undefined} + childrenDraggable={this.props.isSelected() ? true : false} + numCols={this.numCols} + rowHeight={this.rowHeight} + setLayout={this.setLayout} + transformScale={this.props.ScreenToLocalTransform().Scale} + compactType={this.compaction} // determines whether nodes should remain in position, be bound to the top, or to the left + preventCollision={BoolCast(this.props.Document.gridPreventCollision)}// determines whether nodes should move out of the way (i.e. collide) when other nodes are dragged over them + margin={this.margin} + /> + <input className="rowHeightSlider" type="range" + style={{ width: this.props.PanelHeight() - 30 }} + min={1} value={this.rowHeight} max={this.props.PanelHeight() - 30} + onPointerDown={this.onSliderDown} onChange={this.onSliderChange} /> + </div> + </div > + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionGrid/Grid.tsx b/src/client/views/collections/collectionGrid/Grid.tsx new file mode 100644 index 000000000..3d2ed0cf9 --- /dev/null +++ b/src/client/views/collections/collectionGrid/Grid.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import { observer } from "mobx-react"; + +import "../../../../../node_modules/react-grid-layout/css/styles.css"; +import "../../../../../node_modules/react-resizable/css/styles.css"; + +import * as GridLayout from 'react-grid-layout'; +import { Layout } from 'react-grid-layout'; +export { Layout } from 'react-grid-layout'; + + +interface GridProps { + width: number; + nodeList: JSX.Element[] | null; + layout: Layout[] | undefined; + numCols: number; + rowHeight: number; + setLayout: (layout: Layout[]) => void; + transformScale: number; + childrenDraggable: boolean; + preventCollision: boolean; + compactType: string; + margin: number; +} + +/** + * Wrapper around the actual GridLayout of `react-grid-layout`. + */ +@observer +export default class Grid extends React.Component<GridProps> { + render() { + const compactType = this.props.compactType === "vertical" || this.props.compactType === "horizontal" ? this.props.compactType : null; + return ( + <GridLayout className="layout" + layout={this.props.layout} + cols={this.props.numCols} + rowHeight={this.props.rowHeight} + width={this.props.width} + compactType={compactType} + isDroppable={true} + isDraggable={this.props.childrenDraggable} + isResizable={this.props.childrenDraggable} + useCSSTransforms={true} + onLayoutChange={this.props.setLayout} + preventCollision={this.props.preventCollision} + transformScale={1 / this.props.transformScale} // still doesn't work :( + margin={[this.props.margin, this.props.margin]} + > + {this.props.nodeList} + </GridLayout> + ); + } +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index c0e1a0232..21d283547 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -10,7 +10,7 @@ import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; import { CollectionSubView } from '../CollectionSubView'; -import "./collectionMulticolumnView.scss"; +import "./CollectionMulticolumnView.scss"; import ResizeBar from './MulticolumnResizer'; import WidthLabel from './MulticolumnWidthLabel'; import { List } from '../../../../fields/List'; @@ -202,9 +202,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu } - @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); } - + onChildClickHandler = () => ScriptCast(this.Document.onChildClick); + onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); addDocTab = (doc: Doc, where: string) => { if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { @@ -234,6 +233,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={dxf} focus={this.props.focus} + docFilters={this.docFilters} 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 602246d07..d02088a6c 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -7,7 +7,7 @@ import { Doc } from '../../../../fields/Doc'; import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../fields/Types'; import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView'; import { Utils, returnZero, returnFalse, returnOne } from '../../../../Utils'; -import "./collectionMultirowView.scss"; +import "./CollectionMultirowView.scss"; import { computed, trace, observable, action } from 'mobx'; import { Transform } from '../../../util/Transform'; import HeightLabel from './MultirowHeightLabel'; @@ -202,8 +202,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) } - @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return ScriptCast(this.Document.onChildDoubleClick); } + onChildClickHandler = () => ScriptCast(this.Document.onChildClick); + onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); addDocTab = (doc: Doc, where: string) => { if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { @@ -233,6 +233,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={dxf} focus={this.props.focus} + docFilters={this.docFilters} ContainingCollectionDoc={this.props.CollectionView?.props.Document} ContainingCollectionView={this.props.CollectionView} addDocument={this.props.addDocument} |
