├── .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 | [ ](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 |
53 |
54 | Example 1: menu button trigger
55 |
56 | Menu
57 |
58 |
68 |
69 |
70 | Example 2: menu input trigger
71 |
72 |
73 |
74 |
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 | "" +
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 += "Expected: | " + expected + " |
";
920 |
921 | if ( actual !== expected ) {
922 | output += "Result: | " + actual + " |
";
923 | output += "Diff: | " + QUnit.diff( expected, actual ) + " |
";
924 | }
925 |
926 | source = sourceFromStacktrace();
927 |
928 | if ( source ) {
929 | details.source = source;
930 | output += "Source: | " + escapeText( source ) + " |
";
931 | }
932 |
933 | output += "
";
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 += "Result: | " + escapeText( actual ) + " |
";
965 | }
966 |
967 | if ( source ) {
968 | details.source = source;
969 | output += "Source: | " + escapeText( source ) + " |
";
970 | }
971 |
972 | output += "
";
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 |
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));
--------------------------------------------------------------------------------