433 lines
12 KiB
JavaScript
433 lines
12 KiB
JavaScript
|
(function($) {
|
||
|
'use strict';
|
||
|
|
||
|
let _defaults = {
|
||
|
classes: '',
|
||
|
dropdownOptions: {}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @class
|
||
|
*
|
||
|
*/
|
||
|
class FormSelect extends Component {
|
||
|
/**
|
||
|
* Construct FormSelect instance
|
||
|
* @constructor
|
||
|
* @param {Element} el
|
||
|
* @param {Object} options
|
||
|
*/
|
||
|
constructor(el, options) {
|
||
|
super(FormSelect, el, options);
|
||
|
|
||
|
// Don't init if browser default version
|
||
|
if (this.$el.hasClass('browser-default')) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this.el.M_FormSelect = this;
|
||
|
|
||
|
/**
|
||
|
* Options for the select
|
||
|
* @member FormSelect#options
|
||
|
*/
|
||
|
this.options = $.extend({}, FormSelect.defaults, options);
|
||
|
|
||
|
this.isMultiple = this.$el.prop('multiple');
|
||
|
|
||
|
// Setup
|
||
|
this.el.tabIndex = -1;
|
||
|
this._keysSelected = {};
|
||
|
this._valueDict = {}; // Maps key to original and generated option element.
|
||
|
this._setupDropdown();
|
||
|
|
||
|
this._setupEventHandlers();
|
||
|
}
|
||
|
|
||
|
static get defaults() {
|
||
|
return _defaults;
|
||
|
}
|
||
|
|
||
|
static init(els, options) {
|
||
|
return super.init(this, els, options);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get Instance
|
||
|
*/
|
||
|
static getInstance(el) {
|
||
|
let domElem = !!el.jquery ? el[0] : el;
|
||
|
return domElem.M_FormSelect;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Teardown component
|
||
|
*/
|
||
|
destroy() {
|
||
|
this._removeEventHandlers();
|
||
|
this._removeDropdown();
|
||
|
this.el.M_FormSelect = undefined;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Setup Event Handlers
|
||
|
*/
|
||
|
_setupEventHandlers() {
|
||
|
this._handleSelectChangeBound = this._handleSelectChange.bind(this);
|
||
|
this._handleOptionClickBound = this._handleOptionClick.bind(this);
|
||
|
this._handleInputClickBound = this._handleInputClick.bind(this);
|
||
|
|
||
|
$(this.dropdownOptions)
|
||
|
.find('li:not(.optgroup)')
|
||
|
.each((el) => {
|
||
|
el.addEventListener('click', this._handleOptionClickBound);
|
||
|
});
|
||
|
this.el.addEventListener('change', this._handleSelectChangeBound);
|
||
|
this.input.addEventListener('click', this._handleInputClickBound);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove Event Handlers
|
||
|
*/
|
||
|
_removeEventHandlers() {
|
||
|
$(this.dropdownOptions)
|
||
|
.find('li:not(.optgroup)')
|
||
|
.each((el) => {
|
||
|
el.removeEventListener('click', this._handleOptionClickBound);
|
||
|
});
|
||
|
this.el.removeEventListener('change', this._handleSelectChangeBound);
|
||
|
this.input.removeEventListener('click', this._handleInputClickBound);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Select Change
|
||
|
* @param {Event} e
|
||
|
*/
|
||
|
_handleSelectChange(e) {
|
||
|
this._setValueToInput();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Option Click
|
||
|
* @param {Event} e
|
||
|
*/
|
||
|
_handleOptionClick(e) {
|
||
|
e.preventDefault();
|
||
|
let option = $(e.target).closest('li')[0];
|
||
|
let key = option.id;
|
||
|
if (!$(option).hasClass('disabled') && !$(option).hasClass('optgroup') && key.length) {
|
||
|
let selected = true;
|
||
|
|
||
|
if (this.isMultiple) {
|
||
|
// Deselect placeholder option if still selected.
|
||
|
let placeholderOption = $(this.dropdownOptions).find('li.disabled.selected');
|
||
|
if (placeholderOption.length) {
|
||
|
placeholderOption.removeClass('selected');
|
||
|
placeholderOption.find('input[type="checkbox"]').prop('checked', false);
|
||
|
this._toggleEntryFromArray(placeholderOption[0].id);
|
||
|
}
|
||
|
selected = this._toggleEntryFromArray(key);
|
||
|
} else {
|
||
|
$(this.dropdownOptions)
|
||
|
.find('li')
|
||
|
.removeClass('selected');
|
||
|
$(option).toggleClass('selected', selected);
|
||
|
}
|
||
|
|
||
|
// Set selected on original select option
|
||
|
// Only trigger if selected state changed
|
||
|
let prevSelected = $(this._valueDict[key].el).prop('selected');
|
||
|
if (prevSelected !== selected) {
|
||
|
$(this._valueDict[key].el).prop('selected', selected);
|
||
|
this.$el.trigger('change');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
e.stopPropagation();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handle Input Click
|
||
|
*/
|
||
|
_handleInputClick() {
|
||
|
if (this.dropdown && this.dropdown.isOpen) {
|
||
|
this._setValueToInput();
|
||
|
this._setSelectedStates();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Setup dropdown
|
||
|
*/
|
||
|
_setupDropdown() {
|
||
|
this.wrapper = document.createElement('div');
|
||
|
$(this.wrapper).addClass('select-wrapper ' + this.options.classes);
|
||
|
this.$el.before($(this.wrapper));
|
||
|
this.wrapper.appendChild(this.el);
|
||
|
|
||
|
if (this.el.disabled) {
|
||
|
this.wrapper.classList.add('disabled');
|
||
|
}
|
||
|
|
||
|
// Create dropdown
|
||
|
this.$selectOptions = this.$el.children('option, optgroup');
|
||
|
this.dropdownOptions = document.createElement('ul');
|
||
|
this.dropdownOptions.id = `select-options-${M.guid()}`;
|
||
|
$(this.dropdownOptions).addClass(
|
||
|
'dropdown-content select-dropdown ' + (this.isMultiple ? 'multiple-select-dropdown' : '')
|
||
|
);
|
||
|
|
||
|
// Create dropdown structure.
|
||
|
if (this.$selectOptions.length) {
|
||
|
this.$selectOptions.each((el) => {
|
||
|
if ($(el).is('option')) {
|
||
|
// Direct descendant option.
|
||
|
let optionEl;
|
||
|
if (this.isMultiple) {
|
||
|
optionEl = this._appendOptionWithIcon(this.$el, el, 'multiple');
|
||
|
} else {
|
||
|
optionEl = this._appendOptionWithIcon(this.$el, el);
|
||
|
}
|
||
|
|
||
|
this._addOptionToValueDict(el, optionEl);
|
||
|
} else if ($(el).is('optgroup')) {
|
||
|
// Optgroup.
|
||
|
let selectOptions = $(el).children('option');
|
||
|
$(this.dropdownOptions).append(
|
||
|
$('<li class="optgroup"><span>' + el.getAttribute('label') + '</span></li>')[0]
|
||
|
);
|
||
|
|
||
|
selectOptions.each((el) => {
|
||
|
let optionEl = this._appendOptionWithIcon(this.$el, el, 'optgroup-option');
|
||
|
this._addOptionToValueDict(el, optionEl);
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
this.$el.after(this.dropdownOptions);
|
||
|
|
||
|
// Add input dropdown
|
||
|
this.input = document.createElement('input');
|
||
|
$(this.input).addClass('select-dropdown dropdown-trigger');
|
||
|
this.input.setAttribute('type', 'text');
|
||
|
this.input.setAttribute('readonly', 'true');
|
||
|
this.input.setAttribute('data-target', this.dropdownOptions.id);
|
||
|
if (this.el.disabled) {
|
||
|
$(this.input).prop('disabled', 'true');
|
||
|
}
|
||
|
|
||
|
this.$el.before(this.input);
|
||
|
this._setValueToInput();
|
||
|
|
||
|
// Add caret
|
||
|
let dropdownIcon = $(
|
||
|
'<svg class="caret" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/><path d="M0 0h24v24H0z" fill="none"/></svg>'
|
||
|
);
|
||
|
this.$el.before(dropdownIcon[0]);
|
||
|
|
||
|
// Initialize dropdown
|
||
|
if (!this.el.disabled) {
|
||
|
let dropdownOptions = $.extend({}, this.options.dropdownOptions);
|
||
|
|
||
|
// Add callback for centering selected option when dropdown content is scrollable
|
||
|
dropdownOptions.onOpenEnd = (el) => {
|
||
|
let selectedOption = $(this.dropdownOptions)
|
||
|
.find('.selected')
|
||
|
.first();
|
||
|
|
||
|
if (selectedOption.length) {
|
||
|
// Focus selected option in dropdown
|
||
|
M.keyDown = true;
|
||
|
this.dropdown.focusedIndex = selectedOption.index();
|
||
|
this.dropdown._focusFocusedItem();
|
||
|
M.keyDown = false;
|
||
|
|
||
|
// Handle scrolling to selected option
|
||
|
if (this.dropdown.isScrollable) {
|
||
|
let scrollOffset =
|
||
|
selectedOption[0].getBoundingClientRect().top -
|
||
|
this.dropdownOptions.getBoundingClientRect().top; // scroll to selected option
|
||
|
scrollOffset -= this.dropdownOptions.clientHeight / 2; // center in dropdown
|
||
|
this.dropdownOptions.scrollTop = scrollOffset;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
if (this.isMultiple) {
|
||
|
dropdownOptions.closeOnClick = false;
|
||
|
}
|
||
|
this.dropdown = M.Dropdown.init(this.input, dropdownOptions);
|
||
|
}
|
||
|
|
||
|
// Add initial selections
|
||
|
this._setSelectedStates();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add option to value dict
|
||
|
* @param {Element} el original option element
|
||
|
* @param {Element} optionEl generated option element
|
||
|
*/
|
||
|
_addOptionToValueDict(el, optionEl) {
|
||
|
let index = Object.keys(this._valueDict).length;
|
||
|
let key = this.dropdownOptions.id + index;
|
||
|
let obj = {};
|
||
|
optionEl.id = key;
|
||
|
|
||
|
obj.el = el;
|
||
|
obj.optionEl = optionEl;
|
||
|
this._valueDict[key] = obj;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Remove dropdown
|
||
|
*/
|
||
|
_removeDropdown() {
|
||
|
$(this.wrapper)
|
||
|
.find('.caret')
|
||
|
.remove();
|
||
|
$(this.input).remove();
|
||
|
$(this.dropdownOptions).remove();
|
||
|
$(this.wrapper).before(this.$el);
|
||
|
$(this.wrapper).remove();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Setup dropdown
|
||
|
* @param {Element} select select element
|
||
|
* @param {Element} option option element from select
|
||
|
* @param {String} type
|
||
|
* @return {Element} option element added
|
||
|
*/
|
||
|
_appendOptionWithIcon(select, option, type) {
|
||
|
// Add disabled attr if disabled
|
||
|
let disabledClass = option.disabled ? 'disabled ' : '';
|
||
|
let optgroupClass = type === 'optgroup-option' ? 'optgroup-option ' : '';
|
||
|
let multipleCheckbox = this.isMultiple
|
||
|
? `<label><input type="checkbox"${disabledClass}"/><span>${option.innerHTML}</span></label>`
|
||
|
: option.innerHTML;
|
||
|
let liEl = $('<li></li>');
|
||
|
let spanEl = $('<span></span>');
|
||
|
spanEl.html(multipleCheckbox);
|
||
|
liEl.addClass(`${disabledClass} ${optgroupClass}`);
|
||
|
liEl.append(spanEl);
|
||
|
|
||
|
// add icons
|
||
|
let iconUrl = option.getAttribute('data-icon');
|
||
|
if (!!iconUrl) {
|
||
|
let imgEl = $(`<img alt="" src="${iconUrl}">`);
|
||
|
liEl.prepend(imgEl);
|
||
|
}
|
||
|
|
||
|
// Check for multiple type.
|
||
|
$(this.dropdownOptions).append(liEl[0]);
|
||
|
return liEl[0];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Toggle entry from option
|
||
|
* @param {String} key Option key
|
||
|
* @return {Boolean} if entry was added or removed
|
||
|
*/
|
||
|
_toggleEntryFromArray(key) {
|
||
|
let notAdded = !this._keysSelected.hasOwnProperty(key);
|
||
|
let $optionLi = $(this._valueDict[key].optionEl);
|
||
|
|
||
|
if (notAdded) {
|
||
|
this._keysSelected[key] = true;
|
||
|
} else {
|
||
|
delete this._keysSelected[key];
|
||
|
}
|
||
|
|
||
|
$optionLi.toggleClass('selected', notAdded);
|
||
|
|
||
|
// Set checkbox checked value
|
||
|
$optionLi.find('input[type="checkbox"]').prop('checked', notAdded);
|
||
|
|
||
|
// use notAdded instead of true (to detect if the option is selected or not)
|
||
|
$optionLi.prop('selected', notAdded);
|
||
|
|
||
|
return notAdded;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set text value to input
|
||
|
*/
|
||
|
_setValueToInput() {
|
||
|
let values = [];
|
||
|
let options = this.$el.find('option');
|
||
|
|
||
|
options.each((el) => {
|
||
|
if ($(el).prop('selected')) {
|
||
|
let text = $(el).text();
|
||
|
values.push(text);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (!values.length) {
|
||
|
let firstDisabled = this.$el.find('option:disabled').eq(0);
|
||
|
if (firstDisabled.length && firstDisabled[0].value === '') {
|
||
|
values.push(firstDisabled.text());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.input.value = values.join(', ');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set selected state of dropdown to match actual select element
|
||
|
*/
|
||
|
_setSelectedStates() {
|
||
|
this._keysSelected = {};
|
||
|
|
||
|
for (let key in this._valueDict) {
|
||
|
let option = this._valueDict[key];
|
||
|
let optionIsSelected = $(option.el).prop('selected');
|
||
|
$(option.optionEl)
|
||
|
.find('input[type="checkbox"]')
|
||
|
.prop('checked', optionIsSelected);
|
||
|
if (optionIsSelected) {
|
||
|
this._activateOption($(this.dropdownOptions), $(option.optionEl));
|
||
|
this._keysSelected[key] = true;
|
||
|
} else {
|
||
|
$(option.optionEl).removeClass('selected');
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Make option as selected and scroll to selected position
|
||
|
* @param {jQuery} collection Select options jQuery element
|
||
|
* @param {Element} newOption element of the new option
|
||
|
*/
|
||
|
_activateOption(collection, newOption) {
|
||
|
if (newOption) {
|
||
|
if (!this.isMultiple) {
|
||
|
collection.find('li.selected').removeClass('selected');
|
||
|
}
|
||
|
let option = $(newOption);
|
||
|
option.addClass('selected');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get Selected Values
|
||
|
* @return {Array} Array of selected values
|
||
|
*/
|
||
|
getSelectedValues() {
|
||
|
let selectedValues = [];
|
||
|
for (let key in this._keysSelected) {
|
||
|
selectedValues.push(this._valueDict[key].el.value);
|
||
|
}
|
||
|
return selectedValues;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
M.FormSelect = FormSelect;
|
||
|
|
||
|
if (M.jQueryLoaded) {
|
||
|
M.initializeJqueryWrapper(FormSelect, 'formSelect', 'M_FormSelect');
|
||
|
}
|
||
|
})(cash);
|