1055 lines
36 KiB
JavaScript
1055 lines
36 KiB
JavaScript
|
/*
|
|||
|
* 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 <select> when there are no items available to select?
|
|||
|
debugMode: false, // Debug mode keeps original select visible
|
|||
|
jQueryUI: true, // use jQuery UI?
|
|||
|
hideDeleted: true, // Hide items when deleted. If false, items remain but are marked for deletion
|
|||
|
deletedOpacity: 0.5, // opacity of deleted item, set to 1.0 to disable opacity adjustment (applicable only if hideDeleted=true)
|
|||
|
deletedPrepend: '-', // Deleted item values are prepended with this character in the form submission (applicable only if hideDeleted=true)
|
|||
|
useSelect2: true, // use a separate select for unselected child options? (required by Safari)
|
|||
|
removeWhenAdded: false, // no longer used (was a separate select that contained removed items)
|
|||
|
highlightTag: '<span></span>', // Tag to use for highlight element highlight option is in use
|
|||
|
|
|||
|
// Labels
|
|||
|
sortLabel: '<span class="asmIcon asmIconSort">↕</span>', // sortable handle/icon
|
|||
|
removeLabel: '<span class="asmIcon asmIconRemove">×</span>', // Text used in the "remove" link
|
|||
|
highlightAddedLabel: 'Added: ', // Text that precedes highlight of added item
|
|||
|
highlightRemovedLabel: 'Removed: ', // Text that precedes highlight of removed item
|
|||
|
|
|||
|
// Classes
|
|||
|
containerClass: 'asmContainer', // Class for container that wraps this widget
|
|||
|
selectClass: 'asmSelect', // Class for the newly created <select>
|
|||
|
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 <li> 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 <option> to use it. May contain HTML.
|
|||
|
listItemStatusClass: 'asmListItemStatus', // Class for optional status text, set a data-status attribute on the <option> to use it. May contain HTML.
|
|||
|
listItemHandleClass: 'asmListItemHandle', // Class for sort handle
|
|||
|
removeClass: 'asmListItemRemove', // Class given to the "remove" link
|
|||
|
editClass: 'asmListItemEdit',
|
|||
|
highlightClass: 'asmHighlight', // Class given to the highlight <span>
|
|||
|
deletedClass: 'asmListItemDeleted',
|
|||
|
|
|||
|
// Edit option settings
|
|||
|
editLink: '', // Optional URL options can link to with tag {value} replaced by option value, i.e. /path/to/page/edit?id={$value}
|
|||
|
editLabel: '<span class="ui-icon ui-icon-extlink"></span>', // Text used in the "edit" link (if editLink is populated)
|
|||
|
editLinkOnlySelected: true, // When true, edit link only appears for items that were already selected
|
|||
|
editLinkModal: true, // Whether the edit link (if used) should be modal or "longclick" for longclick modal only
|
|||
|
editLinkButtonSelector: 'form button.ui-button:visible', // button selector for finding buttons that should become modal window buttons
|
|||
|
|
|||
|
// Using parent and child options where parent is selected to reveal child options
|
|||
|
// -------------------------------------------------------------------------------
|
|||
|
// 1. Parent option(s) must have a class attribute of 'asmParent'. While optional, I also recommend the label have an
|
|||
|
// ellipsis or something to indicate it is a parent:
|
|||
|
//
|
|||
|
// <option value='foo' class='asmParent'>Foo …</option>
|
|||
|
//
|
|||
|
// 2. Child options must come immediately after parent and have a 'data-asmParent' attribute containing 'value' attribute
|
|||
|
// of parent. While optional, I also recommend the child option include both parent and child label so that the
|
|||
|
// relationship remains clear even after selection, i.e. “Foo > Bar” or “Foo: Bar” or similar:
|
|||
|
//
|
|||
|
// <option value='bar' data-asmParent='foo'>Foo: Bar</option>
|
|||
|
//
|
|||
|
// Please Note:
|
|||
|
// • Selecting a parent option does not modify the value of the selection, it only reveals more selectable options.
|
|||
|
// • Up to two levels of parent/child options are supported (more levels may also work but have not been attempted).
|
|||
|
// • The options below define the settings for parent/child options (most will want to leave as-is):
|
|||
|
|
|||
|
optionParentClass: 'asmParent', // Class you will assign to a parent item that has one or more child items
|
|||
|
optionParentIcon: '⬇', // UTF-8 down-pointing arrow icon added to select parent options, indicating more options to select
|
|||
|
optionChildAttr: 'data-asmParent', // Attribute you will add to child items with its value pointing to the option.asmParent’s value attribute
|
|||
|
optionParentOpenClass: 'asmParentOpen', // Assigned automatically: Class for a parent option that has its children visible
|
|||
|
optionChildClass: 'asmChild', // Assigned automatically: Class for an option that has a parent option
|
|||
|
optionChildIndent: ' ', // Indent applied to child options
|
|||
|
optionParentLabel: '← Click again to make selection'
|
|||
|
|
|||
|
};
|
|||
|
|
|||
|
$.extend(options, customOptions);
|
|||
|
|
|||
|
return this.each(function(index) {
|
|||
|
|
|||
|
var $original = $(this); // the original select multiple
|
|||
|
var $container; // a container that is wrapped around our widget
|
|||
|
var $select; // the new select we have created
|
|||
|
var $select2 = null; // an extra hidden select for holding non-selected, non-visible options
|
|||
|
var $ol; // the list that we are manipulating
|
|||
|
var buildingSelect = false; // is the new select being constructed right now?
|
|||
|
var ieClick = false; // in IE, has a click event occurred? ignore if not
|
|||
|
var ignoreOriginalChangeEvent = false; // originalChangeEvent bypassed when this is true
|
|||
|
var fieldsetCloseItems = {}; // list-item <li> 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 = $("<select></select>")
|
|||
|
.addClass(options.selectClass)
|
|||
|
.addClass($original.attr('class'))
|
|||
|
.attr('name', options.selectClass + index)
|
|||
|
.attr('id', options.selectClass + index);
|
|||
|
if(!options.addable) $select.hide();
|
|||
|
|
|||
|
$selectRemoved = $("<select></select>");
|
|||
|
|
|||
|
$ol = $("<" + options.listType + "></" + options.listType + ">")
|
|||
|
.addClass(options.listClass)
|
|||
|
.attr('id', options.listClass + index);
|
|||
|
|
|||
|
$container = $("<div></div>")
|
|||
|
.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 <select> is clicked (for IE6)
|
|||
|
*
|
|||
|
* @todo is this still necessary in 2019?
|
|||
|
*
|
|||
|
*/
|
|||
|
function selectClickEvent() {
|
|||
|
// IE6 lets you scroll around in a select without it being pulled down
|
|||
|
// making sure a click preceded the change event reduces the chance
|
|||
|
// if unintended items being added. there may be a better solution?
|
|||
|
ieClick = true;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Called when a select or option change event was manually triggered on original select[multiple]
|
|||
|
*
|
|||
|
* When such a change event occurs, we rebuild our own visible $select
|
|||
|
*
|
|||
|
* @param e
|
|||
|
*
|
|||
|
*/
|
|||
|
function originalChangeEvent(e) {
|
|||
|
|
|||
|
if(ignoreOriginalChangeEvent) {
|
|||
|
ignoreOriginalChangeEvent = false;
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
$select.empty();
|
|||
|
if(options.useSelect2 && $select2) $select2.empty();
|
|||
|
$ol.empty();
|
|||
|
buildSelect();
|
|||
|
|
|||
|
// opera has an issue where it needs a force redraw, otherwise
|
|||
|
// the items won't appear until something else forces a redraw
|
|||
|
// @todo is this still necessary in 2019?
|
|||
|
if(typeof $.browser != "undefined") {
|
|||
|
if ($.browser.opera) $ol.hide().fadeIn("fast");
|
|||
|
}
|
|||
|
|
|||
|
if(options.fieldset) setupFieldsets();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Build (or rebuild) the <select> 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("<option>" + title + "</option>");
|
|||
|
|
|||
|
$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> to the $select (called only by buildSelect function)
|
|||
|
*
|
|||
|
* @param optionId The 'id' attribute of the option element to add
|
|||
|
* @param disabled Is the option disabled?
|
|||
|
*
|
|||
|
*/
|
|||
|
function addSelectOption(optionId, disabled) {
|
|||
|
|
|||
|
if(typeof disabled == "undefined") disabled = false;
|
|||
|
|
|||
|
var $O = $('#' + optionId); // option from source select
|
|||
|
var data_asmParent = options.optionChildAttr; // data-asmParent attribute name
|
|||
|
var $option = $("<option>" + $O.html() + "</option>") // 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 = $('<select></select>');
|
|||
|
return $select2;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Hide the given <option> elements
|
|||
|
*
|
|||
|
* @param $options
|
|||
|
*
|
|||
|
*/
|
|||
|
function hideSelectOptions($options) {
|
|||
|
// hide the given select <option> elements
|
|||
|
$options.each(function() {
|
|||
|
var $option = $(this);
|
|||
|
if(options.useSelect2) {
|
|||
|
// use separate select to hold non-visible options (default)
|
|||
|
var $sel2 = getSelect2();
|
|||
|
$sel2.append($option);
|
|||
|
if($option.hasClass(options.optionParentOpenClass)) {
|
|||
|
// if option is a parent, also hide any of its children as well
|
|||
|
hideSelectOptions($select.children(
|
|||
|
'option.' + options.optionChildClass +
|
|||
|
'[' + options.optionChildAttr + '="' + $option.attr('value') + '"]'
|
|||
|
));
|
|||
|
}
|
|||
|
} else {
|
|||
|
// hide option using HTML5 hidden attribute (not supported by Safari)
|
|||
|
$option.attr('hidden', 'hidden');
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Show the given <option> elements
|
|||
|
*
|
|||
|
* @param $options The option elements to show
|
|||
|
* @param $afterOption Make them start to appear after this option element
|
|||
|
*
|
|||
|
*/
|
|||
|
function showSelectOptions($options, $afterOption) {
|
|||
|
$options.each(function() {
|
|||
|
var $option = $(this);
|
|||
|
if(options.useSelect2) {
|
|||
|
if(typeof $afterOption != "undefined") {
|
|||
|
$afterOption.after($option);
|
|||
|
$afterOption = $option;
|
|||
|
} else {
|
|||
|
$select.append($option);
|
|||
|
}
|
|||
|
} else {
|
|||
|
$option.removeAttr('hidden');
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Select the first item from the visible $select that we created
|
|||
|
*
|
|||
|
*/
|
|||
|
function selectFirstItem() {
|
|||
|
$select.children().first().prop("selected", true);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Make the given select <option> disabled (indicating it has been selected)
|
|||
|
*
|
|||
|
* @param $option
|
|||
|
*
|
|||
|
*/
|
|||
|
function disableSelectOption($option) {
|
|||
|
|
|||
|
// because safari is the only browser that makes disabled items look 'disabled'
|
|||
|
// we apply a class that reproduces the disabled look in other browsers
|
|||
|
|
|||
|
// $option.addClass(options.optionDisabledClass).attr("selected", false).attr("disabled", true);
|
|||
|
$option.addClass(options.optionDisabledClass).prop("selected", false).prop("disabled", true);
|
|||
|
|
|||
|
if(options.hideWhenEmpty) {
|
|||
|
if($option.siblings('[disabled!=true]').length < 2) $select.hide();
|
|||
|
}
|
|||
|
if(options.hideWhenAdded) $option.hide();
|
|||
|
if(msie) $select.hide().show(); // this forces IE to update display
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Convert an option[disabled] to be enabled
|
|||
|
*
|
|||
|
* @param $option
|
|||
|
*
|
|||
|
*/
|
|||
|
function enableSelectOption($option) {
|
|||
|
|
|||
|
$option.removeClass(options.optionDisabledClass).prop("disabled", false);
|
|||
|
|
|||
|
if(options.hideWhenEmpty) $select.show();
|
|||
|
if(options.hideWhenAdded) $option.show();
|
|||
|
if(msie) $select.hide().show(); // this forces IE to update display
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Add a selected option to the selected list
|
|||
|
*
|
|||
|
* This creates an <li> from an <option> and adds it to the <ol> selected list
|
|||
|
*
|
|||
|
* @param optionId The 'id' attribute of the <option> to add to the list
|
|||
|
*
|
|||
|
*/
|
|||
|
function addListItem(optionId) {
|
|||
|
|
|||
|
var $O = $('#' + optionId);
|
|||
|
|
|||
|
if(!$O) return; // this is the first item, selectLabel
|
|||
|
|
|||
|
var $removeLink = null;
|
|||
|
if(options.deletable) $removeLink = $("<a></a>")
|
|||
|
.attr("href", "#")
|
|||
|
.addClass(options.removeClass)
|
|||
|
.prepend(options.removeLabel)
|
|||
|
.on('click', function() {
|
|||
|
dropListItem($(this).parent('li').attr('rel'));
|
|||
|
return false;
|
|||
|
});
|
|||
|
|
|||
|
var $itemLabel = $("<span></span>").addClass(options.listItemLabelClass);
|
|||
|
|
|||
|
// optional container where an <option>'s data-status attribute will be displayed
|
|||
|
var $itemStatus = $("<span></span>").addClass(options.listItemStatusClass);
|
|||
|
if($O.attr('data-status')) $itemStatus.html($O.attr('data-status'));
|
|||
|
|
|||
|
// optional container where an <option>'s data-desc attribute will be displayed
|
|||
|
var $itemDesc = $("<span></span>").addClass(options.listItemDescClass);
|
|||
|
|
|||
|
if(options.editLink.length > 0 && ($O.is(':selected') || !options.editLinkOnlySelected)) {
|
|||
|
|
|||
|
var $editLink = $("<a></a>")
|
|||
|
.html($O.html())
|
|||
|
.attr('href', options.editLink.replace(/\{value\}/, $O.val()))
|
|||
|
.append(options.editLabel);
|
|||
|
if(options.editLinkModal === "longclick") {
|
|||
|
$editLink.addClass('asmEditLinkModalLongclick');
|
|||
|
} else if(options.editLinkModal) {
|
|||
|
$editLink.on('click', clickEditLink);
|
|||
|
}
|
|||
|
|
|||
|
$itemLabel.addClass(options.editClass).append($editLink);
|
|||
|
|
|||
|
if($O.attr('data-desc')) {
|
|||
|
var $editLink2 = $("<a></a>")
|
|||
|
.html($O.attr('data-desc'))
|
|||
|
.attr('href', $editLink.attr('href'))
|
|||
|
.append(options.editLabel);
|
|||
|
$itemDesc.addClass(options.editClass).append($editLink2);
|
|||
|
if(options.editLinkModal === "longclick") {
|
|||
|
$editLink2.addClass('asmEditLinkModalLongclick');
|
|||
|
} else if(options.editLinkModal) {
|
|||
|
$editLink2.on('click', clickEditLink);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
} else {
|
|||
|
$itemLabel.html($O.html());
|
|||
|
if($O.attr('data-desc')) $itemDesc.html($O.attr('data-desc'));
|
|||
|
}
|
|||
|
|
|||
|
var $item = $("<li></li>")
|
|||
|
.attr('rel', optionId)
|
|||
|
.addClass(options.listItemClass)
|
|||
|
.append($itemLabel)
|
|||
|
.append($itemDesc)
|
|||
|
.append($itemStatus);
|
|||
|
if($removeLink) $item.append($removeLink);
|
|||
|
$item.hide();
|
|||
|
|
|||
|
if(options.jQueryUI) {
|
|||
|
$item.addClass('ui-state-default')
|
|||
|
.on('mouseenter', function() {
|
|||
|
$(this).addClass('ui-state-hover').removeClass('ui-state-default');
|
|||
|
}).on('mouseleave', function() {
|
|||
|
$(this).addClass('ui-state-default').removeClass('ui-state-hover');
|
|||
|
});
|
|||
|
if(options.sortable) {
|
|||
|
// $item.prepend("<span class='" + options.listItemHandleClass + "'></span>");
|
|||
|
if($O.attr('data-handle')) {
|
|||
|
$item.prepend($($O.attr('data-handle')).addClass(options.listItemHandleClass));
|
|||
|
} else {
|
|||
|
$item.prepend($(options.sortLabel).addClass(options.listItemHandleClass));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if(!buildingSelect) {
|
|||
|
if($O.is(":selected")) return; // already have it
|
|||
|
// $O.attr('selected', true);
|
|||
|
$O.prop('selected', true);
|
|||
|
}
|
|||
|
|
|||
|
if(options.addItemTarget == 'top' && !buildingSelect) {
|
|||
|
$ol.prepend($item);
|
|||
|
if(options.sortable) $original.prepend($O);
|
|||
|
} else {
|
|||
|
$ol.append($item);
|
|||
|
if(options.sortable) $original.append($O);
|
|||
|
}
|
|||
|
|
|||
|
addListItemShow($item);
|
|||
|
|
|||
|
disableSelectOption($("[rel=" + optionId + "]", $select));
|
|||
|
|
|||
|
if(!buildingSelect) {
|
|||
|
setHighlight($item, options.highlightAddedLabel);
|
|||
|
selectFirstItem();
|
|||
|
if(options.sortable) $ol.sortable("refresh");
|
|||
|
if(options.fieldset) {
|
|||
|
var itemName = $O.text();
|
|||
|
if(itemName.indexOf('_END') > 0 && itemName.substring(itemName.length - 4) == '_END') {
|
|||
|
$item.addClass('asmFieldset asmFieldsetEnd');
|
|||
|
} else {
|
|||
|
var fieldsetCloseName = itemName + '_END';
|
|||
|
if(typeof fieldsetCloseItems[fieldsetCloseName] != "undefined") {
|
|||
|
$item.addClass('asmFieldset asmFieldsetStart');
|
|||
|
addListItem(fieldsetCloseItems[fieldsetCloseName].attr('id'));
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Reveal an <li> added to the list (called only by addListItem method)
|
|||
|
*
|
|||
|
* This is primarily here to manage the animation of the show, if used.
|
|||
|
*
|
|||
|
* @param $item An <li> element
|
|||
|
*
|
|||
|
*/
|
|||
|
function addListItemShow($item) {
|
|||
|
if(options.animate && !buildingSelect) {
|
|||
|
$item.animate({
|
|||
|
opacity: "show",
|
|||
|
height: "show"
|
|||
|
}, 100, "swing", function() {
|
|||
|
$item.animate({
|
|||
|
height: "+=2px"
|
|||
|
}, 50, "swing", function() {
|
|||
|
$item.animate({
|
|||
|
height: "-=2px"
|
|||
|
}, 25, "swing");
|
|||
|
});
|
|||
|
});
|
|||
|
} else {
|
|||
|
$item.show();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Remove an <li> item from the HTML <ol> list
|
|||
|
*
|
|||
|
* @param optionId The option 'id' attribute that the item represents
|
|||
|
* @param bool highlightItem Highlight the item?
|
|||
|
*
|
|||
|
*/
|
|||
|
function dropListItem(optionId, highlightItem) {
|
|||
|
|
|||
|
var $O = $('#' + optionId);
|
|||
|
|
|||
|
if(options.hideDeleted || !$O.hasClass('asmOriginalSelected')) {
|
|||
|
|
|||
|
if(typeof highlightItem == "undefined") highlightItem = true;
|
|||
|
|
|||
|
// $O.attr('selected', false);
|
|||
|
$O.prop('selected', false);
|
|||
|
$item = $ol.children("li[rel=" + optionId + "]");
|
|||
|
|
|||
|
dropListItemHide($item);
|
|||
|
enableSelectOption($("option[rel=" + optionId + "]"));
|
|||
|
|
|||
|
if(highlightItem) setHighlight($item, options.highlightRemovedLabel);
|
|||
|
|
|||
|
} else {
|
|||
|
// deleted item remains in list, but marked for deletion
|
|||
|
|
|||
|
$item = $ol.children("li[rel=" + optionId + "]");
|
|||
|
var value = $O.attr('value');
|
|||
|
if(value == "undefined") value = $O.text();
|
|||
|
if($item.hasClass(options.deletedClass)) {
|
|||
|
$item.removeClass(options.deletedClass);
|
|||
|
if(options.deletedOpacity != 1.0) $item.css('opacity', 1.0);
|
|||
|
$O.attr('value', value.substring(options.deletedPrepend.length));
|
|||
|
} else {
|
|||
|
$item.addClass(options.deletedClass);
|
|||
|
if(options.deletedOpacity != 1.0) $item.css('opacity', options.deletedOpacity);
|
|||
|
$O.attr('value', options.deletedPrepend + value);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
triggerOriginalChange(optionId, 'drop');
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Helper for dropListItem() method that removes visible $item <li> with optional animation
|
|||
|
*
|
|||
|
* This is primarily here to manage the animation of the remove, if used.
|
|||
|
*
|
|||
|
* @param $item
|
|||
|
*
|
|||
|
*/
|
|||
|
function dropListItemHide($item) {
|
|||
|
if(options.animate && !buildingSelect) {
|
|||
|
$prevItem = $item.prev("li");
|
|||
|
$item.animate({
|
|||
|
opacity: "hide",
|
|||
|
height: "hide"
|
|||
|
}, 100, "linear", function() {
|
|||
|
$prevItem.animate({
|
|||
|
height: "-=2px"
|
|||
|
}, 50, "swing", function() {
|
|||
|
$prevItem.animate({
|
|||
|
height: "+=2px"
|
|||
|
}, 100, "swing");
|
|||
|
});
|
|||
|
$item.remove();
|
|||
|
});
|
|||
|
} else {
|
|||
|
$item.remove();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Set the contents of the highlight area that appears directly after the visible <select>
|
|||
|
*
|
|||
|
* This method makes the highlight text fade in quickly, then fade out.
|
|||
|
* Applicable only if options.highlight is in use.
|
|||
|
*
|
|||
|
* @param $item
|
|||
|
* @param label
|
|||
|
* @param bool remain Should highlight text remain until another option is selected? (default=false)
|
|||
|
*
|
|||
|
*/
|
|||
|
function setHighlight($item, label, remain) {
|
|||
|
|
|||
|
if(!options.highlight) return;
|
|||
|
if(typeof remain == "undefined") remain = false;
|
|||
|
|
|||
|
$select.next("#" + options.highlightClass + index).remove();
|
|||
|
|
|||
|
var $highlight = $(options.highlightTag)
|
|||
|
.hide()
|
|||
|
.addClass(options.highlightClass)
|
|||
|
.attr('id', options.highlightClass + index);
|
|||
|
|
|||
|
if($item) {
|
|||
|
$highlight.html(label + $item.children("." + options.listItemLabelClass).slice(0, 1).text());
|
|||
|
} else {
|
|||
|
$highlight.html(label);
|
|||
|
}
|
|||
|
|
|||
|
$select.after($highlight);
|
|||
|
|
|||
|
if(remain) {
|
|||
|
$highlight.fadeIn('fast');
|
|||
|
$highlightSpan = $highlight;
|
|||
|
} else {
|
|||
|
$highlight.fadeIn("fast", function() {
|
|||
|
setTimeout(function() {
|
|||
|
$highlight.fadeOut("slow", function() {
|
|||
|
$(this).remove();
|
|||
|
});
|
|||
|
}, 50);
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Trigger a change event on the original select[multiple] so that other scripts can see the change
|
|||
|
*
|
|||
|
* @param optionId The 'id' attribute of the <option> element that is applicable to the change
|
|||
|
* @param type The action that occured, one of: 'add', 'drop' or 'sort'
|
|||
|
*
|
|||
|
*/
|
|||
|
function triggerOriginalChange(optionId, type) {
|
|||
|
|
|||
|
ignoreOriginalChangeEvent = true;
|
|||
|
$option = $("#" + optionId);
|
|||
|
|
|||
|
$original.trigger('change', [{
|
|||
|
'option': $option,
|
|||
|
'value': $option.val(),
|
|||
|
'id': optionId,
|
|||
|
'item': $ol.children("[rel=" + optionId + "]"),
|
|||
|
'type': type
|
|||
|
}]);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Called when an edit link is clicked (requires ProcessWire)
|
|||
|
*
|
|||
|
* Handles management of the modal $iframe window for the edit screen.
|
|||
|
* Applicable only if the edit options are in use.
|
|||
|
* Requires ProcessWire’s modal.js script be laoded from the 'JqueryUI' module.
|
|||
|
*
|
|||
|
* @param e
|
|||
|
* @returns {boolean}
|
|||
|
*
|
|||
|
*/
|
|||
|
function clickEditLink(e) {
|
|||
|
|
|||
|
if(!options.editLinkModal) return true;
|
|||
|
|
|||
|
var $asmItem = $(this).parents('.' + options.listItemClass);
|
|||
|
var href = $(this).attr('href');
|
|||
|
var $iframe = pwModalWindow(href, {}, 'medium');
|
|||
|
|
|||
|
$iframe.on('load', function() {
|
|||
|
// slight delay is necessary in jQuery 3.x, otherwise visible buttons found to be not visible
|
|||
|
setTimeout(function() { iframeLoaded(); }, 100);
|
|||
|
});
|
|||
|
|
|||
|
var iframeLoaded = function() {
|
|||
|
|
|||
|
var $icontents = $iframe.contents();
|
|||
|
var buttons = [];
|
|||
|
var buttonCnt = 0;
|
|||
|
|
|||
|
$icontents.find(options.editLinkButtonSelector).each(function(n) {
|
|||
|
|
|||
|
var $button = $(this);
|
|||
|
var label = $button.text();
|
|||
|
var valid = true;
|
|||
|
var secondary = $button.is('.ui-priority-secondary');
|
|||
|
|
|||
|
for(var i = 0; i < buttonCnt; i++) {
|
|||
|
if(label == buttons[i].text) valid = false;
|
|||
|
}
|
|||
|
|
|||
|
if(valid) {
|
|||
|
buttons[buttonCnt] = {
|
|||
|
text: label,
|
|||
|
'class': (secondary ? 'ui-priority-secondary' : ''),
|
|||
|
click: function() {
|
|||
|
if($button.attr('type') == 'submit') {
|
|||
|
$button.trigger('click');
|
|||
|
$asmItem.effect('highlight', {}, 500);
|
|||
|
|
|||
|
var $asmSetStatus = $icontents.find('#' + options.listItemStatusClass); // first try to find by ID
|
|||
|
if($asmSetStatus.length == 0) $asmSetStatus = $icontents.find(':input.' + options.listItemStatusClass); // then by class, if not ID
|
|||
|
if($asmSetStatus.length > 0) $asmItem.find('.' + options.listItemStatusClass).html($asmSetStatus.eq(0).val());
|
|||
|
|
|||
|
var $asmSetDesc = $icontents.find('#' + options.listItemDescClass); // first try to find by ID
|
|||
|
if($asmSetDesc.length == 0) $asmSetDesc = $icontents.find(':input.' + options.listItemDescClass); // then by class, if not ID
|
|||
|
if($asmSetDesc.length > 0) {
|
|||
|
$asmSetDesc = $asmSetDesc.eq(0);
|
|||
|
var asmSetDesc = $('<textarea />').text($asmSetDesc.val()).html();
|
|||
|
var $desc = $asmItem.find('.' + options.listItemDescClass);
|
|||
|
var $descA = $desc.find('a'); // does it have an <a> in there?
|
|||
|
if($descA.length > 0) {
|
|||
|
$descA.html(asmSetDesc);
|
|||
|
} else {
|
|||
|
$desc.html(asmSetDesc);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
$iframe.dialog('close');
|
|||
|
}
|
|||
|
};
|
|||
|
buttonCnt++;
|
|||
|
}
|
|||
|
$button.hide();
|
|||
|
});
|
|||
|
$iframe.setButtons(buttons);
|
|||
|
};
|
|||
|
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* ProcessWire field fieldset indentation setup
|
|||
|
*
|
|||
|
* In ProcessWire asmSelect is often used to manage lists of fields, which can sometimes include fieldset fields.
|
|||
|
* When a fieldset is present, there are <option> elements to represent the beginning and ending of the fieldset,
|
|||
|
* and option elements that appear within are indented to clarify they are in a fieldset.
|
|||
|
*
|
|||
|
* This method is only called if options.fieldset == true;
|
|||
|
*
|
|||
|
*/
|
|||
|
function setupFieldsets() {
|
|||
|
$ol.find('span.asmFieldsetIndent').remove();
|
|||
|
$ol.children('li').children('span.' + options.listItemLabelClass).each(function() {
|
|||
|
var $t = $(this);
|
|||
|
var label = $t.text();
|
|||
|
if(label.substring(label.length-4) != '_END') return;
|
|||
|
label = label.substring(0, label.length-4);
|
|||
|
var $li = $(this).closest('li.' + options.listItemClass);
|
|||
|
$li.addClass('asmFieldset asmFieldsetEnd');
|
|||
|
while(1) {
|
|||
|
$li = $li.prev('li.' + options.listItemClass);
|
|||
|
if($li.length < 1) break;
|
|||
|
var $span = $li.children('span.' + options.listItemLabelClass);
|
|||
|
var label2 = $span.text();
|
|||
|
if(label2 == label) {
|
|||
|
$li.addClass('asmFieldset asmFieldsetStart');
|
|||
|
break;
|
|||
|
}
|
|||
|
$span.prepend($('<span class="asmFieldsetIndent"></span>'));
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Find all options with a name that ends with _END and populate to fieldsetCloseItems
|
|||
|
*
|
|||
|
* @param $select
|
|||
|
*
|
|||
|
*/
|
|||
|
function findFieldsetCloseItems($select) {
|
|||
|
$select.children('option').each(function() {
|
|||
|
var name = $(this).text();
|
|||
|
if(name.indexOf('_END') > 0 && name.substring(name.length - 4) == '_END') {
|
|||
|
fieldsetCloseItems[name] = $(this);
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// initialize for this iteration
|
|||
|
init();
|
|||
|
});
|
|||
|
};
|
|||
|
|
|||
|
})(jQuery);
|