import React = require("react"); import { library, IconProp } from '@fortawesome/fontawesome-svg-core'; import { faCog, faPlus, faSortDown, faSortUp, faTable } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, untracked } from "mobx"; import { observer } from "mobx-react"; import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table"; import "react-table/react-table.css"; import { Doc, DocListCast, Field, Opt, LayoutSym } from "../../../fields/Doc"; import { Id } from "../../../fields/FieldSymbols"; import { List } from "../../../fields/List"; import { listSpec } from "../../../fields/Schema"; import { SchemaHeaderField, PastelSchemaPalette } from "../../../fields/SchemaHeaderField"; import { ComputedField } from "../../../fields/ScriptField"; import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../fields/Types"; import { Docs, DocumentOptions } from "../../documents/Documents"; import { CompileScript, Transformer, ts } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss'; import { ContextMenu } from "../ContextMenu"; import '../DocumentDecorations.scss'; import { CellProps, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaImageCell, CollectionSchemaListCell } from "./CollectionSchemaCells"; import { CollectionSchemaAddColumnHeader, CollectionSchemaHeader, CollectionSchemaColumnMenu, KeysDropdown } from "./CollectionSchemaHeaders"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { CollectionView } from "./CollectionView"; import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView"; import { setupMoveUpEvents, emptyFunction, returnZero, returnOne, returnFalse, returnEmptyFilter, emptyPath } from "../../../Utils"; import { SnappingManager } from "../../util/SnappingManager"; import Measure from "react-measure"; import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC"; import { SchemaTable } from "./SchemaTable"; //import { SchemaTable } from "./SchemaTable"; library.add(faCog, faPlus, faSortUp, faSortDown); library.add(faTable); // 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 { Any, Number, String, Boolean, Doc, Image, List } // this map should be used for keys that should have a const type of value const columnTypes: Map = new Map([ ["title", ColumnType.String], ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number], ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean], ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number] ]); @observer export class CollectionSchemaView extends CollectionSubView(doc => doc) { private _previewCont?: HTMLDivElement; private DIVIDER_WIDTH = 4; @observable previewDoc: Doc | undefined = undefined; @observable private _focusedTable: Doc = this.props.Document; @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); } @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; } @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @observable _menuWidth = 0; @observable _menuContent: any = ""; @observable _headerOpen = false; @observable _isOpen = false; @observable _node: HTMLDivElement | null = null; @observable _headerIsEditing = false; @observable _col: any = ""; @observable _menuHeight = 0; @observable _pointerX = 0; @observable _pointerY = 0; @computed get menuCoordinates() { const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)); const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)); return this.props.ScreenToLocalTransform().transformPoint(x, y); } @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec(SchemaHeaderField), []); } set columns(columns: SchemaHeaderField[]) { this.props.Document.schemaColumns = new List(columns); } get documentKeys() { const docs = this.childDocs; const keys: { [key: string]: boolean } = {}; // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields. // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked. // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu // is displayed (unlikely) it won't show up until something else changes. //TODO Types untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false)))); this.columns.forEach(key => keys[key.heading] = true); return Array.from(Object.keys(keys)); } @computed get possibleKeys() { return this.documentKeys.filter(key => this.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1); } componentDidMount() { document.addEventListener("pointerdown", this.detectClick); } componentWillUnmount() { document.removeEventListener("pointerdown", this.detectClick); } @action setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing; detectClick = (e: PointerEvent): void => { if (this._node && this._node.contains(e.target as Node)) { } else { this._isOpen = false; this.setHeaderIsEditing(false); this.closeHeader(); } } @action toggleIsOpen = (): void => { this._isOpen = !this._isOpen; this.setHeaderIsEditing(this._isOpen); } changeColumnType = (type: ColumnType, col: any): void => { this.setColumnType(col, type); } changeColumnSort = (desc: boolean | undefined, col: any): void => { this.setColumnSort(col, desc); } changeColumnColor = (color: string, col: any): void => { this.setColumnColor(col, color); } @undoBatch setColumnType = (columnField: SchemaHeaderField, type: ColumnType): void => { if (columnTypes.get(columnField.heading)) return; const columns = this.columns; const index = columns.indexOf(columnField); if (index > -1) { columnField.setType(NumCast(type)); columns[index] = columnField; this.columns = columns; } } @undoBatch setColumnColor = (columnField: SchemaHeaderField, color: string): void => { const columns = this.columns; const index = columns.indexOf(columnField); if (index > -1) { columnField.setColor(color); columns[index] = columnField; this.columns = columns; // need to set the columns to trigger rerender } } @undoBatch @action setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => { const columns = this.columns; const index = columns.findIndex(c => c.heading === columnField.heading); const column = columns[index]; column.setDesc(descending); columns[index] = column; this.columns = columns; } @action setNode = (node: HTMLDivElement): void => { node && (this._node = node); } renderTypes = (col: any) => { if (columnTypes.get(col.heading)) return (null); const type = col.type; return (
this.changeColumnType(ColumnType.Any, col)}> Any
this.changeColumnType(ColumnType.Number, col)}> Number
this.changeColumnType(ColumnType.String, col)}> Text
this.changeColumnType(ColumnType.Boolean, col)}> Checkbox
this.changeColumnType(ColumnType.List, col)}> List
this.changeColumnType(ColumnType.Doc, col)}> Document
this.changeColumnType(ColumnType.Image, col)}> Image
); } renderSorting = (col: any) => { const sort = col.desc; return (
this.changeColumnSort(true, col)}> Sort descending
this.changeColumnSort(false, col)}> Sort ascending
this.changeColumnSort(undefined, col)}> Clear sorting
); } renderColors = (col: any) => { const selected = col.color; const pink = PastelSchemaPalette.get("pink2"); const purple = PastelSchemaPalette.get("purple2"); const blue = PastelSchemaPalette.get("bluegreen1"); const yellow = PastelSchemaPalette.get("yellow4"); const red = PastelSchemaPalette.get("red2"); const gray = "#f1efeb"; return (
this.changeColumnColor(pink!, col)}>
this.changeColumnColor(purple!, col)}>
this.changeColumnColor(blue!, col)}>
this.changeColumnColor(yellow!, col)}>
this.changeColumnColor(red!, col)}>
this.changeColumnColor(gray, col)}>
); } @undoBatch @action changeColumns = (oldKey: string, newKey: string, addNew: boolean) => { const columns = this.columns; if (columns === undefined) { this.columns = new List([new SchemaHeaderField(newKey, "f1efeb")]); } else { if (addNew) { columns.push(new SchemaHeaderField(newKey, "f1efeb")); this.columns = columns; } else { const index = columns.map(c => c.heading).indexOf(oldKey); if (index > -1) { const column = columns[index]; column.setHeading(newKey); columns[index] = column; this.columns = columns; } } } } @action openHeader = (col: any, menu: any, screenx: number, screeny: number) => { this._menuContent = menu; this._col = col; this._headerOpen = !this._headerOpen; this._pointerX = screenx; this._pointerY = screeny; } @action closeHeader = () => { this._headerOpen = false; } renderContent = (col: any) => { return (
c.heading)} canAddNew={true} addNew={false} onSelect={this.changeColumns} setIsEditing={this.setHeaderIsEditing} />
{false ? <> : <> {this.renderTypes(col)} {this.renderSorting(col)} {this.renderColors(col)}
}
); } @undoBatch @action deleteColumn = (key: string) => { const columns = this.columns; if (columns === undefined) { this.columns = new List([]); } else { const index = columns.map(c => c.heading).indexOf(key); if (index > -1) { columns.splice(index, 1); this.columns = columns; } } this.closeHeader(); } getPreviewTransform = (): Transform => { return this.props.ScreenToLocalTransform().translate(- this.borderWidth - 4 - this.tableWidth, - this.borderWidth); } @computed get renderMenu() { return (
this.props.active(true) && e.stopPropagation()} style={{ position: "absolute", background: "white", transform: `translate(${this.menuCoordinates[0]}px, ${this.menuCoordinates[1]}px)` }}> { const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height); this._menuWidth = dim[0]; this._menuHeight = dim[1]; })}> {({ measureRef }) =>
{this.renderContent(this._col)}
}
); } private createTarget = (ele: HTMLDivElement) => { this._previewCont = ele; super.CreateDropTarget(ele); } isFocused = (doc: Doc): boolean => this.props.isSelected() && doc === this._focusedTable; @action setFocused = (doc: Doc) => this._focusedTable = doc; @action setPreviewDoc = (doc: Doc) => this.previewDoc = doc; //toggles preview side-panel of schema @action toggleExpander = () => { this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0; } onDividerDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, action(() => this.toggleExpander())); } @action onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { const nativeWidth = this._previewCont!.getBoundingClientRect(); const minWidth = 40; const maxWidth = 1000; const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; this.props.Document.schemaPreviewWidth = width; return false; } onPointerDown = (e: React.PointerEvent): void => { if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { if (this.props.isSelected(true)) e.stopPropagation(); else { this.props.select(false); } } } @computed get previewDocument(): Doc | undefined { return this.previewDoc; } @computed get dividerDragger() { return this.previewWidth() === 0 ? (null) :
; } @computed get previewPanel() { return
{!this.previewDocument ? (null) : }
; } @computed get schemaTable() { const preview = ""; return ; } @computed public get schemaToolbar() { return
Show Preview
; } @action onTablePointerDown = (e: React.PointerEvent): void => { this.setFocused(this.props.Document); if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected(true)) { e.stopPropagation(); } this._pointerY = e.screenY; this._pointerX = e.screenX; } onResizedChange = (newResized: Resize[], event: any) => { const columns = this.columns; newResized.forEach(resized => { const index = columns.findIndex(c => c.heading === resized.id); const column = columns[index]; column.setWidth(resized.value); columns[index] = column; }); this.columns = columns; } @action setColumns = (columns: SchemaHeaderField[]) => this.columns = columns @undoBatch reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => { const columns = [...columnsValues]; const oldIndex = columns.indexOf(toMove); const relIndex = columns.indexOf(relativeTo); const newIndex = (oldIndex > relIndex && !before) ? relIndex + 1 : (oldIndex < relIndex && before) ? relIndex - 1 : relIndex; if (oldIndex === newIndex) return; columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]); this.columns = columns; } render() { return
this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> {this.schemaTable}
{this.dividerDragger} {!this.previewWidth() ? (null) : this.previewPanel} {this._headerOpen ? this.renderMenu : null}
; } }