@cntwg/html-ctrls-lists 0.0.27

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/lib/list.js ADDED
@@ -0,0 +1,1137 @@
1
+ // [v0.1.074-20251004]
2
+
3
+ // === module init block ===
4
+
5
+ const {
6
+ readAsNumber,
7
+ isPlainObject,
8
+ } = require('@ygracs/bsfoc-lib-js');
9
+
10
+ const {
11
+ TItemsListEx,
12
+ } = require('@ygracs/lists-lib-js');
13
+
14
+ const {
15
+ isHTMLElement, isSelectedHTMLElement, isHiddenHTMLElement,
16
+ showHTMLElement, hideHTMLElement,
17
+ selectHTMLElement, unselectHTMLElement,
18
+ markHTMLElementAsCurrent, unmarkCurrentHTMLElement,
19
+ readAsAttrValue, valueToClassList,
20
+ eventHelper, // [!] not oficialy exported yet
21
+ CSS_CLASS_STRING,
22
+ } = require('@cntwg/html-helper');
23
+
24
+ const { THtmlStubItemsSet } = require('./lists-stubs.js');
25
+
26
+ // === module inner block ===
27
+
28
+ const {
29
+ pushEventHandler, triggerEventHandler,
30
+ } = eventHelper;
31
+
32
+ const {
33
+ CSS_CLASS_DISABLED,
34
+ } = CSS_CLASS_STRING;
35
+
36
+ /**
37
+ * Searches an element in a list by a given attributes of that element.
38
+ * @function srchListElementByAttr
39
+ * @param {TItemsListEx} list
40
+ * @param {string} name - target attribute name
41
+ * @param {string} [value=""] - target attribute value
42
+ * @returns {object}
43
+ * @inner
44
+ */
45
+ function srchListElementByAttr(list, name, value = '') {
46
+ const _value = readAsAttrValue(value);
47
+ let item = null;
48
+ let index = -1;
49
+ let count = list.count;
50
+ let isACCEPTED = false;
51
+ if (count > 0 && name !== '' && _value !== null) {
52
+ const acceptAnyVal = _value === '';
53
+ for (let i = 0; i < count; i++) {
54
+ item = list.getItem(i);
55
+ if (
56
+ isHTMLElement(item)
57
+ && item.hasAttribute(name)
58
+ && (acceptAnyVal || item.getAttribute(name) === value)
59
+ ) {
60
+ index = i;
61
+ isACCEPTED = true;
62
+ break;
63
+ };
64
+ };
65
+ };
66
+ return isACCEPTED ? { index, item } : { index, item: null };
67
+ };
68
+
69
+ // === module main block ===
70
+
71
+ /***
72
+ * (* constant definitions *)
73
+ */
74
+
75
+ const ILC_SMODE_DEF = 0;
76
+ const ILC_SMODE_SHFT = 1;
77
+ const ILC_SMODE_CTRL = 2;
78
+
79
+ /***
80
+ * (* function definitions *)
81
+ */
82
+
83
+ /***
84
+ * (* class definitions *)
85
+ */
86
+
87
+ /**
88
+ * An options set for `THtmlItemsListContainer`-class
89
+ * @typedef {Object} OPT_hlconsett
90
+ * @property {boolean} [autoHideNewItems=false] - indicates whether to hide
91
+ * a new element
92
+ * @property {boolean} [markCurrentItem=false] - indicates whether to mark
93
+ * a current element
94
+ * @property {(string|string[])} [itemBaseClassID] - contains a base class
95
+ * attributes applayed to each a newly added list member
96
+ */
97
+
98
+ /**
99
+ * @classdesc This class implements an interfaco for a list container
100
+ */
101
+ class THtmlItemsListContainer {
102
+ /** @type {?HTMLElement} */
103
+ #_host;
104
+ /** @type {TItemsListEx} */
105
+ #_items;
106
+ /** @type {OPT_hlconsett} */
107
+ #_options;
108
+ /**
109
+ * A container status
110
+ * @typedef {Object} statILCont
111
+ * @property {number} curIndex
112
+ * @property {?HTMLElement} curItem
113
+ * @inner
114
+ */
115
+ /** @type {statILCont} */
116
+ #_status;
117
+
118
+ /**
119
+ * Creates an instance of a list container
120
+ * @param {HTMLElement} host - host element
121
+ * @param {OPT_hlconsett} [opt] - options
122
+ */
123
+ constructor(host, opt) {
124
+ // check host
125
+ this.#_host = isHTMLElement(host) ? host : null;
126
+ // init items container
127
+ const _items = new TItemsListEx();
128
+ this.#_items = _items;
129
+ // load options
130
+ /** @type {OPT_hlconsett} */
131
+ const _options = isPlainObject(opt) ? opt : {};
132
+ let {
133
+ autoHideNewItems,
134
+ markCurrentItem,
135
+ itemBaseClassID,
136
+ } = _options;
137
+ if (typeof autoHideNewItems !== 'boolean') {
138
+ _options.autoHideNewItems = autoHideNewItems = false;
139
+ };
140
+ if (typeof markCurrentItem !== 'boolean') {
141
+ _options.markCurrentItem = markCurrentItem = false;
142
+ };
143
+ itemBaseClassID = valueToClassList(itemBaseClassID, true);
144
+ _options.itemBaseClassID = itemBaseClassID;
145
+ // init status
146
+ /** @type {statILCont} */
147
+ const _status = {
148
+ curIndex: _items.curIndex,
149
+ curItem: _items.curItem,
150
+ };
151
+ this.#_status = _status;
152
+ // save options
153
+ this.#_options = _options;
154
+ }
155
+
156
+ [Symbol.iterator]() {
157
+ let index = 0;
158
+ return {
159
+ next: () => {
160
+ if (index < this.count) {
161
+ return { done: false, value: this.getItem(index++) };
162
+ } else {
163
+ return { done: true, value: undefined };
164
+ };
165
+ },
166
+ return() {
167
+ return { done: true, value: undefined };
168
+ },
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Contains a Qty of an elements
174
+ * @type {number}
175
+ * @readonly
176
+ */
177
+ get count() {
178
+ return this.#_items.count;
179
+ }
180
+
181
+ /**
182
+ * Contains an index of the current element
183
+ * @type {number}
184
+ * @readonly
185
+ */
186
+ get curIndex() {
187
+ return this.#_items.curIndex;
188
+ }
189
+
190
+ /**
191
+ * Returns a minimum value of an index
192
+ * @type {number}
193
+ * @readonly
194
+ */
195
+ get minIndex() {
196
+ return this.#_items.minIndex;
197
+ }
198
+
199
+ /**
200
+ * Returns a maximum value of an index
201
+ * @type {number}
202
+ * @readonly
203
+ */
204
+ get maxIndex() {
205
+ return this.#_items.maxIndex;
206
+ }
207
+
208
+ /**
209
+ * Returns a value of a previous index
210
+ * @type {number}
211
+ * @readonly
212
+ */
213
+ get prevIndex() {
214
+ return this.#_items.prevIndex;
215
+ }
216
+
217
+ /**
218
+ * Returns a value of a next index
219
+ * @type {number}
220
+ * @readonly
221
+ */
222
+ get nextIndex() {
223
+ return this.#_items.nextIndex;
224
+ }
225
+
226
+ /**
227
+ * Returns an element in the current index
228
+ * @type {?HTMLElement}
229
+ * @readonly
230
+ */
231
+ get curItem() {
232
+ const item = this.#_items.curItem;
233
+ return isHTMLElement(item) ? item : null;
234
+ }
235
+
236
+ /**
237
+ * Clears an instance content.
238
+ * @returns {void}
239
+ */
240
+ clear() {
241
+ this.#_items.clear();
242
+ const _status = this.#_status;
243
+ _status.curIndex = this.curIndex;
244
+ _status.curItem = this.curItem;
245
+ const _host = this.#_host;
246
+ if (_host) _host.innerHTML = '';
247
+ }
248
+
249
+ /**
250
+ * Checks if an instance contains no items.
251
+ * @returns {boolean}
252
+ */
253
+ isEmpty() {
254
+ return this.#_items.isEmpty();
255
+ }
256
+
257
+ /**
258
+ * Checks if an instance contains any items.
259
+ * @returns {boolean}
260
+ */
261
+ isNotEmpty() {
262
+ return this.#_items.isNotEmpty();
263
+ }
264
+
265
+ /**
266
+ * Checks if a given value is a valid index and
267
+ * it fits an index range within an instance.
268
+ * @param {(number|string)} value - index value
269
+ * @returns {boolean}
270
+ */
271
+ chkIndex(value) {
272
+ return this.#_items.chkIndex(value);
273
+ }
274
+
275
+ /**
276
+ * Checks if a given value is a valid index and
277
+ * it fits an index range within an instance.
278
+ * @param {(number|string)} value - value to check
279
+ * @param {boolean} [opt=false] - defines a type of result
280
+ * @returns {(boolean|number)}
281
+ * @see TItemsListEx.chkIndexEx
282
+ */
283
+ chkIndexEx(...args) {
284
+ return this.#_items.chkIndexEx(...args);
285
+ }
286
+
287
+ /**
288
+ * Returns an index of a given element.
289
+ * @param {HTMLElement} item - element to search
290
+ * @returns {number}
291
+ * @todo add 2nd param
292
+ * @see TItemsListEx.srchIndex
293
+ */
294
+ srchIndex(item) {
295
+ return isHTMLElement(item) ? this.#_items.srchIndex(item) : -1;
296
+ }
297
+
298
+ /**
299
+ * Returns an index of an element wich has an attribute
300
+ * with a given name and value.
301
+ * @param {string} name - attribute name
302
+ * @param {any} [value=""] - attribute value
303
+ * @returns {number}
304
+ */
305
+ srchIndexByAttr(name, value = '') {
306
+ const _name = typeof name === 'string' ? name.trim() : '';
307
+ const { index } = srchListElementByAttr(this.#_items, _name, value);
308
+ return index;
309
+ };
310
+
311
+ /**
312
+ * Returns an index of an element wich has a given ID.
313
+ * @param {string} value - element ID
314
+ * @returns {number}
315
+ */
316
+ srchIndexByID(value) {
317
+ const { index } = srchListElementByAttr(this.#_items, 'id', value);
318
+ return index;
319
+ };
320
+
321
+ /**
322
+ * Sets a current index.
323
+ * @param {(number|string)} index - element index
324
+ * @returns {boolean}
325
+ */
326
+ setCurIndex(index) {
327
+ const isSUCCEED = this.#_items.setIndex(index);
328
+ if (isSUCCEED) {
329
+ const markCurrentItem = this.#_options.markCurrentItem;
330
+ const _status = this.#_status;
331
+ if (markCurrentItem && _status.curIndex !== -1) {
332
+ unmarkCurrentHTMLElement(_status.curItem);
333
+ };
334
+ const item = this.getItem(index);
335
+ _status.curIndex = Number(index);
336
+ _status.curItem = item;
337
+ if (markCurrentItem) markHTMLElementAsCurrent(item);
338
+ };
339
+ return isSUCCEED;
340
+ }
341
+
342
+ /**
343
+ * Resets a current index.
344
+ * @returns {void}
345
+ */
346
+ rstCurIndex() {
347
+ const _status = this.#_status;
348
+ if (this.#_options.markCurrentItem && _status.curIndex !== -1) {
349
+ unmarkCurrentHTMLElement(_status.curItem);
350
+ };
351
+ this.#_items.rstIndex();
352
+ _status.curIndex = this.#_items.curIndex;
353
+ _status.curItem = this.curItem;
354
+ }
355
+
356
+ /**
357
+ * Returns an item addressed by a given index.
358
+ * @param {(number|string)} index - element index
359
+ * @returns {?HTMLElement}
360
+ */
361
+ getItem(index) {
362
+ const item = this.#_items.getItem(index);
363
+ return isHTMLElement(item) ? item : null;
364
+ }
365
+
366
+ /**
367
+ * Returns an item wich has an attribute with a given name and value.
368
+ * @param {string} name - attribute name
369
+ * @param {any} [value=""] - attribute value
370
+ * @returns {?HTMLElement}
371
+ */
372
+ getItemByAttr(name, value = '') {
373
+ const _name = typeof name === 'string' ? name.trim() : '';
374
+ const { item } = srchListElementByAttr(this.#_items, _name, value);
375
+ return item;
376
+ }
377
+
378
+ /**
379
+ * Returns an item wich has a given ID.
380
+ * @param {string} value - element ID
381
+ * @returns {?HTMLElement}
382
+ */
383
+ getItemByID(value) {
384
+ const { item } = srchListElementByAttr(this.#_items, 'id', value);
385
+ return item;
386
+ }
387
+
388
+ /**
389
+ * Adds an item to an instance.
390
+ * @param {HTMLElement} item - some element
391
+ * @param {boolean} [opt=false] - indicates whether to correct a current index
392
+ * @returns {number}
393
+ */
394
+ addItem(item, opt) {
395
+ const forceCI = typeof opt === 'boolean' ? opt : false;
396
+ const index = (
397
+ isHTMLElement(item)
398
+ ? this.#_items.addItemEx(item, false)
399
+ : -1
400
+ );
401
+ const isSUCCEED = index !== -1;
402
+ if (isSUCCEED) {
403
+ const { autoHideNewItems, itemBaseClassID } = this.#_options;
404
+ if (autoHideNewItems) hideHTMLElement(item);
405
+ if (itemBaseClassID.length > 0) item.classList.add(...itemBaseClassID);
406
+ if (forceCI) this.setCurIndex(index);
407
+ const _host = this.#_host;
408
+ if (_host) _host.append(item);
409
+ };
410
+ return isSUCCEED ? index : -1;
411
+ }
412
+
413
+ /**
414
+ * Deletes an item from an instance.
415
+ * @param {(number|string)} index - element index
416
+ * @param {any} [opt]
417
+ * @param {boolean} [optEx=true]
418
+ * @returns {boolean}
419
+ */
420
+ delItem(index, opt, optEx) {
421
+ const _items = this.#_items;
422
+ const item = _items.delItemEx(index, opt);
423
+ const isSUCCEED = item !== undefined;
424
+ if (isSUCCEED) {
425
+ if (this.#_host && isHTMLElement(item)) item.remove();
426
+ const doProcEx = typeof optEx === 'boolean' ? optEx : true;
427
+ if (doProcEx) {
428
+ const index = _items.curIndex;
429
+ if (index === -1) {
430
+ this.rstCurIndex();
431
+ } else {
432
+ this.setCurIndex(index);
433
+ };
434
+ };
435
+ };
436
+ return isSUCCEED;
437
+ }
438
+
439
+ /**
440
+ * Selects an item addressed by a given index.
441
+ * @param {(number|string)} index - element index
442
+ * @param {boolean} [opt=false] - indicates whether to correct a current index
443
+ * @returns {boolean}
444
+ */
445
+ selectItem(index, opt) {
446
+ const forceCI = typeof opt === 'boolean' ? opt : false;
447
+ const isSUCCEED = selectHTMLElement(this.#_items.getItem(index));
448
+ if (isSUCCEED && forceCI) this.setCurIndex(index);
449
+ return isSUCCEED;
450
+ }
451
+
452
+ /**
453
+ * Unselects an item addressed by a given index.
454
+ * @param {(number|string)} index - element index
455
+ * @returns {boolean}
456
+ */
457
+ unselectItem(index) {
458
+ return unselectHTMLElement(this.#_items.getItem(index));
459
+ }
460
+
461
+ /**
462
+ * Hides an item addressed by a given index.
463
+ * @param {(number|string)} index - element index
464
+ * @returns {boolean}
465
+ */
466
+ hideItem(index) {
467
+ return hideHTMLElement(this.#_items.getItem(index));
468
+ }
469
+
470
+ /**
471
+ * Shows an item addressed by a given index.
472
+ * @param {(number|string)} index - element index
473
+ * @returns {boolean}
474
+ */
475
+ showItem(index) {
476
+ return showHTMLElement(this.#_items.getItem(index));
477
+ }
478
+
479
+ /**
480
+ * Checks whether an item is selected.
481
+ * @param {(number|string)} index - element index
482
+ * @returns {boolean}
483
+ */
484
+ isSelectedItem(index) {
485
+ return isSelectedHTMLElement(this.#_items.getItem(index));
486
+ }
487
+
488
+ /**
489
+ * Checks whether an item is hidden.
490
+ * @param {(number|string)} index - element index
491
+ * @returns {boolean}
492
+ */
493
+ isHiddenItem(index) {
494
+ return isHiddenHTMLElement(this.#_items.getItem(index));
495
+ }
496
+
497
+ };
498
+
499
+ /**
500
+ * A an options set for a `THtmlStubItemsSet`-class constructor
501
+ * @typedef {Object} OPT_stubconsett
502
+ * @property {(any[])} items - array of `name`-`element` pairs
503
+ * @property {string} [defaultItem] - a default element name
504
+ * @property {boolean} [force=false] - run in a force mode
505
+ */
506
+
507
+ /**
508
+ * An options set for `THtmlItemsListController`-class
509
+ * @typedef {Object} OPT_hlctlsett
510
+ * @property {boolean} [autoHideNewItems=false] - indicates whether to hide
511
+ * a new element
512
+ * @property {boolean} [markCurrentItem=false] - indicates whether to mark
513
+ * a current element
514
+ * @property {(string|string[])} [itemBaseClassID] - contains a base class
515
+ * attributes applayed to each a newly added list member
516
+ * @property {boolean} [showStubsIfEmpty=false] - indicates whether to show
517
+ * stubs if empty
518
+ * @property {boolean} [allowGroupSelection=false] - indicates whether
519
+ * a selection of the elements group is allowed
520
+ * @property {boolean} [allowSelectionLocks=false] - indicates whether locking
521
+ * of an element selection is allowed
522
+ * @property {OPT_stubconsett} [stubs] - stub elements options
523
+ */
524
+
525
+ /**
526
+ * @augments THtmlItemsListContainer
527
+ * @classdesc This class enhanced a capabilities implemented
528
+ * in the `THtmlItemsListContainer` class
529
+ */
530
+ class THtmlItemsListController extends THtmlItemsListContainer {
531
+ /** @type {?HTMLElement} */
532
+ #_host;
533
+ /** @type {THtmlStubItemsSet} */
534
+ #_stubs;
535
+ /** @type {Set<HTMLElement>} */
536
+ #_selects;
537
+ /** @property {OPT_hlctlsett} */
538
+ #_options;// = null;
539
+ /**
540
+ * A controler status
541
+ * @typedef {Object} statILCtrl
542
+ * @property {boolean} isSelectionLocked
543
+ * @property {boolean} isStubItemShown
544
+ * @property {boolean} isHostEnabled
545
+ * @property {boolean} catchEventOnHost
546
+ * @property {boolean} execDelItem
547
+ * @property {number} execDelItemDI
548
+ * @property {number} execDelItemCI
549
+ * @inner
550
+ */
551
+ /** @property {statILCtrl} */
552
+ #_status;
553
+ /** @property {Map<string, Function>} */
554
+ #_events;
555
+
556
+ /**
557
+ * Creates a new instance of the class.
558
+ * @param {?HTMLElement} host - host element
559
+ * @param {OPT_hlctlsett} [opt] - options
560
+ */
561
+ constructor(host, opt) {
562
+ // check host element
563
+ const isHostEnabled = isHTMLElement(host);
564
+ const _host = isHostEnabled ? host : null;
565
+ // check options
566
+ /** @type {OPT_hlctlsett} */
567
+ const _options = isPlainObject(opt) ? opt : {};
568
+ // call parent constructor
569
+ super(_host, _options);
570
+ // load options
571
+ let {
572
+ //autoHideNewItems, // [*] processed by parent
573
+ //markCurrentItem, // [*] processed by parent
574
+ //itemBaseClassID, // [*] processed by parent
575
+ showStubsIfEmpty,
576
+ allowGroupSelection,
577
+ allowSelectionLocks,
578
+ stubs: stubsList,
579
+ } = _options;
580
+ if (typeof showStubsIfEmpty !== 'boolean') {
581
+ _options.showStubsIfEmpty = showStubsIfEmpty = false;
582
+ };
583
+ if (typeof allowGroupSelection !== 'boolean') {
584
+ _options.allowGroupSelection = allowGroupSelection = false;
585
+ };
586
+ if (typeof allowSelectionLocks !== 'boolean') {
587
+ _options.allowSelectionLocks = allowSelectionLocks = false;
588
+ };
589
+ // save options
590
+ this.#_options = _options;
591
+ // init status flags set
592
+ /** @type {statILCtrl} */
593
+ const _status = {
594
+ isSelectionLocked: false,
595
+ isStubItemShown: false,
596
+ isHostEnabled: isHostEnabled,
597
+ catchEventOnHost: false,
598
+ execDelItem: false,
599
+ execDelItemDI: -1,
600
+ execDelItemCI: -1,
601
+ }
602
+ // bind ref to host
603
+ this.#_host = _host;
604
+ // init stub items set
605
+ this.#_stubs = new THtmlStubItemsSet(_host, stubsList);
606
+ // init index of selected items
607
+ this.#_selects = new Set();
608
+ // init events controller
609
+ this.#_events = new Map();
610
+ if (isHostEnabled) {
611
+ // set on_click()-event controler
612
+ /**/// use bubblig stage
613
+ _host.addEventListener('click', this.#_on_will_select_item);
614
+ _status.catchEventOnHost = true;
615
+ };
616
+ // save status flags set
617
+ this.#_status = _status;
618
+ }
619
+
620
+ /**
621
+ * @param {object} e - event
622
+ * @returns {void}
623
+ * @private
624
+ */
625
+ #_on_will_select_item = (e) => {
626
+ //console.log('THtmlItemsListController._on_will_select_item() ==> was called...');
627
+ //e.preventDefault(); /* need to reconsider reason for use */
628
+ const {
629
+ eventPhase,
630
+ target,
631
+ currentTarget,
632
+ ctrlKey,
633
+ shiftKey,
634
+ } = e;
635
+ //console.log('CHECK: e => ditail:['+e.detail+']');
636
+ //console.log('CHECK: e => phase:['+eventPhase+']');
637
+ const onClickNum = readAsNumber(e.detail, 0);
638
+ const host = this.#_host;
639
+ const { isSelectionLocked, catchEventOnHost } = this.#_status;
640
+ let curItem = null;
641
+ switch (eventPhase) {
642
+ //* // NOTE: currently on eventPhase = 2 and 3
643
+ //*case 1:
644
+ /**/// capturing stage
645
+ case 2: {
646
+ /**/// target stage
647
+ if (target !== host) curItem = target;
648
+ break;
649
+ }
650
+ case 3: {
651
+ /**/// bubblig stage
652
+ curItem = catchEventOnHost ? target : currentTarget;
653
+ break;
654
+ }
655
+ default: {}
656
+ };
657
+ if (
658
+ !isSelectionLocked
659
+ && curItem instanceof HTMLElement
660
+ && (onClickNum === 0 || onClickNum === 1)
661
+ && !curItem.classList.contains(CSS_CLASS_DISABLED)
662
+ ) {
663
+ //console.log(`CHECK: e => tag:[${curItem.tagName}]`);
664
+ if (host) {
665
+ // find an actual list element in case when some inner element was clicking
666
+ let tmpItem = curItem;
667
+ while (tmpItem) {
668
+ //console.log(`CHECK: e => target.tag:[${tmpItem.tagName}]`);
669
+ tmpItem = tmpItem.parentElement;
670
+ //console.log(`CHECK: e => next.tag:[${tmpItem.tagName}]`);
671
+ if (tmpItem === host) break;
672
+ if (tmpItem) curItem = tmpItem;
673
+ };
674
+ };
675
+ //console.log(`CHECK: e => tag:[${curItem.tagName}]`);
676
+ this.#_selectItemEx(curItem, {
677
+ ctrlKey: ctrlKey,
678
+ shiftKey: shiftKey,
679
+ forceCI: true,
680
+ });
681
+ };
682
+ };
683
+
684
+ /**
685
+ * @param {string} name - event name
686
+ * @param {...any} args
687
+ * @returns {void}
688
+ * @private
689
+ * @see triggerEventHandler
690
+ */
691
+ #_triggerEvent = (name, ...args) => {
692
+ triggerEventHandler(this.#_events, name, ...args);
693
+ };
694
+
695
+ /**
696
+ * Returns a list of the selected elements
697
+ * @type {HTMLElement[]}
698
+ * @readonly
699
+ */
700
+ get SelectedItems() {
701
+ const _selects = this.#_selects;
702
+ // // TODO: consider to use: `[...<value>]`
703
+ let result = [];
704
+ for (let item of _selects) {
705
+ if (item) result.push(item);
706
+ };
707
+ return result;
708
+ }
709
+
710
+ /**
711
+ * Returns a `THtmlStubItemsSet` instance
712
+ * @type {THtmlStubItemsSet}
713
+ * @readonly
714
+ */
715
+ get StubItems() {
716
+ return this.#_stubs;
717
+ }
718
+
719
+ /**
720
+ * Indicates whether a selection is locked
721
+ * @type {boolean}
722
+ * @readonly
723
+ */
724
+ get isSelectionLocked() {
725
+ return this.#_status.isSelectionLocked;
726
+ }
727
+
728
+ /**
729
+ * Clears an instance content.
730
+ * @returns {void}
731
+ * @fires THtmlItemsListController#list-clear
732
+ */
733
+ clear() {
734
+ // // TODO: clear event handler on elements if set
735
+ super.clear();
736
+ this.#_selects.clear();
737
+ if (this.#_options.showStubsIfEmpty) {
738
+ // show default stub-item
739
+ this.#_status.isStubItemShown = this.#_stubs.showDefItem();
740
+ };
741
+ /**
742
+ * @event THtmlItemsListController#list-clear
743
+ * @type {void}
744
+ */
745
+ this.#_triggerEvent('list-clear');
746
+ }
747
+
748
+ /**
749
+ * Locks an element selection.
750
+ * @returns {void}
751
+ */
752
+ lockItemsSelection() {
753
+ if (this.#_options.allowSelectionLocks) {
754
+ this.#_status.isSelectionLocked = true;
755
+ };
756
+ }
757
+
758
+ /**
759
+ * Unlocks an element selection.
760
+ * @returns {void}
761
+ */
762
+ unlockItemsSelection() {
763
+ this.#_status.isSelectionLocked = false;
764
+ }
765
+
766
+ /**
767
+ * Sets a current index.
768
+ * @param {(number|string)} index - element index
769
+ * @returns {boolean}
770
+ * @fires THtmlItemsListController#current-item-chosen
771
+ * @private
772
+ */
773
+ #_setCurIndex(index) {
774
+ const isSUCCEED = super.setCurIndex(index);
775
+ /**
776
+ * @event THtmlItemsListController#current-item-chosen
777
+ * @type {Object}
778
+ * @property {number} index - element index
779
+ * @property {?HTMLElement} item - element
780
+ */
781
+ if (isSUCCEED) this.#_triggerEvent('current-item-chosen', {
782
+ index: Number(index),
783
+ item: null,
784
+ });
785
+ return isSUCCEED;
786
+ };
787
+
788
+ /**
789
+ * Sets a current index.
790
+ * @param {(number|string)} index - element index
791
+ * @returns {boolean}
792
+ */
793
+ setCurIndex(index) {
794
+ return this.selectItem(index, true);
795
+ }
796
+
797
+ /**
798
+ * Resets a current index.
799
+ * @returns {void}
800
+ */
801
+ rstCurIndex() {
802
+ const {
803
+ execDelItem,
804
+ execDelItemDI,
805
+ execDelItemCI,
806
+ } = this.#_status;
807
+ if (execDelItem) {
808
+ let index = execDelItemCI;
809
+ if (execDelItemCI !== -1) index--;
810
+ if (index !== -1 && execDelItemDI !== execDelItemCI) {
811
+ this.unselectItem(index);
812
+ };
813
+ };
814
+ super.rstCurIndex();
815
+ }
816
+
817
+ /**
818
+ * Adds an element to an instance members.
819
+ * @param {HTMLElement} item - some element
820
+ * @param {boolean} [opt=false] - indicates whether to correct a current index
821
+ * @returns {number}
822
+ * @fires THtmlItemsListController#first-item-added
823
+ * @fires THtmlItemsListController#item-added
824
+ */
825
+ addItem(item, opt) {
826
+ const index = super.addItem(item, false);
827
+ if (index !== -1) {
828
+ const { autoHideNewItems, showStubsIfEmpty } = this.#_options;
829
+ const _status = this.#_status;
830
+ const { catchEventOnHost, isStubItemShown } = _status;
831
+ if (!catchEventOnHost) {
832
+ // set event handler on element if it is not set on host
833
+ item.addEventListener('click', this.#_on_will_select_item);
834
+ };
835
+ // TODO: consider if a "boolean" <opt> is enough
836
+ const forceCI = typeof opt === 'boolean' ? opt : false;
837
+ if (!autoHideNewItems) {
838
+ if (index === 0) {
839
+ if (
840
+ showStubsIfEmpty
841
+ && isStubItemShown
842
+ && this.#_stubs.hideDefItem()
843
+ ) {
844
+ // hide default stub-item
845
+ _status.isStubItemShown = false;
846
+ };
847
+ /**
848
+ * @event THtmlItemsListController#first-item-added
849
+ * @type {Object}
850
+ * @property {number} index - element index
851
+ * @property {?HTMLElement} item - element
852
+ */
853
+ this.#_triggerEvent('first-item-added', {
854
+ index: index,
855
+ item: item,
856
+ });
857
+ };
858
+ /**
859
+ * @event THtmlItemsListController#item-added
860
+ * @type {Object}
861
+ * @property {number} index - element index
862
+ * @property {?HTMLElement} item - element
863
+ */
864
+ this.#_triggerEvent('item-added', {
865
+ index: index,
866
+ item: item,
867
+ });
868
+ };
869
+ if (forceCI) {
870
+ this.#_selectItemEx(item, {
871
+ ctrlKey: false,
872
+ shiftKey: false,
873
+ forceCI: forceCI,
874
+ });
875
+ };
876
+ };
877
+ return index;
878
+ }
879
+
880
+ /**
881
+ * Deletes an element from an instance members.
882
+ * @param {(number|string)} index - element index
883
+ * @param {any} [opt]
884
+ * @returns {boolean}
885
+ * @fires THtmlItemsListController#list-clear
886
+ * @fires THtmlItemsListController#item-removed
887
+ */
888
+ delItem(index, opt) {
889
+ const item = super.getItem(index);
890
+ let isSUCCEED = item !== undefined;
891
+ const _status = this.#_status;
892
+ if (isSUCCEED) {
893
+ _status.execDelItem = true;
894
+ _status.execDelItemCI = this.curIndex;
895
+ _status.execDelItemDI = Number(index);
896
+ isSUCCEED = super.delItem(index, opt, false);
897
+ if (isSUCCEED) {
898
+ const _options = this.#_options;
899
+ if (!_status.catchEventOnHost && isHTMLElement(item)) {
900
+ // remove event handler on element if it was set by addItem()
901
+ item.removeEventListener('click', this.#_on_will_select_item);
902
+ };
903
+ if (this.isEmpty()) {
904
+ this.#_selects.clear();
905
+ if (_options.showStubsIfEmpty) {
906
+ // show default stub-item
907
+ _status.isStubItemShown = this.#_stubs.showDefItem();
908
+ };
909
+ /**
910
+ * @see THtmlItemsListController#list-clear
911
+ */
912
+ this.#_triggerEvent('list-clear');
913
+ } else {
914
+ this.#_selects.delete(item);
915
+ /**
916
+ * @event THtmlItemsListController#item-removed
917
+ * @type {Object}
918
+ * @property {number} index - element index
919
+ * @property {?HTMLElement} item - element
920
+ */
921
+ this.#_triggerEvent('item-removed', {
922
+ index: Number(index),
923
+ item: item,
924
+ });
925
+ const current = this.curIndex;
926
+ if (current === -1) {
927
+ this.rstCurIndex();
928
+ } else {
929
+ this.setCurIndex(current);
930
+ };
931
+ };
932
+ };
933
+ _status.execDelItemDI = -1;
934
+ _status.execDelItemCI = -1;
935
+ _status.execDelItem = false;
936
+ };
937
+ return isSUCCEED;
938
+ }
939
+
940
+ /**
941
+ * Selects an element.
942
+ * @param {(number|string)} index - element index
943
+ * @returns {boolean}
944
+ * @private
945
+ * @fires THtmlItemsListController#item-selected
946
+ */
947
+ #_selectItem(index) {
948
+ const item = super.getItem(index);
949
+ const isSUCCEED = selectHTMLElement(item);
950
+ if (isSUCCEED) {
951
+ this.#_selects.add(item);
952
+ /**
953
+ * @event THtmlItemsListController#item-selected
954
+ * @type {Object}
955
+ * @property {number} index - element index
956
+ * @property {?HTMLElement} item - element
957
+ */
958
+ this.#_triggerEvent('item-selected', {
959
+ index: Number(index),
960
+ item: item,
961
+ });
962
+ };
963
+ return isSUCCEED;
964
+ }
965
+
966
+ /**
967
+ * Selects an element.
968
+ * @param {?HTMLElement} item - element
969
+ * @param {object} [opt]
970
+ * @param {boolean} [opt.ctrlKey=false]
971
+ * @param {boolean} [opt.shiftKey=false]
972
+ * @param {boolean} [opt.forceCI=false]
973
+ * @returns {void}
974
+ * @private
975
+ */
976
+ #_selectItemEx(item, opt){
977
+ const _selects = this.#_selects;
978
+ let {
979
+ ctrlKey = false,
980
+ shiftKey = false,
981
+ forceCI = false,
982
+ } = opt;
983
+ let mode = ILC_SMODE_DEF;
984
+ let doUnSelGrp = false;
985
+ let indexCI = this.curIndex;
986
+ let indexNI = this.srchIndex(item);
987
+ if (indexCI !== -1) {
988
+ if (this.#_options.allowGroupSelection) {
989
+ if (shiftKey) {
990
+ mode = ILC_SMODE_SHFT;
991
+ if (_selects.size > 0) doUnSelGrp = true;
992
+ } else if (ctrlKey) {
993
+ mode = ILC_SMODE_CTRL;
994
+ } else if (_selects.size > 0) {
995
+ doUnSelGrp = true;
996
+ };
997
+ } else if (_selects.size > 0) {
998
+ doUnSelGrp = true;
999
+ };
1000
+ };
1001
+ if (doUnSelGrp) {
1002
+ for (let item of _selects) {
1003
+ this.unselectItem(this.srchIndex(item));
1004
+ };
1005
+ _selects.clear();
1006
+ };
1007
+ switch (mode) {
1008
+ case ILC_SMODE_SHFT: {
1009
+ if (indexCI > indexNI) [ indexCI, indexNI ] = [ indexNI, indexCI ];
1010
+ for (let i = indexCI; i < indexNI + 1; i++) {
1011
+ this.#_selectItem(i);
1012
+ };
1013
+ break;
1014
+ }
1015
+ case ILC_SMODE_CTRL: {
1016
+ this.#_selectItem(this.srchIndex(item));
1017
+ break;
1018
+ }
1019
+ default: {
1020
+ if (this.#_selectItem(this.srchIndex(item))) {
1021
+ if (forceCI) this.#_setCurIndex(indexNI);
1022
+ };
1023
+ break;
1024
+ }
1025
+ };
1026
+ }
1027
+
1028
+ /**
1029
+ * Selects an element.
1030
+ * @param {(number|string)} index - element index
1031
+ * @param {boolean} [opt=false] - indicates whether to correct a current index
1032
+ * @returns {boolean}
1033
+ */
1034
+ selectItem(index, opt) {
1035
+ const item = super.getItem(index);
1036
+ let isSUCCEED = isHTMLElement(item);
1037
+ if (isSUCCEED) {
1038
+ const forceCI = typeof opt === 'boolean' ? opt : false;
1039
+ this.#_selectItemEx(item, {
1040
+ ctrlKey: false,
1041
+ shiftKey: false,
1042
+ forceCI: forceCI,
1043
+ });
1044
+ };
1045
+ return isSUCCEED;
1046
+ }
1047
+
1048
+ /**
1049
+ * Unselects an element.
1050
+ * @param {(number|string)} index - element index
1051
+ * @returns {boolean}
1052
+ * @fires THtmlItemsListController#item-unselected
1053
+ */
1054
+ unselectItem(index) {
1055
+ const item = super.getItem(index);
1056
+ let isSUCCEED = unselectHTMLElement(item);
1057
+ if (isSUCCEED) {
1058
+ this.#_selects.delete(item);
1059
+ /**
1060
+ * @event THtmlItemsListController#item-unselected
1061
+ * @type {Object}
1062
+ * @property {number} index - element index
1063
+ * @property {?HTMLElement} item - element
1064
+ */
1065
+ this.#_triggerEvent('item-unselected', {
1066
+ index: Number(index),
1067
+ item: item,
1068
+ });
1069
+ };
1070
+ return isSUCCEED;
1071
+ }
1072
+
1073
+ /**
1074
+ * Hides an element.
1075
+ * @param {(number|string)} index - element index
1076
+ * @returns {boolean}
1077
+ * @fires THtmlItemsListController#item-hidden
1078
+ */
1079
+ hideItem(index) {
1080
+ const item = super.getItem(index);
1081
+ let isSUCCEED = hideHTMLElement(item);
1082
+ if (isSUCCEED) {
1083
+ /**
1084
+ * @event THtmlItemsListController#item-hidden
1085
+ * @type {Object}
1086
+ * @property {number} index - element index
1087
+ * @property {?HTMLElement} item - element
1088
+ */
1089
+ this.#_triggerEvent('item-hidden', {
1090
+ index: Number(index),
1091
+ item: item,
1092
+ });
1093
+ };
1094
+ return isSUCCEED;
1095
+ }
1096
+
1097
+ /**
1098
+ * Shows an element.
1099
+ * @param {(number|string)} index - element index
1100
+ * @returns {boolean}
1101
+ * @fires THtmlItemsListController#item-shown
1102
+ */
1103
+ showItem(index) {
1104
+ const item = super.getItem(index);
1105
+ let isSUCCEED = showHTMLElement(item);
1106
+ if (isSUCCEED) {
1107
+ /**
1108
+ * @event THtmlItemsListController#item-shown
1109
+ * @type {Object}
1110
+ * @property {number} index - element index
1111
+ * @property {?HTMLElement} item - element
1112
+ */
1113
+ this.#_triggerEvent('item-shown', {
1114
+ index: Number(index),
1115
+ item: item,
1116
+ });
1117
+ };
1118
+ return isSUCCEED;
1119
+ }
1120
+
1121
+ /**
1122
+ * Sets a callback function to handle event.
1123
+ * @param {string} name - event name
1124
+ * @param {func} evnt
1125
+ * @returns {void}
1126
+ */
1127
+ on(name, evnt) {
1128
+ pushEventHandler(this.#_events, name, evnt);
1129
+ }
1130
+
1131
+ };
1132
+
1133
+ // === module exports block ===
1134
+
1135
+ exports.THtmlStubItemsSet = THtmlStubItemsSet;
1136
+ exports.THtmlItemsListContainer = THtmlItemsListContainer;
1137
+ exports.THtmlItemsListController = THtmlItemsListController;