@nakashim/tp-custom 0.1.9 → 0.1.11

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.
@@ -1,6 +1,16 @@
1
1
  import { BladeApi, BladeController, createPlugin, parseRecord } from '@tweakpane/core';
2
2
  function sanitizeListStyle(style) {
3
- return style === 'ul' ? 'ul' : 'ol';
3
+ if (style === 'ul' || style === 'radio' || style === 'checkbox') {
4
+ return style;
5
+ }
6
+ return 'ol';
7
+ }
8
+ function resolveMultiSelectByStyle(style, requested) {
9
+ if (style === 'radio')
10
+ return false;
11
+ if (style === 'checkbox')
12
+ return true;
13
+ return requested;
4
14
  }
5
15
  function normalizeMaxItems(value) {
6
16
  if (!Number.isFinite(value)) {
@@ -35,6 +45,73 @@ function normalizeRows(value) {
35
45
  }
36
46
  return Math.min(24, normalized);
37
47
  }
48
+ function normalizeSelectionForItems(items, selectable, multiSelect, selectedIndices, selectedIndex) {
49
+ if (!selectable || items.length === 0)
50
+ return [];
51
+ const source = Array.isArray(selectedIndices) ? selectedIndices : [selectedIndex];
52
+ const next = Array.from(new Set(source
53
+ .map((v) => (Number.isFinite(v) ? Math.floor(v) : NaN))
54
+ .filter((v) => Number.isFinite(v) && v >= 0 && v < items.length))).sort((a, b) => a - b);
55
+ if (multiSelect)
56
+ return next;
57
+ return next.length > 0 ? [next[0]] : [];
58
+ }
59
+ function normalizeDynamicListBladeParams(params) {
60
+ return {
61
+ label: params.label,
62
+ emptyText: params.emptyText ?? 'Empty',
63
+ listStyle: sanitizeListStyle(params.listStyle),
64
+ items: params.items ?? [],
65
+ rows: normalizeRows(params.rows),
66
+ maxItems: normalizeMaxItems(params.maxItems),
67
+ overflowMode: sanitizeOverflowMode(params.overflowMode),
68
+ allowRemove: params.allowRemove ?? true,
69
+ allowReorder: params.allowReorder ?? true,
70
+ inlineLabel: params.inlineLabel ?? false,
71
+ showItemTooltip: params.showItemTooltip ?? false,
72
+ fullWidth: params.fullWidth ?? false,
73
+ clickable: params.clickable ?? false,
74
+ selectable: params.selectable ?? false,
75
+ allowDeselect: params.allowDeselect ?? false,
76
+ multiSelect: params.multiSelect ?? false,
77
+ selectedIndex: typeof params.selectedIndex === 'number' && Number.isFinite(params.selectedIndex)
78
+ ? params.selectedIndex
79
+ : -1,
80
+ selectedIndices: params.selectedIndices ?? [],
81
+ };
82
+ }
83
+ function normalizeSelectionState(items, selectable, multiSelect, value) {
84
+ const arr = Array.isArray(value) ? value : [value];
85
+ const selectedIndices = normalizeSelectionForItems(items, selectable, multiSelect, arr);
86
+ return {
87
+ selectedIndices,
88
+ selectedIndex: selectedIndices[0] ?? -1,
89
+ };
90
+ }
91
+ function isSameSelectionState(a, b) {
92
+ return (a.selectedIndex === b.selectedIndex &&
93
+ a.selectedIndices.length === b.selectedIndices.length &&
94
+ a.selectedIndices.every((value, idx) => value === b.selectedIndices[idx]));
95
+ }
96
+ function remapSelectionAfterRemove(selectedIndices, removedIndex, nextItemsLength) {
97
+ const hadRemoved = selectedIndices.includes(removedIndex);
98
+ const next = selectedIndices
99
+ .filter((i) => i !== removedIndex)
100
+ .map((i) => (i > removedIndex ? i - 1 : i));
101
+ if (hadRemoved && next.length === 0 && nextItemsLength > 0) {
102
+ next.push(Math.min(removedIndex, nextItemsLength - 1));
103
+ }
104
+ return next;
105
+ }
106
+ function remapSelectionAfterMove(selectedIndices, fromIndex, toIndex) {
107
+ return selectedIndices.map((i) => {
108
+ if (i === fromIndex)
109
+ return toIndex;
110
+ if (i === toIndex)
111
+ return fromIndex;
112
+ return i;
113
+ });
114
+ }
38
115
  class DynamicListBladeView {
39
116
  element;
40
117
  labelElement_;
@@ -43,6 +120,7 @@ class DynamicListBladeView {
43
120
  onListClick_;
44
121
  itemRows_;
45
122
  emptyItemElement_;
123
+ emptyTextElement_;
46
124
  items_;
47
125
  listStyle_;
48
126
  rows_;
@@ -51,9 +129,17 @@ class DynamicListBladeView {
51
129
  onRemove_;
52
130
  allowRemove_;
53
131
  allowReorder_;
132
+ emptyText_;
54
133
  inlineLabel_;
55
134
  showItemTooltip_;
56
135
  fullWidth_;
136
+ clickable_;
137
+ selectable_;
138
+ allowDeselect_;
139
+ multiSelect_;
140
+ selectedIndex_;
141
+ selectedIndices_;
142
+ onSelectionChange_;
57
143
  constructor(doc, config) {
58
144
  this.items_ = [...config.items];
59
145
  this.listStyle_ = config.listStyle;
@@ -62,35 +148,36 @@ class DynamicListBladeView {
62
148
  this.onRemove_ = config.onRemove;
63
149
  this.allowRemove_ = config.allowRemove;
64
150
  this.allowReorder_ = config.allowReorder;
151
+ this.emptyText_ = config.emptyText;
65
152
  this.inlineLabel_ = config.inlineLabel;
66
153
  this.showItemTooltip_ = config.showItemTooltip;
67
154
  this.fullWidth_ = config.fullWidth;
155
+ this.clickable_ = config.clickable;
156
+ this.selectable_ = config.selectable;
157
+ this.allowDeselect_ = config.allowDeselect;
158
+ this.multiSelect_ = config.multiSelect;
159
+ this.selectedIndices_ = [];
160
+ this.selectedIndex_ = -1;
161
+ this.setSelection_(config.selectedIndices ?? [config.selectedIndex]);
162
+ this.onSelectionChange_ = config.onSelectionChange;
68
163
  this.rows_ = normalizeRows(config.rows);
69
164
  this.itemRows_ = [];
70
165
  this.emptyItemElement_ = null;
166
+ this.emptyTextElement_ = null;
71
167
  this.onListClick_ = (ev) => {
72
168
  const target = ev.target;
73
169
  if (!target)
74
170
  return;
75
- const button = target.closest('.tp-dynlistblade_btn');
76
- if (!button || button.disabled)
77
- return;
78
- const item = button.closest('.tp-dynlistblade_i');
171
+ const item = target.closest('.tp-dynlistblade_i');
79
172
  if (!item)
80
173
  return;
81
174
  const index = Number(item.dataset.index);
82
175
  if (!Number.isInteger(index))
83
176
  return;
84
- const action = button.dataset.action;
85
- if (action === 'up') {
86
- this.onMoveUp_(index);
87
- }
88
- else if (action === 'down') {
89
- this.onMoveDown_(index);
90
- }
91
- else if (action === 'remove') {
92
- this.onRemove_(index);
93
- }
177
+ const button = target.closest('.tp-dynlistblade_btn');
178
+ if (button && this.handleActionButtonClick_(button, index))
179
+ return;
180
+ this.handleSelectionClick_(index);
94
181
  };
95
182
  const root = doc.createElement('div');
96
183
  root.className = 'tp-dynlistblade';
@@ -116,18 +203,11 @@ class DynamicListBladeView {
116
203
  }
117
204
  set items(next) {
118
205
  this.items_ = [...next];
206
+ this.setSelection_(this.selectedIndices_);
119
207
  this.render_();
120
208
  }
121
209
  setOptions(next) {
122
- if (typeof next.allowRemove === 'boolean') {
123
- this.allowRemove_ = next.allowRemove;
124
- }
125
- if (typeof next.allowReorder === 'boolean') {
126
- this.allowReorder_ = next.allowReorder;
127
- }
128
- if (typeof next.showItemTooltip === 'boolean') {
129
- this.showItemTooltip_ = next.showItemTooltip;
130
- }
210
+ this.applyOptionPatch_(next);
131
211
  this.render_();
132
212
  }
133
213
  set inlineLabel(next) {
@@ -138,6 +218,44 @@ class DynamicListBladeView {
138
218
  this.fullWidth_ = Boolean(next);
139
219
  this.element.classList.toggle('tp-dynlistblade-full', this.fullWidth_);
140
220
  }
221
+ get selectedIndex() {
222
+ return this.selectedIndex_;
223
+ }
224
+ set selectedIndex(next) {
225
+ this.setSelection_([next]);
226
+ this.render_();
227
+ }
228
+ get selectedIndices() {
229
+ return [...this.selectedIndices_];
230
+ }
231
+ set selectedIndices(next) {
232
+ this.setSelection_(next);
233
+ this.render_();
234
+ }
235
+ get selectable() {
236
+ return this.selectable_;
237
+ }
238
+ set selectable(next) {
239
+ this.setOptions({ selectable: next });
240
+ }
241
+ get clickable() {
242
+ return this.clickable_;
243
+ }
244
+ set clickable(next) {
245
+ this.setOptions({ clickable: next });
246
+ }
247
+ get allowDeselect() {
248
+ return this.allowDeselect_;
249
+ }
250
+ set allowDeselect(next) {
251
+ this.setOptions({ allowDeselect: next });
252
+ }
253
+ get multiSelect() {
254
+ return this.multiSelect_;
255
+ }
256
+ set multiSelect(next) {
257
+ this.setOptions({ multiSelect: next });
258
+ }
141
259
  get rows() {
142
260
  return this.rows_;
143
261
  }
@@ -164,7 +282,8 @@ class DynamicListBladeView {
164
282
  this.render_();
165
283
  }
166
284
  createListElement_(doc, style) {
167
- const list = doc.createElement(style);
285
+ const elementTag = style === 'ol' ? 'ol' : 'ul';
286
+ const list = doc.createElement(elementTag);
168
287
  list.className = `tp-dynlistblade_list tp-dynlistblade_list-${style}`;
169
288
  list.addEventListener('click', this.onListClick_);
170
289
  return list;
@@ -182,6 +301,7 @@ class DynamicListBladeView {
182
301
  this.element.style.setProperty('--tp-dynlistblade-rows-gap', String(Math.max(0, rows - 1)));
183
302
  }
184
303
  render_() {
304
+ this.updateListStateClasses_();
185
305
  if (this.items_.length === 0) {
186
306
  this.ensureEmptyState_();
187
307
  return;
@@ -190,25 +310,32 @@ class DynamicListBladeView {
190
310
  this.ensureRowCount_(this.items_.length);
191
311
  this.items_.forEach((text, index) => {
192
312
  const row = this.itemRows_[index];
193
- row.item.dataset.index = String(index);
194
- row.text.textContent = text;
195
- if (this.showItemTooltip_) {
196
- row.text.title = text;
197
- }
198
- else {
199
- row.text.removeAttribute('title');
200
- }
201
- const showActions = this.allowRemove_ || this.allowReorder_;
202
- row.actions.style.display = showActions ? '' : 'none';
203
- row.upButton.style.display = this.allowReorder_ ? '' : 'none';
204
- row.downButton.style.display = this.allowReorder_ ? '' : 'none';
205
- row.removeButton.style.display = this.allowRemove_ ? '' : 'none';
206
- row.upButton.disabled = !this.allowReorder_ || index === 0;
207
- row.downButton.disabled = !this.allowReorder_ || index === this.items_.length - 1;
208
- row.removeButton.disabled = !this.allowRemove_;
313
+ this.renderRowContent_(row, index, text);
314
+ this.renderRowActions_(row, index);
209
315
  this.listElement_.appendChild(row.item);
210
316
  });
211
317
  }
318
+ renderRowContent_(row, index, text) {
319
+ row.item.dataset.index = String(index);
320
+ row.item.classList.toggle('tp-dynlistblade_i-selected', this.selectable_ && this.selectedIndices_.includes(index));
321
+ row.text.textContent = text;
322
+ if (this.showItemTooltip_) {
323
+ row.text.title = text;
324
+ }
325
+ else {
326
+ row.text.removeAttribute('title');
327
+ }
328
+ }
329
+ renderRowActions_(row, index) {
330
+ const showActions = this.allowRemove_ || this.allowReorder_;
331
+ row.actions.style.display = showActions ? '' : 'none';
332
+ row.upButton.style.display = this.allowReorder_ ? '' : 'none';
333
+ row.downButton.style.display = this.allowReorder_ ? '' : 'none';
334
+ row.removeButton.style.display = this.allowRemove_ ? '' : 'none';
335
+ row.upButton.disabled = !this.allowReorder_ || index === 0;
336
+ row.downButton.disabled = !this.allowReorder_ || index === this.items_.length - 1;
337
+ row.removeButton.disabled = !this.allowRemove_;
338
+ }
212
339
  ensureEmptyState_() {
213
340
  if (!this.emptyItemElement_) {
214
341
  const emptyItem = this.element.ownerDocument.createElement('li');
@@ -218,9 +345,13 @@ class DynamicListBladeView {
218
345
  emptyItem.appendChild(emptyRow);
219
346
  const emptyText = this.element.ownerDocument.createElement('span');
220
347
  emptyText.className = 'tp-dynlistblade_t tp-dynlistblade_t-empty';
221
- emptyText.textContent = 'Empty';
348
+ emptyText.textContent = this.emptyText_;
222
349
  emptyRow.appendChild(emptyText);
223
350
  this.emptyItemElement_ = emptyItem;
351
+ this.emptyTextElement_ = emptyText;
352
+ }
353
+ if (this.emptyTextElement_) {
354
+ this.emptyTextElement_.textContent = this.emptyText_;
224
355
  }
225
356
  this.itemRows_.forEach((row) => {
226
357
  if (row.item.parentElement === this.listElement_) {
@@ -278,6 +409,105 @@ class DynamicListBladeView {
278
409
  button.title = title;
279
410
  return button;
280
411
  }
412
+ updateListStateClasses_() {
413
+ this.listElement_.classList.toggle('tp-dynlistblade_list-clickable', this.clickable_);
414
+ this.listElement_.classList.toggle('tp-dynlistblade_list-selectable', this.selectable_);
415
+ this.listElement_.classList.toggle('tp-dynlistblade_list-has-selection', this.selectable_ && this.selectedIndices_.length > 0);
416
+ this.listElement_.classList.toggle('tp-dynlistblade_list-multiselect', this.multiSelect_);
417
+ }
418
+ normalizeSelectedIndices_(value) {
419
+ if (!this.selectable_ || this.items_.length === 0)
420
+ return [];
421
+ const arr = Array.isArray(value) ? value : [];
422
+ const next = Array.from(new Set(arr
423
+ .map((v) => (Number.isFinite(v) ? Math.floor(v) : NaN))
424
+ .filter((v) => Number.isFinite(v) && v >= 0 && v < this.items_.length)));
425
+ next.sort((a, b) => a - b);
426
+ if (this.multiSelect_)
427
+ return next;
428
+ return next.length > 0 ? [next[0]] : [];
429
+ }
430
+ setSelection_(nextSelection) {
431
+ this.selectedIndices_ = this.normalizeSelectedIndices_(nextSelection);
432
+ this.selectedIndex_ = this.selectedIndices_[0] ?? -1;
433
+ }
434
+ applyOptionPatch_(next) {
435
+ if (typeof next.allowRemove === 'boolean') {
436
+ this.allowRemove_ = next.allowRemove;
437
+ }
438
+ if (typeof next.allowReorder === 'boolean') {
439
+ this.allowReorder_ = next.allowReorder;
440
+ }
441
+ if (typeof next.showItemTooltip === 'boolean') {
442
+ this.showItemTooltip_ = next.showItemTooltip;
443
+ }
444
+ if (typeof next.emptyText === 'string') {
445
+ this.emptyText_ = next.emptyText;
446
+ }
447
+ if (typeof next.clickable === 'boolean') {
448
+ this.clickable_ = next.clickable;
449
+ }
450
+ if (typeof next.selectable === 'boolean') {
451
+ this.selectable_ = next.selectable;
452
+ this.setSelection_(this.selectable_ ? this.selectedIndices_ : []);
453
+ }
454
+ if (typeof next.allowDeselect === 'boolean') {
455
+ this.allowDeselect_ = next.allowDeselect;
456
+ }
457
+ if (typeof next.multiSelect === 'boolean') {
458
+ this.multiSelect_ = next.multiSelect;
459
+ const nextSelection = !this.multiSelect_ && this.selectedIndices_.length > 1
460
+ ? this.selectedIndices_.slice(0, 1)
461
+ : this.selectedIndices_;
462
+ this.setSelection_(nextSelection);
463
+ }
464
+ }
465
+ handleActionButtonClick_(button, index) {
466
+ if (button.disabled)
467
+ return true;
468
+ const action = button.dataset.action;
469
+ if (action === 'up') {
470
+ this.onMoveUp_(index);
471
+ return true;
472
+ }
473
+ if (action === 'down') {
474
+ this.onMoveDown_(index);
475
+ return true;
476
+ }
477
+ if (action === 'remove') {
478
+ this.onRemove_(index);
479
+ return true;
480
+ }
481
+ return false;
482
+ }
483
+ handleSelectionClick_(index) {
484
+ if (!this.selectable_)
485
+ return;
486
+ let nextSelection = [...this.selectedIndices_];
487
+ if (this.multiSelect_) {
488
+ const next = new Set(this.selectedIndices_);
489
+ if (next.has(index)) {
490
+ next.delete(index);
491
+ }
492
+ else {
493
+ next.add(index);
494
+ }
495
+ nextSelection = this.normalizeSelectedIndices_([...next]);
496
+ }
497
+ else if (this.selectedIndex_ === index && this.allowDeselect_) {
498
+ nextSelection = [];
499
+ }
500
+ else {
501
+ nextSelection = this.normalizeSelectedIndices_([index]);
502
+ }
503
+ const same = nextSelection.length === this.selectedIndices_.length &&
504
+ nextSelection.every((value, idx) => value === this.selectedIndices_[idx]);
505
+ if (!same) {
506
+ this.setSelection_(nextSelection);
507
+ this.render_();
508
+ }
509
+ this.onSelectionChange_([...this.selectedIndices_]);
510
+ }
281
511
  }
282
512
  class DynamicListBladeController extends BladeController {
283
513
  items_;
@@ -286,24 +516,48 @@ class DynamicListBladeController extends BladeController {
286
516
  overflowMode_;
287
517
  allowRemove_;
288
518
  allowReorder_;
519
+ emptyText_;
289
520
  inlineLabel_;
290
521
  showItemTooltip_;
291
522
  fullWidth_;
523
+ clickable_;
524
+ selectable_;
525
+ allowDeselect_;
526
+ listStyle_;
527
+ requestedMultiSelect_;
528
+ multiSelect_;
529
+ selectedIndex_;
530
+ selectedIndices_;
531
+ actionHandlers_;
292
532
  constructor(doc, config) {
293
533
  const initialItems = clampItemsToMax([...config.items], config.maxItems, true);
534
+ const initialListStyle = sanitizeListStyle(config.listStyle);
535
+ const initialSelectable = Boolean(config.selectable);
536
+ const initialRequestedMultiSelect = Boolean(config.multiSelect);
537
+ const initialMultiSelect = resolveMultiSelectByStyle(initialListStyle, initialRequestedMultiSelect);
538
+ const initialSelectedIndices = normalizeSelectionForItems(initialItems, initialSelectable, initialMultiSelect, config.selectedIndices, config.selectedIndex);
539
+ const initialSelectedIndex = initialSelectedIndices[0] ?? -1;
294
540
  const view = new DynamicListBladeView(doc, {
295
541
  label: config.label,
296
- listStyle: config.listStyle,
542
+ emptyText: config.emptyText,
543
+ listStyle: initialListStyle,
297
544
  items: initialItems,
298
545
  rows: normalizeRows(config.rows),
299
546
  onMoveUp: (index) => this.moveUp(index),
300
547
  onMoveDown: (index) => this.moveDown(index),
301
548
  onRemove: (index) => this.remove(index),
549
+ onSelectionChange: (indices) => this.selectMany(indices),
302
550
  allowRemove: config.allowRemove,
303
551
  allowReorder: config.allowReorder,
304
552
  inlineLabel: config.inlineLabel,
305
553
  showItemTooltip: config.showItemTooltip,
306
554
  fullWidth: config.fullWidth,
555
+ clickable: config.clickable,
556
+ selectable: initialSelectable,
557
+ allowDeselect: config.allowDeselect,
558
+ multiSelect: initialMultiSelect,
559
+ selectedIndex: initialSelectedIndex,
560
+ selectedIndices: initialSelectedIndices,
307
561
  });
308
562
  super({
309
563
  blade: config.blade,
@@ -316,9 +570,19 @@ class DynamicListBladeController extends BladeController {
316
570
  this.overflowMode_ = sanitizeOverflowMode(config.overflowMode);
317
571
  this.allowRemove_ = config.allowRemove;
318
572
  this.allowReorder_ = config.allowReorder;
573
+ this.emptyText_ = config.emptyText;
319
574
  this.inlineLabel_ = config.inlineLabel;
320
575
  this.showItemTooltip_ = config.showItemTooltip;
321
576
  this.fullWidth_ = config.fullWidth;
577
+ this.clickable_ = Boolean(config.clickable);
578
+ this.selectable_ = initialSelectable;
579
+ this.allowDeselect_ = Boolean(config.allowDeselect);
580
+ this.listStyle_ = initialListStyle;
581
+ this.requestedMultiSelect_ = initialRequestedMultiSelect;
582
+ this.multiSelect_ = initialMultiSelect;
583
+ this.selectedIndex_ = initialSelectedIndex;
584
+ this.selectedIndices_ = initialSelectedIndices;
585
+ this.actionHandlers_ = new Set();
322
586
  }
323
587
  get items() {
324
588
  return [...this.items_];
@@ -332,7 +596,8 @@ class DynamicListBladeController extends BladeController {
332
596
  }
333
597
  set items(next) {
334
598
  this.items_ = clampItemsToMax([...next], this.maxItems_, true);
335
- this.view.items = this.items_;
599
+ this.setNormalizedSelection_(this.selectedIndices_);
600
+ this.refreshAfterItemsChange_();
336
601
  }
337
602
  get maxItems() {
338
603
  return this.maxItems_;
@@ -340,7 +605,8 @@ class DynamicListBladeController extends BladeController {
340
605
  set maxItems(next) {
341
606
  this.maxItems_ = normalizeMaxItems(next);
342
607
  this.items_ = clampItemsToMax(this.items_, this.maxItems_, true);
343
- this.view.items = this.items_;
608
+ this.setNormalizedSelection_(this.selectedIndices_);
609
+ this.refreshAfterItemsChange_();
344
610
  }
345
611
  get overflowMode() {
346
612
  return this.overflowMode_;
@@ -362,6 +628,49 @@ class DynamicListBladeController extends BladeController {
362
628
  this.allowReorder_ = Boolean(next);
363
629
  this.view.setOptions({ allowReorder: this.allowReorder_ });
364
630
  }
631
+ get selectable() {
632
+ return this.selectable_;
633
+ }
634
+ set selectable(next) {
635
+ this.selectable_ = Boolean(next);
636
+ this.view.selectable = this.selectable_;
637
+ this.view.multiSelect = this.multiSelect_;
638
+ this.setNormalizedSelection_(this.selectable_ ? this.selectedIndices_ : []);
639
+ this.refreshAfterSelectionChange_();
640
+ }
641
+ get clickable() {
642
+ return this.clickable_;
643
+ }
644
+ set clickable(next) {
645
+ this.clickable_ = Boolean(next);
646
+ this.view.setOptions({ clickable: this.clickable_ });
647
+ }
648
+ get allowDeselect() {
649
+ return this.allowDeselect_;
650
+ }
651
+ set allowDeselect(next) {
652
+ this.allowDeselect_ = Boolean(next);
653
+ this.view.setOptions({ allowDeselect: this.allowDeselect_ });
654
+ }
655
+ get multiSelect() {
656
+ return this.multiSelect_;
657
+ }
658
+ set multiSelect(next) {
659
+ this.requestedMultiSelect_ = Boolean(next);
660
+ this.applySelectionModeFromStyle_();
661
+ }
662
+ get selectedIndex() {
663
+ return this.selectedIndex_;
664
+ }
665
+ set selectedIndex(next) {
666
+ this.select(next);
667
+ }
668
+ get selectedIndices() {
669
+ return [...this.selectedIndices_];
670
+ }
671
+ set selectedIndices(next) {
672
+ this.selectMany(next);
673
+ }
365
674
  get showItemTooltip() {
366
675
  return this.showItemTooltip_;
367
676
  }
@@ -369,6 +678,13 @@ class DynamicListBladeController extends BladeController {
369
678
  this.showItemTooltip_ = Boolean(next);
370
679
  this.view.setOptions({ showItemTooltip: this.showItemTooltip_ });
371
680
  }
681
+ get emptyText() {
682
+ return this.emptyText_;
683
+ }
684
+ set emptyText(next) {
685
+ this.emptyText_ = String(next);
686
+ this.view.setOptions({ emptyText: this.emptyText_ });
687
+ }
372
688
  get inlineLabel() {
373
689
  return this.inlineLabel_;
374
690
  }
@@ -384,14 +700,22 @@ class DynamicListBladeController extends BladeController {
384
700
  this.view.fullWidth = this.fullWidth_;
385
701
  }
386
702
  get listStyle() {
387
- return this.view.listStyle;
703
+ return this.listStyle_;
388
704
  }
389
705
  set listStyle(next) {
390
- this.view.listStyle = next;
706
+ this.listStyle_ = sanitizeListStyle(next);
707
+ this.view.listStyle = this.listStyle_;
708
+ this.applySelectionModeFromStyle_();
391
709
  }
392
710
  setLabel(next) {
393
711
  this.view.label = next;
394
712
  }
713
+ onAction(handler) {
714
+ this.actionHandlers_.add(handler);
715
+ return () => {
716
+ this.actionHandlers_.delete(handler);
717
+ };
718
+ }
395
719
  add(item) {
396
720
  if (Number.isFinite(this.maxItems_)) {
397
721
  const max = this.maxItems_;
@@ -403,30 +727,131 @@ class DynamicListBladeController extends BladeController {
403
727
  }
404
728
  }
405
729
  this.items_.push(item);
406
- this.view.items = this.items_;
730
+ this.setNormalizedSelection_(this.selectedIndices_);
731
+ this.refreshAfterItemsChange_();
407
732
  return true;
408
733
  }
409
734
  remove(index) {
410
735
  if (index < 0 || index >= this.items_.length)
411
736
  return;
737
+ const removedItem = this.items_[index];
738
+ const wasSelected = this.selectedIndices_.includes(index);
412
739
  this.items_.splice(index, 1);
413
- this.view.items = this.items_;
740
+ const next = remapSelectionAfterRemove(this.selectedIndices_, index, this.items_.length);
741
+ this.setNormalizedSelection_(next);
742
+ this.refreshAfterItemsChange_();
743
+ if (wasSelected) {
744
+ this.emitSelectAction_();
745
+ }
746
+ if (typeof removedItem === 'string') {
747
+ this.emitRemoveAction_(index, removedItem);
748
+ }
414
749
  }
415
750
  moveUp(index) {
416
- if (index <= 0 || index >= this.items_.length)
417
- return;
418
- const tmp = this.items_[index - 1];
419
- this.items_[index - 1] = this.items_[index];
420
- this.items_[index] = tmp;
421
- this.view.items = this.items_;
751
+ this.moveBy_(index, -1);
422
752
  }
423
753
  moveDown(index) {
424
- if (index < 0 || index >= this.items_.length - 1)
425
- return;
426
- const tmp = this.items_[index + 1];
427
- this.items_[index + 1] = this.items_[index];
428
- this.items_[index] = tmp;
754
+ this.moveBy_(index, 1);
755
+ }
756
+ select(index) {
757
+ this.applySelectionAndEmit_([index]);
758
+ }
759
+ selectMany(indices) {
760
+ this.applySelectionAndEmit_(indices);
761
+ }
762
+ applySelectionModeFromStyle_() {
763
+ this.multiSelect_ = resolveMultiSelectByStyle(this.listStyle_, this.requestedMultiSelect_);
764
+ if (!this.multiSelect_ && this.selectedIndices_.length > 1) {
765
+ this.selectedIndices_ = this.selectedIndices_.slice(0, 1);
766
+ }
767
+ this.setNormalizedSelection_(this.selectedIndices_);
768
+ this.view.multiSelect = this.multiSelect_;
769
+ this.refreshAfterSelectionChange_();
770
+ }
771
+ setNormalizedSelection_(nextSelection) {
772
+ const next = normalizeSelectionState(this.items_, this.selectable_, this.multiSelect_, nextSelection);
773
+ this.selectedIndices_ = next.selectedIndices;
774
+ this.selectedIndex_ = next.selectedIndex;
775
+ }
776
+ refreshAfterSelectionChange_() {
777
+ this.view.selectedIndex = this.selectedIndex_;
778
+ this.view.selectedIndices = this.selectedIndices_;
779
+ }
780
+ refreshAfterItemsChange_() {
429
781
  this.view.items = this.items_;
782
+ this.refreshAfterSelectionChange_();
783
+ }
784
+ applySelectionAndEmit_(nextSelection) {
785
+ const next = normalizeSelectionState(this.items_, this.selectable_, this.multiSelect_, nextSelection);
786
+ if (isSameSelectionState(next, {
787
+ selectedIndex: this.selectedIndex_,
788
+ selectedIndices: this.selectedIndices_,
789
+ })) {
790
+ this.emitSelectAction_();
791
+ return;
792
+ }
793
+ this.selectedIndices_ = next.selectedIndices;
794
+ this.selectedIndex_ = next.selectedIndex;
795
+ this.refreshAfterSelectionChange_();
796
+ this.emitSelectAction_();
797
+ }
798
+ moveBy_(fromIndex, delta) {
799
+ const toIndex = fromIndex + delta;
800
+ if (fromIndex < 0 || fromIndex >= this.items_.length)
801
+ return;
802
+ if (toIndex < 0 || toIndex >= this.items_.length)
803
+ return;
804
+ const movedItem = this.items_[fromIndex];
805
+ const tmp = this.items_[toIndex];
806
+ this.items_[toIndex] = this.items_[fromIndex];
807
+ this.items_[fromIndex] = tmp;
808
+ this.setNormalizedSelection_(remapSelectionAfterMove(this.selectedIndices_, fromIndex, toIndex));
809
+ this.refreshAfterItemsChange_();
810
+ if (typeof movedItem === 'string') {
811
+ this.emitMoveAction_(fromIndex, toIndex, movedItem);
812
+ }
813
+ }
814
+ emitSelectAction_() {
815
+ this.emitAction_(this.toSelectActionEvent_());
816
+ }
817
+ emitRemoveAction_(index, item) {
818
+ this.emitAction_(this.toRemoveActionEvent_(index, item));
819
+ }
820
+ emitMoveAction_(fromIndex, toIndex, item) {
821
+ this.emitAction_(this.toMoveActionEvent_(fromIndex, toIndex, item));
822
+ }
823
+ toSelectActionEvent_() {
824
+ const item = this.selectedIndex_ >= 0 && this.selectedIndex_ < this.items_.length
825
+ ? this.items_[this.selectedIndex_]
826
+ : null;
827
+ const selectedItems = this.selectedIndices_.map((i) => this.items_[i]).filter((v) => typeof v === 'string');
828
+ return {
829
+ type: 'select',
830
+ index: this.selectedIndex_,
831
+ item,
832
+ indices: [...this.selectedIndices_],
833
+ items: selectedItems,
834
+ };
835
+ }
836
+ toRemoveActionEvent_(index, item) {
837
+ return {
838
+ type: 'remove',
839
+ index,
840
+ item,
841
+ };
842
+ }
843
+ toMoveActionEvent_(fromIndex, toIndex, item) {
844
+ return {
845
+ type: 'move',
846
+ fromIndex,
847
+ toIndex,
848
+ item,
849
+ };
850
+ }
851
+ emitAction_(event) {
852
+ this.actionHandlers_.forEach((handler) => {
853
+ handler(event);
854
+ });
430
855
  }
431
856
  }
432
857
  export class DynamicListBladeApi extends BladeApi {
@@ -478,6 +903,48 @@ export class DynamicListBladeApi extends BladeApi {
478
903
  set showItemTooltip(next) {
479
904
  this.controller.showItemTooltip = Boolean(next);
480
905
  }
906
+ get emptyText() {
907
+ return this.controller.emptyText;
908
+ }
909
+ set emptyText(next) {
910
+ this.controller.emptyText = String(next);
911
+ }
912
+ get selectable() {
913
+ return this.controller.selectable;
914
+ }
915
+ set selectable(next) {
916
+ this.controller.selectable = Boolean(next);
917
+ }
918
+ get clickable() {
919
+ return this.controller.clickable;
920
+ }
921
+ set clickable(next) {
922
+ this.controller.clickable = Boolean(next);
923
+ }
924
+ get allowDeselect() {
925
+ return this.controller.allowDeselect;
926
+ }
927
+ set allowDeselect(next) {
928
+ this.controller.allowDeselect = Boolean(next);
929
+ }
930
+ get multiSelect() {
931
+ return this.controller.multiSelect;
932
+ }
933
+ set multiSelect(next) {
934
+ this.controller.multiSelect = Boolean(next);
935
+ }
936
+ get selectedIndex() {
937
+ return this.controller.selectedIndex;
938
+ }
939
+ set selectedIndex(next) {
940
+ this.controller.selectedIndex = Number(next);
941
+ }
942
+ get selectedIndices() {
943
+ return this.controller.selectedIndices;
944
+ }
945
+ set selectedIndices(next) {
946
+ this.controller.selectedIndices = Array.isArray(next) ? next : [];
947
+ }
481
948
  get fullWidth() {
482
949
  return this.controller.fullWidth;
483
950
  }
@@ -505,6 +972,9 @@ export class DynamicListBladeApi extends BladeApi {
505
972
  moveDown(index) {
506
973
  this.controller.moveDown(index);
507
974
  }
975
+ onAction(handler) {
976
+ return this.controller.onAction(handler);
977
+ }
508
978
  }
509
979
  const DynamicListBladePlugin = createPlugin({
510
980
  id: 'dynamiclistblade',
@@ -513,7 +983,10 @@ const DynamicListBladePlugin = createPlugin({
513
983
  const result = parseRecord(params, (p) => ({
514
984
  view: p.required.constant('dynamiclistblade'),
515
985
  label: p.optional.string,
516
- listStyle: p.optional.custom((value) => value === 'ul' || value === 'ol' ? value : undefined),
986
+ emptyText: p.optional.string,
987
+ listStyle: p.optional.custom((value) => value === 'ul' || value === 'ol' || value === 'radio' || value === 'checkbox'
988
+ ? value
989
+ : undefined),
517
990
  items: p.optional.array(p.required.string),
518
991
  rows: p.optional.number,
519
992
  maxItems: p.optional.number,
@@ -523,41 +996,27 @@ const DynamicListBladePlugin = createPlugin({
523
996
  inlineLabel: p.optional.boolean,
524
997
  showItemTooltip: p.optional.boolean,
525
998
  fullWidth: p.optional.boolean,
999
+ clickable: p.optional.boolean,
1000
+ selectable: p.optional.boolean,
1001
+ allowDeselect: p.optional.boolean,
1002
+ multiSelect: p.optional.boolean,
1003
+ selectedIndex: p.optional.number,
1004
+ selectedIndices: p.optional.array(p.required.number),
526
1005
  }));
527
1006
  if (!result) {
528
1007
  return null;
529
1008
  }
1009
+ const normalized = normalizeDynamicListBladeParams(result);
530
1010
  return {
531
- params: {
532
- ...result,
533
- listStyle: sanitizeListStyle(result.listStyle),
534
- items: result.items ?? [],
535
- rows: normalizeRows(result.rows),
536
- maxItems: normalizeMaxItems(result.maxItems),
537
- overflowMode: sanitizeOverflowMode(result.overflowMode),
538
- allowRemove: result.allowRemove ?? true,
539
- allowReorder: result.allowReorder ?? true,
540
- inlineLabel: result.inlineLabel ?? false,
541
- showItemTooltip: result.showItemTooltip ?? false,
542
- fullWidth: result.fullWidth ?? false,
543
- },
1011
+ params: normalized,
544
1012
  };
545
1013
  },
546
1014
  controller(args) {
1015
+ const normalized = normalizeDynamicListBladeParams(args.params);
547
1016
  return new DynamicListBladeController(args.document, {
548
1017
  blade: args.blade,
549
1018
  viewProps: args.viewProps,
550
- label: args.params.label,
551
- listStyle: sanitizeListStyle(args.params.listStyle),
552
- items: args.params.items ?? [],
553
- rows: normalizeRows(args.params.rows),
554
- maxItems: normalizeMaxItems(args.params.maxItems),
555
- overflowMode: sanitizeOverflowMode(args.params.overflowMode),
556
- allowRemove: args.params.allowRemove ?? true,
557
- allowReorder: args.params.allowReorder ?? true,
558
- inlineLabel: args.params.inlineLabel ?? false,
559
- showItemTooltip: args.params.showItemTooltip ?? false,
560
- fullWidth: args.params.fullWidth ?? false,
1019
+ ...normalized,
561
1020
  });
562
1021
  },
563
1022
  api(args) {
@@ -642,14 +1101,31 @@ export const DynamicListBladePluginBundle = {
642
1101
  .tp-dynlistblade_i {
643
1102
  min-width: 0;
644
1103
  }
645
- .tp-dynlistblade_i::after {
1104
+ .tp-dynlistblade_list-clickable .tp-dynlistblade_i,
1105
+ .tp-dynlistblade_list-selectable .tp-dynlistblade_i {
1106
+ cursor: pointer;
1107
+ }
1108
+ .tp-dynlistblade_list-clickable .tp-dynlistblade_row::after,
1109
+ .tp-dynlistblade_list-selectable .tp-dynlistblade_row::after {
646
1110
  content: '';
647
1111
  display: block;
648
- width: 100%;
649
- height: 1px;
650
- border-radius: 0.5px;
651
- background: rgba(255, 255, 255, 0.03);
652
- margin-block: 1px;
1112
+ width: calc(100% + (var(--tp-c-container-padding-inline) + 2px));
1113
+ height: 100%;
1114
+ border-radius: var(--tp-c-base-border-radius);
1115
+ background: var(--tp-button-background-color);
1116
+ position: absolute;
1117
+ pointer-events: none;
1118
+ margin-inline: calc((var(--tp-c-container-padding-inline) + 2px) / 2 * -1);
1119
+ opacity: 0;
1120
+ }
1121
+ .tp-dynlistblade_list-clickable .tp-dynlistblade_row:hover::after,
1122
+ .tp-dynlistblade_list-selectable .tp-dynlistblade_row:hover::after,
1123
+ .tp-dynlistblade_i.tp-dynlistblade_i-selected .tp-dynlistblade_row::after {
1124
+ opacity: 0.2;
1125
+ }
1126
+ .tp-dynlistblade_i.tp-dynlistblade_i-selected .tp-dynlistblade_row > * {
1127
+ position: relative;
1128
+ z-index: 1;
653
1129
  }
654
1130
  .tp-dynlistblade_i:last-child::after {
655
1131
  display: none;
@@ -659,6 +1135,7 @@ export const DynamicListBladePluginBundle = {
659
1135
  align-items: center;
660
1136
  vertical-align: top;
661
1137
  width: 100%;
1138
+ position: relative;
662
1139
  }
663
1140
  .tp-dynlistblade_row::before {
664
1141
  content: '';
@@ -667,6 +1144,8 @@ export const DynamicListBladePluginBundle = {
667
1144
  display: inline-block;
668
1145
  min-width: 10px;
669
1146
  margin-right: 4px;
1147
+ position: relative;
1148
+ z-index: 1;
670
1149
  }
671
1150
  .tp-dynlistblade_list-ol .tp-dynlistblade_row::before {
672
1151
  content: counter(list-item) '';
@@ -674,6 +1153,18 @@ export const DynamicListBladePluginBundle = {
674
1153
  .tp-dynlistblade_list-ul .tp-dynlistblade_row::before {
675
1154
  content: '-';
676
1155
  }
1156
+ .tp-dynlistblade_list-radio .tp-dynlistblade_row::before {
1157
+ content: '○';
1158
+ }
1159
+ .tp-dynlistblade_list-checkbox .tp-dynlistblade_row::before {
1160
+ content: '☐';
1161
+ }
1162
+ .tp-dynlistblade_list-radio .tp-dynlistblade_i-selected .tp-dynlistblade_row::before {
1163
+ content: '●';
1164
+ }
1165
+ .tp-dynlistblade_list-checkbox .tp-dynlistblade_i-selected .tp-dynlistblade_row::before {
1166
+ content: '◼︎';
1167
+ }
677
1168
  .tp-dynlistblade_t {
678
1169
  width: 100%;
679
1170
  flex: 1;