datool 0.0.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/README.md +218 -0
- package/client-dist/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/client-dist/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/client-dist/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/client-dist/assets/index-BeRNeRUq.css +1 -0
- package/client-dist/assets/index-uoZ4c_I8.js +164 -0
- package/client-dist/index.html +13 -0
- package/index.html +12 -0
- package/package.json +55 -0
- package/src/client/App.tsx +885 -0
- package/src/client/components/connection-status.tsx +43 -0
- package/src/client/components/data-table-cell.tsx +235 -0
- package/src/client/components/data-table-col-icon.tsx +73 -0
- package/src/client/components/data-table-header-col.tsx +225 -0
- package/src/client/components/data-table-search-input.tsx +729 -0
- package/src/client/components/data-table.tsx +2014 -0
- package/src/client/components/stream-controls.tsx +157 -0
- package/src/client/components/theme-provider.tsx +230 -0
- package/src/client/components/ui/button.tsx +68 -0
- package/src/client/components/ui/combobox.tsx +308 -0
- package/src/client/components/ui/context-menu.tsx +261 -0
- package/src/client/components/ui/dropdown-menu.tsx +267 -0
- package/src/client/components/ui/input-group.tsx +153 -0
- package/src/client/components/ui/input.tsx +19 -0
- package/src/client/components/ui/textarea.tsx +18 -0
- package/src/client/components/viewer-settings.tsx +185 -0
- package/src/client/index.css +192 -0
- package/src/client/lib/data-table-search.ts +750 -0
- package/src/client/lib/datool-icons.ts +37 -0
- package/src/client/lib/datool-url-state.ts +159 -0
- package/src/client/lib/filterable-table.ts +146 -0
- package/src/client/lib/table-search-persistence.ts +94 -0
- package/src/client/lib/utils.ts +6 -0
- package/src/client/main.tsx +14 -0
- package/src/index.ts +19 -0
- package/src/node/cli.ts +54 -0
- package/src/node/config.ts +231 -0
- package/src/node/lines.ts +82 -0
- package/src/node/runtime.ts +102 -0
- package/src/node/server.ts +403 -0
- package/src/node/sources/command.ts +82 -0
- package/src/node/sources/file.ts +116 -0
- package/src/node/sources/ssh.ts +59 -0
- package/src/shared/columns.ts +41 -0
- package/src/shared/types.ts +188 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Extension } from "@tiptap/core"
|
|
3
|
+
import Document from "@tiptap/extension-document"
|
|
4
|
+
import Paragraph from "@tiptap/extension-paragraph"
|
|
5
|
+
import Placeholder from "@tiptap/extension-placeholder"
|
|
6
|
+
import Text from "@tiptap/extension-text"
|
|
7
|
+
import { EditorContent, useEditor, type Editor } from "@tiptap/react"
|
|
8
|
+
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state"
|
|
9
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view"
|
|
10
|
+
import { LoaderCircle, Search, XIcon } from "lucide-react"
|
|
11
|
+
|
|
12
|
+
import { useOptionalDataTableContext } from "./data-table"
|
|
13
|
+
import {
|
|
14
|
+
InputGroup,
|
|
15
|
+
InputGroupAddon,
|
|
16
|
+
InputGroupButton,
|
|
17
|
+
} from "@/components/ui/input-group"
|
|
18
|
+
import {
|
|
19
|
+
applySuggestionToValue,
|
|
20
|
+
getSearchSuggestions,
|
|
21
|
+
getSelectorHighlightRanges,
|
|
22
|
+
type DataTableSearchField,
|
|
23
|
+
type DataTableSearchSuggestion,
|
|
24
|
+
} from "@/lib/data-table-search"
|
|
25
|
+
|
|
26
|
+
export type DataTableSearchInputHandle = {
|
|
27
|
+
focus: () => void
|
|
28
|
+
selectAll: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ControlledDataTableSearchInputProps<Row> = {
|
|
32
|
+
fields: DataTableSearchField<Row>[]
|
|
33
|
+
onSearchChange: (value: string) => void
|
|
34
|
+
value: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type UncontrolledDataTableSearchInputProps = {
|
|
38
|
+
fields?: never
|
|
39
|
+
onSearchChange?: never
|
|
40
|
+
value?: never
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type DataTableSearchInputProps<Row> = {
|
|
44
|
+
inputRef?: React.Ref<DataTableSearchInputHandle>
|
|
45
|
+
isLoading?: boolean
|
|
46
|
+
placeholder?: string
|
|
47
|
+
} & (
|
|
48
|
+
| ControlledDataTableSearchInputProps<Row>
|
|
49
|
+
| UncontrolledDataTableSearchInputProps
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
const selectorPluginKey = new PluginKey(
|
|
53
|
+
"data-table-search-selector-highlighter"
|
|
54
|
+
)
|
|
55
|
+
const selectorRefreshKey = "refresh-data-table-search-selector-highlighter"
|
|
56
|
+
const selectorHighlighterState = {
|
|
57
|
+
fields: [] as DataTableSearchField<unknown>[],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createSelectorDecorations(doc: Editor["state"]["doc"]) {
|
|
61
|
+
const ranges = getSelectorHighlightRanges(
|
|
62
|
+
getPlainText(doc),
|
|
63
|
+
selectorHighlighterState.fields
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return DecorationSet.create(
|
|
67
|
+
doc,
|
|
68
|
+
ranges
|
|
69
|
+
.map((range) => {
|
|
70
|
+
const from = textOffsetToDocPosition(doc, range.start)
|
|
71
|
+
const to = textOffsetToDocPosition(doc, range.end)
|
|
72
|
+
|
|
73
|
+
if (from >= to) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return Decoration.inline(
|
|
78
|
+
from,
|
|
79
|
+
to,
|
|
80
|
+
{
|
|
81
|
+
class: "data-table-search-input-selector-token",
|
|
82
|
+
"data-table-search-selector": "",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
inclusiveEnd: false,
|
|
86
|
+
inclusiveStart: false,
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
.filter((decoration): decoration is Decoration => decoration !== null)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const DataTableSelectorHighlighter = Extension.create({
|
|
95
|
+
name: "dataTableSelectorHighlighter",
|
|
96
|
+
addProseMirrorPlugins() {
|
|
97
|
+
return [
|
|
98
|
+
new Plugin({
|
|
99
|
+
key: selectorPluginKey,
|
|
100
|
+
state: {
|
|
101
|
+
init: (_, state) => createSelectorDecorations(state.doc),
|
|
102
|
+
apply: (transaction, decorationSet, _oldState, newState) => {
|
|
103
|
+
if (
|
|
104
|
+
transaction.docChanged ||
|
|
105
|
+
transaction.getMeta(selectorRefreshKey)
|
|
106
|
+
) {
|
|
107
|
+
return createSelectorDecorations(newState.doc)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return decorationSet.map(transaction.mapping, transaction.doc)
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
props: {
|
|
114
|
+
decorations(state) {
|
|
115
|
+
return selectorPluginKey.getState(state)
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
function assignRefValue<T>(ref: React.Ref<T> | undefined, value: T | null) {
|
|
124
|
+
if (typeof ref === "function") {
|
|
125
|
+
ref(value)
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (ref) {
|
|
130
|
+
ref.current = value
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function createEditorDocument(value: string) {
|
|
135
|
+
return {
|
|
136
|
+
content: value
|
|
137
|
+
? [
|
|
138
|
+
{
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
text: value,
|
|
142
|
+
type: "text",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
type: "paragraph",
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
: [
|
|
149
|
+
{
|
|
150
|
+
type: "paragraph",
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
type: "doc",
|
|
154
|
+
} as const
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getPlainText(doc: Editor["state"]["doc"]) {
|
|
158
|
+
return doc.textBetween(0, doc.content.size, "", "")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getCursorOffset(editor: Editor) {
|
|
162
|
+
return editor.state.doc.textBetween(0, editor.state.selection.from, "", "")
|
|
163
|
+
.length
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function textOffsetToDocPosition(doc: Editor["state"]["doc"], offset: number) {
|
|
167
|
+
const totalLength = getPlainText(doc).length
|
|
168
|
+
const clampedOffset = Math.max(0, Math.min(offset, totalLength))
|
|
169
|
+
let remaining = clampedOffset
|
|
170
|
+
let position = TextSelection.atStart(doc).from
|
|
171
|
+
|
|
172
|
+
doc.descendants((node, pos) => {
|
|
173
|
+
if (!node.isText) {
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const text = node.text ?? ""
|
|
178
|
+
|
|
179
|
+
if (remaining <= text.length) {
|
|
180
|
+
position = pos + remaining
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
remaining -= text.length
|
|
185
|
+
position = pos + text.length
|
|
186
|
+
|
|
187
|
+
return true
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return position
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeSingleLineText(value: string) {
|
|
194
|
+
return value.replace(/\r\n?/g, "\n").replace(/\n/g, " ")
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function moveActiveIndex(
|
|
198
|
+
currentIndex: number,
|
|
199
|
+
totalItems: number,
|
|
200
|
+
direction: 1 | -1
|
|
201
|
+
) {
|
|
202
|
+
if (totalItems <= 0) {
|
|
203
|
+
return -1
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (currentIndex < 0) {
|
|
207
|
+
return direction === 1 ? 0 : totalItems - 1
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return (currentIndex + direction + totalItems) % totalItems
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function DataTableSearchInput<Row extends Record<string, unknown>>({
|
|
214
|
+
inputRef,
|
|
215
|
+
isLoading = false,
|
|
216
|
+
placeholder = "Search table...",
|
|
217
|
+
...props
|
|
218
|
+
}: DataTableSearchInputProps<Row>) {
|
|
219
|
+
const context = useOptionalDataTableContext<Row>()
|
|
220
|
+
const fields =
|
|
221
|
+
"fields" in props ? props.fields : (context?.searchFields ?? undefined)
|
|
222
|
+
const value = "value" in props ? props.value : (context?.search ?? undefined)
|
|
223
|
+
const onSearchChange =
|
|
224
|
+
"onSearchChange" in props
|
|
225
|
+
? props.onSearchChange
|
|
226
|
+
: (context?.setSearch ?? undefined)
|
|
227
|
+
const containerRef = React.useRef<HTMLDivElement | null>(null)
|
|
228
|
+
const itemRefs = React.useRef<Array<HTMLButtonElement | null>>([])
|
|
229
|
+
const valueRef = React.useRef(value ?? "")
|
|
230
|
+
const openRef = React.useRef(false)
|
|
231
|
+
const activeIndexRef = React.useRef(-1)
|
|
232
|
+
const suppressNextFocusOpenRef = React.useRef(false)
|
|
233
|
+
const latestSuggestionsRef = React.useRef<DataTableSearchSuggestion[]>([])
|
|
234
|
+
const applySuggestionRef = React.useRef<
|
|
235
|
+
(
|
|
236
|
+
editorInstance: Editor | null,
|
|
237
|
+
suggestion: DataTableSearchSuggestion
|
|
238
|
+
) => void
|
|
239
|
+
>(() => {})
|
|
240
|
+
const pendingSelectionRef = React.useRef<{
|
|
241
|
+
end: number
|
|
242
|
+
start: number
|
|
243
|
+
} | null>(null)
|
|
244
|
+
const [cursor, setCursor] = React.useState(0)
|
|
245
|
+
const [open, setOpen] = React.useState(false)
|
|
246
|
+
const [activeIndex, setActiveIndex] = React.useState(-1)
|
|
247
|
+
|
|
248
|
+
if (!fields || value === undefined || !onSearchChange) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
"DataTableSearchInput must be used inside DataTableProvider or receive fields, value, and onSearchChange props."
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const setActiveSuggestionIndex = React.useCallback((nextIndex: number) => {
|
|
255
|
+
activeIndexRef.current = nextIndex
|
|
256
|
+
setActiveIndex(nextIndex)
|
|
257
|
+
}, [])
|
|
258
|
+
|
|
259
|
+
const suggestions = React.useMemo(
|
|
260
|
+
() => getSearchSuggestions(value, cursor, fields),
|
|
261
|
+
[cursor, fields, value]
|
|
262
|
+
)
|
|
263
|
+
const inputSuggestions = React.useMemo(
|
|
264
|
+
() => suggestions.filter((suggestion) => suggestion.group === "input"),
|
|
265
|
+
[suggestions]
|
|
266
|
+
)
|
|
267
|
+
const valueSuggestions = React.useMemo(
|
|
268
|
+
() => suggestions.filter((suggestion) => suggestion.group === "values"),
|
|
269
|
+
[suggestions]
|
|
270
|
+
)
|
|
271
|
+
const filterSuggestions = React.useMemo(
|
|
272
|
+
() => suggestions.filter((suggestion) => suggestion.group === "filters"),
|
|
273
|
+
[suggestions]
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const syncEditorValue = React.useCallback(
|
|
277
|
+
(
|
|
278
|
+
editorInstance: Editor,
|
|
279
|
+
nextValue: string,
|
|
280
|
+
selection?: {
|
|
281
|
+
end: number
|
|
282
|
+
start: number
|
|
283
|
+
}
|
|
284
|
+
) => {
|
|
285
|
+
editorInstance.commands.setContent(createEditorDocument(nextValue), {
|
|
286
|
+
emitUpdate: false,
|
|
287
|
+
parseOptions: { preserveWhitespace: "full" },
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
if (selection) {
|
|
291
|
+
editorInstance.commands.setTextSelection({
|
|
292
|
+
from: textOffsetToDocPosition(
|
|
293
|
+
editorInstance.state.doc,
|
|
294
|
+
selection.start
|
|
295
|
+
),
|
|
296
|
+
to: textOffsetToDocPosition(editorInstance.state.doc, selection.end),
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
[]
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
const applySuggestion = React.useCallback(
|
|
304
|
+
(editorInstance: Editor | null, suggestion: DataTableSearchSuggestion) => {
|
|
305
|
+
const nextState = applySuggestionToValue(
|
|
306
|
+
value,
|
|
307
|
+
cursor,
|
|
308
|
+
fields,
|
|
309
|
+
suggestion
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
pendingSelectionRef.current = {
|
|
313
|
+
end: nextState.selectionStart,
|
|
314
|
+
start: nextState.selectionStart,
|
|
315
|
+
}
|
|
316
|
+
setCursor(nextState.selectionStart)
|
|
317
|
+
setOpen(nextState.keepOpen)
|
|
318
|
+
|
|
319
|
+
if (!nextState.keepOpen) {
|
|
320
|
+
suppressNextFocusOpenRef.current = true
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (editorInstance) {
|
|
324
|
+
syncEditorValue(
|
|
325
|
+
editorInstance,
|
|
326
|
+
nextState.value,
|
|
327
|
+
pendingSelectionRef.current
|
|
328
|
+
)
|
|
329
|
+
editorInstance.commands.focus()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
onSearchChange(nextState.value)
|
|
333
|
+
},
|
|
334
|
+
[cursor, fields, onSearchChange, syncEditorValue, value]
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
const editor = useEditor(
|
|
338
|
+
{
|
|
339
|
+
content: createEditorDocument(value),
|
|
340
|
+
editorProps: {
|
|
341
|
+
attributes: {
|
|
342
|
+
autocapitalize: "off",
|
|
343
|
+
autocomplete: "off",
|
|
344
|
+
autocorrect: "off",
|
|
345
|
+
class:
|
|
346
|
+
"data-table-search-input-editor min-w-0 flex-1 overflow-x-auto px-2 py-0 text-sm outline-none whitespace-pre",
|
|
347
|
+
spellcheck: "false",
|
|
348
|
+
},
|
|
349
|
+
handleKeyDown: (_view, event) => {
|
|
350
|
+
if (event.key === "Escape") {
|
|
351
|
+
event.preventDefault()
|
|
352
|
+
suppressNextFocusOpenRef.current = true
|
|
353
|
+
setOpen(false)
|
|
354
|
+
return true
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (event.key === "ArrowDown") {
|
|
358
|
+
event.preventDefault()
|
|
359
|
+
setOpen(true)
|
|
360
|
+
setActiveSuggestionIndex(
|
|
361
|
+
moveActiveIndex(
|
|
362
|
+
activeIndexRef.current,
|
|
363
|
+
latestSuggestionsRef.current.length,
|
|
364
|
+
1
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (event.key === "ArrowUp") {
|
|
371
|
+
event.preventDefault()
|
|
372
|
+
setOpen(true)
|
|
373
|
+
setActiveSuggestionIndex(
|
|
374
|
+
moveActiveIndex(
|
|
375
|
+
activeIndexRef.current,
|
|
376
|
+
latestSuggestionsRef.current.length,
|
|
377
|
+
-1
|
|
378
|
+
)
|
|
379
|
+
)
|
|
380
|
+
return true
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (event.key === "Tab") {
|
|
384
|
+
const activeSuggestion =
|
|
385
|
+
latestSuggestionsRef.current[activeIndexRef.current]
|
|
386
|
+
|
|
387
|
+
if (!openRef.current || !activeSuggestion) {
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
event.preventDefault()
|
|
392
|
+
applySuggestionRef.current(editor, activeSuggestion)
|
|
393
|
+
return true
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (event.key === "Enter") {
|
|
397
|
+
event.preventDefault()
|
|
398
|
+
event.stopPropagation()
|
|
399
|
+
|
|
400
|
+
const activeSuggestion =
|
|
401
|
+
latestSuggestionsRef.current[activeIndexRef.current]
|
|
402
|
+
|
|
403
|
+
if (openRef.current && activeSuggestion) {
|
|
404
|
+
applySuggestionRef.current(editor, activeSuggestion)
|
|
405
|
+
} else {
|
|
406
|
+
setCursor(getCursorOffset(editor))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return true
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return false
|
|
413
|
+
},
|
|
414
|
+
handlePaste: (view, event) => {
|
|
415
|
+
const pastedText = event.clipboardData?.getData("text/plain")
|
|
416
|
+
|
|
417
|
+
if (typeof pastedText !== "string") {
|
|
418
|
+
return false
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
event.preventDefault()
|
|
422
|
+
view.dispatch(
|
|
423
|
+
view.state.tr.insertText(
|
|
424
|
+
normalizeSingleLineText(pastedText),
|
|
425
|
+
view.state.selection.from,
|
|
426
|
+
view.state.selection.to
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
return true
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
extensions: [
|
|
433
|
+
Document,
|
|
434
|
+
Paragraph,
|
|
435
|
+
Text,
|
|
436
|
+
Placeholder.configure({
|
|
437
|
+
placeholder,
|
|
438
|
+
}),
|
|
439
|
+
DataTableSelectorHighlighter,
|
|
440
|
+
Extension.create({
|
|
441
|
+
name: "singleLineSearchBehavior",
|
|
442
|
+
addKeyboardShortcuts() {
|
|
443
|
+
return {
|
|
444
|
+
Enter: () => true,
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
}),
|
|
448
|
+
],
|
|
449
|
+
immediatelyRender: true,
|
|
450
|
+
onSelectionUpdate: ({ editor: nextEditor }) => {
|
|
451
|
+
setCursor(getCursorOffset(nextEditor))
|
|
452
|
+
},
|
|
453
|
+
onUpdate: ({ editor: nextEditor }) => {
|
|
454
|
+
const nextValue = getPlainText(nextEditor.state.doc)
|
|
455
|
+
|
|
456
|
+
setCursor(getCursorOffset(nextEditor))
|
|
457
|
+
|
|
458
|
+
if (nextValue !== valueRef.current) {
|
|
459
|
+
onSearchChange(nextValue)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
setOpen(true)
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
[]
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
React.useEffect(() => {
|
|
469
|
+
valueRef.current = value
|
|
470
|
+
}, [value])
|
|
471
|
+
|
|
472
|
+
React.useEffect(() => {
|
|
473
|
+
openRef.current = open
|
|
474
|
+
}, [open])
|
|
475
|
+
|
|
476
|
+
React.useEffect(() => {
|
|
477
|
+
activeIndexRef.current = activeIndex
|
|
478
|
+
}, [activeIndex])
|
|
479
|
+
|
|
480
|
+
React.useEffect(() => {
|
|
481
|
+
latestSuggestionsRef.current = suggestions
|
|
482
|
+
}, [suggestions])
|
|
483
|
+
|
|
484
|
+
React.useEffect(() => {
|
|
485
|
+
applySuggestionRef.current = applySuggestion
|
|
486
|
+
}, [applySuggestion])
|
|
487
|
+
|
|
488
|
+
React.useEffect(() => {
|
|
489
|
+
itemRefs.current.length = suggestions.length
|
|
490
|
+
}, [suggestions.length])
|
|
491
|
+
|
|
492
|
+
React.useEffect(() => {
|
|
493
|
+
selectorHighlighterState.fields = fields as DataTableSearchField<unknown>[]
|
|
494
|
+
}, [fields])
|
|
495
|
+
|
|
496
|
+
React.useEffect(() => {
|
|
497
|
+
if (!editor) {
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
editor.view.dispatch(editor.state.tr.setMeta(selectorRefreshKey, true))
|
|
502
|
+
}, [editor, fields])
|
|
503
|
+
|
|
504
|
+
React.useEffect(() => {
|
|
505
|
+
if (!editor) {
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const nextSelection = pendingSelectionRef.current
|
|
510
|
+
const editorValue = getPlainText(editor.state.doc)
|
|
511
|
+
|
|
512
|
+
if (editorValue !== value) {
|
|
513
|
+
syncEditorValue(editor, value, nextSelection ?? undefined)
|
|
514
|
+
} else if (nextSelection) {
|
|
515
|
+
editor.commands.setTextSelection({
|
|
516
|
+
from: textOffsetToDocPosition(editor.state.doc, nextSelection.start),
|
|
517
|
+
to: textOffsetToDocPosition(editor.state.doc, nextSelection.end),
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
pendingSelectionRef.current = null
|
|
522
|
+
setCursor(getCursorOffset(editor))
|
|
523
|
+
}, [editor, syncEditorValue, value])
|
|
524
|
+
|
|
525
|
+
React.useEffect(() => {
|
|
526
|
+
const nextCursor = Math.min(cursor, value.length)
|
|
527
|
+
|
|
528
|
+
if (nextCursor !== cursor) {
|
|
529
|
+
setCursor(nextCursor)
|
|
530
|
+
}
|
|
531
|
+
}, [cursor, value.length])
|
|
532
|
+
|
|
533
|
+
React.useEffect(() => {
|
|
534
|
+
const nextIndex =
|
|
535
|
+
suggestions.length > 0
|
|
536
|
+
? Math.min(activeIndex, suggestions.length - 1)
|
|
537
|
+
: -1
|
|
538
|
+
|
|
539
|
+
setActiveSuggestionIndex(open ? nextIndex : -1)
|
|
540
|
+
}, [activeIndex, open, setActiveSuggestionIndex, suggestions.length])
|
|
541
|
+
|
|
542
|
+
React.useEffect(() => {
|
|
543
|
+
if (!open || activeIndex < 0) {
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
itemRefs.current[activeIndex]?.scrollIntoView({
|
|
548
|
+
block: "nearest",
|
|
549
|
+
})
|
|
550
|
+
}, [activeIndex, open])
|
|
551
|
+
|
|
552
|
+
React.useEffect(() => {
|
|
553
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
554
|
+
if (!containerRef.current?.contains(event.target as Node)) {
|
|
555
|
+
setOpen(false)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
window.addEventListener("pointerdown", handlePointerDown)
|
|
560
|
+
|
|
561
|
+
return () => window.removeEventListener("pointerdown", handlePointerDown)
|
|
562
|
+
}, [])
|
|
563
|
+
|
|
564
|
+
React.useEffect(() => {
|
|
565
|
+
if (!inputRef) {
|
|
566
|
+
return
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
assignRefValue(inputRef, {
|
|
570
|
+
focus: () => {
|
|
571
|
+
editor?.commands.focus()
|
|
572
|
+
},
|
|
573
|
+
selectAll: () => {
|
|
574
|
+
editor?.chain().focus().selectAll().run()
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
return () => assignRefValue(inputRef, null)
|
|
579
|
+
}, [editor, inputRef])
|
|
580
|
+
|
|
581
|
+
const showLoading = isLoading
|
|
582
|
+
const groupedSuggestions = [
|
|
583
|
+
{ items: inputSuggestions, label: undefined },
|
|
584
|
+
{ items: valueSuggestions, label: "Values" },
|
|
585
|
+
{ items: filterSuggestions, label: "Filters" },
|
|
586
|
+
] satisfies Array<{
|
|
587
|
+
items: DataTableSearchSuggestion[]
|
|
588
|
+
label?: string
|
|
589
|
+
}>
|
|
590
|
+
|
|
591
|
+
let flatIndex = 0
|
|
592
|
+
|
|
593
|
+
return (
|
|
594
|
+
<div ref={containerRef} className="relative min-w-64 flex-1">
|
|
595
|
+
<InputGroup className="h-10 w-full text-lg focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30">
|
|
596
|
+
<InputGroupAddon align="inline-start">
|
|
597
|
+
<span
|
|
598
|
+
aria-hidden="true"
|
|
599
|
+
className="flex size-5 shrink-0 items-center justify-center"
|
|
600
|
+
>
|
|
601
|
+
{showLoading ? (
|
|
602
|
+
<LoaderCircle className="size-4.5 animate-spin" />
|
|
603
|
+
) : (
|
|
604
|
+
<Search className="size-4.5" />
|
|
605
|
+
)}
|
|
606
|
+
</span>
|
|
607
|
+
</InputGroupAddon>
|
|
608
|
+
<div className="min-w-0 flex-1">
|
|
609
|
+
<EditorContent
|
|
610
|
+
editor={editor}
|
|
611
|
+
onBlur={() => {
|
|
612
|
+
requestAnimationFrame(() => {
|
|
613
|
+
if (!containerRef.current?.contains(document.activeElement)) {
|
|
614
|
+
setOpen(false)
|
|
615
|
+
}
|
|
616
|
+
suppressNextFocusOpenRef.current = false
|
|
617
|
+
})
|
|
618
|
+
}}
|
|
619
|
+
onClick={() => {
|
|
620
|
+
setCursor(editor ? getCursorOffset(editor) : 0)
|
|
621
|
+
suppressNextFocusOpenRef.current = false
|
|
622
|
+
setOpen(true)
|
|
623
|
+
}}
|
|
624
|
+
onFocus={() => {
|
|
625
|
+
setCursor(editor ? getCursorOffset(editor) : 0)
|
|
626
|
+
|
|
627
|
+
if (suppressNextFocusOpenRef.current) {
|
|
628
|
+
suppressNextFocusOpenRef.current = false
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
setOpen(true)
|
|
633
|
+
}}
|
|
634
|
+
/>
|
|
635
|
+
</div>
|
|
636
|
+
<InputGroupAddon align="inline-end">
|
|
637
|
+
{value ? (
|
|
638
|
+
<InputGroupButton
|
|
639
|
+
aria-label="Clear search"
|
|
640
|
+
size="icon-xs"
|
|
641
|
+
variant="ghost"
|
|
642
|
+
onClick={() => {
|
|
643
|
+
if (!editor) {
|
|
644
|
+
onSearchChange("")
|
|
645
|
+
setCursor(0)
|
|
646
|
+
setOpen(false)
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
pendingSelectionRef.current = { end: 0, start: 0 }
|
|
651
|
+
setCursor(0)
|
|
652
|
+
setOpen(false)
|
|
653
|
+
syncEditorValue(editor, "", pendingSelectionRef.current)
|
|
654
|
+
editor.commands.focus()
|
|
655
|
+
onSearchChange("")
|
|
656
|
+
}}
|
|
657
|
+
>
|
|
658
|
+
<XIcon className="pointer-events-none" />
|
|
659
|
+
</InputGroupButton>
|
|
660
|
+
) : null}
|
|
661
|
+
</InputGroupAddon>
|
|
662
|
+
</InputGroup>
|
|
663
|
+
|
|
664
|
+
{open ? (
|
|
665
|
+
<div className="absolute top-full left-0 z-50 mt-1 w-full">
|
|
666
|
+
<div className="max-h-[min(calc(var(--spacing(72)---spacing(9)),calc(100svh---spacing(20)))] overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10">
|
|
667
|
+
{suggestions.length === 0 ? (
|
|
668
|
+
<div className="flex w-full justify-center py-2 text-center text-xs/relaxed text-muted-foreground">
|
|
669
|
+
No items found.
|
|
670
|
+
</div>
|
|
671
|
+
) : (
|
|
672
|
+
<div className="max-h-[min(calc(var(--spacing(72)---spacing(9)),calc(100svh---spacing(20)))] overflow-y-auto p-1">
|
|
673
|
+
{groupedSuggestions.map((group) => {
|
|
674
|
+
if (group.items.length === 0) {
|
|
675
|
+
return null
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return (
|
|
679
|
+
<div key={group.label ?? "Input"}>
|
|
680
|
+
{group.label ? (
|
|
681
|
+
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
|
682
|
+
{group.label}
|
|
683
|
+
</div>
|
|
684
|
+
) : null}
|
|
685
|
+
{group.items.map((item) => {
|
|
686
|
+
const itemIndex = flatIndex
|
|
687
|
+
|
|
688
|
+
flatIndex += 1
|
|
689
|
+
|
|
690
|
+
return (
|
|
691
|
+
<button
|
|
692
|
+
key={item.id}
|
|
693
|
+
ref={(node) => {
|
|
694
|
+
itemRefs.current[itemIndex] = node
|
|
695
|
+
}}
|
|
696
|
+
type="button"
|
|
697
|
+
className={`relative flex min-h-7 w-full cursor-default items-center gap-2 rounded-md px-2 py-1 text-left text-xs/relaxed outline-hidden select-none hover:bg-accent hover:text-accent-foreground ${
|
|
698
|
+
itemIndex === activeIndex
|
|
699
|
+
? "bg-accent text-accent-foreground"
|
|
700
|
+
: ""
|
|
701
|
+
}`}
|
|
702
|
+
data-highlighted={
|
|
703
|
+
itemIndex === activeIndex ? "" : undefined
|
|
704
|
+
}
|
|
705
|
+
onClick={() => applySuggestion(editor, item)}
|
|
706
|
+
onMouseDown={(event) => {
|
|
707
|
+
event.preventDefault()
|
|
708
|
+
}}
|
|
709
|
+
onMouseEnter={() =>
|
|
710
|
+
setActiveSuggestionIndex(itemIndex)
|
|
711
|
+
}
|
|
712
|
+
>
|
|
713
|
+
<span className="truncate">{item.label}</span>
|
|
714
|
+
</button>
|
|
715
|
+
)
|
|
716
|
+
})}
|
|
717
|
+
</div>
|
|
718
|
+
)
|
|
719
|
+
})}
|
|
720
|
+
</div>
|
|
721
|
+
)}
|
|
722
|
+
</div>
|
|
723
|
+
</div>
|
|
724
|
+
) : null}
|
|
725
|
+
</div>
|
|
726
|
+
)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export const DataTableSearch = DataTableSearchInput
|