UNPKG

tinybase

Version:

A reactive data store and sync engine.

801 lines (792 loc) 21.1 kB
import { CellView, ResultCellView, ValueView, useCell, useIndexesOrIndexesById, useRelationshipsOrRelationshipsById, useRemoteRowId, useResultRowCount, useResultRowIds, useResultSortedRowIds, useResultTableCellIds, useRowCount, useRowIds, useSetCellCallback, useSetValueCallback, useSliceRowIds, useSortedRowIds, useStoreOrStoreById, useTableCellIds, useValue, useValueIds, } from '../ui-react/index.js'; import React from 'react'; import {Fragment, jsx, jsxs} from 'react/jsx-runtime'; const getTypeOf = (thing) => typeof thing; const EMPTY_STRING = ''; const STRING = getTypeOf(EMPTY_STRING); const BOOLEAN = getTypeOf(true); const NUMBER = getTypeOf(0); const CELL = 'Cell'; const VALUE = 'Value'; const CURRENT_TARGET = 'currentTarget'; const _VALUE = 'value'; const strSplit = (str, separator = EMPTY_STRING, limit) => str.split(separator, limit); const math = Math; const mathMin = math.min; const isFiniteNumber = isFinite; const isUndefined = (thing) => thing == void 0; const isTypeStringOrBoolean = (type) => type == STRING || type == BOOLEAN; const isString = (thing) => getTypeOf(thing) == STRING; const isArray = (thing) => Array.isArray(thing); const arrayMap = (array, cb) => array.map(cb); const getCellOrValueType = (cellOrValue) => { const type = getTypeOf(cellOrValue); return isTypeStringOrBoolean(type) || (type == NUMBER && isFiniteNumber(cellOrValue)) ? type : void 0; }; const getTypeCase = (type, stringCase, numberCase, booleanCase) => type == STRING ? stringCase : type == NUMBER ? numberCase : booleanCase; const object = Object; const objEntries = object.entries; const objNew = (entries = []) => object.fromEntries(entries); const objToArray = (obj, cb) => arrayMap(objEntries(obj), ([id, value]) => cb(value, id)); const objMap = (obj, cb) => objNew(objToArray(obj, (value, id) => [id, cb(value, id)])); const { PureComponent, createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } = React; const getProps = (getProps2, ...ids) => isUndefined(getProps2) ? {} : getProps2(...ids); const getRelationshipsStoreTableIds = (relationships, relationshipId) => [ relationships, relationships?.getStore(), relationships?.getLocalTableId(relationshipId), relationships?.getRemoteTableId(relationshipId), ]; const getIndexStoreTableId = (indexes, indexId) => [ indexes, indexes?.getStore(), indexes?.getTableId(indexId), ]; const DOT = '.'; const EDITABLE = 'editable'; const LEFT_ARROW = '\u2190'; const UP_ARROW = '\u2191'; const RIGHT_ARROW = '\u2192'; const DOWN_ARROW = '\u2193'; const useDottedCellIds = (tableId, store) => arrayMap(useTableCellIds(tableId, store), (cellId) => tableId + DOT + cellId); const useCallbackOrUndefined = (callback, deps, test) => { const returnCallback = useCallback(callback, deps); return test ? returnCallback : void 0; }; const useParams = (...args) => useMemo( () => args, // eslint-disable-next-line react-hooks/exhaustive-deps args, ); const useStoreCellComponentProps = (store, tableId) => useMemo(() => ({store, tableId}), [store, tableId]); const useQueriesCellComponentProps = (queries, queryId) => useMemo(() => ({queries, queryId}), [queries, queryId]); const useSortingAndPagination = ( cellId, descending = false, sortOnClick, offset = 0, limit, total, paginator, onChange, ) => { const [[currentCellId, currentDescending, currentOffset], setState] = useState([cellId, descending, offset]); const setStateAndChange = useCallback( (sortAndOffset) => { setState(sortAndOffset); onChange?.(sortAndOffset); }, [onChange], ); const handleSort = useCallbackOrUndefined( (cellId2) => setStateAndChange([ cellId2, cellId2 == currentCellId ? !currentDescending : false, currentOffset, ]), [setStateAndChange, currentCellId, currentDescending, currentOffset], sortOnClick, ); const handleChangeOffset = useCallback( (offset2) => setStateAndChange([currentCellId, currentDescending, offset2]), [setStateAndChange, currentCellId, currentDescending], ); const PaginatorComponent = paginator === true ? SortedTablePaginator : paginator; return [ [currentCellId, currentDescending, currentOffset], handleSort, useMemo( () => paginator === false ? null : /* @__PURE__ */ jsx(PaginatorComponent, { offset: currentOffset, limit, total, onChange: handleChangeOffset, }), [ paginator, PaginatorComponent, currentOffset, limit, total, handleChangeOffset, ], ), ]; }; const useCells = (defaultCellIds, customCells, defaultCellComponent) => useMemo(() => { const cellIds = customCells ?? defaultCellIds; return objMap( isArray(cellIds) ? objNew(arrayMap(cellIds, (cellId) => [cellId, cellId])) : cellIds, (labelOrCustomCell, cellId) => ({ ...{label: cellId, component: defaultCellComponent}, ...(isString(labelOrCustomCell) ? {label: labelOrCustomCell} : labelOrCustomCell), }), ); }, [customCells, defaultCellComponent, defaultCellIds]); const HtmlTable = ({ className, headerRow, idColumn, params: [ cells, cellComponentProps, rowIds, sortAndOffset, handleSort, paginatorComponent, ], }) => /* @__PURE__ */ jsxs('table', { className, children: [ paginatorComponent ? /* @__PURE__ */ jsx('caption', {children: paginatorComponent}) : null, headerRow === false ? null : /* @__PURE__ */ jsx('thead', { children: /* @__PURE__ */ jsxs('tr', { children: [ idColumn === false ? null : /* @__PURE__ */ jsx(HtmlHeaderCell, { sort: sortAndOffset ?? [], label: 'Id', onClick: handleSort, }), objToArray(cells, ({label}, cellId) => /* @__PURE__ */ jsx( HtmlHeaderCell, { cellId, label, sort: sortAndOffset ?? [], onClick: handleSort, }, cellId, ), ), ], }), }), /* @__PURE__ */ jsx('tbody', { children: arrayMap(rowIds, (rowId) => /* @__PURE__ */ jsxs( 'tr', { children: [ idColumn === false ? null : /* @__PURE__ */ jsx('th', {children: rowId}), objToArray( cells, ({component: CellView2, getComponentProps}, cellId) => /* @__PURE__ */ jsx( 'td', { children: /* @__PURE__ */ jsx(CellView2, { ...getProps(getComponentProps, rowId, cellId), ...cellComponentProps, rowId, cellId, }), }, cellId, ), ), ], }, rowId, ), ), }), ], }); const HtmlHeaderCell = ({ cellId, sort: [sortCellId, sortDescending], label = cellId ?? EMPTY_STRING, onClick, }) => /* @__PURE__ */ jsxs('th', { onClick: useCallbackOrUndefined( () => onClick?.(cellId), [onClick, cellId], onClick, ), className: isUndefined(sortDescending) || sortCellId != cellId ? void 0 : `sorted ${sortDescending ? 'de' : 'a'}scending`, children: [ isUndefined(sortDescending) || sortCellId != cellId ? null : (sortDescending ? DOWN_ARROW : UP_ARROW) + ' ', label, ], }); const RelationshipInHtmlRow = ({ localRowId, params: [ idColumn, cells, localTableId, remoteTableId, relationshipId, relationships, store, ], }) => { const remoteRowId = useRemoteRowId(relationshipId, localRowId, relationships); return /* @__PURE__ */ jsxs('tr', { children: [ idColumn === false ? null : /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx('th', {children: localRowId}), /* @__PURE__ */ jsx('th', {children: remoteRowId}), ], }), objToArray( cells, ({component: CellView2, getComponentProps}, compoundCellId) => { const [tableId, cellId] = strSplit(compoundCellId, DOT, 2); const rowId = tableId === localTableId ? localRowId : tableId === remoteTableId ? remoteRowId : null; return isUndefined(rowId) ? null : /* @__PURE__ */ jsx( 'td', { children: /* @__PURE__ */ jsx(CellView2, { ...getProps(getComponentProps, rowId, cellId), store, tableId, rowId, cellId, }), }, compoundCellId, ); }, ), ], }); }; const EditableThing = ({ thing, onThingChange, className, hasSchema, showType = true, }) => { const [thingType, setThingType] = useState(); const [currentThing, setCurrentThing] = useState(); const [stringThing, setStringThing] = useState(); const [numberThing, setNumberThing] = useState(); const [booleanThing, setBooleanThing] = useState(); if (currentThing !== thing) { setThingType(getCellOrValueType(thing)); setCurrentThing(thing); setStringThing(String(thing)); setNumberThing(Number(thing) || 0); setBooleanThing(Boolean(thing)); } const handleThingChange = useCallback( (thing2, setTypedThing) => { setTypedThing(thing2); setCurrentThing(thing2); onThingChange(thing2); }, [onThingChange], ); const handleTypeChange = useCallback(() => { if (!hasSchema?.()) { const nextType = getTypeCase(thingType, NUMBER, BOOLEAN, STRING); const thing2 = getTypeCase( nextType, stringThing, numberThing, booleanThing, ); setThingType(nextType); setCurrentThing(thing2); onThingChange(thing2); } }, [ hasSchema, onThingChange, stringThing, numberThing, booleanThing, thingType, ]); return /* @__PURE__ */ jsxs('div', { className, children: [ showType ? /* @__PURE__ */ jsx('button', { className: thingType, onClick: handleTypeChange, children: thingType, }) : null, getTypeCase( thingType, /* @__PURE__ */ jsx( 'input', { value: stringThing, onChange: useCallback( (event) => handleThingChange( String(event[CURRENT_TARGET][_VALUE]), setStringThing, ), [handleThingChange], ), }, thingType, ), /* @__PURE__ */ jsx( 'input', { type: 'number', value: numberThing, onChange: useCallback( (event) => handleThingChange( Number(event[CURRENT_TARGET][_VALUE] || 0), setNumberThing, ), [handleThingChange], ), }, thingType, ), /* @__PURE__ */ jsx( 'input', { type: 'checkbox', checked: booleanThing, onChange: useCallback( (event) => handleThingChange( Boolean(event[CURRENT_TARGET].checked), setBooleanThing, ), [handleThingChange], ), }, thingType, ), ), ], }); }; const TableInHtmlTable = ({tableId, store, editable, customCells, ...props}) => /* @__PURE__ */ jsx(HtmlTable, { ...props, params: useParams( useCells( useTableCellIds(tableId, store), customCells, editable ? EditableCellView : CellView, ), useStoreCellComponentProps(store, tableId), useRowIds(tableId, store), ), }); const SortedTableInHtmlTable = ({ tableId, cellId, descending, offset, limit, store, editable, sortOnClick, paginator = false, onChange, customCells, ...props }) => { const [sortAndOffset, handleSort, paginatorComponent] = useSortingAndPagination( cellId, descending, sortOnClick, offset, limit, useRowCount(tableId, store), paginator, onChange, ); return /* @__PURE__ */ jsx(HtmlTable, { ...props, params: useParams( useCells( useTableCellIds(tableId, store), customCells, editable ? EditableCellView : CellView, ), useStoreCellComponentProps(store, tableId), useSortedRowIds(tableId, ...sortAndOffset, limit, store), sortAndOffset, handleSort, paginatorComponent, ), }); }; const ValuesInHtmlTable = ({ store, editable = false, valueComponent: Value = editable ? EditableValueView : ValueView, getValueComponentProps, className, headerRow, idColumn, }) => /* @__PURE__ */ jsxs('table', { className, children: [ headerRow === false ? null : /* @__PURE__ */ jsx('thead', { children: /* @__PURE__ */ jsxs('tr', { children: [ idColumn === false ? null : /* @__PURE__ */ jsx('th', {children: 'Id'}), /* @__PURE__ */ jsx('th', {children: VALUE}), ], }), }), /* @__PURE__ */ jsx('tbody', { children: arrayMap(useValueIds(store), (valueId) => /* @__PURE__ */ jsxs( 'tr', { children: [ idColumn === false ? null : /* @__PURE__ */ jsx('th', {children: valueId}), /* @__PURE__ */ jsx('td', { children: /* @__PURE__ */ jsx(Value, { ...getProps(getValueComponentProps, valueId), valueId, store, }), }), ], }, valueId, ), ), }), ], }); const SliceInHtmlTable = ({ indexId, sliceId, indexes, editable, customCells, ...props }) => { const [resolvedIndexes, store, tableId] = getIndexStoreTableId( useIndexesOrIndexesById(indexes), indexId, ); return /* @__PURE__ */ jsx(HtmlTable, { ...props, params: useParams( useCells( useTableCellIds(tableId, store), customCells, editable ? EditableCellView : CellView, ), useStoreCellComponentProps(store, tableId), useSliceRowIds(indexId, sliceId, resolvedIndexes), ), }); }; const RelationshipInHtmlTable = ({ relationshipId, relationships, editable, customCells, className, headerRow, idColumn = true, }) => { const [resolvedRelationships, store, localTableId, remoteTableId] = getRelationshipsStoreTableIds( useRelationshipsOrRelationshipsById(relationships), relationshipId, ); const cells = useCells( [ ...useDottedCellIds(localTableId, store), ...useDottedCellIds(remoteTableId, store), ], customCells, editable ? EditableCellView : CellView, ); const params = useParams( idColumn, cells, localTableId, remoteTableId, relationshipId, resolvedRelationships, store, ); return /* @__PURE__ */ jsxs('table', { className, children: [ headerRow === false ? null : /* @__PURE__ */ jsx('thead', { children: /* @__PURE__ */ jsxs('tr', { children: [ idColumn === false ? null : /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs('th', { children: [localTableId, '.Id'], }), /* @__PURE__ */ jsxs('th', { children: [remoteTableId, '.Id'], }), ], }), objToArray(cells, ({label}, cellId) => /* @__PURE__ */ jsx('th', {children: label}, cellId), ), ], }), }), /* @__PURE__ */ jsx('tbody', { children: arrayMap(useRowIds(localTableId, store), (localRowId) => /* @__PURE__ */ jsx( RelationshipInHtmlRow, { localRowId, params, }, localRowId, ), ), }), ], }); }; const ResultTableInHtmlTable = ({queryId, queries, customCells, ...props}) => /* @__PURE__ */ jsx(HtmlTable, { ...props, params: useParams( useCells( useResultTableCellIds(queryId, queries), customCells, ResultCellView, ), useQueriesCellComponentProps(queries, queryId), useResultRowIds(queryId, queries), ), }); const ResultSortedTableInHtmlTable = ({ queryId, cellId, descending, offset, limit, queries, sortOnClick, paginator = false, customCells, onChange, ...props }) => { const [sortAndOffset, handleSort, paginatorComponent] = useSortingAndPagination( cellId, descending, sortOnClick, offset, limit, useResultRowCount(queryId, queries), paginator, onChange, ); return /* @__PURE__ */ jsx(HtmlTable, { ...props, params: useParams( useCells( useResultTableCellIds(queryId, queries), customCells, ResultCellView, ), useQueriesCellComponentProps(queries, queryId), useResultSortedRowIds(queryId, ...sortAndOffset, limit, queries), sortAndOffset, handleSort, paginatorComponent, ), }); }; const EditableCellView = ({ tableId, rowId, cellId, store, className, showType, }) => /* @__PURE__ */ jsx(EditableThing, { thing: useCell(tableId, rowId, cellId, store), onThingChange: useSetCellCallback( tableId, rowId, cellId, (cell) => cell, [], store, ), className: className ?? EDITABLE + CELL, showType, hasSchema: useStoreOrStoreById(store)?.hasTablesSchema, }); const EditableValueView = ({valueId, store, className, showType}) => /* @__PURE__ */ jsx(EditableThing, { thing: useValue(valueId, store), onThingChange: useSetValueCallback(valueId, (value) => value, [], store), className: className ?? EDITABLE + VALUE, showType, hasSchema: useStoreOrStoreById(store)?.hasValuesSchema, }); const SortedTablePaginator = ({ onChange, total, offset = 0, limit = total, singular = 'row', plural = singular + 's', }) => { if (offset > total || offset < 0) { offset = 0; onChange(0); } const handlePrevClick = useCallbackOrUndefined( () => onChange(offset - limit), [onChange, offset, limit], offset > 0, ); const handleNextClick = useCallbackOrUndefined( () => onChange(offset + limit), [onChange, offset, limit], offset + limit < total, ); return /* @__PURE__ */ jsxs(Fragment, { children: [ total > limit && /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx('button', { className: 'previous', disabled: offset == 0, onClick: handlePrevClick, children: LEFT_ARROW, }), /* @__PURE__ */ jsx('button', { className: 'next', disabled: offset + limit >= total, onClick: handleNextClick, children: RIGHT_ARROW, }), offset + 1, ' to ', mathMin(total, offset + limit), ' of ', ], }), total, ' ', total != 1 ? plural : singular, ], }); }; export { EditableCellView, EditableValueView, RelationshipInHtmlTable, ResultSortedTableInHtmlTable, ResultTableInHtmlTable, SliceInHtmlTable, SortedTableInHtmlTable, SortedTablePaginator, TableInHtmlTable, ValuesInHtmlTable, };