diff --git a/forms.html b/forms.html index d8daf7a534..52ab1caeec 100644 --- a/forms.html +++ b/forms.html @@ -413,7 +413,20 @@

Select

- + + +
+ +

@@ -474,6 +487,20 @@

Select

<label>Materialize Multiple Select</label> </div> + <div class="input-field col s12"> + <select> + <optgroup label="team 1"> + <option value="1">Option 1</option> + <option value="2">Option 2</option> + </optgroup> + <optgroup label="team 2"> + <option value="3">Option 3</option> + <option value="4">Option 4</option> + </optgroup> + </select> + <label>Materialize Select with Optgroups</label> + </div> + <div class="input-field col s12"> <select multiple> <optgroup label="team 1"> @@ -485,7 +512,7 @@

Select

<option value="4">Option 4</option> </optgroup> </select> - <label>Optgroups</label> + <label>Materialize Multiple Select with Optgroups</label> </div> <div class="input-field col s12 m6"> @@ -570,8 +597,31 @@

Initialization

-

Updating/Destroying Select

-

If you want to update the items inside the select, just rerun the initialization code from above after editing the original select. Or you can destroy the material select with this function below, and create a new select altogether

+

Initialization with options

+

You can initialize the select element with some options.

+

+  $(document).ready(function() {
+    $('select').material_select({
+      'hover': false,
+      'belowOrigin': true,
+      'maxWidth': 500, // width 500px
+      'maxHeight': 200, // height 200px
+      'arrangement': 'top' // Open the dropdown to top
+    });
+  });
+            
+
+
+

Updating Select

+

You can update the material select by passing new values with this function below, in this case you don't need to use the destroying select function.

+

+  $('select').val([value1, value2]).trigger('update'); // for multiple select
+  $('select').val(value).trigger('update'); // for single select
+            
+
+
+

Destroying Select

+

Or you can destroy the material select with this function below, and create a new select altogether


   $('select').material_select('destroy');
             
@@ -579,6 +629,39 @@

Updating/Destroying Select

+
+

Options

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Option nameDescription
hoverIf true, the dropdown will open on hover. Default: false. Available: true. Typeof: boolean
belowOriginIf true, the dropdown will show below the activator. Default: false. Available: true. Typeof: boolean
maxHeightDefines the max-height CSS property of the dropdown Default: null. Typeof: number (100)
maxWidthDefines the max-width CSS property of the dropdown. Default: null. Typeof: number (100)
arrangementDefines the direction of the opening menu (dropdown). Default: 'bottom'. Available: 'bottom'. Typeof: string
+

Radio Buttons

