diff options
| author | Aubrey Li <Aubrey-Li> | 2021-09-15 14:41:24 -0400 |
|---|---|---|
| committer | Aubrey Li <Aubrey-Li> | 2021-09-15 14:41:24 -0400 |
| commit | eb63330e172935343767d0dcc7ffad9bfa1a75c4 (patch) | |
| tree | 031bf155df50200f9652e881aec18002bc9e399e /src/client/views/collections | |
| parent | b7a88c6292c2e7bfffc3cdc4f7c7037922b3de25 (diff) | |
| parent | 8386ad690c10d5c76bbd1b4f85314514b7f11b55 (diff) | |
merge master into trails-aubrey
Diffstat (limited to 'src/client/views/collections')
32 files changed, 2266 insertions, 1370 deletions
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index cae08e1f4..5325d5827 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -428,7 +428,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { const emptyPane = CurrentUserUtils.EmptyPane; emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _backgroundGridShow: true, _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); CollectionDockingView.AddSplit(docToAdd, "", stack); @@ -453,7 +453,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { const emptyPane = CurrentUserUtils.EmptyPane; emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, _backgroundGridShow: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); CollectionDockingView.AddSplit(docToAdd, "", stack); diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx deleted file mode 100644 index 52c836556..000000000 --- a/src/client/views/collections/CollectionLinearView.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { Tooltip } from '@material-ui/core'; -import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; -import { documentSchema } from '../../../fields/documentSchemas'; -import { Id } from '../../../fields/FieldSymbols'; -import { makeInterface } from '../../../fields/Schema'; -import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, Utils } from '../../../Utils'; -import { DragManager } from '../../util/DragManager'; -import { Transform } from '../../util/Transform'; -import { DocumentLinksButton } from '../nodes/DocumentLinksButton'; -import { DocumentView } from '../nodes/DocumentView'; -import { LinkDescriptionPopup } from '../nodes/LinkDescriptionPopup'; -import { StyleProp } from '../StyleProvider'; -import "./CollectionLinearView.scss"; -import { CollectionSubView } from './CollectionSubView'; -import { CollectionViewType } from './CollectionView'; - - -type LinearDocument = makeInterface<[typeof documentSchema,]>; -const LinearDocument = makeInterface(documentSchema); - -@observer -export class CollectionLinearView extends CollectionSubView(LinearDocument) { - @observable public addMenuToggle = React.createRef<HTMLInputElement>(); - @observable private _selectedIndex = -1; - private _dropDisposer?: DragManager.DragDropDisposer; - private _widthDisposer?: IReactionDisposer; - private _selectedDisposer?: IReactionDisposer; - - componentWillUnmount() { - this._dropDisposer?.(); - this._widthDisposer?.(); - this._selectedDisposer?.(); - this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); - } - - componentDidMount() { - this._widthDisposer = reaction(() => 5 + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.length * (this.rootDoc[HeightSym]()) : 10), - width => this.childDocs.length && (this.layoutDoc._width = width), - { fireImmediately: true } - ); - - this._selectedDisposer = reaction( - () => NumCast(this.layoutDoc.selectedIndex), - (i) => runInAction(() => { - this._selectedIndex = i; - let selected: any = undefined; - this.childLayoutPairs.map(async (pair, ind) => { - const isSelected = this._selectedIndex === ind; - if (isSelected) { - selected = pair; - } - else { - ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); - } - }); - if (selected && selected.layout) { - ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); - } - }), - { fireImmediately: true } - ); - } - protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view - this._dropDisposer && this._dropDisposer(); - if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); - } - } - - dimension = () => NumCast(this.rootDoc._height); // 2 * the padding - getTransform = (ele: React.RefObject<HTMLDivElement>) => () => { - if (!ele.current) return Transform.Identity(); - const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current); - return new Transform(-translateX, -translateY, 1); - } - - @action - exitLongLinks = () => { - if (DocumentLinksButton.StartLink) { - if (DocumentLinksButton.StartLink.Document) { - action((e: React.PointerEvent<HTMLDivElement>) => { - Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); - }); - } - } - DocumentLinksButton.StartLink = undefined; - DocumentLinksButton.StartLinkView = undefined; - } - - @action - changeDescriptionSetting = () => { - if (LinkDescriptionPopup.showDescriptions) { - if (LinkDescriptionPopup.showDescriptions === "ON") { - LinkDescriptionPopup.showDescriptions = "OFF"; - LinkDescriptionPopup.descriptionPopup = false; - } else { - LinkDescriptionPopup.showDescriptions = "ON"; - } - } else { - LinkDescriptionPopup.showDescriptions = "OFF"; - LinkDescriptionPopup.descriptionPopup = false; - } - } - - render() { - const guid = Utils.GenerateGuid(); - const flexDir: any = StrCast(this.Document.flexDirection); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - - const menuOpener = <label htmlFor={`${guid}`} style={{ pointerEvents: "all", cursor: "pointer", background: backgroundColor === color ? "black" : backgroundColor, }} - onPointerDown={e => e.stopPropagation()} > - <p>{BoolCast(this.layoutDoc.linearViewIsExpanded) ? "–" : "+"}</p> - </label>; - - return <div className="collectionLinearView-outer"> - <div className="collectionLinearView" ref={this.createDashEventsTarget} > - <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.layoutDoc.linearViewIsExpanded) ? "Close menu" : "Open menu"}</div></>} placement="top"> - {menuOpener} - </Tooltip> - <input id={`${guid}`} type="checkbox" checked={BoolCast(this.layoutDoc.linearViewIsExpanded)} ref={this.addMenuToggle} - onChange={action(() => this.layoutDoc.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> - - <div className="collectionLinearView-content" style={{ height: this.dimension(), flexDirection: flexDir }}> - {this.childLayoutPairs.map((pair, ind) => { - const nested = pair.layout._viewType === CollectionViewType.Linear; - const dref = React.createRef<HTMLDivElement>(); - const scalable = pair.layout.onClick || pair.layout.onDragStart; - return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref} - style={{ - pointerEvents: "all", - minWidth: 30, - width: nested ? pair.layout[WidthSym]() : this.dimension(), - height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension(), - }} > - <DocumentView - Document={pair.layout} - DataDoc={pair.data} - isContentActive={returnFalse} - isDocumentActive={returnTrue} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - addDocTab={this.props.addDocTab} - pinToPres={emptyFunction} - rootSelected={this.props.isSelected} - removeDocument={this.props.removeDocument} - ScreenToLocalTransform={this.getTransform(dref)} - PanelWidth={nested ? pair.layout[WidthSym] : this.dimension} - PanelHeight={nested ? pair.layout[HeightSym] : this.dimension} - renderDepth={this.props.renderDepth + 1} - focus={emptyFunction} - styleProvider={this.props.styleProvider} - layerProvider={this.props.layerProvider} - docViewPath={returnEmptyDoclist} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> - </div>; - })} - </div> - {DocumentLinksButton.StartLink ? <span className="bottomPopup-background" style={{ - pointerEvents: "all" - }} - onPointerDown={e => e.stopPropagation()} > - <span className="bottomPopup-text" > - Creating link from: <b>{DocumentLinksButton.AnnotationId ? "Annotation in " : " "} {StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...'}</b> - </span> - - <Tooltip title={<><div className="dash-tooltip">{"Toggle 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 linking mode</div></>} placement="top"> - <span className="bottomPopup-exit" onClick={this.exitLongLinks}> - Stop - </span> - </Tooltip> - - </span> : null} - </div> - </div>; - } -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index f04b19ef7..c35f088a6 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -1,628 +1,659 @@ @import "../global/globalCssVariables"; -.collectionMenu-cont { - position: relative; - display: inline-flex; - width: 100%; - opacity: 0.9; - z-index: 901; - transition: top .5s; - background: $dark-gray; - color: $white; - transform-origin: top left; - top: 0; - width: 100%; - - .recordButtonOutline { - border-radius: 100%; - width: 18px; - height: 18px; - border: solid 1px $white; - display: flex; - align-items: center; - justify-content: center; - } - - .recordButtonInner { - border-radius: 100%; - width: 70%; - height: 70%; - background: $white; - } - - .collectionMenu { - display: flex; - height: 100%; - overflow: visible; - z-index: 901; - border: unset; - - .collectionMenu-divider { - height: 100%; - margin-left: 3px; - margin-right: 3px; - width: 2px; - background-color: $medium-gray; - } - - .collectionViewBaseChrome { - display: flex; - align-items: center; - - .collectionViewBaseChrome-viewPicker { - font-size: $small-text; - outline-color: $black; - color: $white; - border: none; - background: $dark-gray; - } - - .collectionViewBaseChrome-viewPicker:focus { - outline: none; - border: none; - } - - .collectionViewBaseChrome-viewPicker:active { - outline-color: $black; - } - - .collectionViewBaseChrome-button { - font-size: $small-text; - text-transform: uppercase; - letter-spacing: 2px; - background: $white; - color: $pink; - outline-color: $black; - border: none; - padding: 12px 10px 11px 10px; - margin-left: 10px; - } - - .collectionViewBaseChrome-cmdPicker { - margin-left: 3px; - margin-right: 0px; - font-size: $small-text; - text-transform: capitalize; - color: $white; - border: none; - background: $dark-gray; - } - - .collectionViewBaseChrome-cmdPicker:focus { - border: none; - outline: none; - } - - .commandEntry-outerDiv { - pointer-events: all; - background-color: transparent; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - height: 100%; - overflow: hidden; - - .commandEntry-drop { - color: $white; - width: 30px; - margin-top: auto; - margin-bottom: auto; - } - } - - .commandEntry-outerDiv:hover{ - background-color: $drop-shadow; - - .collectionViewBaseChrome-viewPicker, - .collectionViewBaseChrome-cmdPicker{ - background: $dark-gray; - } - } - - .collectionViewBaseChrome-collapse { - transition: all .5s, opacity 0.3s; - position: absolute; - width: 30px; - transform-origin: top left; - pointer-events: all; - // margin-top: 10px; - } - - @media only screen and (max-device-width: 480px) { - .collectionViewBaseChrome-collapse { - display: none; - } - } - - .collectionViewBaseChrome-template, - .collectionViewBaseChrome-viewModes { - align-items: center; - height: 100%; - display: flex; - background: transparent; - color: $medium-gray; - justify-content: center; - } - - .collectionViewBaseChrome-viewSpecs { - margin-left: 5px; - display: grid; - border: none; - border-right: solid $medium-gray 1px; - - .collectionViewBaseChrome-filterIcon { - position: relative; - display: flex; - margin: auto; - background: $dark-gray; - color: $white; - width: 30px; - height: 30px; - align-items: center; - justify-content: center; - border: none; - border-right: solid $medium-gray 1px; - } - - .collectionViewBaseChrome-viewSpecsInput { - padding: 12px 10px 11px 10px; - border: 0px; - color: $medium-gray; - text-align: center; - letter-spacing: 2px; - outline-color: $black; - font-size: $small-text; - background: $white; - height: 100%; - width: 75px; - } - - .collectionViewBaseChrome-viewSpecsMenu { - overflow: hidden; - transition: height .5s, display .5s; - position: absolute; - top: 60px; - z-index: 100; - display: flex; - flex-direction: column; - background: $white; - box-shadow: $medium-gray 2px 2px 4px; - - .qs-datepicker { - left: unset; - right: 0; - } - - .collectionViewBaseChrome-viewSpecsMenu-row { - display: grid; - grid-template-columns: 150px 200px 150px; - margin-top: 10px; - margin-right: 10px; - - .collectionViewBaseChrome-viewSpecsMenu-rowLeft, - .collectionViewBaseChrome-viewSpecsMenu-rowMiddle, - .collectionViewBaseChrome-viewSpecsMenu-rowRight { - font-size: $small-text; - letter-spacing: 2px; - color: $medium-gray; - margin-left: 10px; - padding: 5px; - border: none; - outline-color: $black; - } - } - - .collectionViewBaseChrome-viewSpecsMenu-lastRow { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - grid-gap: 10px; - margin: 10px; - } - } - } - } - - .collectionStackingViewChrome-cont, - .collectionTreeViewChrome-cont, - .collection3DCarouselViewChrome-cont { - display: flex; - justify-content: space-between; - } - - .collectionGridViewChrome-cont { - display: flex; - margin-left: 10; - - .collectionGridViewChrome-viewPicker { - font-size: $small-text; - //text-transform: uppercase; - //letter-spacing: 2px; - background: $dark-gray; - color: $white; - outline-color: $black; - color: $white; - border: none; - border-right: solid $medium-gray 1px; - } - - .collectionGridViewChrome-viewPicker:active { - outline-color: $black; - } - - .grid-control { - align-self: center; - display: flex; - flex-direction: row; - margin-right: 5px; - - .grid-icon { - margin-right: 5px; - align-self: center; - } - - .flexLabel { - margin-bottom: 0; - } - - .collectionGridViewChrome-entryBox { - width: 50%; - color: $black; - } - - .collectionGridViewChrome-columnButton { - color: $black; - } - } - } - - .collectionStackingViewChrome-sort, - .collectionTreeViewChrome-sort { - display: flex; - align-items: center; - justify-content: space-between; - - .collectionStackingViewChrome-sortIcon, - .collectionTreeViewChrome-sortIcon { - transition: transform .5s; - margin-left: 10px; - } - } - - button:hover { - transform: scale(1); - } - - - .collectionStackingViewChrome-pivotField-cont, - .collectionTreeViewChrome-pivotField-cont, - .collection3DCarouselViewChrome-scrollSpeed-cont { - justify-self: right; - align-items: center; - display: flex; - grid-auto-columns: auto; - font-size: $small-text; - letter-spacing: 2px; - - .collectionStackingViewChrome-pivotField-label, - .collectionTreeViewChrome-pivotField-label, - .collection3DCarouselViewChrome-scrollSpeed-label { - grid-column: 1; - margin-right: 7px; - user-select: none; - font-family: $sans-serif; - letter-spacing: normal; - } - - .collectionStackingViewChrome-sortIcon { - transition: transform .5s; - grid-column: 3; - text-align: center; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - width: 25px; - height: 25px; - border-radius: 100%; - } - - .collectionStackingViewChrome-sortIcon:hover { - background-color: $drop-shadow; - } - - .collectionStackingViewChrome-pivotField, - .collectionTreeViewChrome-pivotField, - .collection3DCarouselViewChrome-scrollSpeed { - color: $white; - grid-column: 2; - grid-row: 1; - width: 90%; - min-width: 100px; - display: flex; - height: 80%; - border-radius: 7px; - align-items: center; - background: $white; - - .editable-view-input, - input, - .editableView-container-editing-oneLine, - .editableView-container-editing { - margin: auto; - border: 0px; - color: $light-gray !important; - text-align: center; - letter-spacing: 2px; - outline-color: $black; - height: 100%; - } - - .react-autosuggest__container { - margin: 0; - color: $medium-gray; - padding: 0px; - } - } - } - - .collectionStackingViewChrome-pivotField:hover, - .collectionTreeViewChrome-pivotField:hover, - .collection3DCarouselViewChrome-scrollSpeed:hover { - cursor: text; - } - - } -} - -.collectionMenu-webUrlButtons { - margin-left: 44; - background: lightGray; +.collectionMenu-container { display: flex; -} - -.webBox-urlEditor { - position: relative; - opacity: 0.9; - z-index: 901; - transition: top .5s; - - .urlEditor { - display: grid; - grid-template-columns: 1fr auto; - padding-bottom: 10px; - overflow: hidden; - margin-top: 5px; - height: 35px; - - .editorBase { - display: flex; - - .editor-collapse { - transition: all .5s, opacity 0.3s; - position: absolute; - width: 40px; - transform-origin: top left; - } - - .switchToText { - color: $medium-gray; - } - - .switchToText:hover { - color: $dark-gray; - } - } - - button:hover { - transform: scale(1); - } - } -} - -.collectionMenu-urlInput { - padding: 12px 10px 11px 10px; - border: 0px; - color: $black; - font-size: $small-text; - letter-spacing: 2px; - outline-color: $black; - background: $white; - width: 100%; - min-width: 350px; - margin-right: 10px; - height: 100%; -} - -.collectionFreeFormMenu-cont { - display: inline-flex; position: relative; + align-content: center; + justify-content: space-between; + background-color: $dark-gray; + height: 35px; + border-bottom: $standard-border; + padding-right: 5px; align-items: center; - height: 100%; - - .color-previewI { - width: 60%; - top: 80%; - position: absolute; - height: 4px; - } - - .color-previewII { - width: 80%; - height: 80%; - margin-left: 10%; - position: absolute; - bottom: 5; - } - - .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 { + .collectionMenu-hardCodedButton { cursor: pointer; - position: relative; - width: 20; - height: 30; - bottom: 0; - background: $dark-gray; - display: inline-flex; - align-items: center; color: $white; - } - - .backKeyframe { - svg { - display: block; - margin: auto; - } - } - - - .numKeyframe { - flex-direction: column; - padding-top: 5px; - } - - .fwdKeyframe { - svg { - display: block; - margin: auto; - } - - border-right: solid $medium-gray 1px; - } -} - -.collectionSchemaViewChrome-cont { - display: flex; - font-size: $small-text; - - .collectionSchemaViewChrome-toggle { - display: flex; - margin-left: 10px; - } - - .collectionSchemaViewChrome-label { - text-transform: uppercase; - letter-spacing: 2px; - margin-right: 5px; + width: 25px; + height: 25px; + padding: 5; + text-align: center; display: flex; - flex-direction: column; justify-content: center; - } - - .collectionSchemaViewChrome-toggler { - width: 100px; - height: 35px; - background-color: $black; + align-items: center; position: relative; - } - - .collectionSchemaViewChrome-togglerButton { - width: 47px; - height: 30px; - background-color: $light-gray; - // position: absolute; - transition: all 0.5s ease; - // top: 3px; - margin-top: 3px; - color: $medium-gray; - letter-spacing: 2px; - text-transform: uppercase; - display: flex; - flex-direction: column; - justify-content: center; - text-align: center; + transition: 0.2s; + border-radius: 3px; - &.on { - margin-left: 3px; - } - - &.off { - margin-left: 50px; + &:hover { + background-color: rgba(0, 0, 0, 0.2); } } } - -.commandEntry-outerDiv { - display: flex; - flex-direction: column; - height: 40px; -} - -.commandEntry-inputArea { - display: flex; - flex-direction: row; - width: 150px; - margin: auto auto auto auto; -} - -.react-autosuggest__container { - position: relative; - width: 100%; - margin-left: 5px; - margin-right: 5px; -} - -.react-autosuggest__input { - border: 1px solid $light-gray; - border-radius: 4px; - width: 100%; -} - -.react-autosuggest__input--focused { - outline: none; -} - -.react-autosuggest__input--open { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.react-autosuggest__suggestions-container { - display: none; -} - -.react-autosuggest__suggestions-container--open { - display: block; - position: fixed; - overflow-y: auto; - max-height: 400px; - width: 180px; - border: 1px solid $light-gray; - background-color: $white; - font-family: $sans-serif; - font-weight: 300; - font-size: $large-header; - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - z-index: 2; -} - -.react-autosuggest__suggestions-list { - margin: 0; - padding: 0; - list-style-type: none; -} - -.react-autosuggest__suggestion { - cursor: pointer; - padding: 10px 20px; -} - -.react-autosuggest__suggestion--highlighted { - background-color: $light-gray; -}
\ No newline at end of file +// .collectionMenu-cont { +// position: relative; +// display: inline-flex; +// width: 100%; +// opacity: 0.9; +// z-index: 901; +// transition: top .5s; +// background: $dark-gray; +// color: $white; +// transform-origin: top left; +// top: 0; +// width: 100%; + +// .recordButtonOutline { +// border-radius: 100%; +// width: 18px; +// height: 18px; +// border: solid 1px $white; +// display: flex; +// align-items: center; +// justify-content: center; +// } + +// .recordButtonInner { +// border-radius: 100%; +// width: 70%; +// height: 70%; +// background: $white; +// } + +// .collectionMenu { +// display: flex; +// height: 100%; +// overflow: visible; +// z-index: 901; +// border: unset; + +// .collectionMenu-divider { +// height: 100%; +// margin-left: 3px; +// margin-right: 3px; +// width: 2px; +// background-color: $medium-gray; +// } + +// .collectionViewBaseChrome { +// display: flex; +// align-items: center; + +// .collectionViewBaseChrome-viewPicker { +// font-size: $small-text; +// outline-color: $black; +// color: $white; +// border: none; +// background: $dark-gray; +// } + +// .collectionViewBaseChrome-viewPicker:focus { +// outline: none; +// border: none; +// } + +// .collectionViewBaseChrome-viewPicker:active { +// outline-color: $black; +// } + +// .collectionViewBaseChrome-button { +// font-size: $small-text; +// text-transform: uppercase; +// letter-spacing: 2px; +// background: $white; +// color: $pink; +// outline-color: $black; +// border: none; +// padding: 12px 10px 11px 10px; +// margin-left: 10px; +// } + +// .collectionViewBaseChrome-cmdPicker { +// margin-left: 3px; +// margin-right: 0px; +// font-size: $small-text; +// text-transform: capitalize; +// color: $white; +// border: none; +// background: $dark-gray; +// } + +// .collectionViewBaseChrome-cmdPicker:focus { +// border: none; +// outline: none; +// } + +// .commandEntry-outerDiv { +// pointer-events: all; +// background-color: transparent; +// display: flex; +// flex-direction: row; +// align-items: center; +// justify-content: center; +// height: 100%; +// overflow: hidden; + +// .commandEntry-drop { +// color: $white; +// width: 30px; +// margin-top: auto; +// margin-bottom: auto; +// } +// } + +// .commandEntry-outerDiv:hover{ +// background-color: $drop-shadow; + +// .collectionViewBaseChrome-viewPicker, +// .collectionViewBaseChrome-cmdPicker{ +// background: $dark-gray; +// } +// } + +// .collectionViewBaseChrome-collapse { +// transition: all .5s, opacity 0.3s; +// position: absolute; +// width: 30px; +// transform-origin: top left; +// pointer-events: all; +// // margin-top: 10px; +// } + +// @media only screen and (max-device-width: 480px) { +// .collectionViewBaseChrome-collapse { +// display: none; +// } +// } + +// .collectionViewBaseChrome-template, +// .collectionViewBaseChrome-viewModes { +// align-items: center; +// height: 100%; +// display: flex; +// background: transparent; +// color: $medium-gray; +// justify-content: center; +// } + +// .collectionViewBaseChrome-viewSpecs { +// margin-left: 5px; +// display: grid; +// border: none; +// border-right: solid $medium-gray 1px; + +// .collectionViewBaseChrome-filterIcon { +// position: relative; +// display: flex; +// margin: auto; +// background: $dark-gray; +// color: $white; +// width: 30px; +// height: 30px; +// align-items: center; +// justify-content: center; +// border: none; +// border-right: solid $medium-gray 1px; +// } + +// .collectionViewBaseChrome-viewSpecsInput { +// padding: 12px 10px 11px 10px; +// border: 0px; +// color: $medium-gray; +// text-align: center; +// letter-spacing: 2px; +// outline-color: $black; +// font-size: $small-text; +// background: $white; +// height: 100%; +// width: 75px; +// } + +// .collectionViewBaseChrome-viewSpecsMenu { +// overflow: hidden; +// transition: height .5s, display .5s; +// position: absolute; +// top: 60px; +// z-index: 100; +// display: flex; +// flex-direction: column; +// background: $white; +// box-shadow: $medium-gray 2px 2px 4px; + +// .qs-datepicker { +// left: unset; +// right: 0; +// } + +// .collectionViewBaseChrome-viewSpecsMenu-row { +// display: grid; +// grid-template-columns: 150px 200px 150px; +// margin-top: 10px; +// margin-right: 10px; + +// .collectionViewBaseChrome-viewSpecsMenu-rowLeft, +// .collectionViewBaseChrome-viewSpecsMenu-rowMiddle, +// .collectionViewBaseChrome-viewSpecsMenu-rowRight { +// font-size: $small-text; +// letter-spacing: 2px; +// color: $medium-gray; +// margin-left: 10px; +// padding: 5px; +// border: none; +// outline-color: $black; +// } +// } + +// .collectionViewBaseChrome-viewSpecsMenu-lastRow { +// display: grid; +// grid-template-columns: 1fr 1fr 1fr; +// grid-gap: 10px; +// margin: 10px; +// } +// } +// } +// } + +// .collectionStackingViewChrome-cont, +// .collectionTreeViewChrome-cont, +// .collection3DCarouselViewChrome-cont { +// display: flex; +// justify-content: space-between; +// } + +// .collectionGridViewChrome-cont { +// display: flex; +// margin-left: 10; + +// .collectionGridViewChrome-viewPicker { +// font-size: $small-text; +// //text-transform: uppercase; +// //letter-spacing: 2px; +// background: $dark-gray; +// color: $white; +// outline-color: $black; +// color: $white; +// border: none; +// border-right: solid $medium-gray 1px; +// } + +// .collectionGridViewChrome-viewPicker:active { +// outline-color: $black; +// } + +// .grid-control { +// align-self: center; +// display: flex; +// flex-direction: row; +// margin-right: 5px; + +// .grid-icon { +// margin-right: 5px; +// align-self: center; +// } + +// .flexLabel { +// margin-bottom: 0; +// } + +// .collectionGridViewChrome-entryBox { +// width: 50%; +// color: $black; +// } + +// .collectionGridViewChrome-columnButton { +// color: $black; +// } +// } +// } + +// .collectionStackingViewChrome-sort, +// .collectionTreeViewChrome-sort { +// display: flex; +// align-items: center; +// justify-content: space-between; + +// .collectionStackingViewChrome-sortIcon, +// .collectionTreeViewChrome-sortIcon { +// transition: transform .5s; +// margin-left: 10px; +// } +// } + +// button:hover { +// transform: scale(1); +// } + + +// .collectionStackingViewChrome-pivotField-cont, +// .collectionTreeViewChrome-pivotField-cont, +// .collection3DCarouselViewChrome-scrollSpeed-cont { +// justify-self: right; +// align-items: center; +// display: flex; +// grid-auto-columns: auto; +// font-size: $small-text; +// letter-spacing: 2px; + +// .collectionStackingViewChrome-pivotField-label, +// .collectionTreeViewChrome-pivotField-label, +// .collection3DCarouselViewChrome-scrollSpeed-label { +// grid-column: 1; +// margin-right: 7px; +// user-select: none; +// font-family: $sans-serif; +// letter-spacing: normal; +// } + +// .collectionStackingViewChrome-sortIcon { +// transition: transform .5s; +// grid-column: 3; +// text-align: center; +// display: flex; +// justify-content: center; +// align-items: center; +// cursor: pointer; +// width: 25px; +// height: 25px; +// border-radius: 100%; +// } + +// .collectionStackingViewChrome-sortIcon:hover { +// background-color: $drop-shadow; +// } + +// .collectionStackingViewChrome-pivotField, +// .collectionTreeViewChrome-pivotField, +// .collection3DCarouselViewChrome-scrollSpeed { +// color: $white; +// grid-column: 2; +// grid-row: 1; +// width: 90%; +// min-width: 100px; +// display: flex; +// height: 80%; +// border-radius: 7px; +// align-items: center; +// background: $white; + +// .editable-view-input, +// input, +// .editableView-container-editing-oneLine, +// .editableView-container-editing { +// margin: auto; +// border: 0px; +// color: $light-gray !important; +// text-align: center; +// letter-spacing: 2px; +// outline-color: $black; +// height: 100%; +// } + +// .react-autosuggest__container { +// margin: 0; +// color: $medium-gray; +// padding: 0px; +// } +// } +// } + +// .collectionStackingViewChrome-pivotField:hover, +// .collectionTreeViewChrome-pivotField:hover, +// .collection3DCarouselViewChrome-scrollSpeed:hover { +// cursor: text; +// } + +// } +// } + +// .collectionMenu-webUrlButtons { +// margin-left: 44; +// background: lightGray; +// display: flex; +// } + +// .webBox-urlEditor { +// position: relative; +// opacity: 0.9; +// z-index: 901; +// transition: top .5s; + +// .urlEditor { +// display: grid; +// grid-template-columns: 1fr auto; +// padding-bottom: 10px; +// overflow: hidden; +// margin-top: 5px; +// height: 35px; + +// .editorBase { +// display: flex; + +// .editor-collapse { +// transition: all .5s, opacity 0.3s; +// position: absolute; +// width: 40px; +// transform-origin: top left; +// } + +// .switchToText { +// color: $medium-gray; +// } + +// .switchToText:hover { +// color: $dark-gray; +// } +// } + +// button:hover { +// transform: scale(1); +// } +// } +// } + +// .collectionMenu-urlInput { +// padding: 12px 10px 11px 10px; +// border: 0px; +// color: $black; +// font-size: $small-text; +// letter-spacing: 2px; +// outline-color: $black; +// background: $white; +// width: 100%; +// min-width: 350px; +// margin-right: 10px; +// height: 100%; +// } + +// .collectionFreeFormMenu-cont { +// display: inline-flex; +// position: relative; +// align-items: center; +// height: 100%; + +// .color-previewI { +// width: 60%; +// top: 80%; +// position: absolute; +// height: 4px; +// } + +// .color-previewII { +// width: 80%; +// height: 80%; +// margin-left: 10%; +// position: absolute; +// bottom: 5; +// } + +// .btn-group { +// display: grid; +// grid-template-columns: auto auto auto auto; +// margin: auto; +// /* Make the buttons appear below each other */ +// } + +// .btn-draw { +// display: inline-flex; +// margin: auto; +// /* Make the buttons appear below each other */ +// } + +// .fwdKeyframe, +// .numKeyframe, +// .backKeyframe { +// cursor: pointer; +// position: relative; +// width: 20; +// height: 30; +// bottom: 0; +// background: $dark-gray; +// display: inline-flex; +// align-items: center; +// color: $white; +// } + +// .backKeyframe { +// svg { +// display: block; +// margin: auto; +// } +// } + + +// .numKeyframe { +// flex-direction: column; +// padding-top: 5px; +// } + +// .fwdKeyframe { +// svg { +// display: block; +// margin: auto; +// } + +// border-right: solid $medium-gray 1px; +// } +// } + +// .collectionSchemaViewChrome-cont { +// display: flex; +// font-size: $small-text; + +// .collectionSchemaViewChrome-toggle { +// display: flex; +// margin-left: 10px; +// } + +// .collectionSchemaViewChrome-label { +// text-transform: uppercase; +// letter-spacing: 2px; +// margin-right: 5px; +// display: flex; +// flex-direction: column; +// justify-content: center; +// } + +// .collectionSchemaViewChrome-toggler { +// width: 100px; +// height: 35px; +// background-color: $black; +// position: relative; +// } + +// .collectionSchemaViewChrome-togglerButton { +// width: 47px; +// height: 30px; +// background-color: $light-gray; +// // position: absolute; +// transition: all 0.5s ease; +// // top: 3px; +// margin-top: 3px; +// color: $medium-gray; +// letter-spacing: 2px; +// text-transform: uppercase; +// display: flex; +// flex-direction: column; +// justify-content: center; +// text-align: center; + +// &.on { +// margin-left: 3px; +// } + +// &.off { +// margin-left: 50px; +// } +// } +// } + + +// .commandEntry-outerDiv { +// display: flex; +// flex-direction: column; +// height: 40px; +// } + +// .commandEntry-inputArea { +// display: flex; +// flex-direction: row; +// width: 150px; +// margin: auto auto auto auto; +// } + +// .react-autosuggest__container { +// position: relative; +// width: 100%; +// margin-left: 5px; +// margin-right: 5px; +// } + +// .react-autosuggest__input { +// border: 1px solid $light-gray; +// border-radius: 4px; +// width: 100%; +// } + +// .react-autosuggest__input--focused { +// outline: none; +// } + +// .react-autosuggest__input--open { +// border-bottom-left-radius: 0; +// border-bottom-right-radius: 0; +// } + +// .react-autosuggest__suggestions-container { +// display: none; +// } + +// .react-autosuggest__suggestions-container--open { +// display: block; +// position: fixed; +// overflow-y: auto; +// max-height: 400px; +// width: 180px; +// border: 1px solid $light-gray; +// background-color: $white; +// font-family: $sans-serif; +// font-weight: 300; +// font-size: $large-header; +// border-bottom-left-radius: 4px; +// border-bottom-right-radius: 4px; +// z-index: 2; +// } + +// .react-autosuggest__suggestions-list { +// margin: 0; +// padding: 0; +// list-style-type: none; +// } + +// .react-autosuggest__suggestion { +// cursor: pointer; +// padding: 10px 20px; +// } + +// .react-autosuggest__suggestion--highlighted { +// background-color: $light-gray; +// }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index a5f54c342..131f5ba46 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -2,7 +2,7 @@ import React = require("react"); import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon, FontAwesomeIconProps } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; -import { action, computed, Lambda, observable, reaction, runInAction } from "mobx"; +import { action, computed, Lambda, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { ColorState } from "react-color"; import { Doc, DocListCast, Opt } from "../../../fields/Doc"; @@ -15,37 +15,47 @@ import { RichTextField } from "../../../fields/RichTextField"; import { listSpec } from "../../../fields/Schema"; import { ScriptField } from "../../../fields/ScriptField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, setupMoveUpEvents, Utils } from "../../../Utils"; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { DragManager } from "../../util/DragManager"; import { Scripting } from "../../util/Scripting"; import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { EditableView } from "../EditableView"; import { GestureOverlay } from "../GestureOverlay"; -import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, ActiveArrowStart, ActiveArrowEnd } from "../InkingStroke"; +import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke"; +import { LightboxView } from "../LightboxView"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; +import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; import { RichTextMenu } from "../nodes/formattedText/RichTextMenu"; import { PresBox } from "../nodes/trails/PresBox"; +import { DefaultStyleProvider } from "../StyleProvider"; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionLinearView } from "./collectionLinear"; import "./CollectionMenu.scss"; import { CollectionViewType, COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { TabDocView } from "./TabDocView"; -import { LightboxView } from "../LightboxView"; -import { Docs } from "../../documents/Documents"; -import { DocumentManager } from "../../util/DocumentManager"; -import { CollectionDockingView } from "./CollectionDockingView"; -import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox"; +import { Colors } from "../global/globalEnums"; + +interface CollectionMenuProps { + panelHeight: () => number; + panelWidth: () => number; +} @observer -export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { +export class CollectionMenu extends AntimodeMenu<CollectionMenuProps>{ @observable static Instance: CollectionMenu; @observable SelectedCollection: DocumentView | undefined; @observable FieldKey: string; + private _docBtnRef = React.createRef<HTMLDivElement>(); + constructor(props: any) { super(props); this.FieldKey = ""; @@ -57,7 +67,7 @@ export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { componentDidMount() { reaction(() => SelectionManager.Views().length && SelectionManager.Views()[0], - (doc) => doc && this.SetSelection(doc)); + view => view && this.SetSelection(view)); } @action @@ -82,34 +92,92 @@ export class CollectionMenu extends AntimodeMenu<AntimodeMenuProps> { } } + buttonBarXf = () => { + if (!this._docBtnRef.current) return Transform.Identity(); + const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current); + return new Transform(-translateX, -translateY, 1 / scale); + } + + @computed get contMenuButtons() { + const selDoc = Doc.UserDoc().contextMenuBtns; + return !(selDoc instanceof Doc) ? (null) : <div className="collectionMenu-contMenuButtons" ref={this._docBtnRef} style={{ height: this.props.panelHeight() }} > + <CollectionLinearView + Document={selDoc} + DataDoc={undefined} + fieldKey={"data"} + dropAction={"alias"} + setHeight={returnFalse} + styleProvider={DefaultStyleProvider} + layerProvider={undefined} + rootSelected={returnTrue} + bringToFront={emptyFunction} + select={emptyFunction} + isContentActive={returnTrue} + isAnyChildContentActive={returnFalse} + isSelected={returnFalse} + docViewPath={returnEmptyDoclist} + moveDocument={returnFalse} + CollectionView={undefined} + addDocument={returnFalse} + addDocTab={returnFalse} + pinToPres={emptyFunction} + removeDocument={returnFalse} + ScreenToLocalTransform={this.buttonBarXf} + PanelWidth={this.props.panelWidth} + PanelHeight={this.props.panelHeight} + renderDepth={0} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + </div>; + } + render() { - const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom"> - <button className="antimodeMenu-button" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> - <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> - </button> - </Tooltip>; const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left"; const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Properties Panel" : "Open Properties Panel"; const prop = <Tooltip title={<div className="dash-tooltip">{propTitle}</div>} key="properties" placement="bottom"> - <button className="antimodeMenu-button" key="properties" style={{ backgroundColor: "#424242" }} + <div className="collectionMenu-hardCodedButton" + style={{ backgroundColor: CurrentUserUtils.propertiesWidth > 0 ? Colors.MEDIUM_BLUE : undefined }} + key="properties" onPointerDown={this.toggleProperties}> <FontAwesomeIcon icon={propIcon} size="lg" /> - </button> + </div> </Tooltip>; - return this.getElement(!this.SelectedCollection ? [/*button*/] : - [<CollectionViewBaseChrome key="chrome" - docView={this.SelectedCollection} - fieldKey={this.SelectedCollection.LayoutFieldKey} - type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, - prop, - /*button*/]); + // NEW BUTTONS + //dash col linear view buttons + const contMenuButtons = + <div className="collectionMenu-container"> + {this.contMenuButtons} + {prop} + </div>; + + return contMenuButtons; + + // const button = <Tooltip title={<div className="dash-tooltip">Pin Menu</div>} key="pin menu" placement="bottom"> + // <button className="antimodeMenu-button" onClick={this.toggleMenuPin} style={{ backgroundColor: "#121721" }}> + // <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} /> + // </button> + // </Tooltip>; + + // OLD BUTTONS + // return this.getElement(!this.SelectedCollection ? [/*button*/] : + // [<CollectionViewBaseChrome key="chrome" + // docView={this.SelectedCollection} + // fieldKey={this.SelectedCollection.LayoutFieldKey} + // type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, + // prop, + // /*button*/]); } } -interface CollectionMenuProps { +interface CollectionViewMenuProps { type: CollectionViewType; fieldKey: string; docView: DocumentView; @@ -118,7 +186,7 @@ interface CollectionMenuProps { const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation(); @observer -export class CollectionViewBaseChrome extends React.Component<CollectionMenuProps> { +export class CollectionViewBaseChrome extends React.Component<CollectionViewMenuProps> { //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\) get document() { return this.props.docView?.props.Document; } @@ -375,10 +443,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp </div>); } - @computed get selectedDocumentView() { - return SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } + @computed get selectedDocumentView() { return SelectionManager.Views().lastElement(); } + @computed get selectedDoc() { return SelectionManager.Docs().lastElement(); } @computed get notACollection() { if (this.selectedDoc) { const layoutField = Doc.LayoutField(this.selectedDoc); @@ -486,8 +552,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp @undoBatch onAliasButtonMoved = (e: PointerEvent) => { const contentDiv = this.selectedDocumentView?.ContentDiv; - if (contentDiv) { - const dragData = new DragManager.DocumentDragData([this.selectedDocumentView!.props.Document]); + if (contentDiv && this.selectedDoc) { + const dragData = new DragManager.DocumentDragData([this.selectedDoc]); const offset = [e.clientX - contentDiv.getBoundingClientRect().x, e.clientY - contentDiv.getBoundingClientRect().y]; dragData.defaultDropAction = "alias"; dragData.canEmbed = true; @@ -564,14 +630,14 @@ export class CollectionViewBaseChrome extends React.Component<CollectionMenuProp } @observer -export class CollectionDockingChrome extends React.Component<CollectionMenuProps> { +export class CollectionDockingChrome extends React.Component<CollectionViewMenuProps> { render() { return (null); } } @observer -export class CollectionFreeFormViewChrome extends React.Component<CollectionMenuProps & { isOverlay: boolean, isDoc?: boolean }> { +export class CollectionFreeFormViewChrome extends React.Component<CollectionViewMenuProps & { isOverlay: boolean, isDoc?: boolean }> { public static Instance: CollectionFreeFormViewChrome; constructor(props: any) { super(props); @@ -582,11 +648,9 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu return this.document[this.props.docView.LayoutFieldKey + (this.props.isOverlay ? "-annotations" : "")]; } @computed get childDocs() { return DocListCast(this.dataField); } - @computed get selectedDocumentView() { return SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; } - @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; } - @computed get isText() { - return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; - } + @computed get selectedDocumentView() { return SelectionManager.Views().lastElement(); } + @computed get selectedDoc() { return SelectionManager.Docs().lastElement(); } + @computed get isText() { return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; } @undoBatch @action @@ -615,12 +679,12 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu private _palette = ["#D0021B", "#F5A623", "#F8E71C", "#8B572A", "#7ED321", "#417505", "#9013FE", "#4A90E2", "#50E3C2", "#B8E986", "#000000", "#4A4A4A", "#9B9B9B", "#FFFFFF", ""]; private _width = ["1", "5", "10", "100"]; private _dotsize = [10, 20, 30, 40]; - private _draw = ["∿", "⎯", "→", "↔︎", "ロ", "O"]; - private _head = ["", "", "", "arrow", "", ""]; - private _end = ["", "", "arrow", "arrow", "", ""]; - private _shapePrims = ["", "line", "line", "line", "rectangle", "circle"]; - private _title = ["pen", "line", "line with arrow", "line with double arrows", "square", "circle",]; - private _faName = ["pen-fancy", "minus", "long-arrow-alt-right", "arrows-alt-h", "square", "circle"]; + private _draw = ["∿", "=", "⎯", "→", "↔︎", "ロ", "O"]; + private _head = ["", "", "", "", "arrow", "", ""]; + private _end = ["", "", "", "arrow", "arrow", "", ""]; + private _shapePrims = ["", "", "line", "line", "line", "rectangle", "circle"]; + private _title = ["pen", "highlighter", "line", "line with arrow", "line with double arrows", "square", "circle"]; + private _faName = ["pen-fancy", "highlighter", "minus", "long-arrow-alt-right", "arrows-alt-h", "square", "circle"]; @observable _selectedPrimitive = this._shapePrims.length; @observable _keepPrimitiveMode = false; // for whether primitive selection enters a one-shot or persistent mode @observable _colorBtn = false; @@ -630,6 +694,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @action clearKeepPrimitiveMode() { this._selectedPrimitive = this._shapePrims.length; } @action primCreated() { if (!this._keepPrimitiveMode) { //get out of ink mode after each stroke= + if (CurrentUserUtils.SelectedTool === InkTool.Highlighter && GestureOverlay.Instance.SavedColor) SetActiveInkColor(GestureOverlay.Instance.SavedColor); CurrentUserUtils.SelectedTool = InkTool.None; this._selectedPrimitive = this._shapePrims.length; SetActiveArrowStart("none"); @@ -670,7 +735,13 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu this._keepPrimitiveMode = keep; if (this._selectedPrimitive !== i) { this._selectedPrimitive = i; - CurrentUserUtils.SelectedTool = InkTool.Pen; + if (this._title[i] === "highlighter") { + CurrentUserUtils.SelectedTool = InkTool.Highlighter; + GestureOverlay.Instance.SavedColor = ActiveInkColor(); + SetActiveInkColor("rgba(245, 230, 95, 0.75)"); + } else { + CurrentUserUtils.SelectedTool = InkTool.Pen; + } SetActiveArrowStart(this._head[i]); SetActiveArrowEnd(this._end[i]); SetActiveBezierApprox("300"); @@ -760,7 +831,6 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </div>; } - @observable viewType = this.selectedDoc?._viewType; render() { return !this.props.docView.layoutDoc ? (null) : @@ -798,7 +868,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu } } @observer -export class CollectionStackingViewChrome extends React.Component<CollectionMenuProps> { +export class CollectionStackingViewChrome extends React.Component<CollectionViewMenuProps> { @observable private _currentKey: string = ""; @observable private suggestions: string[] = []; @@ -919,7 +989,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionMenu @observer -export class CollectionSchemaViewChrome extends React.Component<CollectionMenuProps> { +export class CollectionSchemaViewChrome extends React.Component<CollectionViewMenuProps> { // private _textwrapAllRows: boolean = Cast(this.document.textwrappedSchemaRows, listSpec("string"), []).length > 0; get document() { return this.props.docView.props.Document; } @@ -967,7 +1037,7 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionMenuPr } @observer -export class CollectionTreeViewChrome extends React.Component<CollectionMenuProps> { +export class CollectionTreeViewChrome extends React.Component<CollectionViewMenuProps> { get document() { return this.props.docView.props.Document; } get sortAscending() { @@ -1004,7 +1074,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionMenuProp // Enter scroll speed for 3D Carousel @observer -export class Collection3DCarouselViewChrome extends React.Component<CollectionMenuProps> { +export class Collection3DCarouselViewChrome extends React.Component<CollectionViewMenuProps> { get document() { return this.props.docView.props.Document; } @computed get scrollSpeed() { return this.document._autoScrollSpeed; @@ -1045,7 +1115,7 @@ export class Collection3DCarouselViewChrome extends React.Component<CollectionMe * Chrome for grid view. */ @observer -export class CollectionGridViewChrome extends React.Component<CollectionMenuProps> { +export class CollectionGridViewChrome extends React.Component<CollectionViewMenuProps> { private clicked: boolean = false; private entered: boolean = false; @@ -1226,3 +1296,4 @@ Scripting.addGlobal(function gotoFrame(doc: any, newFrame: any) { CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0); doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); }); + diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 1efea96be..6a22acae8 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -18,6 +18,7 @@ import { SnappingManager } from "../../util/SnappingManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../views/global/globalCssVariables.scss'; +import { SchemaTable } from "../collections/collectionSchema/SchemaTable"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import '../DocumentDecorations.scss'; @@ -25,7 +26,6 @@ import { DocumentView } from "../nodes/DocumentView"; import { DefaultStyleProvider } from "../StyleProvider"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { SchemaTable } from "../collections/collectionSchema/SchemaTable"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 export enum ColumnType { @@ -338,7 +338,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { {this.renderColors(this._col)} <div className="collectionSchema-headerMenu-group"> <button onClick={() => { this.deleteColumn(this._col.heading); }} - >Delete Column</button> + >Hide Column</button> </div> </div>; } diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index e456c0664..59c21210a 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -1,70 +1,94 @@ +@import "../global/globalCssVariables.scss"; + .collectionStackedTimeline { + position: absolute; + width: 100%; + height: 100%; + z-index: 1000; + overflow: hidden; + top: 0px; + + .collectionStackedTimeline-trim-shade { position: absolute; - width: 100%; height: 100%; - border: gray solid 1px; - border-radius: 3px; - z-index: 1000; - overflow: hidden; - top: 0px; + background-color: $dark-gray; + opacity: 0.3; + } - .collectionStackedTimeline-selector { - position: absolute; - width: 10px; - top: 2.5%; - height: 95%; - background: lightblue; - border-radius: 5px; - opacity: 0.3; - z-index: 500; - border-style: solid; - border-color: darkblue; - border-width: 1px; - } + .collectionStackedTimeline-trim-controls { + height: 100%; + position: absolute; + box-sizing: border-box; + border: 2px solid $medium-blue; + display: flex; + justify-content: space-between; + max-width: 100%; - .collectionStackedTimeline-current { - width: 1px; - height: 100%; - background-color: red; - position: absolute; - top: 0px; - pointer-events: none; + .collectionStackedTimeline-trim-handle { + background-color: $medium-blue; + height: 100%; + width: 5px; + cursor: ew-resize; } + } - .collectionStackedTimeline-marker-timeline { - position: absolute; - top: 2.5%; - height: 95%; - border-radius: 4px; - &:hover { - opacity: 1; - } + .collectionStackedTimeline-selector { + position: absolute; + width: 10px; + top: 2.5%; + height: 95%; + background: $light-blue; + border-radius: 3px; + opacity: 0.3; + z-index: 500; + border-style: solid; + border-color: $medium-blue; + border-width: 1px; + } - .collectionStackedTimeline-left-resizer, - .collectionStackedTimeline-resizer { - background: dimgrey; - position: absolute; - top: 0; - height: 100%; - width: 10px; - pointer-events: all; - cursor: ew-resize; - z-index: 100; - } - .collectionStackedTimeline-resizer { - right: 0; - } - .collectionStackedTimeline-left-resizer { - left: 0; - } + .collectionStackedTimeline-current { + width: 1px; + height: 100%; + background-color: $pink; + position: absolute; + top: 0px; + pointer-events: none; + } + + .collectionStackedTimeline-marker-timeline { + position: absolute; + top: 2.5%; + height: 95%; + border-radius: 4px; + &:hover { + opacity: 1; } - .collectionStackedTimeline-waveform { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - pointer-events: none; + .collectionStackedTimeline-left-resizer, + .collectionStackedTimeline-resizer { + background: $medium-gray; + position: absolute; + top: 0; + height: 100%; + width: 10px; + pointer-events: all; + cursor: ew-resize; + z-index: 100; + } + .collectionStackedTimeline-resizer { + right: 0; } -}
\ No newline at end of file + .collectionStackedTimeline-left-resizer { + left: 0; + } + } + + .collectionStackedTimeline-waveform { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + pointer-events: none; + } +} diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index a2c95df6e..65022fdfd 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,5 +1,12 @@ import React = require("react"); -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; +import { + action, + computed, + IReactionDisposer, + observable, + reaction, + runInAction, +} from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { Doc, DocListCast } from "../../../fields/Doc"; @@ -8,7 +15,16 @@ import { List } from "../../../fields/List"; import { listSpec, makeInterface } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast } from "../../../fields/Types"; -import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, StopEvent, returnTrue } from "../../../Utils"; +import { + emptyFunction, + formatTime, + OmitKeys, + returnFalse, + returnOne, + setupMoveUpEvents, + StopEvent, + returnTrue, +} from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { LinkManager } from "../../util/LinkManager"; import { Scripting } from "../../util/Scripting"; @@ -18,9 +34,18 @@ import { undoBatch } from "../../util/UndoManager"; import { AudioWaveform } from "../AudioWaveform"; import { CollectionSubView } from "../collections/CollectionSubView"; import { LightboxView } from "../LightboxView"; -import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; +import { + DocAfterFocusFunc, + DocFocusFunc, + DocumentView, + DocumentViewProps, +} from "../nodes/DocumentView"; import { LabelBox } from "../nodes/LabelBox"; import "./CollectionStackedTimeline.scss"; +import { Colors } from "../global/globalEnums"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SnappingManager } from "../../util/SnappingManager"; +import { DragManager } from "../../util/DragManager"; type PanZoomDocument = makeInterface<[]>; const PanZoomDocument = makeInterface(); @@ -36,11 +61,21 @@ export type CollectionStackedTimelineProps = { endTag: string; mediaPath: string; dictationKey: string; + trimming: boolean; + trimStart: number; + trimEnd: number; + trimDuration: number; + setStartTrim: (newStart: number) => void; + setEndTrim: (newEnd: number) => void; }; @observer -export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument, CollectionStackedTimelineProps>(PanZoomDocument) { - @observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined; +export class CollectionStackedTimeline extends CollectionSubView< + PanZoomDocument, + CollectionStackedTimelineProps +>(PanZoomDocument) { + @observable static SelectingRegion: CollectionStackedTimeline | undefined = + undefined; static RangeScript: ScriptField; static LabelScript: ScriptField; static RangePlayScript: ScriptField; @@ -50,48 +85,111 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument private _markerStart: number = 0; @observable _markerEnd: number = 0; - get duration() { return this.props.duration; } - @computed get currentTime() { return NumCast(this.layoutDoc._currentTimecode); } + get minLength() { + const rect = this._timeline?.getBoundingClientRect(); + if (rect) { + return 0.05 * this.duration; + } + return 0; + } + + get trimStart() { + return this.props.trimStart; + } + + get trimEnd() { + return this.props.trimEnd; + } + + get duration() { + return this.props.duration; + } + + @computed get currentTime() { + return NumCast(this.layoutDoc._currentTimecode); + } @computed get selectionContainer() { - return CollectionStackedTimeline.SelectingRegion !== this ? (null) : <div className="collectionStackedTimeline-selector" style={{ - left: `${Math.min(NumCast(this._markerStart), NumCast(this._markerEnd)) / this.duration * 100}%`, - width: `${Math.abs(this._markerStart - this._markerEnd) / this.duration * 100}%` - }} />; + return CollectionStackedTimeline.SelectingRegion !== this ? null : ( + <div + className="collectionStackedTimeline-selector" + style={{ + left: `${((Math.min(this._markerStart, this._markerEnd) - this.trimStart) / this.props.trimDuration) * 100}%`, + width: `${(Math.abs(this._markerStart - this._markerEnd) / this.props.trimDuration) * 100}%`, + }} + /> + ); } constructor(props: any) { super(props); // onClick play scripts - CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!; - CollectionStackedTimeline.RangePlayScript = CollectionStackedTimeline.RangePlayScript || ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { self: Doc.name, scriptContext: "any", clientX: "number" })!; + CollectionStackedTimeline.RangeScript = + CollectionStackedTimeline.RangeScript || + ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { + self: Doc.name, + scriptContext: "any", + clientX: "number", + })!; + CollectionStackedTimeline.RangePlayScript = + CollectionStackedTimeline.RangePlayScript || + ScriptField.MakeFunction(`scriptContext.playOnClick(this, clientX)`, { + self: Doc.name, + scriptContext: "any", + clientX: "number", + })!; } - componentDidMount() { document.addEventListener("keydown", this.keyEvents, true); } + componentDidMount() { + document.addEventListener("keydown", this.keyEvents, true); + } componentWillUnmount() { document.removeEventListener("keydown", this.keyEvents, true); - if (CollectionStackedTimeline.SelectingRegion === this) runInAction(() => CollectionStackedTimeline.SelectingRegion = undefined); + if (CollectionStackedTimeline.SelectingRegion === this) { + runInAction( + () => (CollectionStackedTimeline.SelectingRegion = undefined) + ); + } } - anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag])); + anchorStart = (anchor: Doc) => + NumCast(anchor._timecodeToShow, NumCast(anchor[this.props.startTag])) anchorEnd = (anchor: Doc, val: any = null) => { const endVal = NumCast(anchor[this.props.endTag], val); - return NumCast(anchor._timecodeToHide, endVal === undefined ? null : endVal); + return NumCast( + anchor._timecodeToHide, + endVal === undefined ? null : endVal + ); } - toTimeline = (screen_delta: number, width: number) => Math.max(0, Math.min(this.duration, screen_delta / width * this.duration)); + toTimeline = (screen_delta: number, width: number) => { + return Math.max( + this.trimStart, + Math.min(this.trimEnd, (screen_delta / width) * this.props.trimDuration + this.trimStart)); + } + rangeClickScript = () => CollectionStackedTimeline.RangeScript; rangePlayScript = () => CollectionStackedTimeline.RangePlayScript; // for creating key anchors with key events @action keyEvents = (e: KeyboardEvent) => { - if (!(e.target instanceof HTMLInputElement) && this.props.isSelected(true)) { + if ( + !(e.target instanceof HTMLInputElement) && + this.props.isSelected(true) + ) { switch (e.key) { case " ": if (!CollectionStackedTimeline.SelectingRegion) { this._markerStart = this._markerEnd = this.currentTime; CollectionStackedTimeline.SelectingRegion = this; } else { - CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime); + CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.props.fieldKey, + this.props.startTag, + this.props.endTag, + this.currentTime + ); CollectionStackedTimeline.SelectingRegion = undefined; } } @@ -101,7 +199,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument getLinkData(l: Doc) { let la1 = l.anchor1 as Doc; let la2 = l.anchor2 as Doc; - const linkTime = NumCast(la2[this.props.startTag], NumCast(la1[this.props.startTag])); + const linkTime = NumCast( + la2[this.props.startTag], + NumCast(la1[this.props.startTag]) + ); if (Doc.AreProtosEqual(la1, this.dataDoc)) { la1 = l.anchor2 as Doc; la2 = l.anchor1 as Doc; @@ -118,10 +219,18 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument const wasPlaying = this.props.playing(); if (wasPlaying) this.props.Pause(); const wasSelecting = CollectionStackedTimeline.SelectingRegion === this; - setupMoveUpEvents(this, e, - action(e => { - if (!wasSelecting && CollectionStackedTimeline.SelectingRegion !== this) { - this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width); + setupMoveUpEvents( + this, + e, + action((e) => { + if ( + !wasSelecting && + CollectionStackedTimeline.SelectingRegion !== this + ) { + this._markerStart = this._markerEnd = this.toTimeline( + clientX - rect.x, + rect.width + ); CollectionStackedTimeline.SelectingRegion = this; } this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width); @@ -134,32 +243,147 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument this._markerStart = this._markerEnd; this._markerEnd = tmp; } - if (!isClick && CollectionStackedTimeline.SelectingRegion === this && (Math.abs(movement[0]) > 15)) { - CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, - this._markerStart, this._markerEnd); + if ( + !isClick && + CollectionStackedTimeline.SelectingRegion === this && + Math.abs(movement[0]) > 15 && + !this.props.trimming + ) { + const anchor = CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.props.fieldKey, + this.props.startTag, + this.props.endTag, + this._markerStart, + this._markerEnd + ); + setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false)); } - (!isClick || !wasSelecting) && (CollectionStackedTimeline.SelectingRegion = undefined); + (!isClick || !wasSelecting) && + (CollectionStackedTimeline.SelectingRegion = undefined); }), (e, doubleTap) => { this.props.select(false); - e.shiftKey && CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime); + e.shiftKey && + CollectionStackedTimeline.createAnchor( + this.rootDoc, + this.dataDoc, + this.props.fieldKey, + this.props.startTag, + this.props.endTag, + this.currentTime + ); !wasPlaying && doubleTap && this.props.Play(); }, - this.props.isSelected(true) || this.props.isContentActive(), undefined, - () => !wasPlaying && this.props.setTime((clientX - rect.x) / rect.width * this.duration)); + this.props.isSelected(true) || this.props.isContentActive(), + undefined, + () => { + !wasPlaying && + (this.props.trimming && this.duration ? + this.props.setTime(((clientX - rect.x) / rect.width) * this.duration) + : + this.props.setTime(((clientX - rect.x) / rect.width) * this.props.trimDuration + this.trimStart) + ); + } + ); } + + } + + @action + trimLeft = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect(); + const clientX = e.movementX; + setupMoveUpEvents( + this, + e, + action((e, [], []) => { + if (rect && this.props.isContentActive()) { + this.props.setStartTrim(Math.min( + Math.max( + this.trimStart + (e.movementX / rect.width) * this.duration, + 0 + ), + this.trimEnd - this.minLength + )); + } + return false; + }), + emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + this.props.setStartTrim(0); + } + }) + ); + } + + @action + trimRight = (e: React.PointerEvent): void => { + const rect = this._timeline?.getBoundingClientRect(); + const clientX = e.movementX; + setupMoveUpEvents( + this, + e, + action((e, [], []) => { + if (rect && this.props.isContentActive()) { + this.props.setEndTrim(Math.max( + Math.min( + this.trimEnd + (e.movementX / rect.width) * this.duration, + this.duration + ), + this.trimStart + this.minLength + )); + } + return false; + }), + emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + this.props.setEndTrim(this.duration); + } + }) + ); + } + + @action + internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) { + if (!de.embedKey && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; + if (!super.onInternalDrop(e, de)) return false; + + + // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view + + return true; + } + + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, 0); + return false; } @undoBatch @action - static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime?: number, anchorEndTime?: number) { + static createAnchor( + rootDoc: Doc, + dataDoc: Doc, + fieldKey: string, + startTag: string, + endTag: string, + anchorStartTime?: number, + anchorEndTime?: number + ) { if (anchorStartTime === undefined) return rootDoc; const anchor = Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`) as any, + title: ComputedField.MakeFunction( + `"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])` + ) as any, + _stayInCollection: true, useLinkSmallAnchor: true, hideLinkButton: true, annotationOn: rootDoc, - _timelineLabel: true + _timelineLabel: true, }); Doc.GetProto(anchor)[startTag] = anchorStartTime; Doc.GetProto(anchor)[endTag] = anchorEndTime; @@ -179,7 +403,10 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument if (this.props.playing()) this.props.Pause(); else this.props.playFrom(seekTimeInSeconds, endTime); } else { - if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) { + if ( + seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && + endTime > NumCast(this.layoutDoc._currentTimecode) + ) { if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) { this.props.Pause(); } else { @@ -194,39 +421,60 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument @action clickAnchor = (anchorDoc: Doc, clientX: number) => { - if (anchorDoc.isLinkButton) LinkManager.FollowLink(undefined, anchorDoc, this.props, false); + if (anchorDoc.isLinkButton) { + LinkManager.FollowLink(undefined, anchorDoc, this.props, false); + } const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25; const endTime = this.anchorEnd(anchorDoc); - if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { + if ( + seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && + endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4 + ) { if (this.props.playing()) this.props.Pause(); else if (this.layoutDoc.autoPlayAnchors) this.props.Play(); else if (!this.layoutDoc.autoPlayAnchors) { const rect = this._timeline?.getBoundingClientRect(); - rect && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width)); + rect && + this.props.setTime(this.toTimeline(clientX - rect.x, rect.width)); } } else { - if (this.layoutDoc.autoPlayAnchors) this.props.playFrom(seekTimeInSeconds, endTime); - else this.props.setTime(seekTimeInSeconds); + if (this.layoutDoc.autoPlayAnchors) { + this.props.playFrom(seekTimeInSeconds, endTime); + } + else { + this.props.setTime(seekTimeInSeconds); + } } return { select: true }; } - // makes sure no anchors overlaps each other by setting the correct position and width - getLevel = (m: Doc, placed: { anchorStartTime: number, anchorEndTime: number, level: number }[]) => { + getLevel = ( + m: Doc, + placed: { anchorStartTime: number; anchorEndTime: number; level: number }[] + ) => { const timelineContentWidth = this.props.PanelWidth(); const x1 = this.anchorStart(m); - const x2 = this.anchorEnd(m, x1 + 10 / timelineContentWidth * this.duration); + const x2 = this.anchorEnd( + m, + x1 + (10 / timelineContentWidth) * this.duration + ); let max = 0; - const overlappedLevels = new Set(placed.map(p => { - const y1 = p.anchorStartTime; - const y2 = p.anchorEndTime; - if ((x1 >= y1 && x1 <= y2) || (x2 >= y1 && x2 <= y2) || - (y1 >= x1 && y1 <= x2) || (y2 >= x1 && y2 <= x2)) { - max = Math.max(max, p.level); - return p.level; - } - })); + const overlappedLevels = new Set( + placed.map((p) => { + const y1 = p.anchorStartTime; + const y2 = p.anchorEndTime; + if ( + (x1 >= y1 && x1 <= y2) || + (x2 >= y1 && x2 <= y2) || + (y1 >= x1 && y1 <= x2) || + (y2 >= x1 && y2 <= x2) + ) { + max = Math.max(max, p.level); + return p.level; + } + }) + ); let level = max + 1; for (let j = max; j >= 0; j--) !overlappedLevels.has(j) && (level = j); @@ -235,82 +483,185 @@ export class CollectionStackedTimeline extends CollectionSubView<PanZoomDocument } dictationHeightPercent = 50; - dictationHeight = () => this.props.PanelHeight() * (100 - this.dictationHeightPercent) / 100; - timelineContentHeight = () => this.props.PanelHeight() * this.dictationHeightPercent / 100; - dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight()); + dictationHeight = () => + (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100 + timelineContentHeight = () => + (this.props.PanelHeight() * this.dictationHeightPercent) / 100 + dictationScreenToLocalTransform = () => + this.props + .ScreenToLocalTransform() + .translate(0, -this.timelineContentHeight()) @computed get renderDictation() { const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null); - return !dictation ? (null) : <div style={{ position: "absolute", height: "100%", top: this.timelineContentHeight(), background: "tan" }}> - <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} - Document={dictation} - PanelHeight={this.dictationHeight} - isAnnotationOverlay={true} - isDocumentActive={returnFalse} - select={emptyFunction} - scaling={returnOne} - xMargin={25} - yMargin={10} - ScreenToLocalTransform={this.dictationScreenToLocalTransform} - whenChildContentsActiveChanged={emptyFunction} - removeDocument={returnFalse} - moveDocument={returnFalse} - addDocument={returnFalse} - CollectionView={undefined} - renderDepth={this.props.renderDepth + 1}> - </DocumentView> - </div>; + return !dictation ? null : ( + <div + style={{ + position: "absolute", + height: "100%", + top: this.timelineContentHeight(), + background: Colors.LIGHT_BLUE, + }} + > + <DocumentView + {...OmitKeys(this.props, [ + "NativeWidth", + "NativeHeight", + "setContentView", + ]).omit} + Document={dictation} + PanelHeight={this.dictationHeight} + isAnnotationOverlay={true} + isDocumentActive={returnFalse} + select={emptyFunction} + scaling={returnOne} + xMargin={25} + yMargin={10} + ScreenToLocalTransform={this.dictationScreenToLocalTransform} + whenChildContentsActiveChanged={emptyFunction} + removeDocument={returnFalse} + moveDocument={returnFalse} + addDocument={returnFalse} + CollectionView={undefined} + renderDepth={this.props.renderDepth + 1} + ></DocumentView> + </div> + ); } @computed get renderAudioWaveform() { - return !this.props.mediaPath ? (null) : - <div className="collectionStackedTimeline-waveform" > + return !this.props.mediaPath ? null : ( + <div className="collectionStackedTimeline-waveform"> <AudioWaveform duration={this.duration} mediaPath={this.props.mediaPath} - dataDoc={this.dataDoc} - PanelHeight={this.timelineContentHeight} /> - </div>; + layoutDoc={this.layoutDoc} + PanelHeight={this.timelineContentHeight} + trimming={this.props.trimming} + /> + </div> + ); } + currentTimecode = () => this.currentTime; render() { const timelineContentWidth = this.props.PanelWidth(); - const overlaps: { anchorStartTime: number, anchorEndTime: number, level: number }[] = []; - const drawAnchors = this.childDocs.map(anchor => ({ level: this.getLevel(anchor, overlaps), anchor })); + const overlaps: { + anchorStartTime: number; + anchorEndTime: number; + level: number; + }[] = []; + const drawAnchors = this.childDocs.map((anchor) => ({ + level: this.getLevel(anchor, overlaps), + anchor, + })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; - const isActive = this.props.isContentActive() || this.props.isSelected(false); - return <div className="collectionStackedTimeline" ref={(timeline: HTMLDivElement | null) => this._timeline = timeline} - onClick={e => isActive && StopEvent(e)} onPointerDown={e => isActive && this.onPointerDownTimeline(e)}> - {drawAnchors.map(d => { - const start = this.anchorStart(d.anchor); - const end = this.anchorEnd(d.anchor, start + 10 / timelineContentWidth * this.duration); - const left = start / this.duration * timelineContentWidth; - const top = d.level / maxLevel * this.timelineContentHeight(); - const timespan = end - start; - return this.props.Document.hideAnchors ? (null) : - <div className={"collectionStackedTimeline-marker-timeline"} key={d.anchor[Id]} - style={{ left, top, width: `${timespan / this.duration * timelineContentWidth}px`, height: `${this.timelineContentHeight() / maxLevel}px` }} - onClick={e => { this.props.playFrom(start, this.anchorEnd(d.anchor)); e.stopPropagation(); }} > - <StackedTimelineAnchor {...this.props} - mark={d.anchor} - rangeClickScript={this.rangeClickScript} - rangePlayScript={this.rangePlayScript} - left={left} - top={top} - width={timelineContentWidth * timespan / this.duration} - height={this.timelineContentHeight() / maxLevel} - toTimeline={this.toTimeline} - layoutDoc={this.layoutDoc} - currentTimecode={this.currentTimecode} - _timeline={this._timeline} - stackedTimeline={this} - /> - </div>; - })} - {this.selectionContainer} - {this.renderAudioWaveform} - {this.renderDictation} - - <div className="collectionStackedTimeline-current" style={{ left: `${this.currentTime / this.duration * 100}%` }} /> - </div>; + const isActive = + this.props.isContentActive() || this.props.isSelected(false); + return (<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? "all" : undefined }}> + <div + className="collectionStackedTimeline" + ref={(timeline: HTMLDivElement | null) => (this._timeline = timeline)} + onClick={(e) => isActive && StopEvent(e)} + onPointerDown={(e) => isActive && this.onPointerDownTimeline(e)} + > + {drawAnchors.map((d) => { + + const start = this.anchorStart(d.anchor); + const end = this.anchorEnd( + d.anchor, + start + (10 / timelineContentWidth) * this.duration + ); + const left = this.props.trimming ? + (start / this.duration) * timelineContentWidth + : (start - this.trimStart) / this.props.trimDuration * timelineContentWidth; + const top = (d.level / maxLevel) * this.timelineContentHeight(); + const timespan = end - start; + const width = (timespan / this.props.trimDuration) * timelineContentWidth; + const height = this.timelineContentHeight() / maxLevel; + return this.props.Document.hideAnchors ? null : ( + <div + className={"collectionStackedTimeline-marker-timeline"} + key={d.anchor[Id]} + style={{ + left, + top, + width: `${width}px`, + height: `${height}px`, + }} + onClick={(e) => { + this.props.playFrom(start, this.anchorEnd(d.anchor)); + e.stopPropagation(); + }} + > + <StackedTimelineAnchor + {...this.props} + mark={d.anchor} + rangeClickScript={this.rangeClickScript} + rangePlayScript={this.rangePlayScript} + left={left} + top={top} + width={width} + height={height} + toTimeline={this.toTimeline} + layoutDoc={this.layoutDoc} + currentTimecode={this.currentTimecode} + _timeline={this._timeline} + stackedTimeline={this} + trimStart={this.trimStart} + trimEnd={this.trimEnd} + /> + </div> + ); + })} + {!this.props.trimming && this.selectionContainer} + {this.renderAudioWaveform} + {this.renderDictation} + + <div + className="collectionStackedTimeline-current" + style={{ + left: this.props.trimming + ? `${(this.currentTime / this.duration) * 100}%` + : `${(this.currentTime - this.trimStart) / (this.trimEnd - this.trimStart) * 100}%`, + }} + /> + + {this.props.trimming && ( + <> + <div + className="collectionStackedTimeline-trim-shade" + style={{ width: `${(this.trimStart / this.duration) * 100}%` }} + ></div> + + <div + className="collectionStackedTimeline-trim-controls" + style={{ + left: `${(this.trimStart / this.duration) * 100}%`, + width: `${((this.trimEnd - this.trimStart) / this.duration) * 100 + }%`, + }} + > + <div + className="collectionStackedTimeline-trim-handle" + onPointerDown={this.trimLeft} + ></div> + <div + className="collectionStackedTimeline-trim-handle" + onPointerDown={this.trimRight} + ></div> + </div> + + <div + className="collectionStackedTimeline-trim-shade" + style={{ + left: `${(this.trimEnd / this.duration) * 100}%`, + width: `${((this.duration - this.trimEnd) / this.duration) * 100 + }%`, + }} + ></div> + </> + )} + </div> + </div>); } } @@ -335,6 +686,8 @@ interface StackedTimelineAnchorProps { currentTimecode: () => number; isSelected: (outsideReaction?: boolean) => boolean; stackedTimeline: CollectionStackedTimeline; + trimStart: number; + trimEnd: number; } @observer class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> { @@ -345,22 +698,41 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> this._lastTimecode = this.props.currentTimecode(); } componentDidMount() { - this._disposer = reaction(() => this.props.currentTimecode(), + this._disposer = reaction( + () => this.props.currentTimecode(), (time) => { - const dictationDoc = Cast(this.props.layoutDoc["data-dictation"], Doc, null); - const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc); - if (!LightboxView.LightboxDoc + const dictationDoc = Cast( + this.props.layoutDoc["data-dictation"], + Doc, + null + ); + const isDictation = + dictationDoc && + DocListCast(this.props.mark.links).some( + (link) => + Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc + ); + if ( + !LightboxView.LightboxDoc && // bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront. // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video. /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/ - && DocListCast(this.props.mark.links).length && + DocListCast(this.props.mark.links).length && time > NumCast(this.props.mark[this.props.startTag]) && time < NumCast(this.props.mark[this.props.endTag]) && - this._lastTimecode < NumCast(this.props.mark[this.props.startTag])) { - LinkManager.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true); + this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) + ) { + LinkManager.FollowLink( + undefined, + this.props.mark, + this.props as any as DocumentViewProps, + false, + true + ); } this._lastTimecode = time; - }); + } + ); } componentWillUnmount() { this._disposer?.(); @@ -373,57 +745,136 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> return this.props.toTimeline(e.clientX - rect.x, rect.width); }; const changeAnchor = (anchor: Doc, left: boolean, time: number) => { - const timelineOnly = Cast(anchor[this.props.startTag], "number", null) !== undefined; - if (timelineOnly) Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true); - else left ? anchor._timecodeToShow = time : anchor._timecodeToHide = time; + const timelineOnly = + Cast(anchor[this.props.startTag], "number", null) !== undefined; + if (timelineOnly) { + Doc.SetInPlace( + anchor, + left ? this.props.startTag : this.props.endTag, + time, + true + ); + } + else { + left + ? (anchor._timecodeToShow = time) + : (anchor._timecodeToHide = time); + } return false; }; - setupMoveUpEvents(this, e, + setupMoveUpEvents( + this, + e, (e) => changeAnchor(anchor, left, newTime(e)), (e) => { this.props.setTime(newTime(e)); this.props._timeline?.releasePointerCapture(e.pointerId); }, - emptyFunction); + emptyFunction + ); } - renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), x: number, y: number, width: number, height: number) { + + @action + computeTitle = () => { + const start = Math.max(NumCast(this.props.mark[this.props.startTag]), this.props.trimStart) - this.props.trimStart; + const end = Math.min(NumCast(this.props.mark[this.props.endTag]), this.props.trimEnd) - this.props.trimStart; + return `#${formatTime(start)}-${formatTime(end)}`; + } + + renderInner = computedFn(function ( + this: StackedTimelineAnchor, + mark: Doc, + script: undefined | (() => ScriptField), + doublescript: undefined | (() => ScriptField), + x: number, + y: number, + width: number, + height: number + ) { const anchor = observable({ view: undefined as any }); - const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => { + const focusFunc = ( + doc: Doc, + willZoom?: boolean, + scale?: number, + afterFocus?: DocAfterFocusFunc, + docTransform?: Transform + ) => { this.props.playLink(mark); this.props.focus(doc, { willZoom, scale, afterFocus, docTransform }); }; return { - anchor, view: <DocumentView key="view" {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} - ref={action((r: DocumentView | null) => anchor.view = r)} - Document={mark} - DataDoc={undefined} - renderDepth={this.props.renderDepth + 1} - LayoutTemplate={undefined} - LayoutTemplateString={LabelBox.LayoutString("data")} - isDocumentActive={returnFalse} - PanelWidth={() => width} - PanelHeight={() => height} - ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-x, -y)} - focus={focusFunc} - rootSelected={returnFalse} - onClick={script} - onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript} - ignoreAutoHeight={false} - hideResizeHandles={true} - bringToFront={emptyFunction} - scriptContext={this.props.stackedTimeline} /> + anchor, + view: ( + <DocumentView + key="view" + {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} + ref={action((r: DocumentView | null) => (anchor.view = r))} + Document={mark} + DataDoc={undefined} + renderDepth={this.props.renderDepth + 1} + LayoutTemplate={undefined} + LayoutTemplateString={LabelBox.LayoutStringWithTitle(LabelBox, "data", this.computeTitle())} + isDocumentActive={returnFalse} + PanelWidth={() => width} + PanelHeight={() => height} + ScreenToLocalTransform={() => + this.props.ScreenToLocalTransform().translate(-x, -y) + } + focus={focusFunc} + rootSelected={returnFalse} + onClick={script} + onDoubleClick={ + this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript + } + ignoreAutoHeight={false} + hideResizeHandles={true} + bringToFront={emptyFunction} + scriptContext={this.props.stackedTimeline} + /> + ), }; }); + render() { - const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.props.left, this.props.top, this.props.width, this.props.height); - return <> - {inner.view} - {!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? (null) : - <> - <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} /> - <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} /> - </>} - </>; + const inner = this.renderInner( + this.props.mark, + this.props.rangeClickScript, + this.props.rangePlayScript, + this.props.left, + this.props.top, + this.props.width, + this.props.height + ); + return ( + <> + {inner.view} + {!inner.anchor.view || + !SelectionManager.IsSelected(inner.anchor.view) ? null : ( + <> + <div + key="left" + className="collectionStackedTimeline-left-resizer" + onPointerDown={(e) => this.onAnchorDown(e, this.props.mark, true)} + /> + <div + key="right" + className="collectionStackedTimeline-resizer" + onPointerDown={(e) => + this.onAnchorDown(e, this.props.mark, false) + } + /> + </> + )} + </> + ); } } -Scripting.addGlobal(function formatToTime(time: number): any { return formatTime(time); });
\ No newline at end of file +Scripting.addGlobal(function formatToTime(time: number): any { + return formatTime(time); +}); +Scripting.addGlobal(function min(num1: number, num2: number): number { + return Math.min(num1, num2); +}); +Scripting.addGlobal(function max(num1: number, num2: number): number { + return Math.max(num1, num2); +});
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 4b123c8b6..2f002736d 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -14,6 +14,30 @@ width: 100%; } +// TODO:glr Turn this into a seperate class +.documentButtonMenu { + position: relative; + height: fit-content; + border-bottom: $standard-border; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + align-content: center; + padding: 5px 0 5px 0; + + .documentExplanation { + width: 90%; + margin: 5px; + font-size: 11px; + background-color: $light-blue; + color: $medium-blue; + padding: 10px; + border-radius: 10px; + border: solid 2px $medium-blue; + } +} + .collectionStackingView, .collectionMasonryView { height: 100%; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index b9bc83d49..648ff5087 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { CursorProperty } from "csstype"; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { DataSym, Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../fields/documentSchemas"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; @@ -11,7 +11,7 @@ import { listSpec, makeInterface } from "../../../fields/Schema"; import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils"; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, Utils, returnTrue, returnEmptyDoclist, returnEmptyFilter } from "../../../Utils"; import { DocUtils, Docs } from "../../documents/Documents"; import { DragManager, dropActionType } from "../../util/DragManager"; import { SnappingManager } from "../../util/SnappingManager"; @@ -23,12 +23,14 @@ import { EditableView } from "../EditableView"; import { LightboxView } from "../LightboxView"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from "../nodes/DocumentView"; -import { StyleProp } from "../StyleProvider"; +import { StyleProp, DefaultStyleProvider } from "../StyleProvider"; import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; import "./CollectionStackingView.scss"; import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionViewType } from "./CollectionView"; +import { FontIconBox } from "../nodes/button/FontIconBox"; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; const _global = (window /* browser */ || global /* node */) as any; type StackingDocument = makeInterface<[typeof collectionSchema, typeof documentSchema]>; @@ -144,7 +146,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, () => this.layoutDoc._columnHeaders = new List() ); this._autoHeightDisposer = reaction(() => this.layoutDoc._autoHeight, - () => this.props.setHeight(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), + autoHeight => autoHeight && this.props.setHeight(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + (this.isStackingView ? Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))) : this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0))))); @@ -234,6 +236,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, dontCenter={this.props.childIgnoreNativeSize ? "xy" : undefined} dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} + showTitle={this.props.childShowTitle} dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} @@ -514,6 +517,47 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, return sections.map((section, i) => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1], i === 0)); } + @computed get buttonMenu() { + const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + // TODO:glr Allow support for multiple buttons + if (menuDoc) { + const width: number = NumCast(menuDoc._width, 30); + const height: number = NumCast(menuDoc._height, 30); + console.log(menuDoc.title, width, height); + return (<div className="buttonMenu-docBtn" + style={{ width: width, height: height }}> + <DocumentView + Document={menuDoc} + DataDoc={menuDoc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={() => 35} + PanelHeight={() => 35} + renderDepth={this.props.renderDepth} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div> + ); + } + } + @computed get nativeWidth() { return this.props.NativeWidth?.() ?? Doc.NativeWidth(this.layoutDoc); } @computed get nativeHeight() { return this.props.NativeHeight?.() ?? Doc.NativeHeight(this.layoutDoc); } @@ -529,34 +573,49 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, SetValue: this.addGroup, contents: "+ ADD A GROUP" }; + const buttonMenu = this.rootDoc.buttonMenu; + const noviceExplainer = this.rootDoc.explainer; return ( - <div className="collectionStackingMasonry-cont" > - <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} - ref={this.createRef} - style={{ - overflowY: this.props.isContentActive() ? "auto" : "hidden", - background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), - pointerEvents: this.backgroundEvents ? "all" : undefined - }} - onScroll={action(e => this._scroll = e.currentTarget.scrollTop)} - onDrop={this.onExternalDrop.bind(this)} - onContextMenu={this.onContextMenu} - onWheel={e => this.props.isContentActive(true) && e.stopPropagation()} > - {this.renderedSections} - {!this.showAddAGroup ? (null) : - <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" - style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> - <EditableView {...editableViewProps} /> - </div>} - {/* {this.chromeHidden || !this.props.isSelected() ? (null) : - <Switch - onChange={this.onToggle} - onClick={this.onToggle} - defaultChecked={true} - checkedChildren="edit" - unCheckedChildren="view" - />} */} - </div> </div> + <> + {buttonMenu || noviceExplainer ? <div className="documentButtonMenu"> + {buttonMenu ? this.buttonMenu : null} + {Doc.UserDoc().noviceMode && noviceExplainer ? + <div className="documentExplanation"> + {noviceExplainer} + </div> + : null + } + </div> : null} + <div className="collectionStackingMasonry-cont" > + <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} + ref={this.createRef} + style={{ + overflowY: this.props.isContentActive() ? "auto" : "hidden", + background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), + pointerEvents: this.backgroundEvents ? "all" : undefined + }} + onScroll={action(e => this._scroll = e.currentTarget.scrollTop)} + onDrop={this.onExternalDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={e => this.props.isContentActive(true) && e.stopPropagation()} > + {this.renderedSections} + {!this.showAddAGroup ? (null) : + <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" + style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> + <EditableView {...editableViewProps} /> + </div>} + {/* {this.chromeHidden || !this.props.isSelected() ? (null) : + <Switch + onChange={this.onToggle} + onClick={this.onToggle} + defaultChecked={true} + checkedChildren="edit" + unCheckedChildren="view" + />} */} + </div> + </div> + </> + ); } } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 47733994b..58289a161 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -111,7 +111,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = "#b4b4b4"); @action pointerLeave = () => this._background = "inherit"; - textCallback = (char: string) => this.addNewTextDoc("", false, true); + textCallback = (char: string) => this.addNewTextDoc("-typed text-", false, true); @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 2f07c0241..cb8b55cb2 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,6 +1,6 @@ import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx"; import CursorField from "../../../fields/CursorField"; -import { Doc, Opt, Field, DocListCast, AclPrivate } from "../../../fields/Doc"; +import { Doc, Opt, Field, DocListCast, AclPrivate, StrListCast } from "../../../fields/Doc"; import { Id, ToString } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; @@ -23,6 +23,7 @@ import ReactLoading from 'react-loading'; export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; SetSubView?: (subView: any) => void; + isAnyChildContentActive: () => boolean; } export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: X) { @@ -88,12 +89,13 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: get childDocList() { return Cast(this.dataField, listSpec(Doc)); } - collectionFilters = () => this._focusFilters ?? Cast(this.props.Document._docFilters, listSpec("string"), []); + collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters); collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec("string"), []); - childDocFilters = () => [...this.props.docFilters(), ...this.collectionFilters()]; + childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; + unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])]; childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()]; IsFiltered = () => this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" : - this.props.docFilters().length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined + this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs); @computed.struct get childDocs() { TraceMobx(); @@ -113,10 +115,10 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField); const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - const docFilters = this.childDocFilters(); + const childDocFilters = this.childDocFilters(); const docRangeFilters = this.childDocRangeFilters(); const searchDocs = this.searchFilterDocs(); - if (this.props.Document.dontRegisterView || (!docFilters.length && !docRangeFilters.length && !searchDocs.length)) { + if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !docRangeFilters.length && !searchDocs.length)) { return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one } @@ -127,24 +129,27 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const docsforFilter: Doc[] = []; childDocs.forEach((d) => { // if (DocUtils.Excluded(d, docFilters)) return; - let notFiltered = d.z || Doc.IsSystem(d) || ((!searchDocs.length || searchDocs.includes(d)) && (DocUtils.FilterDocs([d], docFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0)); - const fieldKey = Doc.LayoutFieldKey(d); - const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); - const data = d[annos ? fieldKey + "-annotations" : fieldKey]; - if (data !== undefined) { - let subDocs = DocListCast(data); - if (subDocs.length > 0) { - let newarray: Doc[] = []; - notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, docFilters, docRangeFilters, viewSpecScript, d).length); - while (subDocs.length > 0 && !notFiltered) { - newarray = []; - subDocs.forEach((t) => { - const fieldKey = Doc.LayoutFieldKey(t); - const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); - notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!docFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], docFilters, docRangeFilters, viewSpecScript, d).length)); - DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); - }); - subDocs = newarray; + let notFiltered = d.z || Doc.IsSystem(d) || (DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, viewSpecScript, this.props.Document).length > 0); + if (notFiltered) { + notFiltered = ((!searchDocs.length || searchDocs.includes(d)) && (DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, viewSpecScript, this.props.Document).length > 0)); + const fieldKey = Doc.LayoutFieldKey(d); + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes("CollectionView"); + const data = d[annos ? fieldKey + "-annotations" : fieldKey]; + if (data !== undefined) { + let subDocs = DocListCast(data); + if (subDocs.length > 0) { + let newarray: Doc[] = []; + notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, docRangeFilters, viewSpecScript, d).length); + while (subDocs.length > 0 && !notFiltered) { + newarray = []; + subDocs.forEach((t) => { + const fieldKey = Doc.LayoutFieldKey(t); + const annos = !Field.toString(Doc.LayoutField(t) as Field).includes("CollectionView"); + notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], childDocFilters, docRangeFilters, viewSpecScript, d).length)); + DocListCast(t[annos ? fieldKey + "-annotations" : fieldKey]).forEach((newdoc) => newarray.push(newdoc)); + }); + subDocs = newarray; + } } } } @@ -211,7 +216,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]); const someMoved = !docDragData.userDropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop); - if ((!dropAction || dropAction === "move" || someMoved) && docDragData.moveDocument) { + if ((!dropAction || dropAction === "same" || dropAction === "move" || someMoved) && docDragData.moveDocument) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); if (movedDocs.length) { @@ -316,12 +321,8 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: } }); } else { - let srcUrl: string | undefined; - let srcWeb: Doc | undefined; - if (SelectionManager.Views().length) { - srcWeb = SelectionManager.Views()[0].props.Document; - srcUrl = (srcWeb.data as WebField).url?.href?.match(/http[s]?:\/\/[^/]*/)?.[0]; - } + const srcWeb = SelectionManager.Docs().lastElement(); + const srcUrl = (srcWeb?.data as WebField).url?.href?.match(/http[s]?:\/\/[^/]*/)?.[0]; const reg = new RegExp(Utils.prepend(""), "g"); const modHtml = srcUrl ? html.replace(reg, srcUrl) : html; const htmlDoc = Docs.Create.HtmlDocument(modHtml, { ...options, title: "-web page-", _width: 300, _height: 300 }); @@ -486,7 +487,5 @@ import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTex import { CollectionView, CollectionViewType, CollectionViewProps } from "./CollectionView"; import { SelectionManager } from "../../util/SelectionManager"; import { OverlayView } from "../OverlayView"; -import { Hypothesis } from "../../util/HypothesisUtils"; import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; -import { FilterBox } from "../nodes/FilterBox"; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index ec461ab94..d370d21ab 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,5 +1,8 @@ @import "../global/globalCssVariables"; +.collectionTreeView-container { + transform-origin: top left; +} .collectionTreeView-dropTarget { border-width: $COLLECTION_BORDER_WIDTH; border-color: transparent; @@ -35,6 +38,11 @@ width: max-content; } + .no-indent-outline { + padding-left: 0; + width: 100%; + } + .editableView-container { font-weight: bold; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index a78034dca..3852987b9 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,15 +1,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, reaction, IReactionDisposer, observable } from "mobx"; +import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc'; +import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { List } from '../../../fields/List'; +import { InkTool } from '../../../fields/InkField'; import { Document, listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; +import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyFunction } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; @@ -25,9 +26,7 @@ import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import { TreeView } from "./TreeView"; import React = require("react"); -import { InkTool } from '../../../fields/InkField'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; -import { CollectionView, CollectionViewType } from './CollectionView'; +import { Transform } from '../../util/Transform'; const _global = (window /* browser */ || global /* node */) as any; export type collectionTreeViewProps = { @@ -63,7 +62,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll this.props.isSelected(outsideReaction) || this._isAnyChildContentActive || this.props.rootSelected(outsideReaction)) ? true : false) + isDisposing = false; componentWillUnmount() { + this.isDisposing = true; super.componentWillUnmount(); this.treedropDisposer?.(); Object.values(this._disposers).forEach(disposer => disposer?.()); @@ -78,15 +79,16 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll refList: Set<any> = new Set(); observer: any; computeHeight = () => { - const hgt = this.paddingTop() + 26/* bcz: ugh: title bar height hack ... get ref and compute instead */ + - Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0); - this.props.setHeight(hgt); + if (this.isDisposing) return; + const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.paddingTop() + this.paddingBot()); + this.layoutDoc._autoHeightMargins = bodyHeight; + this.props.setHeight(this.documentTitleHeight() + bodyHeight); } unobserveHeight = (ref: any) => { this.refList.delete(ref); this.rootDoc.autoHeight && this.computeHeight(); } - observerHeight = (ref: any) => { + observeHeight = (ref: any) => { if (ref) { this.refList.add(ref); this.observer = new _global.ResizeObserver(action((entries: any) => { @@ -106,7 +108,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData) { - const isInTree = () => dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d)); + const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d)); dragData.dropAction = targetAction && !isInTree() ? targetAction : this.doc === dragData?.treeViewDoc ? "same" : dragData.dropAction; } } @@ -201,6 +203,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll NativeWidth={this.documentTitleWidth} NativeHeight={this.documentTitleHeight} focus={this.props.focus} + treeViewDoc={this.props.Document} ScreenToLocalTransform={this.titleTransform} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} @@ -219,8 +222,9 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll } childContextMenuItems = () => { const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []); - const filterScripts = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []); - return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: filterScripts[i], label })); + const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []); + const icons = StrListCast(this.doc.childContextMenuIcons); + return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); } @computed get treeViewElements() { TraceMobx(); @@ -253,50 +257,111 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll true, this.whenChildContentsActiveChanged, this.props.dontRegisterView || Cast(this.props.Document.childDontRegisterViews, "boolean", null), - this.observerHeight, + this.observeHeight, this.unobserveHeight, this.childContextMenuItems() ); } @computed get titleBar() { const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; - return hideTitle ? (null) : (this.doc.treeViewType === "outline" ? this.documentTitle : this.editableTitle)(this.treeChildren); + return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren); } - @computed get renderClearButton() { - return !this.doc.treeViewShowClearButton ? (null) : <div key="toolbar"> - <button className="toolbar-button round-button" title="Empty" onClick={undoBatch(action(() => Doc.GetProto(this.doc)[this.props.fieldKey] = undefined))}> - <FontAwesomeIcon icon={"trash"} size="sm" /> - </button> - </div >; + return35 = () => 35; + @computed get buttonMenu() { + const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + // To create a multibutton menu add a CollectionLinearView + if (menuDoc) { + + const width: number = NumCast(menuDoc._width, 30); + const height: number = NumCast(menuDoc._height, 30); + console.log(menuDoc.title, width, height); + return (<div className="buttonMenu-docBtn" + style={{ width: width, height: height }}> + <DocumentView + Document={menuDoc} + DataDoc={menuDoc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.return35} + PanelHeight={this.return35} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div>); + } } + @observable _explainerHeight: number = 0; + + @computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); } + @computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); } + + @computed get contentScaling() { + const nw = this.nativeWidth; + const nh = this.nativeHeight; + const hscale = nh ? this.props.PanelHeight() / nh : 1; + const wscale = nw ? this.props.PanelWidth() / nw : 1; + return wscale < hscale ? wscale : hscale; + } paddingX = () => NumCast(this.doc._xPadding, 15); paddingTop = () => NumCast(this.doc._yPadding, 20); + paddingBot = () => NumCast(this.doc._yPadding, 20); documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth()); - documentTitleHeight = () => Math.min(this.layoutDoc?.[HeightSym](), (StrCast(this.layoutDoc?._fontSize) ? Number(StrCast(this.layoutDoc?._fontSize, "32px").replace("px", "")) : NumCast(this.layoutDoc?._fontSize)) * 2); + documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins); titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20)); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); - panelWidth = () => this.props.PanelWidth() - 2 * this.paddingX(); + panelWidth = () => (this.props.PanelWidth() - 2 * this.paddingX()) * (this.props.scaling?.() || 1); render() { TraceMobx(); const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined; + const buttonMenu = this.rootDoc.buttonMenu; + const noviceExplainer = this.rootDoc.explainer; return !(this.doc instanceof Doc) || !this.treeChildren ? (null) : - <div className="collectionTreeView-container" onContextMenu={this.onContextMenu}> - <div className="collectionTreeView-dropTarget" - style={{ background: background(), paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }} - onWheel={e => e.stopPropagation()} - onDrop={this.onTreeDrop} - ref={this.createTreeDropTarget}> - {this.titleBar} - {this.renderClearButton} - <ul className="no-indent"> - {this.treeViewElements} - </ul> - </div > - </div>; + <> + {this.titleBar} + <div className="collectionTreeView-container" + style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}} + onContextMenu={this.onContextMenu}> + {buttonMenu || noviceExplainer ? <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}> + {buttonMenu ? this.buttonMenu : null} + {Doc.UserDoc().noviceMode && noviceExplainer ? + <div className="documentExplanation"> + {noviceExplainer} + </div> + : null + } + </div> : null} + <div className="collectionTreeView-dropTarget" + style={{ background: background(), height: `calc(100% - ${this._explainerHeight}px)`, paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingBottom: `${this.paddingBot()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }} + onWheel={e => e.stopPropagation()} + onDrop={this.onTreeDrop} + ref={this.createTreeDropTarget}> + <ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} > + {this.treeViewElements} + </ul> + </div > + </div> + </>; } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 2485c0f6a..0dd1e6e36 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -24,7 +24,7 @@ import { CollectionCarouselView } from './CollectionCarouselView'; import { CollectionDockingView } from "./CollectionDockingView"; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import { CollectionGridView } from './collectionGrid/CollectionGridView'; -import { CollectionLinearView } from './CollectionLinearView'; +import { CollectionLinearView } from './collectionLinear'; import CollectionMapView from './MapView/CollectionMapView'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; @@ -36,6 +36,8 @@ import { CollectionTimeView } from './CollectionTimeView'; import { CollectionTreeView } from "./CollectionTreeView"; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import './CollectionView.scss'; +import { returnEmptyString } from '../../../Utils'; +import { InkTool } from '../../../fields/InkField'; export const COLLECTION_BORDER_WIDTH = 2; const path = require('path'); @@ -62,13 +64,14 @@ export enum CollectionViewType { export interface CollectionViewProps extends FieldViewProps { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) layoutEngine?: () => string; - setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; // property overrides for child documents children?: never | (() => JSX.Element[]) | React.ReactNode; childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) childDocumentsActive?: () => boolean;// whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode) childFitWidth?: () => boolean; + childShowTitle?: () => string; childOpacity?: () => number; childContextMenuItems?: () => { script: ScriptField, label: string }[]; childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children @@ -84,7 +87,7 @@ export interface CollectionViewProps extends FieldViewProps { type CollectionDocument = makeInterface<[typeof documentSchema]>; const CollectionDocument = makeInterface(documentSchema); @observer -export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & CollectionViewProps, CollectionDocument>(CollectionDocument, "") { +export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & CollectionViewProps, CollectionDocument>(CollectionDocument) { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); } @observable private static _safeMode = false; @@ -92,6 +95,11 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + constructor(props: any) { + super(props); + runInAction(() => this._annotationKeySuffix = returnEmptyString); + } + get collectionViewType(): CollectionViewType | undefined { const viewField = StrCast(this.layoutDoc._viewType); if (CollectionView._safeMode) { @@ -113,7 +121,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab // const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto); // const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags); // return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />; - this.isContentActive(); + //this.isContentActive(); } screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); @@ -158,7 +166,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab !Doc.UserDoc().noviceMode && subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" }); subItems.push({ description: "Grid", event: () => func(CollectionViewType.Grid), icon: "th-list" }); - if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.annotationOn) { + if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) { const existingVm = ContextMenu.Instance.findByDescription(category); const catItems = existingVm && "subitems" in existingVm ? existingVm.subitems : []; catItems.push({ description: "Add a Perspective...", addDivider: true, noexpand: true, subitems: subItems, icon: "eye" }); @@ -247,6 +255,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab @observable _subView: any = undefined; + isContentActive = (outsideReaction?: boolean) => { + return this.props.isContentActive() ? true : false; + } render() { TraceMobx(); const props: SubCollectionViewProps = { @@ -256,6 +267,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab moveDocument: this.moveDocument, removeDocument: this.removeDocument, isContentActive: this.isContentActive, + isAnyChildContentActive: this.isAnyChildContentActive, + whenChildContentsActiveChanged: this.whenChildContentsActiveChanged, PanelWidth: this.bodyPanelWidth, PanelHeight: this.props.PanelHeight, ScreenToLocalTransform: this.screenToLocalTransform, diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index dddccd18c..0d045bada 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -1,3 +1,6 @@ +@import "../global/globalCssVariables.scss"; + + input.lm_title:focus, input.lm_title { max-width: unset !important; @@ -57,12 +60,11 @@ input.lm_title { } } -.miniMap-hidden, .miniMap { position: absolute; overflow: hidden; - right: 10; - bottom: 10; + right: 15; + bottom: 15; border: solid 1px; width: 100%; height: 100%; @@ -81,17 +83,21 @@ input.lm_title { } .miniMap-hidden { + cursor: pointer; position: absolute; - bottom: 0; - right: 0; - width: 45px; - height: 45px; - transform: translate(20px, 20px) rotate(45deg); - border-radius: 30px; + bottom: 5; + display: flex; + right: 5; + width: 25px; + height: 25px; + border-radius: 3px; padding: 2px; + justify-content: center; + align-items: center; + align-content: center; + background-color: $light-gray; - > svg { - margin-top: 3px; - transform: translate(0px, 7px); + &:hover { + box-shadow: none; } } diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index e6e71a3c4..73c065482 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -11,7 +11,6 @@ import { DataSym, Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; -import { TraceMobx } from '../../../fields/util'; import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { DocUtils } from '../../documents/Documents'; @@ -34,6 +33,7 @@ import { CollectionView, CollectionViewType } from './CollectionView'; import "./TabDocView.scss"; import React = require("react"); import Color = require('color'); +import { Colors, Shadows } from '../global/globalEnums'; const _global = (window /* browser */ || global /* node */) as any; interface TabDocViewProps { @@ -124,6 +124,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { tab.element[0].prepend(iconWrap); tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), ({ layer, color }) => { + // console.log("TabDocView: " + this.tabColor); + // console.log("lightOrDark: " + lightOrDark(this.tabColor)); const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color titleEle.style.color = textColor; titleEle.style.backgroundColor = "transparent"; @@ -132,12 +134,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { moreInfoDrag.style.backgroundColor = textColor; tab.element[0].style.background = !layer ? color : "dimgrey"; }, { fireImmediately: true }); - // TODO:glr fix - // tab.element[0].style.borderTopRightRadius = "8px"; - // tab.element[0].children[1].appendChild(toggle); - // tab._disposers.layerDisposer = reaction(() => - // ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), - // ({ layer, color }) => toggle.style.background = !layer ? color : "dimgrey", { fireImmediately: true }); } // shifts the focus to this tab when another tab is dragged over it tab.element[0].onmouseenter = (e: MouseEvent) => { @@ -313,6 +309,14 @@ export class TabDocView extends React.Component<TabDocViewProps> { return CollectionDockingView.AddSplit(doc, locationParams, this.stack); } } + remDocTab = (doc: Doc | Doc[]) => { + if (doc === this._document) { + SelectionManager.DeselectAll(); + CollectionDockingView.CloseSplit(this._document); + return true; + } + return false; + } getCurrentFrame = () => { return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); @@ -349,7 +353,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { @computed get layerProvider() { return this._document && DefaultLayerProvider(this._document); } @computed get docView() { - TraceMobx(); return !this._activated || !this._document || this._document._viewType === CollectionViewType.Docking ? (null) : <><DocumentView key={this._document[Id]} ref={action((r: DocumentView) => this._view = r)} renderDepth={0} @@ -366,7 +369,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters} searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} addDocument={undefined} - removeDocument={undefined} + removeDocument={this.remDocTab} addDocTab={this.addDocTab} ScreenToLocalTransform={this.ScreenToLocalTransform} dontCenter={"y"} @@ -384,8 +387,16 @@ export class TabDocView extends React.Component<TabDocViewProps> { background={this.miniMapColor} document={this._document} tabView={this.tabView} /> - <Tooltip style={{ display: this.disableMinimap() ? "none" : undefined }} key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}> - <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + <Tooltip key="ttip" title={<div className="dash-tooltip">{this._document.hideMinimap ? "Open minimap" : "Close minimap"}</div>}> + <div className="miniMap-hidden" + style={{ + display: this.disableMinimap() || this._document._viewType !== "freeform" ? "none" : undefined, + color: this._document.hideMinimap ? Colors.BLACK : Colors.WHITE, + backgroundColor: this._document.hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE, + boxShadow: this._document.hideMinimap ? Shadows.STANDARD_SHADOW : undefined + }} + onPointerDown={e => e.stopPropagation()} + onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > <FontAwesomeIcon icon={"globe-asia"} size="lg" /> </div> </Tooltip> @@ -393,7 +404,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { } render() { - this.tab && CollectionDockingView.Instance.tabMap.delete(this.tab); return ( <div className="collectionDockingView-content" style={{ fontFamily: Doc.UserDoc().renderStyle === "comic" ? "Comic Sans MS" : undefined, @@ -475,7 +485,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. noOverlay={true} // don't render overlay Docs since they won't scale setHeight={returnFalse} - isContentActive={returnTrue} + isContentActive={returnFalse} + isAnyChildContentActive={returnFalse} select={emptyFunction} dropAction={undefined} isSelected={returnFalse} diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 3ee9dbf59..97de097e0 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -9,7 +9,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils, returnOne } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from "../../documents/DocumentTypes"; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; @@ -54,7 +54,7 @@ export interface TreeViewProps { indentDocument?: (editTitle: boolean) => void; outdentDocument?: (editTitle: boolean) => void; ScreenToLocalTransform: () => Transform; - contextMenuItems: { script: ScriptField, filter: ScriptField, label: string }[]; + contextMenuItems: { script: ScriptField, filter: ScriptField, icon: string, label: string }[]; dontRegisterView?: boolean; styleProvider?: StyleProviderFunc | undefined; treeViewHideHeaderFields: () => boolean; @@ -154,8 +154,8 @@ export class TreeView extends React.Component<TreeViewProps> { } else { // choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias // this.props.addDocTab(CurrentUserUtils.ActiveDashboard.isShared ? Doc.MakeAlias(this.props.document) : this.props.document, "add:right"); - // choose an appropriate alias or make one -- -- choose the first alias that (1) the user owns, (2) has no context field - if I own it and someone else does not have it open,, otherwise create an alias - this.props.addDocTab(this.props.document, "add:right"); + const bestAlias = DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); + this.props.addDocTab(bestAlias ?? Doc.MakeAlias(this.props.document), "add:right"); } } @@ -240,10 +240,13 @@ export class TreeView extends React.Component<TreeViewProps> { } makeFolder = () => { - const folder = Docs.Create.TreeDocument([], { title: "-folder-", _stayInCollection: true, isFolder: true }); + const folder = Docs.Create.TreeDocument([], { title: "Untitled folder", _stayInCollection: true, isFolder: true }); TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView }; return this.props.addDocument(folder); } + deleteFolder = () => { + return this.props.removeDoc?.(this.doc); + } preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; @@ -296,7 +299,7 @@ export class TreeView extends React.Component<TreeViewProps> { const aspect = Doc.NativeAspect(layoutDoc); if (layoutDoc._fitWidth) return Math.min(this.props.panelWidth() - treeBulletWidth(), layoutDoc[WidthSym]()); if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.panelWidth() - treeBulletWidth())); - return Math.min(this.props.panelWidth() - treeBulletWidth(), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); + return Math.min((this.props.panelWidth() - treeBulletWidth()) / (this.props.treeView.props.scaling?.() || 1), Doc.NativeWidth(layoutDoc) ? layoutDoc[WidthSym]() : this.layoutDoc[WidthSym]()); } docHeight = () => { const layoutDoc = this.layoutDoc; @@ -365,7 +368,7 @@ export class TreeView extends React.Component<TreeViewProps> { return rows; } - rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.props.panelWidth() - treeBulletWidth()); + rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), (this.props.panelWidth() - treeBulletWidth())) / (this.props.treeView.props.scaling?.() || 1); rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT; rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), treeBulletWidth()); expandPanelHeight = () => { @@ -512,10 +515,12 @@ export class TreeView extends React.Component<TreeViewProps> { DocumentViewInternal.SelectAfterContextMenu = true; } contextMenuItems = () => { - const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, label: "New Folder" }; - const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, label: "Open Alias" }; - const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, label: "Focus or Open" }; - return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [makeFolder] : + const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "New Folder" }; + const deleteFolder = { script: ScriptField.MakeFunction(`scriptContext.deleteFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete Folder" }; + const folderOp = this.childDocs?.length ? makeFolder : deleteFolder; + const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: "copy", label: "Open Alias" }; + const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: "eye", label: "Focus or Open" }; + return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [folderOp] : Doc.IsSystem(this.doc) ? [] : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ? [openAlias, makeFolder] : @@ -525,7 +530,8 @@ export class TreeView extends React.Component<TreeViewProps> { childContextMenuItems = () => { const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []); const customFilters = Cast(this.doc.childContextMenuFilters, listSpec(ScriptField), []); - return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], label })); + const icons = StrListCast(this.doc.childContextMenuIcons); + return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label })); } onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick)); @@ -544,7 +550,7 @@ export class TreeView extends React.Component<TreeViewProps> { switch (property.split(":")[0]) { case StyleProp.Opacity: return this.props.treeView.outlineMode ? undefined : 1; case StyleProp.BackgroundColor: return this.selected ? "#7089bb" : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor)); - case StyleProp.DocContents: return testDocProps(props) && !props?.treeViewDoc ? (null) : + case StyleProp.DocContents: return this.props.treeView.outlineMode ? (null) : <div className="treeView-label" style={{ // just render a title for a tree view label (identified by treeViewDoc being set in 'props') maxWidth: props?.PanelWidth() || undefined, background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor), @@ -646,6 +652,7 @@ export class TreeView extends React.Component<TreeViewProps> { searchFilterDocs={returnEmptyDoclist} ContainingCollectionView={undefined} ContainingCollectionDoc={this.props.treeView.props.Document} + ContentScaling={returnOne} />; const buttons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations + (Doc.IsSystem(this.props.containerCollection) ? ":afterHeader" : "")); @@ -702,10 +709,12 @@ export class TreeView extends React.Component<TreeViewProps> { hideDecorationTitle={this.props.treeView.outlineMode} hideResizeHandles={this.props.treeView.outlineMode} focus={this.refocus} + ContentScaling={returnOne} hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} ScreenToLocalTransform={this.docTransform} renderDepth={this.props.renderDepth + 1} + treeViewDoc={this.props.treeView?.props.Document} rootSelected={returnTrue} layerProvider={returnTrue} docViewPath={this.props.treeView.props.docViewPath} @@ -832,7 +841,7 @@ export class TreeView extends React.Component<TreeViewProps> { dontRegisterView: boolean | undefined, observerHeight: (ref: any) => void, unobserveHeight: (ref: any) => void, - contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string }[]) + contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string, icon: string }[]) ) { const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField); if (viewSpecScript) { @@ -849,6 +858,7 @@ export class TreeView extends React.Component<TreeViewProps> { } const dentDoc = (editTitle: boolean, newParent: Doc, addAfter: Doc | undefined, parent: TreeView | CollectionTreeView | undefined) => { + if (parent instanceof TreeView && parent.props.treeView.fileSysMode && !newParent.isFolder) return; const fieldKey = Doc.LayoutFieldKey(newParent); if (remove && fieldKey && Cast(newParent[fieldKey], listSpec(Doc)) !== undefined) { remove(child); @@ -865,7 +875,7 @@ export class TreeView extends React.Component<TreeViewProps> { const childLayout = Doc.Layout(pair.layout); const rowHeight = () => { const aspect = Doc.NativeAspect(childLayout); - return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); + return (aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]()); }; return <TreeView key={child[Id]} ref={r => treeViewRefs.set(child, r ? r : undefined)} document={pair.layout} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index a8f5e6dd2..3b3e069d8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -2,14 +2,16 @@ import { action, computed, IReactionDisposer, observable, reaction } from "mobx" import { observer } from "mobx-react"; import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; +import { List } from "../../../../fields/List"; import { NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { DocumentType } from "../../../documents/DocumentTypes"; +import { LinkManager } from "../../../util/LinkManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; import React = require("react"); + export interface CollectionFreeFormLinkViewProps { A: DocumentView; B: DocumentView; @@ -40,8 +42,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo if (SnappingManager.GetIsDragging() || !A.ContentDiv || !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(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line. - const acont = A.rootDoc.type === DocumentType.LINK ? A.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; - const bcont = B.rootDoc.type === DocumentType.LINK ? B.ContentDiv.getElementsByClassName("linkAnchorBox-cont") : []; + const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); const adiv = acont.length ? acont[0] : A.ContentDiv; const bdiv = bcont.length ? bcont[0] : B.ContentDiv; const a = adiv.getBoundingClientRect(); @@ -66,8 +68,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { const m = targetAhyperlink.getBoundingClientRect(); const mp = A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - linkDoc.anchor1_x = Math.min(1, mp[0] / A.props.PanelWidth()) * 100; - linkDoc.anchor1_y = Math.min(1, mp[1] / A.props.PanelHeight()) * 100; + const mpx = mp[0] / A.props.PanelWidth(); + const mpy = mp[1] / A.props.PanelHeight(); + if (mpx >= 0 && mpx <= 1) linkDoc.anchor1_x = mpx * 100; + if (mpy >= 0 && mpy <= 1) linkDoc.anchor1_y = mpy * 100; } if (!targetBhyperlink) { if (linkDoc.linkAutoMove) { @@ -77,8 +81,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo } else { const m = targetBhyperlink.getBoundingClientRect(); const mp = B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); - linkDoc.anchor2_x = Math.min(1, mp[0] / B.props.PanelWidth()) * 100; - linkDoc.anchor2_y = Math.min(1, mp[1] / B.props.PanelHeight()) * 100; + const mpx = mp[0] / B.props.PanelWidth(); + const mpy = mp[1] / B.props.PanelHeight(); + if (mpx >= 0 && mpx <= 1) linkDoc.anchor2_x = mpx * 100; + if (mpy >= 0 && mpy <= 1) linkDoc.anchor2_y = mpy * 100; } } @@ -137,8 +143,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo if (!A.ContentDiv || !B.ContentDiv || !LinkDocs.length) return undefined; const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - const adiv = (acont.length ? acont[0] : A.ContentDiv); - const bdiv = (bcont.length ? bcont[0] : B.ContentDiv); + const adiv = acont.length ? acont[0] : A.ContentDiv; + const bdiv = bcont.length ? bcont[0] : B.ContentDiv; for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; for (let bpdiv = bdiv; bpdiv; bpdiv = bpdiv.parentElement as any) if ((bpdiv as any).hidden) return; const a = adiv.getBoundingClientRect(); @@ -173,8 +179,14 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo render() { if (!this.renderData) return (null); const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData; + LinkManager.currentLink = this.props.LinkDocs[0]; + const linkRelationship = StrCast(LinkManager.currentLink?.linkRelationship); //get string representing relationship + const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>; + const linkColorList = Doc.UserDoc().linkColorList as List<string>; + //access stroke color using index of the relationship in the color list (default black) + const strokeColor = linkRelationshipList.indexOf(linkRelationship) === -1 ? "black" : linkColorList[linkRelationshipList.indexOf(linkRelationship)]; return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> - <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2" }} + <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, strokeDasharray: "2 2", stroke: strokeColor }} 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]}`} /> {textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > {StrCast(this.props.LinkDocs[0].description)} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 5e0b31754..dacbb3508 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,29 +1,17 @@ import { computed } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; -import { Utils } from "../../../../Utils"; import { DocumentManager } from "../../../util/DocumentManager"; -import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinksView.scss"; import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView"; import React = require("react"); -import { DocumentType } from "../../../documents/DocumentTypes"; @observer export class CollectionFreeFormLinksView extends React.Component { @computed get uniqueConnections() { - const connections = DocumentManager.Instance.LinkedDocumentViews - .filter(c => c.a.props.Document.type === DocumentType.LINK || c.b.props.Document.type === DocumentType.LINK) - .reduce((drawnPairs, connection) => { - const matchingPairs = drawnPairs.filter(pair => connection.a === pair.a && connection.b === pair.b); - matchingPairs.forEach(drawnPair => drawnPair.l.add(connection.l)); - if (!matchingPairs.length) drawnPairs.push({ a: connection.a, b: connection.b, l: new Set<Doc>([connection.l]) }); - return drawnPairs; - }, [] as { a: DocumentView, b: DocumentView, l: Set<Doc> }[]); - const set = new Map<Doc, { a: DocumentView, b: DocumentView, l: Doc[] }>(); - connections.map(c => !set.has(Array.from(c.l)[0]) && set.set(Array.from(c.l)[0], { a: c.a, b: c.b, l: Array.from(c.l) })); - return Array.from(set.values()).map(c => <CollectionFreeFormLinkView key={c.l[0][Id]} A={c.a} B={c.b} LinkDocs={c.l} />); + return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)).map(c => + <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} /> + ); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index d09d9b9d7..be0b078ec 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,6 +1,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; +import { DateField } from "../../../../fields/DateField"; import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; @@ -17,6 +18,7 @@ import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUp import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; +import { DocumentType } from "../../../documents/DocumentTypes"; import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { DocumentManager } from "../../../util/DocumentManager"; import { DragManager, dropActionType } from "../../../util/DragManager"; @@ -48,8 +50,7 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { DocumentType } from "../../../documents/DocumentTypes"; -import { DateField } from "../../../../fields/DateField"; +import Color = require("color"); export const panZoomSchema = createSchema({ _panX: "number", @@ -74,6 +75,8 @@ export type collectionFreeformViewProps = { scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) engineProps?: any; + dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are trnasparent or not. + // However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents. }; @observer @@ -115,7 +118,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); } - @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && (this.props.ContainingCollectionView?.isContentActive() || this.props.isContentActive()); } + @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && this.props.isContentActive(); } @computed get fitToContentVals() { return { bounds: { ...this.contentBounds, cx: (this.contentBounds.x + this.contentBounds.r) / 2, cy: (this.contentBounds.y + this.contentBounds.b) / 2 }, @@ -168,6 +171,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getContainerTransform = () => this.cachedGetContainerTransform.copy(); getTransformOverlay = () => this.getContainerTransform().translate(1, 1); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); + isAnyChildContentActive = () => this.props.isAnyChildContentActive(); addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox); @@ -179,7 +183,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P addDocument = (newBox: Doc | Doc[]) => { let retVal = false; if (newBox instanceof Doc) { - if (retVal = this.props.addDocument?.(newBox) || false) { + if (retVal = (this.props.addDocument?.(newBox) || false)) { this.bringToFront(newBox); this.updateCluster(newBox); } @@ -205,25 +209,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return retVal; } - updateGroupBounds = () => { - if (!this.props.Document._isGroup) return; - const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); - const cbounds = aggregateBounds(clist, 0, 0); - const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; - const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; - const pbounds = { - x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], - r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], b: (cbounds.b - p[1]) * this.zoomScaling() + c[1] - }; - - this.layoutDoc._width = (pbounds.r - pbounds.x); - this.layoutDoc._height = (pbounds.b - pbounds.y); - this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; - this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; - this.layoutDoc.x = pbounds.x; - this.layoutDoc.y = pbounds.y; - } - isCurrent(doc: Doc) { const dispTime = NumCast(doc._timecodeToShow, -1); const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5); @@ -265,8 +250,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !StrListCast(d._layerTags).includes(StyleLayers.Background) && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront } - this.updateGroupBounds(); - (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); return true; } @@ -506,7 +489,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P const points = ge.points; const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); const inkDoc = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), points, - { title: "ink stroke", x: B.x - Number(ActiveInkWidth()) / 2, y: B.y - Number(ActiveInkWidth()) / 2, _width: B.width + Number(ActiveInkWidth()), _height: B.height + Number(ActiveInkWidth()) }); + { title: "ink stroke", x: B.x - ActiveInkWidth() / 2, y: B.y - ActiveInkWidth() / 2, _width: B.width + ActiveInkWidth(), _height: B.height + ActiveInkWidth() }); this.addDocument(inkDoc); e.stopPropagation(); break; @@ -1031,7 +1014,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P docFilters={this.childDocFilters} docRangeFilters={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} - isContentActive={this.isAnnotationOverlay ? this.props.isContentActive : returnFalse} + isContentActive={returnFalse} isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} focus={this.focusDocument} addDocTab={this.addDocTab} @@ -1048,7 +1031,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P freezeDimensions={this.props.childFreezeDimensions} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} bringToFront={this.bringToFront} - dontRegisterView={this.props.dontRegisterView} + showTitle={this.props.childShowTitle} + dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" : (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined} jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0} @@ -1231,6 +1215,30 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P { fireImmediately: true, name: "doLayout" }); this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); + + this._disposers.groupBounds = reaction(() => { + if (this.props.Document._isGroup && this.childDocs.length === this.childDocList?.length) { + const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); + return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); + } + return undefined; + }, + (cbounds) => { + if (cbounds) { + const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; + const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)]; + const pbounds = { + x: (cbounds.x - p[0]) * this.zoomScaling() + c[0], y: (cbounds.y - p[1]) * this.zoomScaling() + c[1], + r: (cbounds.r - p[0]) * this.zoomScaling() + c[0], b: (cbounds.b - p[1]) * this.zoomScaling() + c[1] + }; + this.layoutDoc._width = (pbounds.r - pbounds.x); + this.layoutDoc._height = (pbounds.b - pbounds.y); + this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2; + this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2; + this.layoutDoc.x = pbounds.x; + this.layoutDoc.y = pbounds.y; + } + }, { fireImmediately: true }); } componentWillUnmount() { @@ -1322,8 +1330,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P !options && ContextMenu.Instance.addItem({ description: "Options...", subitems: optionItems, icon: "eye" }); const mores = ContextMenu.Instance.findByDescription("More..."); const moreItems = mores && "subitems" in mores ? mores.subitems : []; - moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) }); - moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); + if (!Doc.UserDoc().noviceMode) { + moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) }); + moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) }); + } !mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" }); } @@ -1395,6 +1405,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } chooseGridSpace = (gridSpace: number): number => { + if (!this.zoomScaling()) return 50; const divisions = this.props.PanelWidth() / this.zoomScaling() / gridSpace + 3; return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10); } @@ -1452,8 +1463,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}> - <div ref={this._marqueeRef}> - {this.layoutDoc["_backgroundGrid-show"] ? this.backgroundGrid : (null)} + <div ref={this._marqueeRef} style={{ display: this.props.dontRenderDocuments ? "none" : undefined }}> + {this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)} <CollectionFreeFormViewPannableContents isAnnotationOverlay={this.isAnnotationOverlay} transform={this.contentTransform} @@ -1479,6 +1490,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return wscale < hscale ? wscale : hscale; } + private groupDropDisposer?: DragManager.DragDropDisposer; + protected createGroupEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this.groupDropDisposer?.(); + if (ele) { + this.groupDropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc, this.onInternalPreDrop.bind(this)); + } + } + render() { TraceMobx(); const clientRect = this._mainCont?.getBoundingClientRect(); @@ -1522,7 +1541,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P </div>} {this.props.Document._isGroup && SnappingManager.GetIsDragging() && (this.ChildDrag || this.props.layerProvider?.(this.props.Document) === false) ? - <div className="collectionFreeForm-groupDropper" ref={this.createDashEventsTarget} style={{ + <div className="collectionFreeForm-groupDropper" ref={this.createGroupEventsTarget} style={{ width: this.ChildDrag ? "10000" : "100%", height: this.ChildDrag ? "10000" : "100%", left: this.ChildDrag ? "-5000" : 0, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index d0243850f..81f6307d1 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -41,7 +41,7 @@ interface MarqueeViewProps { trySelectCluster: (addToSel: boolean) => boolean; nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; - setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; } @observer export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> @@ -211,7 +211,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // allow marquee if right click OR alt+left click if (e.button === 2 || (e.button === 0 && e.altKey)) { // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { - this.setPreviewCursor(e.clientX, e.clientY, true); + this.setPreviewCursor(e.clientX, e.clientY, true, false); // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. e.preventDefault(); // } @@ -284,8 +284,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque else if (document.getSelection()) { document.getSelection()?.empty(); } } - setPreviewCursor = action((x: number, y: number, drag: boolean) => { - if (drag) { + setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean) => { + if (hide) { + this._downX = this._lastX = x; + this._downY = this._lastY = y; + this._commandExecuted = false; + PreviewCursor.Visible = false; + } else if (drag) { this._downX = this._lastX = x; this._downY = this._lastY = y; this._commandExecuted = false; @@ -313,7 +318,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; if (!this.props.trySelectCluster(e.shiftKey)) { - this.setPreviewCursor(e.clientX, e.clientY, false); + this.setPreviewCursor(e.clientX, e.clientY, false, false); } else e.stopPropagation(); } } @@ -443,8 +448,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque syntaxHighlight = (e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); if (e instanceof KeyboardEvent ? e.key === "i" : true) { - const inks = selected.filter(s => s.proto?.type === DocumentType.INK); - const setDocs = selected.filter(s => s.proto?.type === DocumentType.RTF && s.color); + const inks = selected.filter(s => s.type === DocumentType.INK); + const setDocs = selected.filter(s => s.type === DocumentType.RTF && s.color); const sets = setDocs.map((sd) => Cast(sd.data, RichTextField)?.Text as string); const colors = setDocs.map(sd => FieldValue(sd.color) as string); const wordToColor = new Map<string, string>(); diff --git a/src/client/views/collections/collectionFreeForm/index.ts b/src/client/views/collections/collectionFreeForm/index.ts new file mode 100644 index 000000000..702dc8d42 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/index.ts @@ -0,0 +1,7 @@ +export * from "./CollectionFreeFormLayoutEngines"; +export * from "./CollectionFreeFormLinkView"; +export * from "./CollectionFreeFormLinksView"; +export * from "./CollectionFreeFormRemoteCursors"; +export * from "./CollectionFreeFormView"; +export * from "./MarqueeOptionsMenu"; +export * from "./MarqueeView";
\ No newline at end of file diff --git a/src/client/views/collections/collectionGrid/index.ts b/src/client/views/collections/collectionGrid/index.ts new file mode 100644 index 000000000..be5d5667a --- /dev/null +++ b/src/client/views/collections/collectionGrid/index.ts @@ -0,0 +1,2 @@ +export * from "./Grid"; +export * from "./CollectionGridView";
\ No newline at end of file diff --git a/src/client/views/collections/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index 46e40489b..8fe804466 100644 --- a/src/client/views/collections/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -1,15 +1,31 @@ -@import "../global/globalCssVariables"; -@import "../_nodeModuleOverrides"; +@import "../../global/globalCssVariables"; +@import "../../_nodeModuleOverrides"; .collectionLinearView-outer { overflow: visible; height: 100%; pointer-events: none; + &.true { + padding-left: 5px; + padding-right: 5px; + border-left: $standard-border; + background-color: $medium-blue-alt; + } + + >input:not(:checked)~&.true { + background-color: transparent; + } + .collectionLinearView { display: flex; height: 100%; align-items: center; + gap: 5px; + + .collectionView { + overflow: visible !important; + } >span { background: $dark-gray; @@ -41,7 +57,7 @@ } .bottomPopup-descriptions { - cursor:pointer; + cursor: pointer; display: inline; white-space: nowrap; padding-left: 8px; @@ -54,7 +70,7 @@ } .bottomPopup-exit { - cursor:pointer; + cursor: pointer; display: inline; white-space: nowrap; margin-right: 10px; @@ -67,29 +83,26 @@ } >label { - margin-top: "auto"; - margin-bottom: "auto"; - background: $dark-gray; - color: $white; - display: inline-block; - border-radius: 18px; - font-size: 12.5px; - width: 18px; - height: 18px; - margin-top: auto; - margin-bottom: auto; - margin-right: 3px; + pointer-events: all; cursor: pointer; + background-color: $medium-blue; + padding: 5; + border-radius: 2px; + height: 25; + min-width: 25; + margin: 0; + color: $white; + display: flex; + font-weight: 100; + width: fit-content; transition: transform 0.2s; - } - - label p { - padding-left: 5px; - } + align-items: center; + justify-content: center; + transition: 0.2s; - label:hover { - background: $medium-gray; - transform: scale(1.15); + &:hover{ + filter: brightness(0.85); + } } >input { @@ -110,13 +123,11 @@ display: flex; opacity: 1; position: relative; - margin-top: auto; .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable { position: relative; margin: auto; - margin-left: 3px; transform-origin: center 80%; } diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx new file mode 100644 index 000000000..7fe95fef0 --- /dev/null +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -0,0 +1,226 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@material-ui/core'; +import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +import { documentSchema } from '../../../../fields/documentSchemas'; +import { Id } from '../../../../fields/FieldSymbols'; +import { makeInterface } from '../../../../fields/Schema'; +import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnTrue, Utils } from '../../../../Utils'; +import { DragManager } from '../../../util/DragManager'; +import { Transform } from '../../../util/Transform'; +import { Colors, Shadows } from '../../global/globalEnums'; +import { DocumentLinksButton } from '../../nodes/DocumentLinksButton'; +import { DocumentView } from '../../nodes/DocumentView'; +import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup'; +import { StyleProp } from '../../StyleProvider'; +import { CollectionSubView } from '../CollectionSubView'; +import { CollectionViewType } from '../CollectionView'; +import "./CollectionLinearView.scss"; + + +type LinearDocument = makeInterface<[typeof documentSchema,]>; +const LinearDocument = makeInterface(documentSchema); + +@observer +export class CollectionLinearView extends CollectionSubView(LinearDocument) { + @observable public addMenuToggle = React.createRef<HTMLInputElement>(); + @observable private _selectedIndex = -1; + private _dropDisposer?: DragManager.DragDropDisposer; + private _widthDisposer?: IReactionDisposer; + private _selectedDisposer?: IReactionDisposer; + + componentWillUnmount() { + this._dropDisposer?.(); + this._widthDisposer?.(); + this._selectedDisposer?.(); + this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); + } + + componentDidMount() { + this._widthDisposer = reaction(() => 5 + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.length * (this.rootDoc[HeightSym]()) : 10), + width => this.childDocs.length && (this.layoutDoc._width = width), + { fireImmediately: true } + ); + + this._selectedDisposer = reaction( + () => NumCast(this.layoutDoc.selectedIndex), + (i) => runInAction(() => { + this._selectedIndex = i; + let selected: any = undefined; + this.childLayoutPairs.map(async (pair, ind) => { + const isSelected = this._selectedIndex === ind; + if (isSelected) { + selected = pair; + } + else { + ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); + } + }); + if (selected && selected.layout) { + ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); + } + }), + { fireImmediately: true } + ); + } + protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view + this._dropDisposer && this._dropDisposer(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); + } + } + + dimension = () => NumCast(this.rootDoc._height); // 2 * the padding + getTransform = (ele: Opt<HTMLDivElement>) => { + if (!ele) return Transform.Identity(); + const { scale, translateX, translateY } = Utils.GetScreenTransform(ele); + return new Transform(-translateX, -translateY, 1); + } + + @action + exitLongLinks = () => { + if (DocumentLinksButton.StartLink) { + if (DocumentLinksButton.StartLink.Document) { + action((e: React.PointerEvent<HTMLDivElement>) => { + Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); + }); + } + } + DocumentLinksButton.StartLink = undefined; + DocumentLinksButton.StartLinkView = undefined; + } + + @action + changeDescriptionSetting = () => { + if (LinkDescriptionPopup.showDescriptions) { + if (LinkDescriptionPopup.showDescriptions === "ON") { + LinkDescriptionPopup.showDescriptions = "OFF"; + LinkDescriptionPopup.descriptionPopup = false; + } else { + LinkDescriptionPopup.showDescriptions = "ON"; + } + } else { + LinkDescriptionPopup.showDescriptions = "OFF"; + LinkDescriptionPopup.descriptionPopup = false; + } + } + + myContextMenu = (e: React.MouseEvent) => { + console.log("STOPPING"); + e.stopPropagation(); + e.preventDefault(); + } + + + getDisplayDoc = (doc: Doc) => { + const nested = doc._viewType === CollectionViewType.Linear; + const hidden = doc.hidden === true; + + let dref: Opt<HTMLDivElement>; + const docXf = () => this.getTransform(dref); + // const scalable = pair.layout.onClick || pair.layout.onDragStart; + return hidden ? (null) : <div className={`collectionLinearView-docBtn`} key={doc[Id]} ref={r => dref = r || undefined} + style={{ + pointerEvents: "all", + width: nested ? undefined : NumCast(doc._width), + height: nested ? undefined : NumCast(doc._height), + marginLeft: !nested ? 2.5 : 0, + marginRight: !nested ? 2.5 : 0, + // width: NumCast(pair.layout._width), + // height: NumCast(pair.layout._height), + }} > + <DocumentView + Document={doc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={docXf} + PanelWidth={nested ? doc[WidthSym] : this.dimension} + PanelHeight={nested ? doc[HeightSym] : this.dimension} + renderDepth={this.props.renderDepth + 1} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + </div>; + } + + render() { + const guid = Utils.GenerateGuid(); // Generate a unique ID to use as the label + const flexDir: any = StrCast(this.Document.flexDirection); // Specify direction of linear view content + const flexGap: number = NumCast(this.Document.flexGap); // Specify the gap between linear view content + const expandable: boolean = BoolCast(this.props.Document.linearViewExpandable); // Specify whether it is expandable or not + const floating: boolean = BoolCast(this.props.Document.linearViewFloating); // Specify whether it is expandable or not + + const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); + const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); + const icon: string = StrCast(this.props.Document.icon); // Menu opener toggle + const menuOpener = <label htmlFor={`${guid}`} + style={{ + boxShadow: floating ? Shadows.STANDARD_SHADOW : undefined, + color: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : Colors.BLACK, + backgroundColor: backgroundColor === color ? "black" : BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : Colors.LIGHT_GRAY + }} + onPointerDown={e => e.stopPropagation()} > + <div className="collectionLinearView-menuOpener"> + {BoolCast(this.layoutDoc.linearViewIsExpanded) ? icon ? icon : <FontAwesomeIcon icon={"minus"} /> : icon ? icon : <FontAwesomeIcon icon={"plus"} />} + </div> + </label>; + + return <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : "transparent" }}> + <div className="collectionLinearView" ref={this.createDashEventsTarget} + onContextMenu={this.myContextMenu} > + {!expandable ? (null) : <Tooltip title={<><div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? "Close" : "Open"}</div></>} placement="top"> + {menuOpener} + </Tooltip>} + <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle} + onChange={action(() => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} /> + + <div className="collectionLinearView-content" + style={{ + height: this.dimension(), + flexDirection: flexDir, + gap: flexGap + }}> + {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))} + </div> + {DocumentLinksButton.StartLink && StrCast(this.layoutDoc.title) === "docked buttons" ? <span className="bottomPopup-background" style={{ + pointerEvents: "all" + }} + onPointerDown={e => e.stopPropagation()} > + <span className="bottomPopup-text" > + Creating link from: <b>{DocumentLinksButton.AnnotationId ? "Annotation in " : " "} {StrCast(DocumentLinksButton.StartLink.title).length < 51 ? DocumentLinksButton.StartLink.title : StrCast(DocumentLinksButton.StartLink.title).slice(0, 50) + '...'}</b> + </span> + + <Tooltip title={<><div className="dash-tooltip">{"Toggle 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 linking mode</div></>} placement="top"> + <span className="bottomPopup-exit" onClick={this.exitLongLinks}> + Stop + </span> + </Tooltip> + + </span> : null} + </div> + </div>; + } +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionLinear/index.ts b/src/client/views/collections/collectionLinear/index.ts new file mode 100644 index 000000000..ff73e14ae --- /dev/null +++ b/src/client/views/collections/collectionLinear/index.ts @@ -0,0 +1 @@ +export * from "./CollectionLinearView";
\ No newline at end of file diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx index fd99abce5..ed196349e 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx @@ -293,12 +293,12 @@ export class CollectionSchemaCell extends React.Component<CellProps> { } } const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - const changeMade = value.length !== value.length || value.length - 2 !== value.length; + const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle booleans } else if (inputIsBool) { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - const changeMade = value.length !== value.length || value.length - 2 !== value.length; + const changeMade = value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx index aaa50ba67..a25f962df 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx @@ -191,7 +191,7 @@ export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> {this.renderSorting()} {this.renderColors()} <div className="collectionSchema-headerMenu-group"> - <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Delete Column</button> + <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Hide Column</button> </div> </> } @@ -413,7 +413,7 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { bool = fields ? fields[1] === "check" : false; } return <div key={key} className="key-option" style={{ - border: "1px solid lightgray", paddingLeft: 5, textAlign: "left", + paddingLeft: 5, textAlign: "left", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white", }} > @@ -489,8 +489,10 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> { } render() { return ( - <div style={{ display: "flex" }} ref={this.setNode}> - <FontAwesomeIcon onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }} icon={this.props.icon} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} /> + <div style={{ display: "flex", width: '100%', alignContent: 'center', alignItems: 'center' }} ref={this.setNode}> + <div className="schema-icon" onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }}> + <FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} /> + </div> {/* <FontAwesomeIcon icon={fa.faSearchMinus} size="lg" style={{ display: "inline", paddingBottom: "1px", paddingTop: "4px", cursor: "hand" }} onClick={e => { runInAction(() => { this._isOpen === undefined ? this._isOpen = true : this._isOpen = !this._isOpen }) diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx index f48906ba5..0e19ef3d9 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx @@ -134,9 +134,9 @@ export class MovableRow extends React.Component<MovableRowProps> { <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown} > <div className="row-dragger"> - <div className="row-option" style={{ left: 5 }} onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> - <div className="row-option" style={{ cursor: "grab", left: 25 }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> - <div className="row-option" style={{ left: 40 }} onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> + <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div> + <div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div> + <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, "add:right")}><FontAwesomeIcon icon="external-link-alt" size="sm" /></div> </div> {children} </ReactTableDefaults.TrComponent> diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index 40cdcd14b..3074ce66e 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -108,9 +108,7 @@ } .rt-th { padding: 0; - border: solid lightgray; - border-width: 0 1px; - border-bottom: 2px solid lightgray; + border-left: solid 1px $light-gray; } } .rt-th { @@ -213,6 +211,8 @@ } } + + .collectionSchemaView-header { height: 100%; color: gray; @@ -227,6 +227,15 @@ button.add-column { width: 28px; } +.collectionSchemaView-menuOptions-wrapper { + background: rgb(241, 239, 235); + display: flex; + cursor: default; + height: 100%; + align-content: center; + align-items: center; +} + .collectionSchema-header-menuOptions { color: black; width: 180px; @@ -272,6 +281,9 @@ button.add-column { width: 10px; } } + + + .keys-dropdown { position: relative; //width: 100%; @@ -287,26 +299,7 @@ button.add-column { font-weight: normal; } } - .keys-options-wrapper { - width: 100%; - max-height: 150px; - overflow-y: scroll; - position: absolute; - top: 28px; - box-shadow: 0 10px 16px rgba(0, 0, 0, 0.1); - background-color: white; - .key-option { - background-color: white; - border: 1px solid lightgray; - padding: 2px 3px; - &:not(:first-child) { - border-top: 0; - } - &:hover { - background-color: $light-gray; - } - } - } + } .columnMenu-colors { display: flex; @@ -325,11 +318,53 @@ button.add-column { } } +.schema-icon { + cursor: pointer; + width: 25px; + height: 25px; + display: flex; + align-items: center; + justify-content: center; + align-content: center; + background-color: $medium-blue; + color: white; + margin-right: 5px; + font-size: 10px; + border-radius: 3px; + +} + +.keys-options-wrapper { + position: absolute; + text-align: left; + height: fit-content; + top: 100%; + z-index: 21; + background-color: #ffffff; + box-shadow: 0px 3px 4px rgba(0,0,0,30%); + padding: 1px; + .key-option { + cursor: pointer; + color: #000000; + width: 100%; + height: 25px; + font-weight: 400; + display: flex; + justify-content: left; + align-items: center; + padding-left: 5px; + &:hover { + background-color: $light-gray; + } + } +} + .collectionSchema-row { height: 100%; background-color: white; &.row-focused .rt-td { - background-color: #bfffc0; //$light-gray; + background-color: $light-blue; //$light-gray; + overflow: visible; } &.row-wrapped { .rt-td { @@ -338,39 +373,40 @@ button.add-column { } .row-dragger { display: flex; - justify-content: space-around; - //flex: 50 0 auto; - width: 0; - max-width: 50px; - //height: 100%; + justify-content: space-evenly; + width: 58px; + position: absolute; + /* max-width: 50px; */ min-height: 30px; align-items: center; color: lightgray; background-color: white; transition: color 0.1s ease; .row-option { - // padding: 5px; + color: black; cursor: pointer; - position: absolute; + position: relative; transition: color 0.1s ease; display: flex; flex-direction: column; justify-content: center; z-index: 2; + border-radius: 3px; + padding: 3px; &:hover { - color: gray; + background-color: $light-gray; } } } .collectionSchema-row-wrapper { &.row-above { - border-top: 1px solid red; + border-top: 1px solid $medium-blue; } &.row-below { - border-bottom: 1px solid red; + border-bottom: 1px solid $medium-blue; } &.row-inside { - border: 1px solid red; + border: 2px dashed $medium-blue; } .row-dragging { background-color: blue; @@ -383,24 +419,32 @@ button.add-column { height: unset; } +.collectionSchemaView-cellContents { + width: 100%; +} + .collectionSchemaView-cellWrapper { + display: flex; height: 100%; - padding: 4px; text-align: left; padding-left: 19px; position: relative; + align-items: center; + align-content: center; &:focus { outline: none; } &.editing { padding: 0; + box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); + transform: scale(1.1); + z-index: 40; input { outline: 0; border: none; - background-color: rgb(255, 217, 217); + background-color: $white; width: 100%; height: 100%; - padding: 2px 3px; min-height: 26px; } } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index fed64b620..dfe99ffc8 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -338,7 +338,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { {this.renderColors(this._col)} <div className="collectionSchema-headerMenu-group"> <button onClick={() => { this.deleteColumn(this._col.heading); }} - >Delete Column</button> + >Hide Column</button> </div> </div>; } diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx index abe549072..3833f968b 100644 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx @@ -194,10 +194,10 @@ export class SchemaTable extends React.Component<SchemaTableProps> { const sortIcon = col.desc === undefined ? "caret-right" : col.desc === true ? "caret-down" : "caret-up"; const header = <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: "2px", display: "flex", cursor: "default", height: "100%", }}> {keysDropdown} - <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "hand" }}> + <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: "inline", zIndex: 1, background: "inherit", cursor: "pointer" }}> <FontAwesomeIcon icon={sortIcon} size="lg" /> </div> - {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} + {/* {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} */} </div>; return { @@ -562,7 +562,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> { onPointerDown={this.props.onPointerDown} onClick={this.props.onClick} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} > {this.reactTable} - {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} + {this.props.Document._chromeHidden || this.props.addDocument === returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} {!this._showDoc ? (null) : <div className="collectionSchemaView-documentPreview" ref="overlay" style={{ |
