/* * Alternate Select Multiple (asmSelect) 2.0 - jQuery Plugin * https://processwire.com * * Copyright (c) 2009-2019 by Ryan Cramer * * Licensed under the MIT license. * * */ (function($) { $.fn.asmSelect = function(customOptions) { var options = { // General settings listType: 'ol', // Ordered list 'ol', or unordered list 'ul' sortable: false, // Should the list be sortable? addable: true, // Can items be added to selection? deletable: true, // Can items be removed from selection? highlight: false, // Use the highlight feature? fieldset: false, // Use fieldset support? (for PW Fieldset types) animate: false, // Animate the the adding/removing of items in the list? addItemTarget: 'bottom', // Where to place new selected items in list: top or bottom hideWhenAdded: false, // Hide the option when added to the list? works only in FF hideWhenEmpty: false, // Hide the optionDisabledClass: 'asmOptionDisabled', // Class for items that are already selected / disabled listClass: 'asmList', // Class for the list ($ol) listSortableClass: 'asmListSortable', // Another class given to the list when it is sortable listItemClass: 'asmListItem', // Class for the
  • list items listItemLabelClass: 'asmListItemLabel', // Class for the label text that appears in list items listItemDescClass: 'asmListItemDesc', // Class for optional description text, set a data-desc attribute on the
  • elements in the $select representing '_END' fieldset options var msie = 0; // contains the MSIE browser version when MSIE is detected (primarily for MSIE 6 & 7) var $highlightSpan = null; // active highlight span (is removed when option selected) /** * Initialize an asmSelect * */ function init() { // initialize the alternate select multiple if(options.deletable && !options.addable) options.hideDeleted = false; // identify which items were already selected in the original $original.find('option[selected]').addClass('asmOriginalSelected'); // this loop ensures uniqueness, in case of existing asmSelects placed by ajax (1.0.3) while($("#" + options.containerClass + index).length > 0) index++; $select = $("") .addClass(options.selectClass) .addClass($original.attr('class')) .attr('name', options.selectClass + index) .attr('id', options.selectClass + index); if(!options.addable) $select.hide(); $selectRemoved = $(""); $ol = $("<" + options.listType + ">") .addClass(options.listClass) .attr('id', options.listClass + index); $container = $("
    ") .addClass(options.containerClass) .attr('id', options.containerClass + index); buildSelect(); $select.on('change', selectChangeEvent) .on('click', selectClickEvent); $original.on('change', originalChangeEvent) .wrap($container).before($select).before($ol); if(options.sortable) makeSortable(); /* if(typeof $.browser != "undefined" && typeof $.browser.msie != "undefined") { msie = $.browser.msie ? $.browser.version : 0; } if(msie > 0 && msie < 8) $ol.css('display', 'inline-block'); // Thanks Matthew Hutton */ if(options.fieldset) { setupFieldsets(); findFieldsetCloseItems($original); $original.on('rebuild', function(e) { console.log('asmSelect REBUILD'); findFieldsetCloseItems($(this)); }); } $original.trigger('init'); if(options.editLinkModal === 'longclick') { $ol.on('longclick', 'a.asmEditLinkModalLongclick', clickEditLink); } // if select2 exists, give it the appropriate attributes, hide it, and place it after the interactive select if($select2 && $select2.length) { $select2.addClass($select.attr('class')).removeClass('asmSelect').attr('id', $select.attr('id') + '-helper').hide(); $select.after($select2); } } /** * Make any items in the selected list sortable * * Requires jQuery UI sortables, draggables, droppables * */ function makeSortable() { var fieldsetItems = []; var sortableUpdate = function($ul, e, data) { var $option = $('#' + data.item.attr('rel')); var updatedOptionId = $option.attr('id'); $ul.children("li").each(function(n) { $option = $('#' + $(this).attr('rel')); $original.append($option); }); if(updatedOptionId) { triggerOriginalChange(updatedOptionId, 'sort'); } } $ol.sortable({ items: 'li.' + options.listItemClass, axis: 'y', cancel: 'a.asmEditLinkModalLongclick', update: function(e, data) { if(data.item.hasClass('asmFieldsetStart')) return; sortableUpdate(jQuery(this), e, data); $ol.trigger('sorted', [ data.item ]); }, start: function(e, data) { if(options.jQueryUI) data.item.addClass('ui-state-highlight'); if(data.item.hasClass('asmFieldsetStart')) { var $next = data.item; var stopName = data.item.find('.' + options.listItemLabelClass).text() + '_END'; do { if($next.find('.' + options.listItemLabelClass).text() == stopName) break; $next = $next.next('li'); if($next.length && !$next.hasClass('ui-sortable-placeholder')) { $next.fadeTo(50, 0.7).slideUp('fast'); fieldsetItems.push($next); } } while($next.length); } }, stop: function(e, data) { if(options.jQueryUI) data.item.removeClass('ui-state-highlight'); if(data.item.hasClass('asmFieldsetStart')) { var $lastItem = data.item; for(var n = 0; n < fieldsetItems.length; n++) { var $item = fieldsetItems[n]; $lastItem.after($item); $lastItem = $item; $item.slideDown('fast').fadeTo('fast', 1.0); } fieldsetItems = []; setupFieldsets(); sortableUpdate(jQuery(this), e, data); } else { setupFieldsets(); } } }).addClass(options.listSortableClass); } /** * Event called when an option has been selected on the $select we created * * @param e * @returns {boolean} * */ function selectChangeEvent(e) { // check to make sure it's not an IE screwup, and add it to the list if(msie > 0 && msie < 7 && !ieClick) return; var $select = $(this); var $option = $select.children("option:selected"); if($highlightSpan && $highlightSpan.length) $highlightSpan.remove(); // if item is not selectable then do not proceed if(!$option.attr('value').length) return false; if($option.hasClass(options.optionParentClass)) { // an option with asmParent class was selected parentOptionSelected($select, $option); e.stopPropagation(); return false; } // add the item var id = $option.slice(0,1).attr('rel'); addListItem(id); ieClick = false; triggerOriginalChange(id, 'add'); // for use by user-defined callbacks if($option.hasClass(options.optionChildClass)) { // if an option.asmChild was selected, keep the parent selected afterwards childOptionSelected($select, $option); } } /** * Called by selectChangeEvent() when an option.asmParent is selected to show/hide child options * * Applicable only if parent/child options are in use * * @param $select * @param $option * */ function parentOptionSelected($select, $option) { var $sel = $select; var isOpenParent = $option.hasClass(options.optionParentOpenClass); // is option an asmParent option that is open? if(options.useSelect2 && !isOpenParent) $sel = getSelect2(); var $children = $sel.find( "option." + options.optionChildClass + "[" + options.optionChildAttr + "='" + $option.attr('value') + "']" ); var parentHTML = $option.html(); var openLabel = ' +' + $children.filter(':not(:disabled)').length + ' ' + options.optionParentIcon; if(isOpenParent) { // an already-open parent option has been clicked hideSelectOptions($children); parentHTML = parentHTML.replace(/\+\d+ ./, ''); // note the '.' represents the UTF-8 arrow icon // $option.removeClass(options.optionParentOpenClass).removeAttr('selected'); $option.removeClass(options.optionParentOpenClass).prop('selected', false); } else { // a closed parent has been clicked var indent = options.optionChildIndent; if($option.hasClass(options.optionChildClass)) indent += indent; // double indent $children.each(function() { // indent the child options (if they aren't already) var $child = $(this); var childHTML = $child.html(); // if(!$child.is(':disabled') && childHTML.indexOf(options.optionChildIndent) !== 0) { if(childHTML.indexOf(options.optionChildIndent) !== 0) { $child.html(indent + childHTML); } }); showSelectOptions($children, $option); // $select.find(':selected').removeAttr('selected'); $select.find(':selected').prop('selected', false); // collapse any existing parents that are open (behave as accordion) if(!$option.hasClass(options.optionChildClass)) { $select.find('.' + options.optionParentOpenClass).each(function() { $(this).prop('selected', true).trigger('change'); // trigger close if any existing open }); } // make the parent selected, encouraging them to click to select a child // $option.addClass(options.optionParentOpenClass).attr('selected', 'selected'); $option.addClass(options.optionParentOpenClass).prop('selected', true); parentHTML += openLabel; var highlightOption = options.highlight; options.highlight = true; // temporarily enable, even if not otherwise enabled setHighlight(null, options.optionParentLabel, true); if(!highlightOption) options.highlight = false; // restore option setting } $option.html(parentHTML); } /** * Called by selectChangeEvent() when an option.asmChild is selected * * Applicable only if parent/child options are in use * * @param $select * @param $option * */ function childOptionSelected($select, $option) { // if an option.asmChild was selected, keep the parent selected afterwards // $select.find("option[value='" + $option.attr(options.optionChildAttr) + "']").attr('selected', 'selected'); $select.find("option[value='" + $option.attr(options.optionChildAttr) + "']").prop('selected', true); } /** * Event called when a we created for the user to select items from * */ function buildSelect() { buildingSelect = true; // add a first option to be the home option / default selectLabel var title = $original.attr('title'); // number of items that are not disabled var numActive = 0; if(title === undefined) title = ''; $select.prepend(""); $original.children("option").each(function(n) { var $t = $(this); var id; if(!$t.attr('id')) $t.attr('id', 'asm' + index + 'option' + n); id = $t.attr('id'); if($t.is(":selected")) { addListItem(id); addSelectOption(id, true); } else if($t.is(":disabled")) { addSelectOption(id, true); } else { numActive++; addSelectOption(id); } }); // IE6 requires this on every buildSelect() if(!options.debugMode) $original.hide(); selectFirstItem(); if(options.hideWhenEmpty) { if(numActive > 0) $select.show(); else $select.hide(); } buildingSelect = false; } /** * Add an ") // option for new select .val($O.val()) .attr('rel', optionId); // does the option have the asmParent class? if($O.hasClass(options.optionParentClass)) { $option.addClass(options.optionParentClass); } // copy disabled state if applicable if(disabled) disableSelectOption($option); // does source select have a data-asmParent attribute? if($O.attr(data_asmParent)) { // this is an asmChild option that requires a parent selection before appearing // add asmChild class $option.addClass(options.optionChildClass); // copy the data-asmParent attribute to new option $option.attr(data_asmParent, $O.attr(data_asmParent)); // check if we should make options hidden until the parent is selected if(options.useSelect2) { // place option in the hidden $select2 rather than $select var $sel2 = getSelect2(); $sel2.append($option); } else { // hide the option (not supported by Safari) hideSelectOptions($option); $select.append($option); } } else { // add option to the select $select.append($option); } } /** * Get the $select2 used for hidden child options that are not currently visible * * If the $select2 does not yet exist, it creates it * */ function getSelect2() { // get the select used for hidden options if($select2 && $select2.length) return $select2; $select2 = $(''); return $select2; } /** * Hide the given
  • from an