Skip to content

Commit eca59ac

Browse files
committed
Merge '2.0' git://github.com/kubacode/vue-multiselect into kubacode-2.0
2 parents 1bdcdfb + c30b09a commit eca59ac

File tree

8 files changed

+826
-12
lines changed

8 files changed

+826
-12
lines changed

documentation/partials/examples/Groups.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ div
77
:multiple="true",
88
group-values="libs",
99
group-label="language",
10+
:group-select="true",
1011
placeholder="Type to search",
1112
track-by="name",
1213
label="name",

documentation/partials/examples/_examples.pug

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
+example('CustomOption')
8282
+subsection('Option groups')
8383
:markdown-it
84-
The options list can also contain groups. It requires passing 2 additional props: `group-label` and `group-values`. `group-label` is used to locate the group label. `group-values` should point to the group’s option list.
84+
The options list can also contain groups. It requires passing 3 additional props: `group-label`, `group-values` and `group-select`. `group-label` is used to locate the group label. `group-values` should point to the group’s option list. `group-select` is used to define if selecting the group label should select/unselect all values in the group, or do nothing.
8585

8686
Despite that the available options are grouped, the selected options are stored as a flat array of objects.
8787

gh-pages/index.html

Lines changed: 588 additions & 0 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Multiselect.vue

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,12 @@
9090
</span>
9191
<span
9292
v-if="option && (option.$isLabel || option.$isDisabled)"
93-
:class="optionHighlight(index, option)"
94-
class="multiselect__option multiselect__option--disabled">
93+
:data-select="groupSelect && selectGroupLabelText"
94+
:data-deselect="groupSelect && deselectGroupLabelText"
95+
:class="groupHighlight(index, option)"
96+
@mouseenter.self="groupSelect && pointerSet(index)"
97+
@mousedown.prevent="selectGroup(option)"
98+
class="multiselect__option">
9599
<slot name="option" :option="option" :search="search">
96100
<span>{{ getOptionLabel(option) }}</span>
97101
</slot>
@@ -137,6 +141,15 @@
137141
type: String,
138142
default: 'Press enter to select'
139143
},
144+
/**
145+
* String to show when pointing to an option
146+
* @default 'Press enter to select'
147+
* @type {String}
148+
*/
149+
selectGroupLabel: {
150+
type: String,
151+
default: 'Press enter to select group'
152+
},
140153
/**
141154
* String to show next to selected option
142155
* @default 'Selected'
@@ -155,6 +168,15 @@
155168
type: String,
156169
default: 'Press enter to remove'
157170
},
171+
/**
172+
* String to show when pointing to an alredy selected option
173+
* @default 'Press enter to remove'
174+
* @type {String}
175+
*/
176+
deselectGroupLabel: {
177+
type: String,
178+
default: 'Press enter to deselect group'
179+
},
158180
/**
159181
* Decide whether to show pointer labels
160182
* @default true
@@ -240,11 +262,21 @@
240262
? this.deselectLabel
241263
: ''
242264
},
265+
deselectGroupLabelText () {
266+
return this.showLabels
267+
? this.deselectGroupLabel
268+
: ''
269+
},
243270
selectLabelText () {
244271
return this.showLabels
245272
? this.selectLabel
246273
: ''
247274
},
275+
selectGroupLabelText () {
276+
return this.showLabels
277+
? this.selectGroupLabel
278+
: ''
279+
},
248280
selectedLabelText () {
249281
return this.showLabels
250282
? this.selectedLabel
@@ -650,8 +682,33 @@ fieldset[disabled] .multiselect {
650682
pointer-events: none;
651683
}
652684
685+
.multiselect__option--group {
686+
background: #ededed;
687+
color: #35495E;
688+
}
689+
690+
.multiselect__option--group.multiselect__option--highlight {
691+
background: #35495E;
692+
color: #fff;
693+
}
694+
695+
.multiselect__option--group.multiselect__option--highlight:after {
696+
background: #35495E;
697+
}
698+
653699
.multiselect__option--disabled.multiselect__option--highlight {
654-
background: #dedede !important;
700+
background: #dedede;
701+
}
702+
703+
.multiselect__option--group-selected.multiselect__option--highlight {
704+
background: #FF6A6A;
705+
color: #fff;
706+
}
707+
708+
.multiselect__option--group-selected.multiselect__option--highlight:after {
709+
background: #FF6A6A;
710+
content: attr(data-deselect);
711+
color: #fff;
655712
}
656713
657714
.multiselect-enter-active,

src/multiselectMixin.js

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export default {
136136
default: true
137137
},
138138
/**
139-
* Clear the search input after select()
139+
* Clear the search input after `)
140140
* @default true
141141
* @type {Boolean}
142142
*/
@@ -277,6 +277,16 @@ export default {
277277
groupLabel: {
278278
type: String
279279
},
280+
/**
281+
* Name of the property containing
282+
* the group select flag
283+
* @default false
284+
* @type {Boolean}
285+
*/
286+
groupSelect: {
287+
type: Boolean,
288+
default: false
289+
},
280290
/**
281291
* Array of keyboard keys to block
282292
* when selecting
@@ -483,7 +493,15 @@ export default {
483493
*/
484494
select (option, key) {
485495
/* istanbul ignore else */
486-
if (this.blockKeys.indexOf(key) !== -1 || this.disabled || option.$isLabel || option.$isDisabled) return
496+
if (option.$isLabel && this.groupSelect) {
497+
this.selectGroup(option)
498+
return
499+
}
500+
if (this.blockKeys.indexOf(key) !== -1 ||
501+
this.disabled ||
502+
option.$isDisabled ||
503+
option.$isLabel
504+
) return
487505
/* istanbul ignore else */
488506
if (this.max && this.multiple && this.internalValue.length === this.max) return
489507
/* istanbul ignore else */
@@ -511,6 +529,39 @@ export default {
511529
/* istanbul ignore else */
512530
if (this.closeOnSelect) this.deactivate()
513531
},
532+
/**
533+
* Add the given group options to the list of selected options
534+
* If all group optiona are already selected -> remove it from the results.
535+
*
536+
* @param {Object||String||Integer} group to select/deselect
537+
*/
538+
selectGroup (selectedGroup) {
539+
const group = this.options.find(option => {
540+
return option[this.groupLabel] === selectedGroup.$groupLabel
541+
})
542+
543+
if (!group) return
544+
545+
if (this.wholeGroupSelected(group)) {
546+
group[this.groupValues].forEach(option => {
547+
this.removeElement(option)
548+
})
549+
} else {
550+
group[this.groupValues].forEach(option => {
551+
if (!this.isSelected(option)) {
552+
this.select(option)
553+
}
554+
})
555+
}
556+
},
557+
/**
558+
* Helper to identify if all values in a group are selected
559+
*
560+
* @param {Object} group to validated selected values against
561+
*/
562+
wholeGroupSelected (group) {
563+
return group[this.groupValues].every(this.isSelected)
564+
},
514565
/**
515566
* Removes the given option from the selected options.
516567
* Additionally checks this.allowEmpty prop if option can be removed when

src/pointerMixin.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ export default {
4343
'multiselect__option--selected': this.isSelected(option)
4444
}
4545
},
46+
groupHighlight (index, selectedGroup) {
47+
const group = this.options.find(option => {
48+
return option[this.groupLabel] === selectedGroup.$groupLabel
49+
})
50+
51+
return [
52+
this.groupSelect ? 'multiselect__option--group' : 'multiselect__option--disabled',
53+
{ 'multiselect__option--highlight': index === this.pointer && this.showPointer },
54+
{ 'multiselect__option--group-selected': this.wholeGroupSelected(group) }
55+
]
56+
},
4657
addPointerElement ({ key } = 'Enter') {
4758
/* istanbul ignore else */
4859
if (this.filteredOptions.length > 0) {
@@ -59,7 +70,7 @@ export default {
5970
this.$refs.list.scrollTop = this.pointerPosition - (this.visibleElements - 1) * this.optionHeight
6071
}
6172
/* istanbul ignore else */
62-
if (this.filteredOptions[this.pointer].$isLabel) this.pointerForward()
73+
if (this.filteredOptions[this.pointer].$isLabel && !this.groupSelect) this.pointerForward()
6374
}
6475
this.pointerDirty = true
6576
},
@@ -71,10 +82,10 @@ export default {
7182
this.$refs.list.scrollTop = this.pointerPosition
7283
}
7384
/* istanbul ignore else */
74-
if (this.filteredOptions[this.pointer].$isLabel) this.pointerBackward()
85+
if (this.filteredOptions[this.pointer].$isLabel && !this.groupSelect) this.pointerBackward()
7586
} else {
7687
/* istanbul ignore else */
77-
if (this.filteredOptions[0].$isLabel) this.pointerForward()
88+
if (this.filteredOptions[0].$isLabel && !this.groupSelect) this.pointerForward()
7889
}
7990
this.pointerDirty = true
8091
},
@@ -95,7 +106,10 @@ export default {
95106
: 0
96107
}
97108

