@axzydev/axzy_ui_system 1.2.15 → 1.3.0
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/index.cjs +3067 -1409
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.cts +620 -54
- package/dist/index.d.ts +620 -54
- package/dist/index.js +2983 -1320
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/App.tsx +28 -0
- package/src/components/atoms/badget/badget.tsx +2 -4
- package/src/components/atoms/chip/chip.doc.mdx +47 -0
- package/src/components/atoms/chip/chip.props.ts +32 -0
- package/src/components/atoms/chip/chip.stories.tsx +64 -0
- package/src/components/atoms/chip/chip.tsx +112 -0
- package/src/components/atoms/grid/grid.tsx +1 -2
- package/src/components/molecules/accordion/accordion.doc.mdx +47 -0
- package/src/components/molecules/accordion/accordion.props.ts +35 -0
- package/src/components/molecules/accordion/accordion.stories.tsx +68 -0
- package/src/components/molecules/accordion/accordion.tsx +123 -0
- package/src/components/molecules/chip-input/chip-input.doc.mdx +44 -0
- package/src/components/molecules/chip-input/chip-input.props.ts +42 -0
- package/src/components/molecules/chip-input/chip-input.stories.tsx +52 -0
- package/src/components/molecules/chip-input/chip-input.tsx +205 -0
- package/src/components/molecules/date-picker/datePicker.tsx +45 -56
- package/src/components/molecules/dropdown-menu/dropdown-menu.doc.mdx +46 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.props.ts +41 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.stories.tsx +41 -0
- package/src/components/molecules/dropdown-menu/dropdown-menu.tsx +219 -0
- package/src/components/molecules/field/field.doc.mdx +43 -0
- package/src/components/molecules/field/field.props.ts +23 -0
- package/src/components/molecules/field/field.stories.tsx +42 -0
- package/src/components/molecules/field/field.tsx +54 -0
- package/src/components/molecules/multi-select/multi-select.doc.mdx +55 -0
- package/src/components/molecules/multi-select/multi-select.props.ts +65 -0
- package/src/components/molecules/multi-select/multi-select.stories.tsx +59 -0
- package/src/components/molecules/multi-select/multi-select.tsx +386 -0
- package/src/components/molecules/search-select/search-select.doc.mdx +5 -0
- package/src/components/molecules/search-select/search-select.props.ts +10 -1
- package/src/components/molecules/search-select/search-select.stories.tsx +18 -0
- package/src/components/molecules/search-select/search-select.tsx +102 -28
- package/src/components/molecules/time-picker/timePicker.tsx +14 -26
- package/src/components/molecules/wysiwyg/wysiwyg.props.ts +51 -0
- package/src/components/molecules/wysiwyg/wysiwyg.stories.tsx +95 -0
- package/src/components/molecules/wysiwyg/wysiwyg.tsx +609 -0
- package/src/components/organisms/data-table/dataTable.tsx +16 -5
- package/src/components/organisms/form-builder/fieldRenderer.tsx +144 -27
- package/src/components/organisms/form-builder/formBuilder.context.tsx +77 -15
- package/src/components/organisms/form-builder/formBuilder.doc.mdx +213 -86
- package/src/components/organisms/form-builder/formBuilder.props.ts +118 -36
- package/src/components/organisms/form-builder/formBuilder.stories.tsx +694 -96
- package/src/components/organisms/form-builder/formBuilder.tsx +152 -69
- package/src/components/organisms/form-builder/useFormBuilder.ts +59 -21
- package/src/hooks/useFloatingPanel.ts +100 -0
- package/src/index.ts +24 -4
- package/src/showcases/DataShowcases.tsx +76 -0
- package/src/showcases/FormShowcases.tsx +396 -10
- package/src/showcases/HomeShowcase.tsx +6 -6
- package/src/showcases/NavigationShowcases.tsx +72 -2
- package/src/showcases/StructureShowcases.tsx +64 -0
- package/src/types/chip.types.ts +13 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
} from "react";
|
|
7
|
+
import type { ReactNode } from "react";
|
|
8
|
+
import clsx from "clsx";
|
|
9
|
+
import {
|
|
10
|
+
FaBold,
|
|
11
|
+
FaEraser,
|
|
12
|
+
FaHighlighter,
|
|
13
|
+
FaItalic,
|
|
14
|
+
FaListOl,
|
|
15
|
+
FaListUl,
|
|
16
|
+
FaUnderline,
|
|
17
|
+
} from "react-icons/fa";
|
|
18
|
+
import { ITWysiwygProps, ToolbarAction, toolbarActions } from "./wysiwyg.props";
|
|
19
|
+
import ITTooltip from "@/components/atoms/tooltip/tooltip";
|
|
20
|
+
import ITText from "@/components/atoms/text/text";
|
|
21
|
+
import { inputError, inputLabel, inputWrapper } from "@/utils/styles";
|
|
22
|
+
import { inputSizeTokens } from "@/theme/theme";
|
|
23
|
+
|
|
24
|
+
const INLINE_TAGS = new Set(["STRONG", "B", "EM", "I", "U", "MARK"]);
|
|
25
|
+
|
|
26
|
+
const ACTION_META: Record<
|
|
27
|
+
ToolbarAction,
|
|
28
|
+
{ label: string; icon: ReactNode }
|
|
29
|
+
> = {
|
|
30
|
+
bold: { label: "Negrita", icon: <FaBold /> },
|
|
31
|
+
italic: { label: "Itálica", icon: <FaItalic /> },
|
|
32
|
+
underline: { label: "Subrayado", icon: <FaUnderline /> },
|
|
33
|
+
highlight: { label: "Marcador", icon: <FaHighlighter /> },
|
|
34
|
+
ul: { label: "Lista con viñetas", icon: <FaListUl /> },
|
|
35
|
+
ol: { label: "Lista numerada", icon: <FaListOl /> },
|
|
36
|
+
clear: { label: "Limpiar formato", icon: <FaEraser /> },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ActiveState = Partial<Record<ToolbarAction, boolean>>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Lightweight WYSIWYG editor (no external rich-text dependency). Provides bold,
|
|
43
|
+
* italic, underline, a yellow marker highlight and ordered/unordered lists.
|
|
44
|
+
* Formatting is applied manually over the Selection/Range API (no deprecated
|
|
45
|
+
* `document.execCommand`), toggling elements by wrapping/unwrapping text nodes.
|
|
46
|
+
*
|
|
47
|
+
* The component is uncontrolled at the DOM level: it renders `value` lazily and
|
|
48
|
+
* only re-syncs it when the editor is not focused (`onChange` reports the HTML).
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* <ITWysiwyg
|
|
52
|
+
* label="Descripción"
|
|
53
|
+
* value={content}
|
|
54
|
+
* onChange={setContent}
|
|
55
|
+
* placeholder="Escribe aquí..."
|
|
56
|
+
* />
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* <ITWysiwyg size="lg" highlightColor="#fef08a" toolbar={["bold", "ul"]} />
|
|
60
|
+
*/
|
|
61
|
+
export default function ITWysiwyg({
|
|
62
|
+
value,
|
|
63
|
+
onChange,
|
|
64
|
+
label,
|
|
65
|
+
placeholder,
|
|
66
|
+
error,
|
|
67
|
+
disabled = false,
|
|
68
|
+
readOnly = false,
|
|
69
|
+
name,
|
|
70
|
+
size = "md",
|
|
71
|
+
minHeight = 128,
|
|
72
|
+
highlightColor = "#fde68a",
|
|
73
|
+
toolbar = toolbarActions,
|
|
74
|
+
className,
|
|
75
|
+
}: ITWysiwygProps) {
|
|
76
|
+
const editorRef = useRef<HTMLDivElement>(null);
|
|
77
|
+
const focusedRef = useRef(false);
|
|
78
|
+
const [active, setActive] = useState<ActiveState>({});
|
|
79
|
+
|
|
80
|
+
const isLocked = disabled || readOnly;
|
|
81
|
+
|
|
82
|
+
const sizeConfig = inputSizeTokens[size] ?? inputSizeTokens.md;
|
|
83
|
+
|
|
84
|
+
const findAncestor = useCallback(
|
|
85
|
+
(node: Node | null, predicate: (el: Element) => boolean): Element | null => {
|
|
86
|
+
const editor = editorRef.current;
|
|
87
|
+
if (!editor) return null;
|
|
88
|
+
let cur: Node | null = node;
|
|
89
|
+
while (cur && cur !== editor) {
|
|
90
|
+
if (cur instanceof Element && predicate(cur)) return cur;
|
|
91
|
+
cur = cur.parentNode;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
},
|
|
95
|
+
[]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const isInlineTag = useCallback((el: Element, tag: string) => {
|
|
99
|
+
if (tag === "STRONG" || tag === "B") {
|
|
100
|
+
return el.tagName === "STRONG" || el.tagName === "B";
|
|
101
|
+
}
|
|
102
|
+
if (tag === "EM" || tag === "I") {
|
|
103
|
+
return el.tagName === "EM" || el.tagName === "I";
|
|
104
|
+
}
|
|
105
|
+
return el.tagName === tag;
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const getTextNodesInRange = useCallback((range: Range): Node[] => {
|
|
109
|
+
const editor = editorRef.current;
|
|
110
|
+
if (!editor) return [];
|
|
111
|
+
const out: Node[] = [];
|
|
112
|
+
const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT);
|
|
113
|
+
let node: Node | null;
|
|
114
|
+
while ((node = walker.nextNode())) {
|
|
115
|
+
if (range.intersectsNode(node)) out.push(node);
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
const placeCaretAtEnd = useCallback((target: Node) => {
|
|
121
|
+
const editor = editorRef.current;
|
|
122
|
+
const sel = window.getSelection();
|
|
123
|
+
if (!editor || !sel) return;
|
|
124
|
+
const range = document.createRange();
|
|
125
|
+
range.selectNodeContents(target);
|
|
126
|
+
range.collapse(false);
|
|
127
|
+
sel.removeAllRanges();
|
|
128
|
+
sel.addRange(range);
|
|
129
|
+
if (document.activeElement !== editor) editor.focus();
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const emitChange = useCallback(() => {
|
|
133
|
+
const editor = editorRef.current;
|
|
134
|
+
if (editor) onChange?.(editor.innerHTML);
|
|
135
|
+
}, [onChange]);
|
|
136
|
+
|
|
137
|
+
const updateActive = useCallback(() => {
|
|
138
|
+
const editor = editorRef.current;
|
|
139
|
+
const sel = window.getSelection();
|
|
140
|
+
const node = sel?.anchorNode ?? null;
|
|
141
|
+
if (!editor || !node) return;
|
|
142
|
+
const hasInline = (tag: string) =>
|
|
143
|
+
!!findAncestor(node, (el) => isInlineTag(el, tag));
|
|
144
|
+
const list = findAncestor(node, (el) => el.tagName === "LI");
|
|
145
|
+
const next: ActiveState = {
|
|
146
|
+
bold: hasInline("STRONG"),
|
|
147
|
+
italic: hasInline("EM"),
|
|
148
|
+
underline: hasInline("U"),
|
|
149
|
+
highlight: hasInline("MARK"),
|
|
150
|
+
ul: !!list && list.parentElement?.tagName === "UL",
|
|
151
|
+
ol: !!list && list.parentElement?.tagName === "OL",
|
|
152
|
+
};
|
|
153
|
+
setActive((prev) =>
|
|
154
|
+
JSON.stringify(prev) === JSON.stringify(next) ? prev : next
|
|
155
|
+
);
|
|
156
|
+
}, [findAncestor, isInlineTag]);
|
|
157
|
+
|
|
158
|
+
const unwrapElement = useCallback((el: Element) => {
|
|
159
|
+
const parent = el.parentNode;
|
|
160
|
+
if (!parent) return;
|
|
161
|
+
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
|
162
|
+
el.remove();
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
const applyInline = useCallback(
|
|
166
|
+
(tag: "STRONG" | "EM" | "U" | "MARK", prepare?: (el: HTMLElement) => void) => {
|
|
167
|
+
const editor = editorRef.current;
|
|
168
|
+
const sel = window.getSelection();
|
|
169
|
+
if (!editor || !sel || sel.rangeCount === 0) return;
|
|
170
|
+
const range = sel.getRangeAt(0);
|
|
171
|
+
if (!editor.contains(range.commonAncestorContainer)) return;
|
|
172
|
+
if (document.activeElement !== editor) editor.focus();
|
|
173
|
+
|
|
174
|
+
if (range.collapsed) {
|
|
175
|
+
const existing = findAncestor(sel.anchorNode, (el) =>
|
|
176
|
+
isInlineTag(el, tag)
|
|
177
|
+
);
|
|
178
|
+
if (existing) {
|
|
179
|
+
unwrapElement(existing);
|
|
180
|
+
} else {
|
|
181
|
+
const el = document.createElement(tag);
|
|
182
|
+
prepare?.(el);
|
|
183
|
+
const zw = document.createTextNode("\u200B");
|
|
184
|
+
el.appendChild(zw);
|
|
185
|
+
range.insertNode(el);
|
|
186
|
+
const caret = document.createRange();
|
|
187
|
+
caret.setStart(zw, 1);
|
|
188
|
+
caret.collapse(true);
|
|
189
|
+
sel.removeAllRanges();
|
|
190
|
+
sel.addRange(caret);
|
|
191
|
+
}
|
|
192
|
+
emitChange();
|
|
193
|
+
updateActive();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const nodes = getTextNodesInRange(range).filter(
|
|
198
|
+
(n) => n.textContent && n.textContent.length > 0
|
|
199
|
+
);
|
|
200
|
+
if (nodes.length === 0) return;
|
|
201
|
+
|
|
202
|
+
const fullyWrapped =
|
|
203
|
+
nodes.length > 0 &&
|
|
204
|
+
nodes.every((n) => findAncestor(n, (el) => isInlineTag(el, tag)));
|
|
205
|
+
|
|
206
|
+
if (fullyWrapped) {
|
|
207
|
+
const wrappers = Array.from(
|
|
208
|
+
new Set(
|
|
209
|
+
nodes.map(
|
|
210
|
+
(n) => findAncestor(n, (el) => isInlineTag(el, tag)) as Element
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
const parent = wrappers[0].parentNode;
|
|
215
|
+
wrappers.forEach(unwrapElement);
|
|
216
|
+
if (parent) placeCaretAtEnd(parent);
|
|
217
|
+
} else {
|
|
218
|
+
// Build contiguous runs of unformatted text sharing the same parent.
|
|
219
|
+
const runs: { nodes: Node[]; parent: Element | null }[] = [];
|
|
220
|
+
for (const node of nodes) {
|
|
221
|
+
if (findAncestor(node, (el) => isInlineTag(el, tag))) continue;
|
|
222
|
+
const parentEl = node.parentElement;
|
|
223
|
+
const last = runs[runs.length - 1];
|
|
224
|
+
if (last && last.parent === parentEl) last.nodes.push(node);
|
|
225
|
+
else runs.push({ nodes: [node], parent: parentEl });
|
|
226
|
+
}
|
|
227
|
+
const created: HTMLElement[] = [];
|
|
228
|
+
for (const run of runs) {
|
|
229
|
+
const first = run.nodes[0];
|
|
230
|
+
if (!first || !first.parentNode) continue;
|
|
231
|
+
const el = document.createElement(tag);
|
|
232
|
+
prepare?.(el);
|
|
233
|
+
first.parentNode.insertBefore(el, first);
|
|
234
|
+
run.nodes.forEach((n) => el.appendChild(n));
|
|
235
|
+
created.push(el);
|
|
236
|
+
}
|
|
237
|
+
const last = created[created.length - 1];
|
|
238
|
+
if (last) placeCaretAtEnd(last);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
emitChange();
|
|
242
|
+
updateActive();
|
|
243
|
+
},
|
|
244
|
+
[
|
|
245
|
+
findAncestor,
|
|
246
|
+
getTextNodesInRange,
|
|
247
|
+
isInlineTag,
|
|
248
|
+
placeCaretAtEnd,
|
|
249
|
+
unwrapElement,
|
|
250
|
+
emitChange,
|
|
251
|
+
updateActive,
|
|
252
|
+
]
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const normalizeBlocks = useCallback(() => {
|
|
256
|
+
const editor = editorRef.current;
|
|
257
|
+
if (!editor) return;
|
|
258
|
+
const children = Array.from(editor.childNodes);
|
|
259
|
+
let pending: Node[] = [];
|
|
260
|
+
const flush = () => {
|
|
261
|
+
if (!pending.length) return;
|
|
262
|
+
const div = document.createElement("div");
|
|
263
|
+
const first = pending[0];
|
|
264
|
+
(first.parentNode as Node).insertBefore(div, first);
|
|
265
|
+
pending.forEach((n) => div.appendChild(n));
|
|
266
|
+
pending = [];
|
|
267
|
+
};
|
|
268
|
+
for (const child of children) {
|
|
269
|
+
if (child.nodeType === Node.TEXT_NODE || child instanceof HTMLBRElement) {
|
|
270
|
+
pending.push(child);
|
|
271
|
+
} else {
|
|
272
|
+
flush();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
flush();
|
|
276
|
+
}, []);
|
|
277
|
+
|
|
278
|
+
const convertLiToDiv = useCallback((li: Element) => {
|
|
279
|
+
const list = li.parentElement;
|
|
280
|
+
if (!list || !list.parentNode) return;
|
|
281
|
+
const div = document.createElement("div");
|
|
282
|
+
while (li.firstChild) div.appendChild(li.firstChild);
|
|
283
|
+
list.parentNode.insertBefore(div, list);
|
|
284
|
+
li.remove();
|
|
285
|
+
}, []);
|
|
286
|
+
|
|
287
|
+
const cleanupEmptyLists = useCallback(() => {
|
|
288
|
+
const editor = editorRef.current;
|
|
289
|
+
if (!editor) return;
|
|
290
|
+
editor.querySelectorAll("ul, ol").forEach((list) => {
|
|
291
|
+
if (!list.querySelector("li")) list.remove();
|
|
292
|
+
});
|
|
293
|
+
}, []);
|
|
294
|
+
|
|
295
|
+
const retagList = useCallback((list: Element, type: "ul" | "ol") => {
|
|
296
|
+
if (!list.parentNode) return;
|
|
297
|
+
const nl = document.createElement(type);
|
|
298
|
+
while (list.firstChild) nl.appendChild(list.firstChild);
|
|
299
|
+
list.parentNode.replaceChild(nl, list);
|
|
300
|
+
}, []);
|
|
301
|
+
|
|
302
|
+
const toggleList = useCallback(
|
|
303
|
+
(type: "ul" | "ol") => {
|
|
304
|
+
const editor = editorRef.current;
|
|
305
|
+
const sel = window.getSelection();
|
|
306
|
+
if (!editor || !sel || sel.rangeCount === 0) return;
|
|
307
|
+
const range = sel.getRangeAt(0);
|
|
308
|
+
if (!editor.contains(range.commonAncestorContainer)) return;
|
|
309
|
+
if (document.activeElement !== editor) editor.focus();
|
|
310
|
+
normalizeBlocks();
|
|
311
|
+
|
|
312
|
+
const lines: Element[] = [];
|
|
313
|
+
for (const child of Array.from(editor.children) as Element[]) {
|
|
314
|
+
if (child.tagName === "UL" || child.tagName === "OL") {
|
|
315
|
+
const lis = Array.from(
|
|
316
|
+
child.children as HTMLCollectionOf<Element>
|
|
317
|
+
).filter((li) => range.intersectsNode(li));
|
|
318
|
+
if (lis.length) lines.push(...lis);
|
|
319
|
+
} else if (range.intersectsNode(child)) {
|
|
320
|
+
lines.push(child);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (lines.length === 0) {
|
|
325
|
+
// Fallback: block (or list) containing the caret.
|
|
326
|
+
let node: Node | null = sel.anchorNode;
|
|
327
|
+
while (node && node !== editor && !(node instanceof Element && node.parentElement === editor)) {
|
|
328
|
+
node = node.parentNode;
|
|
329
|
+
}
|
|
330
|
+
if (node instanceof Element && node !== editor) {
|
|
331
|
+
if (node.tagName === "UL" || node.tagName === "OL") {
|
|
332
|
+
const li = node.firstElementChild as Element | null;
|
|
333
|
+
if (li) lines.push(li);
|
|
334
|
+
} else {
|
|
335
|
+
lines.push(node);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (lines.length === 0) return;
|
|
340
|
+
|
|
341
|
+
const liOf = (el: Element) =>
|
|
342
|
+
findAncestor(el, (n) => n.tagName === "LI");
|
|
343
|
+
const anyLi = lines.some((l) => liOf(l));
|
|
344
|
+
const allSameTarget =
|
|
345
|
+
anyLi &&
|
|
346
|
+
lines.every((l) => {
|
|
347
|
+
const li = liOf(l);
|
|
348
|
+
return !!li && li.parentElement?.tagName === type;
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (anyLi && allSameTarget) {
|
|
352
|
+
const lis = Array.from(
|
|
353
|
+
new Set(lines.map((l) => liOf(l) as Element))
|
|
354
|
+
);
|
|
355
|
+
const lastList = lis[0].parentElement;
|
|
356
|
+
lis.forEach(convertLiToDiv);
|
|
357
|
+
cleanupEmptyLists();
|
|
358
|
+
if (lastList?.parentNode) placeCaretAtEnd(lastList);
|
|
359
|
+
} else if (anyLi) {
|
|
360
|
+
const lists = Array.from(
|
|
361
|
+
new Set(lines.map((l) => liOf(l)?.parentElement as Element))
|
|
362
|
+
);
|
|
363
|
+
lists.forEach((list) => {
|
|
364
|
+
if (list.tagName !== type) retagList(list, type);
|
|
365
|
+
});
|
|
366
|
+
if (lists[0]) placeCaretAtEnd(lists[0]);
|
|
367
|
+
} else {
|
|
368
|
+
const runs: Element[][] = [];
|
|
369
|
+
let current: Element[] = [];
|
|
370
|
+
for (const line of lines) {
|
|
371
|
+
const prev = current[current.length - 1];
|
|
372
|
+
if (prev && prev.nextElementSibling === line) current.push(line);
|
|
373
|
+
else {
|
|
374
|
+
if (current.length) runs.push(current);
|
|
375
|
+
current = [line];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (current.length) runs.push(current);
|
|
379
|
+
|
|
380
|
+
let lastList: Element | null = null;
|
|
381
|
+
for (const run of runs) {
|
|
382
|
+
const list = document.createElement(type);
|
|
383
|
+
for (const line of run) {
|
|
384
|
+
const li = document.createElement("li");
|
|
385
|
+
while (line.firstChild) li.appendChild(line.firstChild);
|
|
386
|
+
list.appendChild(li);
|
|
387
|
+
}
|
|
388
|
+
const first = run[0];
|
|
389
|
+
first.parentElement?.insertBefore(list, first);
|
|
390
|
+
run.forEach((line) => line.remove());
|
|
391
|
+
lastList = list;
|
|
392
|
+
}
|
|
393
|
+
if (lastList) placeCaretAtEnd(lastList);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
emitChange();
|
|
397
|
+
updateActive();
|
|
398
|
+
},
|
|
399
|
+
[
|
|
400
|
+
findAncestor,
|
|
401
|
+
normalizeBlocks,
|
|
402
|
+
convertLiToDiv,
|
|
403
|
+
cleanupEmptyLists,
|
|
404
|
+
retagList,
|
|
405
|
+
placeCaretAtEnd,
|
|
406
|
+
emitChange,
|
|
407
|
+
updateActive,
|
|
408
|
+
]
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const clearFormatting = useCallback(() => {
|
|
412
|
+
const editor = editorRef.current;
|
|
413
|
+
const sel = window.getSelection();
|
|
414
|
+
if (!editor || !sel || sel.rangeCount === 0) return;
|
|
415
|
+
const range = sel.getRangeAt(0);
|
|
416
|
+
if (!editor.contains(range.commonAncestorContainer)) return;
|
|
417
|
+
if (document.activeElement !== editor) editor.focus();
|
|
418
|
+
|
|
419
|
+
const nodes = getTextNodesInRange(range).filter(
|
|
420
|
+
(n) => n.textContent && n.textContent.length > 0
|
|
421
|
+
);
|
|
422
|
+
const wrappers = new Set<Element>();
|
|
423
|
+
for (const node of nodes) {
|
|
424
|
+
let cur = node.parentElement;
|
|
425
|
+
while (cur && cur !== editor) {
|
|
426
|
+
if (INLINE_TAGS.has(cur.tagName)) {
|
|
427
|
+
wrappers.add(cur);
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
cur = cur.parentElement;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const toUnwrap = Array.from(wrappers);
|
|
434
|
+
toUnwrap.forEach(unwrapElement);
|
|
435
|
+
const last = toUnwrap[toUnwrap.length - 1];
|
|
436
|
+
if (last?.parentNode) placeCaretAtEnd(last.parentNode);
|
|
437
|
+
|
|
438
|
+
emitChange();
|
|
439
|
+
updateActive();
|
|
440
|
+
}, [
|
|
441
|
+
getTextNodesInRange,
|
|
442
|
+
unwrapElement,
|
|
443
|
+
placeCaretAtEnd,
|
|
444
|
+
emitChange,
|
|
445
|
+
updateActive,
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
// Keep the editor content in sync with `value` while not focused.
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
const editor = editorRef.current;
|
|
451
|
+
if (!editor) return;
|
|
452
|
+
if (!focusedRef.current) {
|
|
453
|
+
const next = value ?? "";
|
|
454
|
+
if (editor.innerHTML !== next) editor.innerHTML = next;
|
|
455
|
+
}
|
|
456
|
+
}, [value]);
|
|
457
|
+
|
|
458
|
+
useEffect(() => {
|
|
459
|
+
const onSelectionChange = () => {
|
|
460
|
+
const editor = editorRef.current;
|
|
461
|
+
const sel = window.getSelection();
|
|
462
|
+
if (!editor || !sel?.anchorNode) return;
|
|
463
|
+
if (editor.contains(sel.anchorNode)) updateActive();
|
|
464
|
+
};
|
|
465
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
466
|
+
return () =>
|
|
467
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
468
|
+
}, [updateActive]);
|
|
469
|
+
|
|
470
|
+
const handleInput = () => emitChange();
|
|
471
|
+
|
|
472
|
+
const handlePaste = (event: React.ClipboardEvent) => {
|
|
473
|
+
event.preventDefault();
|
|
474
|
+
const editor = editorRef.current;
|
|
475
|
+
const sel = window.getSelection();
|
|
476
|
+
if (!editor || !sel || sel.rangeCount === 0) return;
|
|
477
|
+
const text = event.clipboardData.getData("text/plain");
|
|
478
|
+
const range = sel.getRangeAt(0);
|
|
479
|
+
range.deleteContents();
|
|
480
|
+
const textNode = document.createTextNode(text);
|
|
481
|
+
range.insertNode(textNode);
|
|
482
|
+
range.collapse(false);
|
|
483
|
+
sel.removeAllRanges();
|
|
484
|
+
sel.addRange(range);
|
|
485
|
+
emitChange();
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const runAction = (action: ToolbarAction) => {
|
|
489
|
+
switch (action) {
|
|
490
|
+
case "bold":
|
|
491
|
+
applyInline("STRONG");
|
|
492
|
+
break;
|
|
493
|
+
case "italic":
|
|
494
|
+
applyInline("EM");
|
|
495
|
+
break;
|
|
496
|
+
case "underline":
|
|
497
|
+
applyInline("U");
|
|
498
|
+
break;
|
|
499
|
+
case "highlight":
|
|
500
|
+
applyInline("MARK", (el) => (el.style.backgroundColor = highlightColor));
|
|
501
|
+
break;
|
|
502
|
+
case "ul":
|
|
503
|
+
toggleList("ul");
|
|
504
|
+
break;
|
|
505
|
+
case "ol":
|
|
506
|
+
toggleList("ol");
|
|
507
|
+
break;
|
|
508
|
+
case "clear":
|
|
509
|
+
clearFormatting();
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const ToolbarButton = ({
|
|
515
|
+
action,
|
|
516
|
+
}: {
|
|
517
|
+
action: ToolbarAction;
|
|
518
|
+
}) => {
|
|
519
|
+
const meta = ACTION_META[action];
|
|
520
|
+
const isActive = !!active[action];
|
|
521
|
+
return (
|
|
522
|
+
<ITTooltip content={meta.label}>
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
tabIndex={-1}
|
|
526
|
+
disabled={isLocked}
|
|
527
|
+
title={meta.label}
|
|
528
|
+
aria-label={meta.label}
|
|
529
|
+
aria-pressed={isActive}
|
|
530
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
531
|
+
onClick={() => runAction(action)}
|
|
532
|
+
className={clsx(
|
|
533
|
+
"flex h-8 w-8 items-center justify-center rounded-md text-[13px] transition-colors",
|
|
534
|
+
isActive
|
|
535
|
+
? "bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300"
|
|
536
|
+
: "text-secondary-500 hover:bg-secondary-200/70 hover:text-secondary-800 dark:text-secondary-400 dark:hover:bg-slate-700 dark:hover:text-secondary-200",
|
|
537
|
+
isLocked && "cursor-not-allowed opacity-40"
|
|
538
|
+
)}
|
|
539
|
+
>
|
|
540
|
+
{meta.icon}
|
|
541
|
+
</button>
|
|
542
|
+
</ITTooltip>
|
|
543
|
+
);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
return (
|
|
547
|
+
<div className={inputWrapper(className)}>
|
|
548
|
+
{label && (
|
|
549
|
+
<ITText as="label" className={inputLabel(!!error)} htmlFor={name}>
|
|
550
|
+
{label}
|
|
551
|
+
</ITText>
|
|
552
|
+
)}
|
|
553
|
+
<div
|
|
554
|
+
className={clsx(
|
|
555
|
+
"w-full border border-solid rounded-lg overflow-hidden transition-all duration-200",
|
|
556
|
+
error
|
|
557
|
+
? "ring-2 ring-danger-100 border-danger-500"
|
|
558
|
+
: "border-secondary-300 focus-within:ring-2 focus-within:border-primary-500 focus-within:ring-primary-100",
|
|
559
|
+
isLocked && "opacity-50 cursor-not-allowed bg-secondary-100 dark:bg-slate-800"
|
|
560
|
+
)}
|
|
561
|
+
>
|
|
562
|
+
<div
|
|
563
|
+
className={clsx(
|
|
564
|
+
"flex flex-wrap items-center gap-0.5 px-1.5 py-1 border-b border-secondary-200 bg-secondary-50 dark:bg-slate-800",
|
|
565
|
+
isLocked && "bg-secondary-100 dark:bg-slate-900"
|
|
566
|
+
)}
|
|
567
|
+
>
|
|
568
|
+
{toolbar.map((action, i) => (
|
|
569
|
+
<span key={action} className="flex items-center">
|
|
570
|
+
{(i > 0 && (action === "ul" || action === "ol" || action === "clear")) && (
|
|
571
|
+
<span className="mx-1 h-4 w-px bg-secondary-200 dark:bg-slate-600" />
|
|
572
|
+
)}
|
|
573
|
+
<ToolbarButton action={action} />
|
|
574
|
+
</span>
|
|
575
|
+
))}
|
|
576
|
+
</div>
|
|
577
|
+
<div
|
|
578
|
+
id={name}
|
|
579
|
+
ref={editorRef}
|
|
580
|
+
role="textbox"
|
|
581
|
+
aria-multiline="true"
|
|
582
|
+
contentEditable={!isLocked}
|
|
583
|
+
suppressContentEditableWarning
|
|
584
|
+
data-placeholder={placeholder}
|
|
585
|
+
onInput={handleInput}
|
|
586
|
+
onPaste={handlePaste}
|
|
587
|
+
onFocus={() => (focusedRef.current = true)}
|
|
588
|
+
onBlur={() => {
|
|
589
|
+
focusedRef.current = false;
|
|
590
|
+
emitChange();
|
|
591
|
+
}}
|
|
592
|
+
className={clsx(
|
|
593
|
+
"outline-none bg-white dark:bg-slate-900 text-secondary-800 dark:text-secondary-200",
|
|
594
|
+
"empty:before:content-[attr(data-placeholder)] empty:before:text-secondary-400 empty:before:pointer-events-none empty:before:select-none",
|
|
595
|
+
"[&_ul]:list-disc [&_ol]:list-decimal [&_ul]:pl-5 [&_ol]:pl-5 [&_li]:ml-4",
|
|
596
|
+
"[&_mark]:rounded-sm [&_mark]:px-0.5"
|
|
597
|
+
)}
|
|
598
|
+
style={{
|
|
599
|
+
minHeight,
|
|
600
|
+
padding: sizeConfig.padding,
|
|
601
|
+
fontSize: sizeConfig.fontSize,
|
|
602
|
+
lineHeight: "1.6",
|
|
603
|
+
}}
|
|
604
|
+
/>
|
|
605
|
+
</div>
|
|
606
|
+
{error && <ITText as="span" className={inputError}>{error}</ITText>}
|
|
607
|
+
</div>
|
|
608
|
+
);
|
|
609
|
+
}
|
|
@@ -88,16 +88,24 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
88
88
|
|
|
89
89
|
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
90
90
|
const hasFetchedRef = useRef(false);
|
|
91
|
+
const fetchDataRef = useRef(fetchData);
|
|
92
|
+
fetchDataRef.current = fetchData;
|
|
93
|
+
const externalFiltersRef = useRef(externalFilters);
|
|
94
|
+
externalFiltersRef.current = externalFilters;
|
|
95
|
+
|
|
96
|
+
// Snapshot JSON para re-fetch solo cuando los valores cambian (no por nueva
|
|
97
|
+
// identidad del objeto que el padre pase en cada render).
|
|
98
|
+
const externalKey = JSON.stringify(externalFilters);
|
|
91
99
|
|
|
92
100
|
const computedTotalPages = Math.ceil(totalItems / itemsPerPage) || 1;
|
|
93
101
|
|
|
94
102
|
const performFetch = useCallback(async () => {
|
|
95
103
|
setIsLoading(true);
|
|
96
104
|
try {
|
|
97
|
-
const response = await
|
|
105
|
+
const response = await fetchDataRef.current({
|
|
98
106
|
page: currentPage,
|
|
99
107
|
limit: itemsPerPage,
|
|
100
|
-
filters: { ...filters, ...
|
|
108
|
+
filters: { ...filters, ...externalFiltersRef.current },
|
|
101
109
|
sort: sortConfig || undefined,
|
|
102
110
|
});
|
|
103
111
|
setData(response.data || []);
|
|
@@ -111,7 +119,10 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
111
119
|
} finally {
|
|
112
120
|
setIsLoading(false);
|
|
113
121
|
}
|
|
114
|
-
}, [currentPage, itemsPerPage, filters, sortConfig
|
|
122
|
+
}, [currentPage, itemsPerPage, filters, sortConfig]);
|
|
123
|
+
|
|
124
|
+
const performFetchRef = useRef(performFetch);
|
|
125
|
+
performFetchRef.current = performFetch;
|
|
115
126
|
|
|
116
127
|
useEffect(() => {
|
|
117
128
|
if (!fetchOnMount && !hasFetchedRef.current) return;
|
|
@@ -119,13 +130,13 @@ export default function ITDataTable<T extends Record<string, unknown>>({
|
|
|
119
130
|
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
120
131
|
|
|
121
132
|
debounceTimerRef.current = setTimeout(() => {
|
|
122
|
-
|
|
133
|
+
performFetchRef.current();
|
|
123
134
|
}, debounceMs);
|
|
124
135
|
|
|
125
136
|
return () => {
|
|
126
137
|
if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
127
138
|
};
|
|
128
|
-
}, [currentPage, itemsPerPage, sortConfig, filters,
|
|
139
|
+
}, [currentPage, itemsPerPage, sortConfig, filters, externalKey, reloadTrigger, fetchOnMount, debounceMs]);
|
|
129
140
|
|
|
130
141
|
const renderFilterInput = (col: Column<T>) => {
|
|
131
142
|
if (!col.filter) return null;
|