import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, untracked } from 'mobx'; import { observer } from 'mobx-react'; import Measure from 'react-measure'; // import { Resize } from 'react-table'; import { Doc, Opt } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { PastelSchemaPalette, SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { Cast, NumCast } from '../../../../fields/Types'; import { TraceMobx } from '../../../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; import { DocUtils } from '../../../documents/Documents'; import { SelectionManager } from '../../../util/SelectionManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss'; import { DocumentView } from '../../nodes/DocumentView'; import { DefaultStyleProvider } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; // import { SchemaTable } from './SchemaTable'; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 export enum ColumnType { Any, Number, String, Boolean, Doc, Image, List, Date, } // 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], ['_curPage', ColumnType.Number], ['_currentTimecode', ColumnType.Number], ['zIndex', ColumnType.Number], ]); @observer export class CollectionSchemaView extends CollectionSubView() { private _previewCont?: HTMLDivElement; @observable _previewDoc: Doc | undefined = undefined; @observable _focusedTable: Doc = this.props.Document; @observable _col: any = ''; @observable _menuWidth = 0; @observable _headerOpen = false; @observable _headerIsEditing = false; @observable _menuHeight = 0; @observable _pointerX = 0; @observable _pointerY = 0; @observable _openTypes: boolean = false; @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 - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth(); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @computed get scale() { return this.props.ScreenToLocalTransform().Scale; } @computed get columns() { return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []); } set columns(columns: SchemaHeaderField[]) { this.props.Document._schemaHeaders = new List(columns); } @computed get menuCoordinates() { let searchx = 0; let searchy = 0; if (this.props.Document._searchDoc) { const el = document.getElementsByClassName('collectionSchemaView-searchContainer')[0]; if (el !== undefined) { const rect = el.getBoundingClientRect(); searchx = rect.x; searchy = rect.y; } } const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)) - searchx; const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)) - searchy; return this.props.ScreenToLocalTransform().transformPoint(x, y); } 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)); } @action setHeaderIsEditing = (isEditing: boolean) => (this._headerIsEditing = isEditing); @undoBatch setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => { this._openTypes = false; 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; columns.forEach(col => col.setDesc(undefined)); const index = columns.findIndex(c => c.heading === columnField.heading); const column = columns[index]; column.setDesc(descending); columns[index] = column; this.columns = columns; }; renderTypes = (col: any) => { if (columnTypes.get(col.heading)) return null; const type = col.type; const anyType = (
this.setColumnType(col, ColumnType.Any)}> Any
); const numType = (
this.setColumnType(col, ColumnType.Number)}> Number
); const textType = (
this.setColumnType(col, ColumnType.String)}> Text
); const boolType = (
this.setColumnType(col, ColumnType.Boolean)}> Checkbox
); const listType = (
this.setColumnType(col, ColumnType.List)}> List
); const docType = (
this.setColumnType(col, ColumnType.Doc)}> Document
); const imageType = (
this.setColumnType(col, ColumnType.Image)}> Image
); const dateType = (
this.setColumnType(col, ColumnType.Date)}> Date
); const allColumnTypes = (
{anyType} {numType} {textType} {boolType} {listType} {docType} {imageType} {dateType}
); const justColType = type === ColumnType.Any ? anyType : type === ColumnType.Number ? numType : type === ColumnType.String ? textType : type === ColumnType.Boolean ? boolType : type === ColumnType.List ? listType : type === ColumnType.Doc ? docType : type === ColumnType.Date ? dateType : imageType; return (
(this._openTypes = !this._openTypes))}>
{this._openTypes ? allColumnTypes : justColType}
); }; renderSorting = (col: any) => { const sort = col.desc; return (
this.setColumnSort(col, true)}> Sort descending
this.setColumnSort(col, false)}> Sort ascending
this.setColumnSort(col, undefined)}> 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.setColumnColor(col, pink!)}>
this.setColumnColor(col, purple!)}>
this.setColumnColor(col, blue!)}>
this.setColumnColor(col, yellow!)}>
this.setColumnColor(col, red!)}>
this.setColumnColor(col, gray)}>
); }; @undoBatch @action changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => { 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; if (filter) { Doc.setDocFilter(this.props.Document, newKey, filter, 'match'); } else { this.props.Document._docFilters = undefined; } } } } }; @action openHeader = (col: any, screenx: number, screeny: number) => { this._col = col; this._headerOpen = true; this._pointerX = screenx; this._pointerY = screeny; }; @action closeHeader = () => { this._headerOpen = false; }; @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 - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, -this.borderWidth); }; @action onHeaderClick = (e: React.PointerEvent) => { e.stopPropagation(); }; @action onWheel(e: React.WheelEvent) { const scale = this.props.ScreenToLocalTransform().Scale; this.props.isContentActive(true) && e.stopPropagation(); } @computed get renderMenuContent() { TraceMobx(); return (
{this.renderTypes(this._col)} {this.renderColors(this._col)}
); } private createTarget = (ele: HTMLDivElement) => { this._previewCont = ele; super.CreateDropTarget(ele); }; isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable; @action setFocused = (doc: Doc) => (this._focusedTable = doc); @action setPreviewDoc = (doc: Opt) => { SelectionManager.SelectSchemaViewDoc(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, 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() { return ( ); } @computed public get schemaToolbar() { return (
Show Preview
); } onSpecificMenu = (e: React.MouseEvent) => { if ((e.target as any)?.className?.includes?.('collectionSchemaView-cell') || e.target instanceof HTMLSpanElement) { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: 'remove', event: () => this._previewDoc && this.props.removeDocument?.(this._previewDoc), icon: 'trash' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); cm.displayMenu(e.clientX, e.clientY); (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this. e.stopPropagation(); } }; @action onTableClick = (e: React.MouseEvent): void => { if (!(e.target as any)?.className?.includes?.('collectionSchemaView-cell') && !(e.target instanceof HTMLSpanElement)) { this.setPreviewDoc(undefined); } else { e.stopPropagation(); } this.setFocused(this.props.Document); this.closeHeader(); }; 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; }; onZoomMenu = (e: React.WheelEvent) => this.props.isContentActive(true) && e.stopPropagation(); render() { TraceMobx(); if (!this.props.isContentActive()) setTimeout(() => this.closeHeader(), 0); const menuContent = this.renderMenuContent; const menu = (
this.onZoomMenu(e)} onPointerDown={e => this.onHeaderClick(e)} style={{ 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 }) =>
{menuContent}
}
); return (
this.props.isContentActive(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}> {this.schemaTable}
{this.dividerDragger} {!this.previewWidth() ? null : this.previewPanel} {this._headerOpen && this.props.isContentActive() ? menu : null}
); TraceMobx(); return
HELLO
; } }