diff options
Diffstat (limited to 'src/client/views/collections')
33 files changed, 2388 insertions, 1664 deletions
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 1344b70f4..0f3b6f212 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -38,15 +38,15 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume 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={ScriptCast(this.layoutDoc.onChildDoubleClick)} - onClick={ScriptField.MakeScript( - "child._showCaption = 'caption'", - { child: Doc.name }, - { child: childPair.layout })} + onDoubleClick={this.onChildDoubleClick} + onClick={onChildClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.ChildLayoutTemplate} LayoutTemplateString={this.props.ChildLayoutString} @@ -86,7 +86,6 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume interval?: number; startAutoScroll = (direction: number) => { this.interval = window.setInterval(() => { - console.log(this.interval, this.scrollSpeed); this.changeSlide(direction); }, this.scrollSpeed); } @@ -108,29 +107,16 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume }, 1500); } - 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(); } @@ -184,7 +170,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume 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} onContextMenu={this.onContextMenu}> + 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> 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 18d642510..1895c06a1 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,5 +1,23 @@ @import "../../views/globalCssVariables.scss"; +.miniMap { + position: absolute; + overflow: hidden; + right: 10; + bottom: 10; + border: solid 1px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + + .miniOverlay { + width: 100%; + height: 100%; + position: absolute; + .miniThumb { + background: #25252525; + position: absolute; + } + } +} .lm_title { margin-top: 3px; border-radius: 5px; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 79c577b6d..4952dedc9 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -11,7 +11,7 @@ import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnOne, returnTrue, Utils, returnZero, returnEmptyFilter, setupMoveUpEvents, returnFalse } 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'; @@ -28,6 +28,9 @@ 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'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -390,6 +393,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, () => { @@ -430,7 +439,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 @@ -586,7 +595,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" })); @@ -646,16 +654,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} />; } } @@ -664,7 +664,6 @@ interface DockedFrameProps { documentId: FieldId; glContainer: any; libraryPath: (FieldId[]); - backgroundColor?: (doc: Doc) => string | undefined; //collectionDockingView: CollectionDockingView } @observer @@ -818,35 +817,109 @@ 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); + } + 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} - docFilters={returnEmptyFilter} - 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)} + </>; } render() { diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/CollectionLinearView.scss index 123a27deb..b8b72e756 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{ +.collectionLinearView-outer { overflow: hidden; - height:100%; + 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 f38eeaf41..3b31947f7 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)} /> @@ -130,6 +171,31 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { </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.StartLink.props.Document.title} + </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/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 627b22417..9a7ea2c93 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -84,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) && diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionMenu.scss index 822e15aed..9da204787 100644 --- a/src/client/views/collections/CollectionViewChromes.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -1,35 +1,47 @@ @import "../globalCssVariables"; -@import '~js-datepicker/dist/datepicker.min.css'; -.collectionViewChrome-cont { - position: absolute; + +.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; + svg { + margin:auto; + } + } - .collectionViewChrome { + .collectionMenu { display: flex; padding-bottom: 1px; height: 32px; border-bottom: .5px solid rgb(180, 180, 180); 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 { @@ -52,21 +64,22 @@ 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; .commandEntry-drop { color: white; - width: 25px; + width: 30px; margin-top: auto; margin-bottom: auto; } @@ -75,12 +88,18 @@ .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; @@ -88,27 +107,25 @@ color: grey; margin-top: auto; margin-bottom: auto; - margin-left: 5px; } - - .collectionViewBaseChrome-viewModes { - margin-left: 25px; - } - .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 { @@ -184,11 +201,12 @@ font-size: 75%; //text-transform: uppercase; //letter-spacing: 2px; - background: rgb(238, 238, 238); - color: grey; + background: #121721; + color: white; outline-color: black; + color: white; border: none; - //padding: 12px 10px 11px 10px; + border-right: solid gray 1px; } .collectionGridViewChrome-viewPicker:active { @@ -291,29 +309,55 @@ } } -.collectionFreeFormViewChrome-cont { - width: 60px; - display: flex; +.collectionFreeFormMenu-cont { + display: inline-flex; position: relative; align-items: center; + .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: absolute; + position: relative; width: 20; height: 30; bottom: 0; - background: gray; - display: flex; + background: #323232; + display: inline-flex; align-items: center; color: white; } .backKeyframe { - left: 0; - svg { display: block; margin: auto; @@ -321,19 +365,16 @@ } .numKeyframe { - left: 20; - display: flex; flex-direction: column; - padding: 5px; + padding-top: 5px; } .fwdKeyframe { - left: 40; - svg { display: block; margin: auto; } + border-right: solid gray 1px; } } diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx new file mode 100644 index 000000000..0ca86172f --- /dev/null +++ b/src/client/views/collections/CollectionMenu.tsx @@ -0,0 +1,974 @@ +import React = require("react"); +import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; +import { action, computed, observable, reaction, runInAction, Lambda } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, DocListCast, Opt, Field } from "../../../fields/Doc"; +import { BoolCast, Cast, StrCast, NumCast } from "../../../fields/Types"; +import AntimodeMenu from "../AntimodeMenu"; +import "./CollectionMenu.scss"; +import { undoBatch } from "../../util/UndoManager"; +import { CollectionViewType, CollectionView, COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; +import { DragManager } from "../../util/DragManager"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { List } from "../../../fields/List"; +import { EditableView } from "../EditableView"; +import { Id } from "../../../fields/FieldSymbols"; +import { listSpec } from "../../../fields/Schema"; +import FormatShapePane from "./collectionFreeForm/FormatShapePane"; +import { ActiveFillColor, SetActiveInkWidth, ActiveInkColor, SetActiveBezierApprox, SetActiveArrowEnd, SetActiveArrowStart, SetActiveFillColor, SetActiveInkColor } from "../InkingStroke"; +import GestureOverlay from "../GestureOverlay"; +import { InkTool } from "../../../fields/InkField"; +import { DocumentType } from "../../documents/DocumentTypes"; +import { Document } from "../../../fields/documentSchemas"; +import { SelectionManager } from "../../util/SelectionManager"; +import { DocumentView } from "../nodes/DocumentView"; +import { ColorState } from "react-color"; +import { ObjectField } from "../../../fields/ObjectField"; +import { ScriptField } from "../../../fields/ScriptField"; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { DocUtils } from "../../documents/Documents"; + +@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); + } + } + + render() { + const button = <button className="antimodeMenu-button" key="pin menu" title="Pin menu" 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>; + + 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} />, + 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) : ""; }, + }; + + _freeform_commands = [this._viewCommand, this._saveFilterCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; + _stacking_commands = [this._contentCommand, this._templateCommand]; + _masonry_commands = [this._contentCommand, this._templateCommand]; + _schema_commands = [this._templateCommand, this._narrativeCommand]; + _doc_commands = [this._openLinkInCommand, this._onClickCommand]; + _tree_commands = []; + 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: + 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} />); + } + } + 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} > + <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" 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> + </div>; + } + + @computed get viewModes() { + return <div className="collectionViewBaseChrome-viewModes" > + <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" 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> + </div>; + } + + render() { + return ( + <div className="collectionMenu-cont" > + <div className="collectionMenu"> + <div className="collectionViewBaseChrome"> + {this.props.type === CollectionViewType.Invalid || this.props.type === CollectionViewType.Docking ? (null) : this.viewModes} + {this.props.type === CollectionViewType.Docking ? (null) : this.templateChrome} + <div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: "grid" }}> + <button className={"antimodeMenu-button"} onClick={this.toggleViewSpecs} > + <FontAwesomeIcon icon="filter" size="lg" /> + </button> + </div> + + {this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? (null) : <button className={"antimodeMenu-button"} key="float" + style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }} + title="Toggle Overlay Layer" + onClick={() => DocumentView.FloatDoc(this.props.docView)}> + <FontAwesomeIcon icon={["fab", "buffer"]} size={"lg"} /> + </button>} + </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 }> { + 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); + } + @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 _draw = ["⎯", "→", "↔︎", "∿", "↝", "↭", "ロ", "O", "∆"]; + // private _head = ["", "", "arrow", "", "", "arrow", "", "", ""]; + // private _end = ["", "arrow", "arrow", "", "arrow", "arrow", "", "", ""]; + // private _shape = ["line", "line", "line", "", "", "", "rectangle", "circle", "triangle"]; + 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) => + <button className="antimodeMenu-button" title={this._title[i]} key={icon} onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)} + style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}> + {/* {this._draw[i]} */} + <FontAwesomeIcon icon={this._faName[i] as IconProp} size="sm" /> + + </button>)} + </div>; + } + + toggleButton = (key: string, value: boolean, setter: () => {}, icon: FontAwesomeIconProps["icon"], ele: JSX.Element | null) => { + return <button className="antimodeMenu-button" key={key} title={key} + onPointerDown={action(e => setter())} + style={{ backgroundColor: value ? "121212" : "" }}> + <FontAwesomeIcon icon={icon} size="lg" /> + {ele} + </button>; + } + + @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) => + <button className="antimodeMenu-button" key={wid} title="change width" + 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>)} + </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 }} /> + </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 }}></div> + </button>)} + + </div>; + } + + @computed get formatPane() { + return <button className="antimodeMenu-button" key="format" title="toggle foramatting pane" + onPointerDown={action(e => FormatShapePane.Instance.Pinned = !FormatShapePane.Instance.Pinned)} + style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> + <FontAwesomeIcon icon="angle-double-right" size="lg" /> + </button>; + } + + render() { + return !this.props.docView.layoutDoc ? (null) : <div className="collectionFreeFormMenu-cont"> + {this.props.docView.props.renderDepth !== 0 ? (null) : + <div key="map" title="mini map" className="backKeyframe" onClick={this.miniMap}> + <FontAwesomeIcon icon={"map"} size={"lg"} /> + </div> + } + <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> + + {!this.props.isOverlay || this.document.type !== DocumentType.WEB ? (null) : + <button className={"antimodeMenu-button"} key="hypothesis" + style={{ backgroundColor: !this.props.docView.layoutDoc.isAnnotating ? "121212" : undefined, borderRight: "1px solid gray" }} + title="Use Hypothesis" + onClick={() => this.props.docView.layoutDoc.isAnnotating = !this.props.docView.layoutDoc.isAnnotating}> + <FontAwesomeIcon icon={["fab", "hire-a-helper"]} size={"lg"} /> + </button> + } + {!this.props.isOverlay || this.props.docView.layoutDoc.isAnnotating ? + <> + {this.drawButtons} + {this.widthPicker} + {this.colorPicker} + {this.fillPicker} + {this.formatPane} + </> : + (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 (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 22a3877ab..2e4055256 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -39,7 +39,15 @@ 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(() => { @@ -72,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) { - DocUtils.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) { @@ -110,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(); @@ -124,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 11f0edf23..bf826857e 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -152,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); @@ -339,7 +338,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); if (script.compiled) { retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run); - console.log("compiled"); } } @@ -403,17 +401,13 @@ export class CollectionSchemaDateCell extends CollectionSchemaCell { @action handleChange = (date: any) => { - console.log(date); this._date = date; // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); // if (script.compiled) { - // console.log("scripting"); // this.applyToDoc(this._document, this.props.row, this.props.col, script.run); // } else { - console.log(DateCast(date)); // ^ 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; - console.log(this._document[this.props.rowProps.column.id as string]); //} } @@ -478,8 +472,6 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { const results = script.compiled && script.run(); if (results && results.success) { - - console.log(results.result); this._doc = results.result; this._document[this.prop.fieldKey] = results.result; this._docTitle = this._doc?.title; @@ -510,10 +502,8 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell { this._preview = false; } else { if (bool) { - console.log("show doc"); this.props.showDoc(this._doc, this.prop.DataDoc, e.clientX, e.clientY); } else { - console.log("no doc"); this.props.showDoc(undefined); } } @@ -728,7 +718,6 @@ export class CollectionSchemaListCell extends CollectionSchemaCell { @action toggleOpened(open: boolean) { - console.log("open: " + open); this._opened = open; } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index aceaa2bae..11382b722 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -352,7 +352,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @action openHeader = (col: any, screenx: number, screeny: number) => { - console.log("header opening"); this._col = col; this._headerOpen = !this._headerOpen; this._pointerX = screenx; diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 3d8ec2fd5..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 { @@ -425,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 460f1e486..1c5cf4290 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -126,7 +126,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) changed = true; }); } - changed && setTimeout(action(() => { if (this.columnHeaders) { this.columnHeaders.length = 0; this.columnHeaders.push(...columnHeaders); } }), 0); + changed && setTimeout(action(() => this.columnHeaders?.splice(0, this.columnHeaders.length, ...columnHeaders)), 0); return fields; } @@ -166,8 +166,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) { @@ -211,7 +211,7 @@ 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.layoutDoc.childDropAction) as dropActionType} onClick={this.onChildClickHandler} @@ -252,7 +252,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) 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) => { @@ -501,4 +501,4 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) </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 2f4a25bfe..76af70cd1 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"; @@ -279,8 +279,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); @@ -351,6 +350,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; + + return <> + {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} + { + this.collapsed ? (null) : + <div style={{ marginTop: 5 }}> + <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> + } + </>; + } + + + 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 +395,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> - {this.props.parent.Document._columnsHideIfEmpty ? (null) : headingView} - { - this.collapsed ? (null) : - <div style={{ marginTop: 5 }}> - <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 9dad9a056..dacb06e5b 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,4 +1,4 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; import { basename } from 'path'; import CursorField from "../../../fields/CursorField"; import { Doc, Opt, Field, DocListCast } from "../../../fields/Doc"; @@ -19,6 +19,7 @@ import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; import React = require("react"); import * as rp from 'request-promise'; +import ReactLoading from 'react-loading'; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc | Doc[]) => boolean; @@ -54,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 @@ -73,7 +74,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: componentWillUnmount() { this.gestureDisposer?.(); - this.multiTouchDisposer?.(); + this._multiTouchDisposer?.(); } @computed get dataDoc() { @@ -90,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]; } @@ -106,16 +112,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: [...this.props.docFilters(), ...Cast(this.props.Document._docFilters, listSpec("string"), [])]; } @computed get childDocs() { - const docFilters = this.docFilters(); - 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>)[] = []; if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list; @@ -128,6 +124,7 @@ 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); let childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; @@ -136,35 +133,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: if (searchDocs !== undefined && searchDocs.length > 0) { childDocs = searchDocs; } + const docFilters = this.docFilters(); + const docRangeFilters = this.props.ignoreFields?.includes("_docRangeFilters") ? [] : Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - 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 => { - if (facet[value] === "match") { - return d[facetKey] === undefined || Field.toString(d[facetKey] as Field).includes(value); - } - return (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value); - }); - if (!satisfiesFacet) { - return false; - } - } - return true; - }) : childDocs; - const rangeFilteredDocs = filteredDocs.filter(d => { - for (let i = 0; i < docRangeFilters.length; i += 3) { - const key = docRangeFilters[i]; - const min = Number(docRangeFilters[i + 1]); - const max = Number(docRangeFilters[i + 2]); - const val = Cast(d[key], "number", null); - if (val !== undefined && (val < min || val > max)) { - return false; - } - } - return true; - }); - return rangeFilteredDocs; + return this.props.Document.dontRegisterView ? docs : DocUtils.FilterDocs(docs, docFilters, docRangeFilters, viewSpecScript); } @action @@ -229,7 +201,11 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: 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, Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document) || de.embedKey || !this.props.isAnnotationOverlay ? this.addDocument : returnFalse) : 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 { added = this.addDocument(docDragData.droppedDocuments); } @@ -254,6 +230,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; @@ -325,7 +302,7 @@ 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); @@ -333,7 +310,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: 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([], { _backgroundColor: "transparent", _width: 25, _height: 25, x, y, annotationOn: srcWeb }); + 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); @@ -346,9 +323,9 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } } - if (text) { - if (text.includes("www.youtube.com/watch") || text.includes("www.youtube.com/embed")) { - const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/").split("&")[0]; + 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, @@ -373,10 +350,20 @@ 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) { + this.addDocument(Docs.Create.WebDocument(uriList, { + ...options, + title: uriList, + _width: 400, + _height: 315, + _nativeWidth: 600, + _nativeHeight: 472.5 + })); + return; + } const { items } = e.dataTransfer; const { length } = items; @@ -403,7 +390,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: file?.type && files.push(file); file?.type === "application/json" && Utils.readUploadedFileAsText(file).then(result => { - console.log(result); const json = JSON.parse(result as string); this.addDocument(Docs.Create.TreeDocument( json["rectangular-puzzle"].crossword.clues[0].clue.map((c: any) => { @@ -417,13 +403,20 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: }); } } + 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 => DocUtils.iconify(d)); if (set) { - this.addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!); + UndoManager.RunInBatch(() => this.addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!), "drop"); } else { - generatedDocuments.forEach(this.addDocument); + UndoManager.RunInBatch(() => generatedDocuments.forEach(this.addDocument), "drop"); } completed?.(); } else { @@ -431,18 +424,24 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: 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 } from "./CollectionView"; +import { CollectionView, CollectionViewType } from "./CollectionView"; import { SelectionManager } from "../../util/SelectionManager"; +import { OverlayView } from "../OverlayView"; +import { setTimeout } from "timers"; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 620b977fa..705871a6f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -61,8 +61,8 @@ export interface TreeViewProps { 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,7 +76,7 @@ export interface TreeViewProps { * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree */ class TreeView extends React.Component<TreeViewProps> { - private _editTitleScript: ScriptField | undefined; + private _editTitleScript: (() => ScriptField) | undefined; private _header?: React.RefObject<HTMLDivElement> = React.createRef(); private _treedropDisposer?: DragManager.DragDropDisposer; private _dref = React.createRef<HTMLDivElement>(); @@ -100,8 +100,8 @@ class TreeView extends React.Component<TreeViewProps> { childDocList(field: string) { const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; return ((this.props.dataDoc ? DocListCast(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field - (layout ? Cast(layout[field], listSpec(Doc)) : undefined) || // else if there's a layout doc, display it's fields - Cast(this.doc[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])) as Doc[]; // otherwise use the document's data field } @computed get childDocs() { return this.childDocList(this.fieldKey); } @computed get childLinks() { return this.childDocList("links"); } @@ -124,7 +124,8 @@ class TreeView extends React.Component<TreeViewProps> { constructor(props: any) { super(props); - this._editTitleScript = ScriptField.MakeScript(`{setInPlace(self, 'editTitle', '${this._uniqueId}'); selectDoc(self);} `); + 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); } @@ -207,7 +208,7 @@ class TreeView extends React.Component<TreeViewProps> { if (complete.linkDragData) { const sourceDoc = complete.linkDragData.linkSourceDocument; const destDoc = this.doc; - DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link"); + DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link", ""); e.stopPropagation(); } const docDragData = complete.docDragData; @@ -368,13 +369,13 @@ class TreeView extends React.Component<TreeViewProps> { } } - get onCheckedClick() { return this.props.onCheckedClick || ScriptCast(this.doc.onCheckedClick); } + get onCheckedClick() { return this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } @action bulletClick = (e: React.MouseEvent) => { if (this.onCheckedClick && this.doc.type !== DocumentType.COL) { // this.props.document.treeViewChecked = this.props.document.treeViewChecked === "check" ? "x" : this.props.document.treeViewChecked === "x" ? undefined : "check"; - this.onCheckedClick.script.run({ + this.onCheckedClick?.script.run({ this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc, heading: this.props.containingCollection.title, checked: this.doc.treeViewChecked === "check" ? "x" : this.doc.treeViewChecked === "x" ? undefined : "check", @@ -404,6 +405,7 @@ class TreeView extends React.Component<TreeViewProps> { 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. */ @@ -437,7 +439,7 @@ class TreeView extends React.Component<TreeViewProps> { addDocTab={this.props.addDocTab} rootSelected={returnTrue} pinToPres={emptyFunction} - onClick={this.props.onChildClick || this._editTitleScript} + onClick={this.onChildClick} dropAction={this.props.dropAction} moveDocument={this.move} removeDocument={this.removeDoc} @@ -539,8 +541,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); @@ -658,8 +660,8 @@ class TreeView extends React.Component<TreeViewProps> { export type collectionTreeViewProps = { treeViewHideTitle?: boolean; treeViewHideHeaderFields?: boolean; - onCheckedClick?: ScriptField; - onChildClick?: ScriptField; + onCheckedClick?: () => ScriptField; + onChildClick?: () => ScriptField; }; @observer @@ -780,7 +782,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll onClicks.push({ description: "Edit onChecked Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.doc, undefined, "onCheckedClick"), "edit onCheckedClick"), icon: "edit" }); - !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" }); + !existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: onClicks, icon: "hand-point-right" }); } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {}); @@ -794,8 +796,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll </div >; } - onKeyPress = (e: React.KeyboardEvent) => { - console.log(e); + onChildClick = () => { + return this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); } render() { TraceMobx(); @@ -814,7 +816,6 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll paddingTop: `${NumCast(this.doc._yPadding, 20)}px`, pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }} - onKeyPress={this.onKeyPress} onWheel={(e) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()} onDrop={this.onTreeDrop} ref={this.createTreeDropTarget}> @@ -839,7 +840,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll 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.props.onChildClick || ScriptCast(this.doc.onChildClick), this.props.ignoreFields) + this.onChildClick, this.props.ignoreFields) } </ul> </div > diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index f9dc666d6..d41248a77 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -8,30 +8,31 @@ import * as React from 'react'; import Lightbox from 'react-image-lightbox-with-rotate'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../fields/DateField'; -import { AclAddonly, AclReadonly, AclSym, DataSym, Doc, DocListCast, Field, Opt } 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 { 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 { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils, returnEmptyFilter } from '../../../Utils'; +import { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls, SharingPermissions } 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 { CollectionCarouselView } from './CollectionCarouselView'; 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'; @@ -43,12 +44,8 @@ import { CollectionStaffView } from './CollectionStaffView'; import { SubCollectionViewProps } from './CollectionSubView'; import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; -import { CollectionGridView } from './collectionGrid/CollectionGridView'; import './CollectionView.scss'; -import { CollectionViewBaseChrome } from './CollectionViewChromes'; -import { UndoManager } from '../../util/UndoManager'; -import { RichTextField } from '../../../fields/RichTextField'; -import { TextField } from '../../util/ProsemirrorCopy/prompt'; +import { ContextMenuProps } from '../ContextMenuItem'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -90,6 +87,7 @@ export interface CollectionRenderProps { active: () => boolean; whenActiveChanged: (isActive: boolean) => void; PanelWidth: () => number; + PanelHeight: () => number; ChildLayoutTemplate?: () => Doc; ChildLayoutString?: string; } @@ -105,7 +103,15 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus @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); @@ -129,38 +135,56 @@ 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 && !getPlaygroundMode())) { return false; - } else if (this.dataDoc[AclSym] === 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) && - !DocListCast(doc.links).some(d => d.isPushpin)) { - const pushpin = Docs.Create.FontIconDocument({ - 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) - }); - Doc.AddDocToList(context, Doc.LayoutFieldKey(context) + "-annotations", pushpin); - const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin"); - const first = DocListCast(pushpin.links).find(d => d instanceof Doc); - first && (first.hidden = true); - pushpinLink && (Doc.GetProto(pushpinLink).isPushpin = true); - 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 + "-lastModified"] = new DateField(new Date(Date.now())); - targetDataDoc["lastModified"] = new DateField(new Date(Date.now())); + } + else { + if (this.props.Document[AclSym]) { + // change so it only adds if more restrictive + added.forEach(d => { + // const dataDoc = d[DataSym]; + for (const [key, value] of Object.entries(this.props.Document[AclSym])) { + distributeAcls(key, this.AclMap.get(value) as SharingPermissions, d, true); + } + // dataDoc[AclSym] = d[AclSym] = this.props.Document[AclSym]; + }); + } + 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 + "-lastModified"] = new DateField(new Date(Date.now())); + targetDataDoc["lastModified"] = new DateField(new Date(Date.now())); + + } } } return true; @@ -168,13 +192,16 @@ 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 effectiveAcl = GetEffectiveAcl(this.props.Document); + if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || getPlaygroundMode()) { + 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; + } } return false; } @@ -182,7 +209,7 @@ 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)) { @@ -204,7 +231,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus 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]); @@ -237,23 +264,14 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus } } - @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" }); @@ -271,58 +289,62 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus subItems.push({ description: "Pivot/Time", event: () => func(CollectionViewType.Time), icon: "columns" }); subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" }); subItems.push({ description: "Grid", event: () => func(CollectionViewType.Grid), icon: "th-list" }); - if (addExtras && this.props.Document._viewType === CollectionViewType.Freeform) { - subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) }); - } 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 : []; + 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" }); 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: "hand-point-right" }); if (!Doc.UserDoc().noviceMode) { - const more = ContextMenu.Instance.findByDescription("More..."); + 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 && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); + !more && cm.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" }); } } } @@ -472,7 +494,8 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus 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(); @@ -498,6 +521,7 @@ 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`} @@ -525,7 +549,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} @@ -548,6 +572,7 @@ 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, }; @@ -565,7 +590,7 @@ 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.Document.hideFilterView) && !this.props.Document.forceActive ? (null) : + {(!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%" : "55%" }} /> } @@ -573,5 +598,3 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus </div>); } } - - diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx deleted file mode 100644 index 4e91a2928..000000000 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ /dev/null @@ -1,800 +0,0 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction, Lambda } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc, DocListCast, Opt } 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"; - -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: undoBatch((source: Doc[]) => source.length && (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: undoBatch((source: Doc[]) => source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]))), - initialize: emptyFunction, - }; - _contentCommand = { - params: ["target", "source"], title: "=> clear content", - script: "getProto(this.target).data = copyField(this.source);", - immediate: undoBatch((source: Doc[]) => Doc.GetProto(this.target).data = new List<Doc>(source)), // Doc.aliasDocs(source), - initialize: emptyFunction, - }; - _viewCommand = { - params: ["target"], title: "=> reset view", - script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", - immediate: undoBatch((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; }, - }; - _clusterCommand = { - params: ["target"], title: "=> fit content", - script: "this.target._fitToBox = !this.target._fitToBox;", - immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox), - initialize: emptyFunction - }; - _fitContentCommand = { - params: ["target"], title: "=> toggle clusters", - script: "this.target.useClusters = !this.target.useClusters;", - immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), - initialize: emptyFunction - }; - - _freeform_commands = [this._viewCommand, this._fitContentCommand, this._clusterCommand, this._contentCommand, this._templateCommand, this._narrativeCommand]; - _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; - case CollectionViewType.Carousel3D: return this._freeform_commands; - } - return []; - } - 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 : ""); - // 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} />); - case CollectionViewType.Carousel3D: return (<Collection3DCarouselViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />); - case CollectionViewType.Grid: return (<CollectionGridViewChrome 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 { - 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.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, () => { - this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate([])); - }); - } - - @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} - style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }}> - <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" style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }} - 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 StrCast(this.props.CollectionView.props.Document._columnsSort) === "descending"; } - @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._columnsSort = - this.props.CollectionView.props.Document._columnsSort === "descending" ? "ascending" : - this.props.CollectionView.props.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<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> - ); - } -} - -// Enter scroll speed for 3D Carousel -@observer -export class Collection3DCarouselViewChrome extends React.Component<CollectionViewChromeProps> { - @computed get scrollSpeed() { - return this.props.CollectionView.props.Document._autoScrollSpeed; - } - - @action - setValue = (value: string) => { - const numValue = Number(StrCast(value)); - if (numValue > 0) { - this.props.CollectionView.props.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<CollectionViewChromeProps> { - - private clicked: boolean = false; - private entered: boolean = false; - private decrementLimitReached: boolean = false; - @observable private resize = false; - private resizeListenerDisposer: Opt<Lambda>; - - componentDidMount() { - - runInAction(() => this.resize = this.props.CollectionView.props.PanelWidth() < 700); - - // listener to reduce text on chrome resize (panel resize) - this.resizeListenerDisposer = computed(() => this.props.CollectionView.props.PanelWidth()).observe(({ newValue }) => { - runInAction(() => this.resize = newValue < 700); - }); - } - - componentWillUnmount() { - this.resizeListenerDisposer?.(); - } - - get numCols() { return NumCast(this.props.CollectionView.props.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.props.CollectionView.props.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.props.CollectionView.props.Document.rowHeight as number !== e.currentTarget.valueAsNumber) { - // this.props.CollectionView.props.Document.rowHeight = e.currentTarget.valueAsNumber; - // } - // } - // } - - /** - * Sets whether the grid is flexible or not on the grid's Document. - */ - @undoBatch - toggleFlex = () => { - this.props.CollectionView.props.Document.gridFlex = !BoolCast(this.props.CollectionView.props.Document.gridFlex, true); - } - - /** - * Increments the value of numCols on button click - */ - onIncrementButtonClick = () => { - this.clicked = true; - this.entered && (this.props.CollectionView.props.Document.gridNumCols as number)--; - undoBatch(() => this.props.CollectionView.props.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.props.CollectionView.props.Document.gridNumCols as number)++; - undoBatch(() => this.props.CollectionView.props.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.props.CollectionView.props.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.props.CollectionView.props.Document.gridNumCols = this.numCols - 1; - } - else { - this.decrementLimitReached = true; - } - } - - this.clicked = false; - } - - /** - * Toggles the value of preventCollision - */ - toggleCollisions = () => { - this.props.CollectionView.props.Document.gridPreventCollision = !this.props.CollectionView.props.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.props.CollectionView.props.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.props.CollectionView.props.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.props.CollectionView.props.Document.gridPreventCollision} /> - <label className="flexLabel">{this.resize ? "Coll" : "Collisions"}</label> - </span> - - <select className="collectionGridViewChrome-viewPicker" - style={{ marginRight: 5, width: this.props.PanelWidth() < 300 ? 25 : undefined }} - onPointerDown={stopPropagation} - onChange={this.changeCompactType} - value={StrCast(this.props.CollectionView.props.Document.gridStartCompaction, StrCast(this.props.CollectionView.props.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.props.CollectionView.props.Document.gridFlex, true)} /> - <label className="flexLabel">{this.resize ? "Flex" : "Flexible"}</label> - </span> - - <button onClick={() => this.props.CollectionView.props.Document.gridResetLayout = true}> - {!this.resize ? "Reset" : - <FontAwesomeIcon icon="redo-alt" size="1x" />} - </button> - - </div> - ); - } -} diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx index 649406e6c..8c0b8de9d 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 Promise.all((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 (await 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 (await 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 = []; }); } diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index c820cb661..829db9c6a 100644 --- a/src/client/views/collections/SchemaTable.tsx +++ b/src/client/views/collections/SchemaTable.tsx @@ -135,7 +135,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> { @action changeSorting = (col: any) => { - console.log(col.heading); if (col.desc === undefined) { // no sorting this.props.changeColumnSort(col, true); @@ -149,7 +148,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { } @action - changeTitleMode = () => { console.log("header clicked"); this._showTitleDropdown = !this._showTitleDropdown; } + changeTitleMode = () => this._showTitleDropdown = !this._showTitleDropdown; @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get tableColumns(): Column<Doc>[] { @@ -219,7 +218,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { background: col.color, padding: "2px", display: "flex", cursor: "default", height: "100%", }}> - <FontAwesomeIcon icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop:"4px" }} /> + <FontAwesomeIcon icon={icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px" }} /> {/* <div className="keys-dropdown" style={{ display: "inline", zIndex: 1000 }}> */} {keysDropdown} 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 a24693c30..bfe569853 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(); @@ -82,17 +90,31 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo }) , { 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); @@ -105,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/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 546a4307c..57336131a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,53 +1,53 @@ 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 } 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 { 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 { InkingStroke, ActiveArrowStart, ActiveArrowEnd, ActiveInkColor, ActiveFillColor, ActiveInkWidth, ActiveInkBezierApprox, ActiveDash } from "../../InkingStroke"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; +import { SearchUtil } from "../../../util/SearchUtil"; +import { LinkManager } from "../../../util/LinkManager"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -57,7 +57,6 @@ export const panZoomSchema = createSchema({ currentTimecode: "number", displayTimecode: "number", currentFrame: "number", - arrangeScript: ScriptField, arrangeInit: ScriptField, useClusters: "boolean", fitToBox: "boolean", @@ -77,7 +76,8 @@ export type collectionFreeformViewProps = { forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; childPointerEvents?: boolean; - scaleField?: string; + scaleField?: string; // used by formattedTextBox when displaying a sidebar freeform view which needs its own scale field + noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) }; @observer @@ -103,6 +103,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; } @@ -182,7 +186,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P 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); } @@ -207,7 +211,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); - if (this.Document.currentFrame !== undefined && !this.props.isAnnotationOverlay) { + 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.opacity); } else { @@ -248,7 +252,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } else { 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 + linkDragData.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceDocument }, "doc annotation", ""); // TODODO this is where in text links get passed e.stopPropagation(); return true; } @@ -492,10 +496,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); @@ -602,7 +604,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { // bcz: theres should be a better way of doing these than referencing these static instances directly MarqueeOptionsMenu.Instance?.fadeOut(true);// I think it makes sense for the marquee menu to go away when panned. -syip2 - PDFMenu.Instance.fadeOut(true); const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true); @@ -870,7 +871,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]]; @@ -890,7 +891,7 @@ 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); + const offset = annotOn && (contextHgt / 2); this.props.Document._scrollY = NumCast(doc.y) - offset; } @@ -935,22 +936,26 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @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, @@ -999,10 +1004,6 @@ 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); @@ -1030,7 +1031,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 }}> @@ -1068,7 +1069,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) : []; @@ -1143,13 +1144,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action componentDidMount() { super.componentDidMount?.(); - this._layoutComputeReaction = reaction(() => this.doLayoutComputation, + this._layoutComputeReaction = reaction(() => { TraceMobx(); return 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; @@ -1158,6 +1164,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 => { @@ -1204,59 +1233,73 @@ 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 ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" }); - appearanceItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); - appearanceItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" }); + 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" }); + appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }); !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 : []; + viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); + viewCtrlItems.push({ description: (this.Document.useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" }); + !viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" }); + const options = ContextMenu.Instance.findByDescription("Options..."); const optionItems = options && "subitems" in options ? options.subitems : []; - !this.props.isAnnotationOverlay && + !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: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " snap lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }); 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" }); + appearanceItems.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" }); - 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); - } - } - } - }; - input.click(); - } - }); + } !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(); } + + @observable showTimeline = false; intersectRect(r1: { left: number, top: number, width: number, height: number }, @@ -1339,7 +1382,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return false; }); @computed get marqueeView() { - return <MarqueeView {...this.props} + return <MarqueeView + {...this.props} nudge={this.nudge} addDocTab={this.addDocTab} activeDocuments={this.getActiveDocuments} @@ -1349,14 +1393,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <CollectionFreeFormViewPannableContents - centeringShiftX={this.centeringShiftX} - centeringShiftY={this.centeringShiftY} - transition={Cast(this.layoutDoc._viewTransition, "string", null)} - viewDefDivClick={this.props.viewDefDivClick} - zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> - {this.children} - </CollectionFreeFormViewPannableContents> + <div ref={this._marqueeRef}> + <CollectionFreeFormViewPannableContents + centeringShiftX={this.centeringShiftX} + centeringShiftY={this.centeringShiftY} + 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>; } @@ -1373,6 +1418,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} @@ -1391,7 +1437,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }}> {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={{ @@ -1453,4 +1499,4 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF {this.props.children()} </div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss index a9fab4c1e..010beb836 100644 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss @@ -1,4 +1,8 @@ .antimodeMenu-button { + width: 200px; + position: relative; + text-align: left; + .color-previewI { width: 100%; height: 40%; @@ -8,12 +12,25 @@ width: 100%; height: 100%; } +} +.antimenu-Buttonup { + position: absolute; + width: 20; + height: 10; + right: 0; + padding: 0; +} +.formatShapePane-inputBtn { + width: inherit; + position: absolute; } .sketch-picker { background: #323232; + width: 160px !important; + height: 80% !important; .flexbox-fit { background: #323232; @@ -26,6 +43,16 @@ /* 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; @@ -35,7 +62,5 @@ .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..ddc282e57 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx @@ -0,0 +1,487 @@ +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'; +import { DocumentView } from "../../../views/nodes/DocumentView" + +@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 }[] = []; + for (var j = 0; j < ink.length; j++) { + // (new x — oldx) + (oldxpoint * newWidt)/oldWidth + const newX = (doc.x - oldX) + (ink[j].X * doc._width) / oldWidth; + const newY = (doc.y - oldY) + (ink[j].Y * doc._height) / 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 }[] = []; + for (var j = 0; j < ink.length; j++) { + // (new x — oldx) + (oldxpoint * newWidt)/oldWidth + const newX = (doc.x - oldX) + (ink[j].X * doc._width) / oldWidth; + const newY = (doc.y - oldY) + (ink[j].Y * doc._height) / 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 }[] = []; + for (var i = 0; i < ink.length; i++) { + const newX = Math.cos(angle) * (ink[i].X - _centerPoints[index].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[index].Y) + _centerPoints[index].X; + const newY = Math.sin(angle) * (ink[i].X - _centerPoints[index].X) + Math.cos(angle) * (ink[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 XpsInput() { return this.inputBoxDuo("Xps", this.shapeXps, (val: string) => this.shapeXps = val, "X:", "Yps", this.shapeYps, (val: string) => this.shapeYps = val, "Y:"); } + @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 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/InkOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx deleted file mode 100644 index f1032adaa..000000000 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx +++ /dev/null @@ -1,328 +0,0 @@ -import React = require("react"); -import AntimodeMenu from "../../AntimodeMenu"; -import { observer } from "mobx-react"; -import { observable, action, computed } from "mobx"; -import "./InkOptionsMenu.scss"; -import { ActiveInkColor, ActiveInkBezierApprox, ActiveFillColor, ActiveArrowStart, ActiveArrowEnd, SetActiveInkWidth, SetActiveInkColor, SetActiveBezierApprox, SetActiveFillColor, SetActiveArrowStart, SetActiveArrowEnd, ActiveDash, SetActiveDash } from "../../InkingStroke"; -import { Scripting } from "../../../util/Scripting"; -import { InkTool } from "../../../../fields/InkField"; -import { ColorState } from "react-color"; -import { Utils } from "../../../../Utils"; -import GestureOverlay from "../../GestureOverlay"; -import { Doc } from "../../../../fields/Doc"; -import { SelectionManager } from "../../../util/SelectionManager"; -import { DocumentView } from "../../../views/nodes/DocumentView"; -import { Document } from "../../../../fields/documentSchemas"; -import { DocumentType } from "../../../documents/DocumentTypes"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { IconProp, library } from '@fortawesome/fontawesome-svg-core'; -import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faArrowsAlt, faHighlighter, faLink, faPaintRoller, faSleigh, faBars, faFillDrip, faBrush, faPenNib, faShapes, faArrowLeft, faEllipsisH, faBezierCurve, } from "@fortawesome/free-solid-svg-icons"; - -library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faArrowsAlt, faHighlighter, faLink, faPaintRoller, faBars, faFillDrip, faBrush, faPenNib, faShapes, faArrowLeft, faEllipsisH, faBezierCurve); - -@observer -export default class InkOptionsMenu extends AntimodeMenu { - static Instance: InkOptionsMenu; - - private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", "none"]; - private _width = ["1", "5", "10", "100"]; - // private _buttons = ["circle", "triangle", "rectangle", "arrow", "line"]; - // private _icons = ["O", "∆", "ロ", "➜", "-"]; - private _buttons = ["circle", "triangle", "rectangle", "line", "noRec", "",]; - private _icons = ["O", "∆", "ロ", "⎯", "✖︎", " "]; - //arrowStart and arrowEnd must match and defs must exist in Inking Stroke - private _arrowStart = ["arrowHead", "arrowHead", "dot", "dot", "none"]; - private _arrowEnd = ["none", "arrowEnd", "none", "dot", "none"]; - private _arrowIcons = ["→", "↔︎", "•", "••", " "]; - - @observable _colorBtn = false; - @observable _widthBtn = false; - @observable _fillBtn = false; - @observable _arrowBtn = false; - @observable _dashBtn = false; - @observable _shapeBtn = false; - - constructor(props: Readonly<{}>) { - super(props); - InkOptionsMenu.Instance = this; - this._canFade = false; // don't let the inking menu fade away - } - - getColors = () => { - return this._palette; - } - - @action - changeArrow = (arrowStart: string, arrowEnd: string) => { - SetActiveArrowStart(arrowStart); - SetActiveArrowEnd(arrowEnd); - } - - @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 "bezier": - // doc.strokeBezier === 300 ? doc.strokeBezier = 0 : doc.strokeBezier = 300; - break; - case "arrowStart": - doc.arrowStart = String(value); - break; - case "arrowEnd": - doc.arrowEnd = String(value); - break; - case "dash": - doc.dash = Number(value); - default: - break; - } - } - })); - } - - - @action - changeBezier = (e: React.PointerEvent): void => { - SetActiveBezierApprox(!ActiveInkBezierApprox() ? "300" : ""); - this.editProperties(0, "bezier"); - } - @action - changeDash = (e: React.PointerEvent): void => { - SetActiveDash(ActiveDash() === "0" ? "2" : "0"); - this.editProperties(ActiveDash(), "dash"); - } - - @computed get arrowPicker() { - var currIcon; - for (var i = 0; i < this._arrowStart.length; i++) { - if (this._arrowStart[i] === ActiveArrowStart() && this._arrowEnd[i] === ActiveArrowEnd()) { - currIcon = this._arrowIcons[i]; - if (this._arrowIcons[i] === " ") { - currIcon = "➤"; - } - } - } - var arrowPicker = <button - className="antimodeMenu-button" - key="arrow" - onPointerDown={action(e => this._arrowBtn = !this._arrowBtn)} - style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}> - {currIcon} - </button>; - if (this._arrowBtn) { - arrowPicker = <div className="btn2-group" key="arrows"> - {arrowPicker} - {this._arrowStart.map((arrowStart, i) => { - return <button - className="antimodeMenu-button" - key={arrowStart} - onPointerDown={action(() => { SetActiveArrowStart(arrowStart); SetActiveArrowEnd(this._arrowEnd[i]); this.editProperties(arrowStart, "arrowStart"), this.editProperties(this._arrowEnd[i], "arrowEnd"); this._arrowBtn = false; })} - style={{ backgroundColor: this._arrowBtn ? "121212" : "" }}> - {this._arrowIcons[i]} - </button>; - })} - </div>; - } - return arrowPicker; - } - - @computed get widthPicker() { - var widthPicker = <button - className="antimodeMenu-button" - key="width" - onPointerDown={action(e => this._widthBtn = !this._widthBtn)} - style={{ backgroundColor: this._widthBtn ? "121212" : "" }}> - <FontAwesomeIcon icon="bars" size="lg" /> - </button>; - if (this._widthBtn) { - widthPicker = <div className="btn2-group" key="width"> - {widthPicker} - {this._width.map(wid => { - return <button - className="antimodeMenu-button" - key={wid} - onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} - style={{ backgroundColor: this._widthBtn ? "121212" : "" }}> - {wid} - </button>; - })} - </div>; - } - return widthPicker; - } - - - - @computed get colorPicker() { - var colorPicker = <button - className="antimodeMenu-button" - key="color" - title="colorChanger" - onPointerDown={action(e => this._colorBtn = !this._colorBtn)} - style={{ backgroundColor: this._colorBtn ? "121212" : "" }}> - <FontAwesomeIcon icon="pen-nib" size="lg" /> - <div className="color-previewI" style={{ backgroundColor: ActiveInkColor() ?? "121212" }}></div> - - </button>; - if (this._colorBtn) { - colorPicker = <div className="btn-group" key="color"> - {colorPicker} - {this._palette.map(color => { - return <button - className="antimodeMenu-button" - key={color} - onPointerDown={action(() => { this.changeColor(color, "color"); this._colorBtn = false; this.editProperties(color, "color"); })} - style={{ backgroundColor: this._colorBtn ? "121212" : "" }}> - {/* <FontAwesomeIcon icon="pen-nib" size="lg" /> */} - <div className="color-previewII" style={{ backgroundColor: color }}></div> - </button>; - })} - </div>; - } - return colorPicker; - } - - @computed get fillPicker() { - var fillPicker = <button - className="antimodeMenu-button" - key="fill" - title="fillChanger" - onPointerDown={action(e => this._fillBtn = !this._fillBtn)} - style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> - <FontAwesomeIcon icon="fill-drip" size="lg" /> - <div className="color-previewI" style={{ backgroundColor: ActiveFillColor() ?? "121212" }}></div> - </button>; - if (this._fillBtn) { - fillPicker = <div className="btn-group" key="fill"> - {fillPicker} - {this._palette.map(color => { - return <button - className="antimodeMenu-button" - key={color} - onPointerDown={action(() => { this.changeColor(color, "fill"); this._fillBtn = false; this.editProperties(color, "fill"); })} - style={{ backgroundColor: this._fillBtn ? "121212" : "" }}> - <div className="color-previewII" style={{ backgroundColor: color }}></div> - </button>; - })} - - </div>; - } - return fillPicker; - } - - @computed get shapePicker() { - var currIcon; - if (GestureOverlay.Instance.InkShape === "") { - currIcon = <FontAwesomeIcon icon="shapes" size="lg" />; - } else { - for (var i = 0; i < this._icons.length; i++) { - if (GestureOverlay.Instance.InkShape === this._buttons[i]) { - currIcon = this._icons[i]; - } - } - } - var shapePicker = <button - className="antimodeMenu-button" - key="shape" - onPointerDown={action(e => this._shapeBtn = !this._shapeBtn)} - style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}> - {currIcon} - </button>; - if (this._shapeBtn) { - shapePicker = <div className="btn2-group" key="shape"> - {shapePicker} - {this._buttons.map((btn, i) => { - var ttl = btn; - if (btn === "") { - ttl = "no shape"; - } - if (btn === "noRec") { - ttl = "disable shape recognition"; - } - return <button - className="antimodeMenu-button" - title={`Draw ${btn}`} - key={ttl} - onPointerDown={action((e) => { GestureOverlay.Instance.InkShape = btn; this._shapeBtn = false; })} - style={{ backgroundColor: this._shapeBtn ? "121212" : "" }}> - {this._icons[i]} - </button>; - })} - </div>; - } - return shapePicker; - } - - @computed get bezierButton() { - return <button - className="antimodeMenu-button" - title="Bezier changer" - key="bezier" - onPointerDown={e => this.changeBezier(e)} - style={{ backgroundColor: ActiveInkBezierApprox() ? "121212" : "" }}> - <FontAwesomeIcon icon="bezier-curve" size="lg" /> - - </button>; - } - - @computed get dashButton() { - return <button - className="antimodeMenu-button" - title="dash changer" - key="dash" - onPointerDown={e => this.changeDash(e)} - style={{ backgroundColor: ActiveDash() !== "0" ? "121212" : "" }}> - <FontAwesomeIcon icon="ellipsis-h" size="lg" /> - - </button>; - } - - render() { - const buttons = [ - // <button className="antimodeMenu-button" title="Drag" key="drag" onPointerDown={e => this.dragStart(e)}> - // <FontAwesomeIcon icon="arrows-alt" size="lg" /> - // </button>, - this.shapePicker, - this.bezierButton, - this.widthPicker, - this.colorPicker, - this.fillPicker, - this.arrowPicker, - this.dashButton, - ]; - return this.getElement(buttons); - } -} -Scripting.addGlobal(function activatePen(penBtn: any) { - if (penBtn) { - Doc.SetSelectedTool(InkTool.Pen); - InkOptionsMenu.Instance.jumpTo(300, 300); - } else { - Doc.SetSelectedTool(InkTool.None); - InkOptionsMenu.Instance.fadeOut(true); - } -});
\ 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 97ed74c10..764758eee 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,6 +1,7 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; -import { Doc, Opt, DocListCast, DataSym } from "../../../../fields/Doc"; +import { Doc, Opt, DocListCast, DataSym, AclEdit, AclAddonly, AclAdmin } from "../../../../fields/Doc"; +import { GetEffectiveAcl, getPlaygroundMode } from "../../../../fields/util"; import { InkData, InkField, InkTool } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -129,7 +130,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } 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), + _width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: StrCast(Doc.UserDoc().fontSize), _fontFamily: StrCast(Doc.UserDoc().fontFamily), title: "-typed text-" }); @@ -188,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(); + } } } @@ -276,7 +280,8 @@ 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) || getPlaygroundMode()) PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge); this.clearSelection(); } }); @@ -286,7 +291,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { if (Doc.GetSelectedTool() === InkTool.None) { - !(e.nativeEvent as any).formattedHandled && this.setPreviewCursor(e.clientX, e.clientY, false); + 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? @@ -426,8 +434,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 => { @@ -468,7 +474,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 })); }); @@ -493,7 +498,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); diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss index 9c2d5cbff..4d8473be9 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.scss +++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss @@ -8,7 +8,6 @@ .collectionGridView-gridContainer { height: 100%; overflow-y: auto; - background-color: white; overflow-x: hidden; display: flex; @@ -22,7 +21,7 @@ } .react-grid-layout { - width : 100%; + width: 100%; } .react-grid-item>.react-resizable-handle { diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index 2015ca930..21f77e47b 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -1,7 +1,7 @@ import { action, computed, Lambda, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from "react"; -import { Doc, Opt } from '../../../../fields/Doc'; +import { Doc, Opt, WidthSym } from '../../../../fields/Doc'; import { documentSchema } from '../../../../fields/documentSchemas'; import { Id } from '../../../../fields/FieldSymbols'; import { makeInterface } from '../../../../fields/Schema'; @@ -30,8 +30,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { 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 - @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); } + 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; } @@ -47,6 +48,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { @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[] = []; @@ -54,7 +58,15 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { pairs.forEach((pair, i) => { const existing = oldLayouts.find(l => l.i === pair.layout[Id]); if (existing) newLayouts.push(existing); - else this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid)); + 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 }); @@ -68,11 +80,18 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { }); } + /** + * 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, @@ -83,6 +102,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { }; } + /** + * 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); @@ -90,10 +112,16 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { 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); @@ -215,6 +243,9 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { this.savedLayoutList.map((layout, index) => Object.assign(layout, this.unflexedPosition(index))); } + /** + * Handles internal drop of Dash documents. + */ @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const savedLayouts = this.savedLayoutList; @@ -228,12 +259,24 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { } /** + * 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 @@ -248,11 +291,13 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { */ onContextMenu = () => { const displayOptionsMenu: ContextMenuProps[] = []; - displayOptionsMenu.push({ description: "Contents", event: () => this.props.Document.display = "contents", icon: "copy" }); - displayOptionsMenu.push({ description: "Undefined", event: () => this.props.Document.display = undefined, icon: "exclamation" }); + 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, @@ -276,8 +321,11 @@ export class CollectionGridView extends CollectionSubView(GridSchema) { <div className="collectionGridView-contents" ref={this.createDashEventsTarget} style={{ pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }} onContextMenu={this.onContextMenu} - onPointerDown={e => this.onPointerDown(e)} > + 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; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 776266ce6..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) { diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 1703ff4dc..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) { |
