goodteditor-ui 1.0.98 → 1.0.100

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goodteditor-ui",
3
- "version": "1.0.98",
3
+ "version": "1.0.100",
4
4
  "main": "index.js",
5
5
  "homepage": "https://goodt-ui.netlify.app/",
6
6
  "scripts": {
@@ -28,6 +28,8 @@
28
28
  </div>
29
29
  </template>
30
30
  <script>
31
+ import { debounce } from 'lodash';
32
+
31
33
  const Size = {
32
34
  NORMAL: { name: '', class: [] },
33
35
  SMALL: { name: 'small', class: ['input-small'] }
@@ -65,6 +67,10 @@ export default {
65
67
  type: Boolean,
66
68
  default: false
67
69
  },
70
+ debounce: {
71
+ type: Number,
72
+ default: 0
73
+ },
68
74
  embedded: {
69
75
  type: Boolean,
70
76
  default: false
@@ -83,11 +89,35 @@ export default {
83
89
  return Object.values(Size).find(({ name }) => name === size)?.class;
84
90
  }
85
91
  },
92
+ created() {
93
+ if (this.debounce > 0) {
94
+ const { emitInputDebounced } = this;
95
+ this.emitInputDebounced = debounce(emitInputDebounced.bind(this), this.debounce);
96
+ }
97
+ },
86
98
  methods: {
99
+ /**
100
+ * @param {string} val
101
+ * @param {boolean} isDebounced
102
+ */
103
+ emitInput(val = '', isDebounced = false) {
104
+ const { emitInputDebounced, emitInputOriginal } = this;
105
+ const method = isDebounced ? emitInputDebounced : emitInputOriginal;
106
+ method(val);
107
+ },
108
+ /**
109
+ * @param {string} val
110
+ */
111
+ emitInputDebounced(val = '') {
112
+ /**
113
+ * @property {string} value
114
+ */
115
+ this.emitInputOriginal(val);
116
+ },
87
117
  /**
88
118
  * @param {string} val
89
119
  */
90
- emitInput(val = '') {
120
+ emitInputOriginal(val = '') {
91
121
  /**
92
122
  * @property {string} value
93
123
  */
@@ -121,7 +151,7 @@ export default {
121
151
  * @param {InputEvent} e
122
152
  */
123
153
  onInput({ target }) {
124
- this.emitInput(target.value);
154
+ this.emitInput(target.value, true);
125
155
  },
126
156
  /**
127
157
  * @param {InputEvent} e
@@ -149,10 +179,9 @@ export default {
149
179
  </script>
150
180
  <style scoped lang="pcss">
151
181
  .form-control--embedded .input {
182
+ background: transparent;
152
183
  border: none;
153
184
  outline: none;
154
- padding-top: 0;
155
- padding-bottom: 0;
156
185
  min-height: initial;
157
186
  }
158
187
  </style>
@@ -0,0 +1,33 @@
1
+ /** Любой объект или стандартная структура опции */
2
+ export type Option = { label: string; value: any } | any;
3
+
4
+ export interface OptionSettings {
5
+ valueField?: string;
6
+ labelField?: string;
7
+ /** поле, по которому перед группой опций,
8
+ * будет рисоваться хедер группы, когда оно меняется
9
+ * опции должны изначально быть отсортированы по группам
10
+ * */
11
+ groupField?: string | null;
12
+ disabledField?: string;
13
+ valueObjects?: boolean;
14
+ /** Функция для проверки значения опции */
15
+ valueOfOptionChecker?: (option: { label: string; value: any } | Record<string, any>) => boolean;
16
+ }
17
+
18
+ export interface DropdownSettings {
19
+ class?: string | string[];
20
+ maxHeight?: string | null;
21
+ maxItems?: number;
22
+ itemHeight?: string | null;
23
+ }
24
+
25
+ export interface SearchSettings {
26
+ minItems?: number;
27
+ debounce?: number;
28
+ }
29
+
30
+ export type MultipleSettings = {
31
+ pin?: boolean;
32
+ hideOnSelect?: boolean;
33
+ };
@@ -10,16 +10,21 @@
10
10
  <!--
11
11
  @slot search slot
12
12
  -->
13
- <slot v-if="search && popoverShow" name="label-search">
14
- <ui-searchbar
15
- tabindex="1"
16
- v-model="searchQuery"
17
- :size="size"
18
- embedded
19
- class="ui-select-searchbar"
20
- ref="searchbar"
21
- @click.native.stop></ui-searchbar>
22
- </slot>
13
+ <div v-if="hasSearch && popoverShow" class="ui-select-searchbar-wrapper">
14
+ <slot name="label-search" v-bind="{ search: searchQuery }">
15
+ <ui-searchbar
16
+ v-model="searchQuery"
17
+ v-bind="{
18
+ size,
19
+ embedded: true,
20
+ debounce: searchSettings.debounce
21
+ }"
22
+ tabindex="1"
23
+ class="w-100"
24
+ ref="searchbar"
25
+ @click.native.stop></ui-searchbar>
26
+ </slot>
27
+ </div>
23
28
  <template v-else-if="multiple">
24
29
  <template v-if="optionsSelected.length > 0">
25
30
  <!--
@@ -34,7 +39,7 @@
34
39
  v-bind="{
35
40
  selected: optionsSelected,
36
41
  deselectOption,
37
- clear: clearOptions
42
+ clear: clearOptions,
38
43
  }">
39
44
  <slot
40
45
  name="label-multiple"
@@ -101,7 +106,7 @@
101
106
  @slot Reset state icon slot
102
107
  -->
103
108
  <slot
104
- v-if="clear && optionsSelected.length > 0 && (search === false || popoverShow === false)"
109
+ v-if="clear && optionsSelected.length > 0 && (hasSearch === false || popoverShow === false)"
105
110
  name="icon-clear"
106
111
  v-bind="{ clear: clearOptions }">
107
112
  <i
@@ -117,7 +122,7 @@
117
122
  -->
118
123
  <slot name="icon-open" v-if="popoverShow">
119
124
  <i
120
- class="icon mdi mdi-chevron-up"
125
+ class="icon icon-toggle mdi mdi-chevron-up"
121
126
  :class="{
122
127
  'mdi-18px': size === '',
123
128
  'h-auto w-auto': size === 'small'
@@ -128,7 +133,7 @@
128
133
  -->
129
134
  <slot name="icon-close" v-else>
130
135
  <i
131
- class="icon mdi mdi-chevron-down"
136
+ class="icon icon-toggle mdi mdi-chevron-down"
132
137
  :class="{
133
138
  'mdi-18px': size === '',
134
139
  'h-auto w-auto': size === 'small'
@@ -187,7 +192,17 @@
187
192
  selectOption,
188
193
  deselectOption,
189
194
  toggleOption
190
- }"></slot>
195
+ }">
196
+ <slot
197
+ v-if="isShowGroup({ option, index })"
198
+ name="option:before-group"
199
+ v-bind="{
200
+ option,
201
+ index,
202
+ group: getOptionGroup(option)
203
+ }">
204
+ </slot>
205
+ </slot>
191
206
  <!--
192
207
  @slot option slot mode
193
208
  @binding {Object} option option
@@ -218,13 +233,14 @@
218
233
  toggleOption,
219
234
  // legacy
220
235
  optionIndex: index,
221
- isOptionSelected: isOptionSelected(option)
236
+ isOptionSelected: isOptionSelected(option),
237
+ search: searchQuery
222
238
  }">
