diff --git a/jquery.fs.selecter.js b/jquery.fs.selecter.js index 69081bc..80b3322 100644 --- a/jquery.fs.selecter.js +++ b/jquery.fs.selecter.js @@ -1,243 +1,235 @@ -/* - * Selecter v3.2.3 - 2014-10-24 - * A jQuery plugin for replacing default select elements. Part of the Formstone Library. - * http://formstone.it/selecter/ - * - * Copyright 2014 Ben Plum; MIT Licensed - */ - -;(function ($, window) { - "use strict"; - - var guid = 0, - userAgent = (window.navigator.userAgent||window.navigator.vendor||window.opera), - isFirefox = /Firefox/i.test(userAgent), - isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(userAgent), - isFirefoxMobile = (isFirefox && isMobile), - $body = null; - - /** - * @options - * @param callback [function] <$.noop> "Select item callback" - * @param cover [boolean] "Cover handle with option set" - * @param customClass [string] <''> "Class applied to instance" - * @param label [string] <''> "Label displayed before selection" - * @param external [boolean] "Open options as links in new window" - * @param links [boolean] "Open options as links in same window" - * @param mobile [boolean] "Force desktop interaction on mobile" - * @param trim [int] <0> "Trim options to specified length; 0 to disable” - */ - var options = { - callback: $.noop, - cover: false, - customClass: "", - label: "", - external: false, - links: false, - mobile: false, - trim: 0 - }; - - var pub = { - - /** - * @method - * @name defaults - * @description Sets default plugin options - * @param opts [object] <{}> "Options object" - * @example $.selecter("defaults", opts); - */ - defaults: function(opts) { - options = $.extend(options, opts || {}); - return $(this); - }, - - /** - * @method - * @name disable - * @description Disables target instance or option - * @param option [string] "Target option value" - * @example $(".target").selecter("disable", "1"); - */ - disable: function(option) { - return $(this).each(function(i, input) { - var data = $(input).parent(".selecter").data("selecter"); - - if (data) { - if (typeof option !== "undefined") { - var index = data.$items.index( data.$items.filter("[data-value=" + option + "]") ); - - data.$items.eq(index).addClass("disabled"); - data.$options.eq(index).prop("disabled", true); - } else { - if (data.$selecter.hasClass("open")) { - data.$selecter.find(".selecter-selected").trigger("click.selecter"); - } - - data.$selecter.addClass("disabled"); - data.$select.prop("disabled", true); - } - } - }); - }, - - /** - * @method - * @name destroy - * @description Removes instance of plugin - * @example $(".target").selecter("destroy"); - */ - destroy: function() { - return $(this).each(function(i, input) { - var data = $(input).parent(".selecter").data("selecter"); - - if (data) { - if (data.$selecter.hasClass("open")) { - data.$selecter.find(".selecter-selected").trigger("click.selecter"); - } - - // Scroller support - if ($.fn.scroller !== undefined) { - data.$selecter.find(".selecter-options").scroller("destroy"); - } - - data.$select[0].tabIndex = data.tabIndex; - - data.$select.find(".selecter-placeholder").remove(); - data.$selected.remove(); - data.$itemsWrapper.remove(); - - data.$selecter.off(".selecter"); - - data.$select.off(".selecter") - .removeClass("selecter-element") - .show() - .unwrap(); - } - }); - }, - - /** - * @method - * @name enable - * @description Enables target instance or option - * @param option [string] "Target option value" - * @example $(".target").selecter("enable", "1"); - */ - enable: function(option) { - return $(this).each(function(i, input) { - var data = $(input).parent(".selecter").data("selecter"); - - if (data) { - if (typeof option !== "undefined") { - var index = data.$items.index( data.$items.filter("[data-value=" + option + "]") ); - data.$items.eq(index).removeClass("disabled"); - data.$options.eq(index).prop("disabled", false); - } else { - data.$selecter.removeClass("disabled"); - data.$select.prop("disabled", false); - } - } - }); - }, - - - /** - * @method private - * @name refresh - * @description DEPRECATED - Updates instance base on target options - * @example $(".target").selecter("refresh"); - */ - refresh: function() { - return pub.update.apply($(this)); - }, - - /** - * @method - * @name update - * @description Updates instance base on target options - * @example $(".target").selecter("update"); - */ - update: function() { - return $(this).each(function(i, input) { - var data = $(input).parent(".selecter").data("selecter"); - - if (data) { - var index = data.index; - - data.$allOptions = data.$select.find("option, optgroup"); - data.$options = data.$allOptions.filter("option"); - data.index = -1; - - index = data.$options.index(data.$options.filter(":selected")); - - _buildOptions(data); - - if (!data.multiple) { - _update(index, data); - } - } - }); - } - }; - - /** - * @method private - * @name _init - * @description Initializes plugin - * @param opts [object] "Initialization options" - */ - function _init(opts) { - // Local options - opts = $.extend({}, options, opts || {}); - - // Check for Body - if ($body === null) { - $body = $("body"); - } - - // Apply to each element - var $items = $(this); - for (var i = 0, count = $items.length; i < count; i++) { - _build($items.eq(i), opts); - } - return $items; - } - - /** - * @method private - * @name _build - * @description Builds each instance - * @param $select [jQuery object] "Target jQuery object" - * @param opts [object] <{}> "Options object" - */ - function _build($select, opts) { - if (!$select.hasClass("selecter-element")) { - // EXTEND OPTIONS - opts = $.extend({}, opts, $select.data("selecter-options")); - - opts.multiple = $select.prop("multiple"); - opts.disabled = $select.is(":disabled"); - - if (opts.external) { - opts.links = true; +;(function ($, window) { + "use strict"; + + var guid = 0, + userAgent = (window.navigator.userAgent||window.navigator.vendor||window.opera), + isFirefox = /Firefox/i.test(userAgent), + isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(userAgent), + isFirefoxMobile = (isFirefox && isMobile), + $body = null; + + /** + * @options + * @param callback [function] <$.noop> "Select item callback" + * @param cover [boolean] "Cover handle with option set" + * @param customClass [string] <''> "Class applied to instance" + * @param label [string] <''> "Label displayed before selection" + * @param external [boolean] "Open options as links in new window" + * @param links [boolean] "Open options as links in same window" + * @param mobile [boolean] "Force desktop interaction on mobile" + * @param trim [int] <0> "Trim options to specified length; 0 to disable” + */ + var options = { + callback: $.noop, + cover: false, + customClass: "", + label: "", + external: false, + links: false, + mobile: false, + trim: 0 + }; + + var pub = { + + /** + * @method + * @name defaults + * @description Sets default plugin options + * @param opts [object] <{}> "Options object" + * @example $.selecter("defaults", opts); + */ + defaults: function(opts) { + options = $.extend(options, opts || {}); + return $(this); + }, + + /** + * @method + * @name disable + * @description Disables target instance or option + * @param option [string] "Target option value" + * @example $(".target").selecter("disable", "1"); + */ + disable: function(option) { + return $(this).each(function(i, input) { + var data = $(input).parent(".selecter").data("selecter"); + + if (data) { + if (typeof option !== "undefined") { + var index = data.$items.index( data.$items.filter("[data-value=" + option + "]") ); + + data.$items.eq(index).addClass("disabled"); + data.$options.eq(index).prop("disabled", true); + } else { + if (data.$selecter.hasClass("open")) { + data.$selecter.find(".selecter-selected").trigger("click.selecter"); + } + + data.$selecter.addClass("disabled"); + data.$select.prop("disabled", true); + } + } + }); + }, + + /** + * @method + * @name destroy + * @description Removes instance of plugin + * @example $(".target").selecter("destroy"); + */ + destroy: function() { + return $(this).each(function(i, input) { + var data = $(input).parent(".selecter").data("selecter"); + + if (data) { + if (data.$selecter.hasClass("open")) { + data.$selecter.find(".selecter-selected").trigger("click.selecter"); + } + + // Scroller support + if ($.fn.scroller !== undefined) { + data.$selecter.find(".selecter-options").scroller("destroy"); + } + + data.$select[0].tabIndex = data.tabIndex; + + data.$select.find(".selecter-placeholder").remove(); + data.$selected.remove(); + data.$itemsWrapper.remove(); + + data.$selecter.off(".selecter"); + + data.$select.off(".selecter") + .removeClass("selecter-element") + .show() + .unwrap(); + } + }); + }, + + /** + * @method + * @name enable + * @description Enables target instance or option + * @param option [string] "Target option value" + * @example $(".target").selecter("enable", "1"); + */ + enable: function(option) { + return $(this).each(function(i, input) { + var data = $(input).parent(".selecter").data("selecter"); + + if (data) { + if (typeof option !== "undefined") { + var index = data.$items.index( data.$items.filter("[data-value=" + option + "]") ); + data.$items.eq(index).removeClass("disabled"); + data.$options.eq(index).prop("disabled", false); + } else { + data.$selecter.removeClass("disabled"); + data.$select.prop("disabled", false); + } + } + }); + }, + + + /** + * @method private + * @name refresh + * @description DEPRECATED - Updates instance base on target options + * @example $(".target").selecter("refresh"); + */ + refresh: function() { + return pub.update.apply($(this)); + }, + + /** + * @method + * @name update + * @description Updates instance base on target options + * @example $(".target").selecter("update"); + */ + update: function() { + return $(this).each(function(i, input) { + var data = $(input).parent(".selecter").data("selecter"); + + if (data) { + var index = data.index; + + data.$allOptions = data.$select.find("option, optgroup"); + data.$options = data.$allOptions.filter("option"); + data.index = -1; + + index = data.$options.index(data.$options.filter(":selected")); + + _buildOptions(data); + + if (!data.multiple) { + _update(index, data); + } + } + }); + } + }; + + /** + * @method private + * @name _init + * @description Initializes plugin + * @param opts [object] "Initialization options" + */ + function _init(opts) { + // Local options + opts = $.extend({}, options, opts || {}); + + // Check for Body + if ($body === null) { + $body = $("body"); + } + + // Apply to each element + var $items = $(this); + for (var i = 0, count = $items.length; i < count; i++) { + _build($items.eq(i), opts); + } + return $items; + } + + /** + * @method private + * @name _build + * @description Builds each instance + * @param $select [jQuery object] "Target jQuery object" + * @param opts [object] <{}> "Options object" + */ + function _build($select, opts) { + if (!$select.hasClass("selecter-element")) { + // EXTEND OPTIONS + opts = $.extend({}, opts, $select.data("selecter-options")); + + opts.multiple = $select.prop("multiple"); + opts.disabled = $select.is(":disabled"); + + if (opts.external) { + opts.links = true; } // Grab true original index, only if selected attribute exits var $originalOption = $select.find("[selected]").not(":disabled"), originalOptionIndex = $select.find("option").index($originalOption); - - if (!opts.multiple && opts.label !== "") { + + if (!opts.multiple && opts.label !== "") { $select.prepend(''); if (originalOptionIndex > -1) { originalOptionIndex++; - } - } else { - opts.label = ""; - } - - // Build options array - var $allOptions = $select.find("option, optgroup"), + } + } else { + opts.label = ""; + } + + // Build options array + var $allOptions = $select.find("option, optgroup"), $options = $allOptions.filter("option"); // If we didn't actually have a selected elemtn @@ -246,606 +238,608 @@ } // Determine original item - var originalIndex = (originalOptionIndex > -1) ? originalOptionIndex : 0, - originalLabel = (opts.label !== "") ? opts.label : $originalOption.text(), - wrapperTag = "div"; - - // Swap tab index, no more interacting with the actual select! - opts.tabIndex = $select[0].tabIndex; - $select[0].tabIndex = -1; - - // Build HTML - var inner = "", - wrapper = ""; - - // Build wrapper - wrapper += '<' + wrapperTag + ' class="selecter ' + opts.customClass; - // Special case classes - if (isMobile) { - wrapper += ' mobile'; - } else if (opts.cover) { - wrapper += ' cover'; - } - if (opts.multiple) { - wrapper += ' multiple'; - } else { - wrapper += ' closed'; - } - if (opts.disabled) { - wrapper += ' disabled'; - } - wrapper += '" tabindex="' + opts.tabIndex + '">'; - wrapper += ''; - - // Build inner - if (!opts.multiple) { - inner += ''; - inner += $('').text( _trim(originalLabel, opts.trim) ).html(); - inner += ''; - } - inner += '
'; - inner += '
'; - - // Modify DOM - $select.addClass("selecter-element") - .wrap(wrapper) - .after(inner); - - // Store plugin data - var $selecter = $select.parent(".selecter"), - data = $.extend({ - $select: $select, - $allOptions: $allOptions, - $options: $options, - $selecter: $selecter, - $selected: $selecter.find(".selecter-selected"), - $itemsWrapper: $selecter.find(".selecter-options"), - index: -1, - guid: guid++ - }, opts); - - _buildOptions(data); - - if (!data.multiple) { - _update(originalIndex, data); - } - - // Scroller support - if ($.fn.scroller !== undefined) { - data.$itemsWrapper.scroller(); - } - - // Bind click events - data.$selecter.on("touchstart.selecter", ".selecter-selected", data, _onTouchStart) - .on("click.selecter", ".selecter-selected", data, _onClick) - .on("click.selecter", ".selecter-item", data, _onSelect) - .on("close.selecter", data, _onClose) - .data("selecter", data); - - // Change events - data.$select.on("change.selecter", data, _onChange); - - // Focus/Blur events - if (!isMobile) { - data.$selecter.on("focusin.selecter", data, _onFocus) - .on("blur.selecter", data, _onBlur); - - // Handle clicks to associated labels - data.$select.on("focusin.selecter", data, function(e) { - e.data.$selecter.trigger("focus"); - }); - } - } - } - - /** - * @method private - * @name _buildOptions - * @description Builds instance's option set - * @param data [object] "Instance data" - */ - function _buildOptions(data) { - var html = '', - itemTag = (data.links) ? "a" : "span", - j = 0; - - for (var i = 0, count = data.$allOptions.length; i < count; i++) { - var $op = data.$allOptions.eq(i); - - // Option group - if ($op[0].tagName === "OPTGROUP") { - html += ''; - } else { - var opVal = $op.val(); - - if (!$op.attr("value")) { - $op.attr("value", opVal); - } - - html += '<' + itemTag + ' class="selecter-item'; - if ($op.hasClass('selecter-placeholder')) { - html += ' placeholder'; - } - // Default selected value - now handles multi's thanks to @kuilkoff - if ($op.is(':selected')) { - html += ' selected'; - } - // Disabled options - if ($op.is(":disabled")) { - html += ' disabled'; - } - html += '" '; - if (data.links) { - html += 'href="' + opVal + '"'; - } else { - html += 'data-value="' + opVal + '"'; - } - html += '>' + $("").text( _trim($op.text(), data.trim) ).html() + ''; - j++; - } - } - - data.$itemsWrapper.html(html); - data.$items = data.$selecter.find(".selecter-item"); - } - - /** - * @method private - * @name _onTouchStart - * @description Handles touchstart to selected item - * @param e [object] "Event data" - */ - function _onTouchStart(e) { - e.stopPropagation(); - - var data = e.data; - - data.touchStartEvent = e.originalEvent; - - data.touchStartX = data.touchStartEvent.touches[0].clientX; - data.touchStartY = data.touchStartEvent.touches[0].clientY; - - data.$selecter.on("touchmove.selecter", ".selecter-selected", data, _onTouchMove) - .on("touchend.selecter", ".selecter-selected", data, _onTouchEnd); - } - - /** - * @method private - * @name _onTouchMove - * @description Handles touchmove to selected item - * @param e [object] "Event data" - */ - function _onTouchMove(e) { - var data = e.data, - oe = e.originalEvent; - - if (Math.abs(oe.touches[0].clientX - data.touchStartX) > 10 || Math.abs(oe.touches[0].clientY - data.touchStartY) > 10) { - data.$selecter.off("touchmove.selecter touchend.selecter"); - } - } - - /** - * @method private - * @name _onTouchEnd - * @description Handles touchend to selected item - * @param e [object] "Event data" - */ - function _onTouchEnd(e) { - var data = e.data; - - data.touchStartEvent.preventDefault(); - - data.$selecter.off("touchmove.selecter touchend.selecter"); - - _onClick(e); - } - - /** - * @method private - * @name _onClick - * @description Handles click to selected item - * @param e [object] "Event data" - */ - function _onClick(e) { - e.preventDefault(); - e.stopPropagation(); - - var data = e.data; - - if (!data.$select.is(":disabled")) { - $(".selecter").not(data.$selecter).trigger("close.selecter", [data]); - - // Handle mobile, but not Firefox, unless desktop forced - if (!data.mobile && isMobile && !isFirefoxMobile) { - var el = data.$select[0]; - if (window.document.createEvent) { // All - var evt = window.document.createEvent("MouseEvents"); - evt.initMouseEvent("mousedown", false, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - el.dispatchEvent(evt); - } else if (el.fireEvent) { // IE - el.fireEvent("onmousedown"); - } - } else { - // Delegate intent - if (data.$selecter.hasClass("closed")) { - _onOpen(e); - } else if (data.$selecter.hasClass("open")) { - _onClose(e); - } - } - } - } - - /** - * @method private - * @name _onOpen - * @description Opens option set - * @param e [object] "Event data" - */ - function _onOpen(e) { - e.preventDefault(); - e.stopPropagation(); - - var data = e.data; - - // Make sure it's not alerady open - if (!data.$selecter.hasClass("open")) { - var offset = data.$selecter.offset(), - bodyHeight = $body.outerHeight(), - optionsHeight = data.$itemsWrapper.outerHeight(true), - selectedOffset = (data.index >= 0) ? data.$items.eq(data.index).position() : { left: 0, top: 0 }; - - // Calculate bottom of document - if (offset.top + optionsHeight > bodyHeight) { - data.$selecter.addClass("bottom"); - } - - data.$itemsWrapper.show(); - - // Bind Events - data.$selecter.removeClass("closed") - .addClass("open"); - $body.on("click.selecter-" + data.guid, ":not(.selecter-options)", data, _onCloseHelper); - - _scrollOptions(data); - } - } - - /** - * @method private - * @name _onCloseHelper - * @description Determines if event target is outside instance before closing - * @param e [object] "Event data" - */ - function _onCloseHelper(e) { - e.preventDefault(); - e.stopPropagation(); - - if ($(e.currentTarget).parents(".selecter").length === 0) { - _onClose(e); - } - } - - /** - * @method private - * @name _onClose - * @description Closes option set - * @param e [object] "Event data" - */ - function _onClose(e) { - e.preventDefault(); - e.stopPropagation(); - - var data = e.data; - - // Make sure it's actually open - if (data.$selecter.hasClass("open")) { - data.$itemsWrapper.hide(); - data.$selecter.removeClass("open bottom") - .addClass("closed"); - - $body.off(".selecter-" + data.guid); - } - } - - /** - * @method private - * @name _onSelect - * @description Handles option select - * @param e [object] "Event data" - */ - function _onSelect(e) { - e.preventDefault(); - e.stopPropagation(); - - var $target = $(this), - data = e.data; - - if (!data.$select.is(":disabled")) { - if (data.$itemsWrapper.is(":visible")) { - // Update - var index = data.$items.index($target); - - if (index !== data.index) { - _update(index, data); - _handleChange(data); - } - } - - if (!data.multiple) { - // Clean up - _onClose(e); - } - } - } - - /** - * @method private - * @name _onChange - * @description Handles external changes - * @param e [object] "Event data" - */ - function _onChange(e, internal) { - var $target = $(this), - data = e.data; - - if (!internal && !data.multiple) { - var index = data.$options.index(data.$options.filter("[value='" + _escape($target.val()) + "']")); - - _update(index, data); - _handleChange(data); - } - } - - /** - * @method private - * @name _onFocus - * @description Handles instance focus - * @param e [object] "Event data" - */ - function _onFocus(e) { - e.preventDefault(); - e.stopPropagation(); - - var data = e.data; - - if (!data.$select.is(":disabled") && !data.multiple) { - data.$selecter.addClass("focus") - .on("keydown.selecter-" + data.guid, data, _onKeypress); - - $(".selecter").not(data.$selecter) - .trigger("close.selecter", [ data ]); - } - } - - /** - * @method private - * @name _onBlur - * @description Handles instance focus - * @param e [object] "Event data" - */ - function _onBlur(e, internal, two) { - e.preventDefault(); - e.stopPropagation(); - - var data = e.data; - - data.$selecter.removeClass("focus") - .off("keydown.selecter-" + data.guid); - - $(".selecter").not(data.$selecter) - .trigger("close.selecter", [ data ]); - } - - /** - * @method private - * @name _onKeypress - * @description Handles instance keypress, once focused - * @param e [object] "Event data" - */ - function _onKeypress(e) { - var data = e.data; - - if (e.keyCode === 13) { - if (data.$selecter.hasClass("open")) { - _onClose(e); - _update(data.index, data); - } - _handleChange(data); - } else if (e.keyCode !== 9 && (!e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey)) { - // Ignore modifiers & tabs - e.preventDefault(); - e.stopPropagation(); - - var total = data.$items.length - 1, - index = (data.index < 0) ? 0 : data.index; - - // Firefox left/right support thanks to Kylemade - if ($.inArray(e.keyCode, (isFirefox) ? [38, 40, 37, 39] : [38, 40]) > -1) { - // Increment / decrement using the arrow keys - index = index + ((e.keyCode === 38 || (isFirefox && e.keyCode === 37)) ? -1 : 1); - - if (index < 0) { - index = 0; - } - if (index > total) { - index = total; - } - } else { - var input = String.fromCharCode(e.keyCode).toUpperCase(), - letter, - i; - - // Search for input from original index - for (i = data.index + 1; i <= total; i++) { - letter = data.$options.eq(i).text().charAt(0).toUpperCase(); - if (letter === input) { - index = i; - break; - } - } - - // If not, start from the beginning - if (index < 0 || index === data.index) { - for (i = 0; i <= total; i++) { - letter = data.$options.eq(i).text().charAt(0).toUpperCase(); - if (letter === input) { - index = i; - break; - } - } - } - } - - // Update - if (index >= 0) { - _update(index, data); - _scrollOptions(data); - } - } - } - - /** - * @method private - * @name _update - * @description Updates instance based on new target index - * @param index [int] "Selected option index" - * @param data [object] "instance data" - */ - function _update(index, data) { - var $item = data.$items.eq(index), - isSelected = $item.hasClass("selected"), - isDisabled = $item.hasClass("disabled"); - - // Check for disabled options - if (!isDisabled) { - if (data.multiple) { - if (isSelected) { - data.$options.eq(index).prop("selected", null); - $item.removeClass("selected"); - } else { - data.$options.eq(index).prop("selected", true); - $item.addClass("selected"); - } - } else if (index > -1 && index < data.$items.length) { - var newLabel = $item.html(), - newValue = $item.data("value"); - - data.$selected.html(newLabel) - .removeClass('placeholder'); - - data.$items.filter(".selected") - .removeClass("selected"); - - data.$select[0].selectedIndex = index; - - $item.addClass("selected"); - data.index = index; - } else if (data.label !== "") { - data.$selected.html(data.label); - } - } - } - - /** - * @method private - * @name _scrollOptions - * @description Scrolls options wrapper to specific option - * @param data [object] "Instance data" - */ - function _scrollOptions(data) { - var $selected = data.$items.eq(data.index), - selectedOffset = (data.index >= 0 && !$selected.hasClass("placeholder")) ? $selected.position() : { left: 0, top: 0 }; - - if ($.fn.scroller !== undefined) { - data.$itemsWrapper.scroller("scroll", (data.$itemsWrapper.find(".scroller-content").scrollTop() + selectedOffset.top), 0) - .scroller("reset"); - } else { - data.$itemsWrapper.scrollTop( data.$itemsWrapper.scrollTop() + selectedOffset.top ); - } - } - - /** - * @method private - * @name _handleChange - * @description Handles change events - * @param data [object] "Instance data" - */ - function _handleChange(data) { - if (data.links) { - _launch(data); - } else { - data.callback.call(data.$selecter, data.$select.val(), data.index); - data.$select.trigger("change", [ true ]); - } - } - - /** - * @method private - * @name _launch - * @description Launches link - * @param data [object] "Instance data" - */ - function _launch(data) { - //var url = (isMobile) ? data.$select.val() : data.$options.filter(":selected").attr("href"); - var url = data.$select.val(); - - if (data.external) { - // Open link in a new tab/window - window.open(url); - } else { - // Open link in same tab/window - window.location.href = url; - } - } - - /** - * @method private - * @name _trim - * @description Trims text, if specified length is greater then 0 - * @param length [int] "Length to trim at" - * @param text [string] "Text to trim" - * @return [string] "Trimmed string" - */ - function _trim(text, length) { - if (length === 0) { - return text; - } else { - if (text.length > length) { - return text.substring(0, length) + "..."; - } else { - return text; - } - } - } - - /** - * @method private - * @name _escape - * @description Escapes text - * @param text [string] "Text to escape" - */ - function _escape(text) { - return (typeof text === "string") ? text.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1') : text; - } - - $.fn.selecter = function(method) { - if (pub[method]) { - return pub[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else if (typeof method === 'object' || !method) { - return _init.apply(this, arguments); - } - return this; - }; - - $.selecter = function(method) { - if (method === "defaults") { - pub.defaults.apply(this, Array.prototype.slice.call(arguments, 1)); - } - }; -})(jQuery, window); + var originalIndex = (originalOptionIndex > -1) ? originalOptionIndex : 0, + originalLabel = (opts.label !== "") ? opts.label : $originalOption.text(), + wrapperTag = "div"; + + // Swap tab index, no more interacting with the actual select! + opts.tabIndex = $select[0].tabIndex; + $select[0].tabIndex = -1; + + // Build HTML + var inner = "", + wrapper = ""; + + // Build wrapper + wrapper += '<' + wrapperTag + ' class="selecter ' + opts.customClass; + // Special case classes + if (isMobile) { + wrapper += ' mobile'; + } else if (opts.cover) { + wrapper += ' cover'; + } + if (opts.multiple) { + wrapper += ' multiple'; + } else { + wrapper += ' closed'; + } + if (opts.disabled) { + wrapper += ' disabled'; + } + wrapper += '" tabindex="' + opts.tabIndex + '">'; + wrapper += ''; + + // Build inner + if (!opts.multiple) { + inner += ''; + inner += $('').text( _trim(originalLabel, opts.trim) ).html(); + inner += ''; + } + inner += '
'; + inner += '
'; + + // Modify DOM + $select.addClass("selecter-element") + .wrap(wrapper) + .after(inner); + + // Store plugin data + var $selecter = $select.parent(".selecter"), + data = $.extend({ + $select: $select, + $allOptions: $allOptions, + $options: $options, + $selecter: $selecter, + $selected: $selecter.find(".selecter-selected"), + $itemsWrapper: $selecter.find(".selecter-options"), + index: -1, + guid: guid++ + }, opts); + + _buildOptions(data); + + if (!data.multiple) { + _update(originalIndex, data); + } + + // Scroller support + if ($.fn.scroller !== undefined) { + data.$itemsWrapper.scroller(); + } + + // Bind click events + data.$selecter.on("touchstart.selecter", ".selecter-selected", data, _onTouchStart) + .on("click.selecter", ".selecter-selected", data, _onClick) + .on("click.selecter", ".selecter-item", data, _onSelect) + .on("close.selecter", data, _onClose) + .data("selecter", data); + + // Change events + data.$select.on("change.selecter", data, _onChange); + + // Focus/Blur events + if (!isMobile) { + data.$selecter.on("focusin.selecter", data, _onFocus) + .on("blur.selecter", data, _onBlur); + + // Handle clicks to associated labels + data.$select.on("focusin.selecter", data, function(e) { + e.data.$selecter.trigger("focus"); + }); + } + } + } + + /** + * @method private + * @name _buildOptions + * @description Builds instance's option set + * @param data [object] "Instance data" + */ + function _buildOptions(data) { + var html = '', + itemTag = (data.links) ? "a" : "span", + j = 0; + + for (var i = 0, count = data.$allOptions.length; i < count; i++) { + var $op = data.$allOptions.eq(i); + + // Option group + if ($op[0].tagName === "OPTGROUP") { + html += ''; + } else { + var opVal = $op.val(); + + if (!$op.attr("value")) { + $op.attr("value", opVal); + } + + html += '<' + itemTag + ' class="selecter-item'; + if ($op.hasClass('selecter-placeholder')) { + html += ' placeholder'; + } + // Default selected value - now handles multi's thanks to @kuilkoff + if ($op.is(':selected')) { + html += ' selected'; + } + // Disabled options + if ($op.is(":disabled")) { + html += ' disabled'; + } + html += '" '; + if (data.links) { + html += 'href="' + opVal + '"'; + } else { + html += 'data-value="' + opVal + '"'; + } + html += '>' + $("").text( _trim($op.text(), data.trim) ).html() + ''; + j++; + } + } + + data.$itemsWrapper.html(html); + data.$items = data.$selecter.find(".selecter-item"); + } + + /** + * @method private + * @name _onTouchStart + * @description Handles touchstart to selected item + * @param e [object] "Event data" + */ + function _onTouchStart(e) { + e.stopPropagation(); + + var data = e.data; + + data.touchStartEvent = e.originalEvent; + + data.touchStartX = data.touchStartEvent.touches[0].clientX; + data.touchStartY = data.touchStartEvent.touches[0].clientY; + + data.$selecter.on("touchmove.selecter", ".selecter-selected", data, _onTouchMove) + .on("touchend.selecter", ".selecter-selected", data, _onTouchEnd); + } + + /** + * @method private + * @name _onTouchMove + * @description Handles touchmove to selected item + * @param e [object] "Event data" + */ + function _onTouchMove(e) { + var data = e.data, + oe = e.originalEvent; + + if (Math.abs(oe.touches[0].clientX - data.touchStartX) > 10 || Math.abs(oe.touches[0].clientY - data.touchStartY) > 10) { + data.$selecter.off("touchmove.selecter touchend.selecter"); + } + } + + /** + * @method private + * @name _onTouchEnd + * @description Handles touchend to selected item + * @param e [object] "Event data" + */ + function _onTouchEnd(e) { + var data = e.data; + + data.touchStartEvent.preventDefault(); + + data.$selecter.off("touchmove.selecter touchend.selecter"); + + _onClick(e); + } + + /** + * @method private + * @name _onClick + * @description Handles click to selected item + * @param e [object] "Event data" + */ + function _onClick(e) { + e.preventDefault(); + e.stopPropagation(); + + var data = e.data; + + if (!data.$select.is(":disabled")) { + $(".selecter").not(data.$selecter).trigger("close.selecter", [data]); + + // Handle mobile, but not Firefox, unless desktop forced + if (!data.mobile && isMobile && !isFirefoxMobile) { + var el = data.$select[0]; + if (window.document.createEvent) { // All + var evt = window.document.createEvent("MouseEvents"); + evt.initMouseEvent("mousedown", false, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + el.dispatchEvent(evt); + } else if (el.fireEvent) { // IE + el.fireEvent("onmousedown"); + } + } else { + // Delegate intent + if (data.$selecter.hasClass("closed")) { + _onOpen(e); + } else if (data.$selecter.hasClass("open")) { + _onClose(e); + } + } + } + } + + /** + * @method private + * @name _onOpen + * @description Opens option set + * @param e [object] "Event data" + */ + function _onOpen(e) { + e.preventDefault(); + e.stopPropagation(); + + var data = e.data; + + // Make sure it's not alerady open + if (!data.$selecter.hasClass("open")) { + var offset = data.$selecter.offset(), + bodyHeight = $body.outerHeight(), + optionsHeight = data.$itemsWrapper.outerHeight(true), + selectedOffset = (data.index >= 0) ? data.$items.eq(data.index).position() : { left: 0, top: 0 }; + + // Calculate bottom of document + if (offset.top + optionsHeight > bodyHeight) { + data.$selecter.addClass("bottom"); + } + + data.$itemsWrapper.show(); + + // Bind Events + data.$selecter.removeClass("closed") + .addClass("open"); + $body.on("click.selecter-" + data.guid, ":not(.selecter-options)", data, _onCloseHelper); + + _scrollOptions(data); + } + } + + /** + * @method private + * @name _onCloseHelper + * @description Determines if event target is outside instance before closing + * @param e [object] "Event data" + */ + function _onCloseHelper(e) { + e.preventDefault(); + e.stopPropagation(); + + if ($(e.currentTarget).parents(".selecter").length === 0) { + _onClose(e); + } + } + + /** + * @method private + * @name _onClose + * @description Closes option set + * @param e [object] "Event data" + */ + function _onClose(e) { + e.preventDefault(); + e.stopPropagation(); + + var data = e.data; + + // Make sure it's actually open + if (data.$selecter.hasClass("open")) { + data.$itemsWrapper.hide(); + data.$selecter.removeClass("open bottom") + .addClass("closed"); + + $body.off(".selecter-" + data.guid); + } + } + + /** + * @method private + * @name _onSelect + * @description Handles option select + * @param e [object] "Event data" + */ + function _onSelect(e) { + e.preventDefault(); + e.stopPropagation(); + + var $target = $(this), + data = e.data; + + if (!data.$select.is(":disabled")) { + if (data.$itemsWrapper.is(":visible")) { + // Update + var index = data.$items.index($target); + + if (index !== data.index) { + _update(index, data); + _handleChange(data); + } + } + + if (!data.multiple) { + // Clean up + _onClose(e); + } + } + } + + /** + * @method private + * @name _onChange + * @description Handles external changes + * @param e [object] "Event data" + */ + function _onChange(e, internal) { + var $target = $(this), + data = e.data; + + if (!internal && !data.multiple) { + var index = data.$options.index(data.$options.filter("[value='" + _escape($target.val()) + "']")); + + _update(index, data); + _handleChange(data); + } + } + + /** + * @method private + * @name _onFocus + * @description Handles instance focus + * @param e [object] "Event data" + */ + function _onFocus(e) { + e.preventDefault(); + e.stopPropagation(); + + var data = e.data; + + if (!data.$select.is(":disabled") && !data.multiple) { + data.$selecter.addClass("focus") + .on("keydown.selecter-" + data.guid, data, _onKeypress); + + $(".selecter").not(data.$selecter) + .trigger("close.selecter", [ data ]); + } + } + + /** + * @method private + * @name _onBlur + * @description Handles instance focus + * @param e [object] "Event data" + */ + function _onBlur(e, internal, two) { + e.preventDefault(); + e.stopPropagation(); + + var data = e.data; + + data.$selecter.removeClass("focus") + .off("keydown.selecter-" + data.guid); + + $(".selecter").not(data.$selecter) + .trigger("close.selecter", [ data ]); + } + + /** + * @method private + * @name _onKeypress + * @description Handles instance keypress, once focused + * @param e [object] "Event data" + */ + function _onKeypress(e) { + var data = e.data; + + if (e.keyCode === 13 || e.keyCode === 9) { + if (data.$selecter.hasClass("open")) { + _onClose(e); + _update(data.index, data); + } + _handleChange(data); + } else if (e.keyCode !== 9 && (!e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey)) { + // Ignore modifiers & tabs + e.preventDefault(); + e.stopPropagation(); + + var total = data.$items.length - 1, + index = (data.index < 0) ? 0 : data.index; + + // Firefox left/right support thanks to Kylemade + if ($.inArray(e.keyCode, (isFirefox) ? [38, 40, 37, 39] : [38, 40]) > -1) { + // Increment / decrement using the arrow keys + index = index + ((e.keyCode === 38 || (isFirefox && e.keyCode === 37)) ? -1 : 1); + + if (index < 0) { + index = 0; + } + if (index > total) { + index = total; + } + } else { + var input = String.fromCharCode(e.keyCode).toUpperCase(), + letter, + i; + + // Search for input from original index + for (i = data.index + 1; i <= total; i++) { + letter = data.$options.eq(i).text().charAt(0).toUpperCase(); + if (letter === input) { + index = i; + break; + } + } + + // If not, start from the beginning + if (index < 0 || index === data.index) { + for (i = 0; i <= total; i++) { + letter = data.$options.eq(i).text().charAt(0).toUpperCase(); + if (letter === input) { + index = i; + break; + } + } + } + } + + // Update + if (index >= 0) { + _update(index, data); + _scrollOptions(data); + //trigger the change event on the select when updating the selecter + data.$select.change() + } + } + } + + /** + * @method private + * @name _update + * @description Updates instance based on new target index + * @param index [int] "Selected option index" + * @param data [object] "instance data" + */ + function _update(index, data) { + var $item = data.$items.eq(index), + isSelected = $item.hasClass("selected"), + isDisabled = $item.hasClass("disabled"); + + // Check for disabled options + if (!isDisabled) { + if (data.multiple) { + if (isSelected) { + data.$options.eq(index).prop("selected", null); + $item.removeClass("selected"); + } else { + data.$options.eq(index).prop("selected", true); + $item.addClass("selected"); + } + } else if (index > -1 && index < data.$items.length) { + var newLabel = $item.html(), + newValue = $item.data("value"); + + data.$selected.html(newLabel) + .removeClass('placeholder'); + + data.$items.filter(".selected") + .removeClass("selected"); + + data.$select[0].selectedIndex = index; + + $item.addClass("selected"); + data.index = index; + } else if (data.label !== "") { + data.$selected.html(data.label); + } + } + } + + /** + * @method private + * @name _scrollOptions + * @description Scrolls options wrapper to specific option + * @param data [object] "Instance data" + */ + function _scrollOptions(data) { + var $selected = data.$items.eq(data.index), + selectedOffset = (data.index >= 0 && !$selected.hasClass("placeholder")) ? $selected.position() : { left: 0, top: 0 }; + + if ($.fn.scroller !== undefined) { + data.$itemsWrapper.scroller("scroll", (data.$itemsWrapper.find(".scroller-content").scrollTop() + selectedOffset.top), 0) + .scroller("reset"); + } else { + data.$itemsWrapper.scrollTop( data.$itemsWrapper.scrollTop() + selectedOffset.top ); + } + } + + /** + * @method private + * @name _handleChange + * @description Handles change events + * @param data [object] "Instance data" + */ + function _handleChange(data) { + if (data.links) { + _launch(data); + } else { + data.callback.call(data.$selecter, data.$select.val(), data.index); + data.$select.trigger("change", [ true ]); + } + } + + /** + * @method private + * @name _launch + * @description Launches link + * @param data [object] "Instance data" + */ + function _launch(data) { + //var url = (isMobile) ? data.$select.val() : data.$options.filter(":selected").attr("href"); + var url = data.$select.val(); + + if (data.external) { + // Open link in a new tab/window + window.open(url); + } else { + // Open link in same tab/window + window.location.href = url; + } + } + + /** + * @method private + * @name _trim + * @description Trims text, if specified length is greater then 0 + * @param length [int] "Length to trim at" + * @param text [string] "Text to trim" + * @return [string] "Trimmed string" + */ + function _trim(text, length) { + if (length === 0) { + return text; + } else { + if (text.length > length) { + return text.substring(0, length) + "..."; + } else { + return text; + } + } + } + + /** + * @method private + * @name _escape + * @description Escapes text + * @param text [string] "Text to escape" + */ + function _escape(text) { + return (typeof text === "string") ? text.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1') : text; + } + + $.fn.selecter = function(method) { + if (pub[method]) { + return pub[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return _init.apply(this, arguments); + } + return this; + }; + + $.selecter = function(method) { + if (method === "defaults") { + pub.defaults.apply(this, Array.prototype.slice.call(arguments, 1)); + } + }; +})(jQuery, window); diff --git a/src/jquery.fs.selecter.js b/src/jquery.fs.selecter.js index 1df4cb9..3475fa1 100644 --- a/src/jquery.fs.selecter.js +++ b/src/jquery.fs.selecter.js @@ -213,16 +213,16 @@ if (opts.external) { opts.links = true; - } - - // Grab true original index, only if selected attribute exits - var $originalOption = $select.find("[selected]").not(":disabled"), - originalOptionIndex = $select.find("option").index($originalOption); + } + + // Grab true original index, only if selected attribute exits + var $originalOption = $select.find("[selected]").not(":disabled"), + originalOptionIndex = $select.find("option").index($originalOption); if (!opts.multiple && opts.label !== "") { - $select.prepend(''); - if (originalOptionIndex > -1) { - originalOptionIndex++; + $select.prepend(''); + if (originalOptionIndex > -1) { + originalOptionIndex++; } } else { opts.label = ""; @@ -230,14 +230,14 @@ // Build options array var $allOptions = $select.find("option, optgroup"), - $options = $allOptions.filter("option"); - - // If we didn't actually have a selected elemtn - if (!$originalOption.length) { - $originalOption = $options.eq(0); - } - - // Determine original item + $options = $allOptions.filter("option"); + + // If we didn't actually have a selected elemtn + if (!$originalOption.length) { + $originalOption = $options.eq(0); + } + + // Determine original item var originalIndex = (originalOptionIndex > -1) ? originalOptionIndex : 0, originalLabel = (opts.label !== "") ? opts.label : $originalOption.text(), wrapperTag = "div"; @@ -643,7 +643,7 @@ function _onKeypress(e) { var data = e.data; - if (e.keyCode === 13) { + if (e.keyCode === 13 || e.keyCode === 9) { if (data.$selecter.hasClass("open")) { _onClose(e); _update(data.index, data); @@ -698,6 +698,8 @@ if (index >= 0) { _update(index, data); _scrollOptions(data); + //trigger the change event on the select when updating the selecter + data.$select.change() } } }