/** * Uniform * A jQuery plugin to make your form controls look how you want them to. * * @author Josh Pyles * @author Tyler Akins * @author Shahriyar Imanov * * @license MIT * * @see http://opensource.audith.org/uniform */ (function (wind, $, undef) { "use strict"; /** * Use .prop() if jQuery supports it, otherwise fall back to .attr() * @usage All other parameters are passed to jQuery's function * * @param {jQuery} $el jQuery'd element on which we're calling attr/prop * @return {*} The result from jQuery */ function attrOrProp($el /* , args */) { var args = Array.prototype.slice.call(arguments, 1); if ($el.prop) { // jQuery 1.6+ return $el.prop.apply($el, args); } // jQuery 1.5 and below return $el.attr.apply($el, args); } /** * For backwards compatibility with older jQuery libraries, only bind * one thing at a time. Also, this function adds our namespace to * events in one consistent location, shrinking the minified code. * * The properties on the events object are the names of the events * that we are supposed to add to. It can be a space separated list. * The namespace will be added automatically. * * @param {jQuery} $el * @param {Object} options Uniform options for this element * @param {Object} events Events to bind, properties are event names */ function bindMany($el, options, events) { var name, namespaced; for (name in events) { if (events.hasOwnProperty(name)) { namespaced = name.replace(/ |$/g, options.eventNamespace); $el.bind(namespaced, events[name]); } } } /** * Bind the hover, active, focus, and blur UI updates * * @param {jQuery} $el Original element * @param {jQuery} $target Target for the events (our div/span) * @param {Object} options Uniform options for the element $target */ function bindUi($el, $target, options) { bindMany($el, options, { focus: function () { $target.addClass(options.focusClass); }, blur: function () { $target.removeClass(options.focusClass); $target.removeClass(options.activeClass); }, mouseenter: function () { $target.addClass(options.hoverClass); }, mouseleave: function () { $target.removeClass(options.hoverClass); $target.removeClass(options.activeClass); }, "mousedown touchbegin": function () { if (!$el.is(":disabled")) { $target.addClass(options.activeClass); } }, "mouseup touchend": function () { $target.removeClass(options.activeClass); } }); } /** * Remove the hover, focus, active classes. * * @param {jQuery} $el Element with classes * @param {Object} options Uniform options for the element */ function classClearStandard($el, options) { $el.removeClass(options.hoverClass + " " + options.focusClass + " " + options.activeClass); } /** * Add or remove a class, depending on if it's "enabled" * * @param {jQuery} $el Element that has the class added/removed * @param {String} className Class or classes to add/remove * @param {Boolean} enabled True to add the class, false to remove */ function classUpdate($el, className, enabled) { if (enabled) { $el.addClass(className); } else { $el.removeClass(className); } } /** * Updating the "checked" property can be a little tricky. This * changed in jQuery 1.6 and now we can pass booleans to .prop(). * Prior to that, one either adds an attribute ("checked=checked") or * removes the attribute. * * @param {jQuery} $tag Our Uniform span/div * @param {jQuery} $el Original form element * @param {Object} options Uniform options for this element */ function classUpdateChecked($tag, $el, options) { // setTimeout() introduced by #357 setTimeout(function () { var c = "checked", isChecked = $el.is(":" + c); if(!$el.attr("readonly")) { if ($el.prop) { // jQuery 1.6+ $el.prop(c, isChecked); } else { // jQuery 1.5 and below if (isChecked) { $el.attr(c, c); } else { $el.removeAttr(c); } } } classUpdate($tag, options.checkedClass, isChecked); }, 1); } /** * Set or remove the "disabled" class for disabled elements, based on * if the element is detected to be disabled. * * @param {jQuery} $tag Our Uniform span/div * @param {jQuery} $el Original form element * @param {Object} options Uniform options for this element */ function classUpdateDisabled($tag, $el, options) { classUpdate($tag, options.disabledClass, $el.is(":disabled")); } /** * Wrap an element inside of a container or put the container next * to the element. See the code for examples of the different methods. * * Returns the container that was added to the HTML. * * @param {jQuery} $el Element to wrap * @param {jQuery} $container Add this new container around/near $el * @param {String} method One of "after", "before" or "wrap" * @return {jQuery} $container after it has been cloned for adding to $el */ function divSpanWrap($el, $container, method) { switch (method) { case "after": // Result: $el.after($container); return $el.next(); case "before": // Result: $el.before($container); return $el.prev(); case "wrap": // Result: $el.wrap($container); return $el.parent(); } return null; } /** * Create a div/span combo for uniforming an element * * @param {jQuery} $el Element to wrap * @param {Object} options Options for the element, set by the user * @param {Object} divSpanConfig Options for how we wrap the div/span * @return {Object} Contains the div and span as properties */ function divSpan($el, options, divSpanConfig) { var $div, $span, id; if (!divSpanConfig) { divSpanConfig = {}; } divSpanConfig = $.extend({ bind: {}, divClass: null, divWrap: "wrap", spanClass: null, spanHtml: null, spanWrap: "wrap" }, divSpanConfig); $div = $('
'); $span = $(''); // Automatically hide this div/span if the element is hidden. // Do not hide if the element is hidden because a parent is hidden. if (options.autoHide && $el.is(':hidden') && $el.css('display') === 'none') { $div.hide(); } if (divSpanConfig.divClass) { $div.addClass(divSpanConfig.divClass); } if (options.wrapperClass) { $div.addClass(options.wrapperClass); } if (divSpanConfig.spanClass) { $span.addClass(divSpanConfig.spanClass); } id = attrOrProp($el, 'id'); if (options.useID && id) { attrOrProp($div, 'id', options.idPrefix + '-' + id); } if (divSpanConfig.spanHtml) { $span.html(divSpanConfig.spanHtml); } $div = divSpanWrap($el, $div, divSpanConfig.divWrap); $span = divSpanWrap($el, $span, divSpanConfig.spanWrap); classUpdateDisabled($div, $el, options); return { div: $div, span: $span }; } /** * Wrap an element with a span to apply a global wrapper class * * @param {jQuery} $el Element to wrap * @param {Object} options * @return {jQuery} jQuery Wrapper element */ function wrapWithWrapperClass($el, options) { var $span; if (!options.wrapperClass) { return null; } $span = $('').addClass(options.wrapperClass); $span = divSpanWrap($el, $span, "wrap"); return $span; } /** * Test if high contrast mode is enabled. * * In high contrast mode, background images can not be set and * they are always returned as 'none'. * * @return {Boolean} True if in high contrast mode */ function highContrast() { var c, $div, el, rgb; // High contrast mode deals with white and black rgb = 'rgb(120,2,153)'; $div = $('
'); $('body').append($div); el = $div.get(0); // $div.css() will get the style definition, not // the actually displaying style if (wind.getComputedStyle) { c = wind.getComputedStyle(el, '').color; } else { c = (el.currentStyle || el.style || {}).color; } $div.remove(); return c.replace(/ /g, '') !== rgb; } /** * Change text into safe HTML * * @param {String} text * @return {String} HTML version */ function htmlify(text) { if (!text) { return ""; } return $('').text(text).html(); } /** * If not MSIE, return false. * If it is, return the version number. * * @return {Boolean}|{Number} */ function isMsie() { return navigator.cpuClass && !navigator.product; } /** * Return true if this version of IE allows styling * * @return {Boolean} */ function isMsieSevenOrNewer() { return wind.XMLHttpRequest !== undefined; } /** * Check if the element is a multiselect * * @param {jQuery} $el Element * @return {Boolean} true/false */ function isMultiselect($el) { var elSize; if ($el[0].multiple) { return true; } elSize = attrOrProp($el, "size"); return !(!elSize || elSize <= 1); } /** * Meaningless utility function. Used mostly for improving minification. * * @return {Boolean} */ function returnFalse() { return false; } /** * noSelect plugin, very slightly modified * http://mths.be/noselect v1.0.3 * * @param {jQuery} $elem Element that we don't want to select * @param {Object} options Uniform options for the element */ function noSelect($elem, options) { var none = 'none'; bindMany($elem, options, { 'selectstart dragstart mousedown': returnFalse }); $elem.css({ MozUserSelect: none, msUserSelect: none, webkitUserSelect: none, userSelect: none }); } /** * Updates the filename tag based on the value of the real input * element. * * @param {jQuery} $el Actual form element * @param {jQuery} $filenameTag Span/div to update * @param {Object} options Uniform options for this element */ function setFilename($el, $filenameTag, options) { var filenames = $.map($el[0].files, function (file) {return file.name}).join(', '); if (filenames === "") { filenames = options.fileDefaultHtml; } else { filenames = filenames.split(/[\/\\]+/); filenames = filenames[(filenames.length - 1)]; } $filenameTag.text(filenames); } /** * Function from jQuery to swap some CSS values, run a callback, * then restore the CSS. Modified to pass JSLint and handle undefined * values with 'use strict'. * * @param {jQuery} $elements Element * @param {Object} newCss CSS values to swap out * @param {Function} callback Function to run */ function swap($elements, newCss, callback) { var restore, item; restore = []; $elements.each(function () { var name; for (name in newCss) { if (Object.prototype.hasOwnProperty.call(newCss, name)) { restore.push({ el: this, name: name, old: this.style[name] }); this.style[name] = newCss[name]; } } }); callback(); while (restore.length) { item = restore.pop(); item.el.style[item.name] = item.old; } } /** * The browser doesn't provide sizes of elements that are not visible. * This will clone an element and add it to the DOM for calculations. * * @param {jQuery} $el * @param {Function} callback */ function sizingInvisible($el, callback) { var targets; // We wish to target ourselves and any parents as long as // they are not visible targets = $el.parents(); targets.push($el[0]); targets = targets.not(':visible'); swap(targets, { visibility: "hidden", display: "block", position: "absolute" }, callback); } /** * Standard way to unwrap the div/span combination from an element * * @param {jQuery} $el Element that we wish to preserve * @param {Object} options Uniform options for the element * @return {Function} This generated function will perform the given work */ function unwrapUnwrapUnbindFunction($el, options) { return function () { $el.unwrap().unwrap().unbind(options.eventNamespace); }; } var allowStyling = true, // False if IE6 or other unsupported browsers highContrastTest = false, // Was the high contrast test ran? uniformHandlers = [ // Objects that take care of "unification" { // Buttons match: function ($el) { return $el.is("a, button, :submit, :reset, input[type='button']"); }, apply: function ($el, options) { var $div, defaultSpanHtml, ds, getHtml, doingClickEvent; defaultSpanHtml = options.submitDefaultHtml; if ($el.is(":reset")) { defaultSpanHtml = options.resetDefaultHtml; } if ($el.is("a, button")) { // Use the HTML inside the tag getHtml = function () { return $el.html() || defaultSpanHtml; }; } else { // Use the value property of the element getHtml = function () { return htmlify(attrOrProp($el, "value")) || defaultSpanHtml; }; } ds = divSpan($el, options, { divClass: options.buttonClass, spanHtml: getHtml() }); $div = ds.div; bindUi($el, $div, options); doingClickEvent = false; bindMany($div, options, { "click touchend": function () { var ev, res, target, href; if (doingClickEvent) { return false; } if ($el.is(':disabled')) { return false; } doingClickEvent = true; if ($el[0].dispatchEvent) { ev = document.createEvent("MouseEvents"); ev.initEvent("click", true, true); res = $el[0].dispatchEvent(ev); if ($el.is('a') && res) { target = attrOrProp($el, 'target'); href = attrOrProp($el, 'href'); if (!target || target === '_self') { document.location.href = href; } else { wind.open(href, target); } } } else { $el.click(); } doingClickEvent = false; } }); noSelect($div, options); return { remove: function () { // Move $el out $div.after($el); // Remove div and span $div.remove(); // Unbind events $el.unbind(options.eventNamespace); return $el; }, update: function () { classClearStandard($div, options); classUpdateDisabled($div, $el, options); $el.detach(); ds.span.html(getHtml()).append($el); } }; } }, { // Checkboxes match: function ($el) { return $el.is(":checkbox"); }, apply: function ($el, options) { var ds, $div, $span; ds = divSpan($el, options, { divClass: options.checkboxClass }); $div = ds.div; $span = ds.span; // Add focus classes, toggling, active, etc. bindUi($el, $div, options); bindMany($el, options, { "click touchend": function () { classUpdateChecked($span, $el, options); } }); classUpdateChecked($span, $el, options); return { remove: unwrapUnwrapUnbindFunction($el, options), update: function () { classClearStandard($div, options); $span.removeClass(options.checkedClass); classUpdateChecked($span, $el, options); classUpdateDisabled($div, $el, options); } }; } }, { // File selection / uploads match: function ($el) { return $el.is(":file"); }, apply: function ($el, options) { var ds, $div, $filename, $button; // Issue #441: Check if the control supports multiple selection. var multiselect = typeof($el.attr("multiple")) != "undefined"; // The "span" is the button ds = divSpan($el, options, { divClass: options.fileClass, spanClass: options.fileButtonClass, // Issue #441: Choose a display label based on the control supporting multiple selection. spanHtml: multiselect ? options.filesButtonHtml : options.fileButtonHtml, spanWrap: "after" }); $div = ds.div; $button = ds.span; $filename = $("").html(options.fileDefaultHtml); $filename.addClass(options.filenameClass); $filename = divSpanWrap($el, $filename, "after"); // Set the size if (!attrOrProp($el, "size")) { attrOrProp($el, "size", $div.width() / 10); } // Actions function filenameUpdate() { setFilename($el, $filename, options); } bindUi($el, $div, options); // Account for input saved across refreshes filenameUpdate(); // IE7 doesn't fire onChange until blur or second fire. if (isMsie()) { // IE considers browser chrome blocking I/O, so it // suspends tiemouts until after the file has // been selected. bindMany($el, options, { click: function () { $el.trigger("change"); setTimeout(filenameUpdate, 0); } }); } else { // All other browsers behave properly bindMany($el, options, { change: filenameUpdate }); } noSelect($filename, options); noSelect($button, options); return { remove: function () { // Remove filename and button $filename.remove(); $button.remove(); // Unwrap parent div, remove events return $el.unwrap().unbind(options.eventNamespace); }, update: function () { classClearStandard($div, options); setFilename($el, $filename, options); classUpdateDisabled($div, $el, options); } }; } }, { // Input fields (text) match: function ($el) { if ($el.is("input")) { var t = (" " + attrOrProp($el, "type") + " ").toLowerCase(), allowed = " color date datetime datetime-local email month number password search tel text time url week "; return allowed.indexOf(t) >= 0; } return false; }, apply: function ($el, options) { var elType, $wrapper; elType = attrOrProp($el, "type"); $el.addClass(options.inputClass); $wrapper = wrapWithWrapperClass($el, options); bindUi($el, $el, options); if (options.inputAddTypeAsClass) { $el.addClass(elType); } return { remove: function () { $el.removeClass(options.inputClass); if (options.inputAddTypeAsClass) { $el.removeClass(elType); } if ($wrapper) { $el.unwrap(); } }, update: returnFalse }; } }, { // Radio buttons match: function ($el) { return $el.is(":radio"); }, apply: function ($el, options) { var ds, $div, $span; ds = divSpan($el, options, { divClass: options.radioClass }); $div = ds.div; $span = ds.span; // Add classes for focus, handle active, checked bindUi($el, $div, options); bindMany($el, options, { "click touchend": function () { // Fixes #418 - Find all radios with the same name, then update them with $.uniform.update() so the right per-element options are used $el.attr('name') !== undefined ? $.uniform.update($(':radio[name="' + attrOrProp($el, "name") + '"]')) : $.uniform.update($el); } }); classUpdateChecked($span, $el, options); return { remove: unwrapUnwrapUnbindFunction($el, options), update: function () { classClearStandard($div, options); classUpdateChecked($span, $el, options); classUpdateDisabled($div, $el, options); } }; } }, { // Select lists, but do not style multiselects here match: function ($el) { return !!($el.is("select") && !isMultiselect($el)); }, apply: function ($el, options) { var ds, $div, $span, origElemWidth; if (options.selectAutoWidth) { sizingInvisible($el, function () { origElemWidth = $el.width(); }); } ds = divSpan($el, options, { divClass: options.selectClass, spanHtml: ($el.find(":selected:first") || $el.find("option:first")).html(), spanWrap: "before" }); $div = ds.div; $span = ds.span; if (options.selectAutoWidth) { // Use the width of the select and adjust the // span and div accordingly sizingInvisible($el, function () { // Force "display: block" - related to bug #287 swap($([$span[0], $div[0]]), { display: "block" }, function () { var spanPad; spanPad = $span.outerWidth() - $span.width(); $div.width(origElemWidth + spanPad); $span.width(origElemWidth); }); }); } else { // Force the select to fill the size of the div $div.addClass('fixedWidth'); } // Take care of events bindUi($el, $div, options); bindMany($el, options, { change: function () { $span.html($el.find(":selected").html()); $div.removeClass(options.activeClass); }, "click touchend": function () { // IE7 and IE8 may not update the value right // until after click event - issue #238 var selHtml = $el.find(":selected").html(); if ($span.html() !== selHtml) { // Change was detected // Fire the change event on the select tag $el.trigger('change'); } }, keyup: function () { $span.html($el.find(":selected").html()); } }); noSelect($span, options); return { remove: function () { // Remove sibling span $span.remove(); // Unwrap parent div $el.unwrap().unbind(options.eventNamespace); return $el; }, update: function () { if (options.selectAutoWidth) { // Easier to remove and reapply formatting $.uniform.restore($el); $el.uniform(options); } else { classClearStandard($div, options); // Reset current selected text $el[0].selectedIndex = $el[0].selectedIndex; // Force IE to have a ":selected" option (if the field was reset for example) $span.html($el.find(":selected").html()); classUpdateDisabled($div, $el, options); } } }; } }, { // Select lists - multiselect lists only match: function ($el) { return !!($el.is("select") && isMultiselect($el)); }, apply: function ($el, options) { var $wrapper; $el.addClass(options.selectMultiClass); $wrapper = wrapWithWrapperClass($el, options); bindUi($el, $el, options); return { remove: function () { $el.removeClass(options.selectMultiClass); if ($wrapper) { $el.unwrap(); } }, update: returnFalse }; } }, { // Textareas match: function ($el) { return $el.is("textarea"); }, apply: function ($el, options) { var $wrapper; $el.addClass(options.textareaClass); $wrapper = wrapWithWrapperClass($el, options); bindUi($el, $el, options); return { remove: function () { $el.removeClass(options.textareaClass); if ($wrapper) { $el.unwrap(); } }, update: returnFalse }; } } ]; // IE6 can't be styled - can't set opacity on select if (isMsie() && !isMsieSevenOrNewer()) { allowStyling = false; } $.uniform = { // Default options that can be overridden globally or when uniformed // globally: $.uniform.defaults.fileButtonHtml = "Pick A File"; // on uniform: $('input').uniform({fileButtonHtml: "Pick a File"}); defaults: { activeClass: "active", autoHide: true, buttonClass: "button", checkboxClass: "checker", checkedClass: "checked", disabledClass: "disabled", eventNamespace: ".uniform", fileButtonClass: "action", fileButtonHtml: "Choose File", filesButtonHtml: "Choose Files", fileClass: "uploader", fileDefaultHtml: "No file selected", filenameClass: "filename", focusClass: "focus", hoverClass: "hover", idPrefix: "uniform", inputAddTypeAsClass: true, inputClass: "uniform-input", radioClass: "radio", resetDefaultHtml: "Reset", resetSelector: false, // We'll use our own function when you don't specify one selectAutoWidth: true, selectClass: "selector", selectMultiClass: "uniform-multiselect", submitDefaultHtml: "Submit", // Only text allowed textareaClass: "uniform", useID: true, wrapperClass: null }, // All uniformed elements - DOM objects elements: [] }; $.fn.uniform = function (options) { var el = this; options = $.extend({}, $.uniform.defaults, options); // If we are in high contrast mode, do not allow styling if (!highContrastTest) { highContrastTest = true; if (highContrast()) { allowStyling = false; } } // Only uniform on browsers that work if (!allowStyling) { return this; } // Code for specifying a reset button if (options.resetSelector) { $(options.resetSelector).mouseup(function () { wind.setTimeout(function () { $.uniform.update(el); }, 10); }); } return this.each(function () { var $el = $(this), i, handler, callbacks; // Avoid uniforming elements already uniformed - just update if ($el.data("uniformed")) { $.uniform.update($el); return; } // See if we have any handler for this type of element for (i = 0; i < uniformHandlers.length; i = i + 1) { handler = uniformHandlers[i]; if (handler.match($el, options)) { callbacks = handler.apply($el, options); $el.data("uniformed", callbacks); // Store element in our global array $.uniform.elements.push($el.get(0)); return; } } // Could not style this element }); }; $.uniform.restore = $.fn.uniform.restore = function (elem) { if (elem === undef) { elem = $.uniform.elements; } $(elem).each(function () { var $el = $(this), index, elementData; elementData = $el.data("uniformed"); // Skip elements that are not uniformed if (!elementData) { return; } // Unbind events, remove additional markup that was added elementData.remove(); // Remove item from list of uniformed elements index = $.inArray(this, $.uniform.elements); if (index >= 0) { $.uniform.elements.splice(index, 1); } $el.removeData("uniformed"); }); }; $.uniform.update = $.fn.uniform.update = function (elem) { if (elem === undef) { elem = $.uniform.elements; } $(elem).each(function () { var $el = $(this), elementData; elementData = $el.data("uniformed"); // Skip elements that are not uniformed if (!elementData) { return; } elementData.update($el, elementData.options); }); }; }(this, jQuery));