import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { DivHeight, DivWidth, returnEmptyString, returnTrue, setupMoveUpEvents } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { RichTextField } from '../../../fields/RichTextField'; import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, DocCast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; import { DocumentFromField } from '../../documents/DocFromField'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocUtils } from '../../documents/DocUtils'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { DocumentView } from '../nodes/DocumentView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import './CollectionStackingView.scss'; // So this is how we are storing a column interface CSVFieldColumnProps { Doc: Doc; TemplateDataDoc: Opt; docList: Doc[]; heading: string; pivotField: string; chromeHidden?: boolean; colHeaderData: SchemaHeaderField[] | undefined; headingObject: SchemaHeaderField | undefined; yMargin: number; columnWidth: number; numGroupColumns: number; gridGap: number; dontCenter: 'x' | 'xy' | 'y'; type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; headings: () => object[]; // I think that stacking view actually has a single column and then supposedly you can add more columns? Unsure renderChildren: (docs: Doc[]) => JSX.Element[]; addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; colStackRefs: HTMLElement[]; colHeaderRefs: HTMLElement[]; } @observer export class CollectionStackingViewFieldColumn extends ObservableReactComponent { private dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [name: string]: IReactionDisposer } = {}; @observable _background = 'inherit'; @observable _paletteOn = false; @observable _heading = ''; @observable _color = ''; constructor(props: CSVFieldColumnProps) { super(props); makeObservable(this); this._heading = this._props.headingObject ? this._props.headingObject.heading : this._props.heading; this._color = this._props.headingObject ? this._props.headingObject.color : '#f1efeb'; } _ele: HTMLElement | null = null; _eleMasonrySingle = React.createRef(); _headerRef: HTMLDivElement | null = null; protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData) { const sourceDragAction = dragData.dropAction; const sameCollection = !dragData.draggedDocuments.some(d => !this._props.docList.includes(d)); dragData.dropAction = !sameCollection // if doc from another tree ? sourceDragAction || targetDropAction // then use the source's dragAction otherwise the target's : sourceDragAction === dropActionType.inPlace // if source drag is inPlace ? sourceDragAction // keep the doc in place : dropActionType.same; // otherwise use same tree semantics to move within tree e.stopPropagation(); } }; // This is likely similar to what we will be doing. Why do we need to make these refs? // is that the only way to have drop targets? createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this)); else if (this._eleMasonrySingle.current) runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._eleMasonrySingle.current!), 1)); this._ele = ele; }; @action componentDidMount() { this._eleMasonrySingle.current && this.props.colStackRefs.push(this._eleMasonrySingle.current); this._disposers.collapser = reaction( () => this._props.headingObject?.collapsed, collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore { fireImmediately: true } ); } componentWillUnmount() { this._disposers.collapser?.(); this._ele && runInAction(() => this.props.colStackRefs.splice(this.props.colStackRefs.indexOf(this._ele!), 1)); this._ele = null; } @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; this._props.pivotField && drop.docs?.forEach(d => Doc.SetInPlace(d, this._props.pivotField, drop.val, false)); return true; }); getValue = (value: string) => { const 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 */) => { const castedValue = this.getValue(value); if (castedValue) { if (this._props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { return false; } this._props.pivotField && this._props.docList.forEach(d => { d[this._props.pivotField] = castedValue; }) // prettier-ignore 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._props.headingObject?.setColor(color); this._color = color; }; @action pointerEntered = () => { SnappingManager.IsDragging && (this._background = '#b4b4b4'); } // prettier-ignore @action pointerLeave = () => { this._background = 'inherit'}; // prettier-ignore @undoBatch typedNote = () => { const key = this._props.pivotField; const newDoc = Docs.Create.TextDocument('', { _height: 18, _width: 200, _layout_fitWidth: true, _layout_autoHeight: true }); key && (newDoc[key] = this.getValue(this._props.heading)); const maxHeading = this._props.docList.reduce((prevHeading, doc) => (NumCast(doc.heading) > prevHeading ? NumCast(doc.heading) : prevHeading), 0); const heading = maxHeading === 0 || this._props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; newDoc.heading = heading; DocumentView.SetSelectOnLoad(newDoc); return this._props.addDocument?.(newDoc) || false; }; @action deleteColumn = () => { this._props.pivotField && this._props.docList.forEach(d => { d[this._props.pivotField] = undefined; }); if (this._props.colHeaderData && this._props.headingObject) { const index = this._props.colHeaderData.indexOf(this._props.headingObject); this._props.colHeaderData.splice(index, 1); } }; @action collapseSection = () => { this._props.headingObject?.setCollapsed(!this._props.headingObject.collapsed); this.collapsed = BoolCast(this._props.headingObject?.collapsed); }; headerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction); // TODO: I think this is where I'm supposed to edit stuff startDrag = (e: PointerEvent) => { // is MakeEmbedding a way to make a copy of a doc without rendering it? const embedding = Doc.MakeEmbedding(this._props.Doc); embedding._width = this._props.columnWidth / (this._props.colHeaderData?.length || 1); embedding._pivotField = undefined; let value = this.getValue(this._heading); value = typeof value === 'string' ? `"${value}"` : value; embedding.viewSpecScript = ScriptField.MakeFunction(`doc.${this._props.pivotField} === ${value}`, { doc: Doc.name }); if (embedding.viewSpecScript) { DragManager.StartDocumentDrag([this._headerRef!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY); return true; } return false; }; renderColorPicker = () => { const gray = '#f1efeb'; const selected = this._props.headingObject ? this._props.headingObject.color : gray; const colors = ['pink2', 'purple4', 'bluegreen1', 'yellow4', 'gray', 'red2', 'bluegreen7', 'bluegreen5', 'orange1']; return (
{colors.map(col => { const palette = PastelSchemaPalette.get(col); return
this.changeColumnColor(palette!)} />; })}
); }; renderMenu = () => (
Add options here
); @observable private collapsed: boolean = false; private toggleVisibility = action(() => { this.collapsed = !this.collapsed; }); menuCallback = () => { ContextMenu.Instance.clearItems(); const layoutItems: ContextMenuProps[] = []; const docItems: ContextMenuProps[] = []; const dataDoc = this._props.TemplateDataDoc || this._props.Doc; const width = this._ele ? DivWidth(this._ele) : 0; const height = this._ele ? DivHeight(this._ele) : 0; DocUtils.addDocumentCreatorMenuItems( doc => { DocumentView.SetSelectOnLoad(doc); return this._props.addDocument?.(doc); }, this._props.addDocument, 0, 0, true ); Array.from(Object.keys(Doc.GetProto(dataDoc))) .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string') .map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => { const created = DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this._props.Doc)); if (created) { if (this._props.Doc.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, this._props.Doc); } return this._props.addDocument?.(created); } return false; }, icon: 'compress-arrows-alt', }) ); Array.from(Object.keys(Doc.GetProto(dataDoc))) .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length) .map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => { const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey }); if (created) { const container = DocCast(this._props.Doc.rootDocument)?.[DocData] ? Doc.GetProto(this._props.Doc) : this._props.Doc; if (container.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, container); return Doc.AddDocToList(container, Doc.LayoutDataKey(container), created); } return this._props.addDocument?.(created) || false; } return false; }, icon: 'compress-arrows-alt', }) ); !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' }); !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' }); ContextMenu.Instance.setDefaultItem('::', (name: string): void => { Doc.GetProto(this._props.Doc)[name] = ''; const created = Docs.Create.TextDocument('', { title: name, _width: 250, _layout_autoHeight: true }); if (created) { if (this._props.Doc.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, this._props.Doc); } this._props.addDocument?.(created); } }); const pt = this._props .screenToLocalTransform() .inverse() .transformPoint(width - 30, height); ContextMenu.Instance.displayMenu(pt[0], pt[1], undefined, true); }; setRef = (r: HTMLDivElement | null) => { if (this._headerRef && this._props.colHeaderRefs.includes(this._headerRef)) this._props.colHeaderRefs.splice(this._props.colStackRefs.indexOf(this._headerRef), 1); r && this._props.colHeaderRefs.push(r); this._headerRef = r; }; @computed get innards() { TraceMobx(); const key = this._props.pivotField; const heading = this._heading; const columnYMargin = this._props.headingObject ? 0 : this._props.yMargin; const noValueHeader = `NO ${key.toUpperCase()} VALUE`; const evContents = heading || (this._props?.type === 'number' ? '0' : noValueHeader); const headingView = this._props.headingObject ? (
{/* 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. */}
evContents} SetValue={this.headingChanged} contents={evContents} oneLine />
{this._paletteOn ? this.renderColorPicker() : null}
) : null; const templatecols = `${this._props.columnWidth / this._props.numGroupColumns}px `; const { type } = this._props.Doc; return ( <> {this._props.Doc._columnsHideIfEmpty ? null : headingView} {this.collapsed ? null : (
{this._props.renderChildren(this._props.docList)}
{!this._props.chromeHidden && type !== DocumentType.PRES ? ( // TODO: this is the "new" button: see what you can work with here // change cursor to pointer for this, and update dragging cursor // TODO: there is a bug that occurs when adding a freeform document and trying to move it around // TODO: would be great if there was additional space beyond the frame, so that you can actually see your // bottom note // TODO: ok, so we are using a single column, and this is it!
e.stopPropagation()} className="collectionStackingView-addDocumentButton" style={{ width: 'calc(100% - 25px)', maxWidth: this._props.columnWidth / this._props.numGroupColumns - 25, marginBottom: 10 }}> } menuCallback={this.menuCallback} />
) : null}
)} ); } render() { TraceMobx(); const heading = this._heading; return (
{this.innards}
); } }