selectic 1.3.11 → 3.0.3

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.
@@ -220,6 +220,12 @@
220
220
  opacity: 0;
221
221
  }
222
222
 
223
+ .selectic-item_text {
224
+ white-space: nowrap;
225
+ text-overflow: ellipsis;
226
+ overflow: hidden;
227
+ }
228
+
223
229
  .selectic-item__active {
224
230
  background-color: var(--selectic-active-item-bg);
225
231
  color: var(--selectic-active-item-color);
package/src/index.tsx CHANGED
@@ -18,7 +18,7 @@
18
18
  * close [component]: triggered when the list closes.
19
19
  */
20
20
 
21
- import {Vue, Component, Prop, Watch} from 'vtyx';
21
+ import {Vue, Component, Prop, Watch, h} from 'vtyx';
22
22
  import './css/selectic.css';
23
23
 
24
24
  import Store, {
@@ -36,6 +36,7 @@ import Store, {
36
36
  FormatCallback,
37
37
  SelectionOverflow,
38
38
  ListPosition,
39
+ HideFilter,
39
40
  } from './Store';
40
41
  import MainInput from './MainInput';
41
42
  import ExtendedList from './ExtendedList';
@@ -55,8 +56,21 @@ export {
55
56
  FormatCallback,
56
57
  SelectionOverflow,
57
58
  ListPosition,
59
+ HideFilter,
58
60
  };
59
61
 
62
+ type EventType = 'input' | 'change' | 'open' | 'close' | 'focus' | 'blur' | 'item:click';
63
+
64
+ export interface EventOptions {
65
+ instance: Selectic;
66
+ eventType: EventType;
67
+ automatic: boolean;
68
+ }
69
+
70
+ export interface EventChangeOptions extends EventOptions {
71
+ isExcluded: boolean;
72
+ }
73
+
60
74
  export interface ParamProps {
61
75
  /* Method to call to fetch extra data */
62
76
  fetchCallback?: FetchCallback;
@@ -71,7 +85,7 @@ export interface ParamProps {
71
85
  pageSize?: number;
72
86
 
73
87
  /* Hide the search control */
74
- hideFilter?: boolean | 'auto';
88
+ hideFilter?: HideFilter;
75
89
 
76
90
  /* Allow to reverse selection.
77
91
  * If true, parent should support the selectionIsExcluded property.
@@ -126,6 +140,17 @@ export interface ParamProps {
126
140
  keepOpenWithOtherSelectic?: boolean;
127
141
  }
128
142
 
143
+ export type OnCallback = (event: string, ...args: any[]) => void;
144
+ export type GetMethodsCallback = (methods: {
145
+ clearCache: Selectic['clearCache'];
146
+ changeTexts: Selectic['changeTexts'];
147
+ getValue: Selectic['getValue'];
148
+ getSelectedItems: Selectic['getSelectedItems'];
149
+ isEmpty: Selectic['isEmpty'];
150
+ toggleOpen: Selectic['toggleOpen'];
151
+ }) => void;
152
+
153
+
129
154
  export interface Props {
130
155
  /* Selectic's initial value */
131
156
  value?: SelectedValue;
@@ -173,6 +198,17 @@ export interface Props {
173
198
  * attributes of select.
174
199
  */
175
200
  params?: ParamProps;
201
+
202
+ /** _on is used mainly for tests.
203
+ * Its purpose is to propagate $emit event mainly
204
+ * for parents which are not in Vue environment.
205
+ */
206
+ _on?: OnCallback;
207
+
208
+ /** _getMethods is used mainly for tests.
209
+ * Its purpose is to provide public methods outside of a Vue environment.
210
+ */
211
+ _getMethods?: GetMethodsCallback;
176
212
  }
177
213
 
178
214
  export function changeTexts(texts: PartialMessages) {
@@ -234,6 +270,13 @@ export default class Selectic extends Vue<Props> {
234
270
  })})
235
271
  public params: ParamProps;
236
272
 
273
+ /** For tests */
274
+ @Prop()
275
+ public _on?: OnCallback;
276
+
277
+ @Prop()
278
+ public _getMethods?: GetMethodsCallback;
279
+
237
280
  /* }}} */
238
281
  /* {{{ data */
239
282
 
@@ -242,6 +285,7 @@ export default class Selectic extends Vue<Props> {
242
285
  public elementLeft = 0;
243
286
  public elementRight = 0;
244
287
  public width = 0;
288
+ private hasBeenRendered = false;
245
289
 
246
290
  private store: Store = {} as Store;
247
291
 
@@ -253,10 +297,10 @@ export default class Selectic extends Vue<Props> {
253
297
  /* {{{ computed */
254
298
 
255
299
  get isFocused() {
256
- if (!this.store || !this.store.state) {
300
+ if (!this.hasBeenRendered) {
257
301
  return false;
258
302
  }
259
- return this.store.state.isOpen;
303
+ return !!this.store.state.isOpen;
260
304
  }
261
305
 
262
306
  get scrollListener() {
@@ -393,17 +437,18 @@ export default class Selectic extends Vue<Props> {
393
437
  private computeOffset(doNotAddListener = false) {
394
438
  const mainInput = this.$refs.mainInput;
395
439
 
396
- if (!mainInput || !mainInput.$el) {
440
+ const mainEl = mainInput?.$el as HTMLElement;
441
+
442
+ if (!mainEl) {
397
443
  /* This method has been called too soon (before render function) */
398
444
  return;
399
445
  }
400
446
 
401
- const mainEl = mainInput.$el as HTMLElement;
402
447
  const _elementsListeners = this._elementsListeners;
403
448
 
404
449
  /* add listeners */
405
450
  if (!doNotAddListener) {
406
- let el = mainInput.$el as HTMLElement;
451
+ let el = mainEl;
407
452
  while (el) {
408
453
  el.addEventListener('scroll', this.scrollListener, { passive: true });
409
454
  _elementsListeners.push(el);
@@ -452,14 +497,14 @@ export default class Selectic extends Vue<Props> {
452
497
  window.addEventListener('resize', this.windowResize, false);
453
498
  document.addEventListener('click', this.outsideListener, true);
454
499
  this.computeOffset();
455
- this.$emit('open', this);
500
+ this.emit('open');
456
501
  } else {
457
502
  this.removeListeners();
458
503
  if (state.status.hasChanged) {
459
504
  this.$emit('change', this.getValue(), state.selectionIsExcluded, this);
460
505
  this.store.resetChange();
461
506
  }
462
- this.$emit('close', this);
507
+ this.emit('close');
463
508
  }
464
509
  }
465
510
 
@@ -480,7 +525,7 @@ export default class Selectic extends Vue<Props> {
480
525
  /* {{{ watch */
481
526
 
482
527
  @Watch('value')
483
- protected onValueChange() {
528
+ public onValueChange() {
484
529
  const currentValue = this.store.state.internalValue;
485
530
  const newValue = this.value ?? null;
486
531
  const areSimilar = this.compareValues(
@@ -494,17 +539,17 @@ export default class Selectic extends Vue<Props> {
494
539
  }
495
540
 
496
541
  @Watch('selectionIsExcluded')
497
- protected onExcludedChange() {
498
- this.store.selectionIsExcluded = this.selectionIsExcluded;
542
+ public onExcludedChange() {
543
+ this.store.props.selectionIsExcluded = this.selectionIsExcluded;
499
544
  }
500
545
 
501
546
  @Watch('options')
502
- protected onOptionsChange() {
503
- this.store.options = this.options;
547
+ public onOptionsChange() {
548
+ this.store.props.options = Array.from(this.options);
504
549
  }
505
550
 
506
551
  @Watch('texts')
507
- protected onTextsChange() {
552
+ public onTextsChange() {
508
553
  const texts = this.texts;
509
554
 
510
555
  if (texts) {
@@ -513,45 +558,46 @@ export default class Selectic extends Vue<Props> {
513
558
  }
514
559
 
515
560
  @Watch('disabled')
516
- protected onDisabledChange() {
517
- this.store.disabled = this.disabled;
561
+ public onDisabledChange() {
562
+ this.store.props.disabled = this.disabled;
518
563
  }
519
564
 
520
565
  @Watch('groups')
521
- protected onGroupsChanged() {
566
+ public onGroupsChanged() {
522
567
  this.store.changeGroups(this.groups);
523
568
  }
524
569
 
525
570
  @Watch('placeholder')
526
- protected onPlaceholderChanged() {
571
+ public onPlaceholderChanged() {
527
572
  this.store.commit('placeholder', this.placeholder);
528
573
  }
529
574
 
530
575
  @Watch('open')
531
- protected onOpenChanged() {
576
+ public onOpenChanged() {
532
577
  this.store.commit('isOpen', this.open ?? false);
533
578
  }
534
579
 
535
580
  @Watch('isFocused')
536
- protected onFocusChanged() {
581
+ public onFocusChanged() {
537
582
  this.focusToggled();
538
583
  }
539
584
 
540
585
  @Watch('store.state.internalValue')
541
- protected onInternalValueChange() {
586
+ public onInternalValueChange() {
542
587
  const oldValue = this._oldValue;
543
588
  const value = this.getValue();
544
589
  const areSimilar = this.compareValues(oldValue, value);
545
- /* should not trigger when initializing internalValue, but should do if it changes the inital value */
590
+ /* should not trigger when initializing internalValue, but should do
591
+ * if it changes the initial value */
546
592
  const canTrigger = (oldValue !== undefined || !this.hasGivenValue) && !areSimilar;
547
593
 
548
594
  if (canTrigger) {
549
595
  const selectionIsExcluded = this.store.state.selectionIsExcluded;
550
596
 
551
- this.$emit('input', value, selectionIsExcluded, this);
597
+ this.emit('input', value, selectionIsExcluded);
552
598
 
553
599
  if (!this.isFocused) {
554
- this.$emit('change', value, selectionIsExcluded, this);
600
+ this.emit('change', value, selectionIsExcluded);
555
601
  this.store.resetChange();
556
602
  }
557
603
  }
@@ -581,99 +627,145 @@ export default class Selectic extends Vue<Props> {
581
627
  }, 0);
582
628
  }
583
629
 
584
- private extractFromNode(node: Vue.VNode, text = ''): OptionValue {
585
- function styleToString(staticStyle?: {[key: string]: string}): string | undefined {
586
- if (!staticStyle) {
587
- return;
588
- }
589
- let styles = [];
590
- for (const [key, value] of Object.entries(staticStyle)) {
591
- styles.push(`${key}: ${value}`);
592
- }
593
- return styles.join(';');
594
- }
595
-
596
- const domProps = node.data?.domProps;
597
- const attrs = node.data?.attrs;
598
- const id = domProps?.value ?? attrs?.value ?? attrs?.id ?? text;
599
- const className = node.data?.staticClass;
600
- const style = styleToString(node.data?.staticStyle);
601
-
602
- const optVal: OptionValue = {
603
- id,
604
- text,
605
- className,
606
- style,
607
- };
630
+ /* This method is only to emit the events and to replicate them */
631
+ private _emit(event: 'input' | 'change', value: SelectedValue, options: EventChangeOptions): void;
632
+ private _emit(event: 'open' | 'close' | 'focus' | 'blur', options: EventOptions): void;
633
+ private _emit(event: 'item:click', value: OptionId, options: EventOptions): void;
634
+ private _emit(event: EventType, ...args: any[]) {
635
+ this.$emit(event, ...args);
608
636
 
609
- if (attrs) {
610
- for (const [key, val] of Object.entries(attrs)) {
611
- switch(key) {
612
- case 'title':
613
- optVal.title = val;
614
- break;
615
- case 'disabled':
616
- if (val === false) {
617
- optVal.disabled = false;
618
- } else {
619
- optVal.disabled = true;
620
- }
621
- break;
622
- case 'group':
623
- optVal.group = val;
624
- break;
625
- case 'icon':
626
- optVal.icon = val;
627
- break;
628
- case 'data':
629
- optVal.data = val;
630
- break;
631
- default:
632
- if (key.startsWith('data')) {
633
- if (typeof optVal.data !== 'object') {
634
- optVal.data = {};
635
- }
636
- optVal.data[key.slice(5)] = val;
637
- }
638
- }
639
- }
637
+ if (typeof this._on === 'function') {
638
+ this._on(event, ...args);
640
639
  }
641
-
642
- return optVal;
643
- }
644
-
645
- private extractOptionFromNode(node: Vue.VNode): OptionValue {
646
- const children = node.children;
647
- const text = (children && children[0].text || '').trim();
648
-
649
- return this.extractFromNode(node, text);
650
640
  }
651
641
 
652
- private extractOptgroupFromNode(node: Vue.VNode): OptionValue {
653
- const attrs = node.data?.attrs;
654
- const children = node.children || [];
655
- const text = attrs?.label || '';
656
- const options: OptionValue[] = [];
657
-
658
- for (const child of children) {
659
- if (child.tag === 'option') {
660
- options.push(this.extractOptionFromNode(child));
661
- }
642
+ private emit(event: 'input' | 'change', value: SelectedValue, isExcluded: boolean): void;
643
+ private emit(event: 'open' | 'close' | 'focus' | 'blur'): void;
644
+ private emit(event: 'item:click', value: OptionId): void;
645
+ private emit(event: EventType, value?: SelectedValue | OptionId, isExcluded?: boolean) {
646
+ const automatic = this.store.state.status.automaticChange;
647
+ const options: EventOptions = {
648
+ instance: this,
649
+ eventType: event,
650
+ automatic,
651
+ };
652
+ switch (event) {
653
+ case 'input':
654
+ case 'change':
655
+ const changeOptions: EventChangeOptions = Object.assign({
656
+ isExcluded: isExcluded!,
657
+ }, options);
658
+ this._emit(event, value as SelectedValue, changeOptions);
659
+ break;
660
+ case 'open':
661
+ case 'focus':
662
+ this._emit('open', options);
663
+ this._emit('focus', options);
664
+ break;
665
+ case 'close':
666
+ case 'blur':
667
+ this._emit('close', options);
668
+ this._emit('blur', options);
669
+ break;
670
+ case 'item:click':
671
+ this._emit(event, value as OptionId, options);
672
+ break;
662
673
  }
663
-
664
- const opt = this.extractFromNode(node, text);
665
- opt.options = options;
666
-
667
- return opt;
668
674
  }
669
675
 
676
+ // private extractFromNode(node: Vue.VNode, text = ''): OptionValue {
677
+ // function styleToString(staticStyle?: {[key: string]: string}): string | undefined {
678
+ // if (!staticStyle) {
679
+ // return;
680
+ // }
681
+ // let styles = [];
682
+ // for (const [key, value] of Object.entries(staticStyle)) {
683
+ // styles.push(`${key}: ${value}`);
684
+ // }
685
+ // return styles.join(';');
686
+ // }
687
+
688
+ // const domProps = node.data?.domProps;
689
+ // const attrs = node.data?.attrs;
690
+ // const id = domProps?.value ?? attrs?.value ?? attrs?.id ?? text;
691
+ // const className = node.data?.staticClass;
692
+ // const style = styleToString(node.data?.staticStyle);
693
+
694
+ // const optVal: OptionValue = {
695
+ // id,
696
+ // text,
697
+ // className,
698
+ // style,
699
+ // };
700
+
701
+ // if (attrs) {
702
+ // for (const [key, val] of Object.entries(attrs)) {
703
+ // switch(key) {
704
+ // case 'title':
705
+ // optVal.title = val;
706
+ // break;
707
+ // case 'disabled':
708
+ // if (val === false) {
709
+ // optVal.disabled = false;
710
+ // } else {
711
+ // optVal.disabled = true;
712
+ // }
713
+ // break;
714
+ // case 'group':
715
+ // optVal.group = val;
716
+ // break;
717
+ // case 'icon':
718
+ // optVal.icon = val;
719
+ // break;
720
+ // case 'data':
721
+ // optVal.data = val;
722
+ // break;
723
+ // default:
724
+ // if (key.startsWith('data')) {
725
+ // if (typeof optVal.data !== 'object') {
726
+ // optVal.data = {};
727
+ // }
728
+ // optVal.data[key.slice(5)] = val;
729
+ // }
730
+ // }
731
+ // }
732
+ // }
733
+
734
+ // return optVal;
735
+ // }
736
+
737
+ // private extractOptionFromNode(node: Vue.VNode): OptionValue {
738
+ // const children = node.children;
739
+ // const text = (children && children[0].text || '').trim();
740
+
741
+ // return this.extractFromNode(node, text);
742
+ // }
743
+
744
+ // private extractOptgroupFromNode(node: Vue.VNode): OptionValue {
745
+ // const attrs = node.data?.attrs;
746
+ // const children = node.children || [];
747
+ // const text = attrs?.label || '';
748
+ // const options: OptionValue[] = [];
749
+
750
+ // for (const child of children) {
751
+ // if (child.tag === 'option') {
752
+ // options.push(this.extractOptionFromNode(child));
753
+ // }
754
+ // }
755
+
756
+ // const opt = this.extractFromNode(node, text);
757
+ // opt.options = options;
758
+
759
+ // return opt;
760
+ // }
761
+
670
762
  /* }}} */
671
763
  /* {{{ Life cycle */
672
764
 
673
- protected created() {
765
+ public created() {
674
766
  this._elementsListeners = [];
675
767
 
676
- this.store = new Store({ propsData: {
768
+ this.store = new Store({
677
769
  options: this.options,
678
770
  value: this.value,
679
771
  selectionIsExcluded: this.selectionIsExcluded,
@@ -682,10 +774,9 @@ export default class Selectic extends Vue<Props> {
682
774
  groups: this.groups,
683
775
  keepOpenWithOtherSelectic: !!this.params.keepOpenWithOtherSelectic,
684
776
  params: {
685
- multiple: this.multiple,
777
+ multiple: (this.multiple ?? false) !== false,
686
778
  pageSize: this.params.pageSize || 100,
687
- hideFilter: this.params.hideFilter !== undefined
688
- ? this.params.hideFilter : 'auto',
779
+ hideFilter: this.params.hideFilter ?? 'auto',
689
780
  allowRevert: this.params.allowRevert, /* it can be undefined */
690
781
  allowClearSelection: this.params.allowClearSelection || false,
691
782
  autoSelect: this.params.autoSelect === undefined
@@ -700,48 +791,66 @@ export default class Selectic extends Vue<Props> {
700
791
  formatSelection: this.params.formatSelection,
701
792
  listPosition: this.params.listPosition || 'auto',
702
793
  optionBehavior: this.params.optionBehavior, /* it can be undefined */
703
- isOpen: !!this.open,
794
+ isOpen: (this.open ?? false) !== false,
704
795
  },
705
796
  fetchCallback: this.params.fetchCallback,
706
797
  getItemsCallback: this.params.getItemsCallback,
707
- }});
798
+ });
799
+
800
+ if (typeof this._getMethods === 'function') {
801
+ this._getMethods({
802
+ clearCache: this.clearCache.bind(this),
803
+ changeTexts: this.changeTexts.bind(this),
804
+ getValue: this.getValue.bind(this),
805
+ getSelectedItems: this.getSelectedItems.bind(this),
806
+ isEmpty: this.isEmpty.bind(this),
807
+ toggleOpen: this.toggleOpen.bind(this),
808
+ });
809
+ }
708
810
  }
709
811
 
710
- protected mounted() {
711
- setTimeout(() => this.computeOffset(), 0);
812
+ public mounted() {
813
+ setTimeout(() => {
814
+ this.hasBeenRendered = true;
815
+ this.computeOffset();
816
+ }, 100);
712
817
  }
713
818
 
714
- protected beforeUpdate() {
715
- const elements = this.$slots.default;
716
- if (!elements) {
717
- this.store.childOptions = [];
718
- return;
719
- }
720
- const options = [];
721
-
722
- for (const node of elements) {
723
- if (node.tag === 'option') {
724
- const prop = this.extractOptionFromNode(node);
725
- options.push(prop);
726
- } else
727
- if (node.tag === 'optgroup') {
728
- const prop = this.extractOptgroupFromNode(node);
729
- options.push(prop);
730
- }
731
- }
819
+ public beforeUpdate() {
820
+ // const elements = this.$slots.default;
821
+ // if (!elements) {
822
+ // this.store.childOptions = [];
823
+ // return;
824
+ // }
825
+ // const options = [];
826
+
827
+ // for (const node of elements) {
828
+ // if (node.tag === 'option') {
829
+ // const prop = this.extractOptionFromNode(node);
830
+ // options.push(prop);
831
+ // } else
832
+ // if (node.tag === 'optgroup') {
833
+ // const prop = this.extractOptgroupFromNode(node);
834
+ // options.push(prop);
835
+ // }
836
+ // }
732
837
 
733
- this.store.childOptions = options;
838
+ // this.store.childOptions = options;
734
839
  }
735
840
 
736
- protected beforeDestroy() {
841
+ public beforeDestroy() {
737
842
  this.removeListeners();
738
843
  }
739
844
 
740
845
  /* }}} */
741
846
 
742
- protected render() {
743
- const h = this.renderWrapper();
847
+ public render() {
744
848
  const id = this.id || undefined;
849
+ const store = this.store;
850
+
851
+ if (!store.state) {
852
+ return; /* component is not ready yet */
853
+ }
745
854
 
746
855
  return (
747
856
  <div
@@ -749,7 +858,7 @@ export default class Selectic extends Vue<Props> {
749
858
  title={this.title}
750
859
  data-selectic="true"
751
860
  on={{
752
- 'click.prevent.stop': () => this.store.commit('isOpen', true),
861
+ 'click.prevent.stop': () => store.commit('isOpen', true),
753
862
  }}
754
863
  >
755
864
  {/* This input is for DOM submission */}
@@ -759,22 +868,22 @@ export default class Selectic extends Vue<Props> {
759
868
  value={this.inputValue}
760
869
  class="selectic__input-value"
761
870
  on={{
762
- focus: () => this.store.commit('isOpen', true),
871
+ focus: () => store.commit('isOpen', true),
763
872
  blur: this.checkFocus,
764
873
  }}
765
874
  />
766
875
  <MainInput
767
- store={this.store}
876
+ store={store}
768
877
  id={id}
769
878
  on={{
770
- 'item:click': (id: OptionId) => this.$emit('item:click', id, this),
879
+ 'item:click': (id: OptionId) => this.emit('item:click', id),
771
880
  }}
772
881
  ref="mainInput"
773
882
  />
774
883
  {this.isFocused && (
775
884
  <ExtendedList
776
885
  class={this.className}
777
- store={this.store}
886
+ store={store}
778
887
  elementBottom={this.elementBottom}
779
888
  elementTop={this.elementTop}
780
889
  elementLeft={this.elementLeft}