diff options
| author | bobzel <zzzman@gmail.com> | 2023-06-14 09:12:13 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2023-06-14 09:12:13 -0400 |
| commit | 376270791c7fe414c05a87f73afe11146d119c35 (patch) | |
| tree | c6c788c958a5aaca4a9bbdd709d5e6f1d76dde0d /src/client/views/collections/collectionSchema/SchemaTableCell.tsx | |
| parent | 2bc89733ce522527c2f27203b537d99395c9479b (diff) | |
| parent | bf16eca7a84adfdf1c5970e7e4793568ee70325d (diff) | |
Merge branch 'master' into advanced-trails
Diffstat (limited to 'src/client/views/collections/collectionSchema/SchemaTableCell.tsx')
| -rw-r--r-- | src/client/views/collections/collectionSchema/SchemaTableCell.tsx | 337 |
1 files changed, 311 insertions, 26 deletions
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 5f8ffe8b0..97264508c 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -1,64 +1,349 @@ -import React = require('react'); +import * as React from 'react'; +import Select, { MenuPlacement } from 'react-select'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, Field } from '../../../../fields/Doc'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../Utils'; +import { extname } from 'path'; +import DatePicker from 'react-datepicker'; +import { DateField } from '../../../../fields/DateField'; +import { Doc, DocListCast, Field } from '../../../../fields/Doc'; +import { RichTextField } from '../../../../fields/RichTextField'; +import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero, Utils } from '../../../../Utils'; +import { FInfo } from '../../../documents/Documents'; +import { DocFocusOrOpen } from '../../../util/DocumentManager'; import { Transform } from '../../../util/Transform'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { EditableView } from '../../EditableView'; +import { Colors } from '../../global/globalEnums'; +import { OpenWhere } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { DefaultStyleProvider } from '../../StyleProvider'; -import { CollectionSchemaView } from './CollectionSchemaView'; +import { CollectionSchemaView, ColumnType, FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; export interface SchemaTableCellProps { Document: Doc; + col: number; + deselectCell: () => void; + selectCell: (doc: Doc, col: number) => void; + selectedCell: () => [Doc, number] | undefined; fieldKey: string; - columnWidth: number; + 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; } @observer export class SchemaTableCell extends React.Component<SchemaTableCellProps> { - render() { - const props: FieldViewProps = { - Document: this.props.Document, - docFilters: returnEmptyFilter, - docRangeFilters: returnEmptyFilter, + static addFieldDoc = (doc: Doc, where: OpenWhere) => { + DocFocusOrOpen(doc); + return true; + }; + public static renderProps(props: SchemaTableCellProps) { + const { Document, fieldKey, getFinfo, columnWidth, isRowActive } = props; + let protoCount = 0; + let doc: Doc | undefined = Document; + while (doc) { + if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) { + break; + } + protoCount++; + doc = DocCast(doc.proto); + } + const parenCount = Math.max(0, protoCount - 1); + const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; + const textDecoration = color !== 'black' && parenCount ? 'underline' : ''; + const fieldProps: FieldViewProps = { + childFilters: returnEmptyFilter, + childFiltersByRanges: returnEmptyFilter, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, docViewPath: returnEmptyDoclist, - fieldKey: this.props.fieldKey, rootSelected: returnFalse, isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: 'alias', + dropAction: 'embed', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, whenChildContentsActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, - PanelWidth: () => this.props.columnWidth, - PanelHeight: () => CollectionSchemaView._rowHeight, - addDocTab: returnFalse, + addDocTab: SchemaTableCell.addFieldDoc, pinToPres: returnZero, + Document, + fieldKey: fieldKey, + PanelWidth: columnWidth, + PanelHeight: props.rowHeight, }; + const readOnly = getFinfo(fieldKey)?.readOnly ?? false; + const cursor = !readOnly ? 'text' : 'default'; + const pointerEvents: 'all' | 'none' = !readOnly && isRowActive() ? 'all' : 'none'; + return { color, textDecoration, fieldProps, cursor, pointerEvents }; + } + + @computed get selected() { + const selected: [Doc, number] | undefined = this.props.selectedCell(); + return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col; + } + + @computed get defaultCellContent() { + const { color, textDecoration, fieldProps } = SchemaTableCell.renderProps(this.props); + + return ( + <div + className="schemacell-edit-wrapper" + style={{ + color, + textDecoration, + width: '100%', + }}> + <EditableView + oneLine={this.props.oneLine} + allowCRs={this.props.allowCRs} + contents={<FieldView {...fieldProps} />} + editing={this.selected ? undefined : false} + GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)} + SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => { + if (shiftDown && enterKey) { + this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value); + } + const ret = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + this.props.finishEdit?.(); + return ret; + }, 'edit schema cell')} + /> + </div> + ); + } + + get getCellType() { + const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType; + const cellValue = this.props.Document[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 !== 'enumeration') return ColumnType.Any; + if (typeof cellValue === 'boolean') return ColumnType.Boolean; + + if (columnTypeStr && columnTypeStr in FInfotoColType) { + return FInfotoColType[columnTypeStr]; + } + + return ColumnType.Any; + } + + get content() { + const cellType: ColumnType = this.getCellType; + // prettier-ignore + switch (cellType) { + case ColumnType.Image: return <SchemaImageCell {...this.props} />; + case ColumnType.Boolean: return <SchemaBoolCell {...this.props} />; + case ColumnType.RTF: return <SchemaRTFCell {...this.props} />; + case ColumnType.Enumeration: return <SchemaEnumerationCell {...this.props} options={this.props.getFinfo(this.props.fieldKey)?.values?.map(val => val.toString())} />; + case ColumnType.Date: // return <SchemaDateCell {...this.props} />; + default: return this.defaultCellContent; + } + } + + render() { + return ( + <div + className="schema-table-cell" + onPointerDown={action(e => !this.selected && this.props.selectCell(this.props.Document, this.props.col))} + style={{ padding: this.props.padding, maxWidth: this.props.maxWidth?.(), width: this.props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}> + {this.content} + </div> + ); + } +} + +// mj: most of this is adapted from old schema code so I'm not sure what it does tbh +@observer +export class SchemaImageCell extends React.Component<SchemaTableCellProps> { + @observable _previewRef: HTMLImageElement | 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 Utils.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.Document[this.props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc + const alts = DocListCast(this.props.Document[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 : [Utils.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 = (e: React.PointerEvent) => { + if (!this._previewRef) return; + document.body.removeChild(this._previewRef); + }; + render() { + const aspect = Doc.NativeAspect(this.props.Document); // 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 <img src={this.url} width={width} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />; + } +} + +@observer +export class SchemaDateCell extends React.Component<SchemaTableCellProps> { + @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.Document[this.props.fieldKey]); + } + + @action + handleChange = (date: any) => { + // 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 + this.props.Document[this.props.fieldKey] = new DateField(date as Date); + //} + }; + + render() { + return <DatePicker dateFormat={'Pp'} selected={this.date.date} onChange={(date: any) => this.handleChange(date)} />; + } +} +@observer +export class SchemaRTFCell extends React.Component<SchemaTableCellProps> { + @computed get selected() { + const selected: [Doc, number] | undefined = this.props.selectedCell(); + return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col; + } + selectedFunc = () => this.selected; + render() { + const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props); + fieldProps.isContentActive = this.selectedFunc; + return ( + <div className="schemaRTFCell" style={{ display: 'flex', fontStyle: this.selected ? undefined : 'italic', width: '100%', height: '100%', position: 'relative', color, textDecoration, cursor, pointerEvents }}> + {this.selected ? <FormattedTextBox allowScroll={true} {...fieldProps} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))} + </div> + ); + } +} +@observer +export class SchemaBoolCell extends React.Component<SchemaTableCellProps> { + @computed get selected() { + const selected: [Doc, number] | undefined = this.props.selectedCell(); + return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col; + } + render() { + const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props); + return ( + <div className="schemaBoolCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}> + <input + style={{ marginRight: 4 }} + type="checkbox" + checked={BoolCast(this.props.Document[this.props.fieldKey])} + onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => { + if ((value?.nativeEvent as any).shiftKey) { + this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); + } + KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString()); + })} + /> + <EditableView + contents={<FieldView {...fieldProps} />} + editing={this.selected ? undefined : false} + GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)} + SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => { + if (shiftDown && enterKey) { + this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value); + } + const set = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + this.props.finishEdit?.(); + return set; + })} + /> + </div> + ); + } +} +@observer +export class SchemaEnumerationCell extends React.Component<SchemaTableCellProps> { + @computed get selected() { + const selected: [Doc, number] | undefined = this.props.selectedCell(); + return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col; + } + render() { + const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props); + const options = this.props.options?.map(facet => ({ value: facet, label: facet })); return ( - <div className="schema-table-cell" style={{ width: this.props.columnWidth }}> - <div className="schemacell-edit-wrapper" style={this.props.isRowActive() ? { cursor: 'text', pointerEvents: 'auto' } : { cursor: 'default', pointerEvents: 'none' }}> - <EditableView - contents={<FieldView {...props} />} - GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)} - SetValue={(value: string, shiftDown?: boolean, enterKey?: boolean) => { - if (shiftDown && enterKey) { - this.props.setColumnValues(this.props.fieldKey, value); - } - return KeyValueBox.SetField(this.props.Document, this.props.fieldKey, value); + <div className="schemaSelectionCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}> + <div style={{ width: '100%' }}> + <Select + styles={{ + menuPortal: base => ({ + ...base, + left: 0, + top: 0, + transform: `translate(${this.props.transform().TranslateX}px, ${this.props.transform().TranslateY}px)`, + width: Number(base.width) * this.props.transform().Scale, + zIndex: 9999, + }), }} - editing={this.props.isRowActive() ? undefined : false} + menuPortalTarget={this.props.menuTarget} + menuPosition={'absolute'} + placeholder={StrCast(this.props.Document[this.props.fieldKey], 'select...')} + options={options} + isMulti={false} + onChange={val => KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)} /> </div> </div> |
