lib/swift-box.js
/*!
* SwiftBox
* A lightweight combobox plugin for jQuery
* https://github.com/Knotix/SwiftBox/
*
* Copyright 2014 Samuel Hodge
* Released under the GPL license
* http://www.gnu.org/licenses/gpl.html
*
* @TODO: Add support for components when they become relevant
*/
(function(context, window, $, undefined) {
'use strict';
// Add SwiftBox to the current context
context.SwiftBox = swiftbox;
// =========================================================================
// Browser Normalization
// =========================================================================
var document = window.document;
var $window = $(window);
var $document = $(window.document);
// Determine how to get an element's text
var textContent = document.textContent !== undefined ? 'textContent' : 'innerText';
// Get the CSS url so shadow DOM can import the stylesheet
var import_style_href = window.swift_box_style_href;
// Feature support detection
var test_element = document.createElement('div');
var test_template = document.createElement('template');
var test_canvas = document.createElement('canvas');
var supports = {
components : false && !!import_style_href && !!document.register &&
(!!test_element.createShadowRoot || !!test_element.webkitCreateShadowRoot),
templates : !!test_template.content,
canvas : !!test_canvas.getContext
};
// Determine what element to use for templating (IE compatibility)
var template_element = supports.templates ? 'template' : 'div';
// Get the context of the canvas if supported
var canvas_context = supports.canvas ? test_canvas.getContext('2d') : null;
// Import rule for shadow DOM
var component_style_import = supports.components ? '<style>@import url(' + import_style_href + ');</style>' : '';
// Array.indexOf for IE8
function indexOf(array, value) {
if(Array.prototype.indexOf) {
return Array.prototype.indexOf.call(array, value);
}
for(var i = 0; i < array.length; ++i) {
var test_value = array[i];
if(value === test_value) {
return i;
}
}
return -1;
}
// =========================================================================
// Common variables
// =========================================================================
// Used to cache element data for performance
var element_cache = [];
// Stores the option arrays
var option_arrays = [];
// Stores a map between value and index for each option array
var option_array_value_maps = [];
// Stores config objects
var config_objects = [];
// Stores the currently active select
var active_select = null;
// Stores all scrollable parents so we can bind to their scroll event
var scrollable_parents = null;
// Stores the current list of filtered options
var filtered_option_array = [];
// Stores the currently highlighted option's index
var highlighted_option_index = null;
// The maximum number of visible options
var max_visible_options = 10;
// RegExp used to escape RegExp special characters
var escape_regex = /([-[\]{}()*+?.,\\^$|#\s])/g;
// RegExp used to remove leading/trailing whitespace
var trim_regexp = /^\s+|\s+$/g;
// RegExp used to remove tags from option text
var tag_regexp = /<[^>]+>/g;
// Hidden input container template
var hidden_input_container = document.createElement('div');
hidden_input_container.className = 'swift-box-hidden-input-container';
// Hidden input template
var hidden_input = document.createElement('input');
hidden_input.className = 'swift-box-hidden-input';
hidden_input.type = 'hidden';
// Shadow root shim
var shadow_root_shim = document.createElement('div');
shadow_root_shim.className = 'swift-box-shadow-root';
// =========================================================================
// Utility functions
// =========================================================================
/**
* Takes a single element, an array of elements, or a jQuery object and
* normalizes it into a predictable array
* @param {Object} element The SwiftBox element
* @return {Object} An array of the passed in elements
*/
function normalizeElementArray(elements) {
if(elements === undefined || elements === null) {
return [];
}
if(elements.length === undefined) {
return [elements];
}
return elements;
}
/**
* Caches elements within a SwiftBox for quicker retrieval
* @param {Object} element The SwiftBox element
* @return {Object} An object containing references to various elements within the shadow DOM
* @todo Implement weakmaps when they are relevant
*/
function getElementCache(element) {
element = normalizeElementArray(element)[0];
if(!element) {
return {};
}
if(element.__swift_box__ === undefined) {
element.__swift_box__ = element_cache.length;
var shadow_root;
if(supports.components) {
shadow_root = element.shadowRoot || element.webkitShadowRoot;
}
else {
shadow_root = element.querySelector('.swift-box-shadow-root');
}
element_cache.push({
container : shadow_root.querySelector('.swift-box'),
text : shadow_root.querySelector('.swift-box-text'),
button : shadow_root.querySelector('.swift-box-button'),
input_container : element.querySelector('.swift-box-hidden-input-container')
});
}
return element_cache[element.__swift_box__];
}
// =========================================================================
// Custom Tags
// =========================================================================
// Register the tags as components if supported
if(supports.components) {
document.register('swift-box', {
prototype: Object.create(HTMLElement.prototype)
});
document.register('swift-box-options', {
prototype: Object.create(HTMLElement.prototype)
});
}
// Otherwise, create the tags for styling compatibilty
else {
document.createElement('swift-box');
document.createElement('swift-box-options');
}
// =========================================================================
// Templates
// =========================================================================
var input_html = [
'<' + template_element + ' class="swift-box-hidden">',
component_style_import,
// An anchor tag is used so it can be tabbed into
'<a href="#" class="swift-box">',
'<div class="swift-box-text"></div>',
'<div class="swift-box-button">▼</div>',
'</a>',
'</' + template_element + '>'
].join('');
var options_html = [
'<' + template_element + ' class="swift-box-hidden">',
component_style_import,
'<div class="swift-box-options swift-box-hidden">',
'<div class="swift-box-option-filter-container">',
'<input class="swift-box-option-filter-input" tabindex="-1" size="1" placeholder="Filter">',
'<div class="swift-box-option-helpers">',
'<div class="swift-box-option-helper swift-box-option-check-all">Check all visible</div>',
'<div class="swift-box-option-helper swift-box-option-clear">Clear selected</div>',
'</div>',
'</div>',
'<div class="swift-box-option-scroll">',
'<div class="swift-box-option-sizer"></div>',
'<div class="swift-box-option-list">',
new Array(max_visible_options + 2).join([
'<div class="swift-box-option">',
'<span class="swift-box-option-state"></span>',
'<span class="swift-box-option-text"></span>',
'</div>'
].join('')),
'</div>',
'</div>',
'<div class="swift-box-option-none">No Options Found</div>',
'</div>',
'</' + template_element + '>'
].join('');
// Convert the templates to elements and append them to the document
var tmp_dom = document.createElement('div');
tmp_dom.innerHTML = input_html;
var input_template = tmp_dom.children[0];
var tmp_dom = document.createElement('div');
tmp_dom.innerHTML = options_html;
var options_template = tmp_dom.children[0];
document.documentElement.appendChild(input_template);
document.documentElement.appendChild(options_template);
// Get the DOM from the template
var input_template_dom = supports.templates ? input_template.content : input_template.children[0];
var options_template_dom = supports.templates ? options_template.content : options_template.children[0];
// =========================================================================
// Option List
// =========================================================================
// Create the option list - Use document.createElement for compatibility
var swift_box_options = document.createElement('swift-box-options');
// Create the shadow root for the option list
var options_shadow_root = createShadowRoot(swift_box_options, options_template_dom);
// Store some references to important option list elements
var option_container = options_shadow_root.querySelector('.swift-box-options');
var filter_input = options_shadow_root.querySelector('.swift-box-option-filter-input');
var option_check_all = options_shadow_root.querySelector('.swift-box-option-check-all');
var option_clear = options_shadow_root.querySelector('.swift-box-option-clear');
var option_scroll = options_shadow_root.querySelector('.swift-box-option-scroll');
var option_sizer = options_shadow_root.querySelector('.swift-box-option-sizer');
var option_list = options_shadow_root.querySelector('.swift-box-option-list');
var option_elements = options_shadow_root.querySelectorAll('.swift-box-option');
var $option_container = $(option_container);
var $filter_input = $(filter_input);
var $option_check_all = $(option_check_all);
var $option_clear = $(option_clear);
var $option_scroll = $(option_scroll);
var $option_list = $(option_list);
var $option_elements = $(option_elements);
// =========================================================================
// Event Handlers
// =========================================================================
// Clicking the option list refocuses the filter input
$option_container.on('mouseup', function() {
filter_input.focus();
});
// Typing within the filter input filters the options
$filter_input.on('keyup', function(e) {
var value = this.value;
var last_value = this.getAttribute('data-swift-box-last-text');
// Determine if the filter text has changed
// Backspace is checked specifically to allow the user to jump to the
// top of the list even if there is no filter text
var filter_changed = value !== last_value || e.which === 8;
if(filter_changed) {
filterOptions(value, true);
this.setAttribute('data-swift-box-last-text', value);
}
});
// Clicking the "check all" button selects all visible options on multi-selects
$option_check_all.on('click', function() {
if(getDisabled(active_select)) {
return;
}
selectAll(active_select, true);
});
// Clicking the clear button clears the selected values
$option_clear.on('click', function() {
if(getDisabled(active_select)) {
return;
}
setSelectedIndexes(active_select, [], true);
});
// Scrolling renders the options
$option_scroll.on('scroll', function() {
renderOptions();
});
// Hovering over an option highlights it
$option_list.on('mouseenter', '.swift-box-option', function() {
var filtered_index = this.getAttribute('data-swift-box-filtered-index');
highlightOption(filtered_index);
});
// Clicking an option selects it
$option_list.on('mouseup', '.swift-box-option', function(e) {
if(e.which !== 1 || getDisabled(active_select)) {
return;
}
// Highlight the option
var filtered_index = this.getAttribute('data-swift-box-filtered-index');
highlightOption(filtered_index);
// Select the option
selectHighlightedOption();
// For single selects, hide the options when one is clicked
if(!getMultiple(active_select)) {
hideOptions(true);
}
});
// Clicking a select toggles the option list
$document.on('click', 'swift-box', function() {
if(this === active_select || getDisabled(this)) {
hideOptions(true);
}
else {
showOptions(this);
}
});
// Add the focus class when focused
$document.on('focusin', 'swift-box', function() {
if(this !== active_select && !getDisabled(this)) {
addFocusClass(this);
}
});
// Remove the focus class when blurred
$document.on('focusout', 'swift-box', function() {
if(this !== active_select) {
removeFocusClass(this);
}
});
// Prevent default behavior when clicking on an anchor tag within the select
$document.on('click', 'swift-box a', function(e) {
e.preventDefault();
});
// Pressing down arrow or letter keys shows the option list
$document.on('keydown keypress', 'swift-box', function(e) {
if(e.ctrlKey || this === active_select || getDisabled(this)) {
return;
}
var which = e.which;
var show = false;
if(e.type === 'keypress') {
show = which >= 32 || which === 8;
}
else {
show = which === 40;
// Because we use an <a> tag to allow tabbing into the select, we need
// to prevent the enter key from triggering a "click" event on it
if(which === 13) {
e.preventDefault();
// If we are not showing the options, submit the parent form
if(!show) {
$(this).closest('form').trigger('submit');
}
}
}
if(show) {
showOptions(this);
e.preventDefault();
if(e.type === 'keypress' && which >= 32) {
var character = String.fromCharCode(which);
filter_input.value = character;
}
}
});
// Arrow keys maneuver through the option list
$filter_input.on('keydown', function(e) {
if(!active_select) {
return;
}
var keyCode = e.which;
if(keyCode === 38 || keyCode === 40) {
var index = highlighted_option_index;
if(keyCode === 38) {
--index;
}
else {
++index;
}
highlightOption(index, true);
// Prevent the text cursor from jumping to home/end
e.preventDefault();
}
});
// Tab/Enter selects the highlighted option
$document.on('keydown', function(e) {
if(!active_select) {
return;
}
var keyCode = e.which;
if(keyCode === 9 || keyCode === 13) {
// Prevent form submissions
if(keyCode === 13) {
e.preventDefault();
}
// In singular mode, tab or enter selects the current option and hides the options
if(!getMultiple(active_select)) {
selectHighlightedOption();
hideOptions(true);
}
// In multiple mode
else {
// Only the enter key selects options
if(keyCode === 13) {
selectHighlightedOption();
}
// The tab key hides options and moves to the next field
else {
hideOptions(true);
focus(active_select);
}
}
}
});
// Escape hides the options
$document.on('keydown', function(e) {
var keyCode = e.which;
if(active_select && keyCode === 27) {
hideOptions(true);
}
});
// Clicking anywhere in the document hides the options
$document.on('mousedown', function(e) {
if(!active_select) {
return;
}
// Make sure the target of the click is not within the option list
if(!$(e.target).closest('swift-box, swift-box-options').length) {
removeFocusClass(active_select);
hideOptions();
}
else {
filter_input.focus();
}
});
// Shim label behavior
$document.on('click', 'label', function() {
var for_target = this['for']; // Bracket notation for IE8 compatibility
var element;
// Get the element the label is linked to
if(for_target) {
element = document.getElementById(for_target);
}
else {
element = this.querySelector('swift-box');
}
if(element && element !== active_select && element.tagName === 'SWIFT-BOX') {
$(element).trigger('click');
}
});
// Shim form reset behavior
$document.on('reset', 'form', function() {
var elements = this.getElementsByTagName('swift-box');
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var index = getMultiple(element) ? null : 0;
setSelectedIndexes(element, index, true);
}
});
// Resizing the window repositions the option list
$window.on('resize', function() {
if(active_select) {
positionOptions();
}
});
// =========================================================================
// Initiliazation
// =========================================================================
/**
* Converts select elements into SwiftBoxes
* @return {Array} An array of SwiftBoxes
*/
function swiftbox(elements, config) {
elements = normalizeElementArray(elements);
var new_elements = [];
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var tag = element.tagName.toLowerCase();
// If the element is already initialized, we're done
if(tag === 'swift-box') {
new_elements.push(element);
continue;
}
// Make sure the element is a select
if(tag !== 'select') {
throw new Error('Invalid element "' + tag + '". Expected "select"');
}
// Create the new element
var new_element = document.createElement('swift-box');
// Append the select template to the new element
createShadowRoot(new_element, input_template_dom);
// Copy all attributes from the select to the new element
var attributes = element.attributes;
for(var j = 0; j < attributes.length; ++j) {
var attribute = attributes[j];
// Classes need to be appended to the new element
if(attribute.name === 'class') {
new_element.className += ' ' + attribute.value;
}
// All other attributes except name are directly copied
else if(attribute.name !== 'name') {
new_element.setAttribute(attribute.name, attribute.value);
}
}
// Store the name property for use later within hidden inputs
new_element.setAttribute('data-swift-box-name', element.name);
// Add a container for hidden inputs
new_element.appendChild(hidden_input_container.cloneNode(true));
// Replace the old element
var parent_node = element.parentNode;
if(parent_node) {
parent_node.insertBefore(new_element, element);
parent_node.removeChild(element);
}
// Extract existing options
var option_array = extractOptionArrayFromSelect(element);
setOptionArray(new_element, option_array, null);
// Set the selected indexes
var selected_indexes = extractSelectedIndexesFromSelect(element);
setSelectedIndexes(new_element, selected_indexes);
new_elements.push(new_element);
}
// Stores the elements needing configuration
var config_elements;
// If a config object was passed, all elements receive the config
if(config) {
config_elements = new_elements;
}
// Otherwise, only non-configured elements receive the config
else {
config_elements = [];
for(var i = 0; i < new_elements.length; ++i) {
var element = new_elements[i];
if(!element.hasAttribute('data-swift-box-config')) {
config_elements.push(element);
}
}
}
// Add the configuration to the new elements
if(config_elements.length) {
config = $.extend(true, {}, defaults, config);
setConfig(config_elements, config);
}
return new_elements;
}
// =========================================================================
// Config Option Manipulation
// =========================================================================
var defaults = {
filter_function: defaultFilterFunction
};
/**
* Merges a set of config options and makes them part of the default
* @param {Object} config A config object
*/
function setDefaultConfig(config) {
$.extend(true, defaults, config);
}
/**
* Sets the configuration object on a select
* @param {Array} elements The SwiftBox elements
* @param {Object} config The configuration object to set
*/
function setConfig(elements, config) {
elements = normalizeElementArray(elements);
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var index = config_objects.length;
var existing_config = getConfig(element);
var new_config = $.extend(true, {}, defaults, existing_config, config);
config_objects.push(new_config);
element.setAttribute('data-swift-box-config', index);
}
}
/**
* Gets the configuration object on a select
* @param {Array} elements The SwiftBox elements
* @return {Object} The configuration object
*/
function getConfig(element) {
element = normalizeElementArray(element)[0];
var index = element && element.getAttribute('data-swift-box-config');
return config_objects[index];
}
/**
* Gets a single configuration option on a select
* @param {Object} element The SwiftBox element
* @param {String} option The option to get
*/
function getConfigOption(element, option) {
var config = getConfig(element);
return config && config[option];
}
// =========================================================================
// Option Array Manipulation
// =========================================================================
/**
* Sets the options on a select
*
* Accepts the following formats:
* {
* 123: 'foo',
* 456: 'bar'
* }
* or
* [
* {value: 123, text: 'foo'},
* {value: 456, text: 'bar'}
* ]
*
* Be aware that some browsers do not maintain key order within objects, so
* the first method may break when the sort_function argument is null
*
* @param {Array} elements The SwiftBox elements
* @param {Array} option_array The options to set
* @param {Array} sort_function A sort function to run on the options. Passing undefined or true will sort the
* options by text. Passing null will maintain the existing order.
* @param {Boolean} remove_duplicates Set to true to remove duplicate values
*/
function setOptionArray(elements, option_array, sort_function, remove_duplicates) {
elements = normalizeElementArray(elements);
// Normalize the option array
var normalized_option_array = normalizeOptionArray(option_array, sort_function, remove_duplicates);
var option_array_index;
if(normalized_option_array.array.length) {
// Check if the option array already exists
option_array_index = findOptionArray(normalized_option_array.array);
// Add the option array if it does not exist
if(option_array_index === -1) {
option_array_index = option_arrays.length;
option_arrays.push(normalized_option_array.array);
option_array_value_maps.push(normalized_option_array.map);
}
}
// Set the option hash on the elements
setOptionHash(elements, option_array_index);
}
/**
* Gets the option array on a select
* @param {Object} element The SwiftBox element
* @return {Array}
*/
function getOptionArray(element) {
var hash = getOptionHash(element);
return option_arrays[hash];
}
/**
* Add options to a select
* @param {Array} elements The SwiftBox elements
* @param {Array} option_array The options to add
* @param {Array} sort_function A sort function to run on the options. Passing undefined or true will sort the
* options by text. Passing null will maintain the existing order.
* @param {Boolean} remove_duplicates Set to true to remove duplicate values
*/
function addOptionArray(elements, option_array, sort_function, remove_duplicates) {
elements = normalizeElementArray(elements);
var normalized_option_array = normalizeOptionArray(option_array);
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var existing_option_array = getOptionArray(element) || [];
var new_option_array = existing_option_array.concat(normalized_option_array.array);
setOptionArray(element, new_option_array, sort_function, remove_duplicates);
}
}
/**
* Removes a list of values from a select's options
* @param {Array} elements The SwiftBox elements
* @param {Array} value_array An array of values to remove
*/
function removeOptionArray(elements, value_array) {
elements = normalizeElementArray(elements);
if(!(value_array instanceof Array)) {
value_array = [value_array];
}
// Convert the values to strings
for(var i = 0; i < value_array.length; ++i) {
value_array[i] = value_array[i] + '';
}
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var existing_option_array = getOptionArray(element);
var new_option_array = [];
if(!existing_option_array) {
return;
}
for(var j = 0; j < existing_option_array.length; ++j) {
var option = existing_option_array[j];
if(value_array.indexOf(option.value) === -1) {
new_option_array.push(option);
}
}
setOptionArray(element, new_option_array);
}
}
/**
* Selects all options on a multi-select(s)
* @param {Array} elements The SwiftBox elements to select all options on
* @param {Boolean} filtered If the SwiftBox is active, only select options that have been filtered
*/
function selectAll(elements, filtered) {
elements = normalizeElementArray(elements);
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
// Make sure the element is a multi-select
if(!getMultiple(element)) {
return;
}
var selected_indexes = getSelectedIndexes(element);
var filtered_only = filtered && element === active_select;
var option_array = filtered_only ? filtered_option_array : getOptionArray(element);
var new_indexes = [];
var index_map = {};
var trigger_change = false;
// Get the currently selected indexes
for(var j = 0; j < selected_indexes.length; ++j) {
var index = selected_indexes[j];
index_map[index] = true;
}
// Check the remaining options
for(var j = 0; j < option_array.length; ++j) {
var option = option_array[j];
var index = option.index;
if(!index_map[index]) {
trigger_change = true;
}
index_map[index] = true;
}
for(var index in index_map) {
new_indexes.push(index);
}
setSelectedIndexes(element, new_indexes, trigger_change);
}
}
/**
* Sets the option hash on SwiftBoxes
* @param {Array} elements The SwiftBox elements
* @param {Number} hash The hash to set
*/
function setOptionHash(elements, hash) {
elements = normalizeElementArray(elements);
var option_array = option_arrays[hash];
if(hash === undefined || hash === null) {
hash = '';
}
else if(!option_array) {
throw new Error('Invalid option hash: ' + hash);
}
// Calculate the width of the options
var option_width = calculateWidth(elements, option_array);
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
// Get any existing values
var values = getValues(element);
// Get any attempted values
var attempted_values = element.getAttribute('data-swift-box-attempted-values');
// Set the new option hash
element.setAttribute('data-swift-box-options', hash);
// If values were found, set them
if(values.length) {
setValues(element, values);
}
// If we previously tried to set values without any options, try again
else if(attempted_values) {
attempted_values = JSON.parse(attempted_values);
setValues(element, attempted_values);
}
// Otherwise, single selects default to the first option
else if(!getMultiple(element)) {
setSelectedIndexes(element, 0);
}
// Set the width of the element to match the options
element.style.width = option_width + 'px';
// Cache the width
element.setAttribute('data-swift-box-width', option_width);
}
}
/**
* Gets the option hash on a SwiftBox
* @param {Array} element The SwiftBox element
* @return {Number} The option hash
*/
function getOptionHash(element) {
element = normalizeElementArray(element)[0];
return element && element.getAttribute('data-swift-box-options');
}
/**
* Converts an array of options into an optimized array for internal use
* @param {Array} option_array The options to add
* @param {Array} sort_function A sort function to run on the options. Passing undefined or true will sort
* the options by text. Passing null will maintain the existing order.
* @param {Boolean} remove_duplicates Set to true to remove duplicate values
* @return {Array} The normalized option array
*/
function normalizeOptionArray(option_array, sort_function, remove_duplicates) {
if(option_array === undefined || option_array === null) {
option_array = [];
}
if(typeof option_array !== 'object') {
throw new Error('Invalid option_array: ' + option_array);
}
var array = [];
var map = {};
var index = 0;
for(var key in option_array) {
if(!option_array.hasOwnProperty(key)) {
continue;
}
var option = option_array[key];
var value;
var text;
if(option !== null && typeof option === 'object') {
value = option.value === undefined ? option.VALUE : option.value;
text = option.text === undefined ? option.TEXT : option.text;
}
else {
value = key;
text = option;
}
if(value === undefined || value === null) {
throw new Error('No value defined for option at index ' + key);
}
if(text === undefined) {
throw new Error('No text defined for option at index ' + key);
}
// Normalize value and text
value = value + '';
text = (text === null ? '' : text) + '';
text = text.replace(trim_regexp, '').replace(tag_regexp + '', '');
var new_option = {
index : index,
value : value,
text : text,
highlight_text : text
};
var existing_index = map[value];
// If we are removing duplicated, overwrite the option if it exists
if(remove_duplicates && existing_index !== undefined) {
array[existing_index] = new_option;
}
// Otherwise add the option to the array
else {
array.push(new_option);
map[value] = index;
++index;
}
}
// Sort the option array if necessary
if(sort_function !== null && sort_function !== false) {
// If undefined or true is passed in as the sort function, use the default sort
if(sort_function === undefined || sort_function === true) {
sort_function = defaultSortFunction;
}
array.sort(sort_function);
// Update the indexes with the new order
for(var i = 0; i < array.length; ++i) {
var option = array[i];
option.index = i;
map[option.value] = i;
}
}
return {
array : array,
map : map
};
}
/**
* Finds an already normalized option array matching a given option array
* @param {Array} option_array The array of options
* @return {Number} The index of the matching option array with the array of normalized option arrays
*/
function findOptionArray(option_array) {
// Yes, a loop label
option_array_loop:
for(var i = 0; i < option_arrays.length; ++i) {
var existing_option_array = option_arrays[i];
if(option_array.length !== existing_option_array.length) {
continue;
}
for(var j = 0; j < option_array.length; ++j) {
var option = option_array[j];
var existing_option = existing_option_array[j];
if(option.value !== existing_option.value || option.text !== existing_option.text) {
continue option_array_loop;
}
}
return i;
}
return -1;
}
/**
* Extracts options from a traditional <select>
* @param {Object} select The select
* @return {Array} An array of options
*/
function extractOptionArrayFromSelect(select) {
var options = select.options;
var result = [];
for(var i = 0; i < options.length; ++i) {
var option = options[i];
result.push({
value: option.value,
text: option.text
});
}
return result;
}
/**
* Extracts selected indexes from a traditional <select>
* @param {Object} select The select
* @return {Array} An array of indexes
*/
function extractSelectedIndexesFromSelect(select) {
var options = select.options;
var result = [];
for(var i = 0; i < options.length; ++i) {
var option = options[i];
if(option.selected) {
result.push(i);
}
}
return result;
}
/**
* Shows the list of options for a select
* @param {Object} element The SwiftBox element
*/
function showOptions(element) {
element = normalizeElementArray(element)[0];
// If the element is already active, we're done
if(active_select === element) {
return;
}
// Ensure the option container is within the body
// This is done here because the body element may not have existed previously
var main_element = document.body || document.documentElement;
if(swift_box_options.parentNode !== main_element) {
main_element.appendChild(swift_box_options);
}
// Remove the focus class on the currently active select
if(active_select) {
removeFocusClass(active_select);
}
// Store this select as the currently active select
active_select = element;
// Clear the filter input
filter_input.value = '';
filter_input.setAttribute('data-swift-box-last-text', '');
// Add the focus class to the select for styling
addFocusClass(element);
// Toggle the multiple class if the current select allows multiple values
$option_container.toggleClass('swift-box-option-multiple', getMultiple(element));
// Size the option list
var sizer_width = Math.max(element.getAttribute('data-swift-box-width'), element.offsetWidth);
option_container.style.minWidth = sizer_width + 'px';
// Show the option list
$option_container.removeClass('swift-box-hidden');
// Reset the filter
filterOptions('');
// Position the option list
positionOptions();
// Highlight the currently selected option
var selected_indexes = getSelectedIndexes(element);
var highlight_index = selected_indexes[0] || 0;
highlightOption(highlight_index, true, true);
// Focus on the filter input
filter_input.focus();
// Bind to each parent's scroll event to reposition the options
// Scroll events do not bubble, so I think this is the only solution
$(scrollable_parents).off('.swift-box-scroll-event');
scrollable_parents = [];
var parent = element;
while((parent = parent.parentNode)) {
if(parent.offsetHeight > parent.scrollHeight || parent.offsetWidth > parent.scrollWidth) {
scrollable_parents.push(parent);
}
}
$(scrollable_parents).on('scroll.swift-box-scroll-event', function() {
positionOptions();
});
$(element).trigger('swiftboxOpen');
}
/**
* Positions the option container appropriately close to the active select
*/
function positionOptions() {
var bounding_rectangle = active_select.getBoundingClientRect();
var window_width = window.innerWidth;
var window_height = window.innerHeight;
var top_edge = bounding_rectangle.bottom;
var left_edge = bounding_rectangle.left;
var right_edge = left_edge + option_container.offsetWidth;
var bottom_edge = top_edge + option_container.offsetHeight;
var top = top_edge;
var right = null;
var bottom = null;
var left = left_edge;
// Save the current scroll position within the options
var scroll_top = option_scroll.scrollTop;
var scroll_left = option_scroll.scrollLeft;
// Prevent the list from going off the page
if(bottom_edge >= window_height) {
top = null;
bottom = window_height - bounding_rectangle.top;
$option_container.addClass('swift-box-options-bottom');
option_container.insertBefore(option_scroll, option_container.children[0]);
}
else {
$option_container.removeClass('swift-box-options-bottom');
option_container.appendChild(option_scroll);
}
if(left <= 0) {
left = 0;
}
else if(right_edge >= window_width) {
right = 0;
left = null;
}
// Position the option list
option_container.style.top = top === null ? 'auto' : top + 'px';
option_container.style.right = right === null ? 'auto' : right + 'px';
option_container.style.bottom = bottom === null ? 'auto' : bottom + 'px';
option_container.style.left = left === null ? 'auto' : left + 'px';
// Restore the scroll position
option_scroll.scrollTop = scroll_top;
option_scroll.scrollLeft = scroll_left;
// Focus on the filter input
filter_input.focus();
}
/**
* Filters the list of options for a select
* @param {String} filter_text The text to filter the options by
*/
function filterOptions(filter_text) {
// Normalize the filter text
if(filter_text === undefined || filter_text === null) {
filter_text = '';
}
filter_text += '';
// Get the options for the active select
var option_array = getOptionArray(active_select) || [];
// Filter only if text was passed in
if(filter_text.length) {
var filter_function = getConfigOption(active_select, 'filter_function');
if(typeof filter_function !== 'function') {
throw new Error('Invalid filter function: ' + filter_function);
}
filtered_option_array = filter_function(filter_text, option_array);
}
// Otherwise, reset the filtered options to the full option array
else {
for(var i = 0; i < option_array.length; ++i) {
var option = option_array[i];
option.highlight_text = option.text;
}
filtered_option_array = option_array;
}
// Show the empty message if no options match the filter
$option_container.toggleClass('swift-box-option-empty', !filtered_option_array.length);
// Get some dimensions
var option_height = getOptionHeight();
var container_max_height = option_height * max_visible_options;
var sizer_height = option_height * filtered_option_array.length;
option_scroll.scrollTop = 0;
option_scroll.scrollLeft = 0;
option_scroll.style.maxHeight = container_max_height + 'px';
option_sizer.style.height = sizer_height + 'px';
// Highlight the first match
highlightOption(0, true, true);
}
/**
* Renders the options for a select, calculating which options to show
* based on the scroll position
* @param {Number} scroll_top The scroll position of the options
*/
function renderOptions(scroll_top) {
// Hide all options initially
$option_elements.addClass('swift-box-hidden');
// If there are no options, we're done
if(!filtered_option_array.length) {
return;
}
// If no scroll position was passed in, use the current position
if(scroll_top === undefined) {
scroll_top = option_scroll.scrollTop;
}
// Otherwise set the scroll position on the element
else {
option_scroll.scrollTop = scroll_top;
}
// In IE8, setting the scrollTop too high results in a rendering bug,
// so snap it to the bottom if needed
scroll_top = Math.min(scroll_top, option_scroll.scrollHeight - option_scroll.offsetHeight);
// Store the height of a single option
var option_height = getOptionHeight();
// Get the currently selected indexes
var selected_indexes = getSelectedIndexes(active_select);
// Calculate the position of the visible options within the scrollable area
option_list.style.top = (scroll_top - (scroll_top % option_height)) + 'px';
// Calculate which options to show based on the scroll position
var offset = Math.max(Math.floor(scroll_top / option_height), 0);
var limit = Math.min(max_visible_options + 1, filtered_option_array.length - offset);
// For each visible option
for(var i = 0; i < limit; ++i) {
var filtered_index = i + offset;
var option = filtered_option_array[filtered_index];
var option_index = option.index;
var option_element = option_elements[i];
option_element.setAttribute('data-swift-box-filtered-index', filtered_index);
option_element.querySelector('.swift-box-option-text').innerHTML = option.highlight_text;
$(option_element)
.removeClass('swift-box-hidden')
.toggleClass('swift-box-option-highlight', filtered_index === highlighted_option_index)
.toggleClass('swift-box-option-selected', indexOf(selected_indexes, option_index) !== -1);
}
}
/**
* Hides the option list
*/
function hideOptions(refocus) {
// Hide the option list
$option_container.addClass('swift-box-hidden');
// Unbind the scroll events
$(scrollable_parents).off('.swift-box-scroll-event');
// Refocus on the
if(refocus && active_select) {
focus(active_select);
}
$(active_select).trigger('swiftboxClose');
active_select = null;
}
/**
* Highlights an option in the option list, scrolling to it if needed
* @param {Number} index The option index to highlight
* @param {Boolean} scroll Set to true to scroll the option into view
* @param {Boolean} top Set to true to scroll the option to the top of the list
*/
function highlightOption(index, scroll, top) {
scroll = scroll || top;
var scroll_height = option_scroll.offsetHeight;
var option_height = getOptionHeight();
index = +index || 0;
index = Math.max(index, 0);
index = Math.min(index, filtered_option_array.length -1);
var scroll_top;
if(scroll) {
scroll_top = option_scroll.scrollTop;
var option_top = index * option_height;
if(option_top < scroll_top) {
scroll_top = option_top;
}
else if(scroll_top + scroll_height <= option_top) {
if(top) {
scroll_top = option_top;
}
else {
scroll_top = option_top - scroll_height + option_height;
}
}
}
highlighted_option_index = index;
renderOptions(scroll_top);
}
/**
* Selects the currently highlighted option and assigns its value to the currently active select
*/
function selectHighlightedOption() {
var option = filtered_option_array[highlighted_option_index];
if(option === undefined) {
return;
}
var index = option.index;
var selected_indexes = index;
// Multi-selects need to toggle the selected option based on if it
// already exists within the selected options or not
if(getMultiple(active_select)) {
var selected_indexes = getSelectedIndexes(active_select);
var exists = indexOf(selected_indexes, index);
// If the option isn't selected, select it
if(exists === -1) {
selected_indexes.push(index);
}
// Otherwise, deselect it
else {
selected_indexes.splice(exists, 1);
}
}
// Set the new selected indexes
setSelectedIndexes(active_select, selected_indexes, true);
}
/**
* Calculates the height of a single option
* Additionally, this forces the all options to have the same height to account for rounding by the browser
* @return {[type]} [description]
*/
function getOptionHeight() {
// Reset the height on the elements
for(var i = 0; i < option_elements.length; ++i) {
var option_element = option_elements[i];
option_element.style.height = '';
option_element.style.lineHeight = '';
}
// Get the height of the first element
var first_option = option_elements[0];
var $first_option = $(first_option);
var hidden = $first_option.hasClass('swift-box-hidden');
$first_option.removeClass('swift-box-hidden');
var height = Math.round(first_option.offsetHeight);
$first_option.toggleClass('swift-box-hidden', hidden);
// Set the height on the elements so they are all uniform.
// This prevents the browser from using relative pixel widths
// that may result in arbitrary rounding during rendering
for(var i = 0; i < option_elements.length; ++i) {
var option_element = option_elements[i];
option_element.style.height = height + 'px';
option_element.style.lineHeight = height + 'px';
}
return height;
}
/**
* Gets the option value map for a select
* @param {Object} element The SwiftBox element
* @return {Object|null} The value map or null if no options are set
*/
function getOptionValueMap(element) {
var hash = getOptionHash(element);
return option_array_value_maps[hash];
}
/**
* Calculates the width of the select based on the widest option.
* In older browsers that don't support canvas, the width is
* approximated, possibly failing miserably.
* @param {Object} element The SwiftBox element
* @param {Array} option_array The array of options
* @return {Number} The calculated width
*/
function calculateWidth(element, option_array) {
element = normalizeElementArray(element)[0];
if(!element || !option_array || !option_array.length) {
return 0;
}
option_array = option_array || [];
var $element = $(element);
var font_size = $element.css('font-size');
var font_family = $element.css('font-family');
// For performance, only compare the longest of the options
var compare_limit = 100;
if(option_array.length > compare_limit) {
var tmp_option_array = option_array.slice(0);
tmp_option_array.sort(lengthSortFunction);
option_array = tmp_option_array.slice(0, compare_limit);
}
// Set the font on the canvas
if(supports.canvas) {
canvas_context.font = font_size + ' ' + font_family;
}
var max_width = 0;
for(var i = 0; i < option_array.length; ++i) {
var option = option_array[i];
var width;
// In modern browsers, we can accurately measure the text using the canvas
if(supports.canvas) {
width = canvas_context.measureText(option.text).width;
}
// In older browsers, use the text length
else {
width = option.text.length;
}
max_width = Math.max(width, max_width);
}
// In older browsers, estimate based on the text length
if(!supports.canvas) {
max_width = Math.max(max_width, 8) * 0.75 * parseFloat(font_size);
}
// Add the button's width
max_width += getElementCache(element).button.offsetWidth;
// Add some extra pixels to account for padding and scrollbars
max_width += 25;
return max_width;
}
function lengthSortFunction(a, b) {
return a.text.length > b.text.length ? -1 : 1;
}
function defaultSortFunction(a, b) {
if(a.text !== b.text) {
// Empty values should appear at the top
if(a.text === '') {
return -1;
}
return a.text < b.text ? -1 : 1;
}
return a.value < b.value ? -1 : 1;
}
/**
* The default filter function used to filter options
* @param {String} needle The needle to search for
* @param {Array} haystacks The array of haystacks to search in
* @return {Array} The matching haystacks
*/
function defaultFilterFunction(needle, haystacks) {
needle = needle.replace(escape_regex, '\\$1');
var results = [];
var regexp = new RegExp('(' + needle + ')', 'gi');
for(var i = 0; i < haystacks.length; ++i) {
var haystack = haystacks[i];
var text = haystack.text;
var matches = text.match(regexp);
if(!matches) {
continue;
}
haystack.highlight_text = text.replace(regexp, '<mark>$1</mark>');
results.push(haystack);
}
return results;
}
/**
* Toggles multi-select mode on SwiftBoxes
* @param {Array} elements The SwiftBox elements
* @param {Boolean} multiple Set to true to enable multi-select mode
*/
function setMultiple(elements, multiple) {
elements = normalizeElementArray(elements);
multiple = !!multiple;
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
if(multiple) {
element.setAttribute('multiple', '');
}
else {
element.removeAttribute('multiple');
var selected_indexes = getSelectedIndexes(element);
setSelectedIndexes(element, selected_indexes[0] || 0);
}
}
}
/**
* Determines if a SwiftBox is in multi-select mode
* @param {Object} element The SwiftBox element
* @return {Boolean}
*/
function getMultiple(element) {
element = normalizeElementArray(element)[0];
return element && element.hasAttribute('multiple');
}
/**
* Toggles disabled state on SwiftBoxes
* @param {Array} elements The SwiftBox elements
* @param {Boolean} multiple Set to true to disable
*/
function setDisabled(elements, disabled) {
elements = normalizeElementArray(elements);
disabled = !!disabled;
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var container_element = getElementCache(element).container;
if(disabled) {
element.setAttribute('disabled', '');
container_element.removeAttribute('href');
}
else {
element.removeAttribute('disabled');
container_element.href = '#';
}
}
}
/**
* Determines if a select is disabled
* @param {Object} element The SwiftBox element
* @return {Boolean}
*/
function getDisabled(element) {
element = normalizeElementArray(element)[0];
return element && element.hasAttribute('disabled');
}
/**
* Toggles disabled state on SwiftBoxes
* @param {Array} elements The SwiftBox elements
* @param {Number} tab_index The tab index to set
*/
function setTabIndex(elements, tab_index) {
elements = normalizeElementArray(elements);
tab_index = tab_index || 0;
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var container_element = getElementCache(element).container;
container_element.tabIndex = tab_index;
}
}
/**
* Determines if a select is disabled
* @param {Object} element The SwiftBox element
* @return {Number}
*/
function getTabIndex(element) {
var container_element = getElementCache(element).container;
return container_element && container_element.tabIndex;
}
// =========================================================================
// Value Manipulation
// =========================================================================
/**
* Gets all selected values of a select
* @param {Object} element The SwiftBox element
* @return {Array}
*/
function getValues(element) {
element = normalizeElementArray(element)[0];
var selected_indexes = getSelectedIndexes(element);
var option_array = getOptionArray(element);
var values = [];
if(!option_array) {
return [];
}
for(var i = 0; i < selected_indexes.length; ++i) {
var index = selected_indexes[i];
var option = option_array[index];
if(option) {
values.push(option.value);
}
}
return values;
}
/**
* Sets the selected values of a select
* @param {Array} elements The SwiftBox elements
* @param {String|Array} indexes A value or array of values to select
* @param {Boolean} trigger_change Set to true to trigger a change event if the values have changed
*/
function setValues(elements, values, trigger_change) {
elements = normalizeElementArray(elements);
if(!(values instanceof Array)) {
values = [values];
}
// Remove undefined/null values
var clean_values = [];
for(var i = 0; i < values.length; ++i) {
var value = values[i];
if(value !== undefined && value !== null) {
clean_values.push(value);
}
}
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var option_value_map = getOptionValueMap(element);
var indexes = [];
// If there are no options, store the attempted values so we can try
// to set them in the future when options are defined
var attempted_values = '';
if(!option_value_map && clean_values.length) {
attempted_values = JSON.stringify(clean_values);
}
element.setAttribute('data-swift-box-attempted-values', attempted_values);
if(option_value_map) {
for(var j = 0; j < clean_values.length; ++j) {
var value = clean_values[j];
var index = option_value_map[value];
if(index !== undefined) {
indexes.push(index);
}
}
}
setSelectedIndexes(element, indexes, trigger_change);
}
}
/**
* Returns if a select has a specific value in its options
* @param {Object} element The SwiftBox element
* @param {String} value The value to check for
* @return {Boolean}
*/
function hasValue(element, value) {
var option_value_map = getOptionValueMap(element);
if(option_value_map && option_value_map[value] !== undefined) {
return true;
}
return false;
}
/**
* Gets all selected indexes of a select
* @param {Object} element The SwiftBox element
* @return {Array}
*/
function getSelectedIndexes(element) {
element = normalizeElementArray(element)[0];
var indexes = [];
var tmp_indexes = element && element.getAttribute('data-swift-box-indexes');
if(tmp_indexes) {
tmp_indexes = tmp_indexes.split(',');
for(var i = 0; i < tmp_indexes.length; ++i) {
var index = +tmp_indexes[i];
if(isNaN(index)) {
continue;
}
indexes.push(index);
}
}
return indexes;
}
/**
* Sets the selected indexes of a select
* @param {Array} elements The SwiftBox elements
* @param {Number|Array} indexes An index or array of indexes to select
* @param {Boolean} trigger_change Set to true to trigger a change event if the indexes have changed
*/
function setSelectedIndexes(elements, indexes, trigger_change) {
elements = normalizeElementArray(elements);
if(indexes === undefined || indexes === null) {
indexes = [];
}
else if(!(indexes instanceof Array)) {
indexes = [indexes];
}
var changed_elements = [];
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var selected_indexes = getSelectedIndexes(element);
var option_array = getOptionArray(element);
var new_indexes = [];
if(option_array) {
for(var j = 0; j < indexes.length; ++j) {
var index = +indexes[j];
if(isNaN(index)) {
continue;
}
var option = option_array[index];
var valid = !!option;
if(valid) {
new_indexes.push(index);
}
}
new_indexes.sort();
}
// Set the new indexes
element.setAttribute('data-swift-box-indexes', new_indexes.join(','));
// Update the text
var text = [];
for(var j = 0; j < new_indexes.length; ++j) {
var index = new_indexes[j];
var option = option_array[index];
if(option) {
text.push(option.text);
}
}
var element_cache = getElementCache(element);
var text_element = element_cache.text;
var new_text = text.join(', ');
text_element[textContent] = new_text;
// Get the hidden input container
var input_container = element_cache.input_container;
// Clear the existing hidden inputs inside the container
var first_child;
while((first_child = input_container.firstChild)) {
input_container.removeChild(first_child);
}
var no_inputs = element.hasAttribute('data-no-inputs');
if(!no_inputs) {
// Update the hidden inputs to contain the new values
var values = getValues(element);
var name = element.getAttribute('data-swift-box-name');
var input_count = values.length;
// Single selects must have an input
if(!input_count && !getMultiple(element)) {
input_count = 1;
}
// Create a hidden input for each value
for(var j = 0; j < input_count; ++j) {
var input = hidden_input.cloneNode(true);
input.name = name;
input.value = values[j] || '';
// Appending must occur AFTER setting the value for the form.reset() shim to work
input_container.appendChild(input);
}
}
// Trigger a change if the indexes have changed
if(trigger_change) {
var changed = new_indexes.length !== selected_indexes.length;
if(!changed) {
for(var j = 0; j < new_indexes.length; ++j) {
if(new_indexes[j] !== selected_indexes[j]) {
changed = true;
break;
}
}
}
if(changed) {
changed_elements.push(element);
}
}
// Re-render the options if the element is active
if(element === active_select) {
filter_input.focus();
renderOptions();
}
}
// Trigger any change events
if(changed_elements.length) {
$(changed_elements).trigger('change');
}
}
/**
* Sets the display text of a select
* @param {Object} element The SwiftBox element
* @param {String} element The text to set
*/
function setText(elements, text) {
var elements = normalizeElementArray(elements);
if(text === undefined || text === null) {
text = '';
}
text += '';
for(var i = 0; i < elements.length; ++i) {
var element = elements[i];
var text_element = getElementCache(element).text;
text_element[textContent] = text;
}
}
/**
* Gets the display text of a select
* @param {Object} element The SwiftBox element
* @return {String}
*/
function getText(element) {
var element = normalizeElementArray(element)[0];
if(!element) {
return;
}
return getElementCache(element).text[textContent];
}
/**
* Gets the text based on the selected values
* @param {Object} element The SwiftBox element
* @return {String|Array} A string or an array of strings if in multiple mode
*/
function getValueText(element) {
var element = normalizeElementArray(element)[0];
if(!element) {
return;
}
var selected_indexes = getSelectedIndexes(element);
var option_array = getOptionArray(element);
var text = [];
for(var i = 0; i < selected_indexes.length; ++i) {
var index = selected_indexes[i];
var option = option_array[index];
if(option) {
text.push(option.text);
}
}
return text;
}
/**
* Focuses a select
* @param {Object} element The SwiftBox element
*/
function focus(element) {
var container_element = getElementCache(element).container;
if(container_element) {
container_element.focus();
}
}
/**
* Blurs a select
* @param {Object} element The SwiftBox element
*/
function blur(element) {
var container_element = getElementCache(element).container;
if(container_element) {
container_element.blur();
}
}
/**
* Marks a select as focused
* @param {Object} element The SwiftBox element
*/
function addFocusClass(element) {
var container_element = getElementCache(element).container;
$(element).addClass('swift-box-focus');
$(container_element).addClass('swift-box-focus');
}
/**
* Unmarks a select as focused
* @param {Object} element The SwiftBox element
*/
function removeFocusClass(element) {
var container_element = getElementCache(element).container;
$(element).removeClass('swift-box-focus');
$(container_element).removeClass('swift-box-focus');
}
// =========================================================================
// Shadow Root Manipulation
// =========================================================================
/**
* Creates a shadow root using a template
* @param {Object} element The SwiftBox element
* @param {Object} template An element to be used as a template
*/
function createShadowRoot(element, template) {
var root;
if(supports.components) {
root = (element.createShadowRoot || element.webkitCreateShadowRoot).call(element);
}
else {
root = shadow_root_shim.cloneNode(true);
element.appendChild(root);
}
// Append the template to the root
root.appendChild(template.cloneNode(true));
return root;
}
// =========================================================================
// Expose methods
// =========================================================================
swiftbox.setDefaultConfig = setDefaultConfig;
swiftbox.config = function(elements, option, value) {
if(arguments.length <= 1) {
return $.extend(true, {}, getConfig(elements));
}
// If option is an object, we must be setting multiple config options at once
if(typeof option === 'object') {
setConfig(elements, option);
}
// If there are only two arguments, we must be getting an option
else if(arguments.length === 2) {
return getConfigOption(elements, option);
}
// Otherwise, we are setting a single option
else {
var config_object = {};
config_object[option] = value;
setConfig(elements, config_object);
}
return elements;
};
swiftbox.options = function(elements) {
if(arguments.length <= 1) {
return $.extend(true, [], getOptionArray(elements));
}
setOptionArray.apply(null, arguments);
return elements;
};
swiftbox.addOptions = function(elements) {
addOptionArray.apply(null, arguments);
return elements;
};
swiftbox.removeOptions = function(elements) {
removeOptionArray.apply(null, arguments);
return elements;
};
swiftbox.selectAll = function(elements) {
selectAll.apply(null, arguments);
return elements;
};
swiftbox.optionHash = function(elements) {
if(arguments.length <= 1) {
return getOptionHash(elements);
}
setOptionHash.apply(null, arguments);
return elements;
};
swiftbox.showOptions = function(elements) {
showOptions.apply(null, arguments);
return elements;
};
swiftbox.filterOptions = function(elements) {
filterOptions.apply(null, arguments);
return elements;
};
swiftbox.hideOptions = function(elements) {
hideOptions();
return elements;
};
swiftbox.value = function(elements) {
if(arguments.length <= 1) {
var values = getValues(elements);
// For single selects, convert the value array to a single value
if(!getMultiple(elements)) {
values = values[0] || '';
}
return values;
}
setValues.apply(null, arguments);
return elements;
};
swiftbox.hasValue = function() {
return hasValue.apply(null, arguments);
};
swiftbox.selectedIndex = function(elements) {
if(arguments.length <= 1) {
var selected_indexes = getSelectedIndexes(elements);
if(!getMultiple(elements)) {
selected_indexes = selected_indexes[0];
if(selected_indexes === undefined) {
selected_indexes = -1;
}
}
return selected_indexes;
}
setSelectedIndexes.apply(null, arguments);
return elements;
};
swiftbox.text = function(elements) {
if(arguments.length <= 1) {
return getText(elements);
}
setText.apply(null, arguments);
return elements;
};
swiftbox.valueText = function(elements) {
var text = getValueText(elements);
if(!getMultiple(elements)) {
return text[0] || '';
}
return text;
};
swiftbox.tabIndex = function(elements) {
if(arguments.length <= 1) {
return getTabIndex(elements);
}
setTabIndex.apply(null, arguments);
return elements;
};
swiftbox.multiple = function(elements) {
if(arguments.length <= 1) {
return getMultiple(elements);
}
setMultiple.apply(null, arguments);
return elements;
};
swiftbox.disabled = function(elements) {
if(arguments.length <= 1) {
return getDisabled(elements);
}
setDisabled.apply(null, arguments);
return elements;
};
swiftbox.focus = function(elements) {
focus.apply(null, arguments);
return elements;
};
swiftbox.blur = function(elements) {
blur.apply(null, arguments);
return elements;
};
/**
* jQuery plugin function
* @return {Object} The jQuery collection the function was called on
*/
$.fn.swiftbox = function() {
var args = Array.prototype.slice.call(arguments, 0);
// Initialize if the first argument is undefined or an object
if(args[0] === undefined || typeof args[0] === 'object') {
args.unshift(this);
return $(swiftbox.apply(null, args));
}
// Determine the method to be called
var method = args.shift();
if(typeof swiftbox[method] !== 'function') {
throw new Error('Invalid SwiftBox method: ' + method);
}
// Add the elements as the first argument
args.unshift($(swiftbox(this)));
// Call the method
return swiftbox[method].apply(null, args);
};
// Extend the :input pseudo-selector
var jquery_input_selector = $.expr[':'].input;
$.expr[':'].input = function(element) {
var $element = $(element);
// If the element matches the original :input selector
if(jquery_input_selector(element)) {
// Make sure the element is not contained within the SwiftBox
return !$element.closest('swift-box').length;
}
return $element.is('swift-box');
};
// Map properties to specific jQuery functions
var jquery_functions = {
value : $.fn.val,
text : $.fn.text
};
// Extend the $.val method
$.fn.val = function() {
return prop(this, 'value', arguments);
};
// Extend the $.text method
$.fn.text = function() {
return prop(this, 'text', arguments);
};
function prop($this, property, args) {
if(!$this.length) {
if(!args.length) {
return undefined;
}
return $this;
}
var jquery_function = jquery_functions[property];
var swiftbox_function = swiftbox[property];
var $elements = args.length ? $this : $this.first();
var $others = $elements.not('swift-box');
var $swiftboxes = $elements.filter('swift-box');
var result;
if($others.length) {
result = jquery_function.apply($others, args);
}
if($swiftboxes.length) {
var swiftbox_args = Array.prototype.slice.call(args, 0);
swiftbox_args.unshift($swiftboxes);
result = swiftbox_function.apply(null, swiftbox_args);
}
return args.length ? $this : result;
}
}(this, window, jQuery));