├── .gitignore ├── .jshintrc ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── dist ├── menu-all.js ├── menu-all.min.js ├── menu-init.js ├── menu-init.min.js ├── menu-trigger-init.js ├── menu-trigger-init.min.js ├── menu-trigger.js ├── menu-trigger.min.js ├── menu.js └── menu.min.js ├── examples ├── docs.css ├── index.html └── init.js ├── libs ├── jquery │ └── jquery.js ├── qunit │ ├── qunit.css │ └── qunit.js └── shoestring-dev.js ├── package.json ├── src ├── .jshintrc ├── menu-init.js ├── menu-trigger-init.js ├── menu-trigger.js ├── menu.css └── menu.js └── test ├── .jshintrc ├── index.html └── menu-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /node_modules/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true 14 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Important notes 4 | Please don't edit files in the `dist` subdirectory as they are generated via Grunt. You'll find source code in the `src` subdirectory! 5 | 6 | ### Code style 7 | Regarding code style like indentation and whitespace, **follow the conventions you see used in the source already.** 8 | 9 | ### PhantomJS 10 | While Grunt can run the included unit tests via [PhantomJS](http://phantomjs.org/), this shouldn't be considered a substitute for the real thing. Please be sure to test the `test/*.html` unit test file(s) in _actual_ browsers. 11 | 12 | ## Modifying the code 13 | First, ensure that you have the latest [Node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed. 14 | 15 | Test that Grunt's CLI is installed by running `grunt --version`. If the command isn't found, run `npm install -g grunt-cli`. For more information about installing Grunt, see the [getting started guide](http://gruntjs.com/getting-started). 16 | 17 | 1. Fork and clone the repo. 18 | 1. Run `npm install` to install all dependencies (including Grunt). 19 | 1. Run `grunt` to grunt this project. 20 | 21 | Assuming that you don't see any red, you're ready to go. Just be sure to run `grunt` after making any changes, to ensure that nothing is broken. 22 | 23 | ## Submitting pull requests 24 | 25 | 1. Create a new branch, please don't work in your `master` branch directly. 26 | 1. Add failing tests for the change you want to make. Run `grunt` to see the tests fail. 27 | 1. Fix stuff. 28 | 1. Run `grunt` to see if the tests pass. Repeat steps 2-4 until done. 29 | 1. Open `test/*.html` unit test file(s) in actual browser to ensure tests pass everywhere. 30 | 1. Update the documentation to reflect any changes. 31 | 1. Push to your fork and submit a pull request. -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module:true*/ 2 | (function(){ 3 | 'use strict'; 4 | 5 | module.exports = function(grunt) { 6 | 7 | // Project configuration. 8 | grunt.initConfig({ 9 | // Metadata. 10 | pkg: grunt.file.readJSON('package.json'), 11 | banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - ' + 12 | '<%= grunt.template.today("yyyy-mm-dd") %>\n' + 13 | '<%= pkg.homepage ? "* " + pkg.homepage + "\\n" : "" %>' + 14 | '* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>;' + 15 | ' Licensed <%= pkg.license %> */\n', 16 | // Task configuration. 17 | clean: { 18 | files: ['dist'] 19 | }, 20 | concat: { 21 | options: { 22 | banner: '<%= banner %>', 23 | stripBanners: true 24 | }, 25 | distMenu: { 26 | src: ['src/<%= pkg.filename %>.js'], 27 | dest: 'dist/<%= pkg.filename %>.js' 28 | }, 29 | distMenuInit: { 30 | src: ['src/<%= pkg.filename %>-init.js'], 31 | dest: 'dist/<%= pkg.filename %>-init.js' 32 | }, 33 | distMenuTrigger: { 34 | src: ['src/<%= pkg.filename %>-trigger.js'], 35 | dest: 'dist/<%= pkg.filename %>-trigger.js' 36 | }, 37 | distMenuTriggerInit: { 38 | src: ['src/<%= pkg.filename %>-trigger-init.js'], 39 | dest: 'dist/<%= pkg.filename %>-trigger-init.js' 40 | }, 41 | distMenuAll: { 42 | src: ['<%= concat.distMenu.dest %>', '<%= concat.distMenuInit.dest %>', '<%= concat.distMenuTrigger.dest %>','<%= concat.distMenuTriggerInit.dest %>' ], 43 | dest: 'dist/<%= pkg.filename %>-all.js' 44 | } 45 | }, 46 | uglify: { 47 | options: { 48 | banner: '<%= banner %>' 49 | }, 50 | distMenu: { 51 | src: ['<%= concat.distMenu.src %>'], 52 | dest: 'dist/<%= pkg.filename %>.min.js' 53 | }, 54 | distMenuInit: { 55 | src: ['<%= concat.distMenuInit.src %>'], 56 | dest: 'dist/<%= pkg.filename %>-init.min.js' 57 | }, 58 | distMenuTrigger: { 59 | src: ['<%= concat.distMenuTrigger.src %>'], 60 | dest: 'dist/<%= pkg.filename %>-trigger.min.js' 61 | }, 62 | distMenuTriggerInit: { 63 | src: ['<%= concat.distMenuTriggerInit.src %>'], 64 | dest: 'dist/<%= pkg.filename %>-trigger-init.min.js' 65 | }, 66 | distMenuAll: { 67 | src: ['<%= concat.distMenuAll.src %>'], 68 | dest: 'dist/<%= pkg.filename %>-all.min.js' 69 | } 70 | }, 71 | qunit: { 72 | files: ['test/**/*.html'] 73 | }, 74 | jshint: { 75 | gruntfile: { 76 | options: { 77 | jshintrc: '.jshintrc' 78 | }, 79 | src: 'Gruntfile.js' 80 | }, 81 | src: { 82 | options: { 83 | jshintrc: 'src/.jshintrc' 84 | }, 85 | src: ['src/**/*.js'] 86 | }, 87 | test: { 88 | options: { 89 | jshintrc: 'test/.jshintrc' 90 | }, 91 | src: ['test/**/*.js'] 92 | } 93 | }, 94 | watch: { 95 | gruntfile: { 96 | files: '<%= jshint.gruntfile.src %>', 97 | tasks: ['jshint:gruntfile'] 98 | }, 99 | src: { 100 | files: '<%= jshint.src.src %>', 101 | tasks: ['jshint:src', 'qunit'] 102 | }, 103 | test: { 104 | files: '<%= jshint.test.src %>', 105 | tasks: ['jshint:test', 'qunit'] 106 | } 107 | } 108 | }); 109 | 110 | // These plugins provide necessary tasks. 111 | grunt.loadNpmTasks('grunt-contrib-clean'); 112 | grunt.loadNpmTasks('grunt-contrib-concat'); 113 | grunt.loadNpmTasks('grunt-contrib-uglify'); 114 | grunt.loadNpmTasks('grunt-contrib-qunit'); 115 | grunt.loadNpmTasks('grunt-contrib-jshint'); 116 | grunt.loadNpmTasks('grunt-contrib-watch'); 117 | 118 | // Default task. 119 | grunt.registerTask('default', ['jshint', 'qunit', 'clean', 'concat', 'uglify']); 120 | 121 | }; 122 | })(); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Filament Group 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | :warning: This project is archived and the repository is no longer maintained. 2 | 3 | # Menu 4 | 5 | jQuery/Shoestring plugin for progressively enhanced menus 6 | 7 | [![Filament Group](http://filamentgroup.com/images/fg-logo-positive-sm-crop.png) ](http://www.filamentgroup.com/) 8 | 9 | ## Getting Started 10 | Download the [production version][min] or the [development version][max]. 11 | 12 | [min]: https://raw.github.com/filamentgroup/menu/master/dist/menu.min.js 13 | [max]: https://raw.github.com/filamentgroup/menu/master/dist/menu.js 14 | 15 | In your web page: 16 | 17 | ```html 18 | 19 | 20 | 29 | ``` 30 | 31 | ## Demo 32 | Check the demo [here](http://filamentgroup.github.io/Menu/examples/) 33 | 34 | ## Release History 35 | v0.1.0 - First release 36 | -------------------------------------------------------------------------------- /dist/menu-all.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | /*! Menu - v0.1.4 - 2017-03-18 5 | * https://github.com/filamentgroup/menu 6 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 7 | window.jQuery = window.jQuery || window.shoestring; 8 | 9 | (function( $, w ) { 10 | "use strict"; 11 | 12 | var componentName = "Menu", 13 | at = { 14 | ariaHidden: "aria-hidden" 15 | }, 16 | selectClass = "menu-selected", 17 | focusables = "a,input,[tabindex]"; 18 | 19 | var menu = function( element ){ 20 | if( !element ){ 21 | throw new Error( "Element required to initialize object" ); 22 | } 23 | this.element = element; 24 | this.$element = $( element ); 25 | this.opened = true; 26 | }; 27 | 28 | menu.prototype.fill = function( items, selectedText ) { 29 | var html = ""; 30 | 31 | $.each( items, function( i, item ){ 32 | html += "" + item + ""; 35 | }); 36 | 37 | this.$element.find( "ol,ul" ).html( html ); 38 | }; 39 | 40 | menu.prototype.moveSelected = function( placement, focus ){ 41 | var $items = this.$element.find( "li" ), 42 | $selected = $items.filter( "." + selectClass ), 43 | $nextSelected; 44 | 45 | if( !$selected.length || placement === "start" ){ 46 | $nextSelected = $items.eq( 0 ); 47 | } 48 | else if( placement === "next" ){ 49 | $nextSelected = $selected.next(); 50 | if( !$nextSelected.length ){ 51 | $nextSelected = $items.eq( 0 ); 52 | } 53 | } 54 | else { 55 | $nextSelected = $selected.prev(); 56 | if( !$nextSelected.length ){ 57 | $nextSelected = $items.eq( $items.length - 1 ); 58 | } 59 | } 60 | $selected.removeClass( selectClass ); 61 | $nextSelected.addClass( selectClass ); 62 | 63 | if( focus || $( w.document.activeElement ).closest( $selected ).length ){ 64 | if( $nextSelected.is( focusables ) ){ 65 | $nextSelected[ 0 ].focus(); 66 | } 67 | else{ 68 | var $focusChild = $nextSelected.find( focusables ); 69 | if( $focusChild.length ){ 70 | $focusChild[ 0 ].focus(); 71 | } 72 | } 73 | } 74 | }; 75 | 76 | menu.prototype.getSelectedElement = function(){ 77 | return this.$element.find( "li." + selectClass ); 78 | }; 79 | 80 | menu.prototype.selectActive = function(){ 81 | var trigger = this.$element.data( componentName + "-trigger" ); 82 | var $selected = this.getSelectedElement(); 83 | 84 | if( trigger && $( trigger ).is( "input" ) ){ 85 | trigger.value = $selected.text(); 86 | } 87 | $selected.trigger( componentName + ":select" ); 88 | this.close(); 89 | return $selected.text(); 90 | }; 91 | 92 | menu.prototype.keycodes = { 93 | 38 : function(e) { 94 | this.moveSelected( "prev" ); 95 | e.preventDefault(); 96 | }, 97 | 98 | 40 : function(e){ 99 | this.moveSelected( "next" ); 100 | e.preventDefault(); 101 | }, 102 | 103 | 13 : function(){ 104 | // return the selected value 105 | return this.selectActive(); 106 | }, 107 | 108 | 9 : function(e){ 109 | this.moveSelected( e.shiftKey ? "prev" : "next" ); 110 | e.preventDefault(); 111 | }, 112 | 113 | 27 : function(){ 114 | this.close(); 115 | } 116 | }; 117 | 118 | menu.prototype.keyDown = function( e ){ 119 | var fn = this.keycodes[e.keyCode] || function(){}; 120 | return fn.call( this, e ); 121 | }; 122 | 123 | menu.prototype._bindKeyHandling = function(){ 124 | var self = this; 125 | this.$element 126 | .bind( "keydown", function( e ){ 127 | self.keyDown( e ); 128 | } ) 129 | .bind( "mouseover", function( e ){ 130 | self.$element.find( "." + selectClass ).removeClass( selectClass ); 131 | $( e.target ).closest( "li" ).addClass( selectClass ); 132 | }) 133 | .bind( "mouseleave", function( e ){ 134 | $( e.target ).closest( "li" ).removeClass( selectClass ); 135 | }) 136 | .bind( "click", function(){ 137 | self.selectActive(); 138 | }); 139 | }; 140 | 141 | menu.prototype.open = function( trigger, sendFocus ){ 142 | if( this.opened ){ 143 | return; 144 | } 145 | this.$element.attr( at.ariaHidden, false ); 146 | 147 | this.$element.data( componentName + "-trigger", trigger ); 148 | this.opened = true; 149 | this.moveSelected( "start", sendFocus ); 150 | this.$element.trigger( componentName + ":open" ); 151 | }; 152 | 153 | menu.prototype.close = function(){ 154 | if( !this.opened ){ 155 | return; 156 | } 157 | this.$element.attr( at.ariaHidden, true ); 158 | this.opened = false; 159 | var $trigger = this.$element.data( componentName + "-trigger" ); 160 | if( $trigger ){ 161 | $trigger.focus(); 162 | } 163 | this.$element.trigger( componentName + ":close" ); 164 | }; 165 | 166 | menu.prototype.toggle = function( trigger, sendFocus ){ 167 | this[ this.opened ? "close" : "open" ]( trigger, sendFocus ); 168 | }; 169 | 170 | menu.prototype.init = function(){ 171 | // prevent re-init 172 | if( this.$element.data( componentName ) ) { 173 | return; 174 | } 175 | this.$element.data( componentName, this ); 176 | 177 | this.$element.attr( "role", "menu" ); 178 | this.close(); 179 | var self = this; 180 | 181 | $( document ).bind( "mouseup", function(event){ 182 | // only close the menu if the click is outside the menu element 183 | if( ! $(event.target).closest( self.$element[0] ).length ){ 184 | self.close(); 185 | } 186 | }); 187 | 188 | this._bindKeyHandling(); 189 | 190 | this.$element.trigger( componentName + ":init" ); 191 | }; 192 | 193 | menu.prototype.isOpen = function(){ 194 | return this.opened; 195 | }; 196 | 197 | (w.componentNamespace = w.componentNamespace || w)[ componentName ] = menu; 198 | }( jQuery, this )); 199 | 200 | /*! Menu - v0.1.4 - 2017-03-18 201 | * https://github.com/filamentgroup/menu 202 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 203 | /* global Menu:true */ 204 | (function( Menu, $ ) { 205 | 206 | var pluginName = "menu", 207 | initSelector = "[data-" + pluginName + "]"; 208 | 209 | $.fn[ pluginName ] = function(){ 210 | return this.each(function(){ 211 | new window.componentNamespace.Menu( this ).init(); 212 | }); 213 | }; 214 | 215 | // auto-init on enhance (which is called on domready) 216 | $( document ).bind( "enhance", function( e ){ 217 | $( initSelector, e.target )[ pluginName ](); 218 | }); 219 | 220 | }( Menu, jQuery, this )); 221 | 222 | /*! Menu - v0.1.4 - 2017-03-18 223 | * https://github.com/filamentgroup/menu 224 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 225 | (function( $, w ) { 226 | "use strict"; 227 | 228 | var componentName = "Menutrigger"; 229 | 230 | var Menutrigger = function( element ){ 231 | if( !element ){ 232 | throw new Error( "Element required to initialize object" ); 233 | } 234 | this.element = element; 235 | this.$element = $( element ); 236 | this.$menu = $( "#" + this.$element.attr( "data-menu-trigger" ) ); 237 | this.menu = this.$menu.data( "Menu" ); 238 | }; 239 | 240 | Menutrigger.prototype._bindbehavior = function(){ 241 | var self = this; 242 | 243 | if( this.$element.is( "a" ) ){ 244 | this.$element 245 | .attr( "role", "button" ) 246 | .bind( "click", function( e ){ 247 | e.preventDefault(); 248 | self.menu.toggle( this, true ); 249 | }); 250 | } 251 | else if( this.$element.is( "input" ) ){ 252 | this.$element 253 | .bind( "input keyup", function(){ 254 | if( this.value === "" ){ 255 | self.menu.close(); 256 | } 257 | else { 258 | self.menu.open( this, false ); 259 | } 260 | 261 | }) 262 | .bind( "input keydown", function( e ){ 263 | self.menu.keyDown( e ); 264 | }) 265 | .bind( "focus click", function(){ 266 | if( this.value !== "" ){ 267 | self.menu.open(); 268 | } 269 | } ) 270 | .bind( "blur", function(){ 271 | self.menu.close(); 272 | }); 273 | } 274 | }; 275 | 276 | Menutrigger.prototype.init = function(){ 277 | // prevent re-init 278 | if( this.$element.data( componentName ) ) { 279 | return; 280 | } 281 | this.$element.data( componentName, this ); 282 | 283 | // add attrs 284 | this.$element.attr( "aria-controls", this.$menu.attr( "id" ) ); 285 | this.$element.attr( "aria-haspopup", "true" ); 286 | 287 | this._bindbehavior(); 288 | 289 | this.$element.trigger( componentName + ":init" ); 290 | }; 291 | 292 | w[ componentName ] = Menutrigger; 293 | 294 | }( jQuery, this )); 295 | 296 | 297 | /*! Menu - v0.1.4 - 2017-03-18 298 | * https://github.com/filamentgroup/menu 299 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 300 | /* global Menutrigger:true */ 301 | (function( Menutrigger, $ ) { 302 | 303 | var pluginName = "menu-trigger", 304 | initSelector = "[data-" + pluginName + "]"; 305 | 306 | $.fn[ pluginName ] = function(){ 307 | return this.each(function(){ 308 | new Menutrigger( this ).init(); 309 | }); 310 | }; 311 | 312 | // auto-init on enhance (which is called on domready) 313 | $( document ).bind( "enhance", function( e ){ 314 | $( initSelector, e.target )[ pluginName ](); 315 | }); 316 | 317 | }( Menutrigger, jQuery, this )); 318 | 319 | -------------------------------------------------------------------------------- /dist/menu-all.min.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | window.jQuery=window.jQuery||window.shoestring,function(a,b){"use strict";var c="Menu",d={ariaHidden:"aria-hidden"},e="menu-selected",f="a,input,[tabindex]",g=function(b){if(!b)throw new Error("Element required to initialize object");this.element=b,this.$element=a(b),this.opened=!0};g.prototype.fill=function(b,c){var d="";a.each(b,function(a,b){d+=""+b+""}),this.$element.find("ol,ul").html(d)},g.prototype.moveSelected=function(c,d){var g,h=this.$element.find("li"),i=h.filter("."+e);if(i.length&&"start"!==c?"next"===c?(g=i.next(),g.length||(g=h.eq(0))):(g=i.prev(),g.length||(g=h.eq(h.length-1))):g=h.eq(0),i.removeClass(e),g.addClass(e),d||a(b.document.activeElement).closest(i).length)if(g.is(f))g[0].focus();else{var j=g.find(f);j.length&&j[0].focus()}},g.prototype.getSelectedElement=function(){return this.$element.find("li."+e)},g.prototype.selectActive=function(){var b=this.$element.data(c+"-trigger"),d=this.getSelectedElement();return b&&a(b).is("input")&&(b.value=d.text()),d.trigger(c+":select"),this.close(),d.text()},g.prototype.keycodes={38:function(a){this.moveSelected("prev"),a.preventDefault()},40:function(a){this.moveSelected("next"),a.preventDefault()},13:function(){return this.selectActive()},9:function(a){this.moveSelected(a.shiftKey?"prev":"next"),a.preventDefault()},27:function(){this.close()}},g.prototype.keyDown=function(a){var b=this.keycodes[a.keyCode]||function(){};return b.call(this,a)},g.prototype._bindKeyHandling=function(){var b=this;this.$element.bind("keydown",function(a){b.keyDown(a)}).bind("mouseover",function(c){b.$element.find("."+e).removeClass(e),a(c.target).closest("li").addClass(e)}).bind("mouseleave",function(b){a(b.target).closest("li").removeClass(e)}).bind("click",function(){b.selectActive()})},g.prototype.open=function(a,b){this.opened||(this.$element.attr(d.ariaHidden,!1),this.$element.data(c+"-trigger",a),this.opened=!0,this.moveSelected("start",b),this.$element.trigger(c+":open"))},g.prototype.close=function(){if(this.opened){this.$element.attr(d.ariaHidden,!0),this.opened=!1;var a=this.$element.data(c+"-trigger");a&&a.focus(),this.$element.trigger(c+":close")}},g.prototype.toggle=function(a,b){this[this.opened?"close":"open"](a,b)},g.prototype.init=function(){if(!this.$element.data(c)){this.$element.data(c,this),this.$element.attr("role","menu"),this.close();var b=this;a(document).bind("mouseup",function(c){a(c.target).closest(b.$element[0]).length||b.close()}),this._bindKeyHandling(),this.$element.trigger(c+":init")}},g.prototype.isOpen=function(){return this.opened},(b.componentNamespace=b.componentNamespace||b)[c]=g}(jQuery,this),function(a,b){var c="menu",d="[data-"+c+"]";b.fn[c]=function(){return this.each(function(){new window.componentNamespace.Menu(this).init()})},b(document).bind("enhance",function(a){b(d,a.target)[c]()})}(Menu,jQuery,this),function(a,b){"use strict";var c="Menutrigger",d=function(b){if(!b)throw new Error("Element required to initialize object");this.element=b,this.$element=a(b),this.$menu=a("#"+this.$element.attr("data-menu-trigger")),this.menu=this.$menu.data("Menu")};d.prototype._bindbehavior=function(){var a=this;this.$element.is("a")?this.$element.attr("role","button").bind("click",function(b){b.preventDefault(),a.menu.toggle(this,!0)}):this.$element.is("input")&&this.$element.bind("input keyup",function(){""===this.value?a.menu.close():a.menu.open(this,!1)}).bind("input keydown",function(b){a.menu.keyDown(b)}).bind("focus click",function(){""!==this.value&&a.menu.open()}).bind("blur",function(){a.menu.close()})},d.prototype.init=function(){this.$element.data(c)||(this.$element.data(c,this),this.$element.attr("aria-controls",this.$menu.attr("id")),this.$element.attr("aria-haspopup","true"),this._bindbehavior(),this.$element.trigger(c+":init"))},b[c]=d}(jQuery,this),function(a,b){var c="menu-trigger",d="[data-"+c+"]";b.fn[c]=function(){return this.each(function(){new a(this).init()})},b(document).bind("enhance",function(a){b(d,a.target)[c]()})}(Menutrigger,jQuery,this); -------------------------------------------------------------------------------- /dist/menu-init.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | /* global Menu:true */ 5 | (function( Menu, $ ) { 6 | 7 | var pluginName = "menu", 8 | initSelector = "[data-" + pluginName + "]"; 9 | 10 | $.fn[ pluginName ] = function(){ 11 | return this.each(function(){ 12 | new window.componentNamespace.Menu( this ).init(); 13 | }); 14 | }; 15 | 16 | // auto-init on enhance (which is called on domready) 17 | $( document ).bind( "enhance", function( e ){ 18 | $( initSelector, e.target )[ pluginName ](); 19 | }); 20 | 21 | }( Menu, jQuery, this )); 22 | -------------------------------------------------------------------------------- /dist/menu-init.min.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | !function(a,b){var c="menu",d="[data-"+c+"]";b.fn[c]=function(){return this.each(function(){new window.componentNamespace.Menu(this).init()})},b(document).bind("enhance",function(a){b(d,a.target)[c]()})}(Menu,jQuery,this); -------------------------------------------------------------------------------- /dist/menu-trigger-init.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | /* global Menutrigger:true */ 5 | (function( Menutrigger, $ ) { 6 | 7 | var pluginName = "menu-trigger", 8 | initSelector = "[data-" + pluginName + "]"; 9 | 10 | $.fn[ pluginName ] = function(){ 11 | return this.each(function(){ 12 | new Menutrigger( this ).init(); 13 | }); 14 | }; 15 | 16 | // auto-init on enhance (which is called on domready) 17 | $( document ).bind( "enhance", function( e ){ 18 | $( initSelector, e.target )[ pluginName ](); 19 | }); 20 | 21 | }( Menutrigger, jQuery, this )); 22 | 23 | -------------------------------------------------------------------------------- /dist/menu-trigger-init.min.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | !function(a,b){var c="menu-trigger",d="[data-"+c+"]";b.fn[c]=function(){return this.each(function(){new a(this).init()})},b(document).bind("enhance",function(a){b(d,a.target)[c]()})}(Menutrigger,jQuery,this); -------------------------------------------------------------------------------- /dist/menu-trigger.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | (function( $, w ) { 5 | "use strict"; 6 | 7 | var componentName = "Menutrigger"; 8 | 9 | var Menutrigger = function( element ){ 10 | if( !element ){ 11 | throw new Error( "Element required to initialize object" ); 12 | } 13 | this.element = element; 14 | this.$element = $( element ); 15 | this.$menu = $( "#" + this.$element.attr( "data-menu-trigger" ) ); 16 | this.menu = this.$menu.data( "Menu" ); 17 | }; 18 | 19 | Menutrigger.prototype._bindbehavior = function(){ 20 | var self = this; 21 | 22 | if( this.$element.is( "a" ) ){ 23 | this.$element 24 | .attr( "role", "button" ) 25 | .bind( "click", function( e ){ 26 | e.preventDefault(); 27 | self.menu.toggle( this, true ); 28 | }); 29 | } 30 | else if( this.$element.is( "input" ) ){ 31 | this.$element 32 | .bind( "input keyup", function(){ 33 | if( this.value === "" ){ 34 | self.menu.close(); 35 | } 36 | else { 37 | self.menu.open( this, false ); 38 | } 39 | 40 | }) 41 | .bind( "input keydown", function( e ){ 42 | self.menu.keyDown( e ); 43 | }) 44 | .bind( "focus click", function(){ 45 | if( this.value !== "" ){ 46 | self.menu.open(); 47 | } 48 | } ) 49 | .bind( "blur", function(){ 50 | self.menu.close(); 51 | }); 52 | } 53 | }; 54 | 55 | Menutrigger.prototype.init = function(){ 56 | // prevent re-init 57 | if( this.$element.data( componentName ) ) { 58 | return; 59 | } 60 | this.$element.data( componentName, this ); 61 | 62 | // add attrs 63 | this.$element.attr( "aria-controls", this.$menu.attr( "id" ) ); 64 | this.$element.attr( "aria-haspopup", "true" ); 65 | 66 | this._bindbehavior(); 67 | 68 | this.$element.trigger( componentName + ":init" ); 69 | }; 70 | 71 | w[ componentName ] = Menutrigger; 72 | 73 | }( jQuery, this )); 74 | 75 | -------------------------------------------------------------------------------- /dist/menu-trigger.min.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | !function(a,b){"use strict";var c="Menutrigger",d=function(b){if(!b)throw new Error("Element required to initialize object");this.element=b,this.$element=a(b),this.$menu=a("#"+this.$element.attr("data-menu-trigger")),this.menu=this.$menu.data("Menu")};d.prototype._bindbehavior=function(){var a=this;this.$element.is("a")?this.$element.attr("role","button").bind("click",function(b){b.preventDefault(),a.menu.toggle(this,!0)}):this.$element.is("input")&&this.$element.bind("input keyup",function(){""===this.value?a.menu.close():a.menu.open(this,!1)}).bind("input keydown",function(b){a.menu.keyDown(b)}).bind("focus click",function(){""!==this.value&&a.menu.open()}).bind("blur",function(){a.menu.close()})},d.prototype.init=function(){this.$element.data(c)||(this.$element.data(c,this),this.$element.attr("aria-controls",this.$menu.attr("id")),this.$element.attr("aria-haspopup","true"),this._bindbehavior(),this.$element.trigger(c+":init"))},b[c]=d}(jQuery,this); -------------------------------------------------------------------------------- /dist/menu.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | window.jQuery = window.jQuery || window.shoestring; 5 | 6 | (function( $, w ) { 7 | "use strict"; 8 | 9 | var componentName = "Menu", 10 | at = { 11 | ariaHidden: "aria-hidden" 12 | }, 13 | selectClass = "menu-selected", 14 | focusables = "a,input,[tabindex]"; 15 | 16 | var menu = function( element ){ 17 | if( !element ){ 18 | throw new Error( "Element required to initialize object" ); 19 | } 20 | this.element = element; 21 | this.$element = $( element ); 22 | this.opened = true; 23 | }; 24 | 25 | menu.prototype.fill = function( items, selectedText ) { 26 | var html = ""; 27 | 28 | $.each( items, function( i, item ){ 29 | html += "" + item + ""; 32 | }); 33 | 34 | this.$element.find( "ol,ul" ).html( html ); 35 | }; 36 | 37 | menu.prototype.moveSelected = function( placement, focus ){ 38 | var $items = this.$element.find( "li" ), 39 | $selected = $items.filter( "." + selectClass ), 40 | $nextSelected; 41 | 42 | if( !$selected.length || placement === "start" ){ 43 | $nextSelected = $items.eq( 0 ); 44 | } 45 | else if( placement === "next" ){ 46 | $nextSelected = $selected.next(); 47 | if( !$nextSelected.length ){ 48 | $nextSelected = $items.eq( 0 ); 49 | } 50 | } 51 | else { 52 | $nextSelected = $selected.prev(); 53 | if( !$nextSelected.length ){ 54 | $nextSelected = $items.eq( $items.length - 1 ); 55 | } 56 | } 57 | $selected.removeClass( selectClass ); 58 | $nextSelected.addClass( selectClass ); 59 | 60 | if( focus || $( w.document.activeElement ).closest( $selected ).length ){ 61 | if( $nextSelected.is( focusables ) ){ 62 | $nextSelected[ 0 ].focus(); 63 | } 64 | else{ 65 | var $focusChild = $nextSelected.find( focusables ); 66 | if( $focusChild.length ){ 67 | $focusChild[ 0 ].focus(); 68 | } 69 | } 70 | } 71 | }; 72 | 73 | menu.prototype.getSelectedElement = function(){ 74 | return this.$element.find( "li." + selectClass ); 75 | }; 76 | 77 | menu.prototype.selectActive = function(){ 78 | var trigger = this.$element.data( componentName + "-trigger" ); 79 | var $selected = this.getSelectedElement(); 80 | 81 | if( trigger && $( trigger ).is( "input" ) ){ 82 | trigger.value = $selected.text(); 83 | } 84 | $selected.trigger( componentName + ":select" ); 85 | this.close(); 86 | return $selected.text(); 87 | }; 88 | 89 | menu.prototype.keycodes = { 90 | 38 : function(e) { 91 | this.moveSelected( "prev" ); 92 | e.preventDefault(); 93 | }, 94 | 95 | 40 : function(e){ 96 | this.moveSelected( "next" ); 97 | e.preventDefault(); 98 | }, 99 | 100 | 13 : function(){ 101 | // return the selected value 102 | return this.selectActive(); 103 | }, 104 | 105 | 9 : function(e){ 106 | this.moveSelected( e.shiftKey ? "prev" : "next" ); 107 | e.preventDefault(); 108 | }, 109 | 110 | 27 : function(){ 111 | this.close(); 112 | } 113 | }; 114 | 115 | menu.prototype.keyDown = function( e ){ 116 | var fn = this.keycodes[e.keyCode] || function(){}; 117 | return fn.call( this, e ); 118 | }; 119 | 120 | menu.prototype._bindKeyHandling = function(){ 121 | var self = this; 122 | this.$element 123 | .bind( "keydown", function( e ){ 124 | self.keyDown( e ); 125 | } ) 126 | .bind( "mouseover", function( e ){ 127 | self.$element.find( "." + selectClass ).removeClass( selectClass ); 128 | $( e.target ).closest( "li" ).addClass( selectClass ); 129 | }) 130 | .bind( "mouseleave", function( e ){ 131 | $( e.target ).closest( "li" ).removeClass( selectClass ); 132 | }) 133 | .bind( "click", function(){ 134 | self.selectActive(); 135 | }); 136 | }; 137 | 138 | menu.prototype.open = function( trigger, sendFocus ){ 139 | if( this.opened ){ 140 | return; 141 | } 142 | this.$element.attr( at.ariaHidden, false ); 143 | 144 | this.$element.data( componentName + "-trigger", trigger ); 145 | this.opened = true; 146 | this.moveSelected( "start", sendFocus ); 147 | this.$element.trigger( componentName + ":open" ); 148 | }; 149 | 150 | menu.prototype.close = function(){ 151 | if( !this.opened ){ 152 | return; 153 | } 154 | this.$element.attr( at.ariaHidden, true ); 155 | this.opened = false; 156 | var $trigger = this.$element.data( componentName + "-trigger" ); 157 | if( $trigger ){ 158 | $trigger.focus(); 159 | } 160 | this.$element.trigger( componentName + ":close" ); 161 | }; 162 | 163 | menu.prototype.toggle = function( trigger, sendFocus ){ 164 | this[ this.opened ? "close" : "open" ]( trigger, sendFocus ); 165 | }; 166 | 167 | menu.prototype.init = function(){ 168 | // prevent re-init 169 | if( this.$element.data( componentName ) ) { 170 | return; 171 | } 172 | this.$element.data( componentName, this ); 173 | 174 | this.$element.attr( "role", "menu" ); 175 | this.close(); 176 | var self = this; 177 | 178 | $( document ).bind( "mouseup", function(event){ 179 | // only close the menu if the click is outside the menu element 180 | if( ! $(event.target).closest( self.$element[0] ).length ){ 181 | self.close(); 182 | } 183 | }); 184 | 185 | this._bindKeyHandling(); 186 | 187 | this.$element.trigger( componentName + ":init" ); 188 | }; 189 | 190 | menu.prototype.isOpen = function(){ 191 | return this.opened; 192 | }; 193 | 194 | (w.componentNamespace = w.componentNamespace || w)[ componentName ] = menu; 195 | }( jQuery, this )); 196 | -------------------------------------------------------------------------------- /dist/menu.min.js: -------------------------------------------------------------------------------- 1 | /*! Menu - v0.1.4 - 2017-03-18 2 | * https://github.com/filamentgroup/menu 3 | * Copyright (c) 2017 Scott Jehl; Licensed MIT */ 4 | window.jQuery=window.jQuery||window.shoestring,function(a,b){"use strict";var c="Menu",d={ariaHidden:"aria-hidden"},e="menu-selected",f="a,input,[tabindex]",g=function(b){if(!b)throw new Error("Element required to initialize object");this.element=b,this.$element=a(b),this.opened=!0};g.prototype.fill=function(b,c){var d="";a.each(b,function(a,b){d+=""+b+""}),this.$element.find("ol,ul").html(d)},g.prototype.moveSelected=function(c,d){var g,h=this.$element.find("li"),i=h.filter("."+e);if(i.length&&"start"!==c?"next"===c?(g=i.next(),g.length||(g=h.eq(0))):(g=i.prev(),g.length||(g=h.eq(h.length-1))):g=h.eq(0),i.removeClass(e),g.addClass(e),d||a(b.document.activeElement).closest(i).length)if(g.is(f))g[0].focus();else{var j=g.find(f);j.length&&j[0].focus()}},g.prototype.getSelectedElement=function(){return this.$element.find("li."+e)},g.prototype.selectActive=function(){var b=this.$element.data(c+"-trigger"),d=this.getSelectedElement();return b&&a(b).is("input")&&(b.value=d.text()),d.trigger(c+":select"),this.close(),d.text()},g.prototype.keycodes={38:function(a){this.moveSelected("prev"),a.preventDefault()},40:function(a){this.moveSelected("next"),a.preventDefault()},13:function(){return this.selectActive()},9:function(a){this.moveSelected(a.shiftKey?"prev":"next"),a.preventDefault()},27:function(){this.close()}},g.prototype.keyDown=function(a){var b=this.keycodes[a.keyCode]||function(){};return b.call(this,a)},g.prototype._bindKeyHandling=function(){var b=this;this.$element.bind("keydown",function(a){b.keyDown(a)}).bind("mouseover",function(c){b.$element.find("."+e).removeClass(e),a(c.target).closest("li").addClass(e)}).bind("mouseleave",function(b){a(b.target).closest("li").removeClass(e)}).bind("click",function(){b.selectActive()})},g.prototype.open=function(a,b){this.opened||(this.$element.attr(d.ariaHidden,!1),this.$element.data(c+"-trigger",a),this.opened=!0,this.moveSelected("start",b),this.$element.trigger(c+":open"))},g.prototype.close=function(){if(this.opened){this.$element.attr(d.ariaHidden,!0),this.opened=!1;var a=this.$element.data(c+"-trigger");a&&a.focus(),this.$element.trigger(c+":close")}},g.prototype.toggle=function(a,b){this[this.opened?"close":"open"](a,b)},g.prototype.init=function(){if(!this.$element.data(c)){this.$element.data(c,this),this.$element.attr("role","menu"),this.close();var b=this;a(document).bind("mouseup",function(c){a(c.target).closest(b.$element[0]).length||b.close()}),this._bindKeyHandling(),this.$element.trigger(c+":init")}},g.prototype.isOpen=function(){return this.opened},(b.componentNamespace=b.componentNamespace||b)[c]=g}(jQuery,this); -------------------------------------------------------------------------------- /examples/docs.css: -------------------------------------------------------------------------------- 1 | /* Logo */ 2 | .header { 3 | background: #247201 url(http://filamentgroup.com/images/headerbg-new.jpg) no-repeat bottom left; 4 | } 5 | #fg-logo { 6 | text-indent: -9999px; 7 | margin: 0 auto; 8 | width: 287px; 9 | height: 52px; 10 | background-image: url(http://filamentgroup.com/images/fg-logo-icon.png); 11 | } 12 | @media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5){ 13 | #fg-logo { 14 | background-size: 287px 52px; 15 | background-image: url(http://filamentgroup.com/images/fg-logo-icon-lrg.png); 16 | } 17 | } 18 | /* Demo styles */ 19 | body { 20 | font-family: sans-serif; 21 | font-size: 100%; 22 | } 23 | .docs-main { 24 | margin: 1em auto; 25 | max-width: 46em; 26 | } 27 | label { 28 | display: block; 29 | margin: 1em 0; 30 | } 31 | input, 32 | textarea { 33 | display: block; 34 | width: 100%; 35 | -webkit-box-sizing: border-box; 36 | -moz-box-sizing: border-box; 37 | box-sizing: border-box; 38 | 39 | margin-top: .4em; 40 | padding: .6em; 41 | font-size: 100%; 42 | } 43 | 44 | .menu { 45 | background-color: white; 46 | box-sizing: border-box; 47 | border: 1px solid black; 48 | width: 10em; 49 | } 50 | 51 | .menu ul, .menu ol { 52 | list-style: none; 53 | padding: 5px; 54 | margin: 0; 55 | } 56 | 57 | .menu-selected { 58 | color: white; 59 | background-color: #aaa; 60 | } 61 | 62 | input { 63 | box-sizing: border-box; 64 | width: 10em; 65 | } 66 | 67 | h1.docs, 68 | h2.docs, 69 | h3.docs, 70 | h4.docs, 71 | h5.docs { 72 | font-weight: 500; 73 | margin: 1em 0; 74 | text-transform: none; 75 | color: #000; 76 | clear: both; 77 | } 78 | 79 | h1.docs { font-size: 2.8em; margin-top: .1em; text-transform: uppercase; } 80 | h2.docs { font-size: 2.2em; margin-top: 1.5em; border-top:1px solid #ddd; padding-top: .6em; float:none; } 81 | h3.docs { font-size: 1.6em; margin-top: 1.5em; margin-bottom: .5em; } 82 | h4.docs { font-size: 1.4em; margin-top: 1.5em; } 83 | 84 | p.docs, 85 | p.docs-intro, 86 | ol.docs, 87 | ul.docs, 88 | p.docs-note, 89 | dl.docs { 90 | margin: 1em 0; 91 | font-size: 1em; 92 | } 93 | 94 | ul.docs, 95 | ol.docs { 96 | padding-bottom: .5em; 97 | } 98 | ol.docs li, 99 | ul.docs li { 100 | margin-bottom: 8px; 101 | } 102 | ul.docs ul, 103 | ol.docs ul { 104 | padding-top: 8px; 105 | } 106 | .docs code { 107 | font-size: 1.1em; 108 | } 109 | 110 | p.docs strong { 111 | font-weight: bold; 112 | } 113 | 114 | .docs-note { 115 | background-color: #FFFAA4; 116 | } 117 | .docs-note p, 118 | .docs-note pre, 119 | p.docs-note { 120 | padding: .5em; 121 | margin: 0; 122 | } 123 | 124 | 125 | /** 126 | * prism.js default theme for JavaScript, CSS and HTML 127 | * Based on dabblet (http://dabblet.com) 128 | * @author Lea Verou 129 | */ 130 | 131 | code[class*="language-"], 132 | pre[class*="language-"] { 133 | color: black; 134 | text-shadow: 0 1px white; 135 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 136 | direction: ltr; 137 | text-align: left; 138 | white-space: pre; 139 | word-spacing: normal; 140 | font-size: 0.8em; 141 | 142 | -moz-tab-size: 4; 143 | -o-tab-size: 4; 144 | tab-size: 4; 145 | 146 | -webkit-hyphens: none; 147 | -moz-hyphens: none; 148 | -ms-hyphens: none; 149 | hyphens: none; 150 | } 151 | 152 | @media print { 153 | code[class*="language-"], 154 | pre[class*="language-"] { 155 | text-shadow: none; 156 | } 157 | } 158 | 159 | /* Code blocks */ 160 | pre[class*="language-"] { 161 | padding: 1em; 162 | margin: .5em 0; 163 | overflow: auto; 164 | } 165 | 166 | :not(pre) > code[class*="language-"], 167 | pre[class*="language-"] { 168 | background: #f5f2f0; 169 | } 170 | 171 | /* Inline code */ 172 | :not(pre) > code[class*="language-"] { 173 | padding: .1em; 174 | border-radius: .3em; 175 | } 176 | 177 | pre[class*="language-"] { 178 | padding: 1em; 179 | margin: 0; 180 | margin-bottom: 1em; 181 | } 182 | 183 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Menu Examples 6 | 7 | 8 | 47 | 48 | 49 | 50 |
51 |

