Skip to content

Commit 2081cf4

Browse files
committed
feat(MultiSelect): add select all button; improve code syntax
1 parent 34399eb commit 2081cf4

File tree

5 files changed

+207
-139
lines changed

5 files changed

+207
-139
lines changed

js/src/multi-select.js

Lines changed: 102 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ const SELECTOR_OPTION = '.form-multi-select-option'
3535
const SELECTOR_OPTIONS = '.form-multi-select-options'
3636
const SELECTOR_OPTIONS_EMPTY = '.form-multi-select-options-empty'
3737
const SELECTOR_SELECT = '.form-multi-select'
38-
const SELECTOR_SELECTED = '.form-multi-selected'
3938
const SELECTOR_SELECTION = '.form-multi-select-selection'
4039
const SELECTOR_SELECTION_CLEANER = '.form-multi-select-selection-cleaner'
4140

@@ -53,8 +52,10 @@ const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
5352
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
5453

5554
const CLASS_NAME_SELECT = 'form-multi-select'
55+
const CLASS_NAME_SELECT_DROPDOWN = 'form-multi-select-dropdown'
5656
const CLASS_NAME_SELECT_MULTIPLE = 'form-multi-select-multiple'
5757
const CLASS_NAME_SELECT_WITH_CLEANER = 'form-multi-select-with-cleaner'
58+
const CLASS_NAME_SELECT_ALL = 'form-multi-select-all'
5859
const CLASS_NAME_OPTGROUP = 'form-multi-select-optgroup'
5960
const CLASS_NAME_OPTGROUP_LABEL = 'form-multi-select-optgroup-label'
6061
const CLASS_NAME_OPTION = 'form-multi-select-option'
@@ -73,14 +74,16 @@ const CLASS_NAME_TAG_DELETE = 'form-multi-select-tag-delete'
7374
const CLASS_NAME_LABEL = 'label'
7475

