@qoretechnologies/reqraft 0.10.62-pr.142.g62c577f → 0.10.62
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/form/engine/CompactRow.js +1 -1
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +22 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +135 -50
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +3 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +2 -5
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +1 -1
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/helpers/choiceChip.d.ts +98 -0
- package/dist/helpers/choiceChip.d.ts.map +1 -0
- package/dist/helpers/choiceChip.js +181 -0
- package/dist/helpers/choiceChip.js.map +1 -0
- package/package.json +1 -1
- package/src/components/form/engine/CompactRow.tsx +1 -1
- package/src/components/form/engine/FormEngine.stories.tsx +195 -0
- package/src/components/form/fields/rich-text/RichText.stories.tsx +178 -1
- package/src/components/form/fields/rich-text/RichText.tsx +180 -45
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -5
- package/src/components/form/fields/template/TemplateField.tsx +1 -1
- package/src/helpers/choiceChip.ts +227 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Copyright 2026 Qore Technologies, s.r.o.
|
|
2
|
+
import { IQorusAllowedValue, TQorusFormFieldSchema } from '@qoretechnologies/ts-toolkit';
|
|
3
|
+
import { Editor, Node, Path, Range, Transforms } from 'slate';
|
|
4
|
+
import { findAllowedValueOption } from './allowedValues';
|
|
5
|
+
import { getListElementValue } from './options';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A value that IS one of the field's own choices, drawn as that choice.
|
|
9
|
+
*
|
|
10
|
+
* A rich-text field offered choices picks one by writing its value, so an
|
|
11
|
+
* author who chose the Slack channel "general" read `C07ERBFS0P6` in the
|
|
12
|
+
* editor. The value is right — it is what the server resolves — but it is not
|
|
13
|
+
* what was chosen by. These helpers draw such a value as one chip named from
|
|
14
|
+
* the choice, the way a template reference is drawn from the catalogue, while
|
|
15
|
+
* what is stored stays the plain value: the chip's own `value` is the choice's,
|
|
16
|
+
* so the text a document stands for is unchanged, and a document that is
|
|
17
|
+
* nothing but the chip is read back as the value it draws.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The marker a choice chip carries, so it is told apart from a template reference. */
|
|
21
|
+
export const CHOICE_CHIP_META = 'allowedValue';
|
|
22
|
+
|
|
23
|
+
export interface IChoiceChipNode {
|
|
24
|
+
type: 'tag';
|
|
25
|
+
value: string;
|
|
26
|
+
label: string;
|
|
27
|
+
metadata: { [CHOICE_CHIP_META]: true; description?: string };
|
|
28
|
+
children: [{ text: '' }];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The choice's value as the plain string it is; it arrives in the form engine's `{type, value}` envelope. */
|
|
32
|
+
export const choiceValue = (choice: IQorusAllowedValue): string =>
|
|
33
|
+
String(getListElementValue(choice.value) ?? '');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The text a document is, when it is one paragraph of plain text and nothing
|
|
37
|
+
* else; `undefined` for anything more — a chip, a mark, a second paragraph.
|
|
38
|
+
*/
|
|
39
|
+
export const plainTextOfNodes = (nodes: unknown): string | undefined => {
|
|
40
|
+
if (!Array.isArray(nodes) || nodes.length !== 1) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const paragraph = nodes[0] as { type?: unknown; children?: unknown[] } | undefined;
|
|
45
|
+
|
|
46
|
+
if (paragraph?.type !== 'paragraph' || !Array.isArray(paragraph.children)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let text = '';
|
|
51
|
+
|
|
52
|
+
for (const child of paragraph.children as { text?: unknown }[]) {
|
|
53
|
+
if (typeof child?.text !== 'string' || Object.keys(child).some((key) => key !== 'text')) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
text += child.text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return text;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The choice a value is: a whole plain string the field offers, or a document
|
|
64
|
+
* that is one paragraph of exactly that text — which is what an edit of the
|
|
65
|
+
* chip stores, so an id typed back is the choice it was. A string or document
|
|
66
|
+
* holding anything more is the author's own text and is never a choice.
|
|
67
|
+
*/
|
|
68
|
+
export const choiceForValue = (
|
|
69
|
+
value: unknown,
|
|
70
|
+
choices?: IQorusAllowedValue[]
|
|
71
|
+
): IQorusAllowedValue | undefined => {
|
|
72
|
+
const text = typeof value === 'string' ? value : plainTextOfNodes(value);
|
|
73
|
+
|
|
74
|
+
if (!text || !choices?.length) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return findAllowedValueOption(text, { allowed_values: choices } as TQorusFormFieldSchema) as
|
|
79
|
+
| IQorusAllowedValue
|
|
80
|
+
| undefined;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** The one-chip document that draws a choice by its name. */
|
|
84
|
+
export const choiceToNodes = (
|
|
85
|
+
choice: IQorusAllowedValue
|
|
86
|
+
): { type: 'paragraph'; children: ({ text: string } | IChoiceChipNode)[] }[] => {
|
|
87
|
+
const value = choiceValue(choice);
|
|
88
|
+
const description = (choice as { desc?: string; short_desc?: string }).short_desc ?? choice.desc;
|
|
89
|
+
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
type: 'paragraph',
|
|
93
|
+
children: [
|
|
94
|
+
{ text: '' },
|
|
95
|
+
{
|
|
96
|
+
type: 'tag',
|
|
97
|
+
value,
|
|
98
|
+
label: choice.display_name ?? value,
|
|
99
|
+
metadata: { [CHOICE_CHIP_META]: true, ...(description ? { description } : {}) },
|
|
100
|
+
children: [{ text: '' }],
|
|
101
|
+
},
|
|
102
|
+
{ text: '' },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const isChoiceChip = (node: unknown): node is IChoiceChipNode =>
|
|
109
|
+
!!node &&
|
|
110
|
+
typeof node === 'object' &&
|
|
111
|
+
(node as { type?: unknown }).type === 'tag' &&
|
|
112
|
+
!!(node as { metadata?: Record<string, unknown> }).metadata?.[CHOICE_CHIP_META];
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The value a document is, when the document is one choice chip and nothing
|
|
116
|
+
* else; `undefined` when the author has written anything around it, in which
|
|
117
|
+
* case the document itself is the value.
|
|
118
|
+
*/
|
|
119
|
+
export const choiceValueOfNodes = (nodes: unknown): string | undefined => {
|
|
120
|
+
if (!Array.isArray(nodes) || nodes.length !== 1) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const children: unknown[] = (nodes[0] as { children?: unknown[] })?.children ?? [];
|
|
125
|
+
const chips = children.filter(isChoiceChip);
|
|
126
|
+
const text = children
|
|
127
|
+
.filter((node) => !isChoiceChip(node))
|
|
128
|
+
.map((node) => (typeof (node as { text?: unknown })?.text === 'string' ? (node as { text: string }).text : '\u0000'))
|
|
129
|
+
.join('');
|
|
130
|
+
|
|
131
|
+
return chips.length === 1 && text === '' ? chips[0].value : undefined;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* What a choice chip does when the author acts on it, by action.
|
|
136
|
+
*
|
|
137
|
+
* The chip is a void: it holds no text of its own, so a key or a click on it
|
|
138
|
+
* has nothing to land in, and an author who wants to change one letter of the
|
|
139
|
+
* value it draws (`C07ERBFS0P6` to `C07ERBFS0P7`) has no way in unless the
|
|
140
|
+
* chip opens. Each action is configurable, and the default is the one that
|
|
141
|
+
* opens it: a creatable choice is the author's to edit.
|
|
142
|
+
*/
|
|
143
|
+
export interface IChoiceChipBehavior {
|
|
144
|
+
/**
|
|
145
|
+
* Backspace with the cursor right after the chip. `unwrap` (the default)
|
|
146
|
+
* turns the chip into its value as plain text, the cursor at the end of it,
|
|
147
|
+
* for the author to edit; `remove` deletes it whole, as the editor deletes
|
|
148
|
+
* any other chip.
|
|
149
|
+
*/
|
|
150
|
+
backspace?: 'unwrap' | 'remove';
|
|
151
|
+
/**
|
|
152
|
+
* A click on the chip's body. `unwrap` (the default) turns the chip into its
|
|
153
|
+
* value as plain text, the cursor at the end of it; `cursor` only places the
|
|
154
|
+
* cursor after it, as a click on a template chip does.
|
|
155
|
+
*/
|
|
156
|
+
click?: 'unwrap' | 'cursor';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const DEFAULT_CHOICE_CHIP_BEHAVIOR: Required<IChoiceChipBehavior> = {
|
|
160
|
+
backspace: 'unwrap',
|
|
161
|
+
click: 'unwrap',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** Where the node is in the document, by identity; the editor renders the document's own nodes, so a chip handed to a click handler is one of them. */
|
|
165
|
+
export const pathOfNode = (editor: Editor, node: unknown): Path | undefined => {
|
|
166
|
+
// Stepped by hand: the build target cannot iterate a generator (TS2802).
|
|
167
|
+
const nodes = Node.nodes(editor);
|
|
168
|
+
|
|
169
|
+
for (let entry = nodes.next(); !entry.done; entry = nodes.next()) {
|
|
170
|
+
if (entry.value[0] === node) {
|
|
171
|
+
return entry.value[1];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return undefined;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The chip the cursor is on: right after it with nothing between, where
|
|
180
|
+
* Backspace would take it, or inside it, where the arrow keys can leave the
|
|
181
|
+
* cursor. Nothing when the selection spans text or sits anywhere else.
|
|
182
|
+
*/
|
|
183
|
+
export const choiceChipAtCursor = (editor: Editor): Path | undefined => {
|
|
184
|
+
const { selection } = editor;
|
|
185
|
+
|
|
186
|
+
if (!selection || !Range.isCollapsed(selection)) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const { path, offset } = selection.anchor;
|
|
191
|
+
const inside = Editor.above(editor, { at: path, match: isChoiceChip, voids: true });
|
|
192
|
+
|
|
193
|
+
if (inside) {
|
|
194
|
+
return inside[1];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (offset !== 0 || !Path.hasPrevious(path)) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const previous = Path.previous(path);
|
|
202
|
+
|
|
203
|
+
return isChoiceChip(Node.get(editor, previous)) ? previous : undefined;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Opens the chip at `path` into its value as plain text, the cursor at the
|
|
208
|
+
* end of the text: what the author read is what they chose, now editable
|
|
209
|
+
* letter by letter. The document is text from here on, and is stored as the
|
|
210
|
+
* field stores anything typed; it is drawn as a chip again only when it is
|
|
211
|
+
* read back as exactly one of the choices.
|
|
212
|
+
*/
|
|
213
|
+
export const unwrapChoiceChip = (editor: Editor, path: Path): boolean => {
|
|
214
|
+
const chip = Node.get(editor, path);
|
|
215
|
+
|
|
216
|
+
if (!isChoiceChip(chip)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Editor.withoutNormalizing(editor, () => {
|
|
221
|
+
Transforms.removeNodes(editor, { at: path });
|
|
222
|
+
Transforms.insertNodes(editor, [{ text: chip.value }], { at: path });
|
|
223
|
+
Transforms.select(editor, Editor.end(editor, path));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return true;
|
|
227
|
+
};
|