Filament Group

52 |
53 | 54 |

Example 1: menu button trigger

55 | 56 | Menu 57 | 58 |
59 | 67 |
68 | 69 | 70 |

Example 2: menu input trigger

71 | 72 | 73 | 74 |
75 | 83 |
84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /examples/init.js: -------------------------------------------------------------------------------- 1 | // DOM-ready auto-init of plugins. 2 | // Many plugins bind to an "enhance" event to init themselves on dom ready, or when new markup is inserted into the DOM 3 | (function( $ ){ 4 | $( function(){ 5 | $( document ).bind( "enhance", function(){ 6 | $( "body" ).addClass( "enhanced" ); 7 | }); 8 | 9 | $( document ).trigger( "enhance" ); 10 | }); 11 | }( jQuery )); -------------------------------------------------------------------------------- /libs/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } -------------------------------------------------------------------------------- /libs/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.11.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | (function( window ) { 12 | 13 | var QUnit, 14 | assert, 15 | config, 16 | onErrorFnPrev, 17 | testId = 0, 18 | fileName = (sourceFromStacktrace( 0 ) || "" ).replace(/(:\d+)+\)?/, "").replace(/.+\//, ""), 19 | toString = Object.prototype.toString, 20 | hasOwn = Object.prototype.hasOwnProperty, 21 | // Keep a local reference to Date (GH-283) 22 | Date = window.Date, 23 | defined = { 24 | setTimeout: typeof window.setTimeout !== "undefined", 25 | sessionStorage: (function() { 26 | var x = "qunit-test-string"; 27 | try { 28 | sessionStorage.setItem( x, x ); 29 | sessionStorage.removeItem( x ); 30 | return true; 31 | } catch( e ) { 32 | return false; 33 | } 34 | }()) 35 | }, 36 | /** 37 | * Provides a normalized error string, correcting an issue 38 | * with IE 7 (and prior) where Error.prototype.toString is 39 | * not properly implemented 40 | * 41 | * Based on http://es5.github.com/#x15.11.4.4 42 | * 43 | * @param {String|Error} error 44 | * @return {String} error message 45 | */ 46 | errorString = function( error ) { 47 | var name, message, 48 | errorString = error.toString(); 49 | if ( errorString.substring( 0, 7 ) === "[object" ) { 50 | name = error.name ? error.name.toString() : "Error"; 51 | message = error.message ? error.message.toString() : ""; 52 | if ( name && message ) { 53 | return name + ": " + message; 54 | } else if ( name ) { 55 | return name; 56 | } else if ( message ) { 57 | return message; 58 | } else { 59 | return "Error"; 60 | } 61 | } else { 62 | return errorString; 63 | } 64 | }, 65 | /** 66 | * Makes a clone of an object using only Array or Object as base, 67 | * and copies over the own enumerable properties. 68 | * 69 | * @param {Object} obj 70 | * @return {Object} New object with only the own properties (recursively). 71 | */ 72 | objectValues = function( obj ) { 73 | // Grunt 0.3.x uses an older version of jshint that still has jshint/jshint#392. 74 | /*jshint newcap: false */ 75 | var key, val, 76 | vals = QUnit.is( "array", obj ) ? [] : {}; 77 | for ( key in obj ) { 78 | if ( hasOwn.call( obj, key ) ) { 79 | val = obj[key]; 80 | vals[key] = val === Object(val) ? objectValues(val) : val; 81 | } 82 | } 83 | return vals; 84 | }; 85 | 86 | function Test( settings ) { 87 | extend( this, settings ); 88 | this.assertions = []; 89 | this.testNumber = ++Test.count; 90 | } 91 | 92 | Test.count = 0; 93 | 94 | Test.prototype = { 95 | init: function() { 96 | var a, b, li, 97 | tests = id( "qunit-tests" ); 98 | 99 | if ( tests ) { 100 | b = document.createElement( "strong" ); 101 | b.innerHTML = this.nameHtml; 102 | 103 | // `a` initialized at top of scope 104 | a = document.createElement( "a" ); 105 | a.innerHTML = "Rerun"; 106 | a.href = QUnit.url({ testNumber: this.testNumber }); 107 | 108 | li = document.createElement( "li" ); 109 | li.appendChild( b ); 110 | li.appendChild( a ); 111 | li.className = "running"; 112 | li.id = this.id = "qunit-test-output" + testId++; 113 | 114 | tests.appendChild( li ); 115 | } 116 | }, 117 | setup: function() { 118 | if ( this.module !== config.previousModule ) { 119 | if ( config.previousModule ) { 120 | runLoggingCallbacks( "moduleDone", QUnit, { 121 | name: config.previousModule, 122 | failed: config.moduleStats.bad, 123 | passed: config.moduleStats.all - config.moduleStats.bad, 124 | total: config.moduleStats.all 125 | }); 126 | } 127 | config.previousModule = this.module; 128 | config.moduleStats = { all: 0, bad: 0 }; 129 | runLoggingCallbacks( "moduleStart", QUnit, { 130 | name: this.module 131 | }); 132 | } else if ( config.autorun ) { 133 | runLoggingCallbacks( "moduleStart", QUnit, { 134 | name: this.module 135 | }); 136 | } 137 | 138 | config.current = this; 139 | 140 | this.testEnvironment = extend({ 141 | setup: function() {}, 142 | teardown: function() {} 143 | }, this.moduleTestEnvironment ); 144 | 145 | this.started = +new Date(); 146 | runLoggingCallbacks( "testStart", QUnit, { 147 | name: this.testName, 148 | module: this.module 149 | }); 150 | 151 | // allow utility functions to access the current test environment 152 | // TODO why?? 153 | QUnit.current_testEnvironment = this.testEnvironment; 154 | 155 | if ( !config.pollution ) { 156 | saveGlobal(); 157 | } 158 | if ( config.notrycatch ) { 159 | this.testEnvironment.setup.call( this.testEnvironment ); 160 | return; 161 | } 162 | try { 163 | this.testEnvironment.setup.call( this.testEnvironment ); 164 | } catch( e ) { 165 | QUnit.pushFailure( "Setup failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 166 | } 167 | }, 168 | run: function() { 169 | config.current = this; 170 | 171 | var running = id( "qunit-testresult" ); 172 | 173 | if ( running ) { 174 | running.innerHTML = "Running:
" + this.nameHtml; 175 | } 176 | 177 | if ( this.async ) { 178 | QUnit.stop(); 179 | } 180 | 181 | this.callbackStarted = +new Date(); 182 | 183 | if ( config.notrycatch ) { 184 | this.callback.call( this.testEnvironment, QUnit.assert ); 185 | this.callbackRuntime = +new Date() - this.callbackStarted; 186 | return; 187 | } 188 | 189 | try { 190 | this.callback.call( this.testEnvironment, QUnit.assert ); 191 | this.callbackRuntime = +new Date() - this.callbackStarted; 192 | } catch( e ) { 193 | this.callbackRuntime = +new Date() - this.callbackStarted; 194 | 195 | QUnit.pushFailure( "Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 196 | // else next test will carry the responsibility 197 | saveGlobal(); 198 | 199 | // Restart the tests if they're blocking 200 | if ( config.blocking ) { 201 | QUnit.start(); 202 | } 203 | } 204 | }, 205 | teardown: function() { 206 | config.current = this; 207 | if ( config.notrycatch ) { 208 | if ( typeof this.callbackRuntime === "undefined" ) { 209 | this.callbackRuntime = +new Date() - this.callbackStarted; 210 | } 211 | this.testEnvironment.teardown.call( this.testEnvironment ); 212 | return; 213 | } else { 214 | try { 215 | this.testEnvironment.teardown.call( this.testEnvironment ); 216 | } catch( e ) { 217 | QUnit.pushFailure( "Teardown failed on " + this.testName + ": " + ( e.message || e ), extractStacktrace( e, 1 ) ); 218 | } 219 | } 220 | checkPollution(); 221 | }, 222 | finish: function() { 223 | config.current = this; 224 | if ( config.requireExpects && this.expected === null ) { 225 | QUnit.pushFailure( "Expected number of assertions to be defined, but expect() was not called.", this.stack ); 226 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 227 | QUnit.pushFailure( "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack ); 228 | } else if ( this.expected === null && !this.assertions.length ) { 229 | QUnit.pushFailure( "Expected at least one assertion, but none were run - call expect(0) to accept zero assertions.", this.stack ); 230 | } 231 | 232 | var i, assertion, a, b, time, li, ol, 233 | test = this, 234 | good = 0, 235 | bad = 0, 236 | tests = id( "qunit-tests" ); 237 | 238 | this.runtime = +new Date() - this.started; 239 | config.stats.all += this.assertions.length; 240 | config.moduleStats.all += this.assertions.length; 241 | 242 | if ( tests ) { 243 | ol = document.createElement( "ol" ); 244 | ol.className = "qunit-assert-list"; 245 | 246 | for ( i = 0; i < this.assertions.length; i++ ) { 247 | assertion = this.assertions[i]; 248 | 249 | li = document.createElement( "li" ); 250 | li.className = assertion.result ? "pass" : "fail"; 251 | li.innerHTML = assertion.message || ( assertion.result ? "okay" : "failed" ); 252 | ol.appendChild( li ); 253 | 254 | if ( assertion.result ) { 255 | good++; 256 | } else { 257 | bad++; 258 | config.stats.bad++; 259 | config.moduleStats.bad++; 260 | } 261 | } 262 | 263 | // store result when possible 264 | if ( QUnit.config.reorder && defined.sessionStorage ) { 265 | if ( bad ) { 266 | sessionStorage.setItem( "qunit-test-" + this.module + "-" + this.testName, bad ); 267 | } else { 268 | sessionStorage.removeItem( "qunit-test-" + this.module + "-" + this.testName ); 269 | } 270 | } 271 | 272 | if ( bad === 0 ) { 273 | addClass( ol, "qunit-collapsed" ); 274 | } 275 | 276 | // `b` initialized at top of scope 277 | b = document.createElement( "strong" ); 278 | b.innerHTML = this.nameHtml + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 279 | 280 | addEvent(b, "click", function() { 281 | var next = b.parentNode.lastChild, 282 | collapsed = hasClass( next, "qunit-collapsed" ); 283 | ( collapsed ? removeClass : addClass )( next, "qunit-collapsed" ); 284 | }); 285 | 286 | addEvent(b, "dblclick", function( e ) { 287 | var target = e && e.target ? e.target : window.event.srcElement; 288 | if ( target.nodeName.toLowerCase() === "span" || target.nodeName.toLowerCase() === "b" ) { 289 | target = target.parentNode; 290 | } 291 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 292 | window.location = QUnit.url({ testNumber: test.testNumber }); 293 | } 294 | }); 295 | 296 | // `time` initialized at top of scope 297 | time = document.createElement( "span" ); 298 | time.className = "runtime"; 299 | time.innerHTML = this.runtime + " ms"; 300 | 301 | // `li` initialized at top of scope 302 | li = id( this.id ); 303 | li.className = bad ? "fail" : "pass"; 304 | li.removeChild( li.firstChild ); 305 | a = li.firstChild; 306 | li.appendChild( b ); 307 | li.appendChild( a ); 308 | li.appendChild( time ); 309 | li.appendChild( ol ); 310 | 311 | } else { 312 | for ( i = 0; i < this.assertions.length; i++ ) { 313 | if ( !this.assertions[i].result ) { 314 | bad++; 315 | config.stats.bad++; 316 | config.moduleStats.bad++; 317 | } 318 | } 319 | } 320 | 321 | runLoggingCallbacks( "testDone", QUnit, { 322 | name: this.testName, 323 | module: this.module, 324 | failed: bad, 325 | passed: this.assertions.length - bad, 326 | total: this.assertions.length, 327 | duration: this.runtime 328 | }); 329 | 330 | QUnit.reset(); 331 | 332 | config.current = undefined; 333 | }, 334 | 335 | queue: function() { 336 | var bad, 337 | test = this; 338 | 339 | synchronize(function() { 340 | test.init(); 341 | }); 342 | function run() { 343 | // each of these can by async 344 | synchronize(function() { 345 | test.setup(); 346 | }); 347 | synchronize(function() { 348 | test.run(); 349 | }); 350 | synchronize(function() { 351 | test.teardown(); 352 | }); 353 | synchronize(function() { 354 | test.finish(); 355 | }); 356 | } 357 | 358 | // `bad` initialized at top of scope 359 | // defer when previous test run passed, if storage is available 360 | bad = QUnit.config.reorder && defined.sessionStorage && 361 | +sessionStorage.getItem( "qunit-test-" + this.module + "-" + this.testName ); 362 | 363 | if ( bad ) { 364 | run(); 365 | } else { 366 | synchronize( run, true ); 367 | } 368 | } 369 | }; 370 | 371 | // Root QUnit object. 372 | // `QUnit` initialized at top of scope 373 | QUnit = { 374 | 375 | // call on start of module test to prepend name to all tests 376 | module: function( name, testEnvironment ) { 377 | config.currentModule = name; 378 | config.currentModuleTestEnvironment = testEnvironment; 379 | config.modules[name] = true; 380 | }, 381 | 382 | asyncTest: function( testName, expected, callback ) { 383 | if ( arguments.length === 2 ) { 384 | callback = expected; 385 | expected = null; 386 | } 387 | 388 | QUnit.test( testName, expected, callback, true ); 389 | }, 390 | 391 | test: function( testName, expected, callback, async ) { 392 | var test, 393 | nameHtml = "" + escapeText( testName ) + ""; 394 | 395 | if ( arguments.length === 2 ) { 396 | callback = expected; 397 | expected = null; 398 | } 399 | 400 | if ( config.currentModule ) { 401 | nameHtml = "" + escapeText( config.currentModule ) + ": " + nameHtml; 402 | } 403 | 404 | test = new Test({ 405 | nameHtml: nameHtml, 406 | testName: testName, 407 | expected: expected, 408 | async: async, 409 | callback: callback, 410 | module: config.currentModule, 411 | moduleTestEnvironment: config.currentModuleTestEnvironment, 412 | stack: sourceFromStacktrace( 2 ) 413 | }); 414 | 415 | if ( !validTest( test ) ) { 416 | return; 417 | } 418 | 419 | test.queue(); 420 | }, 421 | 422 | // Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 423 | expect: function( asserts ) { 424 | if (arguments.length === 1) { 425 | config.current.expected = asserts; 426 | } else { 427 | return config.current.expected; 428 | } 429 | }, 430 | 431 | start: function( count ) { 432 | // QUnit hasn't been initialized yet. 433 | // Note: RequireJS (et al) may delay onLoad 434 | if ( config.semaphore === undefined ) { 435 | QUnit.begin(function() { 436 | // This is triggered at the top of QUnit.load, push start() to the event loop, to allow QUnit.load to finish first 437 | setTimeout(function() { 438 | QUnit.start( count ); 439 | }); 440 | }); 441 | return; 442 | } 443 | 444 | config.semaphore -= count || 1; 445 | // don't start until equal number of stop-calls 446 | if ( config.semaphore > 0 ) { 447 | return; 448 | } 449 | // ignore if start is called more often then stop 450 | if ( config.semaphore < 0 ) { 451 | config.semaphore = 0; 452 | QUnit.pushFailure( "Called start() while already started (QUnit.config.semaphore was 0 already)", null, sourceFromStacktrace(2) ); 453 | return; 454 | } 455 | // A slight delay, to avoid any current callbacks 456 | if ( defined.setTimeout ) { 457 | window.setTimeout(function() { 458 | if ( config.semaphore > 0 ) { 459 | return; 460 | } 461 | if ( config.timeout ) { 462 | clearTimeout( config.timeout ); 463 | } 464 | 465 | config.blocking = false; 466 | process( true ); 467 | }, 13); 468 | } else { 469 | config.blocking = false; 470 | process( true ); 471 | } 472 | }, 473 | 474 | stop: function( count ) { 475 | config.semaphore += count || 1; 476 | config.blocking = true; 477 | 478 | if ( config.testTimeout && defined.setTimeout ) { 479 | clearTimeout( config.timeout ); 480 | config.timeout = window.setTimeout(function() { 481 | QUnit.ok( false, "Test timed out" ); 482 | config.semaphore = 1; 483 | QUnit.start(); 484 | }, config.testTimeout ); 485 | } 486 | } 487 | }; 488 | 489 | // `assert` initialized at top of scope 490 | // Asssert helpers 491 | // All of these must either call QUnit.push() or manually do: 492 | // - runLoggingCallbacks( "log", .. ); 493 | // - config.current.assertions.push({ .. }); 494 | // We attach it to the QUnit object *after* we expose the public API, 495 | // otherwise `assert` will become a global variable in browsers (#341). 496 | assert = { 497 | /** 498 | * Asserts rough true-ish result. 499 | * @name ok 500 | * @function 501 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 502 | */ 503 | ok: function( result, msg ) { 504 | if ( !config.current ) { 505 | throw new Error( "ok() assertion outside test context, was " + sourceFromStacktrace(2) ); 506 | } 507 | result = !!result; 508 | 509 | var source, 510 | details = { 511 | module: config.current.module, 512 | name: config.current.testName, 513 | result: result, 514 | message: msg 515 | }; 516 | 517 | msg = escapeText( msg || (result ? "okay" : "failed" ) ); 518 | msg = "" + msg + ""; 519 | 520 | if ( !result ) { 521 | source = sourceFromStacktrace( 2 ); 522 | if ( source ) { 523 | details.source = source; 524 | msg += "
Source:
" + escapeText( source ) + "
"; 525 | } 526 | } 527 | runLoggingCallbacks( "log", QUnit, details ); 528 | config.current.assertions.push({ 529 | result: result, 530 | message: msg 531 | }); 532 | }, 533 | 534 | /** 535 | * Assert that the first two arguments are equal, with an optional message. 536 | * Prints out both actual and expected values. 537 | * @name equal 538 | * @function 539 | * @example equal( format( "Received {0} bytes.", 2), "Received 2 bytes.", "format() replaces {0} with next argument" ); 540 | */ 541 | equal: function( actual, expected, message ) { 542 | /*jshint eqeqeq:false */ 543 | QUnit.push( expected == actual, actual, expected, message ); 544 | }, 545 | 546 | /** 547 | * @name notEqual 548 | * @function 549 | */ 550 | notEqual: function( actual, expected, message ) { 551 | /*jshint eqeqeq:false */ 552 | QUnit.push( expected != actual, actual, expected, message ); 553 | }, 554 | 555 | /** 556 | * @name propEqual 557 | * @function 558 | */ 559 | propEqual: function( actual, expected, message ) { 560 | actual = objectValues(actual); 561 | expected = objectValues(expected); 562 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 563 | }, 564 | 565 | /** 566 | * @name notPropEqual 567 | * @function 568 | */ 569 | notPropEqual: function( actual, expected, message ) { 570 | actual = objectValues(actual); 571 | expected = objectValues(expected); 572 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 573 | }, 574 | 575 | /** 576 | * @name deepEqual 577 | * @function 578 | */ 579 | deepEqual: function( actual, expected, message ) { 580 | QUnit.push( QUnit.equiv(actual, expected), actual, expected, message ); 581 | }, 582 | 583 | /** 584 | * @name notDeepEqual 585 | * @function 586 | */ 587 | notDeepEqual: function( actual, expected, message ) { 588 | QUnit.push( !QUnit.equiv(actual, expected), actual, expected, message ); 589 | }, 590 | 591 | /** 592 | * @name strictEqual 593 | * @function 594 | */ 595 | strictEqual: function( actual, expected, message ) { 596 | QUnit.push( expected === actual, actual, expected, message ); 597 | }, 598 | 599 | /** 600 | * @name notStrictEqual 601 | * @function 602 | */ 603 | notStrictEqual: function( actual, expected, message ) { 604 | QUnit.push( expected !== actual, actual, expected, message ); 605 | }, 606 | 607 | "throws": function( block, expected, message ) { 608 | var actual, 609 | expectedOutput = expected, 610 | ok = false; 611 | 612 | // 'expected' is optional 613 | if ( typeof expected === "string" ) { 614 | message = expected; 615 | expected = null; 616 | } 617 | 618 | config.current.ignoreGlobalErrors = true; 619 | try { 620 | block.call( config.current.testEnvironment ); 621 | } catch (e) { 622 | actual = e; 623 | } 624 | config.current.ignoreGlobalErrors = false; 625 | 626 | if ( actual ) { 627 | // we don't want to validate thrown error 628 | if ( !expected ) { 629 | ok = true; 630 | expectedOutput = null; 631 | // expected is a regexp 632 | } else if ( QUnit.objectType( expected ) === "regexp" ) { 633 | ok = expected.test( errorString( actual ) ); 634 | // expected is a constructor 635 | } else if ( actual instanceof expected ) { 636 | ok = true; 637 | // expected is a validation function which returns true is validation passed 638 | } else if ( expected.call( {}, actual ) === true ) { 639 | expectedOutput = null; 640 | ok = true; 641 | } 642 | 643 | QUnit.push( ok, actual, expectedOutput, message ); 644 | } else { 645 | QUnit.pushFailure( message, null, 'No exception was thrown.' ); 646 | } 647 | } 648 | }; 649 | 650 | /** 651 | * @deprecate since 1.8.0 652 | * Kept assertion helpers in root for backwards compatibility. 653 | */ 654 | extend( QUnit, assert ); 655 | 656 | /** 657 | * @deprecated since 1.9.0 658 | * Kept root "raises()" for backwards compatibility. 659 | * (Note that we don't introduce assert.raises). 660 | */ 661 | QUnit.raises = assert[ "throws" ]; 662 | 663 | /** 664 | * @deprecated since 1.0.0, replaced with error pushes since 1.3.0 665 | * Kept to avoid TypeErrors for undefined methods. 666 | */ 667 | QUnit.equals = function() { 668 | QUnit.push( false, false, false, "QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead" ); 669 | }; 670 | QUnit.same = function() { 671 | QUnit.push( false, false, false, "QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead" ); 672 | }; 673 | 674 | // We want access to the constructor's prototype 675 | (function() { 676 | function F() {} 677 | F.prototype = QUnit; 678 | QUnit = new F(); 679 | // Make F QUnit's constructor so that we can add to the prototype later 680 | QUnit.constructor = F; 681 | }()); 682 | 683 | /** 684 | * Config object: Maintain internal state 685 | * Later exposed as QUnit.config 686 | * `config` initialized at top of scope 687 | */ 688 | config = { 689 | // The queue of tests to run 690 | queue: [], 691 | 692 | // block until document ready 693 | blocking: true, 694 | 695 | // when enabled, show only failing tests 696 | // gets persisted through sessionStorage and can be changed in UI via checkbox 697 | hidepassed: false, 698 | 699 | // by default, run previously failed tests first 700 | // very useful in combination with "Hide passed tests" checked 701 | reorder: true, 702 | 703 | // by default, modify document.title when suite is done 704 | altertitle: true, 705 | 706 | // when enabled, all tests must call expect() 707 | requireExpects: false, 708 | 709 | // add checkboxes that are persisted in the query-string 710 | // when enabled, the id is set to `true` as a `QUnit.config` property 711 | urlConfig: [ 712 | { 713 | id: "noglobals", 714 | label: "Check for Globals", 715 | tooltip: "Enabling this will test if any test introduces new properties on the `window` object. Stored as query-strings." 716 | }, 717 | { 718 | id: "notrycatch", 719 | label: "No try-catch", 720 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging exceptions in IE reasonable. Stored as query-strings." 721 | } 722 | ], 723 | 724 | // Set of all modules. 725 | modules: {}, 726 | 727 | // logging callback queues 728 | begin: [], 729 | done: [], 730 | log: [], 731 | testStart: [], 732 | testDone: [], 733 | moduleStart: [], 734 | moduleDone: [] 735 | }; 736 | 737 | // Export global variables, unless an 'exports' object exists, 738 | // in that case we assume we're in CommonJS (dealt with on the bottom of the script) 739 | if ( typeof exports === "undefined" ) { 740 | extend( window, QUnit ); 741 | 742 | // Expose QUnit object 743 | window.QUnit = QUnit; 744 | } 745 | 746 | // Initialize more QUnit.config and QUnit.urlParams 747 | (function() { 748 | var i, 749 | location = window.location || { search: "", protocol: "file:" }, 750 | params = location.search.slice( 1 ).split( "&" ), 751 | length = params.length, 752 | urlParams = {}, 753 | current; 754 | 755 | if ( params[ 0 ] ) { 756 | for ( i = 0; i < length; i++ ) { 757 | current = params[ i ].split( "=" ); 758 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 759 | // allow just a key to turn on a flag, e.g., test.html?noglobals 760 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 761 | urlParams[ current[ 0 ] ] = current[ 1 ]; 762 | } 763 | } 764 | 765 | QUnit.urlParams = urlParams; 766 | 767 | // String search anywhere in moduleName+testName 768 | config.filter = urlParams.filter; 769 | 770 | // Exact match of the module name 771 | config.module = urlParams.module; 772 | 773 | config.testNumber = parseInt( urlParams.testNumber, 10 ) || null; 774 | 775 | // Figure out if we're running the tests from a server or not 776 | QUnit.isLocal = location.protocol === "file:"; 777 | }()); 778 | 779 | // Extend QUnit object, 780 | // these after set here because they should not be exposed as global functions 781 | extend( QUnit, { 782 | assert: assert, 783 | 784 | config: config, 785 | 786 | // Initialize the configuration options 787 | init: function() { 788 | extend( config, { 789 | stats: { all: 0, bad: 0 }, 790 | moduleStats: { all: 0, bad: 0 }, 791 | started: +new Date(), 792 | updateRate: 1000, 793 | blocking: false, 794 | autostart: true, 795 | autorun: false, 796 | filter: "", 797 | queue: [], 798 | semaphore: 1 799 | }); 800 | 801 | var tests, banner, result, 802 | qunit = id( "qunit" ); 803 | 804 | if ( qunit ) { 805 | qunit.innerHTML = 806 | "

