@materializecss/materialize 1.2.0 → 1.2.1
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/Gruntfile.js +722 -712
- package/LICENSE +21 -21
- package/README.md +91 -91
- package/dist/css/materialize.css +68 -135
- package/dist/css/materialize.min.css +12 -12
- package/dist/js/materialize.js +1112 -1112
- package/dist/js/materialize.min.js +6 -6
- package/extras/noUiSlider/nouislider.css +403 -403
- package/extras/noUiSlider/nouislider.js +2147 -2147
- package/js/anime.min.js +34 -34
- package/js/autocomplete.js +479 -479
- package/js/buttons.js +354 -354
- package/js/cards.js +40 -40
- package/js/carousel.js +732 -732
- package/js/cash.js +960 -960
- package/js/characterCounter.js +136 -136
- package/js/chips.js +486 -486
- package/js/collapsible.js +275 -275
- package/js/component.js +44 -44
- package/js/datepicker.js +983 -983
- package/js/dropdown.js +669 -669
- package/js/forms.js +285 -285
- package/js/global.js +428 -428
- package/js/materialbox.js +453 -453
- package/js/modal.js +382 -382
- package/js/parallax.js +138 -138
- package/js/pushpin.js +148 -148
- package/js/range.js +263 -263
- package/js/scrollspy.js +295 -295
- package/js/select.js +391 -391
- package/js/sidenav.js +583 -583
- package/js/slider.js +359 -359
- package/js/tabs.js +402 -402
- package/js/tapTarget.js +315 -315
- package/js/timepicker.js +712 -712
- package/js/toasts.js +325 -325
- package/js/tooltip.js +320 -320
- package/js/waves.js +614 -614
- package/package.json +87 -84
- package/sass/_style.scss +929 -929
- package/sass/components/_badges.scss +55 -55
- package/sass/components/_buttons.scss +322 -322
- package/sass/components/_cards.scss +195 -195
- package/sass/components/_carousel.scss +90 -90
- package/sass/components/_chips.scss +96 -96
- package/sass/components/_collapsible.scss +91 -91
- package/sass/components/_collection.scss +106 -106
- package/sass/components/_color-classes.scss +32 -32
- package/sass/components/_color-variables.scss +370 -370
- package/sass/components/_datepicker.scss +191 -191
- package/sass/components/_dropdown.scss +84 -84
- package/sass/components/_global.scss +646 -646
- package/sass/components/_grid.scss +158 -158
- package/sass/components/_icons-material-design.scss +5 -5
- package/sass/components/_materialbox.scss +42 -42
- package/sass/components/_modal.scss +97 -97
- package/sass/components/_navbar.scss +208 -208
- package/sass/components/_normalize.scss +447 -447
- package/sass/components/_preloader.scss +334 -334
- package/sass/components/_pulse.scss +34 -34
- package/sass/components/_sidenav.scss +214 -214
- package/sass/components/_slider.scss +91 -91
- package/sass/components/_table_of_contents.scss +33 -33
- package/sass/components/_tabs.scss +99 -99
- package/sass/components/_tapTarget.scss +103 -103
- package/sass/components/_timepicker.scss +199 -199
- package/sass/components/_toast.scss +58 -58
- package/sass/components/_tooltip.scss +32 -32
- package/sass/components/_transitions.scss +12 -12
- package/sass/components/_typography.scss +62 -62
- package/sass/components/_variables.scss +352 -352
- package/sass/components/_waves.scss +187 -187
- package/sass/components/forms/_checkboxes.scss +200 -200
- package/sass/components/forms/_file-input.scss +44 -44
- package/sass/components/forms/_forms.scss +22 -22
- package/sass/components/forms/_input-fields.scss +388 -388
- package/sass/components/forms/_radio-buttons.scss +115 -115
- package/sass/components/forms/_range.scss +161 -161
- package/sass/components/forms/_select.scss +199 -199
- package/sass/components/forms/_switches.scss +91 -91
- package/sass/ghpages-materialize.scss +7 -7
- package/sass/materialize.scss +42 -42
package/js/select.js
CHANGED
|
@@ -1,391 +1,391 @@
|
|
|
1
|
-
(function($) {
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
let _defaults = {
|
|
5
|
-
classes: '',
|
|
6
|
-
dropdownOptions: {}
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
class FormSelect extends Component {
|
|
10
|
-
constructor(el, options) {
|
|
11
|
-
super(FormSelect, el, options);
|
|
12
|
-
if (this.$el.hasClass('browser-default')) return;
|
|
13
|
-
this.el.M_FormSelect = this;
|
|
14
|
-
this.options = $.extend({}, FormSelect.defaults, options);
|
|
15
|
-
this.isMultiple = this.$el.prop('multiple');
|
|
16
|
-
this.el.tabIndex = -1;
|
|
17
|
-
this._values = [];
|
|
18
|
-
this.labelEl = null;
|
|
19
|
-
this._labelFor = false;
|
|
20
|
-
this._setupDropdown();
|
|
21
|
-
this._setupEventHandlers();
|
|
22
|
-
}
|
|
23
|
-
static get defaults() {
|
|
24
|
-
return _defaults;
|
|
25
|
-
}
|
|
26
|
-
static init(els, options) {
|
|
27
|
-
return super.init(this, els, options);
|
|
28
|
-
}
|
|
29
|
-
static getInstance(el) {
|
|
30
|
-
let domElem = !!el.jquery ? el[0] : el;
|
|
31
|
-
return domElem.M_FormSelect;
|
|
32
|
-
}
|
|
33
|
-
destroy() {
|
|
34
|
-
// Returns label to its original owner
|
|
35
|
-
if (this._labelFor) this.labelEl.setAttribute("for", this.el.id);
|
|
36
|
-
this._removeEventHandlers();
|
|
37
|
-
this._removeDropdown();
|
|
38
|
-
this.el.M_FormSelect = undefined;
|
|
39
|
-
}
|
|
40
|
-
_setupEventHandlers() {
|
|
41
|
-
this._handleSelectChangeBound = this._handleSelectChange.bind(this);
|
|
42
|
-
this._handleOptionClickBound = this._handleOptionClick.bind(this);
|
|
43
|
-
this._handleInputClickBound = this._handleInputClick.bind(this);
|
|
44
|
-
$(this.dropdownOptions)
|
|
45
|
-
.find('li:not(.optgroup)')
|
|
46
|
-
.each((el) => {
|
|
47
|
-
el.addEventListener('click', this._handleOptionClickBound);
|
|
48
|
-
el.addEventListener('keydown', (e) => {
|
|
49
|
-
if (e.key === " " || e.key === "Enter") this._handleOptionClickBound(e);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
this.el.addEventListener('change', this._handleSelectChangeBound);
|
|
53
|
-
this.input.addEventListener('click', this._handleInputClickBound);
|
|
54
|
-
}
|
|
55
|
-
_removeEventHandlers() {
|
|
56
|
-
$(this.dropdownOptions)
|
|
57
|
-
.find('li:not(.optgroup)')
|
|
58
|
-
.each((el) => {
|
|
59
|
-
el.removeEventListener('click', this._handleOptionClickBound);
|
|
60
|
-
});
|
|
61
|
-
this.el.removeEventListener('change', this._handleSelectChangeBound);
|
|
62
|
-
this.input.removeEventListener('click', this._handleInputClickBound);
|
|
63
|
-
}
|
|
64
|
-
_handleSelectChange(e) {
|
|
65
|
-
this._setValueToInput();
|
|
66
|
-
}
|
|
67
|
-
_handleOptionClick(e) {
|
|
68
|
-
e.preventDefault();
|
|
69
|
-
let virtualOption = $(e.target).closest('li')[0];
|
|
70
|
-
this._selectOptionElement(virtualOption);
|
|
71
|
-
e.stopPropagation();
|
|
72
|
-
}
|
|
73
|
-
_arraysEqual(a, b) {
|
|
74
|
-
if (a === b) return true;
|
|
75
|
-
if (a == null || b == null) return false;
|
|
76
|
-
if (a.length !== b.length) return false;
|
|
77
|
-
for (let i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false;
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
_selectOptionElement(virtualOption) {
|
|
81
|
-
if (!$(virtualOption).hasClass('disabled') && !$(virtualOption).hasClass('optgroup')) {
|
|
82
|
-
const value = this._values.filter((value) => value.optionEl === virtualOption)[0];
|
|
83
|
-
const previousSelectedValues = this.getSelectedValues();
|
|
84
|
-
if (this.isMultiple) {
|
|
85
|
-
// Multi-Select
|
|
86
|
-
this._toggleEntryFromArray(value);
|
|
87
|
-
} else {
|
|
88
|
-
// Single-Select
|
|
89
|
-
this._deselectAll();
|
|
90
|
-
this._selectValue(value);
|
|
91
|
-
}
|
|
92
|
-
// Refresh Input-Text
|
|
93
|
-
this._setValueToInput();
|
|
94
|
-
// Trigger Change-Event only when data is different
|
|
95
|
-
const actualSelectedValues = this.getSelectedValues();
|
|
96
|
-
const selectionHasChanged = !this._arraysEqual(
|
|
97
|
-
previousSelectedValues,
|
|
98
|
-
actualSelectedValues
|
|
99
|
-
);
|
|
100
|
-
if (selectionHasChanged) this.$el.trigger('change');
|
|
101
|
-
}
|
|
102
|
-
if (!this.isMultiple) this.dropdown.close();
|
|
103
|
-
}
|
|
104
|
-
_handleInputClick() {
|
|
105
|
-
if (this.dropdown && this.dropdown.isOpen) {
|
|
106
|
-
this._setValueToInput();
|
|
107
|
-
this._setSelectedStates();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
_setupDropdown() {
|
|
111
|
-
this.wrapper = document.createElement('div');
|
|
112
|
-
$(this.wrapper).addClass('select-wrapper ' + this.options.classes);
|
|
113
|
-
this.$el.before($(this.wrapper));
|
|
114
|
-
|
|
115
|
-
// Move actual select element into overflow hidden wrapper
|
|
116
|
-
let $hideSelect = $('<div class="hide-select"></div>');
|
|
117
|
-
$(this.wrapper).append($hideSelect);
|
|
118
|
-
$hideSelect[0].appendChild(this.el);
|
|
119
|
-
|
|
120
|
-
if (this.el.disabled) this.wrapper.classList.add('disabled');
|
|
121
|
-
|
|
122
|
-
// Create dropdown
|
|
123
|
-
this.$selectOptions = this.$el.children('option, optgroup');
|
|
124
|
-
this.dropdownOptions = document.createElement('ul');
|
|
125
|
-
this.dropdownOptions.id = `select-options-${M.guid()}`;
|
|
126
|
-
$(this.dropdownOptions).addClass(
|
|
127
|
-
'dropdown-content select-dropdown ' + (this.isMultiple ? 'multiple-select-dropdown' : '')
|
|
128
|
-
);
|
|
129
|
-
this.dropdownOptions.setAttribute("role", "listbox");
|
|
130
|
-
this.dropdownOptions.setAttribute("aria-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.input =
|
|
170
|
-
this.input.
|
|
171
|
-
|
|
172
|
-
this.input.setAttribute('
|
|
173
|
-
this.input.setAttribute('
|
|
174
|
-
this.input.setAttribute('
|
|
175
|
-
this.input.setAttribute(
|
|
176
|
-
if (this.el.disabled) $(this.input).prop('disabled', 'true');
|
|
177
|
-
|
|
178
|
-
// Makes new element to assume HTML's select label and
|
|
179
|
-
// aria-attributes, if exists
|
|
180
|
-
if (this.el.hasAttribute("aria-labelledby")){
|
|
181
|
-
this.labelEl = document.getElementById(this.el.getAttribute("aria-labelledby"));
|
|
182
|
-
}
|
|
183
|
-
else if (this.el.id != ""){
|
|
184
|
-
let lbl = $(`label[for='${this.el.id}']`);
|
|
185
|
-
if (lbl.length){
|
|
186
|
-
this.labelEl = lbl[0];
|
|
187
|
-
this.labelEl.removeAttribute("for");
|
|
188
|
-
this._labelFor = true;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
// Tries to find a valid label in parent element
|
|
192
|
-
if (!this.labelEl){
|
|
193
|
-
let el = this.el.parentElement;
|
|
194
|
-
if (el) el = el.getElementsByTagName("label")[0];
|
|
195
|
-
if (el) this.labelEl = el;
|
|
196
|
-
}
|
|
197
|
-
if (this.labelEl && this.labelEl.id == ""){
|
|
198
|
-
this.labelEl.id = "m_select-label-" + M.guid();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (this.labelEl){
|
|
202
|
-
this.labelEl.setAttribute("for", this.input.id);
|
|
203
|
-
this.dropdownOptions.setAttribute("aria-labelledby", this.labelEl.id);
|
|
204
|
-
}
|
|
205
|
-
else this.dropdownOptions.setAttribute("aria-label", "");
|
|
206
|
-
|
|
207
|
-
let attrs = this.el.attributes;
|
|
208
|
-
for (let i = 0; i < attrs.length; ++i){
|
|
209
|
-
const attr = attrs[i];
|
|
210
|
-
if (attr.name.startsWith("aria-"))
|
|
211
|
-
this.input.setAttribute(attr.name, attr.value);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Adds aria-attributes to input element
|
|
215
|
-
this.input.setAttribute("role", "combobox");
|
|
216
|
-
this.input.setAttribute("aria-owns", this.dropdownOptions.id);
|
|
217
|
-
this.input.setAttribute("aria-controls", this.dropdownOptions.id);
|
|
218
|
-
this.input.setAttribute("aria-expanded", false);
|
|
219
|
-
|
|
220
|
-
$(this.wrapper).prepend(this.input);
|
|
221
|
-
this._setValueToInput();
|
|
222
|
-
|
|
223
|
-
// Add caret
|
|
224
|
-
let dropdownIcon = $(
|
|
225
|
-
'<svg class="caret" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'
|
|
226
|
-
);
|
|
227
|
-
$(this.wrapper).prepend(dropdownIcon[0]);
|
|
228
|
-
// Initialize dropdown
|
|
229
|
-
if (!this.el.disabled) {
|
|
230
|
-
let dropdownOptions = $.extend({}, this.options.dropdownOptions);
|
|
231
|
-
dropdownOptions.coverTrigger = false;
|
|
232
|
-
let userOnOpenEnd = dropdownOptions.onOpenEnd;
|
|
233
|
-
let userOnCloseEnd = dropdownOptions.onCloseEnd;
|
|
234
|
-
// Add callback for centering selected option when dropdown content is scrollable
|
|
235
|
-
dropdownOptions.onOpenEnd = (el) => {
|
|
236
|
-
let selectedOption = $(this.dropdownOptions)
|
|
237
|
-
.find('.selected')
|
|
238
|
-
.first();
|
|
239
|
-
if (selectedOption.length) {
|
|
240
|
-
// Focus selected option in dropdown
|
|
241
|
-
M.keyDown = true;
|
|
242
|
-
this.dropdown.focusedIndex = selectedOption.index();
|
|
243
|
-
this.dropdown._focusFocusedItem();
|
|
244
|
-
M.keyDown = false;
|
|
245
|
-
// Handle scrolling to selected option
|
|
246
|
-
if (this.dropdown.isScrollable) {
|
|
247
|
-
let scrollOffset =
|
|
248
|
-
selectedOption[0].getBoundingClientRect().top -
|
|
249
|
-
this.dropdownOptions.getBoundingClientRect().top; // scroll to selected option
|
|
250
|
-
scrollOffset -= this.dropdownOptions.clientHeight / 2; // center in dropdown
|
|
251
|
-
this.dropdownOptions.scrollTop = scrollOffset;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
// Sets "aria-expanded" to "true"
|
|
255
|
-
this.input.setAttribute("aria-expanded", true);
|
|
256
|
-
// Handle user declared onOpenEnd if needed
|
|
257
|
-
if (userOnOpenEnd && typeof userOnOpenEnd === 'function')
|
|
258
|
-
userOnOpenEnd.call(this.dropdown, this.el);
|
|
259
|
-
};
|
|
260
|
-
// Add callback for reseting "expanded" state
|
|
261
|
-
dropdownOptions.onCloseEnd = (el) => {
|
|
262
|
-
// Sets "aria-expanded" to "false"
|
|
263
|
-
this.input.setAttribute("aria-expanded", false);
|
|
264
|
-
// Handle user declared onOpenEnd if needed
|
|
265
|
-
if (userOnCloseEnd && typeof userOnCloseEnd === 'function')
|
|
266
|
-
userOnCloseEnd.call(this.dropdown, this.el);
|
|
267
|
-
};
|
|
268
|
-
// Prevent dropdown from closing too early
|
|
269
|
-
dropdownOptions.closeOnClick = false;
|
|
270
|
-
this.dropdown = M.Dropdown.init(this.input, dropdownOptions);
|
|
271
|
-
}
|
|
272
|
-
// Add initial selections
|
|
273
|
-
this._setSelectedStates();
|
|
274
|
-
}
|
|
275
|
-
_addOptionToValues(realOption, virtualOption) {
|
|
276
|
-
this._values.push({ el: realOption, optionEl: virtualOption });
|
|
277
|
-
}
|
|
278
|
-
_removeDropdown() {
|
|
279
|
-
$(this.wrapper)
|
|
280
|
-
.find('.caret')
|
|
281
|
-
.remove();
|
|
282
|
-
$(this.input).remove();
|
|
283
|
-
$(this.dropdownOptions).remove();
|
|
284
|
-
$(this.wrapper).before(this.$el);
|
|
285
|
-
$(this.wrapper).remove();
|
|
286
|
-
}
|
|
287
|
-
_createAndAppendOptionWithIcon(realOption, type) {
|
|
288
|
-
const li = document.createElement('li');
|
|
289
|
-
li.setAttribute("role", "option");
|
|
290
|
-
if (realOption.disabled){
|
|
291
|
-
li.classList.add('disabled');
|
|
292
|
-
li.setAttribute("aria-disabled", true);
|
|
293
|
-
}
|
|
294
|
-
if (type === 'optgroup-option') li.classList.add(type);
|
|
295
|
-
// Text / Checkbox
|
|
296
|
-
const span = document.createElement('span');
|
|
297
|
-
if (this.isMultiple)
|
|
298
|
-
span.innerHTML = `<label><input type="checkbox"${
|
|
299
|
-
realOption.disabled ? ' disabled="disabled"' : ''
|
|
300
|
-
}><span>${realOption.innerHTML}</span></label>`;
|
|
301
|
-
else span.innerHTML = realOption.innerHTML;
|
|
302
|
-
li.appendChild(span);
|
|
303
|
-
// add Icon
|
|
304
|
-
const iconUrl = realOption.getAttribute('data-icon');
|
|
305
|
-
const classes = realOption.getAttribute('class');
|
|
306
|
-
if (iconUrl) {
|
|
307
|
-
const img = $(`<img alt="" class="${classes}" src="${iconUrl}">`);
|
|
308
|
-
img[0].setAttribute("aria-hidden", true);
|
|
309
|
-
li.prepend(img[0]);
|
|
310
|
-
}
|
|
311
|
-
// Check for multiple type
|
|
312
|
-
$(this.dropdownOptions).append(li);
|
|
313
|
-
return li;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
_selectValue(value) {
|
|
317
|
-
value.el.selected = true;
|
|
318
|
-
value.optionEl.classList.add('selected');
|
|
319
|
-
value.optionEl.setAttribute("aria-selected", true);
|
|
320
|
-
const checkbox = value.optionEl.querySelector('input[type="checkbox"]');
|
|
321
|
-
if (checkbox) checkbox.checked = true;
|
|
322
|
-
}
|
|
323
|
-
_deselectValue(value) {
|
|
324
|
-
value.el.selected = false;
|
|
325
|
-
value.optionEl.classList.remove('selected');
|
|
326
|
-
value.optionEl.setAttribute("aria-selected", false);
|
|
327
|
-
const checkbox = value.optionEl.querySelector('input[type="checkbox"]');
|
|
328
|
-
if (checkbox) checkbox.checked = false;
|
|
329
|
-
}
|
|
330
|
-
_deselectAll() {
|
|
331
|
-
this._values.forEach((value) => {
|
|
332
|
-
this._deselectValue(value);
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
_isValueSelected(value) {
|
|
336
|
-
const realValues = this.getSelectedValues();
|
|
337
|
-
return realValues.some((realValue) => realValue === value.el.value);
|
|
338
|
-
}
|
|
339
|
-
_toggleEntryFromArray(value) {
|
|
340
|
-
const isSelected = this._isValueSelected(value);
|
|
341
|
-
if (isSelected) this._deselectValue(value);
|
|
342
|
-
else this._selectValue(value);
|
|
343
|
-
}
|
|
344
|
-
_getSelectedOptions() {
|
|
345
|
-
return Array.prototype.filter.call(this.el.selectedOptions, (realOption) => realOption);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
_setValueToInput() {
|
|
349
|
-
const realOptions = this._getSelectedOptions();
|
|
350
|
-
const values = this._values.filter((value) => realOptions.indexOf(value.el) >= 0);
|
|
351
|
-
const texts = values.map((value) => value.optionEl.querySelector('span').innerText.trim());
|
|
352
|
-
// Set input-text to first Option with empty value which indicates a description like "choose your option"
|
|
353
|
-
if (texts.length === 0) {
|
|
354
|
-
const firstDisabledOption = this.$el.find('option:disabled').eq(0);
|
|
355
|
-
if (firstDisabledOption.length > 0 && firstDisabledOption[0].value === '') {
|
|
356
|
-
this.input.value = firstDisabledOption.text();
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
this.input.value = texts.join(', ');
|
|
361
|
-
}
|
|
362
|
-
_setSelectedStates() {
|
|
363
|
-
this._values.forEach((value) => {
|
|
364
|
-
const optionIsSelected = $(value.el).prop('selected');
|
|
365
|
-
$(value.optionEl)
|
|
366
|
-
.find('input[type="checkbox"]')
|
|
367
|
-
.prop('checked', optionIsSelected);
|
|
368
|
-
if (optionIsSelected) {
|
|
369
|
-
this._activateOption($(this.dropdownOptions), $(value.optionEl));
|
|
370
|
-
} else {
|
|
371
|
-
$(value.optionEl).removeClass('selected');
|
|
372
|
-
$(value.optionEl).attr("aria-selected", false);
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
_activateOption(ul, li) {
|
|
377
|
-
if (!li) return;
|
|
378
|
-
if (!this.isMultiple) ul.find('li.selected').removeClass('selected');
|
|
379
|
-
$(li).addClass('selected');
|
|
380
|
-
$(li).attr("aria-selected", true);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
getSelectedValues() {
|
|
384
|
-
return this._getSelectedOptions().map((realOption) => realOption.value);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
M.FormSelect = FormSelect;
|
|
389
|
-
|
|
390
|
-
if (M.jQueryLoaded) M.initializeJqueryWrapper(FormSelect, 'formSelect', 'M_FormSelect');
|
|
391
|
-
})(cash);
|
|
1
|
+
(function($) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
let _defaults = {
|
|
5
|
+
classes: '',
|
|
6
|
+
dropdownOptions: {}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
class FormSelect extends Component {
|
|
10
|
+
constructor(el, options) {
|
|
11
|
+
super(FormSelect, el, options);
|
|
12
|
+
if (this.$el.hasClass('browser-default')) return;
|
|
13
|
+
this.el.M_FormSelect = this;
|
|
14
|
+
this.options = $.extend({}, FormSelect.defaults, options);
|
|
15
|
+
this.isMultiple = this.$el.prop('multiple');
|
|
16
|
+
this.el.tabIndex = -1;
|
|
17
|
+
this._values = [];
|
|
18
|
+
this.labelEl = null;
|
|
19
|
+
this._labelFor = false;
|
|
20
|
+
this._setupDropdown();
|
|
21
|
+
this._setupEventHandlers();
|
|
22
|
+
}
|
|
23
|
+
static get defaults() {
|
|
24
|
+
return _defaults;
|
|
25
|
+
}
|
|
26
|
+
static init(els, options) {
|
|
27
|
+
return super.init(this, els, options);
|
|
28
|
+
}
|
|
29
|
+
static getInstance(el) {
|
|
30
|
+
let domElem = !!el.jquery ? el[0] : el;
|
|
31
|
+
return domElem.M_FormSelect;
|
|
32
|
+
}
|
|
33
|
+
destroy() {
|
|
34
|
+
// Returns label to its original owner
|
|
35
|
+
if (this._labelFor) this.labelEl.setAttribute("for", this.el.id);
|
|
36
|
+
this._removeEventHandlers();
|
|
37
|
+
this._removeDropdown();
|
|
38
|
+
this.el.M_FormSelect = undefined;
|
|
39
|
+
}
|
|
40
|
+
_setupEventHandlers() {
|
|
41
|
+
this._handleSelectChangeBound = this._handleSelectChange.bind(this);
|
|
42
|
+
this._handleOptionClickBound = this._handleOptionClick.bind(this);
|
|
43
|
+
this._handleInputClickBound = this._handleInputClick.bind(this);
|
|
44
|
+
$(this.dropdownOptions)
|
|
45
|
+
.find('li:not(.optgroup)')
|
|
46
|
+
.each((el) => {
|
|
47
|
+
el.addEventListener('click', this._handleOptionClickBound);
|
|
48
|
+
el.addEventListener('keydown', (e) => {
|
|
49
|
+
if (e.key === " " || e.key === "Enter") this._handleOptionClickBound(e);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
this.el.addEventListener('change', this._handleSelectChangeBound);
|
|
53
|
+
this.input.addEventListener('click', this._handleInputClickBound);
|
|
54
|
+
}
|
|
55
|
+
_removeEventHandlers() {
|
|
56
|
+
$(this.dropdownOptions)
|
|
57
|
+
.find('li:not(.optgroup)')
|
|
58
|
+
.each((el) => {
|
|
59
|
+
el.removeEventListener('click', this._handleOptionClickBound);
|
|
60
|
+
});
|
|
61
|
+
this.el.removeEventListener('change', this._handleSelectChangeBound);
|
|
62
|
+
this.input.removeEventListener('click', this._handleInputClickBound);
|
|
63
|
+
}
|
|
64
|
+
_handleSelectChange(e) {
|
|
65
|
+
this._setValueToInput();
|
|
66
|
+
}
|
|
67
|
+
_handleOptionClick(e) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
let virtualOption = $(e.target).closest('li')[0];
|
|
70
|
+
this._selectOptionElement(virtualOption);
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
}
|
|
73
|
+
_arraysEqual(a, b) {
|
|
74
|
+
if (a === b) return true;
|
|
75
|
+
if (a == null || b == null) return false;
|
|
76
|
+
if (a.length !== b.length) return false;
|
|
77
|
+
for (let i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
_selectOptionElement(virtualOption) {
|
|
81
|
+
if (!$(virtualOption).hasClass('disabled') && !$(virtualOption).hasClass('optgroup')) {
|
|
82
|
+
const value = this._values.filter((value) => value.optionEl === virtualOption)[0];
|
|
83
|
+
const previousSelectedValues = this.getSelectedValues();
|
|
84
|
+
if (this.isMultiple) {
|
|
85
|
+
// Multi-Select
|
|
86
|
+
this._toggleEntryFromArray(value);
|
|
87
|
+
} else {
|
|
88
|
+
// Single-Select
|
|
89
|
+
this._deselectAll();
|
|
90
|
+
this._selectValue(value);
|
|
91
|
+
}
|
|
92
|
+
// Refresh Input-Text
|
|
93
|
+
this._setValueToInput();
|
|
94
|
+
// Trigger Change-Event only when data is different
|
|
95
|
+
const actualSelectedValues = this.getSelectedValues();
|
|
96
|
+
const selectionHasChanged = !this._arraysEqual(
|
|
97
|
+
previousSelectedValues,
|
|
98
|
+
actualSelectedValues
|
|
99
|
+
);
|
|
100
|
+
if (selectionHasChanged) this.$el.trigger('change');
|
|
101
|
+
}
|
|
102
|
+
if (!this.isMultiple) this.dropdown.close();
|
|
103
|
+
}
|
|
104
|
+
_handleInputClick() {
|
|
105
|
+
if (this.dropdown && this.dropdown.isOpen) {
|
|
106
|
+
this._setValueToInput();
|
|
107
|
+
this._setSelectedStates();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
_setupDropdown() {
|
|
111
|
+
this.wrapper = document.createElement('div');
|
|
112
|
+
$(this.wrapper).addClass('select-wrapper ' + this.options.classes);
|
|
113
|
+
this.$el.before($(this.wrapper));
|
|
114
|
+
|
|
115
|
+
// Move actual select element into overflow hidden wrapper
|
|
116
|
+
let $hideSelect = $('<div class="hide-select"></div>');
|
|
117
|
+
$(this.wrapper).append($hideSelect);
|
|
118
|
+
$hideSelect[0].appendChild(this.el);
|
|
119
|
+
|
|
120
|
+
if (this.el.disabled) this.wrapper.classList.add('disabled');
|
|
121
|
+
|
|
122
|
+
// Create dropdown
|
|
123
|
+
this.$selectOptions = this.$el.children('option, optgroup');
|
|
124
|
+
this.dropdownOptions = document.createElement('ul');
|
|
125
|
+
this.dropdownOptions.id = `select-options-${M.guid()}`;
|
|
126
|
+
$(this.dropdownOptions).addClass(
|
|
127
|
+
'dropdown-content select-dropdown ' + (this.isMultiple ? 'multiple-select-dropdown' : '')
|
|
128
|
+
);
|
|
129
|
+
this.dropdownOptions.setAttribute("role", "listbox");
|
|
130
|
+
this.dropdownOptions.setAttribute("aria-multiselectable", this.isMultiple);
|
|
131
|
+
|
|
132
|
+
// Create dropdown structure
|
|
133
|
+
if (this.$selectOptions.length) {
|
|
134
|
+
this.$selectOptions.each((realOption) => {
|
|
135
|
+
if ($(realOption).is('option')) {
|
|
136
|
+
// Option
|
|
137
|
+
const virtualOption = this._createAndAppendOptionWithIcon(
|
|
138
|
+
realOption,
|
|
139
|
+
this.isMultiple ? 'multiple' : undefined
|
|
140
|
+
);
|
|
141
|
+
this._addOptionToValues(realOption, virtualOption);
|
|
142
|
+
} else if ($(realOption).is('optgroup')) {
|
|
143
|
+
// Optgroup
|
|
144
|
+
const selectOptions = $(realOption).children('option');
|
|
145
|
+
let lId = "opt-group-" + M.guid();
|
|
146
|
+
let groupParent = $(
|
|
147
|
+
`<li class="optgroup" role="group" aria-labelledby="${lId}" tabindex="-1"><span id="${lId}" role="presentation">${realOption.getAttribute('label')}</span></li>`
|
|
148
|
+
)[0];
|
|
149
|
+
let groupChildren = [];
|
|
150
|
+
$(this.dropdownOptions).append(groupParent);
|
|
151
|
+
selectOptions.each((realOption) => {
|
|
152
|
+
const virtualOption = this._createAndAppendOptionWithIcon(
|
|
153
|
+
realOption,
|
|
154
|
+
'optgroup-option'
|
|
155
|
+
);
|
|
156
|
+
let cId = "opt-child-" + M.guid();
|
|
157
|
+
virtualOption.id = cId;
|
|
158
|
+
groupChildren.push(cId);
|
|
159
|
+
this._addOptionToValues(realOption, virtualOption);
|
|
160
|
+
});
|
|
161
|
+
groupParent.setAttribute("aria-owns", groupChildren.join(" "));
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
$(this.wrapper).append(this.dropdownOptions);
|
|
166
|
+
|
|
167
|
+
// Add input dropdown
|
|
168
|
+
this.input = document.createElement('input');
|
|
169
|
+
this.input.id = "m_select-input-" + M.guid();
|
|
170
|
+
$(this.input).addClass('select-dropdown dropdown-trigger');
|
|
171
|
+
this.input.setAttribute('type', 'text');
|
|
172
|
+
this.input.setAttribute('readonly', 'true');
|
|
173
|
+
this.input.setAttribute('data-target', this.dropdownOptions.id);
|
|
174
|
+
this.input.setAttribute('aria-readonly', 'true');
|
|
175
|
+
this.input.setAttribute("aria-required", this.el.hasAttribute("required"));
|
|
176
|
+
if (this.el.disabled) $(this.input).prop('disabled', 'true');
|
|
177
|
+
|
|
178
|
+
// Makes new element to assume HTML's select label and
|
|
179
|
+
// aria-attributes, if exists
|
|
180
|
+
if (this.el.hasAttribute("aria-labelledby")){
|
|
181
|
+
this.labelEl = document.getElementById(this.el.getAttribute("aria-labelledby"));
|
|
182
|
+
}
|
|
183
|
+
else if (this.el.id != ""){
|
|
184
|
+
let lbl = $(`label[for='${this.el.id}']`);
|
|
185
|
+
if (lbl.length){
|
|
186
|
+
this.labelEl = lbl[0];
|
|
187
|
+
this.labelEl.removeAttribute("for");
|
|
188
|
+
this._labelFor = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Tries to find a valid label in parent element
|
|
192
|
+
if (!this.labelEl){
|
|
193
|
+
let el = this.el.parentElement;
|
|
194
|
+
if (el) el = el.getElementsByTagName("label")[0];
|
|
195
|
+
if (el) this.labelEl = el;
|
|
196
|
+
}
|
|
197
|
+
if (this.labelEl && this.labelEl.id == ""){
|
|
198
|
+
this.labelEl.id = "m_select-label-" + M.guid();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (this.labelEl){
|
|
202
|
+
this.labelEl.setAttribute("for", this.input.id);
|
|
203
|
+
this.dropdownOptions.setAttribute("aria-labelledby", this.labelEl.id);
|
|
204
|
+
}
|
|
205
|
+
else this.dropdownOptions.setAttribute("aria-label", "");
|
|
206
|
+
|
|
207
|
+
let attrs = this.el.attributes;
|
|
208
|
+
for (let i = 0; i < attrs.length; ++i){
|
|
209
|
+
const attr = attrs[i];
|
|
210
|
+
if (attr.name.startsWith("aria-"))
|
|
211
|
+
this.input.setAttribute(attr.name, attr.value);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Adds aria-attributes to input element
|
|
215
|
+
this.input.setAttribute("role", "combobox");
|
|
216
|
+
this.input.setAttribute("aria-owns", this.dropdownOptions.id);
|
|
217
|
+
this.input.setAttribute("aria-controls", this.dropdownOptions.id);
|
|
218
|
+
this.input.setAttribute("aria-expanded", false);
|
|
219
|
+
|
|
220
|
+
$(this.wrapper).prepend(this.input);
|
|
221
|
+
this._setValueToInput();
|
|
222
|
+
|
|
223
|
+
// Add caret
|
|
224
|
+
let dropdownIcon = $(
|
|
225
|
+
'<svg class="caret" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'
|
|
226
|
+
);
|
|
227
|
+
$(this.wrapper).prepend(dropdownIcon[0]);
|
|
228
|
+
// Initialize dropdown
|
|
229
|
+
if (!this.el.disabled) {
|
|
230
|
+
let dropdownOptions = $.extend({}, this.options.dropdownOptions);
|
|
231
|
+
dropdownOptions.coverTrigger = false;
|
|
232
|
+
let userOnOpenEnd = dropdownOptions.onOpenEnd;
|
|
233
|
+
let userOnCloseEnd = dropdownOptions.onCloseEnd;
|
|
234
|
+
// Add callback for centering selected option when dropdown content is scrollable
|
|
235
|
+
dropdownOptions.onOpenEnd = (el) => {
|
|
236
|
+
let selectedOption = $(this.dropdownOptions)
|
|
237
|
+
.find('.selected')
|
|
238
|
+
.first();
|
|
239
|
+
if (selectedOption.length) {
|
|
240
|
+
// Focus selected option in dropdown
|
|
241
|
+
M.keyDown = true;
|
|
242
|
+
this.dropdown.focusedIndex = selectedOption.index();
|
|
243
|
+
this.dropdown._focusFocusedItem();
|
|
244
|
+
M.keyDown = false;
|
|
245
|
+
// Handle scrolling to selected option
|
|
246
|
+
if (this.dropdown.isScrollable) {
|
|
247
|
+
let scrollOffset =
|
|
248
|
+
selectedOption[0].getBoundingClientRect().top -
|
|
249
|
+
this.dropdownOptions.getBoundingClientRect().top; // scroll to selected option
|
|
250
|
+
scrollOffset -= this.dropdownOptions.clientHeight / 2; // center in dropdown
|
|
251
|
+
this.dropdownOptions.scrollTop = scrollOffset;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Sets "aria-expanded" to "true"
|
|
255
|
+
this.input.setAttribute("aria-expanded", true);
|
|
256
|
+
// Handle user declared onOpenEnd if needed
|
|
257
|
+
if (userOnOpenEnd && typeof userOnOpenEnd === 'function')
|
|
258
|
+
userOnOpenEnd.call(this.dropdown, this.el);
|
|
259
|
+
};
|
|
260
|
+
// Add callback for reseting "expanded" state
|
|
261
|
+
dropdownOptions.onCloseEnd = (el) => {
|
|
262
|
+
// Sets "aria-expanded" to "false"
|
|
263
|
+
this.input.setAttribute("aria-expanded", false);
|
|
264
|
+
// Handle user declared onOpenEnd if needed
|
|
265
|
+
if (userOnCloseEnd && typeof userOnCloseEnd === 'function')
|
|
266
|
+
userOnCloseEnd.call(this.dropdown, this.el);
|
|
267
|
+
};
|
|
268
|
+
// Prevent dropdown from closing too early
|
|
269
|
+
dropdownOptions.closeOnClick = false;
|
|
270
|
+
this.dropdown = M.Dropdown.init(this.input, dropdownOptions);
|
|
271
|
+
}
|
|
272
|
+
// Add initial selections
|
|
273
|
+
this._setSelectedStates();
|
|
274
|
+
}
|
|
275
|
+
_addOptionToValues(realOption, virtualOption) {
|
|
276
|
+
this._values.push({ el: realOption, optionEl: virtualOption });
|
|
277
|
+
}
|
|
278
|
+
_removeDropdown() {
|
|
279
|
+
$(this.wrapper)
|
|
280
|
+
.find('.caret')
|
|
281
|
+
.remove();
|
|
282
|
+
$(this.input).remove();
|
|
283
|
+
$(this.dropdownOptions).remove();
|
|
284
|
+
$(this.wrapper).before(this.$el);
|
|
285
|
+
$(this.wrapper).remove();
|
|
286
|
+
}
|
|
287
|
+
_createAndAppendOptionWithIcon(realOption, type) {
|
|
288
|
+
const li = document.createElement('li');
|
|
289
|
+
li.setAttribute("role", "option");
|
|
290
|
+
if (realOption.disabled){
|
|
291
|
+
li.classList.add('disabled');
|
|
292
|
+
li.setAttribute("aria-disabled", true);
|
|
293
|
+
}
|
|
294
|
+
if (type === 'optgroup-option') li.classList.add(type);
|
|
295
|
+
// Text / Checkbox
|
|
296
|
+
const span = document.createElement('span');
|
|
297
|
+
if (this.isMultiple)
|
|
298
|
+
span.innerHTML = `<label><input type="checkbox"${
|
|
299
|
+
realOption.disabled ? ' disabled="disabled"' : ''
|
|
300
|
+
}><span>${realOption.innerHTML}</span></label>`;
|
|
301
|
+
else span.innerHTML = realOption.innerHTML;
|
|
302
|
+
li.appendChild(span);
|
|
303
|
+
// add Icon
|
|
304
|
+
const iconUrl = realOption.getAttribute('data-icon');
|
|
305
|
+
const classes = realOption.getAttribute('class');
|
|
306
|
+
if (iconUrl) {
|
|
307
|
+
const img = $(`<img alt="" class="${classes}" src="${iconUrl}">`);
|
|
308
|
+
img[0].setAttribute("aria-hidden", true);
|
|
309
|
+
li.prepend(img[0]);
|
|
310
|
+
}
|
|
311
|
+
// Check for multiple type
|
|
312
|
+
$(this.dropdownOptions).append(li);
|
|
313
|
+
return li;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
_selectValue(value) {
|
|
317
|
+
value.el.selected = true;
|
|
318
|
+
value.optionEl.classList.add('selected');
|
|
319
|
+
value.optionEl.setAttribute("aria-selected", true);
|
|
320
|
+
const checkbox = value.optionEl.querySelector('input[type="checkbox"]');
|
|
321
|
+
if (checkbox) checkbox.checked = true;
|
|
322
|
+
}
|
|
323
|
+
_deselectValue(value) {
|
|
324
|
+
value.el.selected = false;
|
|
325
|
+
value.optionEl.classList.remove('selected');
|
|
326
|
+
value.optionEl.setAttribute("aria-selected", false);
|
|
327
|
+
const checkbox = value.optionEl.querySelector('input[type="checkbox"]');
|
|
328
|
+
if (checkbox) checkbox.checked = false;
|
|
329
|
+
}
|
|
330
|
+
_deselectAll() {
|
|
331
|
+
this._values.forEach((value) => {
|
|
332
|
+
this._deselectValue(value);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
_isValueSelected(value) {
|
|
336
|
+
const realValues = this.getSelectedValues();
|
|
337
|
+
return realValues.some((realValue) => realValue === value.el.value);
|
|
338
|
+
}
|
|
339
|
+
_toggleEntryFromArray(value) {
|
|
340
|
+
const isSelected = this._isValueSelected(value);
|
|
341
|
+
if (isSelected) this._deselectValue(value);
|
|
342
|
+
else this._selectValue(value);
|
|
343
|
+
}
|
|
344
|
+
_getSelectedOptions() {
|
|
345
|
+
return Array.prototype.filter.call(this.el.selectedOptions, (realOption) => realOption);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_setValueToInput() {
|
|
349
|
+
const realOptions = this._getSelectedOptions();
|
|
350
|
+
const values = this._values.filter((value) => realOptions.indexOf(value.el) >= 0);
|
|
351
|
+
const texts = values.map((value) => value.optionEl.querySelector('span').innerText.trim());
|
|
352
|
+
// Set input-text to first Option with empty value which indicates a description like "choose your option"
|
|
353
|
+
if (texts.length === 0) {
|
|
354
|
+
const firstDisabledOption = this.$el.find('option:disabled').eq(0);
|
|
355
|
+
if (firstDisabledOption.length > 0 && firstDisabledOption[0].value === '') {
|
|
356
|
+
this.input.value = firstDisabledOption.text();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
this.input.value = texts.join(', ');
|
|
361
|
+
}
|
|
362
|
+
_setSelectedStates() {
|
|
363
|
+
this._values.forEach((value) => {
|
|
364
|
+
const optionIsSelected = $(value.el).prop('selected');
|
|
365
|
+
$(value.optionEl)
|
|
366
|
+
.find('input[type="checkbox"]')
|
|
367
|
+
.prop('checked', optionIsSelected);
|
|
368
|
+
if (optionIsSelected) {
|
|
369
|
+
this._activateOption($(this.dropdownOptions), $(value.optionEl));
|
|
370
|
+
} else {
|
|
371
|
+
$(value.optionEl).removeClass('selected');
|
|
372
|
+
$(value.optionEl).attr("aria-selected", false);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
_activateOption(ul, li) {
|
|
377
|
+
if (!li) return;
|
|
378
|
+
if (!this.isMultiple) ul.find('li.selected').removeClass('selected');
|
|
379
|
+
$(li).addClass('selected');
|
|
380
|
+
$(li).attr("aria-selected", true);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
getSelectedValues() {
|
|
384
|
+
return this._getSelectedOptions().map((realOption) => realOption.value);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
M.FormSelect = FormSelect;
|
|
389
|
+
|
|
390
|
+
if (M.jQueryLoaded) M.initializeJqueryWrapper(FormSelect, 'formSelect', 'M_FormSelect');
|
|
391
|
+
})(cash);
|