tinybase
Version:
A reactive data store and sync engine.
801 lines (792 loc) • 21.1 kB
JavaScript
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,
};