223
239
  <li
224
240
  :class="{
225
241
  active: isOptionSelected(option),
226
242
  'bg-grey-lighter': index == cursorIndex,
227
- disabled: isOptionDisabled(option)
243
+ 'option-disabled': isOptionDisabled(option)
228
244
  }"
229
245
  :key="index"
230
246
  :title="getOptionLabel(option)"
@@ -263,7 +279,9 @@
263
279
  label: getOptionLabel(option),
264
280
  index,
265
281
  isSelected: isOptionSelected(option),
266
- cursorIndex
282
+ isDisabled: isOptionDisabled(option),
283
+ cursorIndex,
284
+ search: searchQuery
267
285
  }">
268
286
  {{ getOptionLabel(option) }}
269
287
  </slot>
@@ -295,7 +313,8 @@
295
313
  cursorIndex,
296
314
  selectOption,
297
315
  deselectOption,
298
- toggleOption
316
+ toggleOption,
317
+ search: searchQuery
299
318
  }"></slot>
300
319
  </template>
301
320
  <template #footer>
@@ -310,6 +329,7 @@
310
329
  </template>
311
330
  <style lang="less" scoped>
312
331
  .ui-select {
332
+ position: relative;
313
333
  display: inline-flex;
314
334
  align-items: center;
315
335
 
@@ -320,7 +340,8 @@
320
340
  flex: 1 0 0;
321
341
  }
322
342
 
323
- .icon-clear {
343
+ .icon-clear, .icon-toggle {
344
+ z-index: 1;
324
345
  cursor: pointer;
325
346
  }
326
347
 
@@ -336,20 +357,34 @@
336
357
  }
