diff options
Diffstat (limited to 'src/client/views/collections')
21 files changed, 738 insertions, 466 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index f8988338f..58f1f2883 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -24,6 +24,7 @@ export enum CollectionViewType { Stacking, Masonry, Pivot, + Linear, } export namespace CollectionViewType { @@ -36,7 +37,8 @@ export namespace CollectionViewType { ["tree", CollectionViewType.Tree], ["stacking", CollectionViewType.Stacking], ["masonry", CollectionViewType.Masonry], - ["pivot", CollectionViewType.Pivot] + ["pivot", CollectionViewType.Pivot], + ["linear", CollectionViewType.Linear] ]); export const valueOf = (value: string) => { @@ -46,7 +48,7 @@ export namespace CollectionViewType { } export interface CollectionRenderProps { - addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + addDocument: (document: Doc) => boolean; removeDocument: (document: Doc) => boolean; moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; active: () => boolean; @@ -101,22 +103,13 @@ export class CollectionBaseView extends Touchable<CollectionViewProps> { @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); } @action.bound - addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { - var curPage = NumCast(this.props.Document.curPage, -1); - Doc.GetProto(doc).page = curPage; + addDocument(doc: Doc): boolean { if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer Doc.GetProto(doc).annotationOn = this.props.Document; } let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document; let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; - const value = Cast(targetDataDoc[targetField], listSpec(Doc)); - if (value !== undefined) { - if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) { - value.push(doc); - } - } else { - Doc.GetProto(targetDataDoc)[targetField] = new List([doc]); - } + Doc.AddDocToList(targetDataDoc, targetField, doc); Doc.GetProto(doc).lastOpened = new DateField; return true; } @@ -187,7 +180,7 @@ export class CollectionBaseView extends Touchable<CollectionViewProps> { <div id="collectionBaseView" style={{ pointerEvents: this.props.Document.isBackground ? "none" : "all", - boxShadow: this.props.Document.isBackground ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` + boxShadow: this.props.Document.isBackground || viewtype === CollectionViewType.Linear ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}` }} className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 14e513157..1f78c8c97 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -31,14 +31,14 @@ import { SubCollectionViewProps } from "./CollectionSubView"; import React = require("react"); import { ButtonSelector } from './ParentDocumentSelector'; import { DocumentType } from '../../documents/DocumentTypes'; -import { compileFunction } from 'vm'; import { ComputedField } from '../../../new_fields/ScriptField'; library.add(faFile); const _global = (window /* browser */ || global /* node */) as any; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { - @observable public static Instance: CollectionDockingView; + @observable public static Instances: CollectionDockingView[] = []; + @computed public static get Instance() { return CollectionDockingView.Instances[0]; } public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) { return { type: 'react-component', @@ -66,7 +66,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp constructor(props: SubCollectionViewProps) { super(props); - !CollectionDockingView.Instance && runInAction(() => CollectionDockingView.Instance = this); + runInAction(() => !CollectionDockingView.Instances ? CollectionDockingView.Instances = [this] : CollectionDockingView.Instances.push(this)); //Why is this here? (window as any).React = React; (window as any).ReactDOM = ReactDOM; @@ -318,13 +318,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } catch (e) { } - if (this._goldenLayout) this._goldenLayout.destroy(); - runInAction(() => this._goldenLayout = null); + this._goldenLayout && this._goldenLayout.destroy(); + runInAction(() => { + CollectionDockingView.Instances.splice(CollectionDockingView.Instances.indexOf(this), 1); + this._goldenLayout = null; + }); window.removeEventListener('resize', this.onResize); - if (this.reactionDisposer) { - this.reactionDisposer(); - } + this.reactionDisposer && this.reactionDisposer(); } @action onResize = (event: any) => { diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx new file mode 100644 index 000000000..1709b9c99 --- /dev/null +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -0,0 +1,396 @@ +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faPalette } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Doc, WidthSym } from "../../../new_fields/Doc"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { ScriptField } from "../../../new_fields/ScriptField"; +import { NumCast, StrCast } from "../../../new_fields/Types"; +import { Utils, numberRange } from "../../../Utils"; +import { Docs } from "../../documents/Documents"; +import { DragManager } from "../../util/DragManager"; +import { CompileScript } from "../../util/Scripting"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { anchorPoints, Flyout } from "../DocumentDecorations"; +import { EditableView } from "../EditableView"; +import { CollectionStackingView } from "./CollectionStackingView"; +import "./CollectionStackingView.scss"; +import Measure from "react-measure"; + +library.add(faPalette); + +interface CMVFieldRowProps { + rows: () => number; + headings: () => object[]; + heading: string; + headingObject: SchemaHeaderField | undefined; + docList: Doc[]; + parent: CollectionStackingView; + type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined; + createDropTarget: (ele: HTMLDivElement) => void; + screenToLocalTransform: () => Transform; + setDocHeight: (key: string, thisHeight: number) => void; +} + +@observer +export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowProps> { + @observable private _background = "inherit"; + @observable private _createAliasSelected: boolean = false; + + private _dropRef: HTMLDivElement | null = null; + private dropDisposer?: DragManager.DragDropDisposer; + private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 }; + private _contRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _sensitivity: number = 16; + + @observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading; + @observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + + createRowDropRef = (ele: HTMLDivElement | null) => { + this._dropRef = ele; + this.dropDisposer && this.dropDisposer(); + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } }); + } + } + + getTrueHeight = () => { + if (this.collapsed) { + this.props.setDocHeight(this._heading, 20); + } else { + let rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header + let transformScale = this.props.screenToLocalTransform().Scale; + let trueHeight = rawHeight * transformScale; + this.props.setDocHeight(this._heading, trueHeight); + } + } + + @undoBatch + rowDrop = action((e: Event, de: DragManager.DropEvent) => { + this._createAliasSelected = false; + if (de.data instanceof DragManager.DocumentDragData) { + let key = StrCast(this.props.parent.props.Document.sectionFilter); + let castedValue = this.getValue(this._heading); + if (castedValue) { + de.data.droppedDocuments.forEach(d => d[key] = castedValue); + } + else { + de.data.droppedDocuments.forEach(d => d[key] = undefined); + } + this.props.parent.drop(e, de); + e.stopPropagation(); + } + }); + + getValue = (value: string): any => { + let parsed = parseInt(value); + if (!isNaN(parsed)) { + return parsed; + } + if (value.toLowerCase().indexOf("true") > -1) { + return true; + } + if (value.toLowerCase().indexOf("false") > -1) { + return false; + } + return value; + } + + @action + headingChanged = (value: string, shiftDown?: boolean) => { + this._createAliasSelected = false; + let key = StrCast(this.props.parent.props.Document.sectionFilter); + let castedValue = this.getValue(value); + if (castedValue) { + if (this.props.parent.sectionHeaders) { + if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) { + return false; + } + } + this.props.docList.forEach(d => d[key] = castedValue); + if (this.props.headingObject) { + this.props.headingObject.setHeading(castedValue.toString()); + this._heading = this.props.headingObject.heading; + } + return true; + } + return false; + } + + @action + changeColumnColor = (color: string) => { + this._createAliasSelected = false; + if (this.props.headingObject) { + this.props.headingObject.setColor(color); + this._color = color; + } + } + + @action + pointerEnteredRow = () => { + if (SelectionManager.GetIsDragging()) { + this._background = "#b4b4b4"; + } + } + + @action + pointerLeaveRow = () => { + this._createAliasSelected = false; + this._background = "inherit"; + document.removeEventListener("pointermove", this.startDrag); + } + + @action + addDocument = (value: string, shiftDown?: boolean) => { + this._createAliasSelected = false; + let key = StrCast(this.props.parent.props.Document.sectionFilter); + let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value }); + newDoc[key] = this.getValue(this.props.heading); + return this.props.parent.props.addDocument(newDoc); + } + + @action + deleteRow = () => { + this._createAliasSelected = false; + let key = StrCast(this.props.parent.props.Document.sectionFilter); + this.props.docList.forEach(d => d[key] = undefined); + if (this.props.parent.sectionHeaders && this.props.headingObject) { + let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject); + this.props.parent.sectionHeaders.splice(index, 1); + } + } + + @action + collapseSection = () => { + this._createAliasSelected = false; + if (this.props.headingObject) { + this._headingsHack++; + this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed); + this.toggleVisibility(); + } + } + + startDrag = (e: PointerEvent) => { + let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); + if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { + let alias = Doc.MakeAlias(this.props.parent.props.Document); + let key = StrCast(this.props.parent.props.Document.sectionFilter); + let value = this.getValue(this._heading); + value = typeof value === "string" ? `"${value}"` : value; + let script = `return doc.${key} === ${value}`; + let compiled = CompileScript(script, { params: { doc: Doc.name } }); + if (compiled.compiled) { + let scriptField = new ScriptField(compiled); + alias.viewSpecScript = scriptField; + let dragData = new DragManager.DocumentDragData([alias]); + DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY); + } + + e.stopPropagation(); + document.removeEventListener("pointermove", this.startDrag); + document.removeEventListener("pointerup", this.pointerUp); + } + } + + pointerUp = (e: PointerEvent) => { + e.stopPropagation(); + e.preventDefault(); + + document.removeEventListener("pointermove", this.startDrag); + document.removeEventListener("pointerup", this.pointerUp); + } + + headerDown = (e: React.PointerEvent<HTMLDivElement>) => { + e.stopPropagation(); + e.preventDefault(); + + let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); + this._startDragPosition = { x: dx, y: dy }; + + if (this._createAliasSelected) { + document.removeEventListener("pointermove", this.startDrag); + document.addEventListener("pointermove", this.startDrag); + document.removeEventListener("pointerup", this.pointerUp); + document.addEventListener("pointerup", this.pointerUp); + } + this._createAliasSelected = false; + } + + renderColorPicker = () => { + let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + + let pink = PastelSchemaPalette.get("pink2"); + let purple = PastelSchemaPalette.get("purple4"); + let blue = PastelSchemaPalette.get("bluegreen1"); + let yellow = PastelSchemaPalette.get("yellow4"); + let red = PastelSchemaPalette.get("red2"); + let green = PastelSchemaPalette.get("bluegreen7"); + let cyan = PastelSchemaPalette.get("bluegreen5"); + let orange = PastelSchemaPalette.get("orange1"); + let gray = "#f1efeb"; + + return ( + <div className="collectionStackingView-colorPicker"> + <div className="colorOptions"> + <div className={"colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div> + <div className={"colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div> + <div className={"colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div> + <div className={"colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div> + <div className={"colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div> + <div className={"colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div> + <div className={"colorPicker" + (selected === green ? " active" : "")} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div> + <div className={"colorPicker" + (selected === cyan ? " active" : "")} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div> + <div className={"colorPicker" + (selected === orange ? " active" : "")} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div> + </div> + </div> + ); + } + + @action + toggleAlias = () => { + this._createAliasSelected = true; + } + + renderMenu = () => { + let selected = this._createAliasSelected; + return ( + <div className="collectionStackingView-optionPicker"> + <div className="optionOptions"> + <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div> + </div> + </div> + ); + } + + @observable private collapsed: boolean = false; + + private toggleVisibility = action(() => { + this.collapsed = !this.collapsed; + }); + + @observable _headingsHack: number = 1; + + handleResize = (size: any) => { + this.counter += 1; + if (this.counter !== 1) { + this.getTrueHeight(); + } + } + + private counter: number = 0; + + render() { + let cols = this.props.rows(); + let rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); + let key = StrCast(this.props.parent.props.Document.sectionFilter); + let templatecols = ""; + let headings = this.props.headings(); + let heading = this._heading; + let style = this.props.parent; + let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); + let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; + let headerEditableViewProps = { + GetValue: () => evContents, + SetValue: this.headingChanged, + contents: evContents, + oneLine: true, + HeadingObject: this.props.headingObject, + HeadingsHack: this._headingsHack, + toggle: this.toggleVisibility, + color: this._color + }; + let newEditableViewProps = { + GetValue: () => "", + SetValue: this.addDocument, + contents: "+ NEW", + HeadingObject: this.props.headingObject, + HeadingsHack: this._headingsHack, + toggle: this.toggleVisibility, + color: this._color + }; + let headingView = this.props.headingObject ? + <div className="collectionStackingView-sectionHeader" ref={this._headerRef} > + <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> + <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} + title={evContents === `NO ${key.toUpperCase()} VALUE` ? + `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} + style={{ + width: "100%", + background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", + color: "grey" + }}> + {<EditableView {...headerEditableViewProps} />} + {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + <div className="collectionStackingView-sectionColor"> + <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> + <button className="collectionStackingView-sectionColorButton"> + <FontAwesomeIcon icon="palette" size="lg" /> + </button> + </ Flyout > + </div> + } + {evContents === `NO ${key.toUpperCase()} VALUE` ? + (null) : + <button className="collectionStackingView-sectionDelete" onClick={this.deleteRow}> + <FontAwesomeIcon icon="trash" size="lg" /> + </button>} + {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + <div className="collectionStackingView-sectionOptions"> + <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> + <button className="collectionStackingView-sectionOptionButton"> + <FontAwesomeIcon icon="ellipsis-v" size="lg"></FontAwesomeIcon> + </button> + </Flyout> + </div> + } + </div> + </div > : (null); + const background = this._background; //to account for observables in Measure + const collapsed = this.collapsed; + let chromeStatus = this.props.parent.props.Document.chromeStatus; + return ( + <Measure offset onResize={this.handleResize}> + {({ measureRef }) => { + return <div ref={measureRef}> + <div className="collectionStackingView-masonrySection" + key={heading = "empty"} + style={{ width: this.props.parent.NodeWidth, background }} + ref={this.createRowDropRef} + onPointerEnter={this.pointerEnteredRow} + onPointerLeave={this.pointerLeaveRow} + > + {headingView} + {collapsed ? (null) : + < div > + <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`} + ref={this._contRef} + style={{ + padding: `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px`, + width: this.props.parent.NodeWidth, + gridGap: this.props.parent.gridGap, + gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""), + }}> + {this.props.parent.children(this.props.docList)} + {this.props.parent.columnDragger} + </div> + {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? + <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" + style={{ width: style.columnWidth / style.numGroupColumns }}> + <EditableView {...newEditableViewProps} /> + </div> : null + } + </div> + } + </div > + </div>; + }} + </Measure> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss deleted file mode 100644 index 62ec8a5be..000000000 --- a/src/client/views/collections/CollectionPDFView.scss +++ /dev/null @@ -1,11 +0,0 @@ - - -.collectionPdfView-cont { - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - z-index: -1; - overflow: hidden !important; -} diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx deleted file mode 100644 index cc8142ec0..000000000 --- a/src/client/views/collections/CollectionPDFView.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { trace } from "mobx"; -import { observer } from "mobx-react"; -import { Id } from "../../../new_fields/FieldSymbols"; -import { emptyFunction } from "../../../Utils"; -import { ContextMenu } from "../ContextMenu"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView"; -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import "./CollectionPDFView.scss"; -import React = require("react"); - - -@observer -export class CollectionPDFView extends React.Component<FieldViewProps> { - public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") { - return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt); - } - - onContextMenu = (e: React.MouseEvent): void => { - if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" }); - } - } - - subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { - return (<CollectionFreeFormView {...this.props} {...renderProps} CollectionView={this} chromeCollapsed={true} />); - } - - render() { - trace(); - return ( - <CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}> - {this.subView} - </CollectionBaseView> - ); - } -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx index 179e44266..79c032723 100644 --- a/src/client/views/collections/CollectionSchemaCells.tsx +++ b/src/client/views/collections/CollectionSchemaCells.tsx @@ -13,17 +13,13 @@ import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../globalCssVariables.s import '../DocumentDecorations.scss'; import { EditableView } from "../EditableView"; import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { CollectionPDFView } from "./CollectionPDFView"; import "./CollectionSchemaView.scss"; -import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../new_fields/Types"; import { Docs } from "../../documents/Documents"; -import { DocumentContentsView } from "../nodes/DocumentContentsView"; import { SelectionManager } from "../../util/SelectionManager"; import { library } from '@fortawesome/fontawesome-svg-core'; import { faExpand } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import { KeyCodes } from "../../northstar/utils/KeyCodes"; import { undoBatch } from "../../util/UndoManager"; @@ -34,8 +30,8 @@ export interface CellProps { row: number; col: number; rowProps: CellInfo; - CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; - ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + CollectionView: Opt<CollectionView>; + ContainingCollection: Opt<CollectionView>; Document: Doc; fieldKey: string; renderDepth: number; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 52a41fc84..3218f630a 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -21,10 +21,8 @@ import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; import '../DocumentDecorations.scss'; import { DocumentView } from "../nodes/DocumentView"; -import { CollectionPDFView } from "./CollectionPDFView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; -import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import { undoBatch } from "../../util/UndoManager"; import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders"; @@ -51,7 +49,7 @@ const columnTypes: Map<string, ColumnType> = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number], ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], - ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["zIndex", ColumnType.Number] + ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @observer @@ -247,8 +245,8 @@ export interface SchemaTableProps { PanelHeight: () => number; PanelWidth: () => number; childDocs?: Doc[]; - CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; - ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + CollectionView: Opt<CollectionView>; + ContainingCollectionView: Opt<CollectionView>; ContainingCollectionDoc: Opt<Doc>; fieldKey: string; renderDepth: number; @@ -905,11 +903,11 @@ interface CollectionSchemaPreviewProps { ruleProvider: Doc | undefined; focus?: (doc: Doc) => void; showOverlays?: (doc: Doc) => { title?: string, caption?: string }; - CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView; + CollectionView?: CollectionView; CollectionDoc?: Doc; onClick?: ScriptField; getTransform: () => Transform; - addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + addDocument: (document: Doc) => boolean; moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean; removeDocument: (document: Doc) => boolean; active: () => boolean; diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 74da4ef85..b31f0b8e3 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,19 +1,27 @@ @import "../globalCssVariables"; .collectionMasonryView { - display:inline; + display: inline; } -.collectionStackingView{ + +.collectionStackingView { display: flex; } -.collectionStackingView, .collectionMasonryView{ +.collectionStackingMasonry-cont { + position:relative; + height:100%; + width:100%; +} +.collectionStackingView, +.collectionMasonryView { height: 100%; width: 100%; - position: relative; + position: absolute; top: 0; overflow-y: auto; flex-wrap: wrap; transition: top .5s; + .collectionSchemaView-previewDoc { height: 100%; position: absolute; @@ -40,10 +48,12 @@ top: 0; left: 0; } + .collectionStackingView-masonrySingle { height: 100%; position: absolute; } + .collectionStackingView-masonryGrid { margin: auto; height: max-content; @@ -91,17 +101,31 @@ height: 100%; margin: auto; } - + .collectionStackingView-masonrySection { margin: auto; } + .collectionStackingView-collapseBar { + margin-left: 2px; + margin-right: 2px; + margin-top: 2px; + background: $main-accent; + height: 5px; + + &.active { + margin-left: 0; + margin-right: 0; + background: red; + } + } + .collectionStackingView-sectionHeader { text-align: center; margin-left: 2px; margin-right: 2px; margin-top: 10px; - background: gray; + background: $main-accent; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong .editableView-input { @@ -132,7 +156,7 @@ .editableView-input:hover, .editableView-container-editing:hover, .editableView-container-editing-oneLine:hover { - cursor: text + cursor: text; } .editableView-input { @@ -182,11 +206,46 @@ } } - .collectionStackingView-sectionDelete { + .collectionStackingView-sectionOptions { position: absolute; right: 0; top: 0; height: 100%; + + [class*="css"] { + max-width: 102px; + } + + .collectionStackingView-sectionOptionButton { + height: 35px; + } + + .collectionStackingView-optionPicker { + width: 78px; + + .optionOptions { + display: inline; + } + + .optionPicker { + cursor: pointer; + width: 20px; + height: 20px; + border-radius: 10px; + margin: 3px; + + &.active { + color: red; + } + } + } + } + + .collectionStackingView-sectionDelete { + position: absolute; + right: 25px; + top: 0; + height: 100%; } } @@ -288,6 +347,4 @@ .rc-switch-checked .rc-switch-inner { left: 8px; } - - }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 4b81db3a6..57722b9b7 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -23,6 +23,7 @@ import { CollectionSubView } from "./CollectionSubView"; import { ContextMenu } from "../ContextMenu"; import { ContextMenuProps } from "../ContextMenuItem"; import { ScriptBox } from "../ScriptBox"; +import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow"; @observer export class CollectionStackingView extends CollectionSubView(doc => doc) { @@ -32,7 +33,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { _sectionFilterDisposer?: IReactionDisposer; _docXfs: any[] = []; _columnStart: number = 0; - @observable private cursor: CursorProperty = "grab"; + @observable _heightMap = new Map<string, number>(); + @observable _cursor: CursorProperty = "grab"; @observable _scroll = 0; // used to force the document decoration to update when scrolling @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); } @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); } @@ -47,9 +49,31 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250)); } + @computed get NodeWidth() { return this.props.PanelWidth(); } childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); } + children(docs: Doc[]) { + this._docXfs.length = 0; + return docs.map((d, i) => { + let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d); + let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns); + let height = () => this.getDocHeight(pair.layout); + let dref = React.createRef<HTMLDivElement>(); + let dxf = () => this.getDocTransform(pair.layout!, dref.current!); + this._docXfs.push({ dxf: dxf, width: width, height: height }); + let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); + let style = this.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > + {this.getDisplayDoc(pair.layout as Doc, pair.data, dxf, width)} + </div>; + }); + } + @action + setDocHeight = (key: string, sectionHeight: number) => { + this._heightMap.set(key, sectionHeight); + } + get layoutDoc() { // if this document's layout field contains a document (ie, a rendering template), then we will use that // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string. @@ -88,11 +112,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { componentDidMount() { // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported). this._heightDisposer = reaction(() => { - if (this.isStackingView && BoolCast(this.props.Document.autoHeight)) { + if (BoolCast(this.props.Document.autoHeight)) { let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]); - return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght, - (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin) - ), 0); + if (this.isStackingView) { + return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght, + (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)), 0); + } else { + let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0); + sum += 30; + return this.props.ContentScaling() * (sum + (this.Sections.size ? 50 : 0)); + } } return -1; }, @@ -120,7 +149,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { } createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; - this.createDropTarget(ele!); + this.createDropTarget(ele!); //so the whole grid is the drop target? } overlays = (doc: Doc) => { @@ -162,19 +191,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (!d) return 0; let nw = NumCast(d.nativeWidth); let nh = NumCast(d.nativeHeight); + let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); if (!d.ignoreAspect && !d.fitWidth && nw && nh) { let aspect = nw && nh ? nh / nw : 1; - let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid); return wid * aspect; } - return d.fitWidth ? this.props.PanelHeight() - 2 * this.yMargin : d[HeightSym](); + return d.fitWidth ? !d.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : Math.min(wid * NumCast(d.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : d[HeightSym](); } columnDividerDown = (e: React.PointerEvent) => { e.stopPropagation(); e.preventDefault(); - runInAction(() => this.cursor = "grabbing"); + runInAction(() => this._cursor = "grabbing"); document.addEventListener("pointermove", this.onDividerMove); document.addEventListener('pointerup', this.onDividerUp); this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; @@ -189,13 +218,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { @action onDividerUp = (e: PointerEvent): void => { - runInAction(() => this.cursor = "grab"); + runInAction(() => this._cursor = "grab"); document.removeEventListener("pointermove", this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); } @computed get columnDragger() { - return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this.cursor, left: `${this.columnWidth + this.xMargin}px` }} > + return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this._cursor, left: `${this.columnWidth + this.xMargin}px` }} > <FontAwesomeIcon icon={"arrows-alt-h"} /> </div>; } @@ -286,52 +315,29 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { translate(offset[0], offset[1] + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)). scale(NumCast(doc.width, 1) / this.columnWidth); } - masonryChildren(docs: Doc[]) { - this._docXfs.length = 0; - return docs.map((d, i) => { - const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d); - if (!pair.layout || pair.data instanceof Promise) { - return (null); - } - let dref = React.createRef<HTMLDivElement>(); - let width = () => (d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth);/// (uniqueHeadings.length + 1); - let height = () => this.getDocHeight(pair.layout); - let dxf = () => this.getDocTransform(pair.layout!, dref.current!); - let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); - this._docXfs.push({ dxf: dxf, width: width, height: height }); - return !pair.layout ? (null) : <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} > - {this.getDisplayDoc(pair.layout, pair.data, dxf, width)} - </div>; - }); - } - @observable _headingsHack: number = 1; - sectionMasonry(heading: SchemaHeaderField | undefined, docList: Doc[]) { - let cols = Math.max(1, Math.min(docList.length, - Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); - if (isNaN(cols)) { - console.log("naN"); - cols = 1; + sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { + let key = this.sectionFilter; + let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined; + let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]); + if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { + type = types[0]; } - return <div key={heading ? heading.heading : "empty"} className="collectionStackingView-masonrySection"> - {!heading ? (null) : - <div key={`${heading.heading}`} className="collectionStackingView-sectionHeader" style={{ background: heading.color }} - onClick={action(() => this._headingsHack++ && heading.setCollapsed(!heading.collapsed))} > - {heading.heading} - </div>} - {this._headingsHack && heading && heading.collapsed ? (null) : - <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`} - style={{ - padding: `${this.yMargin}px ${this.xMargin}px`, - width: `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`, - gridGap: this.gridGap, - gridTemplateColumns: numberRange(cols).reduce((list, i) => list + ` ${this.columnWidth}px`, ""), - }}> - {this.masonryChildren(docList)} - {this.columnDragger} - </div> - } - </div>; + let rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, + Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap)))); + return <CollectionMasonryViewFieldRow + key={heading ? heading.heading : ""} + rows={rows} + headings={this.headings} + heading={heading ? heading.heading : ""} + headingObject={heading} + docList={docList} + parent={this} + type={type} + createDropTarget={this.createDropTarget} + screenToLocalTransform={this.props.ScreenToLocalTransform} + setDocHeight={this.setDocHeight} + />; } @action @@ -383,26 +389,27 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { sections = entries.sort(this.sortFunc); } return ( - <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} - ref={this.createRef} - onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} - onDrop={this.onDrop.bind(this)} - onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => e.stopPropagation()} > - {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))} - {!this.showAddAGroup ? (null) : - <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" - style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> - <EditableView {...editableViewProps} /> - </div>} - {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch - onChange={this.onToggle} - onClick={this.onToggle} - defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'} - checkedChildren="edit" - unCheckedChildren="view" - /> : null} - </div> + <div className="collectionStackingMasonry-cont" > + <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"} + ref={this.createRef} + onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)} + onDrop={this.onDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={(e: React.WheelEvent) => e.stopPropagation()} > + {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))} + {!this.showAddAGroup ? (null) : + <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" + style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}> + <EditableView {...editableViewProps} /> + </div>} + {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch + onChange={this.onToggle} + onClick={this.onToggle} + defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'} + checkedChildren="edit" + unCheckedChildren="view" + /> : null} + </div> </div> ); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 240adf428..7e54b0f29 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -23,7 +23,6 @@ import "./CollectionStackingView.scss"; library.add(faPalette); - interface CSVFieldColumnProps { cols: () => number; headings: () => object[]; @@ -39,6 +38,7 @@ interface CSVFieldColumnProps { @observer export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> { @observable private _background = "inherit"; + @observable private _createAliasSelected: boolean = false; private _dropRef: HTMLDivElement | null = null; private dropDisposer?: DragManager.DragDropDisposer; @@ -58,8 +58,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } @undoBatch - @action - columnDrop = (e: Event, de: DragManager.DropEvent) => { + columnDrop = action((e: Event, de: DragManager.DropEvent) => { + this._createAliasSelected = false; if (de.data instanceof DragManager.DocumentDragData) { let key = StrCast(this.props.parent.props.Document.sectionFilter); let castedValue = this.getValue(this._heading); @@ -72,29 +72,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC this.props.parent.drop(e, de); e.stopPropagation(); } - } - - children(docs: Doc[]) { - let parent = this.props.parent; - parent._docXfs.length = 0; - return docs.map((d, i) => { - const pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d); - if (!pair.layout || pair.data instanceof Promise) { - return (null); - } - let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !parent.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, parent.columnWidth / parent.numGroupColumns); - let height = () => parent.getDocHeight(pair.layout); - let dref = React.createRef<HTMLDivElement>(); - let dxf = () => parent.getDocTransform(pair.layout!, dref.current!); - parent._docXfs.push({ dxf: dxf, width: width, height: height }); - let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap); - let style = parent.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` }; - return <div className={`collectionStackingView-${parent.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} > - {parent.getDisplayDoc(pair.layout, pair.data, dxf, width)} - </div>; - }); - } - + }); getValue = (value: string): any => { let parsed = parseInt(value); if (!isNaN(parsed)) { @@ -111,6 +89,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action headingChanged = (value: string, shiftDown?: boolean) => { + this._createAliasSelected = false; let key = StrCast(this.props.parent.props.Document.sectionFilter); let castedValue = this.getValue(value); if (castedValue) { @@ -131,6 +110,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action changeColumnColor = (color: string) => { + this._createAliasSelected = false; if (this.props.headingObject) { this.props.headingObject.setColor(color); this._color = color; @@ -140,18 +120,21 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerEntered = () => { if (SelectionManager.GetIsDragging()) { + this._createAliasSelected = false; this._background = "#b4b4b4"; } } @action pointerLeave = () => { + this._createAliasSelected = false; this._background = "inherit"; document.removeEventListener("pointermove", this.startDrag); } @action addDocument = (value: string, shiftDown?: boolean) => { + this._createAliasSelected = false; let key = StrCast(this.props.parent.props.Document.sectionFilter); let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); @@ -163,6 +146,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action deleteColumn = () => { + this._createAliasSelected = false; let key = StrCast(this.props.parent.props.Document.sectionFilter); this.props.docList.forEach(d => d[key] = undefined); if (this.props.parent.sectionHeaders && this.props.headingObject) { @@ -171,6 +155,16 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC } } + @action + collapseSection = () => { + this._createAliasSelected = false; + if (this.props.headingObject) { + this._headingsHack++; + this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed); + this.toggleVisibility(); + } + } + startDrag = (e: PointerEvent) => { let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y); if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) { @@ -204,10 +198,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY); this._startDragPosition = { x: dx, y: dy }; - document.removeEventListener("pointermove", this.startDrag); - document.addEventListener("pointermove", this.startDrag); - document.removeEventListener("pointerup", this.pointerUp); - document.addEventListener("pointerup", this.pointerUp); + if (this._createAliasSelected) { + document.removeEventListener("pointermove", this.startDrag); + document.addEventListener("pointermove", this.startDrag); + document.removeEventListener("pointerup", this.pointerUp); + document.addEventListener("pointerup", this.pointerUp); + } + this._createAliasSelected = false; } renderColorPicker = () => { @@ -240,6 +237,30 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC ); } + @action + toggleAlias = () => { + this._createAliasSelected = true; + } + + renderMenu = () => { + let selected = this._createAliasSelected; + return ( + <div className="collectionStackingView-optionPicker"> + <div className="optionOptions"> + <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div> + </div> + </div > + ); + } + + @observable private collapsed: boolean = false; + + private toggleVisibility = action(() => { + this.collapsed = !this.collapsed; + }); + + @observable _headingsHack: number = 1; + render() { let cols = this.props.cols(); let key = StrCast(this.props.parent.props.Document.sectionFilter); @@ -254,12 +275,20 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC GetValue: () => evContents, SetValue: this.headingChanged, contents: evContents, - oneLine: true + oneLine: true, + HeadingObject: this.props.headingObject, + HeadingsHack: this._headingsHack, + toggle: this.toggleVisibility, + color: this._color }; let newEditableViewProps = { GetValue: () => "", SetValue: this.addDocument, - contents: "+ NEW" + contents: "+ NEW", + HeadingObject: this.props.headingObject, + HeadingsHack: this._headingsHack, + toggle: this.toggleVisibility, + color: this._color }; let headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} @@ -268,6 +297,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC ((uniqueHeadings.length + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1) }}> + <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div> {/* the default bucket (no key value) has a tooltip that describes what it is. Further, it does not have a color and cannot be deleted. */} <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown} @@ -281,9 +311,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <EditableView {...headerEditableViewProps} /> {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> - <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderColorPicker()}> + <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}> <button className="collectionStackingView-sectionColorButton"> - <FontAwesomeIcon icon="palette" size="sm" /> + <FontAwesomeIcon icon="palette" size="lg" /> </button> </ Flyout > </div> @@ -291,36 +321,50 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> - <FontAwesomeIcon icon="trash" /> + <FontAwesomeIcon icon="trash" size="lg" /> </button>} + {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : + <div className="collectionStackingView-sectionOptions"> + <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> + <button className="collectionStackingView-sectionOptionButton"> + <FontAwesomeIcon icon="ellipsis-v" size="lg"></FontAwesomeIcon> + </button> + </Flyout> + </div> + } </div> </div> : (null); for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `; + let chromeStatus = this.props.parent.props.Document.chromeStatus; return ( - <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} + <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }} ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}> - {headingView} - <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} - style={{ - padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`, - margin: "auto", - width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, - height: 'max-content', - position: "relative", - gridGap: style.gridGap, - gridTemplateColumns: singleColumn ? undefined : templatecols, - gridAutoRows: singleColumn ? undefined : "0px" - }} - > - {this.children(this.props.docList)} - {singleColumn ? (null) : this.props.parent.columnDragger} - </div> - {(this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? - <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" - style={{ width: style.columnWidth / style.numGroupColumns }}> - <EditableView {...newEditableViewProps} /> - </div> : null} - </div> + {this.props.parent.Document.hideHeadings ? (null) : headingView} + { + this.collapsed ? (null) : + <div> + <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`} + style={{ + padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`, + margin: "auto", + width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, + height: 'max-content', + position: "relative", + gridGap: style.gridGap, + gridTemplateColumns: singleColumn ? undefined : templatecols, + gridAutoRows: singleColumn ? undefined : "0px" + }}> + {this.props.parent.children(this.props.docList)} + {singleColumn ? (null) : this.props.parent.columnDragger} + </div> + {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? + <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" + style={{ width: style.columnWidth / style.numGroupColumns }}> + <EditableView {...newEditableViewProps} /> + </div> : null} + </div> + } + </div > ); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 28d1eb384..6e8e4fa12 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -18,15 +18,14 @@ import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DocComponent } from "../DocComponent"; import { FieldViewProps } from "../nodes/FieldView"; import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox"; -import { CollectionPDFView } from "./CollectionPDFView"; -import { CollectionVideoView } from "./CollectionVideoView"; import { CollectionView } from "./CollectionView"; import React = require("react"); var path = require('path'); import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; +import { ImageUtils } from "../../util/Import & Export/ImageUtils"; export interface CollectionViewProps extends FieldViewProps { - addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + addDocument: (document: Doc) => boolean; removeDocument: (document: Doc) => boolean; moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; PanelWidth: () => number; @@ -34,25 +33,26 @@ export interface CollectionViewProps extends FieldViewProps { VisibleHeight?: () => number; chromeCollapsed: boolean; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; + showHiddenControls?: boolean; // hack for showing the undo/redo/ink controls in a linear view -- needs to be redone } export interface SubCollectionViewProps extends CollectionViewProps { - CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>; + CollectionView: Opt<CollectionView>; ruleProvider: Doc | undefined; + children?: never | (() => JSX.Element[]) | React.ReactNode; } export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) { private dropDisposer?: DragManager.DragDropDisposer; private _childLayoutDisposer?: IReactionDisposer; - - protected createDropTarget = (ele: HTMLDivElement) => { + protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view this.dropDisposer && this.dropDisposer(); if (ele) { this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); } } - protected CreateDropTarget(ele: HTMLDivElement) { + protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view this.createDropTarget(ele); } @@ -176,14 +176,14 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { DocServer.GetRefField(docid).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView - (f instanceof Doc) && this.props.addDocument(f, false); + (f instanceof Doc) && this.props.addDocument(f); } }); } else { this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options)); } } else if (text) { - this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false); + this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text })); } return; } @@ -194,7 +194,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (img) { let split = img.split("src=\"")[1].split("\"")[0]; let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 }); - this.props.addDocument(doc, false); + ImageUtils.ExtractExif(doc); + this.props.addDocument(doc); return; } else { let path = window.location.origin + "/doc/"; @@ -203,12 +204,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { DocServer.GetRefField(docid).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView - (f instanceof Doc) && this.props.addDocument(f, false); + (f instanceof Doc) && this.props.addDocument(f); } }); } else { let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); - this.props.addDocument(htmlDoc, false); + this.props.addDocument(htmlDoc); } return; } @@ -252,7 +253,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { let type = result["content-type"]; if (type) { Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 }) - .then(doc => doc && this.props.addDocument(doc, false)); + .then(doc => doc && this.props.addDocument(doc)); } }); promises.push(prom); @@ -275,7 +276,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName }; let pathname = Utils.prepend(file.path); Docs.Get.DocumentFromType(type, pathname, full).then(doc => { - doc && (doc.fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "")); + doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "")); doc && this.props.addDocument(doc); }); })); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 882a0f144..abaa9662c 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -28,6 +28,7 @@ import { CollectionSchemaPreview } from './CollectionSchemaView'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import React = require("react"); +import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; export interface TreeViewProps { @@ -187,7 +188,7 @@ class TreeView extends React.Component<TreeViewProps> { onWorkspaceContextMenu = (e: React.MouseEvent): void => { if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) { + if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking && this.props.document !== CurrentUserUtils.UserDocument.workspaces) { ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" }); ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" }); @@ -198,6 +199,7 @@ class TreeView extends React.Component<TreeViewProps> { } else { ContextMenu.Instance.addItem({ description: "Open as Workspace", event: () => MainView.Instance.openWorkspace(this.dataDoc), icon: "caret-square-right" }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" }); + ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); } ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" }); @@ -217,7 +219,7 @@ class TreeView extends React.Component<TreeViewProps> { if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.document; - DocUtils.MakeLink({doc:sourceDoc}, {doc:destDoc}); + DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }); e.stopPropagation(); } if (de.data instanceof DragManager.DocumentDragData) { @@ -258,7 +260,10 @@ class TreeView extends React.Component<TreeViewProps> { let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth); if (aspect) return this.docWidth() * aspect; if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x); - return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50; + return this.props.document.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) : + Math.min(this.docWidth() * NumCast(this.props.document.scrollHeight, NumCast(this.props.document.nativeHeight)) / NumCast(this.props.document.nativeWidth, + NumCast(this.props.containingCollection.height)))) : + NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50; })()); } @@ -512,8 +517,6 @@ export class CollectionTreeView extends CollectionSubView(Document) { private treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; - @observable static NotifsCol: Opt<Doc>; - @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; } protected createTreeDropTarget = (ele: HTMLDivElement) => { @@ -538,7 +541,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { } onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout - if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { + if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.workspaces) { ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" }); e.stopPropagation(); @@ -552,31 +555,10 @@ export class CollectionTreeView extends CollectionSubView(Document) { } outerXf = () => Utils.GetScreenTransform(this._mainEle!); onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {}); - openNotifsCol = () => { - if (CollectionTreeView.NotifsCol) { - this.props.addDocTab(CollectionTreeView.NotifsCol, undefined, "onRight"); - } - } - @computed get renderNotifsButton() { - const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0; - const notifsRef = React.createRef<HTMLDivElement>(); - const dragNotifs = action(() => CollectionTreeView.NotifsCol!); - return <div id="toolbar" key="toolbar"> - <div ref={notifsRef}> - <button className="toolbar-button round-button" title="Notifs" - onClick={this.openNotifsCol} onPointerDown={CollectionTreeView.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}> - <FontAwesomeIcon icon={faBell} size="sm" /> - </button> - <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}> - {length} - </div> - </div> - </div >; - } @computed get renderClearButton() { return <div id="toolbar" key="toolbar"> - <button className="toolbar-button round-button" title="Notifs" + <button className="toolbar-button round-button" title="Empty" onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}> <FontAwesomeIcon icon={faTrash} size="sm" /> </button> @@ -609,7 +591,6 @@ export class CollectionTreeView extends CollectionSubView(Document) { TreeView.loadId = doc[Id]; Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false); })} /> - {this.props.Document.workspaceLibrary ? this.renderNotifsButton : (null)} {this.props.Document.allowClear ? this.renderClearButton : (null)} <ul className="no-indent" style={{ width: "max-content" }} > { diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss deleted file mode 100644 index 509851ebb..000000000 --- a/src/client/views/collections/CollectionVideoView.scss +++ /dev/null @@ -1,51 +0,0 @@ - -.collectionVideoView-cont{ - width: 100%; - height: 100%; - position: inherit; - top: 0; - left:0; - z-index: -1; - display:inline-table; -} -.collectionVideoView-time{ - color : white; - top :25px; - left : 25px; - position: absolute; - background-color: rgba(50, 50, 50, 0.2); - transform-origin: left top; -} -.collectionVideoView-snapshot{ - color : white; - top :25px; - right : 25px; - position: absolute; - background-color: rgba(50, 50, 50, 0.2); - transform-origin: left top; -} -.collectionVideoView-play { - width: 25px; - height: 20px; - bottom: 25px; - left : 25px; - position: absolute; - color : white; - background-color: rgba(50, 50, 50, 0.2); - border-radius: 4px; - text-align: center; - transform-origin: left bottom; -} -.collectionVideoView-full { - width: 25px; - height: 20px; - bottom: 25px; - right : 25px; - position: absolute; - color : white; - background-color: rgba(50, 50, 50, 0.2); - border-radius: 4px; - text-align: center; - transform-origin: right bottom; - -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx deleted file mode 100644 index 5185d9d0e..000000000 --- a/src/client/views/collections/CollectionVideoView.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { action } from "mobx"; -import { observer } from "mobx-react"; -import { NumCast } from "../../../new_fields/Types"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import { VideoBox } from "../nodes/VideoBox"; -import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView"; -import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView"; -import "./CollectionVideoView.scss"; -import React = require("react"); -import { InkingControl } from "../InkingControl"; -import { InkTool } from "../../../new_fields/InkField"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - - -@observer -export class CollectionVideoView extends React.Component<FieldViewProps> { - private _videoBox?: VideoBox; - - public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") { - return FieldView.LayoutString(CollectionVideoView, fieldKey, fieldExt); - } - private get uIButtons() { - let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); - let curTime = NumCast(this.props.Document.curPage); - return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}> - <span>{"" + Math.round(curTime)}</span> - <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> - </div>, - <div className="collectionVideoView-snapshot" key="time" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}> - <FontAwesomeIcon icon="camera" size="lg" /> - </div>, - VideoBox._showControls ? (null) : [ - <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}> - <FontAwesomeIcon icon={this._videoBox && this._videoBox.Playing ? "pause" : "play"} size="lg" /> - </div>, - <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}> - F - </div> - ]]); - } - - @action - onPlayDown = () => { - if (this._videoBox) { - if (this._videoBox.Playing) { - this._videoBox.Pause(); - } else { - this._videoBox.Play(); - } - } - } - - @action - onFullDown = (e: React.PointerEvent) => { - if (this._videoBox) { - this._videoBox.FullScreen(); - e.stopPropagation(); - e.preventDefault(); - } - } - - @action - onSnapshot = (e: React.PointerEvent) => { - if (this._videoBox) { - this._videoBox.Snapshot(); - e.stopPropagation(); - e.preventDefault(); - } - } - - _isclick = 0; - @action - onResetDown = (e: React.PointerEvent) => { - if (this._videoBox) { - this._videoBox.Pause(); - e.stopPropagation(); - this._isclick = 0; - document.addEventListener("pointermove", this.onPointerMove, true); - document.addEventListener("pointerup", this.onPointerUp, true); - InkingControl.Instance.switchTool(InkTool.Eraser); - } - } - - @action - onPointerMove = (e: PointerEvent) => { - this._isclick += Math.abs(e.movementX) + Math.abs(e.movementY); - if (this._videoBox) { - this._videoBox.Seek(Math.max(0, NumCast(this.props.Document.curPage, 0) + Math.sign(e.movementX) * 0.0333)); - } - e.stopImmediatePropagation(); - } - @action - onPointerUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.onPointerMove, true); - document.removeEventListener("pointerup", this.onPointerUp, true); - InkingControl.Instance.switchTool(InkTool.None); - this._isclick < 10 && (this.props.Document.curPage = 0); - } - setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; - - private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { - let props = { ...this.props, ...renderProps }; - return (<> - <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} chromeCollapsed={true} /> - {this.props.isSelected() ? this.uIButtons : (null)} - </>); - } - - render() { - return ( - <CollectionBaseView {...this.props} className="collectionVideoView-cont" > - {this.subView} - </CollectionBaseView>); - } -}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 534246326..3d5b4e562 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -18,6 +18,8 @@ import { CollectionSchemaView } from "./CollectionSchemaView"; import { CollectionStackingView } from './CollectionStackingView'; import { CollectionTreeView } from "./CollectionTreeView"; import { CollectionViewBaseChrome } from './CollectionViewChromes'; +import { ImageUtils } from '../../util/Import & Export/ImageUtils'; +import { CollectionLinearView } from '../CollectionLinearView'; export const COLLECTION_BORDER_WIDTH = 2; library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy); @@ -65,6 +67,7 @@ export class CollectionView extends React.Component<FieldViewProps> { case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } + case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); } case CollectionViewType.Freeform: default: this.props.Document.freeformLayoutEngine = undefined; @@ -123,6 +126,7 @@ export class CollectionView extends React.Component<FieldViewProps> { let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : []; layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" }); !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" }); + ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) }); } } diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx index 23001f0f5..3a66c05f4 100644 --- a/src/client/views/collections/CollectionViewChromes.tsx +++ b/src/client/views/collections/CollectionViewChromes.tsx @@ -243,6 +243,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); + case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />); default: return null; } } @@ -398,6 +399,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option> <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option> + <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Linear View</option> </select> <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}> <input className="collectionViewBaseChrome-viewSpecsInput" diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index df089eb00..fe92eed10 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -10,7 +10,7 @@ export interface CollectionFreeFormLinkViewProps { A: Doc; B: Doc; LinkDocs: Doc[]; - addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; + addDocument: (document: Doc) => boolean; removeDocument: (document: Doc) => boolean; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 9b608a576..db36c4391 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -50,12 +50,6 @@ border-radius: inherit; box-sizing: border-box; position: absolute; - overflow: hidden; - - .marqueeView { - overflow: hidden; - } - top: 0; left: 0; width: 100%; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 915126742..440a0a8e5 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,6 +1,6 @@ import { library } from "@fortawesome/fontawesome-svg-core"; import { faEye } from "@fortawesome/free-regular-svg-icons"; -import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faFileUpload } from "@fortawesome/free-solid-svg-icons"; +import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc"; @@ -95,10 +95,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading); } !this.Document.isRuleProvider && (newBox.heading = heading); - this.addDocument(newBox, false); + this.addDocument(newBox); } - private addDocument = (newBox: Doc, allowDuplicates: boolean) => { - let added = this.props.addDocument(newBox, false); + private addDocument = (newBox: Doc) => { + let added = this.props.addDocument(newBox); added && this.bringToFront(newBox); added && this.updateCluster(newBox); return added; @@ -107,7 +107,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true)); } - public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.page, -1) - NumCast(this.Document.curPage, -1)) < 1.5 || NumCast(doc.page, -1) === -1); } + public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); } public getActiveDocuments = () => { return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); @@ -269,6 +269,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerDown = (e: React.PointerEvent): void => { + if (e.nativeEvent.cancelBubble) return; this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false; if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) { document.removeEventListener("pointermove", this.onPointerMove); @@ -341,7 +342,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } return; } - if (!e.cancelBubble && !this.isAnnotationOverlay) { + if (!e.cancelBubble) { if (this._hitCluster && this.tryDragCluster(e)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); @@ -437,7 +438,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.props.Document.lockedPosition || this.isAnnotationOverlay) return; + if (this.props.Document.lockedPosition || this.props.Document.inOverlay) return; if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } @@ -449,7 +450,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action setPan(panX: number, panY: number) { - if (!this.props.Document.lockedPosition) { + if (!this.props.Document.lockedPosition || this.props.Document.inOverlay) { this.props.Document.panTransformType = "None"; var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); @@ -565,7 +566,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { PanelWidth: layoutDoc[WidthSym], PanelHeight: layoutDoc[HeightSym], ContentScaling: returnOne, - ContainingCollectionView: this.props.CollectionView, + ContainingCollectionView: this.props.ContainingCollectionView, focus: this.focusDocument, backgroundColor: returnEmptyString, parentActive: this.props.active, @@ -747,7 +748,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { if (doc instanceof Doc) { const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y); doc.x = xx, doc.y = yy; - this.props.addDocument && this.props.addDocument(doc, false); + this.props.addDocument && this.props.addDocument(doc); } } } @@ -769,10 +770,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } - private childViews = () => [ - <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, - ...this.views - ] + private childViews = () => { + let children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; + return [ + <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />, + ...children, + ...this.views, + ]; + } render() { // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap) this.props.Document.fitX = this.contentBounds && this.contentBounds.x; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 9fc2e44fb..04f6ec2ad 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -6,6 +6,13 @@ width:100%; height:100%; } +.marqueeView { + overflow: hidden; +} + +.marqueeView:focus-within { + overflow: hidden; +} .marquee { border-style: dashed; box-sizing: border-box; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 4a7fa629c..e9f4c40a6 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -25,7 +25,7 @@ interface MarqueeViewProps { getContainerTransform: () => Transform; getTransform: () => Transform; container: CollectionFreeFormView; - addDocument: (doc: Doc, allowDuplicates: false) => boolean; + addDocument: (doc: Doc) => boolean; activeDocuments: () => Doc[]; selectDocuments: (docs: Doc[]) => void; removeDocument: (doc: Doc) => boolean; @@ -86,7 +86,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> ns.map(line => { let indent = line.search(/\S|$/); let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); - this.props.addDocument(newBox, false); + this.props.addDocument(newBox); y += 40 * this.props.getTransform().Scale; }); })(); @@ -95,7 +95,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> navigator.clipboard.readText().then(text => { let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); if (ns.length === 1 && text.startsWith("http")) { - this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer + this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer } else { this.pasteTable(ns, x, y); } @@ -149,7 +149,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); - this.props.addDocument(newCol, false); + this.props.addDocument(newCol); } } @action @@ -220,7 +220,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } } - setPreviewCursor = (x: number, y: number, drag: boolean) => { + setPreviewCursor = action((x: number, y: number, drag: boolean) => { if (drag) { this._downX = this._lastX = x; this._downY = this._lastY = y; @@ -235,7 +235,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this._downY = y; PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument); } - } + }); @action onClick = (e: React.MouseEvent): void => { @@ -344,12 +344,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.props.removeDocument(d); d.x = NumCast(d.x) - bounds.left - bounds.width / 2; d.y = NumCast(d.y) - bounds.top - bounds.height / 2; - d.page = -1; + d.displayTimecode = undefined; return d; }); } let newCollection = this.getCollection(selected); - this.props.addDocument(newCollection, false); + this.props.addDocument(newCollection); this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); @@ -506,8 +506,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps> render() { let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; - return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> - <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} > + return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> + <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} onScroll={(e) => e.currentTarget.scrollLeft = 0} > {this._visible ? this.marqueeDiv : null} <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} > {this.props.children} |
