xontable 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +29 -0
- package/dist/XOnTable.d.ts +4 -0
- package/dist/XOnTable.d.ts.map +1 -0
- package/dist/XOnTable.js +182 -0
- package/dist/components/ColumnFilterMenu.d.ts +13 -0
- package/dist/components/ColumnFilterMenu.d.ts.map +1 -0
- package/dist/components/ColumnFilterMenu.js +7 -0
- package/dist/components/SelectMenu.d.ts +15 -0
- package/dist/components/SelectMenu.d.ts.map +1 -0
- package/dist/components/SelectMenu.js +38 -0
- package/dist/components/XOnTableGrid.d.ts +62 -0
- package/dist/components/XOnTableGrid.d.ts.map +1 -0
- package/dist/components/XOnTableGrid.js +42 -0
- package/dist/components/XOnTableHeader.d.ts +34 -0
- package/dist/components/XOnTableHeader.d.ts.map +1 -0
- package/dist/components/XOnTableHeader.js +7 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +4 -0
- package/dist/hooks/index.d.ts +14 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +13 -0
- package/dist/hooks/useAutoRows.d.ts +9 -0
- package/dist/hooks/useAutoRows.d.ts.map +1 -0
- package/dist/hooks/useAutoRows.js +41 -0
- package/dist/hooks/useClipboardCatcher.d.ts +15 -0
- package/dist/hooks/useClipboardCatcher.d.ts.map +1 -0
- package/dist/hooks/useClipboardCatcher.js +29 -0
- package/dist/hooks/useColumnFilters.d.ts +17 -0
- package/dist/hooks/useColumnFilters.d.ts.map +1 -0
- package/dist/hooks/useColumnFilters.js +95 -0
- package/dist/hooks/useColumnGroups.d.ts +27 -0
- package/dist/hooks/useColumnGroups.d.ts.map +1 -0
- package/dist/hooks/useColumnGroups.js +89 -0
- package/dist/hooks/useColumnResize.d.ts +12 -0
- package/dist/hooks/useColumnResize.d.ts.map +1 -0
- package/dist/hooks/useColumnResize.js +27 -0
- package/dist/hooks/useEditorOverlay.d.ts +24 -0
- package/dist/hooks/useEditorOverlay.d.ts.map +1 -0
- package/dist/hooks/useEditorOverlay.js +74 -0
- package/dist/hooks/useFillHandle.d.ts +16 -0
- package/dist/hooks/useFillHandle.d.ts.map +1 -0
- package/dist/hooks/useFillHandle.js +52 -0
- package/dist/hooks/useGridKeydown.d.ts +18 -0
- package/dist/hooks/useGridKeydown.d.ts.map +1 -0
- package/dist/hooks/useGridKeydown.js +109 -0
- package/dist/hooks/useOutsideClick.d.ts +7 -0
- package/dist/hooks/useOutsideClick.d.ts.map +1 -0
- package/dist/hooks/useOutsideClick.js +26 -0
- package/dist/hooks/useRangeSelection.d.ts +23 -0
- package/dist/hooks/useRangeSelection.d.ts.map +1 -0
- package/dist/hooks/useRangeSelection.js +48 -0
- package/dist/hooks/useSelectOptions.d.ts +13 -0
- package/dist/hooks/useSelectOptions.d.ts.map +1 -0
- package/dist/hooks/useSelectOptions.js +35 -0
- package/dist/hooks/useTableModel.d.ts +31 -0
- package/dist/hooks/useTableModel.d.ts.map +1 -0
- package/dist/hooks/useTableModel.js +135 -0
- package/dist/hooks/useValidation.d.ts +9 -0
- package/dist/hooks/useValidation.d.ts.map +1 -0
- package/dist/hooks/useValidation.js +42 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/utils/cellKey.d.ts +2 -0
- package/dist/utils/cellKey.d.ts.map +1 -0
- package/dist/utils/cellKey.js +3 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/tsv.d.ts +3 -0
- package/dist/utils/tsv.d.ts.map +1 -0
- package/dist/utils/tsv.js +9 -0
- package/package.json +36 -0
- package/src/styles/xontable.base.css +71 -0
- package/src/styles/xontable.css +3 -0
- package/src/styles/xontable.filter.css +65 -0
- package/src/styles/xontable.theme.css +42 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useClipboardCatcher.d.ts","sourceRoot":"","sources":["../../src/hooks/useClipboardCatcher.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAG5C,KAAK,uBAAuB,GAAG;IAC7B,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,MAAM,MAAM,EAAE,EAAE,CAAC;IAC/B,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,IAAI,CAAC;IAC1C,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,uBAAuB;;;gBAU5D,cAAc;iBAWd,cAAc;EAYrB"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { parseTSV, toTSV } from "../utils";
|
|
3
|
+
export function useClipboardCatcher(options) {
|
|
4
|
+
const { isEditing, getCopyBlock, onPasteBlock, onCopy: onCopyCb } = options;
|
|
5
|
+
const clipRef = useRef(null);
|
|
6
|
+
const focusClipboard = useCallback(() => {
|
|
7
|
+
clipRef.current?.focus();
|
|
8
|
+
clipRef.current?.select();
|
|
9
|
+
}, []);
|
|
10
|
+
const onCopy = useCallback((e) => {
|
|
11
|
+
if (isEditing)
|
|
12
|
+
return;
|
|
13
|
+
e.preventDefault();
|
|
14
|
+
const block = getCopyBlock();
|
|
15
|
+
e.clipboardData.setData("text/plain", toTSV(block));
|
|
16
|
+
onCopyCb?.();
|
|
17
|
+
}, [getCopyBlock, isEditing, onCopyCb]);
|
|
18
|
+
const onPaste = useCallback((e) => {
|
|
19
|
+
if (isEditing)
|
|
20
|
+
return;
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
const text = e.clipboardData.getData("text/plain");
|
|
23
|
+
const block = parseTSV(text);
|
|
24
|
+
if (block.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
onPasteBlock(block);
|
|
27
|
+
}, [isEditing, onPasteBlock]);
|
|
28
|
+
return { clipRef, focusClipboard, onCopy, onPaste };
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ColumnDef } from "../types";
|
|
2
|
+
type FiltersResult<Row extends Record<string, any>> = {
|
|
3
|
+
filterOpenKey: string | null;
|
|
4
|
+
filterSearch: string;
|
|
5
|
+
rowFilter: (row: Row) => boolean;
|
|
6
|
+
getFilterOptions: (key: string) => string[];
|
|
7
|
+
isFilterChecked: (key: string, value: string) => boolean;
|
|
8
|
+
isAllChecked: (key: string) => boolean;
|
|
9
|
+
openFilter: (key: string) => void;
|
|
10
|
+
closeFilter: () => void;
|
|
11
|
+
setFilterSearch: (value: string) => void;
|
|
12
|
+
toggleFilterValue: (key: string, value: string) => void;
|
|
13
|
+
toggleAll: (key: string) => void;
|
|
14
|
+
};
|
|
15
|
+
export declare function useColumnFilters<Row extends Record<string, any>>(columns: ColumnDef<Row>[], rows: Row[]): FiltersResult<Row>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=useColumnFilters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useColumnFilters.d.ts","sourceRoot":"","sources":["../../src/hooks/useColumnFilters.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,KAAK,aAAa,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI;IACpD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;IACjC,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IAC5C,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACzD,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,iBAAiB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,CAmF5H"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
export function useColumnFilters(columns, rows) {
|
|
3
|
+
const [filters, setFilters] = useState({});
|
|
4
|
+
const [filterOpenKey, setFilterOpenKey] = useState(null);
|
|
5
|
+
const [filterSearch, setFilterSearch] = useState("");
|
|
6
|
+
const rowFilter = useCallback((row) => {
|
|
7
|
+
for (const key of Object.keys(filters)) {
|
|
8
|
+
const set = filters[key];
|
|
9
|
+
if (!set)
|
|
10
|
+
continue;
|
|
11
|
+
if (set.size === 0)
|
|
12
|
+
return false;
|
|
13
|
+
const v = row[key];
|
|
14
|
+
const s = v == null ? "" : String(v);
|
|
15
|
+
if (!set.has(s))
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (filterOpenKey && filterSearch.trim() !== "") {
|
|
19
|
+
const v = row[filterOpenKey];
|
|
20
|
+
const s = v == null ? "" : String(v);
|
|
21
|
+
if (!s.toLowerCase().includes(filterSearch.toLowerCase()))
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}, [filterOpenKey, filterSearch, filters]);
|
|
26
|
+
const filterOptions = useMemo(() => {
|
|
27
|
+
const map = new Map();
|
|
28
|
+
columns.forEach((col) => {
|
|
29
|
+
const set = new Set();
|
|
30
|
+
rows.forEach((r) => set.add(String(r[col.key] ?? "")));
|
|
31
|
+
map.set(col.key, Array.from(set).sort());
|
|
32
|
+
});
|
|
33
|
+
return map;
|
|
34
|
+
}, [columns, rows]);
|
|
35
|
+
const getFilterOptions = useCallback((key) => {
|
|
36
|
+
const list = filterOptions.get(key) ?? [];
|
|
37
|
+
if (!filterSearch || filterOpenKey !== key)
|
|
38
|
+
return list;
|
|
39
|
+
const q = filterSearch.toLowerCase();
|
|
40
|
+
return list.filter((v) => v.toLowerCase().includes(q));
|
|
41
|
+
}, [filterOpenKey, filterOptions, filterSearch]);
|
|
42
|
+
const isFilterChecked = useCallback((key, value) => {
|
|
43
|
+
const set = filters[key];
|
|
44
|
+
return !set || set.has(value);
|
|
45
|
+
}, [filters]);
|
|
46
|
+
const isAllChecked = useCallback((key) => !filters[key], [filters]);
|
|
47
|
+
const toggleFilterValue = useCallback((key, value) => {
|
|
48
|
+
const all = filterOptions.get(key) ?? [];
|
|
49
|
+
setFilters((prev) => {
|
|
50
|
+
const cur = prev[key];
|
|
51
|
+
const next = cur ? new Set(cur) : new Set(all);
|
|
52
|
+
if (next.has(value))
|
|
53
|
+
next.delete(value);
|
|
54
|
+
else
|
|
55
|
+
next.add(value);
|
|
56
|
+
const out = { ...prev };
|
|
57
|
+
if (next.size === all.length)
|
|
58
|
+
delete out[key];
|
|
59
|
+
else
|
|
60
|
+
out[key] = next;
|
|
61
|
+
return out;
|
|
62
|
+
});
|
|
63
|
+
}, [filterOptions]);
|
|
64
|
+
const toggleAll = useCallback((key) => {
|
|
65
|
+
setFilters((prev) => {
|
|
66
|
+
const next = { ...prev };
|
|
67
|
+
if (next[key])
|
|
68
|
+
delete next[key];
|
|
69
|
+
else
|
|
70
|
+
next[key] = new Set();
|
|
71
|
+
return next;
|
|
72
|
+
});
|
|
73
|
+
}, []);
|
|
74
|
+
const openFilter = useCallback((key) => {
|
|
75
|
+
setFilterOpenKey((k) => (k === key ? null : key));
|
|
76
|
+
setFilterSearch("");
|
|
77
|
+
}, []);
|
|
78
|
+
const closeFilter = useCallback(() => {
|
|
79
|
+
setFilterOpenKey(null);
|
|
80
|
+
setFilterSearch("");
|
|
81
|
+
}, []);
|
|
82
|
+
return {
|
|
83
|
+
filterOpenKey,
|
|
84
|
+
filterSearch,
|
|
85
|
+
rowFilter,
|
|
86
|
+
getFilterOptions,
|
|
87
|
+
isFilterChecked,
|
|
88
|
+
isAllChecked,
|
|
89
|
+
openFilter,
|
|
90
|
+
closeFilter,
|
|
91
|
+
setFilterSearch,
|
|
92
|
+
toggleFilterValue,
|
|
93
|
+
toggleAll,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ColumnDef } from "../types";
|
|
2
|
+
type GroupHeader = {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
width: number;
|
|
6
|
+
collapsible: boolean;
|
|
7
|
+
collapsed: boolean;
|
|
8
|
+
};
|
|
9
|
+
type VisibleCol<Row extends Record<string, any>> = {
|
|
10
|
+
col: ColumnDef<Row>;
|
|
11
|
+
idx: number | null;
|
|
12
|
+
};
|
|
13
|
+
type ColumnGroupsOptions<Row extends Record<string, any>> = {
|
|
14
|
+
columns: ColumnDef<Row>[];
|
|
15
|
+
collapsedWidth?: number;
|
|
16
|
+
};
|
|
17
|
+
export declare function useColumnGroups<Row extends Record<string, any>>(options: ColumnGroupsOptions<Row>): {
|
|
18
|
+
visibleColumns: VisibleCol<Row>[];
|
|
19
|
+
groupHeaders: GroupHeader[];
|
|
20
|
+
getColWidth: (visibleIndex: number) => number;
|
|
21
|
+
setColWidth: (visibleIndex: number, w: number) => void;
|
|
22
|
+
toggleGroup: (key: string) => void;
|
|
23
|
+
resetWidths: () => void;
|
|
24
|
+
getOrigIndex: (visibleIndex: number) => number | null;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=useColumnGroups.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useColumnGroups.d.ts","sourceRoot":"","sources":["../../src/hooks/useColumnGroups.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,KAAK,WAAW,GAAG;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACpB,CAAC;AASF,KAAK,UAAU,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI;IAAE,GAAG,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE/F,KAAK,mBAAmB,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI;IAC1D,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,eAAe,CAAC,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7D,OAAO,EAAE,mBAAmB,CAAC,GAAG,CAAC;;;gCAwChB,MAAM;gCAUN,MAAM,KAAK,MAAM;uBA6B1B,MAAM;;iCAYG,MAAM;EAaxB"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
export function useColumnGroups(options) {
|
|
3
|
+
const { columns, collapsedWidth = 56 } = options;
|
|
4
|
+
const [collapsed, setCollapsed] = useState({});
|
|
5
|
+
const [colWidths, setColWidths] = useState(columns.map((c) => c.width ?? 140));
|
|
6
|
+
const groups = useMemo(() => {
|
|
7
|
+
const list = [];
|
|
8
|
+
let lastKey = "";
|
|
9
|
+
columns.forEach((col, idx) => {
|
|
10
|
+
const gKey = col.group ?? `__col_${idx}`;
|
|
11
|
+
const label = col.group ?? "";
|
|
12
|
+
const collapsible = col.group != null && col.groupCollapsible !== false;
|
|
13
|
+
if (list.length === 0 || lastKey !== gKey) {
|
|
14
|
+
list.push({ key: gKey, label, cols: [{ col, idx }], collapsible });
|
|
15
|
+
lastKey = gKey;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
list[list.length - 1].cols.push({ col, idx });
|
|
19
|
+
if (!collapsible)
|
|
20
|
+
list[list.length - 1].collapsible = false;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return list;
|
|
24
|
+
}, [columns]);
|
|
25
|
+
const visibleColumns = useMemo(() => {
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const g of groups) {
|
|
28
|
+
const isCollapsed = g.collapsible && collapsed[g.key];
|
|
29
|
+
if (!g.collapsible || !isCollapsed) {
|
|
30
|
+
g.cols.forEach(({ col, idx }) => out.push({ col, idx }));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
out.push({ col: { key: `__group_${g.key}`, label: "", editable: false }, idx: null });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}, [groups, collapsed]);
|
|
38
|
+
const getColWidth = useCallback((visibleIndex) => {
|
|
39
|
+
const v = visibleColumns[visibleIndex];
|
|
40
|
+
if (!v)
|
|
41
|
+
return 140;
|
|
42
|
+
if (v.idx == null)
|
|
43
|
+
return collapsedWidth;
|
|
44
|
+
return colWidths[v.idx] ?? 140;
|
|
45
|
+
}, [collapsedWidth, colWidths, visibleColumns]);
|
|
46
|
+
const setColWidth = useCallback((visibleIndex, w) => {
|
|
47
|
+
const v = visibleColumns[visibleIndex];
|
|
48
|
+
if (!v || v.idx == null)
|
|
49
|
+
return;
|
|
50
|
+
setColWidths((prev) => {
|
|
51
|
+
const next = [...prev];
|
|
52
|
+
next[v.idx] = w;
|
|
53
|
+
return next;
|
|
54
|
+
});
|
|
55
|
+
}, [visibleColumns]);
|
|
56
|
+
const groupHeaders = useMemo(() => {
|
|
57
|
+
return groups.map((g) => {
|
|
58
|
+
const isCollapsed = g.collapsible && collapsed[g.key];
|
|
59
|
+
const width = isCollapsed
|
|
60
|
+
? collapsedWidth
|
|
61
|
+
: g.cols.reduce((sum, { idx }) => sum + (colWidths[idx] ?? 140), 0);
|
|
62
|
+
return {
|
|
63
|
+
key: g.key,
|
|
64
|
+
label: g.label,
|
|
65
|
+
width,
|
|
66
|
+
collapsible: g.collapsible,
|
|
67
|
+
collapsed: Boolean(isCollapsed),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}, [collapsed, collapsedWidth, colWidths, groups]);
|
|
71
|
+
const toggleGroup = useCallback((key) => setCollapsed((p) => ({ ...p, [key]: !p[key] })), []);
|
|
72
|
+
const resetWidths = useCallback(() => {
|
|
73
|
+
setColWidths((prev) => {
|
|
74
|
+
if (!prev.length)
|
|
75
|
+
return columns.map((c) => c.width ?? 140);
|
|
76
|
+
return columns.map((c, i) => prev[i] ?? c.width ?? 140);
|
|
77
|
+
});
|
|
78
|
+
}, [columns]);
|
|
79
|
+
const getOrigIndex = useCallback((visibleIndex) => visibleColumns[visibleIndex]?.idx ?? null, [visibleColumns]);
|
|
80
|
+
return {
|
|
81
|
+
visibleColumns,
|
|
82
|
+
groupHeaders,
|
|
83
|
+
getColWidth,
|
|
84
|
+
setColWidth,
|
|
85
|
+
toggleGroup,
|
|
86
|
+
resetWidths,
|
|
87
|
+
getOrigIndex,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type ResizeOptions = {
|
|
2
|
+
colCount: number;
|
|
3
|
+
getWidth: (c: number) => number;
|
|
4
|
+
setWidth: (c: number, w: number) => void;
|
|
5
|
+
minWidth?: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function useColumnResize(options: ResizeOptions): {
|
|
8
|
+
startResize: (col: number, startX: number) => void;
|
|
9
|
+
isResizing: boolean;
|
|
10
|
+
};
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=useColumnResize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useColumnResize.d.ts","sourceRoot":"","sources":["../../src/hooks/useColumnResize.ts"],"names":[],"mappings":"AAIA,KAAK,aAAa,GAAG;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa;uBAK5C,MAAM,UAAU,MAAM;;EA2B/B"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
export function useColumnResize(options) {
|
|
3
|
+
const { colCount, getWidth, setWidth, minWidth = 60 } = options;
|
|
4
|
+
const [drag, setDrag] = useState(null);
|
|
5
|
+
const startResize = useCallback((col, startX) => {
|
|
6
|
+
if (col < 0 || col >= colCount)
|
|
7
|
+
return;
|
|
8
|
+
setDrag({ col, startX, startW: getWidth(col) });
|
|
9
|
+
}, [colCount, getWidth]);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!drag)
|
|
12
|
+
return;
|
|
13
|
+
const onMove = (e) => {
|
|
14
|
+
const dx = e.clientX - drag.startX;
|
|
15
|
+
const next = Math.max(minWidth, drag.startW + dx);
|
|
16
|
+
setWidth(drag.col, next);
|
|
17
|
+
};
|
|
18
|
+
const onUp = () => setDrag(null);
|
|
19
|
+
window.addEventListener("mousemove", onMove);
|
|
20
|
+
window.addEventListener("mouseup", onUp);
|
|
21
|
+
return () => {
|
|
22
|
+
window.removeEventListener("mousemove", onMove);
|
|
23
|
+
window.removeEventListener("mouseup", onUp);
|
|
24
|
+
};
|
|
25
|
+
}, [drag, minWidth, setWidth]);
|
|
26
|
+
return { startResize, isResizing: Boolean(drag) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { KeyboardEvent, RefObject } from "react";
|
|
2
|
+
import type { CellPos } from "../types";
|
|
3
|
+
type EditorOptions = {
|
|
4
|
+
active: CellPos;
|
|
5
|
+
activeCellRef: RefObject<HTMLDivElement | null>;
|
|
6
|
+
getValue: (r: number, c: number) => string;
|
|
7
|
+
onCommit: (value: string) => void;
|
|
8
|
+
onCancel?: () => void;
|
|
9
|
+
onTab?: (dir: number) => void;
|
|
10
|
+
onEnter?: (dir: number) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function useEditorOverlay(options: EditorOptions): {
|
|
13
|
+
isEditing: boolean;
|
|
14
|
+
draft: string;
|
|
15
|
+
setDraft: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
16
|
+
editorRect: DOMRect | null;
|
|
17
|
+
editorRef: RefObject<HTMLInputElement | null>;
|
|
18
|
+
startEdit: (initialValue?: string) => void;
|
|
19
|
+
commitEdit: (value?: string) => void;
|
|
20
|
+
cancelEdit: () => void;
|
|
21
|
+
onEditorKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
|
|
22
|
+
};
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=useEditorOverlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEditorOverlay.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorOverlay.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,KAAK,aAAa,GAAG;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa;;;;;;+BA2BnC,MAAM;yBAegB,MAAM;;yBAWxC,aAAa,CAAC,gBAAgB,CAAC;EA4BtC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
export function useEditorOverlay(options) {
|
|
3
|
+
const { active, activeCellRef, getValue, onCommit, onCancel, onTab, onEnter } = options;
|
|
4
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
5
|
+
const [draft, setDraft] = useState("");
|
|
6
|
+
const [editorRect, setEditorRect] = useState(null);
|
|
7
|
+
const [selectAll, setSelectAll] = useState(true);
|
|
8
|
+
const editorRef = useRef(null);
|
|
9
|
+
const measureRect = useCallback(() => {
|
|
10
|
+
const el = activeCellRef.current;
|
|
11
|
+
if (!el)
|
|
12
|
+
return;
|
|
13
|
+
setEditorRect(el.getBoundingClientRect());
|
|
14
|
+
requestAnimationFrame(() => {
|
|
15
|
+
const input = editorRef.current;
|
|
16
|
+
if (!input)
|
|
17
|
+
return;
|
|
18
|
+
input.focus();
|
|
19
|
+
if (selectAll) {
|
|
20
|
+
input.select();
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const len = input.value.length;
|
|
24
|
+
input.setSelectionRange(len, len);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}, [activeCellRef, selectAll]);
|
|
28
|
+
const startEdit = useCallback((initialValue) => {
|
|
29
|
+
const current = getValue(active.r, active.c);
|
|
30
|
+
setDraft(initialValue ?? current);
|
|
31
|
+
setSelectAll(initialValue == null);
|
|
32
|
+
setIsEditing(true);
|
|
33
|
+
requestAnimationFrame(measureRect);
|
|
34
|
+
}, [active, getValue, measureRect]);
|
|
35
|
+
const finish = useCallback(() => {
|
|
36
|
+
setIsEditing(false);
|
|
37
|
+
setEditorRect(null);
|
|
38
|
+
}, []);
|
|
39
|
+
const commitEdit = useCallback((value) => {
|
|
40
|
+
onCommit(value ?? draft);
|
|
41
|
+
finish();
|
|
42
|
+
}, [draft, finish, onCommit]);
|
|
43
|
+
const cancelEdit = useCallback(() => {
|
|
44
|
+
onCancel?.();
|
|
45
|
+
finish();
|
|
46
|
+
}, [finish, onCancel]);
|
|
47
|
+
const onEditorKeyDown = useCallback((e) => {
|
|
48
|
+
if (e.key === "Enter") {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
commitEdit();
|
|
51
|
+
onEnter?.(e.shiftKey ? -1 : 1);
|
|
52
|
+
}
|
|
53
|
+
else if (e.key === "Escape") {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
cancelEdit();
|
|
56
|
+
}
|
|
57
|
+
else if (e.key === "Tab") {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
commitEdit();
|
|
60
|
+
onTab?.(e.shiftKey ? -1 : 1);
|
|
61
|
+
}
|
|
62
|
+
}, [cancelEdit, commitEdit, onEnter, onTab]);
|
|
63
|
+
return {
|
|
64
|
+
isEditing,
|
|
65
|
+
draft,
|
|
66
|
+
setDraft,
|
|
67
|
+
editorRect,
|
|
68
|
+
editorRef,
|
|
69
|
+
startEdit,
|
|
70
|
+
commitEdit,
|
|
71
|
+
cancelEdit,
|
|
72
|
+
onEditorKeyDown,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type DragState = {
|
|
2
|
+
startR: number;
|
|
3
|
+
startC: number;
|
|
4
|
+
endR: number;
|
|
5
|
+
endC: number;
|
|
6
|
+
};
|
|
7
|
+
type FillHandleOptions = {
|
|
8
|
+
onApply: (startR: number, startC: number, endR: number, endC: number) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare function useFillHandle(options: FillHandleOptions): {
|
|
11
|
+
drag: DragState | null;
|
|
12
|
+
startDrag: (r: number, c: number) => void;
|
|
13
|
+
isPreview: (r: number, c: number) => boolean;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=useFillHandle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useFillHandle.d.ts","sourceRoot":"","sources":["../../src/hooks/useFillHandle.ts"],"names":[],"mappings":"AAEA,KAAK,SAAS,GAAG;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/E,CAAC;AAEF,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB;;mBAIpB,MAAM,KAAK,MAAM;mBAkC7C,MAAM,KAAK,MAAM;EAmBxB"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
export function useFillHandle(options) {
|
|
3
|
+
const { onApply } = options;
|
|
4
|
+
const [drag, setDrag] = useState(null);
|
|
5
|
+
const startDrag = useCallback((r, c) => {
|
|
6
|
+
setDrag({ startR: r, startC: c, endR: r, endC: c });
|
|
7
|
+
}, []);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (!drag)
|
|
10
|
+
return;
|
|
11
|
+
const onMove = (e) => {
|
|
12
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
13
|
+
const cell = el?.closest("[data-row][data-col]");
|
|
14
|
+
if (!cell)
|
|
15
|
+
return;
|
|
16
|
+
const r = Number(cell.dataset.row);
|
|
17
|
+
const c = Number(cell.dataset.col);
|
|
18
|
+
if (Number.isNaN(r) || Number.isNaN(c))
|
|
19
|
+
return;
|
|
20
|
+
setDrag((prev) => (prev ? { ...prev, endR: r, endC: c } : prev));
|
|
21
|
+
};
|
|
22
|
+
const onUp = () => {
|
|
23
|
+
onApply(drag.startR, drag.startC, drag.endR, drag.endC);
|
|
24
|
+
setDrag(null);
|
|
25
|
+
};
|
|
26
|
+
window.addEventListener("mousemove", onMove);
|
|
27
|
+
window.addEventListener("mouseup", onUp);
|
|
28
|
+
return () => {
|
|
29
|
+
window.removeEventListener("mousemove", onMove);
|
|
30
|
+
window.removeEventListener("mouseup", onUp);
|
|
31
|
+
};
|
|
32
|
+
}, [drag, onApply]);
|
|
33
|
+
const isPreview = useCallback((r, c) => {
|
|
34
|
+
if (!drag)
|
|
35
|
+
return false;
|
|
36
|
+
const dr = Math.abs(drag.endR - drag.startR);
|
|
37
|
+
const dc = Math.abs(drag.endC - drag.startC);
|
|
38
|
+
if (dc >= dr) {
|
|
39
|
+
if (r !== drag.startR)
|
|
40
|
+
return false;
|
|
41
|
+
const from = Math.min(drag.startC, drag.endC);
|
|
42
|
+
const to = Math.max(drag.startC, drag.endC);
|
|
43
|
+
return c >= from && c <= to;
|
|
44
|
+
}
|
|
45
|
+
if (c !== drag.startC)
|
|
46
|
+
return false;
|
|
47
|
+
const from = Math.min(drag.startR, drag.endR);
|
|
48
|
+
const to = Math.max(drag.startR, drag.endR);
|
|
49
|
+
return r >= from && r <= to;
|
|
50
|
+
}, [drag]);
|
|
51
|
+
return { drag, startDrag, isPreview };
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { KeyboardEvent } from "react";
|
|
2
|
+
import type { CellPos } from "../types";
|
|
3
|
+
type GridKeydownOptions = {
|
|
4
|
+
active: CellPos;
|
|
5
|
+
rowCount: number;
|
|
6
|
+
colCount: number;
|
|
7
|
+
isEditing: boolean;
|
|
8
|
+
moveActive: (dr: number, dc: number) => void;
|
|
9
|
+
moveTo: (r: number, c: number) => void;
|
|
10
|
+
startEdit: (initial?: string) => void;
|
|
11
|
+
clearCell: () => void;
|
|
12
|
+
undo: () => void;
|
|
13
|
+
redo: () => void;
|
|
14
|
+
onShiftMove?: (dr: number, dc: number) => void;
|
|
15
|
+
};
|
|
16
|
+
export declare function useGridKeydown(options: GridKeydownOptions): (e: KeyboardEvent<HTMLElement>) => void;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=useGridKeydown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGridKeydown.d.ts","sourceRoot":"","sources":["../../src/hooks/useGridKeydown.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,KAAK,kBAAkB,GAAG;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF,wBAAgB,cAAc,CAAC,OAAO,EAAE,kBAAkB,OAgBlD,aAAa,CAAC,WAAW,CAAC,UA0FjC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
export function useGridKeydown(options) {
|
|
3
|
+
const { active, rowCount, colCount, isEditing, moveActive, moveTo, startEdit, clearCell, undo, redo, onShiftMove, } = options;
|
|
4
|
+
return useCallback((e) => {
|
|
5
|
+
if (isEditing)
|
|
6
|
+
return;
|
|
7
|
+
const isCtrl = e.ctrlKey || e.metaKey;
|
|
8
|
+
const lastRow = Math.max(0, rowCount - 1);
|
|
9
|
+
const lastCol = Math.max(0, colCount - 1);
|
|
10
|
+
if (isCtrl && (e.key === "z" || e.key === "Z")) {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
if (e.shiftKey)
|
|
13
|
+
redo();
|
|
14
|
+
else
|
|
15
|
+
undo();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (isCtrl && (e.key === "y" || e.key === "Y")) {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
redo();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
}
|
|
26
|
+
switch (e.key) {
|
|
27
|
+
case "ArrowUp":
|
|
28
|
+
if (e.shiftKey && onShiftMove)
|
|
29
|
+
onShiftMove(-1, 0);
|
|
30
|
+
else if (isCtrl)
|
|
31
|
+
moveTo(0, active.c);
|
|
32
|
+
else
|
|
33
|
+
moveActive(-1, 0);
|
|
34
|
+
break;
|
|
35
|
+
case "ArrowDown":
|
|
36
|
+
if (e.shiftKey && onShiftMove)
|
|
37
|
+
onShiftMove(1, 0);
|
|
38
|
+
else if (isCtrl)
|
|
39
|
+
moveTo(lastRow, active.c);
|
|
40
|
+
else
|
|
41
|
+
moveActive(1, 0);
|
|
42
|
+
break;
|
|
43
|
+
case "ArrowLeft":
|
|
44
|
+
if (e.shiftKey && onShiftMove)
|
|
45
|
+
onShiftMove(0, -1);
|
|
46
|
+
else if (isCtrl)
|
|
47
|
+
moveTo(active.r, 0);
|
|
48
|
+
else
|
|
49
|
+
moveActive(0, -1);
|
|
50
|
+
break;
|
|
51
|
+
case "ArrowRight":
|
|
52
|
+
if (e.shiftKey && onShiftMove)
|
|
53
|
+
onShiftMove(0, 1);
|
|
54
|
+
else if (isCtrl)
|
|
55
|
+
moveTo(active.r, lastCol);
|
|
56
|
+
else
|
|
57
|
+
moveActive(0, 1);
|
|
58
|
+
break;
|
|
59
|
+
case "Tab":
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
moveActive(0, e.shiftKey ? -1 : 1);
|
|
62
|
+
break;
|
|
63
|
+
case "Enter":
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
startEdit();
|
|
66
|
+
break;
|
|
67
|
+
case "F2":
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
startEdit();
|
|
70
|
+
break;
|
|
71
|
+
case "Home":
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
if (isCtrl)
|
|
74
|
+
moveTo(0, 0);
|
|
75
|
+
else
|
|
76
|
+
moveTo(active.r, 0);
|
|
77
|
+
break;
|
|
78
|
+
case "End":
|
|
79
|
+
e.preventDefault();
|
|
80
|
+
if (isCtrl)
|
|
81
|
+
moveTo(lastRow, lastCol);
|
|
82
|
+
else
|
|
83
|
+
moveTo(active.r, lastCol);
|
|
84
|
+
break;
|
|
85
|
+
case "Backspace":
|
|
86
|
+
case "Delete":
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
clearCell();
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
startEdit(e.key);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}, [
|
|
98
|
+
active,
|
|
99
|
+
clearCell,
|
|
100
|
+
colCount,
|
|
101
|
+
isEditing,
|
|
102
|
+
moveActive,
|
|
103
|
+
moveTo,
|
|
104
|
+
redo,
|
|
105
|
+
rowCount,
|
|
106
|
+
startEdit,
|
|
107
|
+
undo,
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useOutsideClick.d.ts","sourceRoot":"","sources":["../../src/hooks/useOutsideClick.ts"],"names":[],"mappings":"AAEA,KAAK,mBAAmB,GAAG;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB,QAqB3D"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
export function useOutsideClick(options) {
|
|
3
|
+
const { isOpen, onClose } = options;
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
if (!isOpen)
|
|
6
|
+
return;
|
|
7
|
+
const onDoc = (e) => {
|
|
8
|
+
const target = e.target;
|
|
9
|
+
if (!target)
|
|
10
|
+
return;
|
|
11
|
+
if (target.closest(".xontable-filter-menu") || target.closest(".xontable-filter-btn"))
|
|
12
|
+
return;
|
|
13
|
+
onClose();
|
|
14
|
+
};
|
|
15
|
+
const onKey = (e) => {
|
|
16
|
+
if (e.key === "Escape")
|
|
17
|
+
onClose();
|
|
18
|
+
};
|
|
19
|
+
document.addEventListener("mousedown", onDoc);
|
|
20
|
+
document.addEventListener("keydown", onKey);
|
|
21
|
+
return () => {
|
|
22
|
+
document.removeEventListener("mousedown", onDoc);
|
|
23
|
+
document.removeEventListener("keydown", onKey);
|
|
24
|
+
};
|
|
25
|
+
}, [isOpen, onClose]);
|
|
26
|
+
}
|