7576
const Default = {
76-
cleaner: false,
77+
cleaner: true,
7778
multiple: true,
7879
placeholder: 'Select...',
7980
options: false,
8081
optionsMaxHeight: 'auto',
81-
optionsStyle: 'default',
82+
optionsStyle: 'checkbox',
8283
search: false,
8384
searchNoResultsLabel: 'No results found',
85+
selectAll: true,
86+
selectAllLabel: 'Select all options',
8487
selectionType: 'tag',
8588
selectionTypeCounterText: 'item(s) selected'
8689
}
@@ -94,6 +97,8 @@ const DefaultType = {
9497
optionsStyle: 'string',
9598
search: 'boolean',
9699
searchNoResultsLabel: 'string',
100+
selectAll: 'boolean',
101+
selectAllLabel: 'string',
97102
selectionType: 'string',
98103
selectionTypeCounterText: 'string'
99104
}
@@ -108,6 +113,7 @@ class MultiSelect extends BaseComponent {
108113
constructor(element, config) {
109114
super(element)
110115

116+
this._selectAllElement = null
111117
this._selectionElement = null
112118
this._selectionCleanerElement = null
113119
this._searchElement = null
@@ -181,6 +187,28 @@ class MultiSelect extends BaseComponent {
181187
this._addEventListeners()
182188
}
183189

190+
selectAll(options = this._options) {
191+
options.forEach(option => {
192+
if (option.label) {
193+
this.selectAll(option.options)
194+
return
195+
}
196+
197+
this._selectOption(option.value, option.text)
198+
})
199+
}
200+
201+
deselectAll(selection = this._selection) {
202+
selection.forEach(option => {
203+
if (option.label) {
204+
this.deselectAll(option.options)
205+
return
206+
}
207+
208+
this._deselectOption(option.value)
209+
})
210+
}
211+
184212
getValue() {
185213
return this._selection
186214
}
@@ -200,10 +228,16 @@ class MultiSelect extends BaseComponent {
200228
const key = event.keyCode || event.charCode
201229

202230
if ((key === 8 || key === 46) && event.target.value.length === 0) {
203-
this._selectionDeleteLast()
231+
this._deselectLastOption()
204232
}
205233
})
206234

235+
EventHandler.on(this._selectAllElement, EVENT_CLICK, event => {
236+
event.preventDefault()
237+
event.stopPropagation()
238+
this.selectAll()
239+
})
240+
207241
EventHandler.on(this._optionsElement, EVENT_CLICK, event => {
208242
event.preventDefault()
209243
event.stopPropagation()
@@ -213,11 +247,7 @@ class MultiSelect extends BaseComponent {
213247
EventHandler.on(this._selectionCleanerElement, EVENT_CLICK, event => {
214248
event.preventDefault()
215249
event.stopPropagation()
216-
this._selectionClear()
217-
this._updateSelection()
218-
this._updateSelectionCleaner()
219-
this._updateSearch()
220-
this._updateSearchSize()
250+
this.deselectAll()
221251
})
222252

223253
EventHandler.on(this._optionsElement, EVENT_KEYDOWN, event => {
@@ -382,7 +412,7 @@ class MultiSelect extends BaseComponent {
382412
}
383413

384414
_createSelectionCleaner() {
385-
if (this._config.cleaner) {
415+
if (this._config.cleaner && this._config.multiple) {
386416
const cleaner = document.createElement('button')
387417
cleaner.classList.add(CLASS_NAME_SELECTION_CLEANER)
388418
this._clone.append(cleaner)
@@ -404,18 +434,33 @@ class MultiSelect extends BaseComponent {
404434
}
405435

406436
_createOptionsContainer() {
407-
const div = document.createElement('div')
408-
div.classList.add(CLASS_NAME_OPTIONS)
437+
const dropdownDiv = document.createElement('div')
438+
dropdownDiv.classList.add(CLASS_NAME_SELECT_DROPDOWN)
439+
440+
if (this._config.selectAll && this._config.multiple) {
441+
const selectAll = document.createElement('button')
442+
selectAll.classList.add(CLASS_NAME_SELECT_ALL)
443+
selectAll.innerHTML = this._config.selectAllLabel
444+
445+
this._selectAllElement = selectAll
446+
447+
dropdownDiv.append(selectAll)
448+
}
449+
450+
const optionsDiv = document.createElement('div')
451+
optionsDiv.classList.add(CLASS_NAME_OPTIONS)
409452

410453
if (this._config.optionsMaxHeight !== 'auto') {
411-
div.style.maxHeight = `${this._config.optionsMaxHeight}px`
412-
div.style.overflow = 'scroll'
454+
optionsDiv.style.maxHeight = `${this._config.optionsMaxHeight}px`
455+
optionsDiv.style.overflow = 'scroll'
413456
}
414457

415-
this._clone.append(div)
458+
dropdownDiv.append(optionsDiv)
459+
460+
this._clone.append(dropdownDiv)
416461

417-
this._createOptions(div, this._options)
418-
this._optionsElement = div
462+
this._createOptions(optionsDiv, this._options)
463+
this._optionsElement = optionsDiv
419464
}
420465

421466
_createOptions(parentElement, options) {
@@ -466,7 +511,7 @@ class MultiSelect extends BaseComponent {
466511
event.stopPropagation()
467512

468513
tag.remove()
469-
this._selectionDelete(value)
514+
this._deselectOption(value)
470515
this._updateOptionsList()
471516
this._updateSearch()
472517
})
@@ -483,22 +528,17 @@ class MultiSelect extends BaseComponent {
483528
const text = element.textContent
484529

485530
if (this._config.multiple && element.classList.contains(CLASS_NAME_SELECTED)) {
486-
this._selectionDelete(value)
531+
this._deselectOption(value)
487532
} else if (this._config.multiple && !element.classList.contains(CLASS_NAME_SELECTED)) {
488-
this._selectionAdd(value, text)
533+
this._selectOption(value, text)
489534
} else if (!this._config.multiple) {
490-
this._selectionAdd(value, text)
535+
this._selectOption(value, text)
491536
}
492-
493-
this._updateSelection()
494-
this._updateSelectionCleaner()
495-
this._updateSearch()
496-
this._updateSearchSize()
497537
}
498538

499-
_selectionAdd(value, text) {
539+
_selectOption(value, text) {
500540
if (!this._config.multiple) {
501-
this._selectionClear()
541+
this.deselectAll()
502542
}
503543

504544
if (this._selection.filter(e => e.value === value).length === 0) {
@@ -508,27 +548,48 @@ class MultiSelect extends BaseComponent {
508548
})
509549
}
510550

511-
this._selectOption(value)
512-
}
551+
SelectorEngine.findOne(`option[value="${value}"]`, this._element).selected = true
513552

514-
_selectionClear() {
515-
this._selection.length = 0
516-
this._clearOptions()
553+
const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement)
554+
if (option) {
555+
option.classList.add(CLASS_NAME_SELECTED)
556+
}
557+
558+
EventHandler.trigger(this._element, EVENT_CHANGED, {
559+
value: this._selection
560+
})
561+
562+
this._updateSelection()
563+
this._updateSelectionCleaner()
564+
this._updateSearch()
565+
this._updateSearchSize()
517566
}
518567

519-
_selectionDelete(value) {
568+
_deselectOption(value) {
520569
const selected = this._selection.filter(e => e.value !== value)
521570
this._selection = selected
522-
this._unSelectOption(value)
571+
572+
SelectorEngine.findOne(`option[value="${value}"]`, this._element).selected = false
573+
574+
const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement)
575+
if (option) {
576+
option.classList.remove(CLASS_NAME_SELECTED)
577+
}
578+
579+
EventHandler.trigger(this._element, EVENT_CHANGED, {
580+
value: this._selection
581+
})
582+
583+
this._updateSelection()
584+
this._updateSelectionCleaner()
585+
this._updateSearch()
586+
this._updateSearchSize()
523587
}
524588

525-
_selectionDeleteLast() {
589+
_deselectLastOption() {
526590
if (this._selection.length > 0) {
527591
const last = this._selection.pop()
528-
this._selectionDelete(last.value)
529-
this._updateSelection()
530-
this._updateSelectionCleaner()
531-
this._updateSearch()
592+
this._deselectOption(last.value)
532593
}
533594
}
534595

@@ -559,7 +620,7 @@ class MultiSelect extends BaseComponent {
559620
}
560621

561622
_updateSelectionCleaner() {
562-
if (this._config.cleaner === false || this._selectionCleanerElement === null) {
623+
if (!this._config.cleaner || this._selectionCleanerElement === null) {
563624
return
564625
}
565626

@@ -617,41 +678,6 @@ class MultiSelect extends BaseComponent {
617678
}
618679
}
619680

620-
_selectOption(value) {
621-
SelectorEngine.findOne(`option[value="${value}"]`, this._element).selected = true
622-
623-
const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement)
624-
if (option) {
625-
option.classList.add(CLASS_NAME_SELECTED)
626-
}
627-
628-
EventHandler.trigger(this._element, EVENT_CHANGED, {
629-
value: this._selection
630-
})
631-
}
632-
633-
_unSelectOption(value) {
634-
SelectorEngine.findOne(`option[value="${value}"]`, this._element).selected = false
635-
636-
const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement)
637-
if (option) {
638-
option.classList.remove(CLASS_NAME_SELECTED)
639-
}
640-
641-
EventHandler.trigger(this._element, EVENT_CHANGED, {
642-
value: this._selection
643-
})
644-
}
645-
646-
_clearOptions() {
647-
this._element.value = null
648-
SelectorEngine.find(SELECTOR_SELECTED, this._clone).forEach(element => {
649-
element.classList.remove(CLASS_NAME_SELECTED)
650-
})
651-
}
652-
653-
// eslint-disable-next-line no-warning-comments
654-
// TODO: poprawić tą nazwę
655681
_onSearchChange(element) {
656682
if (element) {
657683
this.search(element.value)
@@ -674,10 +700,6 @@ class MultiSelect extends BaseComponent {
674700
})
675701
}
676702

677-
_isHidden(element) {
678-
return element.offsetParent === null
679-
}
680-
681703
_isVisible(element) {
682704
const style = window.getComputedStyle(element)
683705
return (style.display !== 'none')

scss/_variables.scss

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,17 +1060,25 @@ $form-multi-select-search-color: $input-color !default;
10601060
$form-multi-select-search-bg: $input-bg !default;
10611061
$form-multi-select-search-border-radius: $border-radius !default;
10621062

1063-
$form-multi-select-cleaner-width: .75rem !default;
1064-
$form-multi-select-cleaner-height: .75rem !default;
1065-
$form-multi-select-cleaner-padding-x: .5rem !default;
1066-
$form-multi-select-cleaner-padding-y: $form-multi-select-cleaner-padding-x !default;
1067-
$form-multi-select-cleaner-color: $medium-emphasis !default;
1068-
$form-multi-select-cleaner-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$form-multi-select-cleaner-color}'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>") !default;
1069-
$form-multi-select-cleaner-hover-color: $high-emphasis !default;
1070-
$form-multi-select-cleaner-hover-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$form-multi-select-cleaner-hover-color}'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>") !default;
1071-
$form-multi-select-cleaner-focus-color: $high-emphasis !default;
1072-
$form-multi-select-cleaner-focus-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$form-multi-select-cleaner-focus-color}'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>") !default;
1073-
1063+
$form-multi-select-cleaner-width: .75rem !default;
1064+
$form-multi-select-cleaner-height: .75rem !default;
1065+
$form-multi-select-cleaner-padding-x: .5rem !default;
1066+
$form-multi-select-cleaner-padding-y: $form-multi-select-cleaner-padding-x !default;
1067+
$form-multi-select-cleaner-color: $medium-emphasis !default;
1068+
$form-multi-select-cleaner-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$form-multi-select-cleaner-color}'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>") !default;
1069+
$form-multi-select-cleaner-hover-color: $high-emphasis !default;
1070+
$form-multi-select-cleaner-hover-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$form-multi-select-cleaner-hover-color}'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>") !default;
1071+
$form-multi-select-cleaner-focus-color: $high-emphasis !default;
1072+
$form-multi-select-cleaner-focus-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$form-multi-select-cleaner-focus-color}'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>") !default;
1073+
$form-multi-select-cleaner-border-color: $input-border-color !default;
1074+
1075+
$form-multi-select-select-all-padding-y: .5rem !default;
1076+
$form-multi-select-select-all-padding-x: .75rem !default;
1077+
$form-multi-select-select-all-color: $medium-emphasis !default;
1078+
$form-multi-select-select-all-bg: transparent !default;
1079+
$form-multi-select-select-all-hover-color: $high-emphasis !default;
1080+
$form-multi-select-select-all-hover-bg: transparent !default;
1081+
$form-multi-select-select-all-border-color: $input-border-color !default;
10741082

10751083
$form-multi-select-options-padding-y: .5rem !default;
10761084
$form-multi-select-options-padding-x: .75rem !default;
@@ -1083,13 +1091,14 @@ $form-multi-select-options-border-width: $border-width !default;
10831091
$form-multi-select-options-border-radius: $border-radius !default;
10841092

10851093
$form-multi-select-optgroup-label-font-weight: $font-weight-bold !default;
1094+
$form-multi-select-optgroup-label-color: $disabled !default;
10861095

10871096
$form-multi-select-option-padding-y: .5rem !default;
10881097
$form-multi-select-option-padding-x: 1.25rem !default;
10891098
$form-multi-select-option-border-radius: $border-radius !default;
10901099

10911100
$form-multi-select-option-hover-color: shift-color($gray-900, 5) !default;
1092-
$form-multi-select-option-hover-bg: $gray-100 !default;
1101+
$form-multi-select-option-hover-bg: rgba($gray-100, .5) !default;
10931102

10941103
$form-multi-select-option-indicator-width: 1em !default;
10951104
$form-multi-select-option-indicator-bg: $form-check-input-bg !default;

0 commit comments

Comments
 (0)