337
358
  }
338
359
 
339
- &-searchbar {
340
- margin-left: calc(-1 * var(--spacer3));
341
- width: calc(100% + var(--spacer5));
360
+ &-searchbar-wrapper {
361
+ position: absolute;
362
+ top: 0;
363
+ left: 0;
364
+ width: 100%;
365
+ height: 100%;
366
+ display: flex;
367
+ align-items: center;
368
+ padding-right: var(--spacer6);
342
369
  }
343
370
  }
344
371
 
345
372
  .ui-datalist {
346
- .active:hover {
373
+ .option-disabled, .option-disabled:hover {
374
+ background-color: transparent;
375
+ color: var(--color-grey-light);
376
+ cursor: not-allowed;
377
+ }
378
+
379
+ .active:hover:not(.disabled) {
347
380
  background-color: var(--color-primary-hover);
348
381
  }
382
+
349
383
  &.multiple {
350
384
  li + li {
351
385
  border-top: 1px solid transparent;
352
386
  }
387
+
353
388
  .active + .active {
354
389
  border-color: var(--color-primary-hover);
355
390
  }
@@ -361,6 +396,7 @@
361
396
  }
362
397
  </style>
363
398
  <script>
399
+ import { isEqual } from 'lodash';
364
400
  import UiBadge from './Badge.vue';
365
401
  import UiDatalist from './Datalist.vue';
366
402
  import UiPopover from './Popover.vue';
@@ -370,24 +406,11 @@ import UiSearchbar from './Searchbar.vue';
370
406
  import { Key } from './utils/Helpers';
371
407
 
372
408
  /**
373
- * @typedef {{
374
- * valueField?: string,
375
- * labelField?: string,
376
- * disabledField?: string,
377
- * valueObjects?: boolean,
378
- * valueOfOptionChecker?: (option: { label: string, value: any } | Record<string, any>) => boolean
379
- * }} OptionSettings
380
- *
381
- * @typedef {{
382
- * class?: string|string[],
383
- * maxHeight?: null|string,
384
- * maxItems?: number,
385
- * itemHeight?: null|string
386
- * }} DropdownSettings
387
- *
388
- * @typedef {{
389
- * pin?: boolean
390
- * }} MultipleSettings
409
+ * @typedef {import('./Select').Option} Option
410
+ * @typedef {import('./Select').OptionSettings} OptionSettings
411
+ * @typedef {import('./Select').DropdownSettings} DropdownSettings
412
+ * @typedef {import('./Select').SearchSettings} SearchSettings
413
+ * @typedef {import('./Select').MultipleSettings} MultipleSettings
391
414
  */
392
415
 
393
416
  /**
@@ -399,7 +422,6 @@ const DropdownSettingsDefault = {
399
422
  maxItems: 5,
400
423
  itemHeight: null
401
424
  };
402
-
403
425
  /**
404
426
  * @type {OptionSettings}
405
427
  */
@@ -407,15 +429,23 @@ const OptionSettingsDefault = {
407
429
  valueField: 'value',
408
430
  labelField: 'label',
409
431
  disabledField: 'disabled',
432
+ groupField: null,
410
433
  valueObjects: false,
411
434
  valueOfOptionChecker: null
412
435
  };
413
-
414
436
  /**
415
437
  * @type {MultipleSettings}
416
438
  */
417
439
  const MultipleSettingsDefault = {
418
- pin: false
440
+ pin: false,
441
+ hideOnSelect: false
442
+ };
443
+ /**
444
+ * @type {SearchSettings}
445
+ */
446
+ const SearchSettingsDefault = {
447
+ minItems: 5,
448
+ debounce: 0
419
449
  };
420
450
 
421
451
  export default {
@@ -465,31 +495,20 @@ export default {
465
495
  },
466
496
  /**
467
497
  * Allow embedded search
468
- * @type {import('vue').PropOptions<boolean | {
469
- * clear?: boolean,
470
- * }>}
498
+ * @type {import('vue').PropOptions<boolean | SearchSettings>}
471
499
  */
472
500
  search: {
473
501
  type: [Boolean, Object],
474
502
  default: false
475
503
  },
476
504
  /**
477
- * Allow multiple selection
505
+ * Option-related settings
478
506
  * @type {import('vue').PropOptions<OptionSettings>}
507
+ * @default {}
479
508
  */
480
509
  option: {
481
510
  type: Object,
482
- default() {
483
- return {
484
- /*
485
- valueField: 'value',
486
- labelField: 'label',
487
- disabledField: '',
488
- valueObjects: false,
489
- valueOfOptionChecker: null
490
- */
491
- };
492
- }
511
+ default: null
493
512
  },
494
513
  /**
495
514
  * Defines whether 'value' is the option value field or option object
@@ -536,15 +555,11 @@ export default {
536
555
  // DATALIST OPTIONS
537
556
  /**
538
557
  * Dropdown extra config options
539
- * @type {import('vue').PropOptions<DropdownSettings>}
558
+ * @type {import('vue').PropOptions<DropdownSettings|null>}
540
559
  */
541
560
  datalist: {
542
- /*
543
- class: '',
544
- maxHeight: null,
545
- maxItems: 5,
546
- itemHeight: null
547
- */
561
+ type: Object,
562
+ default: null
548
563
  },
549
564
  /**
550
565
  * Datalist css classes (optional)
@@ -576,12 +591,28 @@ export default {
576
591
  },
577
592
  data() {
578
593
  return {
594
+ /**
595
+ * @type {any[]}
596
+ */
579
597
  optionsSelected: [],
580
598
  dataListCursorIndex: -1,
581
599
  searchQuery: ''
582
600
  };
583
601
  },
584
602
  computed: {
603
+ /**
604
+ * @return {boolean}
605
+ */
606
+ isSelectedRemovable() {
607
+ return this.readonly === false && this.disabled === false;
608
+ },
609
+ /**
610
+ * @return {boolean}
611
+ */
612
+ hasSearch() {
613
+ const { search, searchSettings: { minItems }, options } = this;
614
+ return search !== false && options.length > minItems;
615
+ },
585
616
  /**
586
617
  * @return {object}
587
618
  */
@@ -592,12 +623,15 @@ export default {
592
623
  }
593
624
  return this.cssClass;
594
625
  },
626
+ /**
627
+ * @return {string}
628
+ */
595
629
  selectedBadgeTheme() {
596
630
  return this.disabled ? '' : 'primary';
597
631
  },
598
- isSelectedRemovable() {
599
- return this.readonly === false && this.disabled === false;
600
- },
632
+ /**
633
+ * @return {Option[]}
634
+ */
601
635
  optionsInternal() {
602
636
  let {
603
637
  searchQuery,
@@ -620,13 +654,13 @@ export default {
620
654
  * @return {OptionSettings}
621
655
  */
622
656
  optionSettings() {
623
- const { labelField, labelValue, disabledField, labelObjects, valueOfOptionChecker, option } = this;
657
+ const { labelField, valueField, disabledField, valueObjects, valueOfOptionChecker, option } = this;
624
658
  return {
625
659
  ...OptionSettingsDefault,
626
660
  labelField,
627
- labelValue,
661
+ valueField,
628
662
  disabledField,
629
- labelObjects,
663
+ valueObjects,
630
664
  valueOfOptionChecker,
631
665
  ...option
632
666
  };
@@ -635,16 +669,33 @@ export default {
635
669
  * @return {DropdownSettings & { size: string }}
636
670
  */
637
671
  dropdownSettings() {
638
- const { datalistCssClass, maxItems, itemHeight, maxHeight, datalist, size } = this;
672
+ const { datalistCssClass, datalist, size } = this;
639
673
  return {
640
674
  ...DropdownSettingsDefault,
641
675
  class: datalistCssClass,
642
- maxItems,
643
- itemHeight,
644
- maxHeight,
645
676
  size,
646
677
  ...datalist
647
678
  };
679
+ },
680
+ /**
681
+ * @return {SearchSettings}
682
+ */
683
+ searchSettings() {
684
+ const { search } = this;
685
+ return {
686
+ ...SearchSettingsDefault,
687
+ ...search
688
+ };
689
+ },
690
+ /**
691
+ * @return {MultipleSettings}
692
+ */
693
+ multipleSettings() {
694
+ const { multiple } = this;
695
+ return {
696
+ ...MultipleSettingsDefault,
697
+ ...multiple
698
+ };
648
699
  }
649
700
  },
650
701
  watch: {
@@ -659,12 +710,17 @@ export default {
659
710
  },
660
711
  popoverShow(isPopoverShown) {
661
712
  this.$emit('options-toggle', isPopoverShown);
662
- if (this.search !== false) {
713
+ if (this.hasSearch !== false) {
663
714
  this.handleSearchbar(isPopoverShown);
664
715
  }
665
716
  }
666
717
  },
667
718
  methods: {
719
+ /**
720
+ * @param option
721
+ * @param modelItem
722
+ * @return {boolean}
723
+ */
668
724
  isValueOfOptionDefault(option, modelItem) {
669
725
  const { valueObjects } = this.optionSettings;
670
726
  const modelValue = valueObjects ? this.getOptionValue(modelItem) : modelItem;
@@ -773,6 +829,16 @@ export default {
773
829
  getOptionIndex(option) {
774
830
  return this.optionsInternal.indexOf(option);
775
831
  },
832
+ /**
833
+ * @param {Option} option
834
+ * @return {any|null}
835
+ */
836
+ getOptionGroup(option) {
837
+ const {
838
+ optionSettings: { groupField }
839
+ } = this;
840
+ return option == null ? null : option[groupField] ?? null;
841
+ },
776
842
  /**
777
843
  * @param {Option} option
778
844
  * @return {boolean}
@@ -791,6 +857,9 @@ export default {
791
857
  let disabled = option ? option[disabledField] : null;
792
858
  return Boolean(disabled);
793
859
  },
860
+ /**
861
+ * @return {function(): void}
862
+ */
794
863
  createOptionRollback() {
795
864
  const optionsSelected = [...this.optionsSelected];
796
865
  return () => {
@@ -803,18 +872,29 @@ export default {
803
872
  * @param {Option} option
804
873
  */
805
874
  selectOption(option) {
806
- if (this.readonly) {
875
+ const {
876
+ multiple,
877
+ /**
878
+ * @type {MultipleSettings}
879
+ */
880
+ multipleSettings,
881
+ readonly,
882
+ optionsSelected
883
+ } = this;
884
+ if (this.isOptionSelected(option)) {
807
885
  return;
808
886
  }
809
- if (this.isOptionSelected(option)) {
887
+ if (readonly || this.isOptionDisabled(option)) {
810
888
  return;
811
889
  }
812
890
 
813
891
  const rollback = this.createOptionRollback();
814
- if (this.multiple) {
815
- this.optionsSelected.push(option);
892
+ if (multiple) {
893
+ optionsSelected.push(option);
816
894
  } else {
817
895
  this.optionsSelected = [option];
896
+ }
897
+ if (!multiple || multipleSettings.hideOnSelect) {
818
898
  this.popoverShow = false;
819
899
  }
820
900
 
@@ -824,16 +904,25 @@ export default {
824
904
  * @param {Option} option
825
905
  */
826
906
  deselectOption(option) {
827
- const { multiple, readonly, optionsSelected } = this;
828
- if (readonly) {
907
+ const { multiple, multipleSettings, readonly, optionsSelected } = this;
908
+ if (readonly || this.isOptionDisabled(option)) {
829
909
  return;
830
910
  }
831
- if (!multiple) {
832
- this.popoverShow = false;
911
+ if (!this.isOptionSelected(option)) {
833
912
  return;
834
913
  }
914
+
835
915
  const rollback = this.createOptionRollback();
836
- optionsSelected.splice(optionsSelected.indexOf(option), 1);
916
+ if (multiple) {
917
+ this.optionsSelected = optionsSelected.toSpliced(optionsSelected.indexOf(option), 1);
918
+ } else {
919
+ this.optionsSelected = [];
920
+ }
921
+
922
+ if (!multiple || multipleSettings.hideOnSelect) {
923
+ this.popoverShow = false;
924
+ }
925
+
837
926
  this.triggerModelChange(rollback);
838
927
  },
839
928
  /**
@@ -855,6 +944,10 @@ export default {
855
944
  getDatalistRef() {
856
945
  return this.$refs.datalist;
857
946
  },
947
+ /**
948
+ *
949
+ * @param isPopoverShown
950
+ */
858
951
  handleSearchbar(isPopoverShown) {
859
952
  if (!isPopoverShown) {
860
953
  this.searchQuery = '';
@@ -864,6 +957,21 @@ export default {
864
957
  this.$refs.searchbar.focus();
865
958
  });
866
959
  },
960
+ isShowGroup({ option, index }) {
961
+ const { groupField } = this.optionSettings;
962
+ if (groupField == null) {
963
+ return false;
964
+ }
965
+ if (index === 0) {
966
+ return true;
967
+ }
968
+ const { optionsInternal } = this;
969
+ const currentGroup = this.getOptionGroup(option);
970
+ const prevGroup = this.getOptionGroup(optionsInternal[index - 1]);
971
+ return isEqual(prevGroup, currentGroup) === false;
972
+ },
973
+
974
+ // EVENTS
867
975
  onClick() {
868
976
  const { popoverShow, optionsSelected } = this;
869
977
  this.togglePopover();