98-
if (this.filteredOptions.length > 0 && this.filteredOptions[this.pointer].$isLabel) {
109+
if (this.filteredOptions.length > 0 &&
110+
this.filteredOptions[this.pointer].$isLabel &&
111+
!this.groupSelect
112+
) {
99113
this.pointerForward()
100114
}
101115
},

test/unit/specs/Multiselect.spec.js

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,110 @@ describe('Multiselect.vue', () => {
997997
})
998998
})
999999
})
1000-
1000+
describe('#selectGroup()', () => {
1001+
it('should do nothing when selecting a group label and groupSelect == FALSE', () => {
1002+
const onSelectGroup = sinon.spy()
1003+
const vm = new Vue({
1004+
render (h) {
1005+
return h(Multiselect, {
1006+
props: {
1007+
value: this.value,
1008+
options: this.source,
1009+
multiple: true,
1010+
groupValues: 'values',
1011+
groupLabel: 'label'
1012+
},
1013+
on: {
1014+
selectGroup: onSelectGroup
1015+
}
1016+
})
1017+
},
1018+
components: { Multiselect },
1019+
data: {
1020+
value: [],
1021+
source: [{label: 'Label 1', values: [{name: 'Value 1'}, {name: 'Value 2'}]}, {label: 'Label 2', values: [{name: 'Value 3'}, {name: 'Value 4'}]}]
1022+
}
1023+
}).$mount()
1024+
vm.$children[0].selectGroup(vm.$children[0].filteredOptions[0])
1025+
expect(onSelectGroup.called).to.equal(false)
1026+
})
1027+
describe('when selecting a group label and groupSelect == TRUE', () => {
1028+
it('should add values to selected array', () => {
1029+
const vm = new Vue({
1030+
render (h) {
1031+
return h(Multiselect, {
1032+
props: {
1033+
value: this.value,
1034+
options: this.source,
1035+
multiple: true,
1036+
groupValues: 'values',
1037+
groupLabel: 'label',
1038+
groupSelect: true
1039+
}
1040+
})
1041+
},
1042+
components: { Multiselect },
1043+
data: {
1044+
value: [],
1045+
source: [{label: 'Label 1', values: ['Value 1', 'Value 2']}, {label: 'Label 2', values: ['Value 3', 'Value 4']}]
1046+
}
1047+
}).$mount()
1048+
vm.$children[0].selectGroup(vm.$children[0].filteredOptions[0])
1049+
expect(vm.$children[0].internalValue).to.deep.equal(['Value 1', 'Value 2'])
1050+
})
1051+
it('should add objects to selected array', () => {
1052+
const vm = new Vue({
1053+
render (h) {
1054+
return h(Multiselect, {
1055+
props: {
1056+
value: this.value,
1057+
options: this.source,
1058+
multiple: true,
1059+
trackBy: 'name',
1060+
groupValues: 'values',
1061+
groupLabel: 'label',
1062+
groupSelect: true
1063+
}
1064+
})
1065+
},
1066+
components: { Multiselect },
1067+
data: {
1068+
value: [],
1069+
source: [{label: 'Label 1', values: [{name: 'Value 1'}, {name: 'Value 2'}]}, {label: 'Label 2', values: [{name: 'Value 3'}, {name: 'Value 4'}]}]
1070+
}
1071+
}).$mount()
1072+
vm.$children[0].selectGroup(vm.$children[0].filteredOptions[0])
1073+
expect(vm.$children[0].internalValue).to.deep.equal([{name: 'Value 1'}, {name: 'Value 2'}])
1074+
})
1075+
it('should remove already selected objects', () => {
1076+
const vm = new Vue({
1077+
render (h) {
1078+
return h(Multiselect, {
1079+
props: {
1080+
value: this.value,
1081+
options: this.source,
1082+
multiple: true,
1083+
trackBy: 'name',
1084+
groupValues: 'values',
1085+
groupLabel: 'label',
1086+
groupSelect: true
1087+
}
1088+
})
1089+
},
1090+
components: { Multiselect },
1091+
data: {
1092+
value: [],
1093+
source: [{label: 'Label 1', values: [{name: 'Value 1'}, {name: 'Value 2'}]}, {label: 'Label 2', values: [{name: 'Value 3'}, {name: 'Value 4'}]}]
1094+
}
1095+
}).$mount()
1096+
vm.$children[0].selectGroup(vm.$children[0].filteredOptions[0])
1097+
vm.$children[0].selectGroup(vm.$children[0].filteredOptions[3])
1098+
expect(vm.$children[0].internalValue).to.deep.equal([{name: 'Value 1'}, {name: 'Value 2'}, {name: 'Value 3'}, {name: 'Value 4'}])
1099+
vm.$children[0].selectGroup(vm.$children[0].filteredOptions[0])
1100+
expect(vm.$children[0].internalValue).to.deep.equal([{name: 'Value 3'}, {name: 'Value 4'}])
1101+
})
1102+
})
1103+
})
10011104
describe('#removeElement()', () => {
10021105
it('should not do anything if disabled == TRUE', () => {
10031106
const vm = new Vue({

0 commit comments

Comments
 (0)