Skip to content

Commit 47aeb17

Browse files
committed
fix state issues + add edit functionality
1 parent c890393 commit 47aeb17

File tree

1 file changed

+52
-51
lines changed

1 file changed

+52
-51
lines changed

client/packages/lowcoder/src/comps/comps/tagsComp/tagsCompView.tsx

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type TagOption = {
3434
margin?: string;
3535
padding?: string;
3636
width?: string;
37-
icon?: React.ReactNode | string; // ignored at runtime to keep tags clean
37+
icon?: any;
3838
};
3939

4040
const colors = PresetStatusColorTypes;
@@ -108,8 +108,7 @@ const multiTags = (function () {
108108
display: inline-flex;
109109
align-items: center;
110110
min-width: fit-content;
111-
width: ${(props) => props.$customStyle?.width || "auto"};
112-
max-width: 100%;
111+
113112
background: ${(props) => props.$customStyle?.backgroundColor || props.$style?.background};
114113
color: ${(props) => props.$customStyle?.color || props.$style?.text};
115114
border-radius: ${(props) => props.$customStyle?.borderRadius || props.$style?.borderRadius};
@@ -129,17 +128,38 @@ const multiTags = (function () {
129128
opacity: 0.9;
130129
`;
131130

132-
const EditableSpan = styled.span`
131+
const EditInput = styled.input`
132+
border: none;
133133
outline: none;
134-
white-space: nowrap;
134+
background: transparent;
135+
font-size: inherit;
136+
font-weight: inherit;
137+
color: inherit;
138+
`;
139+
140+
const TagIcon = styled.span`
141+
display: inline-flex;
142+
align-items: center;
143+
margin-right: 4px;
144+
145+
&.icon-right {
146+
margin-right: 0;
147+
margin-left: 4px;
148+
}
149+
`;
150+
151+
const TagContent = styled.span`
152+
display: inline-flex;
153+
align-items: center;
135154
`;
136155

156+
157+
137158
const childrenMap = {
138159
options: TagsCompOptionsControl, // initial tags (PropertyView)
139160
style: styleControl(InputLikeStyle, "style"),
140161
onEvent: ButtonEventHandlerControl,
141162
editable: BoolControl, // editable switch field
142-
allowEdit: BoolCodeControl, // enable runtime CRUD
143163
preventDuplicates: BoolCodeControl, // runtime de-dupe
144164
allowEmptyEdits: BoolCodeControl, // allow blank labels on edit
145165
maxTags: BoolCodeControl, // truthy => 50 (or provide number if your control supports)
@@ -160,27 +180,24 @@ const multiTags = (function () {
160180

161181
// State
162182
const [editingIndex, setEditingIndex] = useState<number | null>(null);
183+
const [editValue, setEditValue] = useState<string>("");
163184
const [draft, setDraft] = useState<string>(""); // typing buffer for creating a new tag
164185
const containerRef = useRef<HTMLDivElement>(null);
165-
const editableRef = useRef<HTMLSpanElement>(null);
166-
const initRef = useRef<boolean>(false);
167186

168187
const preventDuplicates = !!props.preventDuplicates;
169188
const allowEmptyEdits = !!props.allowEmptyEdits;
170189
const maxTags = toMax(props.maxTags);
171-
// Seed runtimeOptions from design-time options once
172-
const toJsonSafe = (opts: TagOption[]) => opts.map(({ icon, ...rest }) => ({ ...rest }));
173-
useEffect(() => {
174-
if (!initRef.current) {
175-
dispatch(changeChildAction("runtimeOptions", toJsonSafe(props.options), false));
176-
initRef.current = true;
177-
}
178-
}, [dispatch, props.options]);
179-
180-
const displayOptions = (props as any).runtimeOptions?.length
190+
191+
192+
const displayOptions = (props as any).runtimeOptions?.length && props.editable
181193
? ((props as any).runtimeOptions as TagOption[])
182194
: props.options;
183195

196+
useEffect(() => {
197+
// every time the editable prop changes, we need to update the runtimeOptions
198+
dispatch(changeChildAction("runtimeOptions", [...props.options] as TagOption[], false));
199+
}, [props.editable]);
200+
184201
// Events helper
185202
const fireEvent = (type: "add" | "edit" | "delete" | "change" | "click", payload: any) => {
186203
try { if (props.onEvent) (props.onEvent as any)(type, payload); } catch {}
@@ -221,33 +238,18 @@ const multiTags = (function () {
221238
width: "",
222239
};
223240
const next = [...displayOptions, newTag];
224-
dispatch(changeChildAction("runtimeOptions", toJsonSafe(next), false));
241+
dispatch(changeChildAction("runtimeOptions", next, false));
225242
setDraft("");
226243
fireEvent("add", { label, value: next });
227244
};
228245

229246
const startEdit = (index: number) => {
230247
setEditingIndex(index);
231-
// set content when span mounts via effect-less ref trick below
232-
// we'll fill it in render via default textContent
233-
requestAnimationFrame(() => {
234-
editableRef.current?.focus();
235-
// place caret at end
236-
const range = document.createRange();
237-
const node = editableRef.current;
238-
if (node && node.firstChild) {
239-
range.setStart(node.firstChild, node.firstChild.textContent?.length || 0);
240-
range.collapse(true);
241-
const sel = window.getSelection();
242-
sel?.removeAllRanges();
243-
sel?.addRange(range);
244-
}
245-
});
248+
setEditValue(displayOptions[index]?.label || "");
246249
};
247250

248251
const confirmEdit = (index: number) => {
249-
const raw = editableRef.current?.textContent ?? "";
250-
const val = normalize(raw);
252+
const val = normalize(editValue);
251253
if (!val && !allowEmptyEdits) {
252254
cancelEdit();
253255
return;
@@ -258,25 +260,27 @@ const multiTags = (function () {
258260
}
259261
const prev = displayOptions[index]?.label ?? "";
260262
const next = displayOptions.map((t, i) => (i === index ? { ...t, label: val } : t));
261-
dispatch(changeChildAction("runtimeOptions", toJsonSafe(next), false));
263+
dispatch(changeChildAction("runtimeOptions", next, false));
262264
setEditingIndex(null);
265+
setEditValue("");
263266
fireEvent("edit", { from: prev, to: val, index, value: next });
264267
};
265268

266269
const cancelEdit = () => {
267270
setEditingIndex(null);
271+
setEditValue("");
268272
};
269273

270274
const deleteTag = (index: number) => {
271275
const removed = displayOptions[index]?.label;
272276
const next = displayOptions.filter((_, i) => i !== index);
273-
dispatch(changeChildAction("runtimeOptions", toJsonSafe(next), false));
277+
dispatch(changeChildAction("runtimeOptions", next, false));
274278
fireEvent("delete", { removed, index, value: next });
275279
};
276280

277281
// Container keyboard handling for *adding* without inputs
278282
const onContainerKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
279-
if (!props.allowEdit) return;
283+
if (!props.editable) return;
280284

281285
const { key, ctrlKey, metaKey, altKey } = e;
282286

@@ -335,34 +339,32 @@ const multiTags = (function () {
335339
{displayOptions.map((tag, index) => {
336340
const tagColor = getTagColor(tag.label, displayOptions);
337341
const tagStyle = getTagStyle(tag.label, displayOptions, props.style);
338-
const isEditing = props.allowEdit && editingIndex === index;
342+
const isEditing = props.editable && editingIndex === index;
339343

340344
return (
341345
<StyledTag
342346
key={`tag-${index}`}
343347
$style={props.style}
344348
$customStyle={tagStyle}
349+
icon={tag.icon}
345350
color={tagColor}
346-
closable={props.allowEdit}
351+
closable={props.editable}
347352
onClose={(e) => { e.preventDefault(); deleteTag(index); }}
348353
onDoubleClick={() => startEdit(index)} // double-click to edit
349354
onClick={() => onTagClick(tag, index)} // normal click event
350355
>
351356
{isEditing ? (
352-
<EditableSpan
353-
ref={editableRef}
354-
contentEditable
355-
suppressContentEditableWarning
357+
<EditInput
358+
autoFocus
359+
value={editValue}
360+
onChange={(e) => setEditValue(e.target.value)}
356361
onBlur={() => confirmEdit(index)}
357362
onKeyDown={(e) => {
358363
if (e.key === "Enter") { e.preventDefault(); confirmEdit(index); }
359364
if (e.key === "Escape") { e.preventDefault(); cancelEdit(); }
360-
// stop container from also capturing these keystrokes
361365
e.stopPropagation();
362366
}}
363-
>
364-
{tag.label}
365-
</EditableSpan>
367+
/>
366368
) : (
367369
tag.label
368370
)}
@@ -371,7 +373,7 @@ const multiTags = (function () {
371373
})}
372374

373375
{/* Draft chip appears only while typing; press Enter to commit, Esc to cancel */}
374-
{props.allowEdit && draft && (
376+
{props.editable && draft && (
375377
<DraftTag $style={props.style} $customStyle={{}} color="default">
376378
{draft}
377379
</DraftTag>
@@ -385,7 +387,6 @@ const multiTags = (function () {
385387
<Section name={sectionNames.basic}>
386388
{children.options.propertyView({ label: "Initial Tags (PropertyView)" })}
387389
{children.editable.propertyView({ label: "Editable" })}
388-
{children.allowEdit.propertyView({ label: "Allow Runtime Editing" })}
389390
{children.preventDuplicates.propertyView({ label: "Prevent Duplicates (Runtime)" })}
390391
{children.allowEmptyEdits.propertyView({ label: "Allow Empty Edit (Runtime)" })}
391392
{children.maxTags.propertyView({ label: "Set Max Tags (Runtime) — true=50" })}

0 commit comments

Comments
 (0)