/* eslint-disable no-use-before-define */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Popup, Size, Type } from '@dash/components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import Select from 'react-select'; import { ClientUtils, StopEvent, returnEmptyFilter, returnFalse, returnZero } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field, IdToDoc, returnEmptyDoclist } from '../../../../fields/Doc'; import { RichTextField } from '../../../../fields/RichTextField'; import { ColumnType } from '../../../../fields/SchemaHeaderField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast, toList } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { FInfo, FInfoFieldType } from '../../../documents/Documents'; import { dropActionType } from '../../../util/DropActionTypes'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoable } from '../../../util/UndoManager'; import { EditableView } from '../../EditableView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaCellField } from './SchemaCellField'; import { DocLayout } from '../../../../fields/DocSymbols'; /** * SchemaTableCells make up the majority of the visual representation of the SchemaView. * They are rendered for each cell in the SchemaView, and each represents one field value * of a doc. Editing the content of the cell changes the corresponding doc's field value. */ export interface SchemaTableCellProps { Doc: Doc; col: number; deselectCell: () => void; selectCell: (doc: Doc, col: number, shift: boolean, ctrl: boolean) => void; selectedCells: () => Doc[] | undefined; selectedCol: () => number; fieldKey: string; maxWidth?: () => number; columnWidth: () => number; rowHeight: () => number; padding?: number; // default is 5 -- see scss isRowActive: () => boolean | undefined; getFinfo: (fieldKey: string) => FInfo | undefined; setColumnValues: (field: string, value: string) => boolean; oneLine?: boolean; // whether all input should fit on one line vs allowing textare multiline inputs allowCRs?: boolean; // allow carriage returns in text input (othewrise CR ends the edit) finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView) options?: string[]; menuTarget: HTMLDivElement | null; transform: () => Transform; autoFocus?: boolean; // whether to set focus on creation, othwerise wait for a click rootSelected?: () => boolean; rowSelected: () => boolean; isolatedSelection: (doc: Doc) => [boolean, boolean]; highlightCells: (text: string) => void; eqHighlightFunc: (text: string) => HTMLDivElement[] | []; refSelectModeInfo: { enabled: boolean; currEditing: SchemaCellField | undefined }; selectReference: (doc: Doc, col: number) => void; } function selectedCell(props: SchemaTableCellProps) { return props.isRowActive() && props.selectedCol() === props.col && props.selectedCells()?.filter(d => d === props.Doc)?.length; } @observer export class SchemaTableCell extends ObservableReactComponent { // private _fieldRef: SchemaCellField | null = null; private _submittedValue: string = ''; constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } get docIndex(){return DocumentView.getDocViewIndex(this._props.Doc);} // prettier-ignore get isDefault(){return SchemaColumnHeader.isDefaultField(this._props.fieldKey);} // prettier-ignore get lockedInteraction(){return (this.isDefault || this._props.Doc._lockedSchemaEditing);} // prettier-ignore get backgroundColor() { if (this.lockedInteraction) { return '#F5F5F5'; } return ''; } static addFieldDoc = (docs: Doc | Doc[] /* , where: OpenWhere */) => { DocumentView.FocusOrOpen(toList(docs)[0]); return true; }; public static renderProps(props: SchemaTableCellProps) { const { Doc: Document, fieldKey, /* getFinfo,*/ columnWidth, isRowActive } = props; let protoCount = 0; const layoutDoc = fieldKey.startsWith('_') ? Document[DocLayout] : Document; let doc = Document; while (doc) { if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) break; protoCount++; doc = DocCast(doc.proto); } const color = layoutDoc !== Document ? 'red' : protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; // color of text in cells const textDecoration = ''; const fieldProps: FieldViewProps = { childFilters: returnEmptyFilter, childFiltersByRanges: returnEmptyFilter, docViewPath: returnEmptyDocViewList, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, dragAction: dropActionType.move, renderDepth: 1, noSidebar: true, isContentActive: returnFalse, whenChildContentsActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, addDocTab: SchemaTableCell.addFieldDoc, pinToPres: returnZero, Document: Document, fieldKey: fieldKey, PanelWidth: columnWidth, PanelHeight: props.rowHeight, rootSelected: props.rootSelected, }; const readOnly = false; // getFinfo(fieldKey)?.readOnly ?? false; const cursor = !readOnly ? 'text' : 'default'; const pointerEvents: 'all' | 'none' = !readOnly && isRowActive() ? 'all' : 'none'; return { color, textDecoration, fieldProps, cursor, pointerEvents }; } adjustSelfReference = (field: string) => { const modField = field.replace(/\bthis.\b/g, `d${this.docIndex}.`); return modField; }; // parses a field from the "idToDoc(####)" format to DocumentId (d#) format for readability cleanupField = (field: string) => { let modField = field.slice(); let eqSymbol: string = ''; if (modField.startsWith('=')) { modField = modField.substring(1); eqSymbol += '='; } if (modField.startsWith(':=')) { modField = modField.substring(2); eqSymbol += ':='; } const idPattern = /idToDoc\((.*?)\)/g; let matches; const results = new Array<[id: string, func: string]>(); while ((matches = idPattern.exec(field)) !== null) { results.push([matches[0], matches[1].replace(/"/g, '')]); } results.forEach(idFuncPair => { modField = modField.replace(idFuncPair[0], 'd' + DocumentView.getDocViewIndex(IdToDoc(idFuncPair[1])).toString()); }); if (modField.endsWith(';')) modField = modField.substring(0, modField.length - 1); const inQuotes = (strField: string) => { return (strField.startsWith('`') && strField.endsWith('`')) || (strField.startsWith("'") && strField.endsWith("'")) || (strField.startsWith('"') && strField.endsWith('"')); }; const submittedValue = this._submittedValue.startsWith(eqSymbol) ? this._submittedValue.slice(eqSymbol.length) : this._submittedValue; if (!inQuotes(submittedValue) && inQuotes(modField)) modField = modField.substring(1, modField.length - 1); return eqSymbol + modField; }; @computed get defaultCellContent() { const { color, textDecoration, fieldProps, pointerEvents } = SchemaTableCell.renderProps(this._props); return (
this._props.highlightCells(this.adjustSelfReference(text))} getCells={(text: string) => this._props.eqHighlightFunc(this.adjustSelfReference(text))} ref={r => selectedCell(this._props) && this._props.autoFocus && r?.setIsFocused(true)} oneLine={this._props.oneLine} contents={undefined} fieldContents={fieldProps} editing={selectedCell(this._props) ? undefined : false} GetValue={() => this.cleanupField(Field.toKeyValueString(fieldProps.Document, this._props.fieldKey, SnappingManager.MetaKey))} SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value); this._props.finishEdit?.(); return true; } const ret = Doc.SetField(fieldProps.Document, this._props.fieldKey, value); this._submittedValue = value; this._props.finishEdit?.(); return ret; }, 'edit schema cell')} />
); } get getCellType() { const columnTypeStr = this._props.getFinfo(this._props.fieldKey)?.fieldType; const cellValue = this._props.Doc[this._props.fieldKey]; if (cellValue instanceof ImageField) return ColumnType.Image; if (cellValue instanceof DateField) return ColumnType.Date; if (cellValue instanceof RichTextField) return ColumnType.RTF; if (typeof cellValue === 'number') return ColumnType.Any; if (typeof cellValue === 'string' && columnTypeStr !== FInfoFieldType.enumeration) return ColumnType.Any; if (typeof cellValue === 'boolean') return ColumnType.Boolean; return columnTypeStr ? FInfotoColType[columnTypeStr] : ColumnType.Any; } get content() { // prettier-ignore switch (this.getCellType) { case ColumnType.Image: return ; case ColumnType.Boolean: return ; case ColumnType.RTF: return ; case ColumnType.Enumeration: return Field.toString(val))} />; case ColumnType.Date: return ; default: return this.defaultCellContent; } } @computed get borderColor() { const sides: Array = []; sides[0] = selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // left sides[1] = selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // right sides[2] = !this._props.isolatedSelection(this._props.Doc)[0] && selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // top sides[3] = !this._props.isolatedSelection(this._props.Doc)[1] && selectedCell(this._props) ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined; // bottom return sides; } render() { return (
StopEvent(e)} onPointerDown={action(e => { if (this.lockedInteraction) { e.stopPropagation(); e.preventDefault(); return; } if (this._props.refSelectModeInfo.enabled && !selectedCell(this._props)) { e.stopPropagation(); e.preventDefault(); this._props.selectReference(this._props.Doc, this._props.col); return; } const shift: boolean = e.shiftKey; const ctrl: boolean = e.ctrlKey; if (this._props.isRowActive?.()) { if (selectedCell(this._props) && ctrl) { this._props.selectCell(this._props.Doc, this._props.col, shift, ctrl); e.stopPropagation(); } else !selectedCell(this._props) && this._props.selectCell(this._props.Doc, this._props.col, shift, ctrl); } })} style={{ padding: this._props.padding, maxWidth: this._props.maxWidth?.(), width: this._props.columnWidth() || undefined, borderLeft: this.borderColor[0], borderRight: this.borderColor[1], borderTop: this.borderColor[2], borderBottom: this.borderColor[3], backgroundColor: this.backgroundColor, }}> {this.isDefault ? '' : this.content}
); } } // mj: most of this is adapted from old schema code so I'm not sure what it does tbh @observer export class SchemaImageCell extends ObservableReactComponent { constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } @observable _previewRef: HTMLImageElement | undefined = undefined; choosePath(url: URL) { if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href if (url.href.indexOf(window.location.origin) === -1) return ClientUtils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; // Why is this here — good question const ext = extname(url.href); return url.href.replace(ext, '_s' + ext); } get url() { const field = Cast(this._props.Doc[this._props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this._props.Doc[this._props.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images const altpaths = alts .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) .filter(url => url) .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; // If there is a path, follow it; otherwise, follow a link to a default image icon const url = paths.length ? paths : [ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')]; return url[0]; } @action showHoverPreview = (e: React.PointerEvent) => { this._previewRef = document.createElement('img'); document.body.appendChild(this._previewRef); const ext = extname(this.url); this._previewRef.src = this.url.replace('_s' + ext, '_m' + ext); this._previewRef.style.position = 'absolute'; this._previewRef.style.left = e.clientX + 10 + 'px'; this._previewRef.style.top = e.clientY + 10 + 'px'; this._previewRef.style.zIndex = '1000'; }; @action moveHoverPreview = (e: React.PointerEvent) => { if (!this._previewRef) return; this._previewRef.style.left = e.clientX + 10 + 'px'; this._previewRef.style.top = e.clientY + 10 + 'px'; }; @action removeHoverPreview = () => { if (!this._previewRef) return; document.body.removeChild(this._previewRef); }; render() { const aspect = Doc.NativeAspect(this._props.Doc); // aspect ratio // let width = Math.max(75, this._props.columnWidth); // get a with that is no smaller than 75px // const height = Math.max(75, width / aspect); // get a height either proportional to that or 75 px const height = this._props.rowHeight() ? this._props.rowHeight() - (this._props.padding || 6) * 2 : undefined; const width = height ? height * aspect : undefined; // increase the width of the image if necessary to maintain proportionality return ; } } @observer export class SchemaDateCell extends ObservableReactComponent { constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } @observable _pickingDate: boolean = false; @computed get date(): DateField { // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined. return DateCast(this._props.Doc[this._props.fieldKey]); } handleChange = undoable((date: Date | null) => { // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); // if (script.compiled) { // this.applyToDoc(this._document, this._props.row, this._props.col, script.run); // } else { // ^ DateCast is always undefined for some reason, but that is what the field should be set to date && (this._props.Doc[this._props.fieldKey] = new DateField(date)); // } }, 'date change'); render() { const { pointerEvents } = SchemaTableCell.renderProps(this._props); return ( <>
{pointerEvents === 'none' || !selectedCell(this._props) ? null : ( } size={Size.XSMALL} type={Type.TERT} color={SnappingManager.userColor} background={SnappingManager.userBackgroundColor} popup={
} /> )} ); } } @observer export class SchemaRTFCell extends ObservableReactComponent { constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } // if the text box blurs and none of its contents are focused(), then the edit finishes selectedFunc = () => !!selectedCell(this._props); render() { const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); fieldProps.isContentActive = this.selectedFunc; return (
{selectedCell(this._props) ? this._props.finishEdit?.()} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
); } } @observer export class SchemaBoolCell extends ObservableReactComponent { constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } render() { const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); return (
e.stopPropagation()} style={{ marginRight: 4 }} type="checkbox" checked={BoolCast(this._props.Doc[this._props.fieldKey])} onChange={undoable((value: React.ChangeEvent | undefined) => { if ((value?.nativeEvent as MouseEvent | PointerEvent).shiftKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); } else Doc.SetField(this._props.Doc, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); }, 'set bool cell')} /> Field.toKeyValueString(this._props.Doc, this._props.fieldKey)} SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value); this._props.finishEdit?.(); return true; } const set = Doc.SetField(this._props.Doc, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Doc) ? true : undefined); this._props.finishEdit?.(); return set; }, 'set bool cell')} />
); } } @observer export class SchemaEnumerationCell extends ObservableReactComponent { constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } render() { const { color, textDecoration, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); const options = this._props.options?.map(facet => ({ value: facet, label: facet })); return (