@shipfox/react-ui 0.32.2 → 0.33.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/dist/components/alert/index.d.ts +1 -1
- package/dist/components/avatar/avatar-group.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/avatar/index.d.ts +2 -2
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/badge/icon-badge.d.ts +1 -1
- package/dist/components/badge/index.d.ts +4 -4
- package/dist/components/button/button-link.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/button/icon-button.d.ts +1 -1
- package/dist/components/button/index.d.ts +3 -3
- package/dist/components/button-group/index.d.ts +1 -1
- package/dist/components/calendar/index.d.ts +1 -1
- package/dist/components/card/card.d.ts +1 -1
- package/dist/components/card/index.d.ts +1 -1
- package/dist/components/checkbox/checkbox-label.d.ts +1 -1
- package/dist/components/checkbox/checkbox-links.d.ts +1 -1
- package/dist/components/checkbox/index.d.ts +3 -3
- package/dist/components/code-block/code-block-footer.js +7 -2
- package/dist/components/code-block/index.d.ts +3 -3
- package/dist/components/combobox/combobox.d.ts +1 -1
- package/dist/components/combobox/index.d.ts +1 -1
- package/dist/components/command/command.d.ts +1 -1
- package/dist/components/command/index.d.ts +1 -1
- package/dist/components/confetti/index.d.ts +1 -1
- package/dist/components/count-up/index.d.ts +1 -1
- package/dist/components/dashboard/components/charts/bar-chart.d.ts +1 -1
- package/dist/components/dashboard/components/charts/index.d.ts +5 -5
- package/dist/components/dashboard/components/charts/line-chart.d.ts +1 -1
- package/dist/components/dashboard/components/charts/utils.d.ts +2 -2
- package/dist/components/dashboard/components/dashboard-alert.d.ts +1 -1
- package/dist/components/dashboard/components/mobile-sidebar.d.ts +1 -1
- package/dist/components/dashboard/components/sidebar.d.ts +1 -1
- package/dist/components/dashboard/components/topbar-button.d.ts +1 -1
- package/dist/components/dashboard/context/dashboard-context.d.ts +2 -2
- package/dist/components/dashboard/context/index.d.ts +4 -4
- package/dist/components/dashboard/context/types.d.ts +1 -1
- package/dist/components/dashboard/context/utils.d.ts +1 -1
- package/dist/components/dashboard/index.d.ts +18 -18
- package/dist/components/dashboard/pages/index.d.ts +2 -2
- package/dist/components/dashboard/table/index.d.ts +2 -2
- package/dist/components/dashboard/toolbar/filter-button.d.ts +1 -1
- package/dist/components/dashboard/toolbar/index.d.ts +9 -9
- package/dist/components/date-picker/index.d.ts +1 -1
- package/dist/components/date-time-range-picker/index.d.ts +1 -1
- package/dist/components/dot-grid/index.d.ts +1 -1
- package/dist/components/dropdown-menu/dropdown-menu.d.ts +1 -1
- package/dist/components/dropdown-menu/index.d.ts +1 -1
- package/dist/components/dynamic-item/dynamic-item.d.ts +1 -1
- package/dist/components/dynamic-item/index.d.ts +1 -1
- package/dist/components/empty-state/empty-state.d.ts +1 -1
- package/dist/components/empty-state/index.d.ts +1 -1
- package/dist/components/form/index.d.ts +1 -1
- package/dist/components/icon/custom/index.d.ts +14 -14
- package/dist/components/icon/icon.d.ts +1 -1
- package/dist/components/icon/index.d.ts +1 -1
- package/dist/components/index.d.ts +47 -47
- package/dist/components/inline-tips/index.d.ts +1 -1
- package/dist/components/input/index.d.ts +1 -1
- package/dist/components/interval-selector/hooks/index.d.ts +3 -3
- package/dist/components/interval-selector/hooks/use-interval-selector-input.d.ts +1 -1
- package/dist/components/interval-selector/hooks/use-interval-selector-navigation.d.ts +1 -1
- package/dist/components/interval-selector/hooks/use-interval-selector.d.ts +2 -2
- package/dist/components/interval-selector/index.d.ts +2 -2
- package/dist/components/interval-selector/interval-selector-calendar.d.ts +1 -1
- package/dist/components/interval-selector/interval-selector-input.d.ts +1 -1
- package/dist/components/interval-selector/interval-selector-suggestions.d.ts +1 -1
- package/dist/components/interval-selector/interval-selector.d.ts +1 -1
- package/dist/components/interval-selector/utils/format.d.ts +1 -1
- package/dist/components/interval-selector/utils/index.d.ts +2 -2
- package/dist/components/item/index.d.ts +1 -1
- package/dist/components/kbd/index.d.ts +1 -1
- package/dist/components/label/index.d.ts +1 -1
- package/dist/components/modal/index.d.ts +1 -1
- package/dist/components/moving-border/index.d.ts +1 -1
- package/dist/components/popover/index.d.ts +1 -1
- package/dist/components/scroll-area/index.d.ts +1 -1
- package/dist/components/search/index.d.ts +6 -6
- package/dist/components/search/search-inline.d.ts +1 -1
- package/dist/components/search/search-modal.d.ts +1 -1
- package/dist/components/search/search-trigger.d.ts +1 -1
- package/dist/components/select/index.d.ts +1 -1
- package/dist/components/select/select.d.ts +1 -1
- package/dist/components/sheet/index.d.ts +1 -1
- package/dist/components/shiny-text/index.d.ts +1 -1
- package/dist/components/shipql-editor/index.d.ts +2 -2
- package/dist/components/shipql-editor/lexical/on-blur-plugin.d.ts +2 -1
- package/dist/components/shipql-editor/lexical/on-blur-plugin.js +8 -4
- package/dist/components/shipql-editor/lexical/shipql-leaf-node.d.ts +5 -2
- package/dist/components/shipql-editor/lexical/shipql-leaf-node.js +18 -8
- package/dist/components/shipql-editor/lexical/shipql-plugin.d.ts +3 -2
- package/dist/components/shipql-editor/lexical/shipql-plugin.js +188 -34
- package/dist/components/shipql-editor/shipql-editor-inner.d.ts +2 -2
- package/dist/components/shipql-editor/shipql-editor-inner.js +52 -7
- package/dist/components/shipql-editor/shipql-editor.d.ts +4 -3
- package/dist/components/shipql-editor/suggestions/generate-suggestions.d.ts +9 -3
- package/dist/components/shipql-editor/suggestions/generate-suggestions.js +94 -20
- package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.d.ts +1 -1
- package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.js +2 -2
- package/dist/components/shipql-editor/suggestions/shipql-suggestion-item.d.ts +2 -2
- package/dist/components/shipql-editor/suggestions/shipql-suggestion-item.js +74 -10
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-dropdown.d.ts +4 -3
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-dropdown.js +56 -11
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.d.ts +2 -2
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.js +7 -5
- package/dist/components/shipql-editor/suggestions/types.d.ts +25 -3
- package/dist/components/skeleton/index.d.ts +1 -1
- package/dist/components/slider/index.d.ts +1 -1
- package/dist/components/switch/index.d.ts +1 -1
- package/dist/components/table/index.d.ts +4 -4
- package/dist/components/table/table.stories.columns.d.ts +1 -1
- package/dist/components/tabs/index.d.ts +1 -1
- package/dist/components/textarea/index.d.ts +1 -1
- package/dist/components/theme/index.d.ts +1 -1
- package/dist/components/theme/theme-provider.d.ts +1 -1
- package/dist/components/toast/index.d.ts +2 -2
- package/dist/components/tooltip/index.d.ts +1 -1
- package/dist/components/typography/index.d.ts +3 -3
- package/dist/hooks/index.d.ts +6 -6
- package/dist/hooks/useTheme.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/styles.css +2 -1
- package/dist/utils/format/index.d.ts +5 -5
- package/dist/utils/index.d.ts +6 -6
- package/package.json +10 -10
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
2
2
|
import { parse, removeBySource, stringify } from '@shipfox/shipql-parser';
|
|
3
|
-
import { $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isRangeSelection, $isTextNode, $setSelection, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_NORMAL, createCommand, INSERT_PARAGRAPH_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ENTER_COMMAND } from 'lexical';
|
|
3
|
+
import { $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isRangeSelection, $isTextNode, $setSelection, BLUR_COMMAND, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, createCommand, FOCUS_COMMAND, INSERT_PARAGRAPH_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ENTER_COMMAND } from 'lexical';
|
|
4
4
|
import { useEffect, useRef } from 'react';
|
|
5
5
|
import { handleArrowRightFromLeaf } from './handle-arrow-right-from-leaf.js';
|
|
6
|
-
import { $createShipQLLeafNode, $isShipQLLeafNode, isGroupedCompound, isSimpleLeaf, leafSource } from './shipql-leaf-node.js';
|
|
6
|
+
import { $createShipQLLeafNode, $isShipQLLeafNode, $setLeafFreeTextError, isGroupedCompound, isSimpleLeaf, leafSource } from './shipql-leaf-node.js';
|
|
7
7
|
function collectLeaves(ast) {
|
|
8
8
|
// Simple terminals are always a single leaf chip.
|
|
9
9
|
if (isSimpleLeaf(ast)) {
|
|
@@ -86,7 +86,15 @@ function needsRebuild(children, segments) {
|
|
|
86
86
|
}
|
|
87
87
|
return false;
|
|
88
88
|
}
|
|
89
|
+
function getOrderedLeafStateKey(text, counts) {
|
|
90
|
+
const index = (counts.get(text) ?? 0) + 1;
|
|
91
|
+
counts.set(text, index);
|
|
92
|
+
return `${text}::${index}`;
|
|
93
|
+
}
|
|
89
94
|
function getAbsoluteOffset(para, point) {
|
|
95
|
+
if (point.type === 'element') {
|
|
96
|
+
return para.getChildren().slice(0, point.offset).reduce((offset, child)=>offset + ($isTextNode(child) ? child.getTextContentSize() : 0), 0);
|
|
97
|
+
}
|
|
90
98
|
let offset = 0;
|
|
91
99
|
for (const child of para.getChildren()){
|
|
92
100
|
if (child.getKey() === point.key) return offset + point.offset;
|
|
@@ -94,19 +102,62 @@ function getAbsoluteOffset(para, point) {
|
|
|
94
102
|
}
|
|
95
103
|
return offset + point.offset;
|
|
96
104
|
}
|
|
105
|
+
function getDesiredLeafErrorState(segments, selectionOffset, allowFreeText, isFocused) {
|
|
106
|
+
const counts = new Map();
|
|
107
|
+
const leafRanges = [];
|
|
108
|
+
let offset = 0;
|
|
109
|
+
for (const seg of segments){
|
|
110
|
+
if (seg.kind === 'leaf') {
|
|
111
|
+
leafRanges.push({
|
|
112
|
+
stateKey: getOrderedLeafStateKey(seg.text, counts),
|
|
113
|
+
node: seg.node,
|
|
114
|
+
start: offset,
|
|
115
|
+
end: offset + seg.text.length
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
offset += seg.text.length;
|
|
119
|
+
}
|
|
120
|
+
const activeLeafKey = selectionOffset >= 0 ? leafRanges.find((leaf)=>selectionOffset >= leaf.start && selectionOffset <= leaf.end)?.stateKey ?? null : null;
|
|
121
|
+
const errorState = new Map();
|
|
122
|
+
for (const leaf of leafRanges){
|
|
123
|
+
const shouldError = !allowFreeText && isTextLeaf(leaf.node) && (!isFocused || leaf.stateKey !== activeLeafKey);
|
|
124
|
+
errorState.set(leaf.stateKey, shouldError);
|
|
125
|
+
}
|
|
126
|
+
return errorState;
|
|
127
|
+
}
|
|
128
|
+
function leafErrorStateMatches(children, desiredErrorState) {
|
|
129
|
+
const counts = new Map();
|
|
130
|
+
for (const child of children){
|
|
131
|
+
if (!$isShipQLLeafNode(child)) continue;
|
|
132
|
+
const stateKey = getOrderedLeafStateKey(child.getTextContent(), counts);
|
|
133
|
+
if ((desiredErrorState.get(stateKey) ?? false) !== child.getLatest().__freeTextError) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
/** Returns true if a leaf segment represents a free-text node (bare word / quoted string). */ function isTextLeaf(node) {
|
|
140
|
+
if (node.type === 'text') return true;
|
|
141
|
+
if (node.type === 'not' && node.expr.type === 'text') return true;
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
97
144
|
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
98
145
|
/** Payload: the Lexical node key of the leaf to remove. */ export const REMOVE_LEAF_COMMAND = createCommand('REMOVE_LEAF_COMMAND');
|
|
99
146
|
// ─── Plugin ───────────────────────────────────────────────────────────────────
|
|
100
147
|
const REBUILD_TAG = 'shipql-rebuild';
|
|
101
|
-
export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }) {
|
|
148
|
+
export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay, allowFreeText = true }) {
|
|
102
149
|
const [editor] = useLexicalComposerContext();
|
|
103
150
|
// Keep latest callback accessible inside Lexical listeners without re-registering.
|
|
104
151
|
const onLeafFocusRef = useRef(onLeafFocus);
|
|
105
152
|
onLeafFocusRef.current = onLeafFocus;
|
|
106
153
|
const formatLeafDisplayRef = useRef(formatLeafDisplay);
|
|
107
154
|
formatLeafDisplayRef.current = formatLeafDisplay;
|
|
155
|
+
const allowFreeTextRef = useRef(allowFreeText);
|
|
156
|
+
allowFreeTextRef.current = allowFreeText;
|
|
108
157
|
// Track the key of the last focused leaf to avoid redundant callbacks.
|
|
109
158
|
const lastFocusedKeyRef = useRef(null);
|
|
159
|
+
// Track whether the editor is focused so we can show errors only when blurred.
|
|
160
|
+
const isFocusedRef = useRef(true);
|
|
110
161
|
useEffect(()=>{
|
|
111
162
|
// Block newlines — ShipQL is a single-line language.
|
|
112
163
|
const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, (event)=>{
|
|
@@ -156,6 +207,85 @@ export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }) {
|
|
|
156
207
|
});
|
|
157
208
|
return true;
|
|
158
209
|
}, COMMAND_PRIORITY_HIGH);
|
|
210
|
+
// ── Blur / Focus: toggle free-text error styling via rebuild ────────────
|
|
211
|
+
function syncLeafNodesWithCurrentState() {
|
|
212
|
+
editor.update(()=>{
|
|
213
|
+
const para = $getRoot().getFirstChild();
|
|
214
|
+
if (!para) return;
|
|
215
|
+
const text = para.getTextContent();
|
|
216
|
+
const children = para.getChildren();
|
|
217
|
+
const sel = $getSelection();
|
|
218
|
+
const selectionOffset = $isRangeSelection(sel) ? getAbsoluteOffset(para, sel.anchor) : -1;
|
|
219
|
+
if (!text) {
|
|
220
|
+
if (children.some($isShipQLLeafNode)) {
|
|
221
|
+
para.clear();
|
|
222
|
+
}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let ast = null;
|
|
226
|
+
try {
|
|
227
|
+
ast = parse(text);
|
|
228
|
+
} catch {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!ast) {
|
|
232
|
+
if (!children.some($isShipQLLeafNode)) return;
|
|
233
|
+
para.clear();
|
|
234
|
+
para.append($createTextNode(text));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const segments = tokenize(text, collectLeaves(ast));
|
|
238
|
+
const fmt = formatLeafDisplayRef.current;
|
|
239
|
+
const desiredLeafErrors = getDesiredLeafErrorState(segments, selectionOffset, allowFreeTextRef.current, isFocusedRef.current);
|
|
240
|
+
if (!needsRebuild(children, segments) && leafErrorStateMatches(children, desiredLeafErrors)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
para.clear();
|
|
244
|
+
const newNodes = [];
|
|
245
|
+
const counts = new Map();
|
|
246
|
+
for (const seg of segments){
|
|
247
|
+
const node = seg.kind === 'leaf' ? $createShipQLLeafNode(seg.text, seg.node, fmt?.(seg.text, seg.node), desiredLeafErrors.get(getOrderedLeafStateKey(seg.text, counts)) ?? false) : $createTextNode(seg.text);
|
|
248
|
+
newNodes.push(node);
|
|
249
|
+
para.append(node);
|
|
250
|
+
}
|
|
251
|
+
if (selectionOffset >= 0 && newNodes.length > 0) {
|
|
252
|
+
let remaining = selectionOffset;
|
|
253
|
+
let lastTextNode = null;
|
|
254
|
+
for (const node of newNodes){
|
|
255
|
+
if (!$isTextNode(node)) continue;
|
|
256
|
+
lastTextNode = node;
|
|
257
|
+
const len = node.getTextContentSize();
|
|
258
|
+
if (remaining <= len) {
|
|
259
|
+
const nextSelection = $createRangeSelection();
|
|
260
|
+
nextSelection.anchor.set(node.getKey(), remaining, 'text');
|
|
261
|
+
nextSelection.focus.set(node.getKey(), remaining, 'text');
|
|
262
|
+
$setSelection(nextSelection);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
remaining -= len;
|
|
266
|
+
}
|
|
267
|
+
if (lastTextNode && $isTextNode(lastTextNode)) {
|
|
268
|
+
const endSelection = $createRangeSelection();
|
|
269
|
+
const end = lastTextNode.getTextContentSize();
|
|
270
|
+
endSelection.anchor.set(lastTextNode.getKey(), end, 'text');
|
|
271
|
+
endSelection.focus.set(lastTextNode.getKey(), end, 'text');
|
|
272
|
+
$setSelection(endSelection);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}, {
|
|
276
|
+
tag: REBUILD_TAG
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const unregisterBlur = editor.registerCommand(BLUR_COMMAND, ()=>{
|
|
280
|
+
isFocusedRef.current = false;
|
|
281
|
+
if (!allowFreeTextRef.current) syncLeafNodesWithCurrentState();
|
|
282
|
+
return false;
|
|
283
|
+
}, COMMAND_PRIORITY_LOW);
|
|
284
|
+
const unregisterFocus = editor.registerCommand(FOCUS_COMMAND, ()=>{
|
|
285
|
+
isFocusedRef.current = true;
|
|
286
|
+
if (!allowFreeTextRef.current) syncLeafNodesWithCurrentState();
|
|
287
|
+
return false;
|
|
288
|
+
}, COMMAND_PRIORITY_LOW);
|
|
159
289
|
const unregisterUpdate = editor.registerUpdateListener(({ editorState, tags })=>{
|
|
160
290
|
// After our own rebuild we only need to refresh the leaf-focus callback.
|
|
161
291
|
if (tags.has(REBUILD_TAG)) {
|
|
@@ -175,6 +305,8 @@ export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }) {
|
|
|
175
305
|
let shouldRebuild = false;
|
|
176
306
|
let nextSegments = [];
|
|
177
307
|
let savedOffset = -1;
|
|
308
|
+
let desiredLeafErrors = new Map();
|
|
309
|
+
let needsErrorStateUpdate = false;
|
|
178
310
|
editorState.read(()=>{
|
|
179
311
|
const para = $getRoot().getFirstChild();
|
|
180
312
|
if (!para) return;
|
|
@@ -197,7 +329,7 @@ export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }) {
|
|
|
197
329
|
}
|
|
198
330
|
] : [];
|
|
199
331
|
const sel = $getSelection();
|
|
200
|
-
if ($isRangeSelection(sel)
|
|
332
|
+
if ($isRangeSelection(sel)) {
|
|
201
333
|
savedOffset = getAbsoluteOffset(para, sel.anchor);
|
|
202
334
|
}
|
|
203
335
|
}
|
|
@@ -209,16 +341,16 @@ export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }) {
|
|
|
209
341
|
return;
|
|
210
342
|
}
|
|
211
343
|
const segments = tokenize(text, collectLeaves(ast));
|
|
344
|
+
const sel = $getSelection();
|
|
345
|
+
if ($isRangeSelection(sel)) {
|
|
346
|
+
savedOffset = getAbsoluteOffset(para, sel.anchor);
|
|
347
|
+
}
|
|
348
|
+
desiredLeafErrors = getDesiredLeafErrorState(segments, savedOffset, allowFreeTextRef.current, isFocusedRef.current);
|
|
212
349
|
if (needsRebuild(children, segments)) {
|
|
213
350
|
shouldRebuild = true;
|
|
214
351
|
nextSegments = segments;
|
|
215
|
-
const sel = $getSelection();
|
|
216
|
-
if ($isRangeSelection(sel) && sel.anchor.type === 'text') {
|
|
217
|
-
savedOffset = getAbsoluteOffset(para, sel.anchor);
|
|
218
|
-
}
|
|
219
352
|
} else {
|
|
220
|
-
// No structural change —
|
|
221
|
-
const sel = $getSelection();
|
|
353
|
+
// No structural change — update leaf-focus and deferred text-node errors.
|
|
222
354
|
if ($isRangeSelection(sel)) {
|
|
223
355
|
const anchor = sel.anchor.getNode();
|
|
224
356
|
const key = $isShipQLLeafNode(anchor) ? anchor.getKey() : null;
|
|
@@ -226,32 +358,72 @@ export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }) {
|
|
|
226
358
|
lastFocusedKeyRef.current = key;
|
|
227
359
|
onLeafFocusRef.current?.(key !== null && $isShipQLLeafNode(anchor) ? anchor.getShipQLNode() : null);
|
|
228
360
|
}
|
|
361
|
+
} else if (lastFocusedKeyRef.current !== null) {
|
|
362
|
+
lastFocusedKeyRef.current = null;
|
|
363
|
+
onLeafFocusRef.current?.(null);
|
|
229
364
|
}
|
|
365
|
+
needsErrorStateUpdate = !leafErrorStateMatches(children, desiredLeafErrors);
|
|
230
366
|
}
|
|
231
367
|
});
|
|
232
|
-
if (!shouldRebuild)
|
|
368
|
+
if (!shouldRebuild) {
|
|
369
|
+
if (needsErrorStateUpdate) {
|
|
370
|
+
editor.update(()=>{
|
|
371
|
+
const para = $getRoot().getFirstChild();
|
|
372
|
+
if (!para) return;
|
|
373
|
+
const counts = new Map();
|
|
374
|
+
for (const child of para.getChildren()){
|
|
375
|
+
if (!$isShipQLLeafNode(child)) continue;
|
|
376
|
+
const stateKey = getOrderedLeafStateKey(child.getTextContent(), counts);
|
|
377
|
+
$setLeafFreeTextError(child, desiredLeafErrors.get(stateKey) ?? false);
|
|
378
|
+
}
|
|
379
|
+
}, {
|
|
380
|
+
tag: REBUILD_TAG
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
233
385
|
// ── Rebuild node structure ─────────────────────────────────────────────
|
|
234
386
|
editor.update(()=>{
|
|
235
387
|
const para = $getRoot().getFirstChild();
|
|
236
388
|
if (!para) return;
|
|
237
389
|
para.clear();
|
|
238
390
|
const fmt = formatLeafDisplayRef.current;
|
|
239
|
-
const
|
|
391
|
+
const occurrenceCounts = new Map();
|
|
392
|
+
const newNodes = nextSegments.map((seg)=>{
|
|
393
|
+
if (seg.kind === 'leaf') {
|
|
394
|
+
const stateKey = getOrderedLeafStateKey(seg.text, occurrenceCounts);
|
|
395
|
+
const freeTextError = desiredLeafErrors.get(stateKey) ?? false;
|
|
396
|
+
return $createShipQLLeafNode(seg.text, seg.node, fmt?.(seg.text, seg.node), freeTextError);
|
|
397
|
+
}
|
|
398
|
+
return $createTextNode(seg.text);
|
|
399
|
+
});
|
|
240
400
|
for (const node of newNodes)para.append(node);
|
|
241
401
|
// Restore cursor to the same absolute character position.
|
|
242
402
|
if (savedOffset >= 0 && newNodes.length > 0) {
|
|
243
403
|
let remaining = savedOffset;
|
|
404
|
+
let lastTextNode = null;
|
|
405
|
+
let restoredSelection = false;
|
|
244
406
|
for (const node of newNodes){
|
|
245
|
-
|
|
407
|
+
if (!$isTextNode(node)) continue;
|
|
408
|
+
lastTextNode = node;
|
|
409
|
+
const len = node.getTextContentSize();
|
|
246
410
|
if (remaining <= len) {
|
|
247
411
|
const sel = $createRangeSelection();
|
|
248
412
|
sel.anchor.set(node.getKey(), remaining, 'text');
|
|
249
413
|
sel.focus.set(node.getKey(), remaining, 'text');
|
|
250
414
|
$setSelection(sel);
|
|
415
|
+
restoredSelection = true;
|
|
251
416
|
break;
|
|
252
417
|
}
|
|
253
418
|
remaining -= len;
|
|
254
419
|
}
|
|
420
|
+
if (!restoredSelection && lastTextNode && $isTextNode(lastTextNode)) {
|
|
421
|
+
const sel = $createRangeSelection();
|
|
422
|
+
const end = lastTextNode.getTextContentSize();
|
|
423
|
+
sel.anchor.set(lastTextNode.getKey(), end, 'text');
|
|
424
|
+
sel.focus.set(lastTextNode.getKey(), end, 'text');
|
|
425
|
+
$setSelection(sel);
|
|
426
|
+
}
|
|
255
427
|
}
|
|
256
428
|
}, {
|
|
257
429
|
tag: REBUILD_TAG
|
|
@@ -261,32 +433,14 @@ export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }) {
|
|
|
261
433
|
// registered after Lexical has already committed its initialConfig editorState
|
|
262
434
|
// (useEffect runs post-paint), so the listener never fires for the first render.
|
|
263
435
|
// We do the full rebuild here instead of relying on the listener being triggered.
|
|
264
|
-
|
|
265
|
-
const para = $getRoot().getFirstChild();
|
|
266
|
-
if (!para) return;
|
|
267
|
-
const text = para.getTextContent();
|
|
268
|
-
if (!text) return;
|
|
269
|
-
const children = para.getChildren();
|
|
270
|
-
let ast = null;
|
|
271
|
-
try {
|
|
272
|
-
ast = parse(text);
|
|
273
|
-
} catch {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (!ast) return;
|
|
277
|
-
const segments = tokenize(text, collectLeaves(ast));
|
|
278
|
-
if (!needsRebuild(children, segments)) return;
|
|
279
|
-
const fmt = formatLeafDisplayRef.current;
|
|
280
|
-
para.clear();
|
|
281
|
-
for (const seg of segments){
|
|
282
|
-
para.append(seg.kind === 'leaf' ? $createShipQLLeafNode(seg.text, seg.node, fmt?.(seg.text, seg.node)) : $createTextNode(seg.text));
|
|
283
|
-
}
|
|
284
|
-
});
|
|
436
|
+
syncLeafNodesWithCurrentState();
|
|
285
437
|
return ()=>{
|
|
286
438
|
unregisterEnter();
|
|
287
439
|
unregisterParagraph();
|
|
288
440
|
unregisterArrowRight();
|
|
289
441
|
unregisterRemoveLeaf();
|
|
442
|
+
unregisterBlur();
|
|
443
|
+
unregisterFocus();
|
|
290
444
|
unregisterUpdate();
|
|
291
445
|
};
|
|
292
446
|
}, [
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ShipQLEditorInnerProps } from './shipql-editor';
|
|
2
|
-
export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange, formatLeafDisplay, }: ShipQLEditorInnerProps): import("react/jsx-runtime").JSX.Element;
|
|
1
|
+
import type { ShipQLEditorInnerProps } from './shipql-editor.js';
|
|
2
|
+
export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange, formatLeafDisplay, allowFreeText, }: ShipQLEditorInnerProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=shipql-editor-inner.d.ts.map
|
|
@@ -4,10 +4,11 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
|
4
4
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
5
5
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
6
6
|
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
|
|
7
|
+
import { hasTextNodes } from '@shipfox/shipql-parser';
|
|
7
8
|
import { Input } from '../../components/input/index.js';
|
|
8
9
|
import { Popover, PopoverAnchor } from '../../components/popover/index.js';
|
|
9
10
|
import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';
|
|
10
|
-
import { useCallback, useRef, useState } from 'react';
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
12
|
import { cn } from '../../utils/cn.js';
|
|
12
13
|
import { Icon } from '../icon/icon.js';
|
|
13
14
|
import { LeafCloseOverlay } from './lexical/leaf-close-overlay.js';
|
|
@@ -21,16 +22,19 @@ import { ShipQLSuggestionsPlugin } from './suggestions/shipql-suggestions-plugin
|
|
|
21
22
|
const INPUT_CLASSES = 'block w-full rounded-6 bg-background-field-base py-2 pl-32 pr-58 sm:pr-64 text-md text-foreground-neutral-base caret-foreground-neutral-base outline-none focus:border-border-highlights-interactive shadow-button-neutral';
|
|
22
23
|
const INPUT_ERROR_CLASSES = 'shadow-border-error';
|
|
23
24
|
const BUTTON_CLASSES = 'shrink-0 text-foreground-neutral-subtle hover:text-foreground-neutral-base transition-all duration-150 flex justify-center items-center cursor-pointer w-28 sm:w-32 h-full';
|
|
24
|
-
export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange, formatLeafDisplay }) {
|
|
25
|
+
export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange, formatLeafDisplay, allowFreeText }) {
|
|
25
26
|
const [suggestionsOpen, setSuggestionsOpen] = useState(false);
|
|
26
27
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
27
28
|
const [items, setItems] = useState([]);
|
|
28
29
|
const [focusedLeafNode, setFocusedLeafNode] = useState(null);
|
|
29
30
|
const [isNegated, setIsNegated] = useState(false);
|
|
30
31
|
const [showSyntaxHelp, setShowSyntaxHelp] = useState(false);
|
|
32
|
+
const [isTextModeBlurError, setIsTextModeBlurError] = useState(false);
|
|
31
33
|
const isSelectingRef = useRef(false);
|
|
32
34
|
const applyRef = useRef(null);
|
|
33
35
|
const negationPrefixRef = useRef('');
|
|
36
|
+
const containerRef = useRef(null);
|
|
37
|
+
const suggestionsSuppressedRef = useRef(false);
|
|
34
38
|
const hasSuggestions = Boolean(facets && facets.length > 0);
|
|
35
39
|
const showValueActions = Boolean(currentFacet);
|
|
36
40
|
const isRangeContext = items.length === 1 && items[0]?.type === 'range-slider';
|
|
@@ -56,9 +60,43 @@ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder,
|
|
|
56
60
|
const handleToggleSyntaxHelp = useCallback(()=>{
|
|
57
61
|
setShowSyntaxHelp((prev)=>!prev);
|
|
58
62
|
}, []);
|
|
63
|
+
const handleDismissSuggestions = useCallback(()=>{
|
|
64
|
+
suggestionsSuppressedRef.current = true;
|
|
65
|
+
setSuggestionsOpen(false);
|
|
66
|
+
}, []);
|
|
67
|
+
const handleSuggestionsOpenChange = useCallback((open)=>{
|
|
68
|
+
if (open && suggestionsSuppressedRef.current) return;
|
|
69
|
+
setSuggestionsOpen(open);
|
|
70
|
+
}, []);
|
|
71
|
+
const handleEditorMouseDownCapture = useCallback(()=>{
|
|
72
|
+
suggestionsSuppressedRef.current = false;
|
|
73
|
+
}, []);
|
|
74
|
+
useEffect(()=>{
|
|
75
|
+
if (!suggestionsOpen) return;
|
|
76
|
+
const handleMouseDown = (event)=>{
|
|
77
|
+
if (isSelectingRef.current) return;
|
|
78
|
+
const target = event.target;
|
|
79
|
+
if (target && containerRef.current?.contains(target)) return;
|
|
80
|
+
if (target?.closest?.('[data-shipql-suggestions]')) return;
|
|
81
|
+
handleDismissSuggestions();
|
|
82
|
+
const activeElement = document.activeElement;
|
|
83
|
+
if (activeElement && containerRef.current?.contains(activeElement)) {
|
|
84
|
+
activeElement.blur();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
document.addEventListener('mousedown', handleMouseDown, true);
|
|
88
|
+
return ()=>{
|
|
89
|
+
document.removeEventListener('mousedown', handleMouseDown, true);
|
|
90
|
+
};
|
|
91
|
+
}, [
|
|
92
|
+
suggestionsOpen,
|
|
93
|
+
handleDismissSuggestions
|
|
94
|
+
]);
|
|
59
95
|
return /*#__PURE__*/ _jsxs("div", {
|
|
96
|
+
ref: containerRef,
|
|
60
97
|
"data-shipql-editor": true,
|
|
61
98
|
className: cn('relative', className),
|
|
99
|
+
onMouseDownCapture: handleEditorMouseDownCapture,
|
|
62
100
|
children: [
|
|
63
101
|
mode === 'editor' ? /*#__PURE__*/ _jsxs(Popover, {
|
|
64
102
|
open: hasSuggestions && suggestionsOpen,
|
|
@@ -108,10 +146,12 @@ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder,
|
|
|
108
146
|
}),
|
|
109
147
|
/*#__PURE__*/ _jsx(ShipQLPlugin, {
|
|
110
148
|
onLeafFocus: handleLeafFocus,
|
|
111
|
-
formatLeafDisplay: formatLeafDisplay
|
|
149
|
+
formatLeafDisplay: formatLeafDisplay,
|
|
150
|
+
allowFreeText: allowFreeText
|
|
112
151
|
}),
|
|
113
152
|
/*#__PURE__*/ _jsx(OnBlurPlugin, {
|
|
114
|
-
onChange: onChange
|
|
153
|
+
onChange: onChange,
|
|
154
|
+
allowFreeText: allowFreeText
|
|
115
155
|
}),
|
|
116
156
|
/*#__PURE__*/ _jsx(OnTextChangePlugin, {
|
|
117
157
|
onTextChange: onTextChange
|
|
@@ -125,7 +165,7 @@ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder,
|
|
|
125
165
|
valueSuggestions: valueSuggestions ?? [],
|
|
126
166
|
isLoadingValueSuggestions: isLoadingValueSuggestions ?? false,
|
|
127
167
|
open: suggestionsOpen,
|
|
128
|
-
setOpen:
|
|
168
|
+
setOpen: handleSuggestionsOpenChange,
|
|
129
169
|
selectedIndex: selectedIndex,
|
|
130
170
|
setSelectedIndex: setSelectedIndex,
|
|
131
171
|
items: items,
|
|
@@ -144,6 +184,7 @@ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder,
|
|
|
144
184
|
selectedIndex: selectedIndex,
|
|
145
185
|
isSelectingRef: isSelectingRef,
|
|
146
186
|
onSelect: handleSelect,
|
|
187
|
+
onClose: handleDismissSuggestions,
|
|
147
188
|
isLoading: isLoadingValueSuggestions,
|
|
148
189
|
isNegated: isNegated,
|
|
149
190
|
onToggleNegate: handleToggleNegate,
|
|
@@ -163,10 +204,11 @@ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder,
|
|
|
163
204
|
className: "shrink-0 text-foreground-neutral-muted"
|
|
164
205
|
}),
|
|
165
206
|
className: cn(INPUT_CLASSES, disabled && 'pointer-events-none opacity-50'),
|
|
166
|
-
"aria-invalid": isError,
|
|
207
|
+
"aria-invalid": isError || isTextModeBlurError,
|
|
167
208
|
value: text,
|
|
168
209
|
onChange: (e)=>{
|
|
169
210
|
const newText = e.target.value;
|
|
211
|
+
setIsTextModeBlurError(false);
|
|
170
212
|
onTextChange(newText);
|
|
171
213
|
const facetNames = facets ? normalizeFacets(facets) : [];
|
|
172
214
|
const facetCtx = detectFacetContext(newText, facetNames);
|
|
@@ -178,8 +220,11 @@ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder,
|
|
|
178
220
|
},
|
|
179
221
|
onBlur: (e)=>{
|
|
180
222
|
const ast = tryParse(e.target.value);
|
|
181
|
-
|
|
223
|
+
const hasDeferredFreeTextError = Boolean(ast && !allowFreeText && hasTextNodes(ast));
|
|
224
|
+
setIsTextModeBlurError(hasDeferredFreeTextError);
|
|
225
|
+
if (ast && !hasDeferredFreeTextError) onChange?.(ast);
|
|
182
226
|
},
|
|
227
|
+
onFocus: ()=>setIsTextModeBlurError(false),
|
|
183
228
|
placeholder: placeholder,
|
|
184
229
|
disabled: disabled
|
|
185
230
|
}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AstNode } from '@shipfox/shipql-parser';
|
|
2
|
-
import type { LeafAstNode } from './lexical/shipql-leaf-node';
|
|
3
|
-
import type { FacetDef, FormatLeafDisplay } from './suggestions/types';
|
|
4
|
-
export type { FacetDef, FormatLeafDisplay, RangeFacetConfig } from './suggestions/types';
|
|
2
|
+
import type { LeafAstNode } from './lexical/shipql-leaf-node.js';
|
|
3
|
+
import type { FacetDef, FormatLeafDisplay } from './suggestions/types.js';
|
|
4
|
+
export type { FacetDef, FacetMetadata, FormatLeafDisplay, RangeFacetConfig, } from './suggestions/types.js';
|
|
5
5
|
export type LeafChangePayload = {
|
|
6
6
|
partialValue: string;
|
|
7
7
|
ast: AstNode | null;
|
|
@@ -20,6 +20,7 @@ export interface ShipQLEditorProps {
|
|
|
20
20
|
isLoadingValueSuggestions?: boolean;
|
|
21
21
|
onLeafChange?: (payload: LeafChangePayload) => void;
|
|
22
22
|
formatLeafDisplay?: FormatLeafDisplay;
|
|
23
|
+
allowFreeText?: boolean;
|
|
23
24
|
}
|
|
24
25
|
export interface ShipQLEditorInnerProps extends ShipQLEditorProps {
|
|
25
26
|
mode: 'editor' | 'text';
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { type AstNode } from '@shipfox/shipql-parser';
|
|
2
|
-
import type { LeafAstNode } from '../lexical/shipql-leaf-node';
|
|
3
|
-
import type { FacetDef, RangeFacetConfig, SuggestionItem } from './types';
|
|
2
|
+
import type { LeafAstNode } from '../lexical/shipql-leaf-node.js';
|
|
3
|
+
import type { FacetDef, FacetGroupInfo, RangeFacetConfig, SuggestionItem } from './types.js';
|
|
4
4
|
export declare function tryParse(text: string): AstNode | null;
|
|
5
5
|
export declare function normalizeFacets(facets: FacetDef[]): string[];
|
|
6
|
-
export declare function getFacetConfig(facets: FacetDef[],
|
|
6
|
+
export declare function getFacetConfig(facets: FacetDef[], id: string): RangeFacetConfig | undefined;
|
|
7
|
+
/** Returns the human-readable display label, falling back to the raw id. */
|
|
8
|
+
export declare function getFacetLabel(facets: FacetDef[], id: string): string;
|
|
9
|
+
/** Returns the description for a facet, if any. */
|
|
10
|
+
export declare function getFacetDescription(facets: FacetDef[], id: string): string | undefined;
|
|
11
|
+
/** Returns grouping information for a facet, with defaults applied. */
|
|
12
|
+
export declare function getFacetGroupInfo(facets: FacetDef[], id: string): FacetGroupInfo;
|
|
7
13
|
export declare function extractFacetFromLeaf(leaf: LeafAstNode): string | undefined;
|
|
8
14
|
/** Extracts the negation prefix used in a `not` node's source (either 'NOT ' or '-'). */
|
|
9
15
|
export declare function negationPrefixFromSource(source: string): string;
|