" + escapeText( document.title ) + "

" + 807 | "

" + 808 | "
" + 809 | "

" + 810 | "
    "; 811 | } 812 | 813 | tests = id( "qunit-tests" ); 814 | banner = id( "qunit-banner" ); 815 | result = id( "qunit-testresult" ); 816 | 817 | if ( tests ) { 818 | tests.innerHTML = ""; 819 | } 820 | 821 | if ( banner ) { 822 | banner.className = ""; 823 | } 824 | 825 | if ( result ) { 826 | result.parentNode.removeChild( result ); 827 | } 828 | 829 | if ( tests ) { 830 | result = document.createElement( "p" ); 831 | result.id = "qunit-testresult"; 832 | result.className = "result"; 833 | tests.parentNode.insertBefore( result, tests ); 834 | result.innerHTML = "Running...
     "; 835 | } 836 | }, 837 | 838 | // Resets the test setup. Useful for tests that modify the DOM. 839 | reset: function() { 840 | var fixture = id( "qunit-fixture" ); 841 | if ( fixture ) { 842 | fixture.innerHTML = config.fixture; 843 | } 844 | }, 845 | 846 | // Trigger an event on an element. 847 | // @example triggerEvent( document.body, "click" ); 848 | triggerEvent: function( elem, type, event ) { 849 | if ( document.createEvent ) { 850 | event = document.createEvent( "MouseEvents" ); 851 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 852 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 853 | 854 | elem.dispatchEvent( event ); 855 | } else if ( elem.fireEvent ) { 856 | elem.fireEvent( "on" + type ); 857 | } 858 | }, 859 | 860 | // Safe object type checking 861 | is: function( type, obj ) { 862 | return QUnit.objectType( obj ) === type; 863 | }, 864 | 865 | objectType: function( obj ) { 866 | if ( typeof obj === "undefined" ) { 867 | return "undefined"; 868 | // consider: typeof null === object 869 | } 870 | if ( obj === null ) { 871 | return "null"; 872 | } 873 | 874 | var match = toString.call( obj ).match(/^\[object\s(.*)\]$/), 875 | type = match && match[1] || ""; 876 | 877 | switch ( type ) { 878 | case "Number": 879 | if ( isNaN(obj) ) { 880 | return "nan"; 881 | } 882 | return "number"; 883 | case "String": 884 | case "Boolean": 885 | case "Array": 886 | case "Date": 887 | case "RegExp": 888 | case "Function": 889 | return type.toLowerCase(); 890 | } 891 | if ( typeof obj === "object" ) { 892 | return "object"; 893 | } 894 | return undefined; 895 | }, 896 | 897 | push: function( result, actual, expected, message ) { 898 | if ( !config.current ) { 899 | throw new Error( "assertion outside test context, was " + sourceFromStacktrace() ); 900 | } 901 | 902 | var output, source, 903 | details = { 904 | module: config.current.module, 905 | name: config.current.testName, 906 | result: result, 907 | message: message, 908 | actual: actual, 909 | expected: expected 910 | }; 911 | 912 | message = escapeText( message ) || ( result ? "okay" : "failed" ); 913 | message = "" + message + ""; 914 | output = message; 915 | 916 | if ( !result ) { 917 | expected = escapeText( QUnit.jsDump.parse(expected) ); 918 | actual = escapeText( QUnit.jsDump.parse(actual) ); 919 | output += ""; 920 | 921 | if ( actual !== expected ) { 922 | output += ""; 923 | output += ""; 924 | } 925 | 926 | source = sourceFromStacktrace(); 927 | 928 | if ( source ) { 929 | details.source = source; 930 | output += ""; 931 | } 932 | 933 | output += "
    Expected:
    " + expected + "
    Result:
    " + actual + "
    Diff:
    " + QUnit.diff( expected, actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 934 | } 935 | 936 | runLoggingCallbacks( "log", QUnit, details ); 937 | 938 | config.current.assertions.push({ 939 | result: !!result, 940 | message: output 941 | }); 942 | }, 943 | 944 | pushFailure: function( message, source, actual ) { 945 | if ( !config.current ) { 946 | throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace(2) ); 947 | } 948 | 949 | var output, 950 | details = { 951 | module: config.current.module, 952 | name: config.current.testName, 953 | result: false, 954 | message: message 955 | }; 956 | 957 | message = escapeText( message ) || "error"; 958 | message = "" + message + ""; 959 | output = message; 960 | 961 | output += ""; 962 | 963 | if ( actual ) { 964 | output += ""; 965 | } 966 | 967 | if ( source ) { 968 | details.source = source; 969 | output += ""; 970 | } 971 | 972 | output += "
    Result:
    " + escapeText( actual ) + "
    Source:
    " + escapeText( source ) + "
    "; 973 | 974 | runLoggingCallbacks( "log", QUnit, details ); 975 | 976 | config.current.assertions.push({ 977 | result: false, 978 | message: output 979 | }); 980 | }, 981 | 982 | url: function( params ) { 983 | params = extend( extend( {}, QUnit.urlParams ), params ); 984 | var key, 985 | querystring = "?"; 986 | 987 | for ( key in params ) { 988 | if ( !hasOwn.call( params, key ) ) { 989 | continue; 990 | } 991 | querystring += encodeURIComponent( key ) + "=" + 992 | encodeURIComponent( params[ key ] ) + "&"; 993 | } 994 | return window.location.protocol + "//" + window.location.host + 995 | window.location.pathname + querystring.slice( 0, -1 ); 996 | }, 997 | 998 | extend: extend, 999 | id: id, 1000 | addEvent: addEvent 1001 | // load, equiv, jsDump, diff: Attached later 1002 | }); 1003 | 1004 | /** 1005 | * @deprecated: Created for backwards compatibility with test runner that set the hook function 1006 | * into QUnit.{hook}, instead of invoking it and passing the hook function. 1007 | * QUnit.constructor is set to the empty F() above so that we can add to it's prototype here. 1008 | * Doing this allows us to tell if the following methods have been overwritten on the actual 1009 | * QUnit object. 1010 | */ 1011 | extend( QUnit.constructor.prototype, { 1012 | 1013 | // Logging callbacks; all receive a single argument with the listed properties 1014 | // run test/logs.html for any related changes 1015 | begin: registerLoggingCallback( "begin" ), 1016 | 1017 | // done: { failed, passed, total, runtime } 1018 | done: registerLoggingCallback( "done" ), 1019 | 1020 | // log: { result, actual, expected, message } 1021 | log: registerLoggingCallback( "log" ), 1022 | 1023 | // testStart: { name } 1024 | testStart: registerLoggingCallback( "testStart" ), 1025 | 1026 | // testDone: { name, failed, passed, total, duration } 1027 | testDone: registerLoggingCallback( "testDone" ), 1028 | 1029 | // moduleStart: { name } 1030 | moduleStart: registerLoggingCallback( "moduleStart" ), 1031 | 1032 | // moduleDone: { name, failed, passed, total } 1033 | moduleDone: registerLoggingCallback( "moduleDone" ) 1034 | }); 1035 | 1036 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 1037 | config.autorun = true; 1038 | } 1039 | 1040 | QUnit.load = function() { 1041 | runLoggingCallbacks( "begin", QUnit, {} ); 1042 | 1043 | // Initialize the config, saving the execution queue 1044 | var banner, filter, i, label, len, main, ol, toolbar, userAgent, val, 1045 | urlConfigCheckboxesContainer, urlConfigCheckboxes, moduleFilter, 1046 | numModules = 0, 1047 | moduleFilterHtml = "", 1048 | urlConfigHtml = "", 1049 | oldconfig = extend( {}, config ); 1050 | 1051 | QUnit.init(); 1052 | extend(config, oldconfig); 1053 | 1054 | config.blocking = false; 1055 | 1056 | len = config.urlConfig.length; 1057 | 1058 | for ( i = 0; i < len; i++ ) { 1059 | val = config.urlConfig[i]; 1060 | if ( typeof val === "string" ) { 1061 | val = { 1062 | id: val, 1063 | label: val, 1064 | tooltip: "[no tooltip available]" 1065 | }; 1066 | } 1067 | config[ val.id ] = QUnit.urlParams[ val.id ]; 1068 | urlConfigHtml += ""; 1074 | } 1075 | 1076 | moduleFilterHtml += ""; 1089 | 1090 | // `userAgent` initialized at top of scope 1091 | userAgent = id( "qunit-userAgent" ); 1092 | if ( userAgent ) { 1093 | userAgent.innerHTML = navigator.userAgent; 1094 | } 1095 | 1096 | // `banner` initialized at top of scope 1097 | banner = id( "qunit-header" ); 1098 | if ( banner ) { 1099 | banner.innerHTML = "" + banner.innerHTML + " "; 1100 | } 1101 | 1102 | // `toolbar` initialized at top of scope 1103 | toolbar = id( "qunit-testrunner-toolbar" ); 1104 | if ( toolbar ) { 1105 | // `filter` initialized at top of scope 1106 | filter = document.createElement( "input" ); 1107 | filter.type = "checkbox"; 1108 | filter.id = "qunit-filter-pass"; 1109 | 1110 | addEvent( filter, "click", function() { 1111 | var tmp, 1112 | ol = document.getElementById( "qunit-tests" ); 1113 | 1114 | if ( filter.checked ) { 1115 | ol.className = ol.className + " hidepass"; 1116 | } else { 1117 | tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 1118 | ol.className = tmp.replace( / hidepass /, " " ); 1119 | } 1120 | if ( defined.sessionStorage ) { 1121 | if (filter.checked) { 1122 | sessionStorage.setItem( "qunit-filter-passed-tests", "true" ); 1123 | } else { 1124 | sessionStorage.removeItem( "qunit-filter-passed-tests" ); 1125 | } 1126 | } 1127 | }); 1128 | 1129 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem( "qunit-filter-passed-tests" ) ) { 1130 | filter.checked = true; 1131 | // `ol` initialized at top of scope 1132 | ol = document.getElementById( "qunit-tests" ); 1133 | ol.className = ol.className + " hidepass"; 1134 | } 1135 | toolbar.appendChild( filter ); 1136 | 1137 | // `label` initialized at top of scope 1138 | label = document.createElement( "label" ); 1139 | label.setAttribute( "for", "qunit-filter-pass" ); 1140 | label.setAttribute( "title", "Only show tests and assertons that fail. Stored in sessionStorage." ); 1141 | label.innerHTML = "Hide passed tests"; 1142 | toolbar.appendChild( label ); 1143 | 1144 | urlConfigCheckboxesContainer = document.createElement("span"); 1145 | urlConfigCheckboxesContainer.innerHTML = urlConfigHtml; 1146 | urlConfigCheckboxes = urlConfigCheckboxesContainer.getElementsByTagName("input"); 1147 | // For oldIE support: 1148 | // * Add handlers to the individual elements instead of the container 1149 | // * Use "click" instead of "change" 1150 | // * Fallback from event.target to event.srcElement 1151 | addEvents( urlConfigCheckboxes, "click", function( event ) { 1152 | var params = {}, 1153 | target = event.target || event.srcElement; 1154 | params[ target.name ] = target.checked ? true : undefined; 1155 | window.location = QUnit.url( params ); 1156 | }); 1157 | toolbar.appendChild( urlConfigCheckboxesContainer ); 1158 | 1159 | if (numModules > 1) { 1160 | moduleFilter = document.createElement( 'span' ); 1161 | moduleFilter.setAttribute( 'id', 'qunit-modulefilter-container' ); 1162 | moduleFilter.innerHTML = moduleFilterHtml; 1163 | addEvent( moduleFilter.lastChild, "change", function() { 1164 | var selectBox = moduleFilter.getElementsByTagName("select")[0], 1165 | selectedModule = decodeURIComponent(selectBox.options[selectBox.selectedIndex].value); 1166 | 1167 | window.location = QUnit.url( { module: ( selectedModule === "" ) ? undefined : selectedModule } ); 1168 | }); 1169 | toolbar.appendChild(moduleFilter); 1170 | } 1171 | } 1172 | 1173 | // `main` initialized at top of scope 1174 | main = id( "qunit-fixture" ); 1175 | if ( main ) { 1176 | config.fixture = main.innerHTML; 1177 | } 1178 | 1179 | if ( config.autostart ) { 1180 | QUnit.start(); 1181 | } 1182 | }; 1183 | 1184 | addEvent( window, "load", QUnit.load ); 1185 | 1186 | // `onErrorFnPrev` initialized at top of scope 1187 | // Preserve other handlers 1188 | onErrorFnPrev = window.onerror; 1189 | 1190 | // Cover uncaught exceptions 1191 | // Returning true will surpress the default browser handler, 1192 | // returning false will let it run. 1193 | window.onerror = function ( error, filePath, linerNr ) { 1194 | var ret = false; 1195 | if ( onErrorFnPrev ) { 1196 | ret = onErrorFnPrev( error, filePath, linerNr ); 1197 | } 1198 | 1199 | // Treat return value as window.onerror itself does, 1200 | // Only do our handling if not surpressed. 1201 | if ( ret !== true ) { 1202 | if ( QUnit.config.current ) { 1203 | if ( QUnit.config.current.ignoreGlobalErrors ) { 1204 | return true; 1205 | } 1206 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1207 | } else { 1208 | QUnit.test( "global failure", extend( function() { 1209 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 1210 | }, { validTest: validTest } ) ); 1211 | } 1212 | return false; 1213 | } 1214 | 1215 | return ret; 1216 | }; 1217 | 1218 | function done() { 1219 | config.autorun = true; 1220 | 1221 | // Log the last module results 1222 | if ( config.currentModule ) { 1223 | runLoggingCallbacks( "moduleDone", QUnit, { 1224 | name: config.currentModule, 1225 | failed: config.moduleStats.bad, 1226 | passed: config.moduleStats.all - config.moduleStats.bad, 1227 | total: config.moduleStats.all 1228 | }); 1229 | } 1230 | 1231 | var i, key, 1232 | banner = id( "qunit-banner" ), 1233 | tests = id( "qunit-tests" ), 1234 | runtime = +new Date() - config.started, 1235 | passed = config.stats.all - config.stats.bad, 1236 | html = [ 1237 | "Tests completed in ", 1238 | runtime, 1239 | " milliseconds.
    ", 1240 | "", 1241 | passed, 1242 | " assertions of ", 1243 | config.stats.all, 1244 | " passed, ", 1245 | config.stats.bad, 1246 | " failed." 1247 | ].join( "" ); 1248 | 1249 | if ( banner ) { 1250 | banner.className = ( config.stats.bad ? "qunit-fail" : "qunit-pass" ); 1251 | } 1252 | 1253 | if ( tests ) { 1254 | id( "qunit-testresult" ).innerHTML = html; 1255 | } 1256 | 1257 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 1258 | // show ✖ for good, ✔ for bad suite result in title 1259 | // use escape sequences in case file gets loaded with non-utf-8-charset 1260 | document.title = [ 1261 | ( config.stats.bad ? "\u2716" : "\u2714" ), 1262 | document.title.replace( /^[\u2714\u2716] /i, "" ) 1263 | ].join( " " ); 1264 | } 1265 | 1266 | // clear own sessionStorage items if all tests passed 1267 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 1268 | // `key` & `i` initialized at top of scope 1269 | for ( i = 0; i < sessionStorage.length; i++ ) { 1270 | key = sessionStorage.key( i++ ); 1271 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 1272 | sessionStorage.removeItem( key ); 1273 | } 1274 | } 1275 | } 1276 | 1277 | // scroll back to top to show results 1278 | if ( window.scrollTo ) { 1279 | window.scrollTo(0, 0); 1280 | } 1281 | 1282 | runLoggingCallbacks( "done", QUnit, { 1283 | failed: config.stats.bad, 1284 | passed: passed, 1285 | total: config.stats.all, 1286 | runtime: runtime 1287 | }); 1288 | } 1289 | 1290 | /** @return Boolean: true if this test should be ran */ 1291 | function validTest( test ) { 1292 | var include, 1293 | filter = config.filter && config.filter.toLowerCase(), 1294 | module = config.module && config.module.toLowerCase(), 1295 | fullName = (test.module + ": " + test.testName).toLowerCase(); 1296 | 1297 | // Internally-generated tests are always valid 1298 | if ( test.callback && test.callback.validTest === validTest ) { 1299 | delete test.callback.validTest; 1300 | return true; 1301 | } 1302 | 1303 | if ( config.testNumber ) { 1304 | return test.testNumber === config.testNumber; 1305 | } 1306 | 1307 | if ( module && ( !test.module || test.module.toLowerCase() !== module ) ) { 1308 | return false; 1309 | } 1310 | 1311 | if ( !filter ) { 1312 | return true; 1313 | } 1314 | 1315 | include = filter.charAt( 0 ) !== "!"; 1316 | if ( !include ) { 1317 | filter = filter.slice( 1 ); 1318 | } 1319 | 1320 | // If the filter matches, we need to honour include 1321 | if ( fullName.indexOf( filter ) !== -1 ) { 1322 | return include; 1323 | } 1324 | 1325 | // Otherwise, do the opposite 1326 | return !include; 1327 | } 1328 | 1329 | // so far supports only Firefox, Chrome and Opera (buggy), Safari (for real exceptions) 1330 | // Later Safari and IE10 are supposed to support error.stack as well 1331 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 1332 | function extractStacktrace( e, offset ) { 1333 | offset = offset === undefined ? 3 : offset; 1334 | 1335 | var stack, include, i; 1336 | 1337 | if ( e.stacktrace ) { 1338 | // Opera 1339 | return e.stacktrace.split( "\n" )[ offset + 3 ]; 1340 | } else if ( e.stack ) { 1341 | // Firefox, Chrome 1342 | stack = e.stack.split( "\n" ); 1343 | if (/^error$/i.test( stack[0] ) ) { 1344 | stack.shift(); 1345 | } 1346 | if ( fileName ) { 1347 | include = []; 1348 | for ( i = offset; i < stack.length; i++ ) { 1349 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 1350 | break; 1351 | } 1352 | include.push( stack[ i ] ); 1353 | } 1354 | if ( include.length ) { 1355 | return include.join( "\n" ); 1356 | } 1357 | } 1358 | return stack[ offset ]; 1359 | } else if ( e.sourceURL ) { 1360 | // Safari, PhantomJS 1361 | // hopefully one day Safari provides actual stacktraces 1362 | // exclude useless self-reference for generated Error objects 1363 | if ( /qunit.js$/.test( e.sourceURL ) ) { 1364 | return; 1365 | } 1366 | // for actual exceptions, this is useful 1367 | return e.sourceURL + ":" + e.line; 1368 | } 1369 | } 1370 | function sourceFromStacktrace( offset ) { 1371 | try { 1372 | throw new Error(); 1373 | } catch ( e ) { 1374 | return extractStacktrace( e, offset ); 1375 | } 1376 | } 1377 | 1378 | /** 1379 | * Escape text for attribute or text content. 1380 | */ 1381 | function escapeText( s ) { 1382 | if ( !s ) { 1383 | return ""; 1384 | } 1385 | s = s + ""; 1386 | // Both single quotes and double quotes (for attributes) 1387 | return s.replace( /['"<>&]/g, function( s ) { 1388 | switch( s ) { 1389 | case '\'': 1390 | return '''; 1391 | case '"': 1392 | return '"'; 1393 | case '<': 1394 | return '<'; 1395 | case '>': 1396 | return '>'; 1397 | case '&': 1398 | return '&'; 1399 | } 1400 | }); 1401 | } 1402 | 1403 | function synchronize( callback, last ) { 1404 | config.queue.push( callback ); 1405 | 1406 | if ( config.autorun && !config.blocking ) { 1407 | process( last ); 1408 | } 1409 | } 1410 | 1411 | function process( last ) { 1412 | function next() { 1413 | process( last ); 1414 | } 1415 | var start = new Date().getTime(); 1416 | config.depth = config.depth ? config.depth + 1 : 1; 1417 | 1418 | while ( config.queue.length && !config.blocking ) { 1419 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 1420 | config.queue.shift()(); 1421 | } else { 1422 | window.setTimeout( next, 13 ); 1423 | break; 1424 | } 1425 | } 1426 | config.depth--; 1427 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 1428 | done(); 1429 | } 1430 | } 1431 | 1432 | function saveGlobal() { 1433 | config.pollution = []; 1434 | 1435 | if ( config.noglobals ) { 1436 | for ( var key in window ) { 1437 | // in Opera sometimes DOM element ids show up here, ignore them 1438 | if ( !hasOwn.call( window, key ) || /^qunit-test-output/.test( key ) ) { 1439 | continue; 1440 | } 1441 | config.pollution.push( key ); 1442 | } 1443 | } 1444 | } 1445 | 1446 | function checkPollution() { 1447 | var newGlobals, 1448 | deletedGlobals, 1449 | old = config.pollution; 1450 | 1451 | saveGlobal(); 1452 | 1453 | newGlobals = diff( config.pollution, old ); 1454 | if ( newGlobals.length > 0 ) { 1455 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join(", ") ); 1456 | } 1457 | 1458 | deletedGlobals = diff( old, config.pollution ); 1459 | if ( deletedGlobals.length > 0 ) { 1460 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1461 | } 1462 | } 1463 | 1464 | // returns a new Array with the elements that are in a but not in b 1465 | function diff( a, b ) { 1466 | var i, j, 1467 | result = a.slice(); 1468 | 1469 | for ( i = 0; i < result.length; i++ ) { 1470 | for ( j = 0; j < b.length; j++ ) { 1471 | if ( result[i] === b[j] ) { 1472 | result.splice( i, 1 ); 1473 | i--; 1474 | break; 1475 | } 1476 | } 1477 | } 1478 | return result; 1479 | } 1480 | 1481 | function extend( a, b ) { 1482 | for ( var prop in b ) { 1483 | if ( b[ prop ] === undefined ) { 1484 | delete a[ prop ]; 1485 | 1486 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1487 | } else if ( prop !== "constructor" || a !== window ) { 1488 | a[ prop ] = b[ prop ]; 1489 | } 1490 | } 1491 | 1492 | return a; 1493 | } 1494 | 1495 | /** 1496 | * @param {HTMLElement} elem 1497 | * @param {string} type 1498 | * @param {Function} fn 1499 | */ 1500 | function addEvent( elem, type, fn ) { 1501 | // Standards-based browsers 1502 | if ( elem.addEventListener ) { 1503 | elem.addEventListener( type, fn, false ); 1504 | // IE 1505 | } else { 1506 | elem.attachEvent( "on" + type, fn ); 1507 | } 1508 | } 1509 | 1510 | /** 1511 | * @param {Array|NodeList} elems 1512 | * @param {string} type 1513 | * @param {Function} fn 1514 | */ 1515 | function addEvents( elems, type, fn ) { 1516 | var i = elems.length; 1517 | while ( i-- ) { 1518 | addEvent( elems[i], type, fn ); 1519 | } 1520 | } 1521 | 1522 | function hasClass( elem, name ) { 1523 | return (" " + elem.className + " ").indexOf(" " + name + " ") > -1; 1524 | } 1525 | 1526 | function addClass( elem, name ) { 1527 | if ( !hasClass( elem, name ) ) { 1528 | elem.className += (elem.className ? " " : "") + name; 1529 | } 1530 | } 1531 | 1532 | function removeClass( elem, name ) { 1533 | var set = " " + elem.className + " "; 1534 | // Class name may appear multiple times 1535 | while ( set.indexOf(" " + name + " ") > -1 ) { 1536 | set = set.replace(" " + name + " " , " "); 1537 | } 1538 | // If possible, trim it for prettiness, but not neccecarily 1539 | elem.className = window.jQuery ? jQuery.trim( set ) : ( set.trim ? set.trim() : set ); 1540 | } 1541 | 1542 | function id( name ) { 1543 | return !!( typeof document !== "undefined" && document && document.getElementById ) && 1544 | document.getElementById( name ); 1545 | } 1546 | 1547 | function registerLoggingCallback( key ) { 1548 | return function( callback ) { 1549 | config[key].push( callback ); 1550 | }; 1551 | } 1552 | 1553 | // Supports deprecated method of completely overwriting logging callbacks 1554 | function runLoggingCallbacks( key, scope, args ) { 1555 | var i, callbacks; 1556 | if ( QUnit.hasOwnProperty( key ) ) { 1557 | QUnit[ key ].call(scope, args ); 1558 | } else { 1559 | callbacks = config[ key ]; 1560 | for ( i = 0; i < callbacks.length; i++ ) { 1561 | callbacks[ i ].call( scope, args ); 1562 | } 1563 | } 1564 | } 1565 | 1566 | // Test for equality any JavaScript type. 1567 | // Author: Philippe Rathé 1568 | QUnit.equiv = (function() { 1569 | 1570 | // Call the o related callback with the given arguments. 1571 | function bindCallbacks( o, callbacks, args ) { 1572 | var prop = QUnit.objectType( o ); 1573 | if ( prop ) { 1574 | if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { 1575 | return callbacks[ prop ].apply( callbacks, args ); 1576 | } else { 1577 | return callbacks[ prop ]; // or undefined 1578 | } 1579 | } 1580 | } 1581 | 1582 | // the real equiv function 1583 | var innerEquiv, 1584 | // stack to decide between skip/abort functions 1585 | callers = [], 1586 | // stack to avoiding loops from circular referencing 1587 | parents = [], 1588 | 1589 | getProto = Object.getPrototypeOf || function ( obj ) { 1590 | return obj.__proto__; 1591 | }, 1592 | callbacks = (function () { 1593 | 1594 | // for string, boolean, number and null 1595 | function useStrictEquality( b, a ) { 1596 | /*jshint eqeqeq:false */ 1597 | if ( b instanceof a.constructor || a instanceof b.constructor ) { 1598 | // to catch short annotaion VS 'new' annotation of a 1599 | // declaration 1600 | // e.g. var i = 1; 1601 | // var j = new Number(1); 1602 | return a == b; 1603 | } else { 1604 | return a === b; 1605 | } 1606 | } 1607 | 1608 | return { 1609 | "string": useStrictEquality, 1610 | "boolean": useStrictEquality, 1611 | "number": useStrictEquality, 1612 | "null": useStrictEquality, 1613 | "undefined": useStrictEquality, 1614 | 1615 | "nan": function( b ) { 1616 | return isNaN( b ); 1617 | }, 1618 | 1619 | "date": function( b, a ) { 1620 | return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); 1621 | }, 1622 | 1623 | "regexp": function( b, a ) { 1624 | return QUnit.objectType( b ) === "regexp" && 1625 | // the regex itself 1626 | a.source === b.source && 1627 | // and its modifers 1628 | a.global === b.global && 1629 | // (gmi) ... 1630 | a.ignoreCase === b.ignoreCase && 1631 | a.multiline === b.multiline && 1632 | a.sticky === b.sticky; 1633 | }, 1634 | 1635 | // - skip when the property is a method of an instance (OOP) 1636 | // - abort otherwise, 1637 | // initial === would have catch identical references anyway 1638 | "function": function() { 1639 | var caller = callers[callers.length - 1]; 1640 | return caller !== Object && typeof caller !== "undefined"; 1641 | }, 1642 | 1643 | "array": function( b, a ) { 1644 | var i, j, len, loop; 1645 | 1646 | // b could be an object literal here 1647 | if ( QUnit.objectType( b ) !== "array" ) { 1648 | return false; 1649 | } 1650 | 1651 | len = a.length; 1652 | if ( len !== b.length ) { 1653 | // safe and faster 1654 | return false; 1655 | } 1656 | 1657 | // track reference to avoid circular references 1658 | parents.push( a ); 1659 | for ( i = 0; i < len; i++ ) { 1660 | loop = false; 1661 | for ( j = 0; j < parents.length; j++ ) { 1662 | if ( parents[j] === a[i] ) { 1663 | loop = true;// dont rewalk array 1664 | } 1665 | } 1666 | if ( !loop && !innerEquiv(a[i], b[i]) ) { 1667 | parents.pop(); 1668 | return false; 1669 | } 1670 | } 1671 | parents.pop(); 1672 | return true; 1673 | }, 1674 | 1675 | "object": function( b, a ) { 1676 | var i, j, loop, 1677 | // Default to true 1678 | eq = true, 1679 | aProperties = [], 1680 | bProperties = []; 1681 | 1682 | // comparing constructors is more strict than using 1683 | // instanceof 1684 | if ( a.constructor !== b.constructor ) { 1685 | // Allow objects with no prototype to be equivalent to 1686 | // objects with Object as their constructor. 1687 | if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) || 1688 | ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) { 1689 | return false; 1690 | } 1691 | } 1692 | 1693 | // stack constructor before traversing properties 1694 | callers.push( a.constructor ); 1695 | // track reference to avoid circular references 1696 | parents.push( a ); 1697 | 1698 | for ( i in a ) { // be strict: don't ensures hasOwnProperty 1699 | // and go deep 1700 | loop = false; 1701 | for ( j = 0; j < parents.length; j++ ) { 1702 | if ( parents[j] === a[i] ) { 1703 | // don't go down the same path twice 1704 | loop = true; 1705 | } 1706 | } 1707 | aProperties.push(i); // collect a's properties 1708 | 1709 | if (!loop && !innerEquiv( a[i], b[i] ) ) { 1710 | eq = false; 1711 | break; 1712 | } 1713 | } 1714 | 1715 | callers.pop(); // unstack, we are done 1716 | parents.pop(); 1717 | 1718 | for ( i in b ) { 1719 | bProperties.push( i ); // collect b's properties 1720 | } 1721 | 1722 | // Ensures identical properties name 1723 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1724 | } 1725 | }; 1726 | }()); 1727 | 1728 | innerEquiv = function() { // can take multiple arguments 1729 | var args = [].slice.apply( arguments ); 1730 | if ( args.length < 2 ) { 1731 | return true; // end transition 1732 | } 1733 | 1734 | return (function( a, b ) { 1735 | if ( a === b ) { 1736 | return true; // catch the most you can 1737 | } else if ( a === null || b === null || typeof a === "undefined" || 1738 | typeof b === "undefined" || 1739 | QUnit.objectType(a) !== QUnit.objectType(b) ) { 1740 | return false; // don't lose time with error prone cases 1741 | } else { 1742 | return bindCallbacks(a, callbacks, [ b, a ]); 1743 | } 1744 | 1745 | // apply transition with (1..n) arguments 1746 | }( args[0], args[1] ) && arguments.callee.apply( this, args.splice(1, args.length - 1 )) ); 1747 | }; 1748 | 1749 | return innerEquiv; 1750 | }()); 1751 | 1752 | /** 1753 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1754 | * http://flesler.blogspot.com Licensed under BSD 1755 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1756 | * 1757 | * @projectDescription Advanced and extensible data dumping for Javascript. 1758 | * @version 1.0.0 1759 | * @author Ariel Flesler 1760 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1761 | */ 1762 | QUnit.jsDump = (function() { 1763 | function quote( str ) { 1764 | return '"' + str.toString().replace( /"/g, '\\"' ) + '"'; 1765 | } 1766 | function literal( o ) { 1767 | return o + ""; 1768 | } 1769 | function join( pre, arr, post ) { 1770 | var s = jsDump.separator(), 1771 | base = jsDump.indent(), 1772 | inner = jsDump.indent(1); 1773 | if ( arr.join ) { 1774 | arr = arr.join( "," + s + inner ); 1775 | } 1776 | if ( !arr ) { 1777 | return pre + post; 1778 | } 1779 | return [ pre, inner + arr, base + post ].join(s); 1780 | } 1781 | function array( arr, stack ) { 1782 | var i = arr.length, ret = new Array(i); 1783 | this.up(); 1784 | while ( i-- ) { 1785 | ret[i] = this.parse( arr[i] , undefined , stack); 1786 | } 1787 | this.down(); 1788 | return join( "[", ret, "]" ); 1789 | } 1790 | 1791 | var reName = /^function (\w+)/, 1792 | jsDump = { 1793 | // type is used mostly internally, you can fix a (custom)type in advance 1794 | parse: function( obj, type, stack ) { 1795 | stack = stack || [ ]; 1796 | var inStack, res, 1797 | parser = this.parsers[ type || this.typeOf(obj) ]; 1798 | 1799 | type = typeof parser; 1800 | inStack = inArray( obj, stack ); 1801 | 1802 | if ( inStack !== -1 ) { 1803 | return "recursion(" + (inStack - stack.length) + ")"; 1804 | } 1805 | if ( type === "function" ) { 1806 | stack.push( obj ); 1807 | res = parser.call( this, obj, stack ); 1808 | stack.pop(); 1809 | return res; 1810 | } 1811 | return ( type === "string" ) ? parser : this.parsers.error; 1812 | }, 1813 | typeOf: function( obj ) { 1814 | var type; 1815 | if ( obj === null ) { 1816 | type = "null"; 1817 | } else if ( typeof obj === "undefined" ) { 1818 | type = "undefined"; 1819 | } else if ( QUnit.is( "regexp", obj) ) { 1820 | type = "regexp"; 1821 | } else if ( QUnit.is( "date", obj) ) { 1822 | type = "date"; 1823 | } else if ( QUnit.is( "function", obj) ) { 1824 | type = "function"; 1825 | } else if ( typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined" ) { 1826 | type = "window"; 1827 | } else if ( obj.nodeType === 9 ) { 1828 | type = "document"; 1829 | } else if ( obj.nodeType ) { 1830 | type = "node"; 1831 | } else if ( 1832 | // native arrays 1833 | toString.call( obj ) === "[object Array]" || 1834 | // NodeList objects 1835 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1836 | ) { 1837 | type = "array"; 1838 | } else if ( obj.constructor === Error.prototype.constructor ) { 1839 | type = "error"; 1840 | } else { 1841 | type = typeof obj; 1842 | } 1843 | return type; 1844 | }, 1845 | separator: function() { 1846 | return this.multiline ? this.HTML ? "
    " : "\n" : this.HTML ? " " : " "; 1847 | }, 1848 | // extra can be a number, shortcut for increasing-calling-decreasing 1849 | indent: function( extra ) { 1850 | if ( !this.multiline ) { 1851 | return ""; 1852 | } 1853 | var chr = this.indentChar; 1854 | if ( this.HTML ) { 1855 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1856 | } 1857 | return new Array( this._depth_ + (extra||0) ).join(chr); 1858 | }, 1859 | up: function( a ) { 1860 | this._depth_ += a || 1; 1861 | }, 1862 | down: function( a ) { 1863 | this._depth_ -= a || 1; 1864 | }, 1865 | setParser: function( name, parser ) { 1866 | this.parsers[name] = parser; 1867 | }, 1868 | // The next 3 are exposed so you can use them 1869 | quote: quote, 1870 | literal: literal, 1871 | join: join, 1872 | // 1873 | _depth_: 1, 1874 | // This is the list of parsers, to modify them, use jsDump.setParser 1875 | parsers: { 1876 | window: "[Window]", 1877 | document: "[Document]", 1878 | error: function(error) { 1879 | return "Error(\"" + error.message + "\")"; 1880 | }, 1881 | unknown: "[Unknown]", 1882 | "null": "null", 1883 | "undefined": "undefined", 1884 | "function": function( fn ) { 1885 | var ret = "function", 1886 | // functions never have name in IE 1887 | name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; 1888 | 1889 | if ( name ) { 1890 | ret += " " + name; 1891 | } 1892 | ret += "( "; 1893 | 1894 | ret = [ ret, QUnit.jsDump.parse( fn, "functionArgs" ), "){" ].join( "" ); 1895 | return join( ret, QUnit.jsDump.parse(fn,"functionCode" ), "}" ); 1896 | }, 1897 | array: array, 1898 | nodelist: array, 1899 | "arguments": array, 1900 | object: function( map, stack ) { 1901 | var ret = [ ], keys, key, val, i; 1902 | QUnit.jsDump.up(); 1903 | keys = []; 1904 | for ( key in map ) { 1905 | keys.push( key ); 1906 | } 1907 | keys.sort(); 1908 | for ( i = 0; i < keys.length; i++ ) { 1909 | key = keys[ i ]; 1910 | val = map[ key ]; 1911 | ret.push( QUnit.jsDump.parse( key, "key" ) + ": " + QUnit.jsDump.parse( val, undefined, stack ) ); 1912 | } 1913 | QUnit.jsDump.down(); 1914 | return join( "{", ret, "}" ); 1915 | }, 1916 | node: function( node ) { 1917 | var len, i, val, 1918 | open = QUnit.jsDump.HTML ? "<" : "<", 1919 | close = QUnit.jsDump.HTML ? ">" : ">", 1920 | tag = node.nodeName.toLowerCase(), 1921 | ret = open + tag, 1922 | attrs = node.attributes; 1923 | 1924 | if ( attrs ) { 1925 | for ( i = 0, len = attrs.length; i < len; i++ ) { 1926 | val = attrs[i].nodeValue; 1927 | // IE6 includes all attributes in .attributes, even ones not explicitly set. 1928 | // Those have values like undefined, null, 0, false, "" or "inherit". 1929 | if ( val && val !== "inherit" ) { 1930 | ret += " " + attrs[i].nodeName + "=" + QUnit.jsDump.parse( val, "attribute" ); 1931 | } 1932 | } 1933 | } 1934 | ret += close; 1935 | 1936 | // Show content of TextNode or CDATASection 1937 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 1938 | ret += node.nodeValue; 1939 | } 1940 | 1941 | return ret + open + "/" + tag + close; 1942 | }, 1943 | // function calls it internally, it's the arguments part of the function 1944 | functionArgs: function( fn ) { 1945 | var args, 1946 | l = fn.length; 1947 | 1948 | if ( !l ) { 1949 | return ""; 1950 | } 1951 | 1952 | args = new Array(l); 1953 | while ( l-- ) { 1954 | // 97 is 'a' 1955 | args[l] = String.fromCharCode(97+l); 1956 | } 1957 | return " " + args.join( ", " ) + " "; 1958 | }, 1959 | // object calls it internally, the key part of an item in a map 1960 | key: quote, 1961 | // function calls it internally, it's the content of the function 1962 | functionCode: "[code]", 1963 | // node calls it internally, it's an html attribute value 1964 | attribute: quote, 1965 | string: quote, 1966 | date: quote, 1967 | regexp: literal, 1968 | number: literal, 1969 | "boolean": literal 1970 | }, 1971 | // if true, entities are escaped ( <, >, \t, space and \n ) 1972 | HTML: false, 1973 | // indentation unit 1974 | indentChar: " ", 1975 | // if true, items in a collection, are separated by a \n, else just a space. 1976 | multiline: true 1977 | }; 1978 | 1979 | return jsDump; 1980 | }()); 1981 | 1982 | // from jquery.js 1983 | function inArray( elem, array ) { 1984 | if ( array.indexOf ) { 1985 | return array.indexOf( elem ); 1986 | } 1987 | 1988 | for ( var i = 0, length = array.length; i < length; i++ ) { 1989 | if ( array[ i ] === elem ) { 1990 | return i; 1991 | } 1992 | } 1993 | 1994 | return -1; 1995 | } 1996 | 1997 | /* 1998 | * Javascript Diff Algorithm 1999 | * By John Resig (http://ejohn.org/) 2000 | * Modified by Chu Alan "sprite" 2001 | * 2002 | * Released under the MIT license. 2003 | * 2004 | * More Info: 2005 | * http://ejohn.org/projects/javascript-diff-algorithm/ 2006 | * 2007 | * Usage: QUnit.diff(expected, actual) 2008 | * 2009 | * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) == "the quick brown fox jumped jumps over" 2010 | */ 2011 | QUnit.diff = (function() { 2012 | /*jshint eqeqeq:false, eqnull:true */ 2013 | function diff( o, n ) { 2014 | var i, 2015 | ns = {}, 2016 | os = {}; 2017 | 2018 | for ( i = 0; i < n.length; i++ ) { 2019 | if ( !hasOwn.call( ns, n[i] ) ) { 2020 | ns[ n[i] ] = { 2021 | rows: [], 2022 | o: null 2023 | }; 2024 | } 2025 | ns[ n[i] ].rows.push( i ); 2026 | } 2027 | 2028 | for ( i = 0; i < o.length; i++ ) { 2029 | if ( !hasOwn.call( os, o[i] ) ) { 2030 | os[ o[i] ] = { 2031 | rows: [], 2032 | n: null 2033 | }; 2034 | } 2035 | os[ o[i] ].rows.push( i ); 2036 | } 2037 | 2038 | for ( i in ns ) { 2039 | if ( !hasOwn.call( ns, i ) ) { 2040 | continue; 2041 | } 2042 | if ( ns[i].rows.length === 1 && hasOwn.call( os, i ) && os[i].rows.length === 1 ) { 2043 | n[ ns[i].rows[0] ] = { 2044 | text: n[ ns[i].rows[0] ], 2045 | row: os[i].rows[0] 2046 | }; 2047 | o[ os[i].rows[0] ] = { 2048 | text: o[ os[i].rows[0] ], 2049 | row: ns[i].rows[0] 2050 | }; 2051 | } 2052 | } 2053 | 2054 | for ( i = 0; i < n.length - 1; i++ ) { 2055 | if ( n[i].text != null && n[ i + 1 ].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null && 2056 | n[ i + 1 ] == o[ n[i].row + 1 ] ) { 2057 | 2058 | n[ i + 1 ] = { 2059 | text: n[ i + 1 ], 2060 | row: n[i].row + 1 2061 | }; 2062 | o[ n[i].row + 1 ] = { 2063 | text: o[ n[i].row + 1 ], 2064 | row: i + 1 2065 | }; 2066 | } 2067 | } 2068 | 2069 | for ( i = n.length - 1; i > 0; i-- ) { 2070 | if ( n[i].text != null && n[ i - 1 ].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null && 2071 | n[ i - 1 ] == o[ n[i].row - 1 ]) { 2072 | 2073 | n[ i - 1 ] = { 2074 | text: n[ i - 1 ], 2075 | row: n[i].row - 1 2076 | }; 2077 | o[ n[i].row - 1 ] = { 2078 | text: o[ n[i].row - 1 ], 2079 | row: i - 1 2080 | }; 2081 | } 2082 | } 2083 | 2084 | return { 2085 | o: o, 2086 | n: n 2087 | }; 2088 | } 2089 | 2090 | return function( o, n ) { 2091 | o = o.replace( /\s+$/, "" ); 2092 | n = n.replace( /\s+$/, "" ); 2093 | 2094 | var i, pre, 2095 | str = "", 2096 | out = diff( o === "" ? [] : o.split(/\s+/), n === "" ? [] : n.split(/\s+/) ), 2097 | oSpace = o.match(/\s+/g), 2098 | nSpace = n.match(/\s+/g); 2099 | 2100 | if ( oSpace == null ) { 2101 | oSpace = [ " " ]; 2102 | } 2103 | else { 2104 | oSpace.push( " " ); 2105 | } 2106 | 2107 | if ( nSpace == null ) { 2108 | nSpace = [ " " ]; 2109 | } 2110 | else { 2111 | nSpace.push( " " ); 2112 | } 2113 | 2114 | if ( out.n.length === 0 ) { 2115 | for ( i = 0; i < out.o.length; i++ ) { 2116 | str += "" + out.o[i] + oSpace[i] + ""; 2117 | } 2118 | } 2119 | else { 2120 | if ( out.n[0].text == null ) { 2121 | for ( n = 0; n < out.o.length && out.o[n].text == null; n++ ) { 2122 | str += "" + out.o[n] + oSpace[n] + ""; 2123 | } 2124 | } 2125 | 2126 | for ( i = 0; i < out.n.length; i++ ) { 2127 | if (out.n[i].text == null) { 2128 | str += "" + out.n[i] + nSpace[i] + ""; 2129 | } 2130 | else { 2131 | // `pre` initialized at top of scope 2132 | pre = ""; 2133 | 2134 | for ( n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) { 2135 | pre += "" + out.o[n] + oSpace[n] + ""; 2136 | } 2137 | str += " " + out.n[i].text + nSpace[i] + pre; 2138 | } 2139 | } 2140 | } 2141 | 2142 | return str; 2143 | }; 2144 | }()); 2145 | 2146 | // for CommonJS enviroments, export everything 2147 | if ( typeof exports !== "undefined" ) { 2148 | extend( exports, QUnit ); 2149 | } 2150 | 2151 | // get at whatever the global object is, like window in browsers 2152 | }( (function() {return this;}.call()) )); -------------------------------------------------------------------------------- /libs/shoestring-dev.js: -------------------------------------------------------------------------------- 1 | /*! Shoestring - v1.0.3 - 2015-04-10 2 | * http://github.com/filamentgroup/shoestring/ 3 | * Copyright (c) 2015 Scott Jehl, Filament Group, Inc; Licensed MIT & GPLv2 */ 4 | (function( w, undefined ){ 5 | /** 6 | * The shoestring object constructor. 7 | * 8 | * @param {string,object} prim The selector to find or element to wrap. 9 | * @param {object} sec The context in which to match the `prim` selector. 10 | * @returns shoestring 11 | * @this window 12 | */ 13 | function shoestring( prim, sec ){ 14 | var pType = typeof( prim ), 15 | ret = [], 16 | sel; 17 | 18 | // return an empty shoestring object 19 | if( !prim ){ 20 | return new Shoestring( ret ); 21 | } 22 | 23 | // ready calls 24 | if( prim.call ){ 25 | return shoestring.ready( prim ); 26 | } 27 | 28 | // handle re-wrapping shoestring objects 29 | if( prim.constructor === Shoestring && !sec ){ 30 | return prim; 31 | } 32 | 33 | // if string starting with <, make html 34 | if( pType === "string" && prim.indexOf( "<" ) === 0 ){ 35 | var dfrag = document.createElement( "div" ); 36 | 37 | dfrag.innerHTML = prim; 38 | 39 | // TODO depends on children (circular) 40 | return shoestring( dfrag ).children().each(function(){ 41 | dfrag.removeChild( this ); 42 | }); 43 | } 44 | 45 | // if string, it's a selector, use qsa 46 | if( pType === "string" ){ 47 | if( sec ){ 48 | return shoestring( sec ).find( prim ); 49 | } 50 | 51 | try { 52 | sel = document.querySelectorAll( prim ); 53 | } catch( e ) { 54 | shoestring.error( 'queryselector', prim ); 55 | } 56 | 57 | return new Shoestring( sel, prim ); 58 | } 59 | 60 | // array like objects or node lists 61 | if( Object.prototype.toString.call( pType ) === '[object Array]' || 62 | (window.NodeList && prim instanceof window.NodeList) ){ 63 | 64 | return new Shoestring( prim, prim ); 65 | } 66 | 67 | // if it's an array, use all the elements 68 | if( prim.constructor === Array ){ 69 | return new Shoestring( prim, prim ); 70 | } 71 | 72 | // otherwise assume it's an object the we want at an index 73 | return new Shoestring( [prim], prim ); 74 | } 75 | 76 | var Shoestring = function( ret, prim ) { 77 | this.length = 0; 78 | this.selector = prim; 79 | shoestring.merge(this, ret); 80 | }; 81 | 82 | // TODO only required for tests 83 | Shoestring.prototype.reverse = [].reverse; 84 | 85 | // For adding element set methods 86 | shoestring.fn = Shoestring.prototype; 87 | 88 | // expose for testing purposes only 89 | shoestring.Shoestring = Shoestring; 90 | 91 | // For extending objects 92 | // TODO move to separate module when we use prototypes 93 | shoestring.extend = function( first, second ){ 94 | for( var i in second ){ 95 | if( second.hasOwnProperty( i ) ){ 96 | first[ i ] = second[ i ]; 97 | } 98 | } 99 | 100 | return first; 101 | }; 102 | 103 | // taken directly from jQuery 104 | shoestring.merge = function( first, second ) { 105 | var len, j, i; 106 | 107 | len = +second.length, 108 | j = 0, 109 | i = first.length; 110 | 111 | for ( ; j < len; j++ ) { 112 | first[ i++ ] = second[ j ]; 113 | } 114 | 115 | first.length = i; 116 | 117 | return first; 118 | }; 119 | 120 | // expose 121 | window.shoestring = shoestring; 122 | 123 | 124 | 125 | shoestring.enUS = { 126 | errors: { 127 | "prefix": "Shoestring does not support", 128 | 129 | "ajax-url-query": "data with urls that have existing query params", 130 | "click": "the click method. Try using trigger( 'click' ) instead.", 131 | "css-get" : "getting computed attributes from the DOM.", 132 | "has-class" : "the hasClass method. Try using .is( '.klassname' ) instead.", 133 | "html-function" : "passing a function into .html. Try generating the html you're passing in an outside function", 134 | "live-delegate" : "the .live or .delegate methods. Use .bind or .on instead.", 135 | "map": "the map method. Try using .each to make a new object.", 136 | "next-selector" : "passing selectors into .next, try .next().filter( selector )", 137 | "off-delegate" : ".off( events, selector, handler ) or .off( events, selector ). Use .off( eventName, callback ) instead.", 138 | "next-until" : "the .nextUntil method. Use .next in a loop until you reach the selector, don't include the selector", 139 | "on-delegate" : "the .on method with three or more arguments. Using .on( eventName, callback ) instead.", 140 | "outer-width": "the outerWidth method. Try combining .width() with .css for padding-left, padding-right, and the border of the left and right side.", 141 | "prev-selector" : "passing selectors into .prev, try .prev().filter( selector )", 142 | "prevall-selector" : "passing selectors into .prevAll, try .prevAll().filter( selector )", 143 | "queryselector": "all CSS selectors on querySelector (varies per browser support). Specifically, this failed: ", 144 | "show-hide": "the show or hide methods. Use display: block (or whatever you'd like it to be) or none instead", 145 | "text-setter": "setting text via the .text method.", 146 | "toggle-class" : "the toggleClass method. Try using addClass or removeClass instead.", 147 | "trim": "the trim method. Try using replace(/^\\s+|\\s+$/g, ''), or just String.prototype.trim if you don't need to support IE8" 148 | } 149 | }; 150 | 151 | shoestring.error = function( id, str ) { 152 | var errors = shoestring.enUS.errors; 153 | throw new Error( errors.prefix + " " + errors[id] + ( str ? " " + str : "" ) ); 154 | }; 155 | 156 | 157 | 158 | var xmlHttp = function() { 159 | try { 160 | return new XMLHttpRequest(); 161 | } 162 | catch( e ){ 163 | return new ActiveXObject( "Microsoft.XMLHTTP" ); 164 | } 165 | }; 166 | 167 | /** 168 | * Make an HTTP request to a url. 169 | * 170 | * **NOTE** the following options are supported: 171 | * 172 | * - *method* - The HTTP method used with the request. Default: `GET`. 173 | * - *data* - Raw object with keys and values to pass with request as query params. Default `null`. 174 | * - *headers* - Set of request headers to add. Default `{}`. 175 | * - *async* - Whether the opened request is asynchronouse. Default `true`. 176 | * - *success* - Callback for successful request and response. Passed the response data. 177 | * - *error* - Callback for failed request and response. 178 | * - *cancel* - Callback for cancelled request and response. 179 | * 180 | * @param {string} url The url to request. 181 | * @param {object} options The options object, see Notes. 182 | * @return shoestring 183 | * @this shoestring 184 | */ 185 | 186 | shoestring.ajax = function( url, options ) { 187 | var params = "", req = xmlHttp(), settings, key; 188 | 189 | settings = shoestring.extend( {}, shoestring.ajax.settings ); 190 | 191 | if( options ){ 192 | shoestring.extend( settings, options ); 193 | } 194 | 195 | if( !url ){ 196 | url = settings.url; 197 | } 198 | 199 | if( !req || !url ){ 200 | return; 201 | } 202 | 203 | // create parameter string from data object 204 | if( settings.data ){ 205 | for( key in settings.data ){ 206 | if( settings.data.hasOwnProperty( key ) ){ 207 | if( params !== "" ){ 208 | params += "&"; 209 | } 210 | params += encodeURIComponent( key ) + "=" + 211 | encodeURIComponent( settings.data[key] ); 212 | } 213 | } 214 | } 215 | 216 | // append params to url for GET requests 217 | if( settings.method === "GET" && params ){ 218 | if( url.indexOf("?") >= 0 ){ 219 | shoestring.error( 'ajax-url-query' ); 220 | } 221 | 222 | url += "?" + params; 223 | } 224 | 225 | req.open( settings.method, url, settings.async ); 226 | 227 | if( req.setRequestHeader ){ 228 | req.setRequestHeader( "X-Requested-With", "XMLHttpRequest" ); 229 | 230 | // Set 'Content-type' header for POST requests 231 | if( settings.method === "POST" && params ){ 232 | req.setRequestHeader( "Content-type", "application/x-www-form-urlencoded" ); 233 | } 234 | 235 | for( key in settings.headers ){ 236 | if( settings.headers.hasOwnProperty( key ) ){ 237 | req.setRequestHeader(key, settings.headers[ key ]); 238 | } 239 | } 240 | } 241 | 242 | req.onreadystatechange = function () { 243 | if( req.readyState === 4 ){ 244 | // Trim the whitespace so shoestring('
    ') works 245 | var res = (req.responseText || '').replace(/^\s+|\s+$/g, ''); 246 | if( req.status.toString().indexOf( "0" ) === 0 ){ 247 | return settings.cancel( res, req.status, req ); 248 | } 249 | else if ( req.status.toString().match( /^(4|5)/ ) && RegExp.$1 ){ 250 | return settings.error( res, req.status, req ); 251 | } 252 | else if (settings.success) { 253 | return settings.success( res, req.status, req ); 254 | } 255 | } 256 | }; 257 | 258 | if( req.readyState === 4 ){ 259 | return req; 260 | } 261 | 262 | // Send request 263 | if( settings.method === "POST" && params ){ 264 | req.send( params ); 265 | } else { 266 | req.send(); 267 | } 268 | 269 | return req; 270 | }; 271 | 272 | shoestring.ajax.settings = { 273 | success: function(){}, 274 | error: function(){}, 275 | cancel: function(){}, 276 | method: "GET", 277 | async: true, 278 | data: null, 279 | headers: {} 280 | }; 281 | 282 | 283 | 284 | /** 285 | * Helper function wrapping a call to [ajax](ajax.js.html) using the `GET` method. 286 | * 287 | * @param {string} url The url to GET from. 288 | * @param {function} callback Callback to invoke on success. 289 | * @return shoestring 290 | * @this shoestring 291 | */ 292 | shoestring.get = function( url, callback ){ 293 | return shoestring.ajax( url, { success: callback } ); 294 | }; 295 | 296 | 297 | 298 | /** 299 | * Load the HTML response from `url` into the current set of elements. 300 | * 301 | * @param {string} url The url to GET from. 302 | * @param {function} callback Callback to invoke after HTML is inserted. 303 | * @return shoestring 304 | * @this shoestring 305 | */ 306 | shoestring.fn.load = function( url, callback ){ 307 | var self = this, 308 | args = arguments, 309 | intCB = function( data ){ 310 | self.each(function(){ 311 | shoestring( this ).html( data ); 312 | }); 313 | 314 | if( callback ){ 315 | callback.apply( self, args ); 316 | } 317 | }; 318 | 319 | shoestring.ajax( url, { success: intCB } ); 320 | return this; 321 | }; 322 | 323 | 324 | 325 | /** 326 | * Helper function wrapping a call to [ajax](ajax.js.html) using the `POST` method. 327 | * 328 | * @param {string} url The url to POST to. 329 | * @param {object} data The data to send. 330 | * @param {function} callback Callback to invoke on success. 331 | * @return shoestring 332 | * @this shoestring 333 | */ 334 | shoestring.post = function( url, data, callback ){ 335 | return shoestring.ajax( url, { data: data, method: "POST", success: callback } ); 336 | }; 337 | 338 | 339 | 340 | /** 341 | * Iterates over `shoestring` collections. 342 | * 343 | * @param {function} callback The callback to be invoked on each element and index 344 | * @return shoestring 345 | * @this shoestring 346 | */ 347 | shoestring.fn.each = function( callback ){ 348 | return shoestring.each( this, callback ); 349 | }; 350 | 351 | shoestring.each = function( collection, callback ) { 352 | var val; 353 | for( var i = 0, il = collection.length; i < il; i++ ){ 354 | val = callback.call( collection[i], i, collection[i] ); 355 | if( val === false ){ 356 | break; 357 | } 358 | } 359 | 360 | return collection; 361 | }; 362 | 363 | 364 | 365 | /** 366 | * Check for array membership. 367 | * 368 | * @param {object} needle The thing to find. 369 | * @param {object} haystack The thing to find the needle in. 370 | * @return {boolean} 371 | * @this window 372 | */ 373 | shoestring.inArray = function( needle, haystack ){ 374 | var isin = -1; 375 | for( var i = 0, il = haystack.length; i < il; i++ ){ 376 | if( haystack.hasOwnProperty( i ) && haystack[ i ] === needle ){ 377 | isin = i; 378 | } 379 | } 380 | return isin; 381 | }; 382 | 383 | 384 | 385 | /** 386 | * Bind callbacks to be run when the DOM is "ready". 387 | * 388 | * @param {function} fn The callback to be run 389 | * @return shoestring 390 | * @this shoestring 391 | */ 392 | shoestring.ready = function( fn ){ 393 | if( ready && fn ){ 394 | fn.call( document ); 395 | } 396 | else if( fn ){ 397 | readyQueue.push( fn ); 398 | } 399 | else { 400 | runReady(); 401 | } 402 | 403 | return [document]; 404 | }; 405 | 406 | // TODO necessary? 407 | shoestring.fn.ready = function( fn ){ 408 | shoestring.ready( fn ); 409 | return this; 410 | }; 411 | 412 | // Empty and exec the ready queue 413 | var ready = false, 414 | readyQueue = [], 415 | runReady = function(){ 416 | if( !ready ){ 417 | while( readyQueue.length ){ 418 | readyQueue.shift().call( document ); 419 | } 420 | ready = true; 421 | } 422 | }; 423 | 424 | // Quick IE8 shiv 425 | if( !window.addEventListener ){ 426 | window.addEventListener = function( evt, cb ){ 427 | return window.attachEvent( "on" + evt, cb ); 428 | }; 429 | } 430 | 431 | // If DOM is already ready at exec time, depends on the browser. 432 | // From: https://github.com/mobify/mobifyjs/blob/526841be5509e28fc949038021799e4223479f8d/src/capture.js#L128 433 | if (document.attachEvent ? document.readyState === "complete" : document.readyState !== "loading") { 434 | runReady(); 435 | } else { 436 | if( !document.addEventListener ){ 437 | document.attachEvent( "DOMContentLoaded", runReady ); 438 | document.attachEvent( "onreadystatechange", runReady ); 439 | } else { 440 | document.addEventListener( "DOMContentLoaded", runReady, false ); 441 | document.addEventListener( "readystatechange", runReady, false ); 442 | } 443 | window.addEventListener( "load", runReady, false ); 444 | } 445 | 446 | 447 | 448 | /** 449 | * Get data attached to the first element or set data values on all elements in the current set. 450 | * 451 | * @param {string} name The data attribute name. 452 | * @param {any} value The value assigned to the data attribute. 453 | * @return {any|shoestring} 454 | * @this shoestring 455 | */ 456 | shoestring.fn.data = function( name, value ){ 457 | if( name !== undefined ){ 458 | if( value !== undefined ){ 459 | return this.each(function(){ 460 | if( !this.shoestringData ){ 461 | this.shoestringData = {}; 462 | } 463 | 464 | this.shoestringData[ name ] = value; 465 | }); 466 | } 467 | else { 468 | return this[ 0 ] && this[ 0 ].shoestringData ? this[ 0 ].shoestringData[ name ] : undefined; 469 | } 470 | } 471 | else { 472 | return this[ 0 ] ? this[ 0 ].shoestringData || {} : undefined; 473 | } 474 | }; 475 | 476 | 477 | /** 478 | * Remove data associated with `name` or all the data, for each element in the current set. 479 | * 480 | * @param {string} name The data attribute name. 481 | * @return shoestring 482 | * @this shoestring 483 | */ 484 | shoestring.fn.removeData = function( name ){ 485 | return this.each(function(){ 486 | if( name !== undefined && this.shoestringData ){ 487 | this.shoestringData[ name ] = undefined; 488 | delete this.shoestringData[ name ]; 489 | } else { 490 | this[ 0 ].shoestringData = {}; 491 | } 492 | }); 493 | }; 494 | 495 | 496 | 497 | /** 498 | * An alias for the `shoestring` constructor. 499 | */ 500 | window.$ = shoestring; 501 | 502 | 503 | 504 | /** 505 | * Add a class to each DOM element in the set of elements. 506 | * 507 | * @param {string} className The name of the class to be added. 508 | * @return shoestring 509 | * @this shoestring 510 | */ 511 | shoestring.fn.addClass = function( className ){ 512 | var classes = className.replace(/^\s+|\s+$/g, '').split( " " ); 513 | 514 | return this.each(function(){ 515 | for( var i = 0, il = classes.length; i < il; i++ ){ 516 | if( this.className !== undefined && 517 | (this.className === "" || 518 | !this.className.match( new RegExp( "(^|\\s)" + classes[ i ] + "($|\\s)"))) ){ 519 | this.className += " " + classes[ i ]; 520 | } 521 | } 522 | }); 523 | }; 524 | 525 | 526 | 527 | /** 528 | * Add elements matching the selector to the current set. 529 | * 530 | * @param {string} selector The selector for the elements to add from the DOM 531 | * @return shoestring 532 | * @this shoestring 533 | */ 534 | shoestring.fn.add = function( selector ){ 535 | var ret = []; 536 | this.each(function(){ 537 | ret.push( this ); 538 | }); 539 | 540 | shoestring( selector ).each(function(){ 541 | ret.push( this ); 542 | }); 543 | 544 | return shoestring( ret ); 545 | }; 546 | 547 | 548 | 549 | /** 550 | * Insert an element or HTML string after each element in the current set. 551 | * 552 | * @param {string|HTMLElement} fragment The HTML or HTMLElement to insert. 553 | * @return shoestring 554 | * @this shoestring 555 | */ 556 | shoestring.fn.after = function( fragment ){ 557 | if( typeof( fragment ) === "string" || fragment.nodeType !== undefined ){ 558 | fragment = shoestring( fragment ); 559 | } 560 | 561 | if( fragment.length > 1 ){ 562 | fragment = fragment.reverse(); 563 | } 564 | return this.each(function( i ){ 565 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 566 | var insertEl = i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ]; 567 | this.parentNode.insertBefore( insertEl, this.nextSibling ); 568 | } 569 | }); 570 | }; 571 | 572 | 573 | 574 | /** 575 | * Insert an element or HTML string as the last child of each element in the set. 576 | * 577 | * @param {string|HTMLElement} fragment The HTML or HTMLElement to insert. 578 | * @return shoestring 579 | * @this shoestring 580 | */ 581 | shoestring.fn.append = function( fragment ){ 582 | if( typeof( fragment ) === "string" || fragment.nodeType !== undefined ){ 583 | fragment = shoestring( fragment ); 584 | } 585 | 586 | return this.each(function( i ){ 587 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 588 | this.appendChild( i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ] ); 589 | } 590 | }); 591 | }; 592 | 593 | 594 | 595 | /** 596 | * Insert the current set as the last child of the elements matching the selector. 597 | * 598 | * @param {string} selector The selector after which to append the current set. 599 | * @return shoestring 600 | * @this shoestring 601 | */ 602 | shoestring.fn.appendTo = function( selector ){ 603 | return this.each(function(){ 604 | shoestring( selector ).append( this ); 605 | }); 606 | }; 607 | 608 | 609 | 610 | /** 611 | * Get the value of the first element of the set or set the value of all the elements in the set. 612 | * 613 | * @param {string} name The attribute name. 614 | * @param {string} value The new value for the attribute. 615 | * @return {shoestring|string|undefined} 616 | * @this {shoestring} 617 | */ 618 | shoestring.fn.attr = function( name, value ){ 619 | var nameStr = typeof( name ) === "string"; 620 | 621 | if( value !== undefined || !nameStr ){ 622 | return this.each(function(){ 623 | if( nameStr ){ 624 | this.setAttribute( name, value ); 625 | } else { 626 | for( var i in name ){ 627 | if( name.hasOwnProperty( i ) ){ 628 | this.setAttribute( i, name[ i ] ); 629 | } 630 | } 631 | } 632 | }); 633 | } else { 634 | return this[ 0 ] ? this[ 0 ].getAttribute( name ) : undefined; 635 | } 636 | }; 637 | 638 | 639 | 640 | /** 641 | * Insert an element or HTML string before each element in the current set. 642 | * 643 | * @param {string|HTMLElement} fragment The HTML or HTMLElement to insert. 644 | * @return shoestring 645 | * @this shoestring 646 | */ 647 | shoestring.fn.before = function( fragment ){ 648 | if( typeof( fragment ) === "string" || fragment.nodeType !== undefined ){ 649 | fragment = shoestring( fragment ); 650 | } 651 | 652 | return this.each(function( i ){ 653 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 654 | this.parentNode.insertBefore( i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ], this ); 655 | } 656 | }); 657 | }; 658 | 659 | 660 | 661 | /** 662 | * Get the children of the current collection. 663 | * @return shoestring 664 | * @this shoestring 665 | */ 666 | shoestring.fn.children = function(){ 667 | var ret = [], 668 | childs, 669 | j; 670 | this.each(function(){ 671 | childs = this.children; 672 | j = -1; 673 | 674 | while( j++ < childs.length-1 ){ 675 | if( shoestring.inArray( childs[ j ], ret ) === -1 ){ 676 | ret.push( childs[ j ] ); 677 | } 678 | } 679 | }); 680 | return shoestring(ret); 681 | }; 682 | 683 | 684 | 685 | /** 686 | * Clone and return the current set of nodes into a new `shoestring` object. 687 | * 688 | * @return shoestring 689 | * @this shoestring 690 | */ 691 | shoestring.fn.clone = function() { 692 | var ret = []; 693 | 694 | this.each(function() { 695 | ret.push( this.cloneNode( true ) ); 696 | }); 697 | 698 | return shoestring( ret ); 699 | }; 700 | 701 | 702 | 703 | /** 704 | * Checks the current set of elements against the selector, if one matches return `true`. 705 | * 706 | * @param {string} selector The selector to check. 707 | * @return {boolean} 708 | * @this {shoestring} 709 | */ 710 | shoestring.fn.is = function( selector ){ 711 | var ret = false, self = this, parents, check; 712 | 713 | // assume a dom element 714 | if( typeof selector !== "string" ){ 715 | // array-like, ie shoestring objects or element arrays 716 | if( selector.length && selector[0] ){ 717 | check = selector; 718 | } else { 719 | check = [selector]; 720 | } 721 | 722 | return _checkElements(this, check); 723 | } 724 | 725 | parents = this.parent(); 726 | 727 | if( !parents.length ){ 728 | parents = shoestring( document ); 729 | } 730 | 731 | parents.each(function( i, e ) { 732 | var children; 733 | 734 | try { 735 | children = e.querySelectorAll( selector ); 736 | } catch( e ) { 737 | shoestring.error( 'queryselector', selector ); 738 | } 739 | 740 | ret = _checkElements( self, children ); 741 | }); 742 | 743 | return ret; 744 | }; 745 | 746 | function _checkElements(needles, haystack){ 747 | var ret = false; 748 | 749 | needles.each(function() { 750 | var j = 0; 751 | 752 | while( j < haystack.length ){ 753 | if( this === haystack[j] ){ 754 | ret = true; 755 | } 756 | 757 | j++; 758 | } 759 | }); 760 | 761 | return ret; 762 | } 763 | 764 | 765 | 766 | /** 767 | * Find an element matching the selector in the set of the current element and its parents. 768 | * 769 | * @param {string} selector The selector used to identify the target element. 770 | * @return shoestring 771 | * @this shoestring 772 | */ 773 | shoestring.fn.closest = function( selector ){ 774 | var ret = []; 775 | 776 | if( !selector ){ 777 | return shoestring( ret ); 778 | } 779 | 780 | this.each(function(){ 781 | var element, $self = shoestring( element = this ); 782 | 783 | if( $self.is(selector) ){ 784 | ret.push( this ); 785 | return; 786 | } 787 | 788 | while( element.parentElement ) { 789 | if( shoestring(element.parentElement).is(selector) ){ 790 | ret.push( element.parentElement ); 791 | break; 792 | } 793 | 794 | element = element.parentElement; 795 | } 796 | }); 797 | 798 | return shoestring( ret ); 799 | }; 800 | 801 | 802 | 803 | shoestring.cssExceptions = { 804 | 'float': [ 'cssFloat', 'styleFloat' ] // styleFloat is IE8 805 | }; 806 | 807 | 808 | 809 | /** 810 | * A polyfill to support computed styles in IE < 9 811 | * 812 | * NOTE this is taken directly from https://github.com/jonathantneal/polyfill 813 | */ 814 | (function () { 815 | function getComputedStylePixel(element, property, fontSize) { 816 | element.document; // Internet Explorer sometimes struggles to read currentStyle until the element's document is accessed. 817 | 818 | var 819 | value = element.currentStyle[property].match(/([\d\.]+)(%|cm|em|in|mm|pc|pt|)/) || [0, 0, ''], 820 | size = value[1], 821 | suffix = value[2], 822 | rootSize; 823 | 824 | fontSize = !fontSize ? fontSize : /%|em/.test(suffix) && element.parentElement ? getComputedStylePixel(element.parentElement, 'fontSize', null) : 16; 825 | rootSize = property === 'fontSize' ? fontSize : /width/i.test(property) ? element.clientWidth : element.clientHeight; 826 | 827 | return suffix === '%' ? size / 100 * rootSize : 828 | suffix === 'cm' ? size * 0.3937 * 96 : 829 | suffix === 'em' ? size * fontSize : 830 | suffix === 'in' ? size * 96 : 831 | suffix === 'mm' ? size * 0.3937 * 96 / 10 : 832 | suffix === 'pc' ? size * 12 * 96 / 72 : 833 | suffix === 'pt' ? size * 96 / 72 : 834 | size; 835 | } 836 | 837 | function setShortStyleProperty(style, property) { 838 | var 839 | borderSuffix = property === 'border' ? 'Width' : '', 840 | t = property + 'Top' + borderSuffix, 841 | r = property + 'Right' + borderSuffix, 842 | b = property + 'Bottom' + borderSuffix, 843 | l = property + 'Left' + borderSuffix; 844 | 845 | style[property] = (style[t] === style[r] && style[t] === style[b] && style[t] === style[l] ? [ style[t] ] : 846 | style[t] === style[b] && style[l] === style[r] ? [ style[t], style[r] ] : 847 | style[l] === style[r] ? [ style[t], style[r], style[b] ] : 848 | [ style[t], style[r], style[b], style[l] ]).join(' '); 849 | } 850 | 851 | // 852 | function CSSStyleDeclaration(element) { 853 | var 854 | style = this, 855 | currentStyle = element.currentStyle, 856 | fontSize = getComputedStylePixel(element, 'fontSize'), 857 | unCamelCase = function (match) { 858 | return '-' + match.toLowerCase(); 859 | }, 860 | property; 861 | 862 | for (property in currentStyle) { 863 | Array.prototype.push.call(style, property === 'styleFloat' ? 'float' : property.replace(/[A-Z]/, unCamelCase)); 864 | 865 | if (property === 'width') { 866 | style[property] = element.offsetWidth + 'px'; 867 | } else if (property === 'height') { 868 | style[property] = element.offsetHeight + 'px'; 869 | } else if (property === 'styleFloat') { 870 | style.float = currentStyle[property]; 871 | } else if (/margin.|padding.|border.+W/.test(property) && style[property] !== 'auto') { 872 | style[property] = Math.round(getComputedStylePixel(element, property, fontSize)) + 'px'; 873 | } else if (/^outline/.test(property)) { 874 | // errors on checking outline 875 | try { 876 | style[property] = currentStyle[property]; 877 | } catch (error) { 878 | style.outlineColor = currentStyle.color; 879 | style.outlineStyle = style.outlineStyle || 'none'; 880 | style.outlineWidth = style.outlineWidth || '0px'; 881 | style.outline = [style.outlineColor, style.outlineWidth, style.outlineStyle].join(' '); 882 | } 883 | } else { 884 | style[property] = currentStyle[property]; 885 | } 886 | } 887 | 888 | setShortStyleProperty(style, 'margin'); 889 | setShortStyleProperty(style, 'padding'); 890 | setShortStyleProperty(style, 'border'); 891 | 892 | style.fontSize = Math.round(fontSize) + 'px'; 893 | } 894 | 895 | CSSStyleDeclaration.prototype = { 896 | constructor: CSSStyleDeclaration, 897 | // .getPropertyPriority 898 | getPropertyPriority: function () { 899 | throw new Error('NotSupportedError: DOM Exception 9'); 900 | }, 901 | // .getPropertyValue 902 | getPropertyValue: function (property) { 903 | return this[property.replace(/-\w/g, function (match) { 904 | return match[1].toUpperCase(); 905 | })]; 906 | }, 907 | // .item 908 | item: function (index) { 909 | return this[index]; 910 | }, 911 | // .removeProperty 912 | removeProperty: function () { 913 | throw new Error('NoModificationAllowedError: DOM Exception 7'); 914 | }, 915 | // .setProperty 916 | setProperty: function () { 917 | throw new Error('NoModificationAllowedError: DOM Exception 7'); 918 | }, 919 | // .getPropertyCSSValue 920 | getPropertyCSSValue: function () { 921 | throw new Error('NotSupportedError: DOM Exception 9'); 922 | } 923 | }; 924 | 925 | if( !window.getComputedStyle ) { 926 | // .getComputedStyle 927 | // NOTE Window is not defined in all browsers 928 | window.getComputedStyle = function (element) { 929 | return new CSSStyleDeclaration(element); 930 | }; 931 | 932 | if ( window.Window ) { 933 | window.Window.prototype.getComputedStyle = window.getComputedStyle; 934 | } 935 | } 936 | })(); 937 | 938 | 939 | 940 | (function() { 941 | var cssExceptions = shoestring.cssExceptions; 942 | 943 | // IE8 uses marginRight instead of margin-right 944 | function convertPropertyName( str ) { 945 | return str.replace( /\-([A-Za-z])/g, function ( match, character ) { 946 | return character.toUpperCase(); 947 | }); 948 | } 949 | 950 | function _getStyle( element, property ) { 951 | // polyfilled in getComputedStyle module 952 | return window.getComputedStyle( element, null ).getPropertyValue( property ); 953 | } 954 | 955 | var vendorPrefixes = [ '', '-webkit-', '-ms-', '-moz-', '-o-', '-khtml-' ]; 956 | 957 | /** 958 | * Private function for getting the computed style of an element. 959 | * 960 | * **NOTE** Please use the [css](../css.js.html) method instead. 961 | * 962 | * @method _getStyle 963 | * @param {HTMLElement} element The element we want the style property for. 964 | * @param {string} property The css property we want the style for. 965 | */ 966 | shoestring._getStyle = function( element, property ) { 967 | var convert, value, j, k; 968 | 969 | if( cssExceptions[ property ] ) { 970 | for( j = 0, k = cssExceptions[ property ].length; j < k; j++ ) { 971 | value = _getStyle( element, cssExceptions[ property ][ j ] ); 972 | 973 | if( value ) { 974 | return value; 975 | } 976 | } 977 | } 978 | 979 | for( j = 0, k = vendorPrefixes.length; j < k; j++ ) { 980 | convert = convertPropertyName( vendorPrefixes[ j ] + property ); 981 | 982 | // VendorprefixKeyName || key-name 983 | value = _getStyle( element, convert ); 984 | 985 | if( convert !== property ) { 986 | value = value || _getStyle( element, property ); 987 | } 988 | 989 | if( vendorPrefixes[ j ] ) { 990 | // -vendorprefix-key-name 991 | value = value || _getStyle( element, vendorPrefixes[ j ] + property ); 992 | } 993 | 994 | if( value ) { 995 | return value; 996 | } 997 | } 998 | 999 | return undefined; 1000 | }; 1001 | })(); 1002 | 1003 | 1004 | 1005 | (function() { 1006 | var cssExceptions = shoestring.cssExceptions; 1007 | 1008 | // IE8 uses marginRight instead of margin-right 1009 | function convertPropertyName( str ) { 1010 | return str.replace( /\-([A-Za-z])/g, function ( match, character ) { 1011 | return character.toUpperCase(); 1012 | }); 1013 | } 1014 | 1015 | /** 1016 | * Private function for setting the style of an element. 1017 | * 1018 | * **NOTE** Please use the [css](../css.js.html) method instead. 1019 | * 1020 | * @method _setStyle 1021 | * @param {HTMLElement} element The element we want to style. 1022 | * @param {string} property The property being used to style the element. 1023 | * @param {string} value The css value for the style property. 1024 | */ 1025 | shoestring._setStyle = function( element, property, value ) { 1026 | var convertedProperty = convertPropertyName(property); 1027 | 1028 | element.style[ property ] = value; 1029 | 1030 | if( convertedProperty !== property ) { 1031 | element.style[ convertedProperty ] = value; 1032 | } 1033 | 1034 | if( cssExceptions[ property ] ) { 1035 | for( var j = 0, k = cssExceptions[ property ].length; j -1 ){ 1127 | ret.push( this ); 1128 | } 1129 | } 1130 | }); 1131 | 1132 | return shoestring( ret ); 1133 | }; 1134 | 1135 | 1136 | 1137 | /** 1138 | * Find descendant elements of the current collection. 1139 | * 1140 | * @param {string} selector The selector used to find the children 1141 | * @return shoestring 1142 | * @this shoestring 1143 | */ 1144 | shoestring.fn.find = function( selector ){ 1145 | var ret = [], 1146 | finds; 1147 | this.each(function(){ 1148 | try { 1149 | finds = this.querySelectorAll( selector ); 1150 | } catch( e ) { 1151 | shoestring.error( 'queryselector', selector ); 1152 | } 1153 | 1154 | for( var i = 0, il = finds.length; i < il; i++ ){ 1155 | ret = ret.concat( finds[i] ); 1156 | } 1157 | }); 1158 | return shoestring( ret ); 1159 | }; 1160 | 1161 | 1162 | 1163 | /** 1164 | * Returns the first element of the set wrapped in a new `shoestring` object. 1165 | * 1166 | * @return shoestring 1167 | * @this shoestring 1168 | */ 1169 | shoestring.fn.first = function(){ 1170 | return this.eq( 0 ); 1171 | }; 1172 | 1173 | 1174 | 1175 | /** 1176 | * Returns the raw DOM node at the passed index. 1177 | * 1178 | * @param {integer} index The index of the element to wrap and return. 1179 | * @return HTMLElement 1180 | * @this shoestring 1181 | */ 1182 | shoestring.fn.get = function( index ){ 1183 | return this[ index ]; 1184 | }; 1185 | 1186 | 1187 | 1188 | /** 1189 | * Private function for setting/getting the offset property for height/width. 1190 | * 1191 | * **NOTE** Please use the [width](width.js.html) or [height](height.js.html) methods instead. 1192 | * 1193 | * @param {shoestring} set The set of elements. 1194 | * @param {string} name The string "height" or "width". 1195 | * @param {float|undefined} value The value to assign. 1196 | * @return shoestring 1197 | * @this window 1198 | */ 1199 | shoestring._dimension = function( set, name, value ){ 1200 | var offsetName; 1201 | 1202 | if( value === undefined ){ 1203 | offsetName = name.replace(/^[a-z]/, function( letter ) { 1204 | return letter.toUpperCase(); 1205 | }); 1206 | 1207 | return set[ 0 ][ "offset" + offsetName ]; 1208 | } else { 1209 | // support integer values as pixels 1210 | value = typeof value === "string" ? value : value + "px"; 1211 | 1212 | return set.each(function(){ 1213 | this.style[ name ] = value; 1214 | }); 1215 | } 1216 | }; 1217 | 1218 | 1219 | 1220 | /** 1221 | * Gets the height value of the first element or sets the height for the whole set. 1222 | * 1223 | * @param {float|undefined} value The value to assign. 1224 | * @return shoestring 1225 | * @this shoestring 1226 | */ 1227 | shoestring.fn.height = function( value ){ 1228 | return shoestring._dimension( this, "height", value ); 1229 | }; 1230 | 1231 | 1232 | 1233 | var set = function( html ){ 1234 | if( typeof html === "string" ){ 1235 | return this.each(function(){ 1236 | this.innerHTML = html; 1237 | }); 1238 | } else { 1239 | var h = ""; 1240 | if( typeof html.length !== "undefined" ){ 1241 | for( var i = 0, l = html.length; i < l; i++ ){ 1242 | h += html[i].outerHTML; 1243 | } 1244 | } else { 1245 | h = html.outerHTML; 1246 | } 1247 | return this.each(function(){ 1248 | this.innerHTML = h; 1249 | }); 1250 | } 1251 | }; 1252 | /** 1253 | * Gets or sets the `innerHTML` from all the elements in the set. 1254 | * 1255 | * @param {string|undefined} html The html to assign 1256 | * @return {string|shoestring} 1257 | * @this shoestring 1258 | */ 1259 | shoestring.fn.html = function( html ){ 1260 | if( !!html && typeof html === "function" ){ 1261 | shoestring.error( 'html-function' ); 1262 | } 1263 | if( typeof html !== "undefined" ){ 1264 | return set.call( this, html ); 1265 | } else { // get 1266 | var pile = ""; 1267 | 1268 | this.each(function(){ 1269 | pile += this.innerHTML; 1270 | }); 1271 | 1272 | return pile; 1273 | } 1274 | }; 1275 | 1276 | 1277 | 1278 | (function() { 1279 | function _getIndex( set, test ) { 1280 | var i, result, element; 1281 | 1282 | for( i = result = 0; i < set.length; i++ ) { 1283 | element = set.item ? set.item(i) : set[i]; 1284 | 1285 | if( test(element) ){ 1286 | return result; 1287 | } 1288 | 1289 | // ignore text nodes, etc 1290 | // NOTE may need to be more permissive 1291 | if( element.nodeType === 1 ){ 1292 | result++; 1293 | } 1294 | } 1295 | 1296 | return -1; 1297 | } 1298 | 1299 | /** 1300 | * Find the index in the current set for the passed selector. 1301 | * Without a selector it returns the index of the first node within the array of its siblings. 1302 | * 1303 | * @param {string|undefined} selector The selector used to search for the index. 1304 | * @return {integer} 1305 | * @this {shoestring} 1306 | */ 1307 | shoestring.fn.index = function( selector ){ 1308 | var self, children; 1309 | 1310 | self = this; 1311 | 1312 | // no arg? check the children, otherwise check each element that matches 1313 | if( selector === undefined ){ 1314 | children = ( ( this[ 0 ] && this[0].parentNode ) || document.documentElement).childNodes; 1315 | 1316 | // check if the element matches the first of the set 1317 | return _getIndex(children, function( element ) { 1318 | return self[0] === element; 1319 | }); 1320 | } else { 1321 | 1322 | // check if the element matches the first selected node from the parent 1323 | return _getIndex(self, function( element ) { 1324 | return element === (shoestring( selector, element.parentNode )[ 0 ]); 1325 | }); 1326 | } 1327 | }; 1328 | })(); 1329 | 1330 | 1331 | 1332 | /** 1333 | * Insert the current set after the elements matching the selector. 1334 | * 1335 | * @param {string} selector The selector after which to insert the current set. 1336 | * @return shoestring 1337 | * @this shoestring 1338 | */ 1339 | shoestring.fn.insertAfter = function( selector ){ 1340 | return this.each(function(){ 1341 | shoestring( selector ).after( this ); 1342 | }); 1343 | }; 1344 | 1345 | 1346 | 1347 | /** 1348 | * Insert the current set before the elements matching the selector. 1349 | * 1350 | * @param {string} selector The selector before which to insert the current set. 1351 | * @return shoestring 1352 | * @this shoestring 1353 | */ 1354 | shoestring.fn.insertBefore = function( selector ){ 1355 | return this.each(function(){ 1356 | shoestring( selector ).before( this ); 1357 | }); 1358 | }; 1359 | 1360 | 1361 | 1362 | /** 1363 | * Returns the last element of the set wrapped in a new `shoestring` object. 1364 | * 1365 | * @return shoestring 1366 | * @this shoestring 1367 | */ 1368 | shoestring.fn.last = function(){ 1369 | return this.eq( this.length - 1 ); 1370 | }; 1371 | 1372 | 1373 | 1374 | /** 1375 | * Returns a `shoestring` object with the set of siblings of each element in the original set. 1376 | * 1377 | * @return shoestring 1378 | * @this shoestring 1379 | */ 1380 | shoestring.fn.next = function(){ 1381 | if( arguments.length > 0 ){ 1382 | shoestring.error( 'next-selector' ); 1383 | } 1384 | 1385 | var result = []; 1386 | 1387 | // TODO need to implement map 1388 | this.each(function() { 1389 | var children, item, found; 1390 | 1391 | // get the child nodes for this member of the set 1392 | children = shoestring( this.parentNode )[0].childNodes; 1393 | 1394 | for( var i = 0; i < children.length; i++ ){ 1395 | item = children.item( i ); 1396 | 1397 | // found the item we needed (found) which means current item value is 1398 | // the next node in the list, as long as it's viable grab it 1399 | // NOTE may need to be more permissive 1400 | if( found && item.nodeType === 1 ){ 1401 | result.push( item ); 1402 | break; 1403 | } 1404 | 1405 | // find the current item and mark it as found 1406 | if( item === this ){ 1407 | found = true; 1408 | } 1409 | } 1410 | }); 1411 | 1412 | return shoestring( result ); 1413 | }; 1414 | 1415 | 1416 | 1417 | /** 1418 | * Removes elements from the current set. 1419 | * 1420 | * @param {string} selector The selector to use when removing the elements. 1421 | * @return shoestring 1422 | * @this shoestring 1423 | */ 1424 | shoestring.fn.not = function( selector ){ 1425 | var ret = []; 1426 | 1427 | this.each(function(){ 1428 | var found = shoestring( selector, this.parentNode ); 1429 | 1430 | if( shoestring.inArray(this, found) === -1 ){ 1431 | ret.push( this ); 1432 | } 1433 | }); 1434 | 1435 | return shoestring( ret ); 1436 | }; 1437 | 1438 | 1439 | 1440 | /** 1441 | * Returns an object with the `top` and `left` properties corresponging to the first elements offsets. 1442 | * 1443 | * @return object 1444 | * @this shoestring 1445 | */ 1446 | shoestring.fn.offset = function(){ 1447 | return { 1448 | top: this[ 0 ].offsetTop, 1449 | left: this[ 0 ].offsetLeft 1450 | }; 1451 | }; 1452 | 1453 | 1454 | 1455 | /** 1456 | * Returns the set of first parents for each element in the current set. 1457 | * 1458 | * @return shoestring 1459 | * @this shoestring 1460 | */ 1461 | shoestring.fn.parent = function(){ 1462 | var ret = [], 1463 | parent; 1464 | 1465 | this.each(function(){ 1466 | // no parent node, assume top level 1467 | // jQuery parent: return the document object for or the parent node if it exists 1468 | parent = (this === document.documentElement ? document : this.parentNode); 1469 | 1470 | // if there is a parent and it's not a document fragment 1471 | if( parent && parent.nodeType !== 11 ){ 1472 | ret.push( parent ); 1473 | } 1474 | }); 1475 | 1476 | return shoestring(ret); 1477 | }; 1478 | 1479 | 1480 | 1481 | /** 1482 | * Returns the set of all parents matching the selector if provided for each element in the current set. 1483 | * 1484 | * @param {string} selector The selector to check the parents with. 1485 | * @return shoestring 1486 | * @this shoestring 1487 | */ 1488 | shoestring.fn.parents = function( selector ){ 1489 | var ret = []; 1490 | 1491 | this.each(function(){ 1492 | var curr = this, match; 1493 | 1494 | while( curr.parentElement && !match ){ 1495 | curr = curr.parentElement; 1496 | 1497 | if( selector ){ 1498 | if( curr === shoestring( selector )[0] ){ 1499 | match = true; 1500 | 1501 | if( shoestring.inArray( curr, ret ) === -1 ){ 1502 | ret.push( curr ); 1503 | } 1504 | } 1505 | } else { 1506 | if( shoestring.inArray( curr, ret ) === -1 ){ 1507 | ret.push( curr ); 1508 | } 1509 | } 1510 | } 1511 | }); 1512 | 1513 | return shoestring(ret); 1514 | }; 1515 | 1516 | 1517 | 1518 | /** 1519 | * Add an HTML string or element before the children of each element in the current set. 1520 | * 1521 | * @param {string|HTMLElement} fragment The HTML string or element to add. 1522 | * @return shoestring 1523 | * @this shoestring 1524 | */ 1525 | shoestring.fn.prepend = function( fragment ){ 1526 | if( typeof( fragment ) === "string" || fragment.nodeType !== undefined ){ 1527 | fragment = shoestring( fragment ); 1528 | } 1529 | 1530 | return this.each(function( i ){ 1531 | 1532 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 1533 | var insertEl = i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ]; 1534 | if ( this.firstChild ){ 1535 | this.insertBefore( insertEl, this.firstChild ); 1536 | } else { 1537 | this.appendChild( insertEl ); 1538 | } 1539 | } 1540 | }); 1541 | }; 1542 | 1543 | 1544 | 1545 | /** 1546 | * Add each element of the current set before the children of the selected elements. 1547 | * 1548 | * @param {string} selector The selector for the elements to add the current set to.. 1549 | * @return shoestring 1550 | * @this shoestring 1551 | */ 1552 | shoestring.fn.prependTo = function( selector ){ 1553 | return this.each(function(){ 1554 | shoestring( selector ).prepend( this ); 1555 | }); 1556 | }; 1557 | 1558 | 1559 | 1560 | /** 1561 | * Returns a `shoestring` object with the set of *one* siblingx before each element in the original set. 1562 | * 1563 | * @return shoestring 1564 | * @this shoestring 1565 | */ 1566 | shoestring.fn.prev = function(){ 1567 | if( arguments.length > 0 ){ 1568 | shoestring.error( 'prev-selector' ); 1569 | } 1570 | 1571 | var result = []; 1572 | 1573 | // TODO need to implement map 1574 | this.each(function() { 1575 | var children, item, found; 1576 | 1577 | // get the child nodes for this member of the set 1578 | children = shoestring( this.parentNode )[0].childNodes; 1579 | 1580 | for( var i = children.length -1; i >= 0; i-- ){ 1581 | item = children.item( i ); 1582 | 1583 | // found the item we needed (found) which means current item value is 1584 | // the next node in the list, as long as it's viable grab it 1585 | // NOTE may need to be more permissive 1586 | if( found && item.nodeType === 1 ){ 1587 | result.push( item ); 1588 | break; 1589 | } 1590 | 1591 | // find the current item and mark it as found 1592 | if( item === this ){ 1593 | found = true; 1594 | } 1595 | } 1596 | }); 1597 | 1598 | return shoestring( result ); 1599 | }; 1600 | 1601 | 1602 | 1603 | /** 1604 | * Returns a `shoestring` object with the set of *all* siblings before each element in the original set. 1605 | * 1606 | * @return shoestring 1607 | * @this shoestring 1608 | */ 1609 | shoestring.fn.prevAll = function(){ 1610 | if( arguments.length > 0 ){ 1611 | shoestring.error( 'prevall-selector' ); 1612 | } 1613 | 1614 | var result = []; 1615 | 1616 | this.each(function() { 1617 | var $previous = shoestring( this ).prev(); 1618 | 1619 | while( $previous.length ){ 1620 | result.push( $previous[0] ); 1621 | $previous = $previous.prev(); 1622 | } 1623 | }); 1624 | 1625 | return shoestring( result ); 1626 | }; 1627 | 1628 | 1629 | 1630 | // Property normalization, a subset taken from jQuery src 1631 | shoestring.propFix = { 1632 | "class": "className", 1633 | contenteditable: "contentEditable", 1634 | "for": "htmlFor", 1635 | readonly: "readOnly", 1636 | tabindex: "tabIndex" 1637 | }; 1638 | 1639 | 1640 | 1641 | /** 1642 | * Gets the property value from the first element or sets the property value on all elements of the currrent set. 1643 | * 1644 | * @param {string} name The property name. 1645 | * @param {any} value The property value. 1646 | * @return {any|shoestring} 1647 | * @this shoestring 1648 | */ 1649 | shoestring.fn.prop = function( name, value ){ 1650 | if( !this[0] ){ 1651 | return; 1652 | } 1653 | 1654 | name = shoestring.propFix[ name ] || name; 1655 | 1656 | if( value !== undefined ){ 1657 | return this.each(function(){ 1658 | this[ name ] = value; 1659 | }); 1660 | } else { 1661 | return this[ 0 ][ name ]; 1662 | } 1663 | }; 1664 | 1665 | 1666 | 1667 | /** 1668 | * Remove an attribute from each element in the current set. 1669 | * 1670 | * @param {string} name The name of the attribute. 1671 | * @return shoestring 1672 | * @this shoestring 1673 | */ 1674 | shoestring.fn.removeAttr = function( name ){ 1675 | return this.each(function(){ 1676 | this.removeAttribute( name ); 1677 | }); 1678 | }; 1679 | 1680 | 1681 | 1682 | /** 1683 | * Remove a class from each DOM element in the set of elements. 1684 | * 1685 | * @param {string} className The name of the class to be removed. 1686 | * @return shoestring 1687 | * @this shoestring 1688 | */ 1689 | shoestring.fn.removeClass = function( cname ){ 1690 | var classes = cname.replace(/^\s+|\s+$/g, '').split( " " ); 1691 | 1692 | return this.each(function(){ 1693 | var newClassName, regex; 1694 | 1695 | for( var i = 0, il = classes.length; i < il; i++ ){ 1696 | if( this.className !== undefined ){ 1697 | regex = new RegExp( "(^|\\s)" + classes[ i ] + "($|\\s)", "gmi" ); 1698 | newClassName = this.className.replace( regex, " " ); 1699 | 1700 | this.className = newClassName.replace(/^\s+|\s+$/g, ''); 1701 | } 1702 | } 1703 | }); 1704 | }; 1705 | 1706 | 1707 | 1708 | /** 1709 | * Remove the current set of elements from the DOM. 1710 | * 1711 | * @return shoestring 1712 | * @this shoestring 1713 | */ 1714 | shoestring.fn.remove = function(){ 1715 | return this.each(function(){ 1716 | if( this.parentNode ) { 1717 | this.parentNode.removeChild( this ); 1718 | } 1719 | }); 1720 | }; 1721 | 1722 | 1723 | 1724 | /** 1725 | * Remove a proprety from each element in the current set. 1726 | * 1727 | * @param {string} name The name of the property. 1728 | * @return shoestring 1729 | * @this shoestring 1730 | */ 1731 | shoestring.fn.removeProp = function( property ){ 1732 | var name = shoestring.propFix[ property ] || property; 1733 | 1734 | return this.each(function(){ 1735 | this[ name ] = undefined; 1736 | delete this[ name ]; 1737 | }); 1738 | }; 1739 | 1740 | 1741 | 1742 | /** 1743 | * Replace each element in the current set with that argument HTML string or HTMLElement. 1744 | * 1745 | * @param {string|HTMLElement} fragment The value to assign. 1746 | * @return shoestring 1747 | * @this shoestring 1748 | */ 1749 | shoestring.fn.replaceWith = function( fragment ){ 1750 | if( typeof( fragment ) === "string" ){ 1751 | fragment = shoestring( fragment ); 1752 | } 1753 | 1754 | var ret = []; 1755 | 1756 | if( fragment.length > 1 ){ 1757 | fragment = fragment.reverse(); 1758 | } 1759 | this.each(function( i ){ 1760 | var clone = this.cloneNode( true ), 1761 | insertEl; 1762 | ret.push( clone ); 1763 | 1764 | // If there is no parentNode, this is pointless, drop it. 1765 | if( !this.parentNode ){ return; } 1766 | 1767 | if( fragment.length === 1 ){ 1768 | insertEl = i > 0 ? fragment[ 0 ].cloneNode( true ) : fragment[ 0 ]; 1769 | this.parentNode.replaceChild( insertEl, this ); 1770 | } else { 1771 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 1772 | insertEl = i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ]; 1773 | this.parentNode.insertBefore( insertEl, this.nextSibling ); 1774 | } 1775 | this.parentNode.removeChild( this ); 1776 | } 1777 | }); 1778 | 1779 | return shoestring( ret ); 1780 | }; 1781 | 1782 | 1783 | 1784 | shoestring.inputTypes = [ 1785 | "text", 1786 | "hidden", 1787 | "password", 1788 | "color", 1789 | "date", 1790 | "datetime", 1791 | // "datetime\-local" matched by datetime 1792 | "email", 1793 | "month", 1794 | "number", 1795 | "range", 1796 | "search", 1797 | "tel", 1798 | "time", 1799 | "url", 1800 | "week" 1801 | ]; 1802 | 1803 | shoestring.inputTypeTest = new RegExp( shoestring.inputTypes.join( "|" ) ); 1804 | 1805 | 1806 | /** 1807 | * Serialize child input element values into an object. 1808 | * 1809 | * @return shoestring 1810 | * @this shoestring 1811 | */ 1812 | shoestring.fn.serialize = function(){ 1813 | var data = {}; 1814 | 1815 | shoestring( "input, select", this ).each(function(){ 1816 | var type = this.type, name = this.name, value = this.value; 1817 | 1818 | if( shoestring.inputTypeTest.test( type ) || 1819 | ( type === "checkbox" || type === "radio" ) && 1820 | this.checked ){ 1821 | 1822 | data[ name ] = value; 1823 | } else if( this.nodeName === "SELECT" ){ 1824 | data[ name ] = this.options[ this.selectedIndex ].nodeValue; 1825 | } 1826 | }); 1827 | 1828 | return data; 1829 | }; 1830 | 1831 | 1832 | 1833 | /** 1834 | * Get all of the sibling elements for each element in the current set. 1835 | * 1836 | * @return shoestring 1837 | * @this shoestring 1838 | */ 1839 | shoestring.fn.siblings = function(){ 1840 | if( !this.length ) { 1841 | return shoestring( [] ); 1842 | } 1843 | 1844 | var sibs = [], el = this[ 0 ].parentNode.firstChild; 1845 | 1846 | do { 1847 | if( el.nodeType === 1 && el !== this[ 0 ] ) { 1848 | sibs.push( el ); 1849 | } 1850 | 1851 | el = el.nextSibling; 1852 | } while( el ); 1853 | 1854 | return shoestring( sibs ); 1855 | }; 1856 | 1857 | 1858 | 1859 | var getText = function( elem ){ 1860 | var node, 1861 | ret = "", 1862 | i = 0, 1863 | nodeType = elem.nodeType; 1864 | 1865 | if ( !nodeType ) { 1866 | // If no nodeType, this is expected to be an array 1867 | while ( (node = elem[i++]) ) { 1868 | // Do not traverse comment nodes 1869 | ret += getText( node ); 1870 | } 1871 | } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { 1872 | // Use textContent for elements 1873 | // innerText usage removed for consistency of new lines (jQuery #11153) 1874 | if ( typeof elem.textContent === "string" ) { 1875 | return elem.textContent; 1876 | } else { 1877 | // Traverse its children 1878 | for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { 1879 | ret += getText( elem ); 1880 | } 1881 | } 1882 | } else if ( nodeType === 3 || nodeType === 4 ) { 1883 | return elem.nodeValue; 1884 | } 1885 | // Do not include comment or processing instruction nodes 1886 | 1887 | return ret; 1888 | }; 1889 | 1890 | /** 1891 | * Recursively retrieve the text content of the each element in the current set. 1892 | * 1893 | * @return shoestring 1894 | * @this shoestring 1895 | */ 1896 | shoestring.fn.text = function() { 1897 | if( arguments.length > 0 ){ 1898 | shoestring.error( 'text-setter' ); 1899 | } 1900 | 1901 | return getText( this ); 1902 | }; 1903 | 1904 | 1905 | 1906 | 1907 | /** 1908 | * Get the value of the first element or set the value of all elements in the current set. 1909 | * 1910 | * @param {string} value The value to set. 1911 | * @return shoestring 1912 | * @this shoestring 1913 | */ 1914 | shoestring.fn.val = function( value ){ 1915 | var el; 1916 | if( value !== undefined ){ 1917 | return this.each(function(){ 1918 | if( this.tagName === "SELECT" ){ 1919 | var optionSet, option, 1920 | options = this.options, 1921 | values = [], 1922 | i = options.length, 1923 | newIndex; 1924 | 1925 | values[0] = value; 1926 | while ( i-- ) { 1927 | option = options[ i ]; 1928 | if ( (option.selected = shoestring.inArray( option.value, values ) >= 0) ) { 1929 | optionSet = true; 1930 | newIndex = i; 1931 | } 1932 | } 1933 | // force browsers to behave consistently when non-matching value is set 1934 | if ( !optionSet ) { 1935 | this.selectedIndex = -1; 1936 | } else { 1937 | this.selectedIndex = newIndex; 1938 | } 1939 | } else { 1940 | this.value = value; 1941 | } 1942 | }); 1943 | } else { 1944 | el = this[0]; 1945 | 1946 | if( el.tagName === "SELECT" ){ 1947 | if( el.selectedIndex < 0 ){ return ""; } 1948 | return el.options[ el.selectedIndex ].value; 1949 | } else { 1950 | return el.value; 1951 | } 1952 | } 1953 | }; 1954 | 1955 | 1956 | 1957 | /** 1958 | * Gets the width value of the first element or sets the width for the whole set. 1959 | * 1960 | * @param {float|undefined} value The value to assign. 1961 | * @return shoestring 1962 | * @this shoestring 1963 | */ 1964 | shoestring.fn.width = function( value ){ 1965 | return shoestring._dimension( this, "width", value ); 1966 | }; 1967 | 1968 | 1969 | 1970 | /** 1971 | * Wraps the child elements in the provided HTML. 1972 | * 1973 | * @param {string} html The wrapping HTML. 1974 | * @return shoestring 1975 | * @this shoestring 1976 | */ 1977 | shoestring.fn.wrapInner = function( html ){ 1978 | return this.each(function(){ 1979 | var inH = this.innerHTML; 1980 | 1981 | this.innerHTML = ""; 1982 | shoestring( this ).append( shoestring( html ).html( inH ) ); 1983 | }); 1984 | }; 1985 | 1986 | 1987 | 1988 | function initEventCache( el, evt ) { 1989 | if ( !el.shoestringData ) { 1990 | el.shoestringData = {}; 1991 | } 1992 | if ( !el.shoestringData.events ) { 1993 | el.shoestringData.events = {}; 1994 | } 1995 | if ( !el.shoestringData.loop ) { 1996 | el.shoestringData.loop = {}; 1997 | } 1998 | if ( !el.shoestringData.events[ evt ] ) { 1999 | el.shoestringData.events[ evt ] = []; 2000 | } 2001 | } 2002 | 2003 | function addToEventCache( el, evt, eventInfo ) { 2004 | var obj = {}; 2005 | obj.isCustomEvent = eventInfo.isCustomEvent; 2006 | obj.callback = eventInfo.callfunc; 2007 | obj.originalCallback = eventInfo.originalCallback; 2008 | obj.namespace = eventInfo.namespace; 2009 | 2010 | el.shoestringData.events[ evt ].push( obj ); 2011 | 2012 | if( eventInfo.customEventLoop ) { 2013 | el.shoestringData.loop[ evt ] = eventInfo.customEventLoop; 2014 | } 2015 | } 2016 | 2017 | // In IE8 the events trigger in a reverse order (LIFO). This code 2018 | // unbinds and rebinds all callbacks on an element in the a FIFO order. 2019 | function reorderEvents( node, eventName ) { 2020 | if( node.addEventListener || !node.shoestringData || !node.shoestringData.events ) { 2021 | // add event listner obviates the need for all the callback order juggling 2022 | return; 2023 | } 2024 | 2025 | var otherEvents = node.shoestringData.events[ eventName ] || []; 2026 | for( var j = otherEvents.length - 1; j >= 0; j-- ) { 2027 | // DOM Events only, Custom events maintain their own order internally. 2028 | if( !otherEvents[ j ].isCustomEvent ) { 2029 | node.detachEvent( "on" + eventName, otherEvents[ j ].callback ); 2030 | node.attachEvent( "on" + eventName, otherEvents[ j ].callback ); 2031 | } 2032 | } 2033 | } 2034 | 2035 | /** 2036 | * Bind a callback to an event for the currrent set of elements. 2037 | * 2038 | * @param {string} evt The event(s) to watch for. 2039 | * @param {object,function} data Data to be included with each event or the callback. 2040 | * @param {function} originalCallback Callback to be invoked when data is define.d. 2041 | * @return shoestring 2042 | * @this shoestring 2043 | */ 2044 | shoestring.fn.bind = function( evt, data, originalCallback ){ 2045 | 2046 | if( arguments.length > 3 ){ 2047 | shoestring.error( 'on-delegate' ); 2048 | } 2049 | if( typeof data === "string" ){ 2050 | shoestring.error( 'on-delegate' ); 2051 | } 2052 | if( typeof data === "function" ){ 2053 | originalCallback = data; 2054 | data = null; 2055 | } 2056 | 2057 | var evts = evt.split( " " ), 2058 | docEl = document.documentElement; 2059 | 2060 | // NOTE the `triggeredElement` is purely for custom events from IE 2061 | function encasedCallback( e, namespace, triggeredElement ){ 2062 | var result; 2063 | 2064 | if( e._namespace && e._namespace !== namespace ) { 2065 | return; 2066 | } 2067 | 2068 | e.data = data; 2069 | e.namespace = e._namespace; 2070 | 2071 | var returnTrue = function(){ 2072 | return true; 2073 | }; 2074 | 2075 | e.isDefaultPrevented = function(){ 2076 | return false; 2077 | }; 2078 | 2079 | var originalPreventDefault = e.preventDefault; 2080 | var preventDefaultConstructor = function(){ 2081 | if( originalPreventDefault ) { 2082 | return function(){ 2083 | e.isDefaultPrevented = returnTrue; 2084 | originalPreventDefault.call(e); 2085 | }; 2086 | } else { 2087 | return function(){ 2088 | e.isDefaultPrevented = returnTrue; 2089 | e.returnValue = false; 2090 | }; 2091 | } 2092 | }; 2093 | 2094 | // thanks https://github.com/jonathantneal/EventListener 2095 | e.target = triggeredElement || e.target || e.srcElement; 2096 | e.preventDefault = preventDefaultConstructor(); 2097 | e.stopPropagation = e.stopPropagation || function () { 2098 | e.cancelBubble = true; 2099 | }; 2100 | 2101 | result = originalCallback.apply(this, [ e ].concat( e._args ) ); 2102 | 2103 | if( result === false ){ 2104 | e.preventDefault(); 2105 | e.stopPropagation(); 2106 | } 2107 | 2108 | return result; 2109 | } 2110 | 2111 | // This is exclusively for custom events on browsers without addEventListener (IE8) 2112 | function propChange( originalEvent, boundElement, namespace ) { 2113 | var lastEventInfo = document.documentElement[ originalEvent.propertyName ], 2114 | triggeredElement = lastEventInfo.el; 2115 | 2116 | var boundCheckElement = boundElement; 2117 | 2118 | if( boundElement === document && triggeredElement !== document ) { 2119 | boundCheckElement = document.documentElement; 2120 | } 2121 | 2122 | if( triggeredElement !== undefined && 2123 | shoestring( triggeredElement ).closest( boundCheckElement ).length ) { 2124 | 2125 | originalEvent._namespace = lastEventInfo._namespace; 2126 | originalEvent._args = lastEventInfo._args; 2127 | encasedCallback.call( boundElement, originalEvent, namespace, triggeredElement ); 2128 | } 2129 | } 2130 | 2131 | return this.each(function(){ 2132 | var domEventCallback, 2133 | customEventCallback, 2134 | customEventLoop, 2135 | oEl = this; 2136 | 2137 | for( var i = 0, il = evts.length; i < il; i++ ){ 2138 | var split = evts[ i ].split( "." ), 2139 | evt = split[ 0 ], 2140 | namespace = split.length > 0 ? split[ 1 ] : null; 2141 | 2142 | domEventCallback = function( originalEvent ) { 2143 | if( oEl.ssEventTrigger ) { 2144 | originalEvent._namespace = oEl.ssEventTrigger._namespace; 2145 | originalEvent._args = oEl.ssEventTrigger._args; 2146 | 2147 | oEl.ssEventTrigger = null; 2148 | } 2149 | return encasedCallback.call( oEl, originalEvent, namespace ); 2150 | }; 2151 | customEventCallback = null; 2152 | customEventLoop = null; 2153 | 2154 | initEventCache( this, evt ); 2155 | 2156 | if( "addEventListener" in this ){ 2157 | this.addEventListener( evt, domEventCallback, false ); 2158 | } else if( this.attachEvent ){ 2159 | if( this[ "on" + evt ] !== undefined ) { 2160 | this.attachEvent( "on" + evt, domEventCallback ); 2161 | } else { 2162 | customEventCallback = (function() { 2163 | var eventName = evt; 2164 | return function( e ) { 2165 | if( e.propertyName === eventName ) { 2166 | propChange( e, oEl, namespace ); 2167 | } 2168 | }; 2169 | })(); 2170 | 2171 | // only assign one onpropertychange per element 2172 | if( this.shoestringData.events[ evt ].length === 0 ) { 2173 | customEventLoop = (function() { 2174 | var eventName = evt; 2175 | return function( e ) { 2176 | if( !oEl.shoestringData || !oEl.shoestringData.events ) { 2177 | return; 2178 | } 2179 | var events = oEl.shoestringData.events[ eventName ]; 2180 | if( !events ) { 2181 | return; 2182 | } 2183 | 2184 | // TODO stopImmediatePropagation 2185 | for( var j = 0, k = events.length; j < k; j++ ) { 2186 | events[ j ].callback( e ); 2187 | } 2188 | }; 2189 | })(); 2190 | 2191 | docEl.attachEvent( "onpropertychange", customEventLoop ); 2192 | } 2193 | } 2194 | } 2195 | 2196 | addToEventCache( this, evt, { 2197 | callfunc: customEventCallback || domEventCallback, 2198 | isCustomEvent: !!customEventCallback, 2199 | customEventLoop: customEventLoop, 2200 | originalCallback: originalCallback, 2201 | namespace: namespace 2202 | }); 2203 | 2204 | // Don’t reorder custom events, only DOM Events. 2205 | if( !customEventCallback ) { 2206 | reorderEvents( oEl, evt ); 2207 | } 2208 | } 2209 | }); 2210 | }; 2211 | 2212 | shoestring.fn.on = shoestring.fn.bind; 2213 | 2214 | shoestring.fn.live = function(){ 2215 | shoestring.error( 'live-delegate' ); 2216 | }; 2217 | 2218 | shoestring.fn.delegate = function(){ 2219 | shoestring.error( 'live-delegate' ); 2220 | }; 2221 | 2222 | 2223 | 2224 | /** 2225 | * Unbind a previous bound callback for an event. 2226 | * 2227 | * @param {string} event The event(s) the callback was bound to.. 2228 | * @param {function} callback Callback to unbind. 2229 | * @return shoestring 2230 | * @this shoestring 2231 | */ 2232 | shoestring.fn.unbind = function( event, callback ){ 2233 | 2234 | if( arguments.length >= 3 || typeof callback === "string" ){ 2235 | shoestring.error( 'off-delegate' ); 2236 | } 2237 | 2238 | var evts = event ? event.split( " " ) : []; 2239 | 2240 | return this.each(function(){ 2241 | if( !this.shoestringData || !this.shoestringData.events ) { 2242 | return; 2243 | } 2244 | 2245 | if( !evts.length ) { 2246 | unbindAll.call( this ); 2247 | } else { 2248 | var split, evt, namespace; 2249 | for( var i = 0, il = evts.length; i < il; i++ ){ 2250 | split = evts[ i ].split( "." ), 2251 | evt = split[ 0 ], 2252 | namespace = split.length > 0 ? split[ 1 ] : null; 2253 | 2254 | if( evt ) { 2255 | unbind.call( this, evt, namespace, callback ); 2256 | } else { 2257 | unbindAll.call( this, namespace, callback ); 2258 | } 2259 | } 2260 | } 2261 | }); 2262 | }; 2263 | 2264 | function unbind( evt, namespace, callback ) { 2265 | var bound = this.shoestringData.events[ evt ]; 2266 | if( !(bound && bound.length) ) { 2267 | return; 2268 | } 2269 | 2270 | var matched = [], j, jl; 2271 | for( j = 0, jl = bound.length; j < jl; j++ ) { 2272 | if( !namespace || namespace === bound[ j ].namespace ) { 2273 | if( callback === undefined || callback === bound[ j ].originalCallback ) { 2274 | if( "removeEventListener" in window ){ 2275 | this.removeEventListener( evt, bound[ j ].callback, false ); 2276 | } else if( this.detachEvent ){ 2277 | // dom event 2278 | this.detachEvent( "on" + evt, bound[ j ].callback ); 2279 | 2280 | // only unbind custom events if its the last one on the element 2281 | if( bound.length === 1 && this.shoestringData.loop && this.shoestringData.loop[ evt ] ) { 2282 | document.documentElement.detachEvent( "onpropertychange", this.shoestringData.loop[ evt ] ); 2283 | } 2284 | } 2285 | matched.push( j ); 2286 | } 2287 | } 2288 | } 2289 | 2290 | for( j = 0, jl = matched.length; j < jl; j++ ) { 2291 | this.shoestringData.events[ evt ].splice( j, 1 ); 2292 | } 2293 | } 2294 | 2295 | function unbindAll( namespace, callback ) { 2296 | for( var evtKey in this.shoestringData.events ) { 2297 | unbind.call( this, evtKey, namespace, callback ); 2298 | } 2299 | } 2300 | 2301 | shoestring.fn.off = shoestring.fn.unbind; 2302 | 2303 | 2304 | /** 2305 | * Bind a callback to an event for the currrent set of elements, unbind after one occurence. 2306 | * 2307 | * @param {string} event The event(s) to watch for. 2308 | * @param {function} callback Callback to invoke on the event. 2309 | * @return shoestring 2310 | * @this shoestring 2311 | */ 2312 | shoestring.fn.one = function( event, callback ){ 2313 | var evts = event.split( " " ); 2314 | 2315 | return this.each(function(){ 2316 | var thisevt, cbs = {}, $t = shoestring( this ); 2317 | 2318 | for( var i = 0, il = evts.length; i < il; i++ ){ 2319 | thisevt = evts[ i ]; 2320 | 2321 | cbs[ thisevt ] = function( e ){ 2322 | var $t = shoestring( this ); 2323 | 2324 | for( var j in cbs ) { 2325 | $t.unbind( j, cbs[ j ] ); 2326 | } 2327 | 2328 | return callback.apply( this, [ e ].concat( e._args ) ); 2329 | }; 2330 | 2331 | $t.bind( thisevt, cbs[ thisevt ] ); 2332 | } 2333 | }); 2334 | }; 2335 | 2336 | 2337 | 2338 | /** 2339 | * Trigger an event on the first element in the set, no bubbling, no defaults. 2340 | * 2341 | * @param {string} event The event(s) to trigger. 2342 | * @param {object} args Arguments to append to callback invocations. 2343 | * @return shoestring 2344 | * @this shoestring 2345 | */ 2346 | shoestring.fn.triggerHandler = function( event, args ){ 2347 | var e = event.split( " " )[ 0 ], 2348 | el = this[ 0 ], 2349 | ret; 2350 | 2351 | // TODO needs IE8 support 2352 | // See this.fireEvent( 'on' + evts[ i ], document.createEventObject() ); instead of click() etc in trigger. 2353 | if( document.createEvent && el.shoestringData && el.shoestringData.events && el.shoestringData.events[ e ] ){ 2354 | var bindings = el.shoestringData.events[ e ]; 2355 | for (var i in bindings ){ 2356 | if( bindings.hasOwnProperty( i ) ){ 2357 | event = document.createEvent( "Event" ); 2358 | event.initEvent( e, true, true ); 2359 | event._args = args; 2360 | args.unshift( event ); 2361 | 2362 | ret = bindings[ i ].originalCallback.apply( event.target, args ); 2363 | } 2364 | } 2365 | } 2366 | 2367 | return ret; 2368 | }; 2369 | 2370 | 2371 | 2372 | /** 2373 | * Trigger an event on each of the DOM elements in the current set. 2374 | * 2375 | * @param {string} event The event(s) to trigger. 2376 | * @param {object} args Arguments to append to callback invocations. 2377 | * @return shoestring 2378 | * @this shoestring 2379 | */ 2380 | shoestring.fn.trigger = function( event, args ){ 2381 | var evts = event.split( " " ); 2382 | 2383 | return this.each(function(){ 2384 | var split, evt, namespace; 2385 | for( var i = 0, il = evts.length; i < il; i++ ){ 2386 | split = evts[ i ].split( "." ), 2387 | evt = split[ 0 ], 2388 | namespace = split.length > 0 ? split[ 1 ] : null; 2389 | 2390 | if( evt === "click" ){ 2391 | if( this.tagName === "INPUT" && this.type === "checkbox" && this.click ){ 2392 | this.click(); 2393 | return false; 2394 | } 2395 | } 2396 | 2397 | if( document.createEvent ){ 2398 | var event = document.createEvent( "Event" ); 2399 | event.initEvent( evt, true, true ); 2400 | event._args = args; 2401 | event._namespace = namespace; 2402 | 2403 | this.dispatchEvent( event ); 2404 | } else if ( document.createEventObject ) { 2405 | if( ( "" + this[ evt ] ).indexOf( "function" ) > -1 ) { 2406 | this.ssEventTrigger = { 2407 | _namespace: namespace, 2408 | _args: args 2409 | }; 2410 | 2411 | this[ evt ](); 2412 | } else { 2413 | document.documentElement[ evt ] = { 2414 | "el": this, 2415 | _namespace: namespace, 2416 | _args: args 2417 | }; 2418 | } 2419 | } 2420 | } 2421 | }); 2422 | }; 2423 | 2424 | 2425 | 2426 | 2427 | shoestring.fn.hasClass = function(){ 2428 | shoestring.error( 'has-class' ); 2429 | }; 2430 | 2431 | 2432 | 2433 | shoestring.fn.hide = function(){ 2434 | shoestring.error( 'show-hide' ); 2435 | }; 2436 | 2437 | 2438 | 2439 | shoestring.fn.outerWidth = function(){ 2440 | shoestring.error( 'outer-width' ); 2441 | }; 2442 | 2443 | 2444 | 2445 | shoestring.fn.show = function(){ 2446 | shoestring.error( 'show-hide' ); 2447 | }; 2448 | 2449 | 2450 | 2451 | shoestring.fn.click = function(){ 2452 | shoestring.error( 'click' ); 2453 | }; 2454 | 2455 | 2456 | 2457 | shoestring.map = function(){ 2458 | shoestring.error( 'map' ); 2459 | }; 2460 | 2461 | 2462 | 2463 | shoestring.fn.map = function(){ 2464 | shoestring.error( 'map' ); 2465 | }; 2466 | 2467 | 2468 | 2469 | shoestring.trim = function(){ 2470 | shoestring.error( 'trim' ); 2471 | }; 2472 | 2473 | 2474 | 2475 | (function() { 2476 | shoestring.trackedMethodsKey = "shoestringMethods"; 2477 | 2478 | // simple check for localStorage from Modernizr - https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js 2479 | function supportsStorage() { 2480 | var mod = "modernizr"; 2481 | try { 2482 | localStorage.setItem(mod, mod); 2483 | localStorage.removeItem(mod); 2484 | return true; 2485 | } catch(e) { 2486 | return false; 2487 | } 2488 | } 2489 | 2490 | // return a new function closed over the old implementation 2491 | function recordProxy( old, name ) { 2492 | return function() { 2493 | var tracked; 2494 | try { 2495 | tracked = JSON.parse(window.localStorage.getItem( shoestring.trackedMethodsKey ) || "{}"); 2496 | } catch (e) { 2497 | if( e instanceof SyntaxError) { 2498 | tracked = {}; 2499 | } 2500 | } 2501 | 2502 | tracked[ name ] = true; 2503 | window.localStorage.setItem( shoestring.trackedMethodsKey, JSON.stringify(tracked) ); 2504 | 2505 | return old.apply(this, arguments); 2506 | }; 2507 | } 2508 | 2509 | // proxy each of the methods defined on fn 2510 | if( supportsStorage() ){ 2511 | for( var method in shoestring.fn ){ 2512 | if( shoestring.fn.hasOwnProperty(method) ) { 2513 | shoestring.fn[ method ] = recordProxy(shoestring.fn[ method ], method); 2514 | } 2515 | } 2516 | } 2517 | })(); 2518 | 2519 | 2520 | 2521 | })( this ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fg-menu", 3 | "filename": "menu", 4 | "title": "Menu", 5 | "description": "jQuery Plugin for progressively enhanced menus", 6 | "version": "0.1.5", 7 | "homepage": "https://github.com/filamentgroup/menu", 8 | "author": { 9 | "name": "Scott Jehl", 10 | "email": "scott@filamentgroup.com", 11 | "url": "http://filamentgroup.com" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/filamentgroup/menu.git" 16 | }, 17 | "bugs": "https://github.com/filamentgroup/menu/issues", 18 | "license": "MIT", 19 | "dependencies": { 20 | "jquery": "*" 21 | }, 22 | "devDependencies": { 23 | "grunt-contrib-jshint": "~0.6.0", 24 | "grunt-contrib-qunit": "~0.2.0", 25 | "grunt-contrib-concat": "~0.3.0", 26 | "grunt-contrib-uglify": "~0.2.0", 27 | "grunt-contrib-watch": "~0.4.0", 28 | "grunt-contrib-clean": "~0.4.0", 29 | "grunt": "~0.4.2" 30 | }, 31 | "keywords": [] 32 | } 33 | -------------------------------------------------------------------------------- /src/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "browser": true, 14 | "predef": ["jQuery"] 15 | } -------------------------------------------------------------------------------- /src/menu-init.js: -------------------------------------------------------------------------------- 1 | /* 2 | * menu plugin 3 | * 4 | * Copyright (c) 2013 Filament Group, Inc. 5 | * Licensed under MIT 6 | */ 7 | 8 | /* global Menu:true */ 9 | (function( Menu, $ ) { 10 | 11 | var pluginName = "menu", 12 | initSelector = "[data-" + pluginName + "]"; 13 | 14 | $.fn[ pluginName ] = function(){ 15 | return this.each(function(){ 16 | new window.componentNamespace.Menu( this ).init(); 17 | }); 18 | }; 19 | 20 | // auto-init on enhance (which is called on domready) 21 | $( document ).bind( "enhance", function( e ){ 22 | $( initSelector, e.target )[ pluginName ](); 23 | }); 24 | 25 | }( Menu, jQuery, this )); 26 | -------------------------------------------------------------------------------- /src/menu-trigger-init.js: -------------------------------------------------------------------------------- 1 | /* 2 | * menu trigger plugin 3 | * 4 | * Copyright (c) 2013 Filament Group, Inc. 5 | * Licensed under MIT 6 | */ 7 | 8 | /* global Menutrigger:true */ 9 | (function( Menutrigger, $ ) { 10 | 11 | var pluginName = "menu-trigger", 12 | initSelector = "[data-" + pluginName + "]"; 13 | 14 | $.fn[ pluginName ] = function(){ 15 | return this.each(function(){ 16 | new Menutrigger( this ).init(); 17 | }); 18 | }; 19 | 20 | // auto-init on enhance (which is called on domready) 21 | $( document ).bind( "enhance", function( e ){ 22 | $( initSelector, e.target )[ pluginName ](); 23 | }); 24 | 25 | }( Menutrigger, jQuery, this )); 26 | 27 | -------------------------------------------------------------------------------- /src/menu-trigger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * menu-trigger plugin 3 | * 4 | * Copyright (c) 2013 Filament Group, Inc. 5 | * Licensed under MIT 6 | */ 7 | 8 | (function( $, w ) { 9 | "use strict"; 10 | 11 | var componentName = "Menutrigger"; 12 | 13 | var Menutrigger = function( element ){ 14 | if( !element ){ 15 | throw new Error( "Element required to initialize object" ); 16 | } 17 | this.element = element; 18 | this.$element = $( element ); 19 | this.$menu = $( "#" + this.$element.attr( "data-menu-trigger" ) ); 20 | this.menu = this.$menu.data( "Menu" ); 21 | }; 22 | 23 | Menutrigger.prototype._bindbehavior = function(){ 24 | var self = this; 25 | 26 | if( this.$element.is( "a" ) ){ 27 | this.$element 28 | .attr( "role", "button" ) 29 | .bind( "click", function( e ){ 30 | e.preventDefault(); 31 | self.menu.toggle( this, true ); 32 | }); 33 | } 34 | else if( this.$element.is( "input" ) ){ 35 | this.$element 36 | .bind( "input keyup", function(){ 37 | if( this.value === "" ){ 38 | self.menu.close(); 39 | } 40 | else { 41 | self.menu.open( this, false ); 42 | } 43 | 44 | }) 45 | .bind( "input keydown", function( e ){ 46 | self.menu.keyDown( e ); 47 | }) 48 | .bind( "focus click", function(){ 49 | if( this.value !== "" ){ 50 | self.menu.open(); 51 | } 52 | } ) 53 | .bind( "blur", function(){ 54 | self.menu.close(); 55 | }); 56 | } 57 | }; 58 | 59 | Menutrigger.prototype.init = function(){ 60 | // prevent re-init 61 | if( this.$element.data( componentName ) ) { 62 | return; 63 | } 64 | this.$element.data( componentName, this ); 65 | 66 | // add attrs 67 | this.$element.attr( "aria-controls", this.$menu.attr( "id" ) ); 68 | this.$element.attr( "aria-haspopup", "true" ); 69 | 70 | this._bindbehavior(); 71 | 72 | this.$element.trigger( componentName + ":init" ); 73 | }; 74 | 75 | w[ componentName ] = Menutrigger; 76 | 77 | }( jQuery, this )); 78 | 79 | -------------------------------------------------------------------------------- /src/menu.css: -------------------------------------------------------------------------------- 1 | /* menu component */ 2 | [data-menu] { 3 | position: absolute; 4 | } 5 | [data-menu][aria-hidden=true] { 6 | display: none; 7 | } -------------------------------------------------------------------------------- /src/menu.js: -------------------------------------------------------------------------------- 1 | /* 2 | * menu plugin 3 | * 4 | * Copyright (c) 2013 Filament Group, Inc. 5 | * Licensed under MIT 6 | */ 7 | 8 | window.jQuery = window.jQuery || window.shoestring; 9 | 10 | (function( $, w ) { 11 | "use strict"; 12 | 13 | var componentName = "Menu", 14 | at = { 15 | ariaHidden: "aria-hidden" 16 | }, 17 | selectClass = "menu-selected", 18 | focusables = "a,input,[tabindex]"; 19 | 20 | var menu = function( element ){ 21 | if( !element ){ 22 | throw new Error( "Element required to initialize object" ); 23 | } 24 | this.element = element; 25 | this.$element = $( element ); 26 | this.opened = true; 27 | }; 28 | 29 | menu.prototype.fill = function( items, selectedText ) { 30 | var html = ""; 31 | 32 | $.each( items, function( i, item ){ 33 | html += "" + item + ""; 36 | }); 37 | 38 | this.$element.find( "ol,ul" ).html( html ); 39 | }; 40 | 41 | menu.prototype.moveSelected = function( placement, focus ){ 42 | var $items = this.$element.find( "li" ), 43 | $selected = $items.filter( "." + selectClass ), 44 | $nextSelected; 45 | 46 | if( !$selected.length || placement === "start" ){ 47 | $nextSelected = $items.eq( 0 ); 48 | } 49 | else if( placement === "next" ){ 50 | $nextSelected = $selected.next(); 51 | if( !$nextSelected.length ){ 52 | $nextSelected = $items.eq( 0 ); 53 | } 54 | } 55 | else { 56 | $nextSelected = $selected.prev(); 57 | if( !$nextSelected.length ){ 58 | $nextSelected = $items.eq( $items.length - 1 ); 59 | } 60 | } 61 | $selected.removeClass( selectClass ); 62 | $nextSelected.addClass( selectClass ); 63 | 64 | if( focus || $( w.document.activeElement ).closest( $selected ).length ){ 65 | if( $nextSelected.is( focusables ) ){ 66 | $nextSelected[ 0 ].focus(); 67 | } 68 | else{ 69 | var $focusChild = $nextSelected.find( focusables ); 70 | if( $focusChild.length ){ 71 | $focusChild[ 0 ].focus(); 72 | } 73 | } 74 | } 75 | }; 76 | 77 | menu.prototype.getSelectedElement = function(){ 78 | return this.$element.find( "li." + selectClass ); 79 | }; 80 | 81 | menu.prototype.selectActive = function(){ 82 | var trigger = this.$element.data( componentName + "-trigger" ); 83 | var $selected = this.getSelectedElement(); 84 | 85 | if( trigger && $( trigger ).is( "input" ) ){ 86 | trigger.value = $selected.text(); 87 | } 88 | $selected.trigger( componentName + ":select" ); 89 | this.close(); 90 | return $selected.text(); 91 | }; 92 | 93 | menu.prototype.keycodes = { 94 | 38 : function(e) { 95 | this.moveSelected( "prev" ); 96 | e.preventDefault(); 97 | }, 98 | 99 | 40 : function(e){ 100 | this.moveSelected( "next" ); 101 | e.preventDefault(); 102 | }, 103 | 104 | 13 : function(){ 105 | // return the selected value 106 | return this.selectActive(); 107 | }, 108 | 109 | 9 : function(e){ 110 | this.moveSelected( e.shiftKey ? "prev" : "next" ); 111 | e.preventDefault(); 112 | }, 113 | 114 | 27 : function(){ 115 | this.close(); 116 | } 117 | }; 118 | 119 | menu.prototype.keyDown = function( e ){ 120 | var fn = this.keycodes[e.keyCode] || function(){}; 121 | return fn.call( this, e ); 122 | }; 123 | 124 | menu.prototype._bindKeyHandling = function(){ 125 | var self = this; 126 | this.$element 127 | .bind( "keydown", function( e ){ 128 | self.keyDown( e ); 129 | } ) 130 | .bind( "mouseover", function( e ){ 131 | self.$element.find( "." + selectClass ).removeClass( selectClass ); 132 | $( e.target ).closest( "li" ).addClass( selectClass ); 133 | }) 134 | .bind( "mouseleave", function( e ){ 135 | $( e.target ).closest( "li" ).removeClass( selectClass ); 136 | }) 137 | .bind( "click", function(){ 138 | self.selectActive(); 139 | }); 140 | }; 141 | 142 | menu.prototype.open = function( trigger, sendFocus ){ 143 | if( this.opened ){ 144 | return; 145 | } 146 | this.$element.attr( at.ariaHidden, false ); 147 | 148 | this.$element.data( componentName + "-trigger", trigger ); 149 | this.opened = true; 150 | this.moveSelected( "start", sendFocus ); 151 | this.$element.trigger( componentName + ":open" ); 152 | }; 153 | 154 | menu.prototype.close = function(){ 155 | if( !this.opened ){ 156 | return; 157 | } 158 | this.$element.attr( at.ariaHidden, true ); 159 | this.opened = false; 160 | var $trigger = this.$element.data( componentName + "-trigger" ); 161 | if( $trigger ){ 162 | $trigger.focus(); 163 | } 164 | this.$element.trigger( componentName + ":close" ); 165 | }; 166 | 167 | menu.prototype.toggle = function( trigger, sendFocus ){ 168 | this[ this.opened ? "close" : "open" ]( trigger, sendFocus ); 169 | }; 170 | 171 | menu.prototype.init = function(){ 172 | // prevent re-init 173 | if( this.$element.data( componentName ) ) { 174 | return; 175 | } 176 | this.$element.data( componentName, this ); 177 | 178 | this.$element.attr( "role", "menu" ); 179 | this.close(); 180 | var self = this; 181 | 182 | $( document ).bind( "mouseup", function(event){ 183 | // only close the menu if the click is outside the menu element 184 | if( ! $(event.target).closest( self.$element[0] ).length ){ 185 | self.close(); 186 | } 187 | }); 188 | 189 | this._bindKeyHandling(); 190 | 191 | this.$element.trigger( componentName + ":init" ); 192 | }; 193 | 194 | menu.prototype.isOpen = function(){ 195 | return this.opened; 196 | }; 197 | 198 | (w.componentNamespace = w.componentNamespace || w)[ componentName ] = menu; 199 | }( jQuery, this )); 200 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "browser": true, 14 | "predef": [ 15 | "jQuery", 16 | "QUnit", 17 | "module", 18 | "test", 19 | "asyncTest", 20 | "expect", 21 | "start", 22 | "stop", 23 | "ok", 24 | "equal", 25 | "notEqual", 26 | "deepEqual", 27 | "notDeepEqual", 28 | "strictEqual", 29 | "notStrictEqual", 30 | "throws" 31 | ] 32 | } -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Menu Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    17 |
    18 |
    19 | 27 |
    28 |
    29 |
    30 | 31 | 32 | -------------------------------------------------------------------------------- /test/menu-test.js: -------------------------------------------------------------------------------- 1 | /*global Menu:true*/ 2 | (function() { 3 | /* 4 | ======== A Handy Little QUnit Reference ======== 5 | http://api.qunitjs.com/ 6 | 7 | Test methods: 8 | module(name, {[setup][ ,teardown]}) 9 | test(name, callback) 10 | expect(numberOfAssertions) 11 | stop(increment) 12 | start(decrement) 13 | Test assertions: 14 | ok(value, [message]) 15 | equal(actual, expected, [message]) 16 | notEqual(actual, expected, [message]) 17 | deepEqual(actual, expected, [message]) 18 | notDeepEqual(actual, expected, [message]) 19 | strictEqual(actual, expected, [message]) 20 | notStrictEqual(actual, expected, [message]) 21 | throws(block, [expected], [message]) 22 | */ 23 | 24 | test( "Menu is defined", function(){ 25 | ok( Menu ); 26 | } ); 27 | 28 | }(jQuery)); --------------------------------------------------------------------------------