visualselect.js
323 lines
| 1 | /*! |
| 2 | * Visual Select - A jQuery plugin for replacing HTML select element with a visual selection tool. |
| 3 | * |
| 4 | * @version 1.3.0 |
| 5 | * @requires jQuery 1.9+ |
| 6 | * @author Averta [averta.net] |
| 7 | * @package Axiom Framework |
| 8 | * @copyright Copyright © 2017 Averta, all rights reserved |
| 9 | */ |
| 10 | |
| 11 | ;(function ( $, window, document, undefined ) { |
| 12 | |
| 13 | "use strict"; |
| 14 | |
| 15 | // Create the defaults once |
| 16 | var pluginName = 'avertaVisualSelect', |
| 17 | defaults = { |
| 18 | item : 'axi-select-item', // visual select item [class name] |
| 19 | selected : 'axi-selected', // selected item [class name] |
| 20 | caption : 'axi-select-caption', // caption under visual item [class name] |
| 21 | container : 'axi-visual-select', // select items container [class name] |
| 22 | |
| 23 | insertCaption : false, // whether insert captions to visual items |
| 24 | insertSymbol : true, // whether insert symbol to visual items |
| 25 | insertTitleAttr : true, // adds title attribute to visual item |
| 26 | autoHideElement : true, // hide HTML select element after init |
| 27 | imgTest : /\.jpg|\.png|\.gif|.jpeg|\.svg/ // test for image src |
| 28 | }, |
| 29 | |
| 30 | attributesMap = { |
| 31 | 'type' : 'symbolType', |
| 32 | 'title-attr' : 'insertTitleAttr', |
| 33 | 'auto-hide' : 'autoHideElement', |
| 34 | 'caption' : 'insertCaption' |
| 35 | }; |
| 36 | |
| 37 | // The actual plugin constructor |
| 38 | function Plugin( element, options ) { |
| 39 | this.element = element; |
| 40 | this.$element = $(element); |
| 41 | this.options = $.extend( {}, defaults, options) ; |
| 42 | |
| 43 | // read attributes |
| 44 | for ( var key in attributesMap ) { |
| 45 | var value = attributesMap[ key ], |
| 46 | dataAttr = this.$element.data( key ); |
| 47 | |
| 48 | if ( dataAttr === undefined ) { |
| 49 | continue; |
| 50 | } |
| 51 | |
| 52 | this.options[ value ] = dataAttr; |
| 53 | } |
| 54 | |
| 55 | this._defaults = defaults; |
| 56 | this._name = pluginName; |
| 57 | |
| 58 | this.init(); |
| 59 | } |
| 60 | |
| 61 | $.extend(Plugin.prototype, { |
| 62 | |
| 63 | init : function(){ |
| 64 | var self = this, |
| 65 | st = self.options; |
| 66 | |
| 67 | self.multiple = self.$element.attr('multiple') === 'multiple'; |
| 68 | |
| 69 | if ( st.autoHideElement ) { |
| 70 | self.$element.css('display', 'none'); |
| 71 | } |
| 72 | |
| 73 | // generate select items |
| 74 | self.$selectCont = $('<div class="' + st.container + '"></div>').insertAfter(self.$element); |
| 75 | self.generate(); |
| 76 | |
| 77 | self.$element.on( 'change', this.update.bind( this ) ); |
| 78 | |
| 79 | }, |
| 80 | |
| 81 | /** |
| 82 | * on visual select item clicked |
| 83 | * @private |
| 84 | * @param {jQuery Event} event |
| 85 | */ |
| 86 | _onItemClick : function (event) { |
| 87 | var $visualItem = $(event.currentTarget), |
| 88 | $selectOption = $visualItem.data('selectOption'), |
| 89 | st = this.options; |
| 90 | |
| 91 | if ( this.multiple ) { |
| 92 | |
| 93 | if ( $visualItem.hasClass(st.selected) ) { |
| 94 | $visualItem.removeClass(st.selected); |
| 95 | $selectOption.removeAttr('selected'); |
| 96 | } else { |
| 97 | $visualItem.addClass(st.selected); |
| 98 | $selectOption.attr('selected', 'selected'); |
| 99 | |
| 100 | var val = this.$element.val(); |
| 101 | if ( val === null ) { |
| 102 | val = []; |
| 103 | } |
| 104 | |
| 105 | val.push( $selectOption.attr( 'value' ) ); |
| 106 | |
| 107 | this.$element.val( val ); |
| 108 | } |
| 109 | |
| 110 | } else if ( !$visualItem.hasClass(st.selected) ) { |
| 111 | |
| 112 | $visualItem.addClass(st.selected); |
| 113 | $selectOption.attr('selected', 'selected'); |
| 114 | this.$element.val( $selectOption.attr( 'value' ) ); |
| 115 | |
| 116 | if ( this.$selectedItem ) { |
| 117 | this.$selectedItem.removeClass(st.selected); |
| 118 | this.$selectedItem.data('selectOption').removeAttr('selected'); |
| 119 | } |
| 120 | |
| 121 | this.$selectedItem = $visualItem; |
| 122 | } |
| 123 | |
| 124 | this._internalTrigger = true; |
| 125 | this.$element.trigger('change'); |
| 126 | |
| 127 | }, |
| 128 | |
| 129 | /** |
| 130 | * Generates video element sources by parsing the data-video-src attribute on element |
| 131 | */ |
| 132 | _generateVideoSource: function( videoSrc ) { |
| 133 | var source = ''; |
| 134 | videoSrc.split( ',' ).forEach( function( src ) { |
| 135 | src = src.split( ' ' ); |
| 136 | source += '<source src="' + src[0] + '" type="video/' + src[1] + '">'; |
| 137 | } ); |
| 138 | |
| 139 | return source; |
| 140 | }, |
| 141 | |
| 142 | /** |
| 143 | * On video ready to play |
| 144 | */ |
| 145 | _videoInit: function( event ) { |
| 146 | $(event.currentTarget).on( 'mouseenter', function() { |
| 147 | this.play(); |
| 148 | }).on( 'mouseleave', function() { |
| 149 | this.pause(); |
| 150 | this.currentTime = 0; |
| 151 | }); |
| 152 | }, |
| 153 | |
| 154 | /** |
| 155 | * updates selected items in visual form |
| 156 | */ |
| 157 | update: function() { |
| 158 | if ( this._internalTrigger ) { |
| 159 | this._internalTrigger = false; |
| 160 | return; |
| 161 | } |
| 162 | |
| 163 | var self = this, |
| 164 | st = this.options, |
| 165 | $items = self.$selectCont.find( '.' + st.item ), |
| 166 | val = self.$element.val(); |
| 167 | |
| 168 | self.$element.find( 'option' ).each( function( index, option ) { |
| 169 | var $option = $(option), |
| 170 | $visualItem = $items.eq( index ); |
| 171 | if ( val.indexOf( $option.val() ) !== -1 ) { |
| 172 | self.$selectedItem = $visualItem.addClass( st.selected ); |
| 173 | } else { |
| 174 | $visualItem.removeClass( st.selected ); |
| 175 | } |
| 176 | |
| 177 | } ); |
| 178 | }, |
| 179 | /** |
| 180 | * Add new item in visual form |
| 181 | */ |
| 182 | addItem: function ( value, text ) { |
| 183 | var self = this, |
| 184 | $lastOption = self.$element.find('option').last(), |
| 185 | $newOption = $lastOption.clone(); |
| 186 | |
| 187 | if ( !$newOption.length ) { |
| 188 | $newOption = $('<option></option>'); |
| 189 | self.$element.val( value ); |
| 190 | } |
| 191 | |
| 192 | $newOption.attr( 'value', value ); |
| 193 | $newOption.text( text ); |
| 194 | $newOption.appendTo( self.$element ); |
| 195 | self.$element.val( value ); |
| 196 | this.generate(true); |
| 197 | this.update(); |
| 198 | }, |
| 199 | /** |
| 200 | * create visual items from HTML select element |
| 201 | * @param {boolean} reset Remove old visual items [it's useful for updating visual select] |
| 202 | * @public |
| 203 | */ |
| 204 | generate : function (reset) { |
| 205 | var self = this, |
| 206 | st = self.options; |
| 207 | |
| 208 | if ( reset ) { |
| 209 | this.$selectCont.find('.' + st.item).remove(); |
| 210 | } |
| 211 | |
| 212 | self.$element.find('option').each(function(){ |
| 213 | var $selectOption = $(this), |
| 214 | $visualItem = $('<div class="' + st.item + '"></div>'), |
| 215 | symbol = $selectOption.data('symbol'), |
| 216 | videoSrc = $selectOption.data('video-src'), |
| 217 | caption = $selectOption.html(), |
| 218 | cssClass = $selectOption.data('class'); |
| 219 | |
| 220 | if ( cssClass ) { |
| 221 | $visualItem.addClass(cssClass); |
| 222 | } |
| 223 | |
| 224 | // insert visual symbol to select item |
| 225 | |
| 226 | if ( st.insertSymbol ) { |
| 227 | if ( videoSrc ) { |
| 228 | $visualItem.attr('item-type', 'video'); |
| 229 | var $videoElement = $('<video></video>').attr( 'muted', '' ).attr( 'loop', '' ) |
| 230 | .append( self._generateVideoSource( videoSrc ) ) |
| 231 | .appendTo( $visualItem ); |
| 232 | |
| 233 | $videoElement[0].addEventListener( 'loadedmetadata', self._videoInit ); |
| 234 | } else if ( st.imgTest.test( symbol ) || $selectOption.data( 'type' ) === 'image' ) { |
| 235 | $('<img/>').attr('src', symbol) |
| 236 | .attr('alt', caption) |
| 237 | .appendTo($visualItem); |
| 238 | } else { |
| 239 | $('<span></span>').addClass(symbol) |
| 240 | .appendTo($visualItem); |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | // insert caption |
| 245 | if ( st.insertCaption ) { |
| 246 | $('<span class="' + st.caption + '">' + caption + '</span>').appendTo($visualItem); |
| 247 | } |
| 248 | |
| 249 | $visualItem.on('click', self._onItemClick.bind( self )) |
| 250 | .data('selectOption', $selectOption) |
| 251 | .appendTo(self.$selectCont); |
| 252 | |
| 253 | if ( st.insertTitleAttr ) { |
| 254 | $visualItem.attr('title', caption); |
| 255 | } |
| 256 | |
| 257 | if ( $selectOption.attr('selected') === 'selected' ) { |
| 258 | self.$selectedItem = $visualItem.addClass(st.selected); |
| 259 | } |
| 260 | |
| 261 | }); |
| 262 | } |
| 263 | |
| 264 | }); |
| 265 | |
| 266 | |
| 267 | $.fn[pluginName] = function ( options ) { |
| 268 | var args = arguments; |
| 269 | |
| 270 | // Is the first parameter an object (options), or was omitted, |
| 271 | // instantiate a new instance of the plugin. |
| 272 | if (options === undefined || typeof options === 'object') { |
| 273 | return this.each(function () { |
| 274 | |
| 275 | // Only allow the plugin to be instantiated once, |
| 276 | // so we check that the element has no plugin instantiation yet |
| 277 | if (!$.data(this, 'plugin_' + pluginName)) { |
| 278 | |
| 279 | // if it has no instance, create a new one, |
| 280 | // pass options to our plugin constructor, |
| 281 | // and store the plugin instance |
| 282 | // in the elements jQuery data object. |
| 283 | $.data(this, 'plugin_' + pluginName, new Plugin( this, options )); |
| 284 | } |
| 285 | }); |
| 286 | |
| 287 | // If the first parameter is a string and it doesn't start |
| 288 | // with an underscore or "contains" the `init`-function, |
| 289 | // treat this as a call to a public method. |
| 290 | } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { |
| 291 | |
| 292 | // Cache the method call |
| 293 | // to make it possible |
| 294 | // to return a value |
| 295 | var returns; |
| 296 | |
| 297 | this.each(function () { |
| 298 | var instance = $.data(this, 'plugin_' + pluginName); |
| 299 | |
| 300 | // Tests that there's already a plugin-instance |
| 301 | // and checks that the requested public method exists |
| 302 | if (instance instanceof Plugin && typeof instance[options] === 'function') { |
| 303 | |
| 304 | // Call the method of our plugin instance, |
| 305 | // and pass it the supplied arguments. |
| 306 | returns = instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) ); |
| 307 | } |
| 308 | |
| 309 | // Allow instances to be destroyed via the 'destroy' method |
| 310 | if (options === 'destroy') { |
| 311 | $.data(this, 'plugin_' + pluginName, null); |
| 312 | } |
| 313 | }); |
| 314 | |
| 315 | // If the earlier cached method |
| 316 | // gives a value back return the value, |
| 317 | // otherwise return this to preserve chainability. |
| 318 | return returns !== undefined ? returns : this; |
| 319 | } |
| 320 | } |
| 321 | |
| 322 | }(jQuery, window, document)); |
| 323 |