Button = Switch.extend( function( base ){ var defaults = { on: 'click', /** * The time in milliseconds in which to throttle events. * Events will only be triggred once per throttle time. * This is useful when timing complex css transitions. * @property throttle * @type {Number} */ throttle: 100, /** * By default the default is prevented setting this to true * allows the hash to be updated and urls to resolve * @property preventDefault * @type {Number} */ preventDefault: true }, root = 'lu/Button/decorators/', decorators = { first: root + 'first', last: root + 'last', load: root + 'load', next: root + 'next', pause: root + 'pause', play: root + 'play', previous: root + 'previous', select: root + 'select', state: root + 'state', def: root + 'default' }; /** * Used for binding the space bar to the * Button's 'on' event as specified in the * configuration. * @method bindSpaceBar * @private */ function bindSpaceBar( instance, on ){ instance.$element.on( 'keyup', function( event ){ if( event.keyCode === 32 ){ instance.trigger( on ); } } ); } return { /** * Class constructor * @method initialize * @public * @param {Object} $element JQuery object for the element wrapped by the component * @param {Object} settings Configuration settings */ init: function( $element, settings ){ var self = this, action, requirements = [], decorator; _.defaults( settings, defaults ); base.init.call( this, $element, settings ); action = settings.action; if( action !== undefined ){ switch( action ){ case 'first': requirements.push( decorators.first ); requirements.push( decorators.def ); break; case 'last': requirements.push( decorators.last ); requirements.push( decorators.def ); break; case 'load': requirements.push( decorators.load ); requirements.push( decorators.def ); break; case 'next': requirements.push( decorators.next ); requirements.push( decorators.def ); break; case 'pause': requirements.push( decorators.pause ); requirements.push( decorators.def ); break; case 'play': requirements.push( decorators.play ); requirements.push( decorators.def ); break; case 'previous': requirements.push( decorators.previous ); requirements.push( decorators.def ); break; case 'select': requirements.push( decorators.select ); break; case 'state': requirements.push( decorators.state ); break; default: throw new Error( 'Button decorator "' + action + '" does not exist!' ); } } else { requirements.push( decorators.def ); } require.ensure( requirements, function( require, module, exports ){ _.each( requirements, function( decorator, index ){ decorator = require( decorator )( settings ); Fiber.decorate( self, decorator ); self.decorators.push(decorator); } ); self.trigger( 'dependencies-resolved' ); } ); //binds the space-bar to the on event bindSpaceBar( this, settings.on ); /** * Gets the url for the button -- either from the config setting or from the HREF * @method getUrl * @public * @return {String} The URL for the button */ self.getUrl = function() { return settings.url || $element.attr('href'); }; }, /** * Adds a disabled state to the Button * as well as adding the prop disabled if * it is a button or input element. * @method disable * @public * @return {Object} The Button instance */ disable: function(){ var $element = this.$element; this.addState( constants.states.DISABLED ); if( $element.is( constants.HAS_A18_ATTRS ) ){ $element.prop( constants.DISABLED, true ); } return this; }, /** * Removes the disabled state from the Button * as well the prop disabled if * it is a button or input element. * @method enable * @public * @return {Object} The Button instance */ enable: function(){ var $element = this.$element; this.removeState( constants.states.DISABLED ); if( $element.is( constants.HAS_A18_ATTRS ) ){ $element.prop( constants.DISABLED, false ); } return this; } }; } );
Container = Switch.extend( function ( base ) { /** * Default configuration values * @property defaults * @type Object * @private * @final */ var defaults = { /** * The default state or states to be applied to the Container. * This can be an Array of strings or comma-delimited string * representing multiple states. * It can also be a string representing a single state * @property states * @type {String|Array} * @default null */ states: null, content: null, /** * A URL to be used as a content source. * @property url * @type {String} * @default null */ url: null, /** * A CSS selctor for an element to be used as a content source. * @property selector * @type {String} * @default null */ selector: null, /** * Set to true if the content should be loaded in an iframe * @property frame * @type {Boolean} * @default false */ frame: false, /** * When true the $element's height is set to the content height. * @property autoHeight * @type {Boolean} * @default false */ autoHeight: false, /** * When true the $element's width is set to the content width. * @property autoWidth * @type {Boolean} * @default false */ autoWidth: false, /** * A selector that specifies a target within the Container to inject content. * @property target * @type {String} * @default null */ target: null }; return { /** * Constructor * @method init * @public * @param {Object} $element JQuery object for the element wrapped by * the component * @param {Object} settings Configuration settings */ init: function( $element, settings ){ /** * Instance of Container * @property Container * @type Object * @private */ var self = this, /** * The content of the container * @property content * @type String * @private */ content, target; _.defaults( settings, defaults ); base.init.call( self, $element, settings ); /** * A cache to store the height and width of the $element * @property cache * @type Object * @public */ self.cache = {}; /** * A jquery object to inject content into * @property target * @type {Object} * @public */ self.$target = null; target = settings.target; if( target ){ self.$target = $element.find( target ); } else { self.$target = $element; } /** * Loads content and then triggers an update event. Called on load event. * @method load * @private * @param {Object} $target Jquery object for the target node * @param {String|Object} source The source to load or obtain a URL from * @param {String} method the method to be used when inserting content * @return {Object} Container */ function load( $target, source, method ){ var isUrl = helpers.isUrl( source ), loadedContent, tmpData, url; if( !isUrl ){ if (typeof source === "object" && source.getUrl) { url = source.getUrl(); } else if (typeof source === "string") { url = source; } else if ( $target.is( 'a' ) ){ url = $target.attr( 'href' ); } // DO WE NEED THIS??? if( !url && arguments.length > 1 ){ method = url; } } if( url.indexOf( '#' ) === 0 ){ loadedContent = $( url ).html(); self.trigger( constants.events.UPDATE, [loadedContent, method] ); return self; } if( settings.frame === true ){ loadedContent = '<iframe src="' + url + '"></iframe>'; self.trigger( constants.events.UPDATE, [loadedContent, method] ); return self; } self.removeState( constants.states.LOADED ); self.addState( constants.states.LOADING ); $.ajax( { url: url, success: function( data, textStatus, jXHR ){ var newContent, anchor = helpers.parseUri( url ).anchor; if( settings.selector ){ newContent = $( data ).find( settings.selector ).html(); } else if( anchor ){ newContent = $( data ).find( '#' + anchor ).html() || data; } else { newContent = data; } self.removeState( constants.states.LOADING ); self.addState( constants.states.LOADED ); self.trigger( constants.events.UPDATE, [newContent, method] ); }, failure: function(){ self.removeState( constants.states.LOADING ).addState( constants.states.ERRED ); } } ); return self; } /* * Updates content on an update event * @method update * @private * @param {Object} $target Jquery object for the target node * @param {String} updateContent The content to set * @param {String} method The method to use for setting the content. * This can specified as 'prepend' or 'append'. If theese are not * specified the content is replaced. * @return {Function} Container.setState */ function update( $target, updateContent, method ){ switch( method ){ case 'append': self.appendContent( updateContent ); break; case 'prepend': self.prependContent( updateContent ); break; default: self.setContent( updateContent ); break; } } /** * Returns the contents of the Container * @method getContent * @public * @return {Array} contents */ self.getContent = function(){ return content; }; /** * Sets the content of the Container replacing current content * @method setContent * param {String} value The content to set. * @public * @return {Object} Container */ self.setContent = function( value ){ content = value; self.$target.html( content ); if( settings.autoHeight ){ delete this.cache.height; self.setHeight( this.getHeight() ); } if( settings.autoWidth ){ delete this.cache.width; self.setWidth( this.getWidth() ); } self.trigger( constants.events.UPDATED, $element ); return self; }; /** * Appends content to the Container * @method appendContent * param {String} value The content to append. * @public * @return {Function} Container.setContent */ self.appendContent = function( value ){ self.setContent( content + value ); return self; }; /** * Prepend content to the Container * @method prepend Content * param {String} value The content to prepend. * @public * @return {Function} self.setContent */ self.prependContent = function( value ){ return self.setContent( value + content ); }; if( settings.url ){ //Load content from url self.trigger( constants.events.LOAD ); } else { //Store the $elements content content = $element.html(); } //sets the height of the container automagically if autoHeight is set to true. if( settings.autoHeight ){ this.setHeight( this.getHeight() ); } //sets the width of the container automagically if autoHeight is set to true. if( settings.autoWidth ){ self.setWidth( self.getWidth() ); } // //Bind update event to update self.on( constants.events.UPDATE, function(event, content, method ) { event.stopPropagation(); update( $( event.target ), content, method ); }); //Bind load event to load self.on( constants.events.LOAD, function(event, content, method ) { event.stopPropagation(); //load( $(event.target), content, method ); var things = [$(event.target)]; if (content) { things.push(content); } if (method) { things.push(method); } load.apply(this, things); }); }, /** * Returns the computed height of the Container; result has no units * @method getHeight * @public * @return {Integer} Computed height of the Container (result drops units) */ getHeight: function(){ var height = this.cache.height, $target = this.$target; if( !height ){ if( $target ){ height = $target.height(); } else { height = this.$element.height(); } this.cache.height = height; } return height; }, /** * Sets the height of the Container. * @method setHeight * @param {Integer} value The height in pixels to set * @public * @return {Object} Container */ setHeight: function( value ){ var $target = this.$target; this.cache.height = value; if( $target ){ $target.height( value ); } else { this.$element.height( value ); } return this; }, /** * Returns the computed width of the Container; result has no units * @method getHeight * @public * @return {Integer} Computed height of the Container (result drops units) */ getWidth: function(){ var width = this.cache.width, $target = this.$target; if( !width ) { if( $target ){ width = $target.width(); } else { width = this.$element.width(); } this.cache.width = width; } return width; }, /** * Sets the width of the Container * @method setWidth * @param {Integer} value The width in pixels to set * @public * @return {Object} Container */ setWidth: function( value ){ var $target = this.$target; this.cache.width = value; if( $target ){ $target.width( value ); } else { this.$element.width( value ); } return this; } }; } );
List = Switch.extend( function( base ){ var SELECTED = constants.statePrefix + constants.states.SELECTED, LIST_TAGS = 'ul, ol, dl', defaults = { index: undefined }, root = 'lu/List/decorators/', decorators = { viewport: root + 'viewport' }; return { /** * @constructor * @method init * @public * @param {Object} $element JQuery object for the element wrapped by the component * @param {Object} settings Configuration settings */ init: function( $element, settings ){ var self = this, Selected, Previous, $items, index; _.defaults( settings, defaults ); base.init.call( this, $element, settings ); var requirements = []; if (settings.viewport) { requirements.push(decorators.viewport); } require.ensure( requirements, function( require, module, exports ){ _.each( requirements, function( decorator, index ){ decorator = require( decorator )( settings ); Fiber.decorate( self, decorator ); } ); self.trigger( 'dependencies-resolved' ); } ); /** * gets the 0 based index of the selected item. * @method index * @public * @return {Number} index */ this.index = function(){ return index; }; /** * Gets the selectable items * @public * @return {Object} a JQuery object-reference to the items */ this.items = function(){ var $items; if( settings.items ){ if( typeof settings.items === 'string' ){ $items = $element.find( settings.items ); } else { $items = settings.items; } } else { if( $element.is( LIST_TAGS ) ){ $items = $element.children(); } else { $items = $element.children( LIST_TAGS ).first().children(); } } if( !$items ){ $items = $element.children(); } return $items; }; /** * Returns the currently-selected Container. * @method current * @public * @return {Object} JQuery object-reference to the selected item */ this.current = function(){ return Selected; }; /** * Select an item in the list * @method select * @public * @param {Integer|String|Object} item The index of the item to * select, a css selector, or a JQuery collection containing the item. * @return {Object} self */ this.select = function( item ){ var component, componentData, $item, idx; //return if no item was passed in. if( item === undefined ){ return this; } //try to determine the index of the item being selected idx = ( typeof item === 'number' ) ? item : undefined; //Figure out what to select based on the param passed in. if( typeof item === 'number' && item <= this.size() - 1 ){ $item = this.$items.eq( item ); } else if( typeof item === 'string' ){ $item = this.$items.filter( item ); $item = ( $item.size() === 1 ) ? $item : undefined; } else if( item instanceof $ && item.size() === 1 ){ if( item.is( this.$items ) ){ $item = item; } } //We could not determine which item to select so... if( $item === undefined ){ this.trigger( constants.events.OUT_OF_BOUNDS, [this] ); return this; } //Get the index of the item to be selected if we don't have it from above if( idx === undefined ) { idx = this.$items.index( $item ); } if( idx > this.index() ){ this.addState( constants.states.FORWARD ).removeState( constants.states.REVERSE ); } else if( idx < this.index() ){ this.addState( constants.states.REVERSE ).removeState( constants.states.FORWARD ); } _.each( $item.lu( 'getComponents' ), function( comp, key ){ var instance; if( !component ){ instance = comp.instance; if( instance ){ if( typeof instance.removeState === 'function' && typeof instance.addState === 'function' ){ component = comp; } } } } ); //an acceptable component was not found. if( !component ){ Lu.map( $item, 'Switch', function(){} ); Lu.execute( $item ); component = $item.lu( 'getComponents' ).Switch; } //Once the item is fully instantiated, select it. component.deferral.then( function( Switch ){ var current = self.current(); //If there is a currently selected item remove the selected state if( current ){ current.removeState( constants.states.SELECTED ); } else { self.$items.filter( '.' + SELECTED ).not( Switch.$element ).removeClass( SELECTED ); } Selected = Switch; index = idx; Selected.addState( constants.states.SELECTED ); self.trigger( constants.events.SELECTED, [self] ); } ); return this; }; this.$items = this.items(); index = settings.index; if( index === undefined ){ var $selected = this.$items.filter('.' + SELECTED ); index = this.$items.index( $selected ); if( index === -1 ){ index = 0; } } //Automatically select an item during init self.select( index ); this.on( constants.events.SELECT, function( event, component ){ event.stopPropagation(); var $element = component.$element, controls = $element.attr( 'aria-controls' ), href, $item; if( !controls ){ href = $element.attr( 'href' ); if( href ){ controls = helpers.parseUri( href ).anchor; } } if( controls ){ $item = $( '#' + controls ); if( $item.is( self.$items ) ){ self.select( $item ); } else { self.select( component.$element.closest( self.$items ) ); } } else { self.select( component.$element.closest( self.$items ) ); } } ); this.on( constants.events.NEXT, function( event ){ event.stopPropagation(); self.next(); } ); this.on( constants.events.PREVIOUS, function( event ){ event.stopPropagation(); self.previous(); } ); this.on( constants.events.FIRST, function( event ){ event.stopPropagation(); self.first(); } ); this.on( constants.events.LAST, function( event ){ event.stopPropagation(); self.last(); } ); this.on( constants.events.STATED, function( event, component ){ event.stopPropagation(); var current; if( !component.$element.is( self.$items ) ){ return; } current = self.current(); if( current ){ if( component.$element.is( current.$element ) ){ return; } } if( component.hasState ){ if( component.hasState( constants.states.SELECTED ) ){ self.select( component.$element ); } } } ); }, /** * adds a new item to $element * @method append * @public * @param {Array} A jQuery Collection of $items to append * @return {Object} self */ add: function( $item ){ this.$items.parent().append( $item ); this.$items = this.items(); return this; }, /** * Removes an item from $element * @method remove * @public * @param {Array} A jQuery Collection of $items to remove * @return {Object} self */ remove: function( $item ){ $( $( $item ), this.$items ).remove(); this.$items = this.items(); return this; }, /** * Selects the next item in the list. * @method next * @public * @return {Object} self */ next: function(){ if( this.hasNext() ){ this.select( this.index() + 1 ); } else { this.trigger( constants.events.OUT_OF_BOUNDS, [this] ); } return this; }, /** * Selects the previous item in the list. * @method previous * @public * @return {Object} self */ previous: function(){ if( this.hasPrevious() ){ this.select( this.index() - 1 ); } else { this.trigger( constants.events.OUT_OF_BOUNDS, [this] ); } return this; }, /** * Selects the last item in the list. * @method last * @public * @return {Object} self */ last: function(){ this.select( this.$items.eq( this.size() - 1 ) ); return this; }, /** * Selects the first item in the list. * @method first * @public * @return {Object} self */ first: function(){ this.select( 0 ); return this; }, /** * Determines if there are any higher-index items in the list. * @method hasNext * @public * @return {Boolean} true if not at the last item in the list */ hasNext: function(){ return ( this.index() < this.size() - 1 ); }, /** * Determines if there are any lower-index items in the list. * @method hasPrevious * @public * @return {Boolean} true if not at the first item in the list */ hasPrevious: function(){ return ( this.index() > 0 ); }, /** * Returns the number of items in the list. * @method size * @public * @return {Number} The number of items in the list */ size: function(){ return this.$items.size(); } }; } );