/** * jQuery Harmonia * Replace an (un)ordered list with a form select. * * @author Micky Hulse * @link http://mky.io * @docs https://github.com/mhulse/jquery-harmonia * @copyright Copyright (c) 2014 Micky Hulse. * @license Released under the Apache License, Version 2.0. * @version 1.1.1 * @date 2014/07/20 */ //---------------------------------- // Notes to self: //console.profile('profile foo'); // ... code here ... //console.profileEnd('profile foo'); // ... or: // console.time('timing foo'); // ... code here ... // console.timeEnd('timing foo'); //---------------------------------- ;(function($, window) { /** * Function-level strict mode syntax. * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode */ 'use strict'; //-------------------------------------------------------------------------- // // Local "globals": // //-------------------------------------------------------------------------- /** * Javascript console detection protection. * * @see http://www.paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ */ var console = window.console || { log : $.noop, warn : $.noop }, //---------------------------------- /** * The plugin namespace. */ NS = 'harmonia', //-------------------------------------------------------------------------- // // Defaults/settings: // //-------------------------------------------------------------------------- /** * Public defaults. * * @type { object } */ defaults = { currentPage : false, // Select the current page? Default: `false`. optionDefault : 'Choose ...', // Default option for `<select>`. Default: `Choose ...`. openTab : false, // Open link in new tab? Default is current window. Default: `false`. classSelect : NS + '-select', // Class name for `<select>`; class applied to generated `<select>` element(s). Default: `harmonia-select`. classInit : NS + '-js-enabled', // Target menu; class name applied to instantiated element(s). use : '', // Replacement function to use when adding `<select>` to the DOM. Allowed values are `after`, `append`, `before` (default), `html`, and `prepend`. // Best if set via `data-` attribute options object: idSelect : '', // ID name for `<select>`; default is no ID. elementTarget : '', // Desired location to put the `<select>`; defaults to `before` (see `use` option) the current instantiated element. // Callbacks: onInit : $.noop, // Callback after plugin data initialized. onAfterInit : $.noop, // Callback after plugin initialization. onAddOption : $.noop, // Callback when a new option has been added. onChange : $.noop // Callback when `<select>` changes. }, // defaults //-------------------------------------------------------------------------- // // Public methods: // //-------------------------------------------------------------------------- /** * Methods object. * * @type { object } */ methods = { /** * Init constructor. * * @type { function } * @param { object } options Options object literal. * @this { object.jquery } * @return { object.jquery } Returns target object(s) for chaining purposes. */ init : function(options) { //---------------------------------- // Loop & return each this: //---------------------------------- return this.each(function() { //---------------------------------- // Declare, hoist and initialize: //---------------------------------- var $this = $(this), // Target object. data = $this.data(NS), // Namespace instance data. settings; // Settings object. //---------------------------------- // Data? //---------------------------------- if ( ! data) { //---------------------------------- // Initialize: //---------------------------------- settings = $.extend(true, {}, defaults, $.fn[NS].defaults, options, $this.data(NS + 'Options')); // Recursively merge defaults, options and HTML5 `data-` attribute options. //---------------------------------- // Namespaced instance data: //---------------------------------- $this.data(NS, { init : false, settings : settings, target : $this, matched : false, lis : $this.children('li'), select : $('<select>', { 'class' : settings.classSelect }), element : ((settings.elementTarget) ? $(settings.elementTarget) : ''), use : ((settings.use && (/^(?:after|append|before|html|prepend|text)$/).test(settings.use)) ? settings.use : 'before') // If input is valid method name, use that; otherwise, default to `before` method. }); //---------------------------------- // Easy access: //---------------------------------- data = $this.data(NS); } //---------------------------------- // Data initialization check: //---------------------------------- if ( ! data.init) { //---------------------------------- // Call main: //---------------------------------- _main.call($this, data); } else { //---------------------------------- // Ouch! //---------------------------------- console.warn('jQuery.%s thinks it\'s already initialized on %o.', NS, this); } }); }, // init //---------------------------------- /** * Removes plugin from element. * * @type { function } * @this { object.jquery } * @return { object.jquery } Returns target object(s) for chaining purposes. */ destroy : function() { //---------------------------------- // Loop & return each this: //---------------------------------- return this.each(function() { //---------------------------------- // Declare, hoist and initialize: //---------------------------------- var $this = $(this), data = $this.data(NS); // Get instance data. //---------------------------------- // Data? //---------------------------------- if (data) { //---------------------------------- // Remove root menu CSS class: //---------------------------------- $this.removeClass(data.settings.classInit); //---------------------------------- // Remove generated HTML: //---------------------------------- data.select.remove(); // All bound events and jQuery data associated with the elements are removed: http://api.jquery.com/remove/ //---------------------------------- // Namespaced instance data: //---------------------------------- $this.removeData(NS); } }); } // destroy }, // methods //-------------------------------------------------------------------------- // // Private methods: // //-------------------------------------------------------------------------- /** * Called after plugin initialization. * * @private * @type { function } * @this { object.jquery } */ _main = function(data) { //---------------------------------- // Declare, hoist and initialize: //---------------------------------- var $default; //---------------------------------- // Data? //---------------------------------- if (typeof data == 'undefined') { //---------------------------------- // Attempt to determine data: //---------------------------------- data = this.data(NS); } //---------------------------------- // Data? //---------------------------------- if (data) { //---------------------------------- // Yup! //---------------------------------- data.init = true; // Data initialization flag. //---------------------------------- // Callback: //---------------------------------- data.settings.onInit.call(data.target, data); //---------------------------------- // Check for object(s): //---------------------------------- if (data.lis.length) { //---------------------------------- // Root menu CSS class: //---------------------------------- data.target.addClass(data.settings.classInit); //---------------------------------- // Is there a `<select>` ID? //---------------------------------- if (data.settings.idSelect.length) { //---------------------------------- // Apply to `<select>`: //---------------------------------- data.select.attr('id', data.settings.idSelect); } //---------------------------------- // Default `<select>` `<option>`? //---------------------------------- if (data.settings.optionDefault) { //---------------------------------- // Get/create the `<option>`: //---------------------------------- $default = _optionize.call(data.target, $('<a>'), data.settings.optionDefault); //---------------------------------- // Append `<option>` to `<select>`: //---------------------------------- $default.appendTo(data.select); } //---------------------------------- // Create the other `<option>`s: //---------------------------------- data.lis.each(function() { // http://css-tricks.com/examples/ConvertMenuToDropdown/optgroup.php //---------------------------------- // Declare, hoist and initialize: //---------------------------------- var $this = $(this), $children = $this.children(), // Get immediate children. $lists, $group; //---------------------------------- // Child list to `<optgroup>`? //---------------------------------- if ($children.filter('ul, ol').length) { // Allow for `<ul>` and `<ol>`. //---------------------------------- // Find all child `<li>` items: //---------------------------------- $lists = $children.find('li'); //---------------------------------- // Do we have children `<li>` items? //---------------------------------- if ($lists.length) { //---------------------------------- // Append `<optgroup>`: //---------------------------------- $group = $('<optgroup>', { // Get the first child (should be element like `<a>` or `<span>`): 'label': $children.first().text() // @TODO: Disabled optgroups? }).appendTo(data.select); //---------------------------------- // Append `<optgroup>` `<option>`s: //---------------------------------- $lists.each(function() { //---------------------------------- // Convert `<a>` to `<option>`: //---------------------------------- _appendize.call(data.target, $(this), $group); }); } } else { //---------------------------------- // Convert `<a>` to `<option>`: //---------------------------------- _appendize.call(data.target, $this, data.select); } }); //---------------------------------- // Add change event to `<select>`: //---------------------------------- _changeize.call(data.target); //---------------------------------- // Target element? //---------------------------------- if (data.element.length) { //---------------------------------- // Insert using target el: //---------------------------------- data.element[data.use](data.select); } else { //---------------------------------- // Insert using instantiated el: //---------------------------------- data.target[data.use](data.select); } //---------------------------------- // Callback: //---------------------------------- data.settings.onAfterInit.call(data.target, data); // Done! } else { //---------------------------------- // Problemos: //---------------------------------- console.warn('jQuery.%s thinks there\'s a problem with your markup.', NS); } } }, // _main //---------------------------------- /** * Converts `<a>` to `<option>` and appends to passed element. * * @private * @type { function } * @this { object.jquery } The base target object. * @param { object.jquery } $li List item. * @param { object.jquery } $to Element to append list item to. * @return void */ _appendize = function($li, $to) { //---------------------------------- // Declare, hoist and initialize: //---------------------------------- var data = this.data(NS), // Get instance data. $a = $li.children('a'), // Find child `<a>` item. $option; //---------------------------------- // Do we have children `<a>` items? //---------------------------------- if ($a.length) { //---------------------------------- // Get/create the `<option>`: //---------------------------------- $option = _optionize.call(data.target, $a); //---------------------------------- // Do we have an `<option>`? //---------------------------------- if ($option.length) { //---------------------------------- // Append `<option>` to `<select>`: //---------------------------------- $option.appendTo($to); } } else { //---------------------------------- // Doh! //---------------------------------- console.warn('jQuery.%s can\'t find child hrefs for %o\'s %o.', NS, this, $li); } }, // _appendize //---------------------------------- /** * Create options for the `<select>` menu. * * @private * @type { function } * @this { object.jquery } The base target object. * @param { object.jquery } $a The `<a>` to convert a `<select>`. * @param { string } text Optional text for `<option>`. * @return { * } The `<select>` `<option>` or an empty string. */ _optionize = function($a, text) { //---------------------------------- // Declare, hoist and initialize: //---------------------------------- var $return = '', data = this.data(NS), // Get instance data. $option, selected, link, href, ahref; //---------------------------------- // Data? //---------------------------------- if (data) { //---------------------------------- // Default arg values: //---------------------------------- text = ((typeof text != 'undefined') && text.length) ? text : _textualize.call($a); // Use arg or `<a>` text. //---------------------------------- // Text? //---------------------------------- if (text.length) { //---------------------------------- // Initialize: //---------------------------------- $option = $('<option>'); // Create `<option>` element. selected = false; link = ($a.attr('href') || ''); // Current `<a>`'s href. //---------------------------------- // Avoid junk: //---------------------------------- if (link && (link != '#')) { // @TODO Better checking here? //---------------------------------- // Something currently selected? //---------------------------------- if ( ! data.matched) { //---------------------------------- // Force select via class? //---------------------------------- if ($a.hasClass('selected')) { selected = true; // Yup. Force selected. } else if (data.settings.currentPage) { //---------------------------------- // Get hrefs: //---------------------------------- href = window.location.href.toLowerCase(); ahref = link.toLowerCase(); //---------------------------------- // Compare urls directly or index: //---------------------------------- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf if ((href == ahref) || (href.indexOf(ahref) != -1)) { selected = true; } } //---------------------------------- // Selected!!!!!!!!!!!!!!!!!!!!!!!!! //---------------------------------- if (selected) { //---------------------------------- // Set selected attribute: //---------------------------------- $option.attr('selected', 'selected'); // Pheeeew. :D //---------------------------------- // Set flag and stop checking: //---------------------------------- data.matched = true; // Flippin' switches! } } //---------------------------------- // Location value: //---------------------------------- $option.attr('value', link); } //---------------------------------- // Assign text to `<option>`: //---------------------------------- $return = $option.text($.trim(text)); } else { //---------------------------------- // Oopsies: //---------------------------------- console.warn('jQuery.%s thinks there\'s no text for %o.', NS, this); } } //---------------------------------- // Callback: //---------------------------------- data.settings.onAddOption.call(data.target, data, $return); //---------------------------------- // Return `<option>` or nothing: //---------------------------------- return $return; }, // _optionize //---------------------------------- /** * Get text value of a jQuery object, but not its children. * * @see http://viralpatel.net/blogs/jquery-get-text-element-without-child-element/ * * @private * @type { function } * @this { object.jquery } Any jQuery object. * @return { * } The object's text or an empty string. */ _textualize = function() { return (this.length) ? this.clone().children().remove().end().text() : ''; }, // _textualize //---------------------------------- /** * Bind "change" event handler to `<select>`. * * @private * @type { function } * @this { object.jquery } Any jQuery object. * @return void */ _changeize = function() { //---------------------------------- // Declare, hoist and initialize: //---------------------------------- var data = this.data(NS); //---------------------------------- // Bind "change" event handler: //---------------------------------- data.select.change(function() { //---------------------------------- // Declare, hoist and initialize: //---------------------------------- var $this = $(this), val; //---------------------------------- // Callback: //---------------------------------- data.settings.onChange.call(data.target, data, $this); // @TODO: Is this the best spot for this? //---------------------------------- // Get link value: //---------------------------------- val = $this.val(); //---------------------------------- // Follow link value? //---------------------------------- if (val && (val !== '#')) { // @TODO: Improve link validation? //---------------------------------- // Ignore default `<select>`: //---------------------------------- if (val !== data.settings.optionDefault) { //---------------------------------- // Open tab or use current window: //---------------------------------- if (data.settings.openTab) { window.open(val); // New tab. } else { window.location = val; // Current window. } } } }); }; // _changeize //-------------------------------------------------------------------------- // // Method calling logic: // //-------------------------------------------------------------------------- /** * Boilerplate plugin logic. * * @see http://learn.jquery.com/plugins/ * * @constructor * @type { function } * @param { string } method String method identifier. * @return { method } Calls plugin method with supplied params. */ $.fn[NS] = function(method) { if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if ((typeof method == 'object') || ( ! method)) { return methods.init.apply(this, arguments); } else { $.error('jQuery.%s thinks that %s doesn\'t exist', NS, method); } }; // $.fn[NS] //-------------------------------------------------------------------- /** * Public defaults. * * Example (before instantiation): * * $.fn.harmonia.defaults.idSelect = 'foo'; * * @see http://stackoverflow.com/questions/11306375/plugin-authoring-how-to-allow-myplugin-defaults-key-value * * @type { object } */ $.fn[NS].defaults = defaults; }(jQuery, window));