@@ -34,7 +34,7 @@ type TagOption = {
34
34
margin ?:string ;
35
35
padding ?:string ;
36
36
width ?:string ;
37
- icon ?:React . ReactNode | string ; // ignored at runtime to keep tags clean
37
+ icon ?:any ;
38
38
} ;
39
39
40
40
const colors = PresetStatusColorTypes ;
@@ -108,8 +108,7 @@ const multiTags = (function () {
108
108
display: inline-flex;
109
109
align-items: center;
110
110
min-width: fit-content;
111
- width:${ ( props ) => props . $customStyle ?. width || "auto" } ;
112
- max-width: 100%;
111
+
113
112
background:${ ( props ) => props . $customStyle ?. backgroundColor || props . $style ?. background } ;
114
113
color:${ ( props ) => props . $customStyle ?. color || props . $style ?. text } ;
115
114
border-radius:${ ( props ) => props . $customStyle ?. borderRadius || props . $style ?. borderRadius } ;
@@ -129,17 +128,38 @@ const multiTags = (function () {
129
128
opacity: 0.9;
130
129
` ;
131
130
132
- const EditableSpan = styled . span `
131
+ const EditInput = styled . input `
132
+ border: none;
133
133
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;
135
154
` ;
136
155
156
+
157
+
137
158
const childrenMap = {
138
159
options :TagsCompOptionsControl , // initial tags (PropertyView)
139
160
style :styleControl ( InputLikeStyle , "style" ) ,
140
161
onEvent :ButtonEventHandlerControl ,
141
162
editable :BoolControl , // editable switch field
142
- allowEdit :BoolCodeControl , // enable runtime CRUD
143
163
preventDuplicates :BoolCodeControl , // runtime de-dupe
144
164
allowEmptyEdits :BoolCodeControl , // allow blank labels on edit
145
165
maxTags :BoolCodeControl , // truthy => 50 (or provide number if your control supports)
@@ -160,27 +180,24 @@ const multiTags = (function () {
160
180
161
181
// State
162
182
const [ editingIndex , setEditingIndex ] = useState < number | null > ( null ) ;
183
+ const [ editValue , setEditValue ] = useState < string > ( "" ) ;
163
184
const [ draft , setDraft ] = useState < string > ( "" ) ; // typing buffer for creating a new tag
164
185
const containerRef = useRef < HTMLDivElement > ( null ) ;
165
- const editableRef = useRef < HTMLSpanElement > ( null ) ;
166
- const initRef = useRef < boolean > ( false ) ;
167
186
168
187
const preventDuplicates = ! ! props . preventDuplicates ;
169
188
const allowEmptyEdits = ! ! props . allowEmptyEdits ;
170
189
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
181
193
?( ( props as any ) . runtimeOptions as TagOption [ ] )
182
194
:props . options ;
183
195
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
+
184
201
// Events helper
185
202
const fireEvent = ( type :"add" | "edit" | "delete" | "change" | "click" , payload :any ) => {
186
203
try { if ( props . onEvent ) ( props . onEvent as any ) ( type , payload ) ; } catch { }
@@ -221,33 +238,18 @@ const multiTags = (function () {
221
238
width :"" ,
222
239
} ;
223
240
const next = [ ...displayOptions , newTag ] ;
224
- dispatch ( changeChildAction ( "runtimeOptions" , toJsonSafe ( next ) , false ) ) ;
241
+ dispatch ( changeChildAction ( "runtimeOptions" , next , false ) ) ;
225
242
setDraft ( "" ) ;
226
243
fireEvent ( "add" , { label, value :next } ) ;
227
244
} ;
228
245
229
246
const startEdit = ( index :number ) => {
230
247
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 || "" ) ;
246
249
} ;
247
250
248
251
const confirmEdit = ( index :number ) => {
249
- const raw = editableRef . current ?. textContent ?? "" ;
250
- const val = normalize ( raw ) ;
252
+ const val = normalize ( editValue ) ;
251
253
if ( ! val && ! allowEmptyEdits ) {
252
254
cancelEdit ( ) ;
253
255
return ;
@@ -258,25 +260,27 @@ const multiTags = (function () {
258
260
}
259
261
const prev = displayOptions [ index ] ?. label ?? "" ;
260
262
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 ) ) ;
262
264
setEditingIndex ( null ) ;
265
+ setEditValue ( "" ) ;
263
266
fireEvent ( "edit" , { from :prev , to :val , index, value :next } ) ;
264
267
} ;
265
268
266
269
const cancelEdit = ( ) => {
267
270
setEditingIndex ( null ) ;
271
+ setEditValue ( "" ) ;
268
272
} ;
269
273
270
274
const deleteTag = ( index :number ) => {
271
275
const removed = displayOptions [ index ] ?. label ;
272
276
const next = displayOptions . filter ( ( _ , i ) => i !== index ) ;
273
- dispatch ( changeChildAction ( "runtimeOptions" , toJsonSafe ( next ) , false ) ) ;
277
+ dispatch ( changeChildAction ( "runtimeOptions" , next , false ) ) ;
274
278
fireEvent ( "delete" , { removed, index, value :next } ) ;
275
279
} ;
276
280
277
281
// Container keyboard handling for *adding* without inputs
278
282
const onContainerKeyDown :React . KeyboardEventHandler < HTMLDivElement > = ( e ) => {
279
- if ( ! props . allowEdit ) return ;
283
+ if ( ! props . editable ) return ;
280
284
281
285
const { key, ctrlKey, metaKey, altKey} = e ;
282
286
@@ -335,34 +339,32 @@ const multiTags = (function () {
335
339
{ displayOptions . map ( ( tag , index ) => {
336
340
const tagColor = getTagColor ( tag . label , displayOptions ) ;
337
341
const tagStyle = getTagStyle ( tag . label , displayOptions , props . style ) ;
338
- const isEditing = props . allowEdit && editingIndex === index ;
342
+ const isEditing = props . editable && editingIndex === index ;
339
343
340
344
return (
341
345
< StyledTag
342
346
key = { `tag-${ index } ` }
343
347
$style = { props . style }
344
348
$customStyle = { tagStyle }
349
+ icon = { tag . icon }
345
350
color = { tagColor }
346
- closable = { props . allowEdit }
351
+ closable = { props . editable }
347
352
onClose = { ( e ) => { e . preventDefault ( ) ; deleteTag ( index ) ; } }
348
353
onDoubleClick = { ( ) => startEdit ( index ) } // double-click to edit
349
354
onClick = { ( ) => onTagClick ( tag , index ) } // normal click event
350
355
>
351
356
{ isEditing ?(
352
- < EditableSpan
353
- ref = { editableRef }
354
- contentEditable
355
- suppressContentEditableWarning
357
+ < EditInput
358
+ autoFocus
359
+ value = { editValue }
360
+ onChange = { ( e ) => setEditValue ( e . target . value ) }
356
361
onBlur = { ( ) => confirmEdit ( index ) }
357
362
onKeyDown = { ( e ) => {
358
363
if ( e . key === "Enter" ) { e . preventDefault ( ) ; confirmEdit ( index ) ; }
359
364
if ( e . key === "Escape" ) { e . preventDefault ( ) ; cancelEdit ( ) ; }
360
- // stop container from also capturing these keystrokes
361
365
e . stopPropagation ( ) ;
362
366
} }
363
- >
364
- { tag . label }
365
- </ EditableSpan >
367
+ />
366
368
) :(
367
369
tag . label
368
370
) }
@@ -371,7 +373,7 @@ const multiTags = (function () {
371
373
} ) }
372
374
373
375
{ /* Draft chip appears only while typing; press Enter to commit, Esc to cancel */ }
374
- { props . allowEdit && draft && (
376
+ { props . editable && draft && (
375
377
< DraftTag $style = { props . style } $customStyle = { { } } color = "default" >
376
378
{ draft }
377
379
</ DraftTag >
@@ -385,7 +387,6 @@ const multiTags = (function () {
385
387
< Section name = { sectionNames . basic } >
386
388
{ children . options . propertyView ( { label :"Initial Tags (PropertyView)" } ) }
387
389
{ children . editable . propertyView ( { label :"Editable" } ) }
388
- { children . allowEdit . propertyView ( { label :"Allow Runtime Editing" } ) }
389
390
{ children . preventDuplicates . propertyView ( { label :"Prevent Duplicates (Runtime)" } ) }
390
391
{ children . allowEmptyEdits . propertyView ( { label :"Allow Empty Edit (Runtime)" } ) }
391
392
{ children . maxTags . propertyView ( { label :"Set Max Tags (Runtime) — true=50" } ) }