diff --git a/js/dropdown.js b/js/dropdown.js index a5fc1e9be4..ab14a16590 100644 --- a/js/dropdown.js +++ b/js/dropdown.js @@ -7,7 +7,7 @@ return this; }; - $.fn.dropdown = function (option) { + $.fn.dropdown = function(option) { var defaults = { inDuration: 300, outDuration: 225, @@ -15,219 +15,249 @@ hover: false, gutter: 0, // Spacing from edge belowOrigin: false, - alignment: 'left' + alignment: 'left', + maxWidth: null, + maxHeight: null, + arrangement: 'bottom' }; - this.each(function(){ - var origin = $(this); - var options = $.extend({}, defaults, option); - var isFocused = false; - - // Dropdown menu - var activates = $("#"+ origin.attr('data-activates')); - - function updateOptions() { - if (origin.data('induration') !== undefined) - options.inDuration = origin.data('inDuration'); - if (origin.data('outduration') !== undefined) - options.outDuration = origin.data('outDuration'); - if (origin.data('constrainwidth') !== undefined) - options.constrain_width = origin.data('constrainwidth'); - if (origin.data('hover') !== undefined) - options.hover = origin.data('hover'); - if (origin.data('gutter') !== undefined) - options.gutter = origin.data('gutter'); - if (origin.data('beloworigin') !== undefined) - options.belowOrigin = origin.data('beloworigin'); - if (origin.data('alignment') !== undefined) - options.alignment = origin.data('alignment'); - } - - updateOptions(); - - // Attach dropdown to its activator - origin.after(activates); - - /* - Helper function to position and resize dropdown. - Used in hover and click handler. - */ - function placeDropdown(eventType) { - // Check for simultaneous focus and click events. - if (eventType === 'focus') { - isFocused = true; + this.each(function() { + var origin = $(this); + var options = $.extend({}, defaults, option); + var isFocused = false; + + // Dropdown menu + var activates = $("#" + origin.attr('data-activates')); + + function updateOptions() { + if (origin.data('induration') !== undefined) + options.inDuration = origin.data('inDuration'); + if (origin.data('outduration') !== undefined) + options.outDuration = origin.data('outDuration'); + if (origin.data('constrainwidth') !== undefined) + options.constrain_width = origin.data('constrainwidth'); + if (origin.data('hover') !== undefined) + options.hover = origin.data('hover'); + if (origin.data('gutter') !== undefined) + options.gutter = origin.data('gutter'); + if (origin.data('beloworigin') !== undefined) + options.belowOrigin = origin.data('beloworigin'); + if (origin.data('alignment') !== undefined) + options.alignment = origin.data('alignment'); + if (origin.data('maxWidth') !== undefined) + options.maxWidth = origin.data('maxWidth'); + if (origin.data('maxHeight') !== undefined) + options.maxHeight = origin.data('maxHeight'); + if (origin.data('arrangement') !== undefined) + options.arrangement = origin.data('arrangement'); } - // Check html data attributes updateOptions(); - // Set Dropdown state - activates.addClass('active'); - origin.addClass('active'); + // Attach dropdown to its activator + origin.after(activates); - // Constrain width - if (options.constrain_width === true) { - activates.css('width', origin.outerWidth()); + /* + Helper function to position and resize dropdown. + Used in hover and click handler. + */ + function placeDropdown(eventType) { + // Check for simultaneous focus and click events. + if (eventType === 'focus') { + isFocused = true; + } - } else { - activates.css('white-space', 'nowrap'); - } + // Check html data attributes + updateOptions(); - // Offscreen detection - var windowHeight = window.innerHeight; - var originHeight = origin.innerHeight(); - var offsetLeft = origin.offset().left; - var offsetTop = origin.offset().top - $(window).scrollTop(); - var currAlignment = options.alignment; - var activatesLeft, gutterSpacing; - - // Below Origin - var verticalOffset = 0; - if (options.belowOrigin === true) { - verticalOffset = originHeight; - } + // Set Dropdown state + activates.addClass('active'); + origin.addClass('active'); - // Check for scrolling positioned container. - var scrollOffset = 0; - var wrapper = origin.parent(); - if (!wrapper.is('body') && wrapper[0].scrollHeight > wrapper[0].clientHeight) { - scrollOffset = wrapper[0].scrollTop; - } + // Constrain width + if (options.constrain_width) { + activates.css('width', origin.outerWidth()); + } else { + activates.css('white-space', 'nowrap'); + } - if (offsetLeft + activates.innerWidth() > $(window).width()) { - // Dropdown goes past screen on right, force right alignment - currAlignment = 'right'; + if (options.maxWidth) { + activates.css('max-width', options.maxWidth); + } - } else if (offsetLeft - activates.innerWidth() + origin.innerWidth() < 0) { - // Dropdown goes past screen on left, force left alignment - currAlignment = 'left'; - } - // Vertical bottom offscreen detection - if (offsetTop + activates.innerHeight() > windowHeight) { - // If going upwards still goes offscreen, just crop height of dropdown. - if (offsetTop + originHeight - activates.innerHeight() < 0) { - var adjustedHeight = windowHeight - offsetTop - verticalOffset; - activates.css('max-height', adjustedHeight); - } else { - // Flow upwards. - if (!verticalOffset) { - verticalOffset += originHeight; - } - verticalOffset -= activates.innerHeight(); + if (options.maxHeight) { + activates.css('max-height', options.maxHeight); } - } - // Handle edge alignment - if (currAlignment === 'left') { - gutterSpacing = options.gutter; - leftPosition = origin.position().left + gutterSpacing; - } - else if (currAlignment === 'right') { - var offsetRight = origin.position().left + origin.outerWidth() - activates.outerWidth(); - gutterSpacing = -options.gutter; - leftPosition = offsetRight + gutterSpacing; - } + // Offscreen detection + var windowHeight = window.innerHeight; + var originHeight = origin.innerHeight(); + var offsetLeft = origin.offset().left; + var offsetTop = origin.offset().top - $(window).scrollTop(); + var currAlignment = options.alignment; + var gutterSpacing; - // Position dropdown - activates.css({ - position: 'absolute', - top: origin.position().top + verticalOffset + scrollOffset, - left: leftPosition - }); + // Below Origin + var verticalOffset = 0; + if (options.belowOrigin) { + verticalOffset = originHeight; + } + if (offsetLeft + activates.innerWidth() > $(window).width()) { + // Dropdown goes past screen on right, force right alignment + currAlignment = 'right'; - // Show dropdown - activates.stop(true, true).css('opacity', 0) - .slideDown({ - queue: false, - duration: options.inDuration, - easing: 'easeOutCubic', - complete: function() { - $(this).css('height', ''); + } else if (offsetLeft - activates.innerWidth() + origin.innerWidth() < 0) { + // Dropdown goes past screen on left, force left alignment + currAlignment = 'left'; } - }) - .animate( {opacity: 1}, {queue: false, duration: options.inDuration, easing: 'easeOutSine'}); - } - - function hideDropdown() { - // Check for simultaneous focus and click events. - isFocused = false; - activates.fadeOut(options.outDuration); - activates.removeClass('active'); - origin.removeClass('active'); - setTimeout(function() { activates.css('max-height', ''); }, options.outDuration); - } - - // Hover - if (options.hover) { - var open = false; - origin.unbind('click.' + origin.attr('id')); - // Hover handler to show dropdown - origin.on('mouseenter', function(e){ // Mouse over - if (open === false) { - placeDropdown(); - open = true; + + // Vertical bottom offscreen detection + if (offsetTop + activates.innerHeight() > windowHeight) { + // If going upwards still goes offscreen, just crop height of dropdown. + if (offsetTop + originHeight - activates.innerHeight() < 0) { + var adjustedHeight = windowHeight - offsetTop - verticalOffset; + activates.css('max-height', adjustedHeight); + } else { + // Flow upwards. + if (!verticalOffset) { + verticalOffset += originHeight; + } + verticalOffset -= activates.innerHeight(); + } } - }); - origin.on('mouseleave', function(e){ - // If hover on origin then to something other than dropdown content, then close - var toEl = e.toElement || e.relatedTarget; // added browser compatibility for target element - if(!$(toEl).closest('.dropdown-content').is(activates)) { - activates.stop(true, true); - hideDropdown(); - open = false; + + if (options.arrangement === 'top') { + if (options.maxHeight) { + if (activates.height() < options.maxHeight) { + verticalOffset = -activates.height() + originHeight; + } else { + verticalOffset = -options.maxHeight + originHeight; + } + } else { + verticalOffset = -activates.height() + originHeight; + } } - }); - activates.on('mouseleave', function(e){ // Mouse out - var toEl = e.toElement || e.relatedTarget; - if(!$(toEl).closest('.dropdown-button').is(origin)) { - activates.stop(true, true); - hideDropdown(); - open = false; + // Handle edge alignment + if (currAlignment === 'left') { + gutterSpacing = options.gutter; + leftPosition = origin.position().left + gutterSpacing; + } else if (currAlignment === 'right') { + var offsetRight = origin.position().left + origin.outerWidth() - activates.outerWidth(); + gutterSpacing = -options.gutter; + leftPosition = offsetRight + gutterSpacing; } - }); - // Click - } else { - // Click handler to show dropdown - origin.unbind('click.' + origin.attr('id')); - origin.bind('click.'+origin.attr('id'), function(e){ - if (!isFocused) { - if ( origin[0] == e.currentTarget && - !origin.hasClass('active') && - ($(e.target).closest('.dropdown-content').length === 0)) { - e.preventDefault(); // Prevents button click from moving window - placeDropdown('click'); + // Position dropdown + activates.css({ + position: 'absolute', + top: origin.position().top + verticalOffset, + left: leftPosition + }); + + // Show dropdown + activates.stop(true, true).css('opacity', 0) + .slideDown({ + queue: false, + duration: options.inDuration, + easing: 'easeOutCubic', + complete: function() { + $(this).css('height', ''); + } + }) + .animate({ + opacity: 1 + }, { + queue: false, + duration: options.inDuration, + easing: 'easeOutSine' + }); + } + + function hideDropdown() { + // Check for simultaneous focus and click events. + isFocused = false; + activates.fadeOut(options.outDuration); + activates.removeClass('active'); + origin.removeClass('active'); + + if (!options.maxHeight) { + setTimeout(function() { + activates.css('max-height', ''); + }, options.outDuration); + } + } + + // Hover + if (options.hover) { + var open = false; + origin.unbind('click.' + origin.attr('id')); + // Hover handler to show dropdown + origin.on('mouseenter', function(e) { // Mouse over + if (open === false) { + placeDropdown(); + open = true; } - // If origin is clicked and menu is open, close menu - else if (origin.hasClass('active')) { + }); + origin.on('mouseleave', function(e) { + // If hover on origin then to something other than dropdown content, then close + var toEl = e.toElement || e.relatedTarget; // added browser compatibility for target element + if (!$(toEl).closest('.dropdown-content').is(activates)) { + activates.stop(true, true); hideDropdown(); - $(document).unbind('click.'+ activates.attr('id') + ' touchstart.' + activates.attr('id')); + open = false; } - // If menu open, add click close handler to document - if (activates.hasClass('active')) { - $(document).bind('click.'+ activates.attr('id') + ' touchstart.' + activates.attr('id'), function (e) { - if (!activates.is(e.target) && !origin.is(e.target) && (!origin.find(e.target).length) ) { - hideDropdown(); - $(document).unbind('click.'+ activates.attr('id') + ' touchstart.' + activates.attr('id')); - } - }); + }); + + activates.on('mouseleave', function(e) { // Mouse out + var toEl = e.toElement || e.relatedTarget; + if (!$(toEl).closest('.dropdown-button').is(origin)) { + activates.stop(true, true); + hideDropdown(); + open = false; } - } - }); + }); - } // End else + // Click + } else { + // Click handler to show dropdown + origin.unbind('click.' + origin.attr('id')); + origin.bind('click.' + origin.attr('id'), function(e) { + if (!isFocused) { + if (origin[0] == e.currentTarget && + !origin.hasClass('active') && + ($(e.target).closest('.dropdown-content').length === 0)) { + e.preventDefault(); // Prevents button click from moving window + placeDropdown('click'); + } + // If origin is clicked and menu is open, close menu + else if (origin.hasClass('active')) { + hideDropdown(); + $(document).unbind('click.' + activates.attr('id') + ' touchstart.' + activates.attr('id')); + } + // If menu open, add click close handler to document + if (activates.hasClass('active')) { + $(document).bind('click.' + activates.attr('id') + ' touchstart.' + activates.attr('id'), function(e) { + if (!activates.is(e.target) && !origin.is(e.target) && (!origin.find(e.target).length)) { + hideDropdown(); + $(document).unbind('click.' + activates.attr('id') + ' touchstart.' + activates.attr('id')); + } + }); + } + } + }); - // Listen to open and close event - useful for select component - origin.on('open', function(e, eventType) { - placeDropdown(eventType); - }); - origin.on('close', hideDropdown); + } // End else + // Listen to open and close event - useful for select component + origin.on('open', function(e, eventType) { + placeDropdown(eventType); + }); + origin.on('close', hideDropdown); + }); - }); }; // End dropdown plugin $(document).ready(function(){ diff --git a/js/forms.js b/js/forms.js index 0a073baa74..9a279f8d32 100644 --- a/js/forms.js +++ b/js/forms.js @@ -276,320 +276,701 @@ /******************* * Select Plugin * ******************/ - $.fn.material_select = function (callback) { - $(this).each(function(){ - var $select = $(this); + $.fn.material_select = function(callback) { + // Return true if the element passed as parameter matches to the tag name + function isTagName(tagName, element) { + return tagName === element.tagName; + } - if ($select.hasClass('browser-default')) { - return; // Continue to next (return false breaks out of entire loop) - } + // Fill the input with all values from selected options + function fillContentInput(selectedOptions, _this) { + var value = []; - var multiple = $select.attr('multiple') ? true : false, - lastID = $select.data('select-id'); // Tear down structure if Select needs to be rebuilt + // Push all the text values in value + for (var i = 0, countOptions = selectedOptions.length; i < countOptions; i++) { + value.push(selectedOptions[i].textContent); + } - if (lastID) { - $select.parent().find('span.caret').remove(); - $select.parent().find('input').remove(); + // If no selected, get the text from the first disabled + if (!value.length) { + // Prevent error if there is no disabled option + var disabledElement = _this.elements.dropdown.querySelector('li.disabled'); - $select.unwrap(); - $('ul#select-options-'+lastID).remove(); + if (disabledElement) { + value.push(disabledElement.textContent); + } } - // If destroying the select, remove the selelct-id and reset it to it's uninitialized state. - if(callback === 'destroy') { - $select.data('select-id', null).removeClass('initialized'); - return; - } + _this.elements.input.value = value.join(', '); + } - var uniqueID = Materialize.guid(); - $select.data('select-id', uniqueID); - var wrapper = $('
'); - wrapper.addClass($select.attr('class')); - var options = $(''), - selectChildren = $select.children('option, optgroup'), - valuesSelected = [], - optionsHover = false; - - var label = $select.find('option:selected').html() || $select.find('option:first').html() || ""; - - // Function that renders and appends the option taking into - // account type and possible image icon. - var appendOptionWithIcon = function(select, option, type) { - // Add disabled attr if disabled - var disabledClass = (option.is(':disabled')) ? 'disabled ' : ''; - - // add icons - var icon_url = option.data('icon'); - var classes = option.attr('class'); - if (!!icon_url) { - var classString = ''; - if (!!classes) classString = ' class="' + classes + '"'; - - // Check for multiple type. - if (type === 'multiple') { - options.append($('
  • ' + option.html() + '
  • ')); - } else { - options.append($('
  • ' + option.html() + '
  • ')); - } - return true; - } + // Get all elements from a specific optgroup + function getOptgroupElements(optgroupLabel, _this) { + var elements = _this.elements.select.querySelectorAll('optgroup[label="' + optgroupLabel + '"] option'); - // Check for multiple type. - if (type === 'multiple') { - options.append($('
  • ' + option.html() + '
  • ')); - } else { - options.append($('
  • ' + option.html() + '
  • ')); - } - }; + return elements; + } - /* Create dropdown structure. */ - if (selectChildren.length) { - selectChildren.each(function() { - if ($(this).is('option')) { - // Direct descendant option. - if (multiple) { - appendOptionWithIcon($select, $(this), 'multiple'); + // Destroy a select + function destroySelect(select) { + var defaultSelectParent = select.parentNode.parentNode, + selectParent = select.parentNode; + + // Reset the select + select.dataset.id = null; + select.classList.remove('initialized'); + // Reorder the select in the DOM before material_select changes + defaultSelectParent.appendChild(select); + defaultSelectParent.removeChild(selectParent); + defaultSelectParent.insertBefore(select, defaultSelectParent.querySelector('label')); + } - } else { - appendOptionWithIcon($select, $(this)); - } - } else if ($(this).is('optgroup')) { - // Optgroup. - var selectOptions = $(this).children('option'); - options.append($('
  • ' + $(this).attr('label') + '
  • ')); - - selectOptions.each(function() { - appendOptionWithIcon($select, $(this)); - }); - } + // Update the select with new values + function updateSelect(_this) { + // Reset the select + resetSelect(_this); + + // Fill the dropdown with new values + if (_this.prop.isSelectedOption) { + var selected = _this.prop.selected(); + + Array.prototype.forEach.call(selected, function(option) { + // True is specify for performing on load + toggleSelectElement(option, _this, true); }); } + } - options.find('li:not(.optgroup)').each(function (i) { - $(this).click(function (e) { - // Check if option element is disabled - if (!$(this).hasClass('disabled') && !$(this).hasClass('optgroup')) { - var selected = true; + // Reset the select with no selected status + function resetSelect(_this) { + if (_this.prop.multiple) { + Array.prototype.forEach.call(_this.elements.checkboxes, function(element) { + element.checked = false; + }); + } else { + Array.prototype.forEach.call(_this.elements.radio, function(element) { + element.checked = false; + }); + } - if (multiple) { - $('input[type="checkbox"]', this).prop('checked', function(i, v) { return !v; }); - selected = toggleEntryFromArray(valuesSelected, $(this).index(), $select); - $newSelect.trigger('focus'); - } else { - options.find('li').removeClass('active'); - $(this).toggleClass('active'); - $newSelect.val($(this).text()); - } + Array.prototype.forEach.call(_this.elements.li, function(element) { + element.classList.remove('active'); + }); + } - activateOption(options, $(this)); - $select.find('option').eq(i).prop('selected', selected); - // Trigger onchange() event - $select.trigger('change'); - if (typeof callback !== 'undefined') callback(); - } + // Make option as active or selected and scroll to its position + function activateOption(collection, newOption) { + if (newOption) { + // Remove selected class from previous selected element + var selected = collection.querySelector('li.selected'); - e.stopPropagation(); - }); - }); + if (selected) { + selected.classList.remove('selected'); + } - // Wrap Elements - $select.wrap(wrapper); - // Add Select Display Element - var dropdownIcon = $(''); - if ($select.is(':disabled')) - dropdownIcon.addClass('disabled'); + newOption.classList.add('selected'); + $(collection).scrollTo(newOption); + } + } - // escape double quotes - var sanitizedLabelHtml = label.replace(/"/g, '"'); + /* Create a new select with his own data + * + * Select Constructor + * + */ + function CreateSelect(select, options) { + var _this = {}; + + // Reference for class and optgroup and multiple attributes + _this.prop = (function() { + var self = {}; + self.id = Materialize.guid(); + self.className = select.className; + self.optgroup = select.querySelector('optgroup') ? true : false; + self.multiple = select.getAttribute('multiple') !== null ? true : false; + self.hover = false; + self.elements = select.querySelectorAll('*'); + self.isSelectedOption = function() { + var selectedOption = select.options[_this.elements.select.selectedIndex] ? true : false; + + return selectedOption; + }, + self.selected = function() { + var list = [], + option; - var $newSelect = $(''); - $select.before($newSelect); - $newSelect.before(dropdownIcon); + for (var i = 0, countOptions = _this.elements.select.options.length; i < countOptions; i++) { + option = _this.elements.select.options[i]; - $newSelect.after(options); - // Check if section element is disabled - if (!$select.is(':disabled')) { - $newSelect.dropdown({'hover': false, 'closeOnClick': false}); - } + if (option.selected) { + list.push(option); + } + } - // Copy tabindex - if ($select.attr('tabindex')) { - $($newSelect[0]).attr('tabindex', $select.attr('tabindex')); - } + return list; + }; + self.options = {}; + + return self; + })(); + _this.elements = {}; + + // Refer the select + _this.elements.select = select; + _this.elements.select.dataset.id = _this.prop.id; + // Add trigger for change event + $(_this.elements.select).on('update', function() { + updateSelect(_this); + }); - $select.addClass('initialized'); + // Create the caret + _this.elements.caret = document.createElement('span'); + _this.elements.caret.className = 'caret'; + _this.elements.caret.innerHTML = '▼'; + + // Create the dropdown + _this.elements.dropdown = document.createElement('ul'); + _this.elements.dropdown.id = 'select-options-' + _this.prop.id; + // Refer the select id as data-id + if (_this.elements.select.id) { + _this.elements.dropdown.dataset.id = _this.elements.select.id; + } + _this.elements.dropdown.className = 'dropdown-content select-dropdown'; + // Add event + $(_this.elements.dropdown).hover(function() { + _this.prop.hover = true; + }, function() { + _this.prop.hover = false; + }); - $newSelect.on({ - 'focus': function (){ - if ($('ul.select-dropdown').not(options[0]).is(':visible')) { + // Create the wrapper + _this.elements.wrapper = document.createElement('div'); + _this.elements.wrapper.className = _this.prop.className; + _this.elements.wrapper.classList.add('select-wrapper'); + + // Create the input + _this.elements.input = document.createElement('input'); + _this.elements.input.type = 'text'; + _this.elements.input.className = 'select-dropdown'; + _this.elements.input.setAttribute('readonly', true); + _this.elements.input.dataset.activates = 'select-options-' + _this.prop.id; + if (_this.elements.select.dataset.maxlength) { + _this.elements.input.dataset.maxlength = _this.elements.select.dataset.maxlength; + } + // Add events + $(_this.elements.input).on({ + 'focus': function() { + if ($('ul.select-dropdown').not(_this.elements.dropdown).is(':visible')) { $('input.select-dropdown').trigger('close'); } - if (!options.is(':visible')) { - $(this).trigger('open', ['focus']); - var label = $(this).val(); - var selectedOption = options.find('li').filter(function() { - return $(this).text().toLowerCase() === label.toLowerCase(); - })[0]; - activateOption(options, selectedOption); + + if (!$(_this.elements.dropdown).is(':visible')) { + $(_this.elements.input).trigger('open', ['focus']); } }, - 'click': function (e){ + 'click': function(e) { e.stopPropagation(); + }, + 'blur': function() { + if (!_this.prop.multiple) { + $(_this.elements.input).trigger('close'); + } } }); - $newSelect.on('blur', function() { - if (!multiple) { - $(this).trigger('close'); + $(window).on({ + 'click': function() { + _this.prop.multiple && (_this.prop.hover || $(_this.elements.input).trigger('close')); } - options.find('li.selected').removeClass('selected'); }); - options.hover(function() { - optionsHover = true; - }, function () { - optionsHover = false; - }); + // Copy tabindex + if (_this.elements.select.getAttribute('tabindex')) { + _this.elements.input.setAttribute('tabindex', _this.elements.select.getAttribute('tabindex')); + } - $(window).on({ - 'click': function () { - multiple && (optionsHover || $newSelect.trigger('close')); + // Set the onkeydown function + _this.filterQuery = []; + + function onKeyDown(e) { + var code = e.keyCode || e.which; + + // TAB - switch to another input + if (code === 9) { + $(_this.elements.input).trigger('close'); + + return; } - }); - // Add initial multiple selections. - if (multiple) { - $select.find("option:selected:not(:disabled)").each(function () { - var index = $(this).index(); + // ARROW DOWN WHEN SELECT IS CLOSED - open select dropdown + if (code === 40 && _this.elements.dropdown.offsetParent === null) { + $(_this.elements.input).trigger('open'); - toggleEntryFromArray(valuesSelected, index, $select); - options.find("li").eq(index).find(":checkbox").prop("checked", true); - }); - } + return; + } - // Make option as selected and scroll to selected position - var activateOption = function(collection, newOption) { - if (newOption) { - collection.find('li.selected').removeClass('selected'); - var option = $(newOption); - option.addClass('selected'); - options.scrollTo(option); + // ENTER WHEN SELECT IS CLOSED - submit form + if (code === 13 && _this.elements.dropdown.offsetParent === null) { + return; } - }; - // Allow user to search by typing - // this array is cleared after 1 second - var filterQuery = [], - onKeyDown = function(e){ - // TAB - switch to another input - if(e.which == 9){ - $newSelect.trigger('close'); - return; - } + e.preventDefault(); - // ARROW DOWN WHEN SELECT IS CLOSED - open select options - if(e.which == 40 && !options.is(':visible')){ - $newSelect.trigger('open'); - return; - } + // CASE WHEN USER TYPE LETTERS + var letter = String.fromCharCode(code).toLowerCase(), + nonLetters = [9, 13, 27, 38, 40], + selectedOption; - // ENTER WHEN SELECT IS CLOSED - submit form - if(e.which == 13 && !options.is(':visible')){ - return; - } + if (letter && (nonLetters.indexOf(code) === -1)) { + _this.filterQuery.push(letter); - e.preventDefault(); + var array = [], + options = _this.elements.dropdown.querySelectorAll('li'); - // CASE WHEN USER TYPE LETTERS - var letter = String.fromCharCode(e.which).toLowerCase(), - nonLetters = [9,13,27,38,40]; - if (letter && (nonLetters.indexOf(e.which) === -1)) { - filterQuery.push(letter); + Array.prototype.forEach.call(options, function(option) { + array.push(option); + }); - var string = filterQuery.join(''), - newOption = options.find('li').filter(function() { - return $(this).text().toLowerCase().indexOf(string) === 0; - })[0]; + var string = _this.filterQuery.join(''), + newOption = array.filter(function(option) { + return option.textContent.toLowerCase().indexOf(string) === 0; + })[0]; - if (newOption) { - activateOption(options, newOption); - } - } + if (newOption) { + activateOption(_this.elements.dropdown, newOption); + } + } - // ENTER - select option and close when select options are opened - if (e.which == 13) { - var activeOption = options.find('li.selected:not(.disabled)')[0]; - if(activeOption){ - $(activeOption).trigger('click'); - if (!multiple) { - $newSelect.trigger('close'); - } - } - } + // ENTER - select option and close when select dropdown are opened + if (code === 13) { + var activeOption = _this.elements.dropdown.querySelector('li.selected:not(.disabled)'); - // ARROW DOWN - move to next not disabled option - if (e.which == 40) { - if (options.find('li.selected').length) { - newOption = options.find('li.selected').next('li:not(.disabled)')[0]; - } else { - newOption = options.find('li:not(.disabled)')[0]; - } - activateOption(options, newOption); - } + if (activeOption) { + $(activeOption).trigger('click'); - // ESC - close options - if (e.which == 27) { - $newSelect.trigger('close'); + if (!_this.prop.multiple) { + $(_this.elements.input).trigger('close'); } + } + } - // ARROW UP - move to previous not disabled option - if (e.which == 38) { - newOption = options.find('li.selected').prev('li:not(.disabled)')[0]; - if(newOption) - activateOption(options, newOption); - } + if (code === 40) { + selectedOption = _this.elements.dropdown.querySelector('li.selected'); - // Automaticaly clean filter query so user can search again by starting letters - setTimeout(function(){ filterQuery = []; }, 1000); - }; + if (selectedOption) { + newOption = _this.elements.dropdown.querySelector('li.selected').nextSibling; + } else { + newOption = _this.elements.dropdown.querySelector('li:not(.disabled)'); + } - $newSelect.on('keydown', onKeyDown); - }); + activateOption(_this.elements.dropdown, newOption); + } + + // ESC - close dropdown + if (code === 27) { + $(_this.elements.input).trigger('close'); + } + + // ARROW UP - move to previous not disabled option + if (code === 38) { + selectedOption = _this.elements.dropdown.querySelector('li.selected'); + + if (selectedOption) { + newOption = selectedOption.previousSibling; + activateOption(_this.elements.dropdown, newOption); + } + } + + // Automatically clean filter query so user can search again by starting letters + setTimeout(function() { + _this.filterQuery.length = 0; + }, 1000); + } + + // Add event + $(_this.elements.input).on('keydown', onKeyDown); + + // Fill the dropdown + Array.prototype.forEach.call(_this.prop.elements, function(element) { + prepareElement(element, _this); + }); + + // Refer all the checkboxes and list option previously created + if (_this.prop.multiple) { + _this.elements.checkboxes = _this.elements.dropdown.querySelectorAll('input[type="checkbox"]'); + } else { + _this.elements.radio = _this.elements.dropdown.querySelectorAll('input[type="radio"]'); + } + _this.elements.li = _this.elements.dropdown.querySelectorAll('li'); + + _this.elements.wrapper.appendChild(_this.elements.caret); + _this.elements.wrapper.appendChild(_this.elements.input); + _this.elements.wrapper.appendChild(_this.elements.dropdown); + // Append the wrapper into the select parent + _this.elements.select.parentNode.insertBefore(_this.elements.wrapper, _this.elements.select.parentNode.firstChild); + // Move the parent into the wrapper + _this.elements.wrapper.appendChild(select); + // Initialized the select + _this.elements.select.classList.add('initialized'); + + // Check if preselected option are existing + if (_this.prop.isSelectedOption()) { + var selected = _this.prop.selected(); + + // Select all the selected option + Array.prototype.forEach.call(selected, function(option) { + // Select / deselect option + toggleSelectElement(option, _this, true); + }); + } else { + var disabledOption = _this.elements.select.querySelector('option:disabled'); - function toggleEntryFromArray(entriesArray, entryIndex, select) { - var index = entriesArray.indexOf(entryIndex), - notAdded = index === -1; + // If no options are preselected, select the first disabled + if (disabledOption) { + var equivalent = equivalentItem(disabledOption, _this), + input = equivalent.querySelector('input[type="radio"]'); - if (notAdded) { - entriesArray.push(entryIndex); + // We don't want checkbox because a disabled option isn't selected + if (input) { + input.checked = true; + } + + // Fill the content input + fillContentInput([disabledOption], _this); + } + } + + // Initialize options + if (options.hover !== undefined) { + _this.prop.options.hover = options.hover; + } + if (options.belowOrigin !== undefined) { + _this.prop.options.belowOrigin = options.belowOrigin; + } + if (options.maxHeight !== undefined) { + _this.prop.options.maxHeight = options.maxHeight; + } + if (options.maxWidth !== undefined) { + _this.prop.options.maxWidth = options.maxWidth; + } + if (options.arrangement !== undefined) { + _this.prop.options.arrangement = options.arrangement; + } + + // Initialize the dropdown + $(_this.elements.input).dropdown(_this.prop.options); + + return _this; + } + + // If all elements from parent are active so select it too + function selectParent(child, parent, _this) { + var siblings = (function() { + var self = {}; + self.all = getOptgroupElements(child.dataset.optgroup, _this); + self.selected = []; + + return self; + })(), + option = { + equivalent: parent, + input: parent.querySelector('input[type="checkbox"]'), + }; + + // For each siblings we check if they're active + for (var i = 0, countSiblings = siblings.all.length; i < countSiblings; i++) { + siblings.all[i].selected ? siblings.selected.push(true) : siblings.selected.push(false); + } + + // If all siblings are selected select parent + if (siblings.selected.indexOf(false) === -1) { + selectElement(option); } else { - entriesArray.splice(index, 1); + deselectElement(option); + } + } + + // Returns the equivalent item to the element passed as parameter (li, option, optgroup) + function equivalentItem(element, _this) { + var tagName = element.tagName, + item; + + switch (tagName) { + case 'LI': + item = _this.elements.select.querySelector('option[value="' + element.dataset.value + '"]'); + + break; + case 'OPTION': + item = _this.elements.dropdown.querySelector('li[data-value="' + element.value + '"]'); + + break; + case 'OPTGROUP': + item = _this.elements.dropdown.querySelector('li[data-value="' + element.label + '"]'); + + break; + default: + console.log('Error. Please open an issue at Materialize.'); + + break; + } + + return item; + } + + // Select an option and element + function selectElement(option) { + if (option.element) { + option.element.selected = true; } - select.siblings('ul.dropdown-content').find('li').eq(entryIndex).toggleClass('active'); + option.equivalent.classList.add('active'); + option.input.checked = true; + } - // use notAdded instead of true (to detect if the option is selected or not) - select.find('option').eq(entryIndex).prop('selected', notAdded); - setValueToInput(entriesArray, select); + // Deselect an option and element + function deselectElement(option) { + if (option.element) { + option.element.selected = false; + } - return notAdded; + option.equivalent.classList.remove('active'); + option.input.checked = false; } - function setValueToInput(entriesArray, select) { - var value = ''; + // Select an element + function toggleSelectElement(element, _this, onload) { + var option = {}; + + // If element is LI reverse the attributions + if (isTagName('LI', element)) { + option.element = equivalentItem(element, _this); + + if (!option.element) { + return; + } + + option.equivalent = element; + option.parent = option.element.parentNode; + } + + // Fill the object which contains all its data + option = (function() { + option.element = option.element || element; + option.equivalent = option.equivalent || equivalentItem(element, _this); + option.input = option.equivalent.querySelector('input'); + option.selected = option.element.selected; + option.parent = option.parent || option.element.parentNode; + + return option; + })(); + + // Return if there is no findable li or if the option is already selected for single select + if (!option.equivalent || (!_this.prop.multiple && option.equivalent.classList.contains('active'))) { + return; + } + + // If select is single + if (!_this.prop.multiple && !onload) { + var oldSelectedOption = _this.prop.selected()[0], + oldSelectedElement = equivalentItem(oldSelectedOption, _this); - for (var i = 0, count = entriesArray.length; i < count; i++) { - var text = select.find('option').eq(entriesArray[i]).text(); + // Fill an object relative to the old selected option + var oldOption = (function() { + var self = {}; + self.element = oldSelectedOption, + self.equivalent = oldSelectedElement, + self.input = self.equivalent.querySelector('input'); - i === 0 ? value += text : value += ', ' + text; + return self; + })(); + + deselectElement(oldOption); } - if (value === '') { - value = select.find('option:disabled').eq(0).text(); + // Deselect options here + if (option.selected && !onload) { + deselectElement(option); + // Select options here + } else { + selectElement(option); } - select.siblings('input.select-dropdown').val(value); + // Select parent if all its children are selected + if (_this.prop.optgroup && _this.prop.multiple && isTagName('OPTGROUP', option.parent)) { + var parent = equivalentItem(option.parent, _this); + + // Verification of existing parent + if (parent) { + selectParent(option.equivalent, parent, _this); + } + } + + // Fill the input value with new values + fillContentInput(_this.prop.selected(), _this); } + + // Toggle select all li relative to an optgroup li + function toggleSelectAllElements(optgroup, _this) { + var status = optgroup.classList.contains('active'), + allChildren = getOptgroupElements(optgroup.dataset.value, _this), + matches; + + if (status) { + // Filter for every optgroup's option is active and isn't disabled + matches = Array.prototype.slice.call(allChildren).filter(function(element) { + return element.selected && !element.disabled; + }); + } else { + // Filter for every optgroup's option isn't active and isn't disabled + matches = Array.prototype.slice.call(allChildren).filter(function(element) { + return !element.selected && !element.disabled; + }); + } + + // Toggle all options stored + Array.prototype.forEach.call(matches, function(li) { + toggleSelectElement(li, _this); + }); + } + + // Prepare element by passing relative values to it + function prepareElement(element, _this) { + // Check if element is optgroup + isTagName('OPTGROUP', element) ? CreateElement(element, _this, true) : CreateElement(element, _this, false); + } + + /* Create a new option with the correct params + * + * Li Constructor + * + */ + function CreateElement(element, _this, optgroup) { + var options = { + iconUrl: element.dataset.icon, + imageClasses: element.className, + text: element.getAttribute('label') || element.textContent, + dataValue: element.getAttribute('label') || element.value, + disabled: element.disabled || element.parentNode.disabled + }; + + _this.elements.dropdown.appendChild(createLiElement()); + + function createLiElement() { + var liElement = document.createElement('li'), + spanElement = document.createElement('span'); + + // Add icon url if exists + if (options.iconUrl) { + var image = document.createElement('img'); + image.src = options.iconUrl; + image.className = options.imageClasses; + + liElement.appendChild(image); + } + + var label = document.createElement('label'), + input; + + // Refer the optgroup parent for each option that isn't an optgroup + if (_this.prop.optgroup && !optgroup) { + var parent = element.parentNode; + + // Refer the label as data-label if the optgroup has a label + if (isTagName('OPTGROUP', parent) && parent.label) { + liElement.dataset.optgroup = parent.label; + } + } + + // Create input and label + if (_this.prop.multiple) { + input = document.createElement('input'); + input.type = 'checkbox'; + + spanElement.appendChild(input); + spanElement.appendChild(label); + } else { + input = document.createElement('input'); + input.type = 'radio'; + + spanElement.appendChild(input); + spanElement.appendChild(label); + } + + // Set span content + spanElement.innerHTML += options.text.replace(/"/g, '"').trim(); + + // Verify the current element isn't disabled before applying class + // Set datavalue for the current element + element.id ? liElement.dataset.id = element.id : ''; + liElement.className += optgroup ? 'optgroup' : ''; + options.disabled ? liElement.classList.add('disabled') : ''; + liElement.dataset.value = options.dataValue; + + // Add event handler + $(liElement).on('click', function() { + // Return if element is disabled + if (liElement.classList.contains('disabled')) { + return; + } + + if (_this.prop.multiple) { + if (optgroup) { + // Avoid multiple selection by clicking on a optgroup which have disabled option + var disabledElements = getOptgroupElements(liElement.dataset.value, _this); + disabledElements = Array.prototype.slice.call(disabledElements).filter(function(element) { + return element.classList.contains('disabled'); + }); + + // Avoid clicking if the element is disabled + if (liElement.classList.contains('disabled') || disabledElements.length) { + return; + } + + // Select all children relative to the optgroup clicked + toggleSelectAllElements(liElement, _this); + } else { + toggleSelectElement(liElement, _this); + } + } else { + // Return if element is optgroup for single select + if (liElement.classList.contains('optgroup')) { + return; + } + + toggleSelectElement(liElement, _this); + } + + // Scroll to the option + $(_this.elements.dropdown).scrollTo(liElement); + }); + + liElement.appendChild(spanElement); + + return liElement; + } + } + + Array.prototype.forEach.call(this, function(selectElement) { + var initialized = selectElement.classList.contains('initialized'); + + // Destroy callback + if ('destroy' === callback && initialized) { + destroySelect(selectElement); + + return; + } + + // Return if user choose to apply a native select or select is disabled or initialized or if select isn't filled + if (selectElement.classList.contains('browser-default') || initialized || selectElement.disabled || !selectElement.options.length) { + return; // Continue to the next select + } + + // Initialize the select + var Select = CreateSelect(selectElement, callback); + }); + + // For not breaking jQuery chaining return a jQuery element + return $(this); }; }( jQuery )); diff --git a/sass/components/_dropdown.scss b/sass/components/_dropdown.scss index 71ab9f5528..e89f0c6f5a 100644 --- a/sass/components/_dropdown.scss +++ b/sass/components/_dropdown.scss @@ -42,7 +42,7 @@ padding: (($dropdown-item-height - 22) / 2) 16px; } - & > span > label { + & > span > label, & > span > input[type="radio"] + label, & > span > input[type="checkbox"] + label { top: 1px; left: 3px; height: 18px; diff --git a/sass/components/_form.scss b/sass/components/_form.scss index 1b1da0fa35..f6a9194fcf 100644 --- a/sass/components/_form.scss +++ b/sass/components/_form.scss @@ -688,14 +688,6 @@ input[type=checkbox]:not(:disabled) ~ .lever:active:after { } } -select { display: none; } -select.browser-default { display: block; } - -// Disabled styles -select:disabled { - color: rgba(0,0,0,.3); -} - .select-wrapper input.select-dropdown:disabled { color: rgba(0,0,0,.3); cursor: default; @@ -709,15 +701,32 @@ select:disabled { color: rgba(0,0,0,.3); } -.select-dropdown li.disabled, -.select-dropdown li.disabled > span, -.select-dropdown li.optgroup { - color: rgba(0,0,0,.3); - background-color: transparent; +.select-dropdown li { + &.disabled, &.disabled > span, &.optgroup { + color: rgba(0,0,0,.3); + background-color: transparent; + } + + &.disabled { + cursor: not-allowed; + + &.selected { + background-color: rgba(0, 0, 0, 0.05); + } + + [type="checkbox"]:checked + label:before, [type="checkbox"]:checked:not(.filled-in) + label:after { + border-right: 2px solid rgba(0, 0, 0, .3); + border-bottom: 2px solid rgba(0, 0, 0, .3); + } + } } // Icons .select-dropdown li { + &[data-optgroup] { + padding-left: 1rem; + } + img { height: $dropdown-item-height - 10; width: $dropdown-item-height - 10; @@ -728,18 +737,15 @@ select:disabled { // Optgroup styles .select-dropdown li.optgroup { + background-color: rgba(0, 0, 0, .02); border-top: 1px solid $dropdown-hover-bg-color; - &.selected > span { - color: rgba(0, 0, 0, .7); + &.selected, .active { + background-color: rgba(0, 0, 0, .1); } & > span { - color: rgba(0, 0, 0, .4); - } - - & ~ li:not(.optgroup) { - padding-left: 1rem; + color: rgba(0, 0, 0, .7); } } @@ -952,4 +958,14 @@ select { border: 1px solid #f2f2f2; border-radius: 2px; height: 3rem; + display: none; + + &.browser-default { + display: block; + } + + // Disabled styles + &:disabled { + color: rgba(0,0,0,.3); + } }