diff options
| author | Melissa Zhang <mzhang19096@gmail.com> | 2020-07-23 13:33:29 -0700 |
|---|---|---|
| committer | Melissa Zhang <mzhang19096@gmail.com> | 2020-07-23 13:33:29 -0700 |
| commit | 38b264599af2dca710b6c54d76cf30aade5e2f49 (patch) | |
| tree | 4ec3b7fb03fcc4c81160dedea651f41cdb84c331 /src/client/views/collections | |
| parent | 86a8751ca5a66fd42d7b2b3dee56210f2ef2b62d (diff) | |
| parent | 3bcc0e3a8ce4ab67dff4b3d62191c346764aa351 (diff) | |
merge with master
Diffstat (limited to 'src/client/views/collections')
28 files changed, 1296 insertions, 952 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 bd0e4fc9a..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..53b2d5254 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 @@ -646,16 +655,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 +665,6 @@ interface DockedFrameProps { documentId: FieldId; glContainer: any; libraryPath: (FieldId[]); - backgroundColor?: (doc: Doc) => string | undefined; //collectionDockingView: CollectionDockingView } @observer @@ -818,35 +818,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.tsx b/src/client/views/collections/CollectionLinearView.tsx index 0bbe97ec9..cd6e60de6 100644 --- a/src/client/views/collections/CollectionLinearView.tsx +++ b/src/client/views/collections/CollectionLinearView.tsx @@ -16,6 +16,7 @@ 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,]>; @@ -110,17 +111,22 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { 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, - // 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> + <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)} /> @@ -170,12 +176,21 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) { }} onPointerDown={e => e.stopPropagation()} > <span className="bottomPopup-text" > - Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in" : ""} {DocumentLinksButton.StartLink.title} </span> - <span className="bottomPopup-descriptions" onClick={this.changeDescriptionSetting} - > Labels: {LinkDescriptionPopup.showDescriptions ? LinkDescriptionPopup.showDescriptions : "ON"} + Creating link from: {DocumentLinksButton.AnnotationId ? "Annotation in" : ""} {DocumentLinksButton.StartLink.title} </span> - <span className="bottomPopup-exit" onClick={this.exitLongLinks} - >Exit</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} /> */} 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 b1e8d20ad..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,7 +88,7 @@ .collectionViewBaseChrome-collapse { transition: all .5s, opacity 0.3s; position: absolute; - width: 40px; + width: 30px; transform-origin: top left; pointer-events: all; // margin-top: 10px; @@ -94,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 { @@ -190,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 { @@ -297,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; @@ -327,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/CollectionViewChromes.tsx b/src/client/views/collections/CollectionMenu.tsx index 7f1fe7649..992c1f600 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -1,74 +1,107 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { action, computed, observable, runInAction, Lambda } from "mobx"; +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 * 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 { BoolCast, Cast, StrCast, NumCast } from "../../../fields/Types"; +import AntimodeMenu from "../AntimodeMenu"; +import "./CollectionMenu.scss"; 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 { 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"; -interface CollectionViewChromeProps { - CollectionView: CollectionView; - type: CollectionViewType; - collapse?: (value: boolean) => any; - PanelWidth: () => number; +@observer +export default class CollectionMenu extends AntimodeMenu { + static Instance: CollectionMenu; + + @observable SelectedCollection: CollectionView | undefined; + + constructor(props: Readonly<{}>) { + super(props); + 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); + } + + @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" CollectionView={this.SelectedCollection} type={StrCast(this.SelectedCollection.props.Document._viewType) as CollectionViewType} />, + button]); + } } -interface Filter { - key: string; - value: string; - contains: boolean; +interface CollectionMenuProps { + CollectionView: CollectionView; + type: CollectionViewType; } const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); @observer -export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionViewBaseChrome extends React.Component<CollectionMenuProps> { //(!)?\(\(\(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]))), + params: ["target", "source"], title: "item view", + script: "this.target.childLayoutTemplate = getDocTemplate(this.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", + 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", + 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; }, + params: ["target"], title: "bookmark view", + script: "this.target._panX = this['target-panX']; this.target._panY = this['target-panY']; this.target._viewScale = this['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", + 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", + params: ["target"], title: "toggle clusters", script: "this.target.useClusters = !this.target.useClusters;", immediate: undoBatch((source: Doc[]) => this.target.useClusters = !this.target.useClusters), initialize: emptyFunction @@ -99,14 +132,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro 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 @@ -130,93 +155,16 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro 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} />); + case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Carousel3D: return (<Collection3DCarouselViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Grid: return (<CollectionGridViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); default: return null; } } @@ -270,15 +218,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } @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 }}> + 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}> - <div className="commandEntry-drop"> - <FontAwesomeIcon icon="bullseye" size="2x" /> - </div> + <button className={"antimodeMenu-button"} > + <FontAwesomeIcon icon="bullseye" size="lg" /> + </button> <select - className="collectionViewBaseChrome-cmdPicker" onPointerDown={stopPropagation} onChange={this.commandChanged} value={this._currentKey} - style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }}> + 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> @@ -289,14 +235,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } @computed get viewModes() { - const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled"; - return <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}> + 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}> - <div className="commandEntry-drop"> - <FontAwesomeIcon icon="bullseye" size="2x" /> - </div> + <button className={"antimodeMenu-button"}> + <FontAwesomeIcon icon="bullseye" size="lg" /> + </button> <select - className="collectionViewBaseChrome-viewPicker" style={{ width: this.props.PanelWidth() < 300 ? 15 : undefined }} + className="collectionViewBaseChrome-viewPicker" onPointerDown={stopPropagation} onChange={this.viewChanged} value={StrCast(this.props.CollectionView.props.Document._viewType)}> @@ -315,34 +260,17 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } 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="collectionMenu-cont" > + <div className="collectionMenu"> <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 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> </div> {this.subChrome} </div> @@ -352,8 +280,12 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro } @observer -export class CollectionFreeFormViewChrome extends React.Component<CollectionViewChromeProps> { - +export class CollectionFreeFormViewChrome extends React.Component<CollectionMenuProps> { + public static Instance: CollectionFreeFormViewChrome; + constructor(props: any) { + super(props); + CollectionFreeFormViewChrome.Instance = this; + } get Document() { return this.props.CollectionView.props.Document; } @computed get dataField() { return this.props.CollectionView.props.Document[Doc.LayoutFieldKey(this.props.CollectionView.props.Document)]; @@ -364,7 +296,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView @undoBatch @action nextKeyframe = (): void => { - const currentFrame = NumCast(this.Document.currentFrame); + const currentFrame = Cast(this.Document.currentFrame, "number", null); if (currentFrame === undefined) { this.Document.currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); @@ -376,7 +308,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView @undoBatch @action prevKeyframe = (): void => { - const currentFrame = NumCast(this.Document.currentFrame); + const currentFrame = Cast(this.Document.currentFrame, "number", null); if (currentFrame === undefined) { this.Document.currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0); @@ -384,9 +316,155 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView 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"]; + + @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" key={icon} onPointerDown={() => func(i, false)} onDoubleClick={() => func(i, true)} + style={{ backgroundColor: i === this._selected ? "121212" : "", fontSize: "20" }}> + {this._draw[i]} + </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 => + <button className="antimodeMenu-button" key={wid} + onPointerDown={action(() => { SetActiveInkWidth(wid); this._widthBtn = false; this.editProperties(wid, "width"); })} + style={{ backgroundColor: this._widthBtn ? "121212" : "", zIndex: 1001 }}> + {wid} + </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="chevron-right" size="lg" /> + </button>; + } + render() { return this.Document.isAnnotationOverlay ? (null) : - <div className="collectionFreeFormViewChrome-cont"> + <div className="collectionFreeFormMenu-cont"> + <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> @@ -397,12 +475,17 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}> <FontAwesomeIcon icon={"caret-right"} size={"lg"} /> </div> + + {this.widthPicker} + {this.colorPicker} + {this.fillPicker} + {this.drawButtons} + {this.formatPane} </div>; } } - @observer -export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionStackingViewChrome extends React.Component<CollectionMenuProps> { @observable private _currentKey: string = ""; @observable private suggestions: string[] = []; @@ -502,7 +585,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView @observer -export class CollectionSchemaViewChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionSchemaViewChrome extends React.Component<CollectionMenuProps> { // private _textwrapAllRows: boolean = Cast(this.props.CollectionView.props.Document.textwrappedSchemaRows, listSpec("string"), []).length > 0; @undoBatch @@ -549,7 +632,7 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh } @observer -export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionTreeViewChrome extends React.Component<CollectionMenuProps> { get sortAscending() { return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"]; @@ -585,7 +668,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro // Enter scroll speed for 3D Carousel @observer -export class Collection3DCarouselViewChrome extends React.Component<CollectionViewChromeProps> { +export class Collection3DCarouselViewChrome extends React.Component<CollectionMenuProps> { @computed get scrollSpeed() { return this.props.CollectionView.props.Document._autoScrollSpeed; } @@ -624,7 +707,7 @@ export class Collection3DCarouselViewChrome extends React.Component<CollectionVi * Chrome for grid view. */ @observer -export class CollectionGridViewChrome extends React.Component<CollectionViewChromeProps> { +export class CollectionGridViewChrome extends React.Component<CollectionMenuProps> { private clicked: boolean = false; private entered: boolean = false; @@ -770,7 +853,7 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewChro </span> <select className="collectionGridViewChrome-viewPicker" - style={{ marginRight: 5, width: this.props.PanelWidth() < 300 ? 25 : undefined }} + style={{ marginRight: 5 }} onPointerDown={stopPropagation} onChange={this.changeCompactType} value={StrCast(this.props.CollectionView.props.Document.gridStartCompaction, StrCast(this.props.CollectionView.props.Document.gridCompaction))}> @@ -797,4 +880,4 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewChro </div> ); } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index d76b6d204..eecaf7672 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -151,7 +151,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); @@ -265,7 +264,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> { return "0"; } else { const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey])); - console.log(cfield); if (type === "number") { return StrCast(cfield); } @@ -286,7 +284,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"); } } @@ -350,17 +347,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]); //} } @@ -425,8 +418,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; @@ -457,10 +448,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); } } @@ -675,7 +664,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 3c42a2f1c..5553bbbb7 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -332,7 +332,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.tsx b/src/client/views/collections/CollectionStackingView.tsx index 9d8790dda..dd4c34885 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -123,7 +123,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; } @@ -163,8 +163,8 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument) this.createDashEventsTarget(ele!); //so the whole grid is the drop target? } - @computed get onChildClickHandler() { return this.props.childClickScript || ScriptCast(this.Document.onChildClick); } - @computed get onChildDoubleClickHandler() { return this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } + @computed get onChildClickHandler() { return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); } + @computed get onChildDoubleClickHandler() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } addDocTab = (doc: Doc, where: string) => { if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { @@ -249,7 +249,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) => { 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 ce6872695..8480a56cc 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 } 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; @@ -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]; } @@ -214,6 +220,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; @@ -306,9 +313,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, @@ -333,10 +340,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; @@ -363,7 +380,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) => { @@ -377,13 +393,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 { @@ -391,13 +414,17 @@ 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"; @@ -405,4 +432,6 @@ import { DocumentType } from "../../documents/DocumentTypes"; import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox"; import { CollectionView } 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 26c41f524..651357e5d 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>(); @@ -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); } @@ -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 df21d6a28..e2f78d6f9 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import Lightbox from 'react-image-lightbox-with-rotate'; import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app import { DateField } from '../../../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 } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; @@ -17,7 +17,7 @@ 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 { TraceMobx, GetEffectiveAcl, getPlaygroundMode, distributeAcls } from '../../../fields/util'; import { emptyFunction, emptyPath, returnEmptyFilter, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -47,7 +47,8 @@ import { SubCollectionViewProps } from './CollectionSubView'; import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import './CollectionView.scss'; -import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import CollectionMenu from './CollectionMenu'; +import { SharingPermissions } from '../../util/SharingManager'; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -106,6 +107,13 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + private AclMap = new Map<symbol, string>([ + [AclPrivate, SharingPermissions.None], + [AclReadonly, SharingPermissions.View], + [AclAddonly, SharingPermissions.Add], + [AclEdit, SharingPermissions.Edit] + ]); + get collectionViewType(): CollectionViewType | undefined { const viewField = StrCast(this.props.Document._viewType); if (CollectionView._safeMode) { @@ -128,36 +136,54 @@ 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 === 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())); + } + 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())); + } } } return true; @@ -165,13 +191,15 @@ 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; + if (GetEffectiveAcl(this.props.Document) === AclEdit || 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; } @@ -234,16 +262,8 @@ 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); } @@ -272,7 +292,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus 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" }); + !existingVm && ContextMenu.Instance.addItem({ description: category, noexpand: true, subitems: subItems, icon: "eye" }); } onContextMenu = (e: React.MouseEvent): void => { @@ -285,18 +305,18 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus return newRendition; }, false); - const existing = cm.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 && cm.addItem({ description: "Options...", subitems: layoutItems, icon: "hand-point-right" }); + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" }); const existingOnClick = cm.findByDescription("OnClick..."); const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; @@ -305,16 +325,18 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus { 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 && cm.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 = cm.findByDescription("More..."); @@ -470,7 +492,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(); @@ -523,7 +546,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} @@ -549,6 +572,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus ChildLayoutTemplate: this.childLayoutTemplate, ChildLayoutString: this.childLayoutString, }; + setTimeout(action(() => this.props.isSelected(true) && (CollectionMenu.Instance.SelectedCollection = this)), 0); const boxShadow = Doc.UserDoc().renderStyle === "comic" || this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31) " : "#9c9396 "} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`; return (<div className={"collectionView"} onContextMenu={this.onContextMenu} diff --git a/src/client/views/collections/SchemaTable.tsx b/src/client/views/collections/SchemaTable.tsx index 9e3b4d961..cde795098 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>[] { 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 9bf425db2..979b21321 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -46,6 +46,8 @@ import "./CollectionFreeFormView.scss"; import MarqueeOptionsMenu from "./MarqueeOptionsMenu"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); +import { SearchUtil } from "../../../util/SearchUtil"; +import { LinkManager } from "../../../util/LinkManager"; library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload); @@ -75,7 +77,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 @@ -101,6 +104,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @observable _clusterSets: (Doc[])[] = []; @observable _timelineRef = React.createRef<Timeline>(); + @observable _marqueeRef = React.createRef<HTMLDivElement>(); + @observable canPanX: boolean = true; + @observable canPanY: boolean = true; + @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; } @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } @@ -180,7 +187,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); } @@ -205,7 +212,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 { @@ -490,10 +497,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); this._inkToTextStartX = start[0]; this._inkToTextStartY = start[1]; - console.log("start"); break; case GestureUtils.Gestures.EndBracket: - console.log("end"); if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "rtf" && s.color); @@ -932,9 +937,9 @@ 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 { @@ -946,8 +951,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P 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, @@ -1027,7 +1032,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 }}> @@ -1065,7 +1070,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,10 +1148,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this._layoutComputeReaction = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); + + this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } + componentWillUnmount() { this._layoutComputeReaction?.(); + this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } + @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } elementFunc = () => this._layoutElements; @@ -1155,6 +1165,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 => { @@ -1205,55 +1238,69 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P 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("View 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: "View 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 }, @@ -1336,7 +1383,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} @@ -1346,14 +1394,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>; } @@ -1370,6 +1419,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} @@ -1388,7 +1438,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={{ diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.scss b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss new file mode 100644 index 000000000..88876471c --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.scss @@ -0,0 +1,64 @@ +.antimodeMenu-button { + width: 200px; + position: relative; + text-align: left; + + .color-previewI { + width: 100%; + height: 40%; + } + + .color-previewII { + width: 100%; + height: 100%; + } +} + +.antimenu-Buttonup { + position: absolute; + width: 20; + height: 10; + right: 0; + padding: 0; +} + +.formatShapePane-inputBtn { + width: inherit; + position: absolute; +} + +.sketch-picker { + background: #323232; + + .flexbox-fit { + background: #323232; + } +} + +.btn-group { + display: grid; + grid-template-columns: auto auto auto auto; + /* Make the buttons appear below each other */ +} + +.btn-group-palette { + display: block; + /* Make the buttons appear below each other */ +} + +.btn-draw { + display: inline; + /* Make the buttons appear below each other */ +} + +.btn2-group { + display: block; + background: #323232; + grid-template-columns: auto; + + /* Make the buttons appear below each other */ + .antimodeMenu-button { + background: #323232; + display: block; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx new file mode 100644 index 000000000..4e328d838 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FormatShapePane.tsx @@ -0,0 +1,307 @@ +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"; + +@observer +export default class FormatShapePane extends AntimodeMenu { + static Instance: FormatShapePane; + + private _lastFill = "#D0021B"; + private _lastLine = "#D0021B"; + private _lastDash = "2"; + private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF"]; + private _mode = ["fill-drip", "ruler-combined"]; + + @observable private _subOpen = [false, false, false, false]; + @observable private _currMode = "fill-drip"; + @observable private _lock = false; + @observable private _fillBtn = false; + @observable private _lineBtn = false; + + 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 "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 => { + const oldWidth = NumCast(i.rootDoc._width); + i.rootDoc._width = oldWidth + (dirs === "up" ? 10 : - 10); + this._lock && (i.rootDoc._height = (i.rootDoc._width / oldWidth * NumCast(i.rootDoc._height))); + }); + break; + case "hgt": this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + const oldHeight = NumCast(i.rootDoc._height); + i.rootDoc._height = oldHeight + (dirs === "up" ? 10 : - 10); + this._lock && (i.rootDoc._width = (i.rootDoc._height / oldHeight * NumCast(i.rootDoc._width))); + }); + break; + } + } + + @undoBatch + @action + rotate = (degrees: number) => { + this.selectedInk?.forEach(action(inkView => { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.x && doc.y && doc._width && doc._height && doc.data) { + const angle = Number(degrees) - Number(doc.rotation); + doc.rotation = Number(degrees); + 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); + const _centerPoints: { X: number, Y: number }[] = []; + _centerPoints.push({ X: left, Y: top }); + + const newPoints: { X: number, Y: number }[] = []; + for (var i = 0; i < ink.length; i++) { + const newX = Math.cos(angle) * (ink[i].X - _centerPoints[0].X) - Math.sin(angle) * (ink[i].Y - _centerPoints[0].Y) + _centerPoints[0].X; + const newY = Math.sin(angle) * (ink[i].X - _centerPoints[0].X) + Math.cos(angle) * (ink[i].Y - _centerPoints[0].Y) + _centerPoints[0].Y; + newPoints.push({ X: newX, Y: newY }); + } + 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) * inkView.props.ScreenToLocalTransform().Scale; + doc._width = (right2 - left2) * inkView.props.ScreenToLocalTransform().Scale; + } + } + })); + } + + + colorPicker(setter: (color: string) => {}) { + return <div className="btn-group-palette" key="colorpicker" > + {this._palette.map(color => + <button className="antimodeMenu-button" key={color} onPointerDown={undoBatch(action(() => setter(color)))} style={{ zIndex: 1001, position: "relative" }}> + <div className="color-previewII" style={{ backgroundColor: color }} /> + </button>)} + </div>; + } + inputBox = (key: string, value: any, setter: (val: string) => {}) => { + return <> + <input style={{ color: "black", width: 80, position: "absolute", right: 20 }} + type="text" value={value} + onChange={e => setter(e.target.value)} + autoFocus /> + <button className="antiMenu-Buttonup" key="up" onPointerDown={undoBatch(action(() => this.upDownButtons("up", key)))}> + ˄ + </button> + <br /> + <button className="antiMenu-Buttonup" key="down" onPointerDown={undoBatch(action(() => this.upDownButtons("down", key)))} style={{ marginTop: -8 }}> + ˅ + </button> + </>; + } + + colorButton(value: string, setter: () => {}) { + return <> + <button className="antimodeMenu-button" key="fill" onPointerDown={undoBatch(action(e => setter()))} style={{ position: "absolute", right: 80 }}> + <FontAwesomeIcon icon="fill-drip" size="lg" /> + <div className="color-previewI" style={{ backgroundColor: value ?? "121212" }} /> + </button> + <br /> <br /> + </>; + } + + @computed get fillButton() { return this.colorButton(this.colorFil, () => this._fillBtn = !this._fillBtn); } + @computed get lineButton() { return this.colorButton(this.colorStk, () => this._lineBtn = !this._lineBtn); } + + @computed get fillPicker() { return this.colorPicker((color: string) => this.colorFil = color); } + @computed get linePicker() { return this.colorPicker((color: string) => this.colorStk = color); } + + @computed get stkInput() { return this.inputBox("stk", this.widthStk, (val: string) => this.widthStk = val); } + @computed get hgtInput() { return this.inputBox("hgt", this.shapeHgt, (val: string) => this.shapeHgt = val); } + @computed get widInput() { return this.inputBox("wid", this.shapeWid, (val: string) => this.shapeWid = val); } + @computed get rotInput() { return this.inputBox("rot", this.shapeRot, (val: string) => this.shapeRot = val); } + @computed get XpsInput() { return this.inputBox("Xps", this.shapeXps, (val: string) => this.shapeXps = val); } + @computed get YpsInput() { return this.inputBox("Yps", this.shapeYps, (val: string) => this.shapeYps = val); } + + @computed get propertyGroupItems() { + const fillCheck = <div key="fill" style={{ display: this._subOpen[0] ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> + <input className="formatShapePane-inputBtn" type="radio" checked={this.unFilled} onChange={undoBatch(action(() => this.unFilled = true))} /> + No Fill + <br /> + <input className="formatShapePane-inputBtn" type="radio" checked={this.solidFil} onChange={undoBatch(action(() => this.solidFil = true))} /> + Solid Fill + <br /> <br /> + {this.solidFil ? "Color" : ""} + {this.solidFil ? this.fillButton : ""} + {this._fillBtn && this.solidFil ? this.fillPicker : ""} + </div>; + + const markers = <> + <input key="markHead" className="formatShapePane-inputBtn" type="checkbox" checked={this.markHead !== ""} onChange={undoBatch(action(() => this.markHead = this.markHead ? "" : "arrow"))} /> + Arrow Head + <br /> + <input key="markTail" className="formatShapePane-inputBtn" type="checkbox" checked={this.markTail !== ""} onChange={undoBatch(action(() => this.markTail = this.markTail ? "" : "arrow"))} /> + Arrow End + <br /> + </>; + + const lineCheck = <div key="lineCheck" style={{ display: this._subOpen[1] ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> + <input className="formatShapePane-inputBtn" type="radio" checked={this.unStrokd} onChange={undoBatch(action(() => this.unStrokd = true))} /> + No Line + <br /> + <input className="formatShapePane-inputBtn" type="radio" checked={this.solidStk} onChange={undoBatch(action(() => this.solidStk = true))} /> + Solid Line + <br /> + <input className="formatShapePane-inputBtn" type="radio" checked={this.dashdStk ? true : false} onChange={undoBatch(action(() => this.dashdStk = "2"))} /> + Dash Line + <br /> + <br /> + {(this.solidStk || this.dashdStk) ? "Color" : ""} + {(this.solidStk || this.dashdStk) ? this.lineButton : ""} + {(this.solidStk || this.dashdStk) && this._lineBtn ? this.linePicker : ""} + <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={e => this.widthStk = e.target.value} /> : (null)} + <br /> <br /> + {(this.solidStk || this.dashdStk) ? markers : ""} + </div>; + + const sizeCheck = <div key="sizeCheck" style={{ display: this._subOpen[2] ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> + Height {this.hgtInput} + <br /> <br /> + Width {this.widInput} + <br /> <br /> + <input className="formatShapePane-inputBtn" style={{ right: 0 }} type="checkbox" checked={this._lock} onChange={undoBatch(action(() => this._lock = !this._lock))} /> + Lock Ratio + <br /> <br /> + Rotation {this.rotInput} + <br /> <br /> + </div>; + + const positionCheck = <div key="posCheck" style={{ display: this._subOpen[3] ? "" : "none", width: "inherit", backgroundColor: "#323232", color: "white", }}> + Horizontal {this.XpsInput} + <br /> <br /> + Vertical {this.YpsInput} + <br /> <br /> + </div>; + + const subMenus = this._currMode === "fill-drip" ? [`fill`, `line`] : [`size`, `position`]; + const menuItems = this._currMode === "fill-drip" ? [fillCheck, lineCheck] : [sizeCheck, positionCheck]; + const indexOffset = this._currMode === "fill-drip" ? 0 : 2; + 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.propertyGroupBtn, this.propertyGroupItems]); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss b/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss deleted file mode 100644 index 753de6bef..000000000 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.scss +++ /dev/null @@ -1,78 +0,0 @@ -.antimodeMenu-button { - .color-previewI { - width: 100%; - height: 40%; - } - - .color-previewII { - width: 100%; - height: 100%; - } - -} - -.sketch-picker { - background: #323232; - - .flexbox-fit { - background: #323232; - } -} - -.btn-group { - display: grid; - grid-template-columns: auto auto auto auto; - /* Make the buttons appear below each other */ -} - -.btn2-group { - display: block; - background: #323232; - grid-template-columns: auto; - - /* Make the buttons appear below each other */ - .antimodeMenu-button { - background: #323232; - display: block; - - } -} - -@media only screen and (max-device-width: 480px) { - .antimodeMenu-button { - font-size: 50%; - - .color-preview { - width: 100%; - height: 100%; - } - - } - - .sketch-picker { - background: #323232; - - .flexbox-fit { - background: #323232; - } - } - - .btn-group { - display: grid; - grid-template-columns: auto auto; - /* Make the buttons appear below each other */ - } - - .btn2-group { - display: block; - background: #323232; - grid-template-columns: auto; - - /* Make the buttons appear below each other */ - .antimodeMenu-button { - background: #323232; - display: block; - font-size: 50%; - } - } -}
\ 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 f47fca6ac..000000000 --- a/src/client/views/collections/collectionFreeForm/InkOptionsMenu.tsx +++ /dev/null @@ -1,332 +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); - } -}); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b47236bea..aa9a3b4ae 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 } 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 (effectiveAcl === AclEdit || effectiveAcl === AclAddonly || 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 })); }); 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 cd25c21b4..21d283547 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -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 51dcdcfe6..d02088a6c 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -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) { |
