artabro/wire/modules/Inputfield/InputfieldAsmSelect/asmselect/jquery.asmselect.js
2024-08-27 11:35:37 +02:00

1054 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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">&#8597;</span>', // sortable handle/icon
removeLabel: '<span class="asmIcon asmIconRemove">&times;</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.asmParents 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: '&nbsp;&nbsp; ', // 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 ProcessWires 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);