├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── VERSION ├── css └── lectric.css ├── examples ├── autoplay.html ├── images │ └── scrubber.png ├── scrubber.html ├── simple.html └── thin-frames.html ├── js └── lectric.js └── tests ├── index.html ├── lectric.js └── qunit ├── qunit.css └── qunit.js /.gitignore: -------------------------------------------------------------------------------- 1 | js/*.min.js 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'closure-compiler' 4 | gem 'jslint_on_rails' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | closure-compiler (0.3.3) 5 | jslint_on_rails (1.0.3) 6 | 7 | PLATFORMS 8 | ruby 9 | 10 | DEPENDENCIES 11 | closure-compiler 12 | jslint_on_rails 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2011 Brett C. Buddin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lectric Slider 2 | 3 | Lectric is a JavaScript slider that is touch-enabled and takes advantage of hardware acceleration. It looks awesome on Apple touch devices. You can see an early version of this software implemented on [mckinney.com](http://mckinney.com). 4 | 5 | It's Electric! 6 | 7 | **Requires:** [jQuery](http://github.com/jquery/jquery) 8 | 9 | ## Installation 10 | 11 | Put this in your ``: 12 | 13 | 14 | 15 | 16 | 17 | 18 | ## Usage 19 | 20 | HTML: 21 | 22 |
23 |
Page 1
24 |
Page 2
25 |
Page 3
26 |
Page 4
27 |
28 | 29 | JavaScript: 30 | 31 | var slider = new Lectric(); 32 | slider.init('#slider'); 33 | 34 | ## Optional Parameters 35 | 36 | You can specify a few extra parameters when you call the `init` method. Those include: 37 | 38 | - `next` *(selector)*: Next button 39 | - `previous` *(selector)*: Previous button 40 | - `limitLeft` *(boolean)*: Prohibits the slider from moving left 41 | - `limitRight` *(boolean)*: Prohibits the slider from moving right 42 | - `itemClassName` *(string)*: Class name of the individual pages of the slider (defaults to "item") 43 | - `itemWrapperClassName` *(string)*: Class name of the container that wraps all items (defaults to "items") 44 | - `animateEasing` *(string)*: A string indicating which easing function to use for the transition (non-mobile only). 45 | - `animateDuration` *(integer or string)*: A string (e.g. "fast" or "slow") or number (in milliseconds) determining how long a slide animation will run. 46 | - `hooks` *(map)*: Map of callback functions that should be subscribed to the various hooks (see next section for more about hooks) 47 | - `tossing` *(boolean)*: Turns tossing of the slider on. This lets you "toss" the slider ahead more than one page. (defaults to false) 48 | - `tossFunction` *(function)*: A function to use for calculating the distance (from the touchend point) the slider should be tossed. 49 | 50 | For example, let's provide a slider with next/previous buttons: 51 | 52 | var slider = new Lectric(); 53 | slider.init('#slider', {next: '.next', previous: '.previous'}); 54 | 55 | ## Hook System 56 | 57 | Lectric is designed to give you a great deal of visibility of its insides. To help you extend Lectric, we've provided a simple hook system for you to tap into. Hooks have specific names and are invoked at specific times in the execution of the slider's timeline. 58 | 59 | Subscribing to a hook looks something like this: 60 | 61 | slider.on('slide', function(s, event) { 62 | console.log('We just moved! Our current position is:' + s.position.x); 63 | }); 64 | 65 | Unsubscribing from a hook looks like this: 66 | 67 | var handler = slider.on('slide', function(s, event) { 68 | console.log('We just moved! Our current position is:' + s.position.x); 69 | }); 70 | slider.off('slide', handler); // Unsubscribe handler from slider 71 | 72 | The hooks available to you are: 73 | 74 | - `init`: Triggered after the slider is initialized 75 | - `start`: Triggered when the user puts her finger down on the slider 76 | - `slide`: Triggered when the position of the slider is moved 77 | - `firstSlide`: Triggered the first time the position of the slider is moved (for a single touch event) 78 | - `end`: Triggered when the user lifts her finger off of the slider 79 | - `endNoSlide`: Triggered when the user lifts her finger off of the slider and did not move the slider 80 | - `animationEnd`: Triggered when the slide animation has completed 81 | - `nextButton`: Triggered when the next button is pressed 82 | - `previousButton`: Triggered when the previous button is pressed 83 | 84 | The callback function that you provide the `on` function will pass your callback two parameters: slider controller instance you are manipulating and the jQuery event object that was triggered. Having a reference to the controller object will allow you to augment the behaviour of the slider itself. 85 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'closure-compiler' 4 | require 'jslint' 5 | require 'webrick' 6 | 7 | prefix = File.dirname(__FILE__) 8 | lectric = File.join(prefix, 'js', 'lectric.js') 9 | lectric_min = File.join(prefix, 'js', 'lectric.min.js') 10 | version = File.join(prefix, 'VERSION') 11 | 12 | task :default => :build 13 | 14 | desc "Build and minify Lectric." 15 | task :build => [:lint, :stamp_version, :minify] do 16 | puts "Lectric build complete." 17 | end 18 | 19 | desc "Stamp the library with the current version" 20 | task :stamp_version => :version do 21 | contents = File.read(lectric) 22 | file = File.open(lectric, 'w') 23 | file.puts contents.gsub(/(Lectric v)([\d\w\.-]+)/, "\\1#{@version}") 24 | file.close 25 | end 26 | 27 | desc "Run library against JSLint" 28 | task :lint do 29 | lint = JSLint::Lint.new( 30 | :paths => ['js/**/*.js'], 31 | :exclude_paths => ['js/**/*.min.js'] 32 | ) 33 | lint.run 34 | end 35 | 36 | desc "Compress the library using Google's Closure Compiler" 37 | task :minify => :version do 38 | puts "Minifying Lectric..." 39 | comments = <<-EOS 40 | /*! 41 | * Lectric v#{@version} 42 | * http://github.com/mckinney/lectric 43 | * 44 | * Copyright 2011, McKinney 45 | * Licensed under the MIT license. 46 | * http://github.com/mckinney/lectric/blob/master/LICENSE 47 | * 48 | * Author: Brett C. Buddin 49 | */ 50 | EOS 51 | 52 | file = File.open(lectric_min, 'w') 53 | file.puts comments + Closure::Compiler.new.compile(File.open(lectric, 'r')) 54 | file.close 55 | end 56 | 57 | task :version do 58 | @version = File.read(version).strip 59 | puts "VERSION: #{@version}" 60 | end 61 | 62 | desc "Starts an HTTP server in the current directory" 63 | task :server do 64 | config = {:Port => 3000, :DocumentRoot => '.'} 65 | server = WEBrick::HTTPServer.new config 66 | ['INT', 'TERM'].each do |signal| 67 | trap(signal) { server.shutdown } 68 | end 69 | 70 | server.start 71 | end 72 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.4.5 2 | -------------------------------------------------------------------------------- /css/lectric.css: -------------------------------------------------------------------------------- 1 | .lectric-slider,.lectric-slider .items { 2 | position: relative; 3 | } 4 | .lectric-slider .items { 5 | left: 0; 6 | width: 100000px; 7 | } 8 | .lectric-slider-touch { 9 | -webkit-transform: translate3d(0, 0, 0); 10 | } 11 | .lectric-slider-touch .items { 12 | -webkit-transition-timing-function: cubic-bezier(0, 0, 0.2, 1); 13 | -webkit-font-smoothing: antialiased; 14 | } 15 | .lectric-slider-touch .item { 16 | -webkit-transform: translate3d(0, 0, 0); 17 | } 18 | -------------------------------------------------------------------------------- /examples/autoplay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Autoplay 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 62 | 63 | 64 |
65 |
66 |
Hello world
67 |
Hello world
68 |
Hello world
69 |
Hello world
70 |
Hello world
71 |
72 | 73 | 74 | 75 | 76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 | 86 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /examples/images/scrubber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brettbuddin/lectric/61c0f7e7a2c77ab978b080add5e26240f257c0f2/examples/images/scrubber.png -------------------------------------------------------------------------------- /examples/scrubber.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrubber 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 62 | 63 | 64 |
65 |
66 |
Hello world
67 |
Hello world
68 |
Hello world
69 |
Hello world
70 |
Hello world
71 |
72 | 73 | 74 | 75 | 76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | 85 | 86 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 34 | 35 | 36 |
37 |
Hello world
38 |
Hello world
39 |
Hello world
40 |
Hello world
41 |
42 | 43 | 44 | 45 | 46 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /examples/thin-frames.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simple 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 34 | 35 | 36 |
37 |
Hello world 1
38 |
Hello world 2
39 |
Hello world 3
40 |
Hello world 4
41 |
Hello world 5
42 |
Hello world 6
43 |
Hello world 7
44 |
Hello world 8
45 |
46 | 47 | 48 | 49 | 50 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /js/lectric.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Lectric v0.4.5 3 | * http://github.com/brettbuddin/lectric 4 | * 5 | * Copyright 2011, Brett C. Buddin 6 | * Licensed under the MIT license. 7 | * http://github.com/brettbuddin/lectric/blob/master/LICENSE 8 | * 9 | * Author: Brett C. Buddin (http://github.com/brettbuddin) 10 | */ 11 | (function( factory ) { 12 | //AMD 13 | if(typeof define === 'function' && define.amd) { 14 | define(['jquery'], factory); 15 | 16 | //NODE 17 | } else if(typeof module === 'object' && module.exports) { 18 | var $ = require('jquery'); 19 | module.exports = factory($); 20 | 21 | //GLOBAL 22 | } else { 23 | window.Lectric = factory(jQuery); 24 | } 25 | })(function($) { 26 | var ua = navigator.userAgent.toLowerCase(); 27 | isWebkit = !!ua.match(/applewebkit/i); 28 | var supportsTouch = false; 29 | try { 30 | document.createEvent("TouchEvent"); 31 | supportsTouch = true; 32 | } catch (e) {} 33 | 34 | var cssWithoutUnit = function(element, attribute) { 35 | var measure = element.css(attribute); 36 | return (measure !== undefined) ? parseInt(measure.replace('px', ''), 10) : 0; 37 | }; 38 | 39 | var Position = function(x, y) { 40 | if (x && x.hasOwnProperty('x') && x.hasOwnProperty('y')) { 41 | x = x.x; 42 | y = x.y; 43 | } 44 | this.x = x; 45 | this.y = y; 46 | }; 47 | Position.prototype = { 48 | difference: function(p) { 49 | return new Position(p.x - this.x, p.y - this.y); 50 | } 51 | }; 52 | 53 | 54 | 55 | var Lectric = function() { 56 | if (supportsTouch && isWebkit) { 57 | return new TouchSlider(); 58 | } else { 59 | return new BaseSlider(); 60 | } 61 | }; 62 | 63 | var BaseSlider = function() {}; 64 | 65 | // Initialize the BaseSlider. 66 | // 67 | // text - The String CSS selector of the slider container. 68 | // opts - The Map of extra parameters. 69 | // 70 | // Returns nothing. 71 | BaseSlider.prototype.init = function(target, opts) { 72 | this.opts = $.extend({ 73 | reverse: false, 74 | next: undefined, 75 | previous: undefined, 76 | itemWrapperClassName: 'items', 77 | itemClassName: 'item', 78 | limitLeft: false, 79 | limitRight: false, 80 | animateEasing: 'swing', 81 | animateDuration: $.fx.speeds._default, 82 | hooks: {} 83 | }, opts); 84 | 85 | this.position = new Position(0, 0); 86 | this.startPosition = new Position(this.position); 87 | this.lastPosition = new Position(this.position); 88 | 89 | // Set up the styling of the slider 90 | var element = $('
', { 91 | 'class': this.opts.itemWrapperClassName 92 | }); 93 | element.css('width', '1000000px'); 94 | 95 | var itemSelector = '.' + this.opts.itemClassName; 96 | var itemWrapperSelector = '.' + this.opts.itemWrapperClassName; 97 | 98 | this.target = $(target); 99 | this.target.css('overflow', 'hidden'); 100 | this.target.find(itemSelector).css('float', 'left').wrapAll(element); 101 | this.target.addClass('lectric-slider'); 102 | this.element = this.target.find(itemWrapperSelector); 103 | this.element.itemSelector = itemSelector; 104 | this.element.itemWrapperSelector = itemWrapperSelector; 105 | 106 | var self = this; 107 | 108 | var type = supportsTouch ? 'touchstart' : 'click'; 109 | $(this.opts.next).bind(type, function(e) { 110 | e.preventDefault(); 111 | var page = self.page(); 112 | self.to(page + 1); 113 | self.element.trigger('nextButton.lectric'); 114 | }); 115 | 116 | $(this.opts.previous).bind(type, function(e) { 117 | e.preventDefault(); 118 | var page = self.page(); 119 | self.to(page - 1); 120 | self.element.trigger('previousButton.lectric'); 121 | }); 122 | 123 | // Keep clicks from doing what they do if 124 | // we support touch on this device 125 | if (supportsTouch) { 126 | $(this.opts.next).click(function(e) { 127 | e.preventDefault(); 128 | }); 129 | 130 | $(this.opts.previous).click(function(e) { 131 | e.preventDefault(); 132 | }); 133 | } 134 | 135 | // Bind callbacks passed in at initialization 136 | $.each(this.opts.hooks, function(name, fn) { 137 | if ($.isArray(fn)) { 138 | $.each(fn, function(fn2) { 139 | self.on(name, fn2); 140 | }); 141 | } else { 142 | self.on(name, fn); 143 | } 144 | }); 145 | 146 | this.element.trigger('init.lectric'); 147 | }; 148 | 149 | // Update the current position of the slider. 150 | // 151 | // opts - The Map of extra parameters: 152 | // animate - Boolean of whether or not to animate between two states. 153 | // triggerSlide - Boolean of whether or not to trigger the move hook. 154 | // 155 | // Returns nothing. 156 | BaseSlider.prototype.update = function(opts) { 157 | var options = $.extend({animate: true, triggerSlide: true}, opts); 158 | 159 | var self = this; 160 | var after = function() { 161 | self.element.trigger('animationEnd.lectric'); 162 | $(this).dequeue(); 163 | }; 164 | 165 | if (options.animate) { 166 | this.element.animate({left: this.position.x + 'px'}, 167 | this.opts.animateDuration, 168 | this.opts.animateEasing 169 | ).queue(after); 170 | } else { 171 | this.element.css({left: this.position.x + 'px'}).queue(after); 172 | } 173 | 174 | if (options.triggerSlide) { this.element.trigger('slide.lectric'); } 175 | }; 176 | 177 | 178 | // Subscribe a callback function to a hook. 179 | // 180 | // name - The String name of the hook. 181 | // fn - The Function callback to execute when the hook is triggered. 182 | // 183 | // Returns the Function callback that was bound to the hook. 184 | BaseSlider.prototype.on = function(name, fn) { 185 | var self = this; 186 | var callback = function(e) { 187 | if (e.target == self.element[0]) { 188 | fn(self, e); 189 | } 190 | }; 191 | 192 | this.element.bind(name + '.lectric', callback); 193 | return callback; 194 | }; 195 | BaseSlider.prototype.bind = function(name, fn) { 196 | this.on(name, fn); 197 | }; 198 | 199 | // Unsubscribe a callback function from a hook or unsubscribe all callbacks from a hook. 200 | // 201 | // name - The String name of the hook. 202 | // fn - The Function handler to unbind from the element. 203 | // 204 | // Returns nothing. 205 | BaseSlider.prototype.off = function(name, fn) { 206 | if (typeof fn !== undefined && $.isFunction(fn)) { 207 | this.element.unbind(name + '.lectric', fn); 208 | } else { 209 | this.element.unbind(name + '.lectric'); 210 | } 211 | }; 212 | BaseSlider.prototype.unbind = function(name, fn) { 213 | this.off(name, fn); 214 | }; 215 | 216 | // Retrieve the current page of the slider. 217 | // 218 | // Returns the Integer page number of the slider. 219 | BaseSlider.prototype.page = function() { 220 | return Math.abs(Math.round(this.position.x / this.itemWidth())); 221 | }; 222 | 223 | // Move to a specific page number. 224 | // 225 | // page - The Integer page number to move to. 226 | // 227 | // Returns nothing. 228 | BaseSlider.prototype.to = function(page) { 229 | var previous = this.position.x; 230 | this.position.x = this.limitXBounds(this.xForPage(page)); 231 | if (this.position.x !== previous) { 232 | this.update(); 233 | } 234 | }; 235 | 236 | // Move to a specific item in the slider, regardless of its position. 237 | // 238 | // item - The DOM Reference of the item you'd like to move to. 239 | // 240 | // Returns nothing. 241 | BaseSlider.prototype.toItem = function(item) { 242 | var all = this.element.find(this.element.itemSelector); 243 | 244 | var i; 245 | var length = all.length; 246 | for (i = 0; i < length; i++) { 247 | if ($(all[i])[0] == item[0]) { this.to(i); } 248 | } 249 | }; 250 | 251 | // Retrieve the current X position. 252 | // 253 | // page - The Integer page number. 254 | // 255 | // Returns the Integer X position of the slider. 256 | BaseSlider.prototype.xForPage = function(page) { 257 | var flip = (this.opts.reverse) ? 1 : -1; 258 | return flip * page * this.itemWidth(); 259 | }; 260 | 261 | 262 | // Retrieve the width of a single item (including margin-right and padding). 263 | // 264 | // Returns the Integer width of a single item. 265 | BaseSlider.prototype.itemWidth = function() { 266 | var first = this.element.find(this.element.itemSelector).eq(0); 267 | var padding = cssWithoutUnit(first, 'paddingRight') + cssWithoutUnit(first, 'paddingLeft'); 268 | return cssWithoutUnit(first, 'marginRight') + padding + first.width(); 269 | }; 270 | 271 | // Retrieve number of items in the slider. 272 | // 273 | // Returns the Integer number of items. 274 | BaseSlider.prototype.itemCount = function() { 275 | return this.element.find(this.element.itemSelector).size(); 276 | }; 277 | 278 | 279 | // Constrain the X position to within the slider beginning and end. 280 | // 281 | // x - The Integer X position 282 | // 283 | // Returns the Integer X position after being constrained. 284 | BaseSlider.prototype.limitXBounds = function(x) { 285 | var itemWidth = this.itemWidth(); 286 | var itemCount = this.itemCount(); 287 | var extraSpaceInTarget = this.target.width() - itemWidth; 288 | var totalWidth = (itemWidth * itemCount) - extraSpaceInTarget; 289 | 290 | 291 | if (this.opts.reverse) { 292 | x = (x > totalWidth - itemWidth) ? totalWidth - itemWidth : x; 293 | x = (x < 0) ? 0 : x; 294 | } else { 295 | x = (x < -totalWidth + itemWidth) ? -totalWidth + itemWidth : x; 296 | x = (x > 0) ? 0 : x; 297 | } 298 | 299 | if ((this.position.x - x > 0 && this.opts.limitRight) || 300 | (this.position.x - x < 0 && this.opts.limitLeft)) { 301 | x = this.position.x; 302 | } 303 | 304 | return x; 305 | }; 306 | 307 | 308 | 309 | var TouchSlider = function() {}; 310 | TouchSlider.prototype = new BaseSlider(); 311 | TouchSlider.superobject = BaseSlider.prototype; 312 | 313 | // Initialize the TouchSlider. 314 | // 315 | // text - The String CSS selector of the slider container. 316 | // opts - The Map of extra parameters. 317 | // 318 | // Returns nothing. 319 | TouchSlider.prototype.init = function(target, opts) { 320 | TouchSlider.superobject.init.call(this, target, opts); 321 | this.opts = $.extend({ 322 | tossFunction: function(x, dx, dt) { 323 | return x + dx * 100 / dt; 324 | }, 325 | tossing: false 326 | }, this.opts); 327 | $(target).addClass('lectric-slider-touch'); 328 | 329 | this.gesturing = false; 330 | $(target)[0].addEventListener('touchstart', this, false); 331 | $(target)[0].addEventListener('webkitTransitionEnd', this, false); 332 | }; 333 | 334 | // Proxy the events triggered on the element to another function. 335 | // 336 | // event - The Event triggered on the element 337 | // 338 | // Returns nothing. 339 | TouchSlider.prototype.handleEvent = function(event) { 340 | TouchEvents[event.type].call(this, event); 341 | }; 342 | 343 | 344 | 345 | // Update the current position of the slider. 346 | // 347 | // opts - The Map of extra parameters: 348 | // animate - Boolean of whether or not to animate between two states. 349 | // triggerSlide - Boolean of whether or not to trigger the move hook. 350 | // 351 | // Returns nothing. 352 | TouchSlider.prototype.update = function(opts) { 353 | var options = $.extend({animate: true, triggerSlide: true}, opts); 354 | if (options.animate) { this.decayOn(); } 355 | this.element.css({'-webkit-transform': 'translate3d(' + this.position.x + 'px, 0, 0)'}); 356 | 357 | if (options.triggerSlide) { this.element.trigger('slide.lectric'); } 358 | }; 359 | 360 | 361 | // Turn off CSS3 animation decay. 362 | // 363 | // Returns nothing. 364 | TouchSlider.prototype.decayOff = function() { 365 | this.element.css({'-webkit-transition-duration': '0s'}); 366 | this.element.css({'-webkit-transition-property': 'none'}); 367 | }; 368 | 369 | // Turn on CSS3 animation decay. 370 | // 371 | // Returns nothing. 372 | TouchSlider.prototype.decayOn = function() { 373 | var duration = this.opts.animateDuration; 374 | if (typeof duration === "number") { 375 | duration = duration / 1000; 376 | } else { 377 | if (duration in $.fx.speeds) { 378 | duration = $.fx.speeds[duration]; 379 | } else { 380 | duration = $.fx.speeds._default; 381 | } 382 | } 383 | this.element.css({'-webkit-transition-duration': duration + 's'}); 384 | this.element.css({'-webkit-transition-property': '-webkit-transform'}); 385 | }; 386 | 387 | var TouchEvents = { 388 | click: function(e) { 389 | if (this.moved) { e.preventDefault(); } 390 | this.element[0].removeEventListener('click', this, false); 391 | return false; 392 | }, 393 | 394 | touchstart: function(e) { 395 | this.currentTarget = e.currentTarget; 396 | this.startPosition.x = e.touches[0].pageX - this.position.x; 397 | this.startPosition.y = e.touches[0].pageY - this.position.y; 398 | this.moved = false; 399 | 400 | window.addEventListener('gesturestart', this, false); 401 | window.addEventListener('gestureend', this, false); 402 | window.addEventListener('touchmove', this, false); 403 | window.addEventListener('touchend', this, false); 404 | this.element[0].addEventListener('click', this, false); 405 | 406 | this.decayOff(); 407 | 408 | this.element.trigger('start.lectric'); 409 | }, 410 | 411 | touchmove: function(e) { 412 | if (this.gesturing) { return false; } 413 | 414 | if (!this.moved) { 415 | var deltaY = e.touches[0].pageY - this.startPosition.y; 416 | var deltaX = e.touches[0].pageX - this.startPosition.x; 417 | if (Math.abs(deltaY) < 15) { 418 | e.preventDefault(); 419 | } 420 | 421 | this.element.trigger('firstSlide.lectric'); 422 | } 423 | 424 | this.moved = true; 425 | this.lastPosition.x = this.position.x; 426 | this.lastPosition.y = this.position.y; 427 | this.lastMoveTime = new Date(); 428 | 429 | this.position.x = this.limitXBounds(e.touches[0].pageX - this.startPosition.x); 430 | 431 | this.update({animate: false}); 432 | }, 433 | 434 | touchend: function(e) { 435 | window.removeEventListener('gesturestart', this, false); 436 | window.removeEventListener('gestureend', this, false); 437 | window.removeEventListener('touchmove', this, false); 438 | window.removeEventListener('touchend', this, false); 439 | 440 | if (this.moved) { 441 | var dx = this.position.x - this.lastPosition.x; 442 | var dt = (new Date()) - this.lastMoveTime + 1; 443 | 444 | var width = this.itemWidth(); 445 | 446 | if (this.opts.tossing) { 447 | var tossedX = this.limitXBounds(this.opts.tossFunction(this.position.x, dx, dt)); 448 | this.position.x = Math.round(tossedX / width) * width; 449 | } else { 450 | this.position.x = Math.round(this.position.x / width) * width; 451 | } 452 | 453 | this.update(); 454 | this.element.trigger('end.lectric'); 455 | } else { 456 | this.element.trigger('endNoSlide.lectric'); 457 | } 458 | 459 | this.currentTarget = undefined; 460 | }, 461 | 462 | gesturestart: function(e) { 463 | this.gesturing = true; 464 | }, 465 | 466 | gestureend: function(e) { 467 | this.gesturing = false; 468 | }, 469 | 470 | webkitTransitionEnd: function(e) { 471 | this.element.trigger('animationEnd.lectric'); 472 | } 473 | }; 474 | 475 | Lectric.BaseSlider = BaseSlider; 476 | Lectric.TouchSlider = TouchSlider; 477 | 478 | return Lectric; 479 | }); 480 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 36 | 37 | 38 |

QUnit example

39 |

40 |
41 |

42 |
    43 |
    test markup, will be hidden
    44 | 45 |
    46 |
    Hello world
    47 |
    Hello world
    48 |
    Hello world
    49 |
    Hello world
    50 |
    51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/lectric.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var slider = new Lectric(); 3 | slider.init('#slider', {next: '.next', previous: '.previous'}); 4 | 5 | test("setup", function() { 6 | expect(1); 7 | 8 | ok(window.Lectric, 'global Lectric object is created'); 9 | }); 10 | 11 | test("structure", function() { 12 | expect(6); 13 | 14 | equals(slider.itemWidth(), 530, "report the width of each individual item"); 15 | equals(slider.itemCount(), 4, "report the number of pages"); 16 | equals(slider.element.parent().css('position'), 'relative', "relative positioning set to container"); 17 | equals(slider.element.css('position'), 'relative', "relative positioning set to items container"); 18 | equals(slider.element.css('left'), '0px', "assigns an initial value to left"); 19 | ok(slider.element.parent().hasClass('lectric-slider'), "has the lectric-slider class assigned to the container"); 20 | }); 21 | 22 | test("movement", function() { 23 | expect(3); 24 | 25 | slider.on('animationEnd', function() { 26 | start(); 27 | }); 28 | equals(slider.page(), 0, "start on item 0"); 29 | 30 | slider.to(1); 31 | stop(); 32 | equals(slider.page(), 1, "move to item 1"); 33 | 34 | slider.toItem($('#slider .item').eq(3)); 35 | stop(); 36 | equals(slider.page(), 3, "move to item 3"); 37 | }); 38 | 39 | test("subscribing to hooks", function() { 40 | expect(1); 41 | 42 | var handler = slider.on('hello', function(s, e) { 43 | equals(e.type, 'hello'); 44 | start(); 45 | }); 46 | slider.element.trigger('hello.lectric'); 47 | stop(); 48 | slider.off('hello', handler); 49 | }); 50 | 51 | test("unsubscribing from hooks", function() { 52 | expect(1); 53 | 54 | var counter = 0; 55 | var handler = slider.on('hello', function(s, e) { 56 | counter++; 57 | start(); 58 | }); 59 | 60 | slider.element.trigger('hello.lectric'); 61 | stop(); 62 | 63 | slider.off('hello', handler); 64 | slider.element.trigger('hello.lectric'); 65 | equals(counter, 1); 66 | }); 67 | 68 | test("next button", function() { 69 | expect(1); 70 | 71 | var click = function(e) { 72 | equals(e.type, 'click'); 73 | start(); 74 | }; 75 | $('.next').bind('click', click); 76 | $('.next').trigger('click'); 77 | stop(); 78 | }); 79 | 80 | test("previous button", function() { 81 | expect(1); 82 | 83 | var click = function(e) { 84 | equals(e.type, 'click'); 85 | start(); 86 | }; 87 | $('.previous').bind('click', click); 88 | $('.previous').trigger('click'); 89 | stop(); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** Font Family and Sizes */ 2 | 3 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 4 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; 5 | } 6 | 7 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 8 | #qunit-tests { font-size: smaller; } 9 | 10 | 11 | /** Resets */ 12 | 13 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | 19 | /** Header */ 20 | 21 | #qunit-header { 22 | padding: 0.5em 0 0.5em 1em; 23 | 24 | color: #8699a4; 25 | background-color: #0d3349; 26 | 27 | font-size: 1.5em; 28 | line-height: 1em; 29 | font-weight: normal; 30 | 31 | border-radius: 15px 15px 0 0; 32 | -moz-border-radius: 15px 15px 0 0; 33 | -webkit-border-top-right-radius: 15px; 34 | -webkit-border-top-left-radius: 15px; 35 | } 36 | 37 | #qunit-header a { 38 | text-decoration: none; 39 | color: #c2ccd1; 40 | } 41 | 42 | #qunit-header a:hover, 43 | #qunit-header a:focus { 44 | color: #fff; 45 | } 46 | 47 | #qunit-banner { 48 | height: 5px; 49 | } 50 | 51 | #qunit-testrunner-toolbar { 52 | padding: 0.5em 0 0.5em 2em; 53 | color: #5E740B; 54 | background-color: #eee; 55 | } 56 | 57 | #qunit-userAgent { 58 | padding: 0.5em 0 0.5em 2.5em; 59 | background-color: #2b81af; 60 | color: #fff; 61 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 62 | } 63 | 64 | 65 | /** Tests: Pass/Fail */ 66 | 67 | #qunit-tests { 68 | list-style-position: inside; 69 | } 70 | 71 | #qunit-tests li { 72 | padding: 0.4em 0.5em 0.4em 2.5em; 73 | border-bottom: 1px solid #fff; 74 | list-style-position: inside; 75 | } 76 | 77 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 78 | display: none; 79 | } 80 | 81 | #qunit-tests li strong { 82 | cursor: pointer; 83 | } 84 | 85 | #qunit-tests ol { 86 | margin-top: 0.5em; 87 | padding: 0.5em; 88 | 89 | background-color: #fff; 90 | 91 | border-radius: 15px; 92 | -moz-border-radius: 15px; 93 | -webkit-border-radius: 15px; 94 | 95 | box-shadow: inset 0px 2px 13px #999; 96 | -moz-box-shadow: inset 0px 2px 13px #999; 97 | -webkit-box-shadow: inset 0px 2px 13px #999; 98 | } 99 | 100 | #qunit-tests table { 101 | border-collapse: collapse; 102 | margin-top: .2em; 103 | } 104 | 105 | #qunit-tests th { 106 | text-align: right; 107 | vertical-align: top; 108 | padding: 0 .5em 0 0; 109 | } 110 | 111 | #qunit-tests td { 112 | vertical-align: top; 113 | } 114 | 115 | #qunit-tests pre { 116 | margin: 0; 117 | white-space: pre-wrap; 118 | word-wrap: break-word; 119 | } 120 | 121 | #qunit-tests del { 122 | background-color: #e0f2be; 123 | color: #374e0c; 124 | text-decoration: none; 125 | } 126 | 127 | #qunit-tests ins { 128 | background-color: #ffcaca; 129 | color: #500; 130 | text-decoration: none; 131 | } 132 | 133 | /*** Test Counts */ 134 | 135 | #qunit-tests b.counts { color: black; } 136 | #qunit-tests b.passed { color: #5E740B; } 137 | #qunit-tests b.failed { color: #710909; } 138 | 139 | #qunit-tests li li { 140 | margin: 0.5em; 141 | padding: 0.4em 0.5em 0.4em 0.5em; 142 | background-color: #fff; 143 | border-bottom: none; 144 | list-style-position: inside; 145 | } 146 | 147 | /*** Passing Styles */ 148 | 149 | #qunit-tests li li.pass { 150 | color: #5E740B; 151 | background-color: #fff; 152 | border-left: 26px solid #C6E746; 153 | } 154 | 155 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 156 | #qunit-tests .pass .test-name { color: #366097; } 157 | 158 | #qunit-tests .pass .test-actual, 159 | #qunit-tests .pass .test-expected { color: #999999; } 160 | 161 | #qunit-banner.qunit-pass { background-color: #C6E746; } 162 | 163 | /*** Failing Styles */ 164 | 165 | #qunit-tests li li.fail { 166 | color: #710909; 167 | background-color: #fff; 168 | border-left: 26px solid #EE5757; 169 | } 170 | 171 | #qunit-tests > li:last-child { 172 | border-radius: 0 0 15px 15px; 173 | -moz-border-radius: 0 0 15px 15px; 174 | -webkit-border-bottom-right-radius: 15px; 175 | -webkit-border-bottom-left-radius: 15px; 176 | } 177 | 178 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 179 | #qunit-tests .fail .test-name, 180 | #qunit-tests .fail .module-name { color: #000000; } 181 | 182 | #qunit-tests .fail .test-actual { color: #EE5757; } 183 | #qunit-tests .fail .test-expected { color: green; } 184 | 185 | #qunit-banner.qunit-fail { background-color: #EE5757; } 186 | 187 | 188 | /** Result */ 189 | 190 | #qunit-testresult { 191 | padding: 0.5em 0.5em 0.5em 2.5em; 192 | 193 | color: #2b81af; 194 | background-color: #D2E0E6; 195 | 196 | border-bottom: 1px solid white; 197 | } 198 | 199 | /** Fixture */ 200 | 201 | #qunit-fixture { 202 | position: absolute; 203 | top: -10000px; 204 | left: -10000px; 205 | } 206 | -------------------------------------------------------------------------------- /tests/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | try { 17 | return !!sessionStorage.getItem; 18 | } catch(e){ 19 | return false; 20 | } 21 | })() 22 | }; 23 | 24 | var testId = 0; 25 | 26 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 27 | this.name = name; 28 | this.testName = testName; 29 | this.expected = expected; 30 | this.testEnvironmentArg = testEnvironmentArg; 31 | this.async = async; 32 | this.callback = callback; 33 | this.assertions = []; 34 | }; 35 | Test.prototype = { 36 | init: function() { 37 | var tests = id("qunit-tests"); 38 | if (tests) { 39 | var b = document.createElement("strong"); 40 | b.innerHTML = "Running " + this.name; 41 | var li = document.createElement("li"); 42 | li.appendChild( b ); 43 | li.className = "running"; 44 | li.id = this.id = "test-output" + testId++; 45 | tests.appendChild( li ); 46 | } 47 | }, 48 | setup: function() { 49 | if (this.module != config.previousModule) { 50 | if ( config.previousModule ) { 51 | QUnit.moduleDone( { 52 | name: config.previousModule, 53 | failed: config.moduleStats.bad, 54 | passed: config.moduleStats.all - config.moduleStats.bad, 55 | total: config.moduleStats.all 56 | } ); 57 | } 58 | config.previousModule = this.module; 59 | config.moduleStats = { all: 0, bad: 0 }; 60 | QUnit.moduleStart( { 61 | name: this.module 62 | } ); 63 | } 64 | 65 | config.current = this; 66 | this.testEnvironment = extend({ 67 | setup: function() {}, 68 | teardown: function() {} 69 | }, this.moduleTestEnvironment); 70 | if (this.testEnvironmentArg) { 71 | extend(this.testEnvironment, this.testEnvironmentArg); 72 | } 73 | 74 | QUnit.testStart( { 75 | name: this.testName 76 | } ); 77 | 78 | // allow utility functions to access the current test environment 79 | // TODO why?? 80 | QUnit.current_testEnvironment = this.testEnvironment; 81 | 82 | try { 83 | if ( !config.pollution ) { 84 | saveGlobal(); 85 | } 86 | 87 | this.testEnvironment.setup.call(this.testEnvironment); 88 | } catch(e) { 89 | QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); 90 | } 91 | }, 92 | run: function() { 93 | if ( this.async ) { 94 | QUnit.stop(); 95 | } 96 | 97 | if ( config.notrycatch ) { 98 | this.callback.call(this.testEnvironment); 99 | return; 100 | } 101 | try { 102 | this.callback.call(this.testEnvironment); 103 | } catch(e) { 104 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 105 | QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); 106 | // else next test will carry the responsibility 107 | saveGlobal(); 108 | 109 | // Restart the tests if they're blocking 110 | if ( config.blocking ) { 111 | start(); 112 | } 113 | } 114 | }, 115 | teardown: function() { 116 | try { 117 | checkPollution(); 118 | this.testEnvironment.teardown.call(this.testEnvironment); 119 | } catch(e) { 120 | QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); 121 | } 122 | }, 123 | finish: function() { 124 | if ( this.expected && this.expected != this.assertions.length ) { 125 | QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); 126 | } 127 | 128 | var good = 0, bad = 0, 129 | tests = id("qunit-tests"); 130 | 131 | config.stats.all += this.assertions.length; 132 | config.moduleStats.all += this.assertions.length; 133 | 134 | if ( tests ) { 135 | var ol = document.createElement("ol"); 136 | 137 | for ( var i = 0; i < this.assertions.length; i++ ) { 138 | var assertion = this.assertions[i]; 139 | 140 | var li = document.createElement("li"); 141 | li.className = assertion.result ? "pass" : "fail"; 142 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 143 | ol.appendChild( li ); 144 | 145 | if ( assertion.result ) { 146 | good++; 147 | } else { 148 | bad++; 149 | config.stats.bad++; 150 | config.moduleStats.bad++; 151 | } 152 | } 153 | 154 | // store result when possible 155 | QUnit.config.reorder && defined.sessionStorage && sessionStorage.setItem("qunit-" + this.testName, bad); 156 | 157 | if (bad == 0) { 158 | ol.style.display = "none"; 159 | } 160 | 161 | var b = document.createElement("strong"); 162 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 163 | 164 | addEvent(b, "click", function() { 165 | var next = b.nextSibling, display = next.style.display; 166 | next.style.display = display === "none" ? "block" : "none"; 167 | }); 168 | 169 | addEvent(b, "dblclick", function(e) { 170 | var target = e && e.target ? e.target : window.event.srcElement; 171 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 172 | target = target.parentNode; 173 | } 174 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 175 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 176 | } 177 | }); 178 | 179 | var li = id(this.id); 180 | li.className = bad ? "fail" : "pass"; 181 | li.removeChild( li.firstChild ); 182 | li.appendChild( b ); 183 | li.appendChild( ol ); 184 | 185 | } else { 186 | for ( var i = 0; i < this.assertions.length; i++ ) { 187 | if ( !this.assertions[i].result ) { 188 | bad++; 189 | config.stats.bad++; 190 | config.moduleStats.bad++; 191 | } 192 | } 193 | } 194 | 195 | try { 196 | QUnit.reset(); 197 | } catch(e) { 198 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 199 | } 200 | 201 | QUnit.testDone( { 202 | name: this.testName, 203 | failed: bad, 204 | passed: this.assertions.length - bad, 205 | total: this.assertions.length 206 | } ); 207 | }, 208 | 209 | queue: function() { 210 | var test = this; 211 | synchronize(function() { 212 | test.init(); 213 | }); 214 | function run() { 215 | // each of these can by async 216 | synchronize(function() { 217 | test.setup(); 218 | }); 219 | synchronize(function() { 220 | test.run(); 221 | }); 222 | synchronize(function() { 223 | test.teardown(); 224 | }); 225 | synchronize(function() { 226 | test.finish(); 227 | }); 228 | } 229 | // defer when previous test run passed, if storage is available 230 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.testName); 231 | if (bad) { 232 | run(); 233 | } else { 234 | synchronize(run); 235 | }; 236 | } 237 | 238 | }; 239 | 240 | var QUnit = { 241 | 242 | // call on start of module test to prepend name to all tests 243 | module: function(name, testEnvironment) { 244 | config.currentModule = name; 245 | config.currentModuleTestEnviroment = testEnvironment; 246 | }, 247 | 248 | asyncTest: function(testName, expected, callback) { 249 | if ( arguments.length === 2 ) { 250 | callback = expected; 251 | expected = 0; 252 | } 253 | 254 | QUnit.test(testName, expected, callback, true); 255 | }, 256 | 257 | test: function(testName, expected, callback, async) { 258 | var name = '' + testName + '', testEnvironmentArg; 259 | 260 | if ( arguments.length === 2 ) { 261 | callback = expected; 262 | expected = null; 263 | } 264 | // is 2nd argument a testEnvironment? 265 | if ( expected && typeof expected === 'object') { 266 | testEnvironmentArg = expected; 267 | expected = null; 268 | } 269 | 270 | if ( config.currentModule ) { 271 | name = '' + config.currentModule + ": " + name; 272 | } 273 | 274 | if ( !validTest(config.currentModule + ": " + testName) ) { 275 | return; 276 | } 277 | 278 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 279 | test.module = config.currentModule; 280 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 281 | test.queue(); 282 | }, 283 | 284 | /** 285 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 286 | */ 287 | expect: function(asserts) { 288 | config.current.expected = asserts; 289 | }, 290 | 291 | /** 292 | * Asserts true. 293 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 294 | */ 295 | ok: function(a, msg) { 296 | a = !!a; 297 | var details = { 298 | result: a, 299 | message: msg 300 | }; 301 | msg = escapeHtml(msg); 302 | QUnit.log(details); 303 | config.current.assertions.push({ 304 | result: a, 305 | message: msg 306 | }); 307 | }, 308 | 309 | /** 310 | * Checks that the first two arguments are equal, with an optional message. 311 | * Prints out both actual and expected values. 312 | * 313 | * Prefered to ok( actual == expected, message ) 314 | * 315 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 316 | * 317 | * @param Object actual 318 | * @param Object expected 319 | * @param String message (optional) 320 | */ 321 | equal: function(actual, expected, message) { 322 | QUnit.push(expected == actual, actual, expected, message); 323 | }, 324 | 325 | notEqual: function(actual, expected, message) { 326 | QUnit.push(expected != actual, actual, expected, message); 327 | }, 328 | 329 | deepEqual: function(actual, expected, message) { 330 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 331 | }, 332 | 333 | notDeepEqual: function(actual, expected, message) { 334 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 335 | }, 336 | 337 | strictEqual: function(actual, expected, message) { 338 | QUnit.push(expected === actual, actual, expected, message); 339 | }, 340 | 341 | notStrictEqual: function(actual, expected, message) { 342 | QUnit.push(expected !== actual, actual, expected, message); 343 | }, 344 | 345 | raises: function(block, expected, message) { 346 | var actual, ok = false; 347 | 348 | if (typeof expected === 'string') { 349 | message = expected; 350 | expected = null; 351 | } 352 | 353 | try { 354 | block(); 355 | } catch (e) { 356 | actual = e; 357 | } 358 | 359 | if (actual) { 360 | // we don't want to validate thrown error 361 | if (!expected) { 362 | ok = true; 363 | // expected is a regexp 364 | } else if (QUnit.objectType(expected) === "regexp") { 365 | ok = expected.test(actual); 366 | // expected is a constructor 367 | } else if (actual instanceof expected) { 368 | ok = true; 369 | // expected is a validation function which returns true is validation passed 370 | } else if (expected.call({}, actual) === true) { 371 | ok = true; 372 | } 373 | } 374 | 375 | QUnit.ok(ok, message); 376 | }, 377 | 378 | start: function() { 379 | config.semaphore--; 380 | if (config.semaphore > 0) { 381 | // don't start until equal number of stop-calls 382 | return; 383 | } 384 | if (config.semaphore < 0) { 385 | // ignore if start is called more often then stop 386 | config.semaphore = 0; 387 | } 388 | // A slight delay, to avoid any current callbacks 389 | if ( defined.setTimeout ) { 390 | window.setTimeout(function() { 391 | if ( config.timeout ) { 392 | clearTimeout(config.timeout); 393 | } 394 | 395 | config.blocking = false; 396 | process(); 397 | }, 13); 398 | } else { 399 | config.blocking = false; 400 | process(); 401 | } 402 | }, 403 | 404 | stop: function(timeout) { 405 | config.semaphore++; 406 | config.blocking = true; 407 | 408 | if ( timeout && defined.setTimeout ) { 409 | clearTimeout(config.timeout); 410 | config.timeout = window.setTimeout(function() { 411 | QUnit.ok( false, "Test timed out" ); 412 | QUnit.start(); 413 | }, timeout); 414 | } 415 | }, 416 | 417 | url: function( params ) { 418 | params = extend( extend( {}, QUnit.urlParams ), params ); 419 | var querystring = "?", 420 | key; 421 | for ( key in params ) { 422 | querystring += encodeURIComponent( key ) + "=" + 423 | encodeURIComponent( params[ key ] ) + "&"; 424 | } 425 | return window.location.pathname + querystring.slice( 0, -1 ); 426 | } 427 | }; 428 | 429 | // Backwards compatibility, deprecated 430 | QUnit.equals = QUnit.equal; 431 | QUnit.same = QUnit.deepEqual; 432 | 433 | // Maintain internal state 434 | var config = { 435 | // The queue of tests to run 436 | queue: [], 437 | 438 | // block until document ready 439 | blocking: true, 440 | 441 | // by default, run previously failed tests first 442 | // very useful in combination with "Hide passed tests" checked 443 | reorder: true, 444 | 445 | noglobals: false, 446 | notrycatch: false 447 | }; 448 | 449 | // Load paramaters 450 | (function() { 451 | var location = window.location || { search: "", protocol: "file:" }, 452 | params = location.search.slice( 1 ).split( "&" ), 453 | length = params.length, 454 | urlParams = {}, 455 | current; 456 | 457 | if ( params[ 0 ] ) { 458 | for ( var i = 0; i < length; i++ ) { 459 | current = params[ i ].split( "=" ); 460 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 461 | // allow just a key to turn on a flag, e.g., test.html?noglobals 462 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 463 | urlParams[ current[ 0 ] ] = current[ 1 ]; 464 | if ( current[ 0 ] in config ) { 465 | config[ current[ 0 ] ] = current[ 1 ]; 466 | } 467 | } 468 | } 469 | 470 | QUnit.urlParams = urlParams; 471 | config.filter = urlParams.filter; 472 | 473 | // Figure out if we're running the tests from a server or not 474 | QUnit.isLocal = !!(location.protocol === 'file:'); 475 | })(); 476 | 477 | // Expose the API as global variables, unless an 'exports' 478 | // object exists, in that case we assume we're in CommonJS 479 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 480 | extend(window, QUnit); 481 | window.QUnit = QUnit; 482 | } else { 483 | extend(exports, QUnit); 484 | exports.QUnit = QUnit; 485 | } 486 | 487 | // define these after exposing globals to keep them in these QUnit namespace only 488 | extend(QUnit, { 489 | config: config, 490 | 491 | // Initialize the configuration options 492 | init: function() { 493 | extend(config, { 494 | stats: { all: 0, bad: 0 }, 495 | moduleStats: { all: 0, bad: 0 }, 496 | started: +new Date, 497 | updateRate: 1000, 498 | blocking: false, 499 | autostart: true, 500 | autorun: false, 501 | filter: "", 502 | queue: [], 503 | semaphore: 0 504 | }); 505 | 506 | var tests = id( "qunit-tests" ), 507 | banner = id( "qunit-banner" ), 508 | result = id( "qunit-testresult" ); 509 | 510 | if ( tests ) { 511 | tests.innerHTML = ""; 512 | } 513 | 514 | if ( banner ) { 515 | banner.className = ""; 516 | } 517 | 518 | if ( result ) { 519 | result.parentNode.removeChild( result ); 520 | } 521 | 522 | if ( tests ) { 523 | result = document.createElement( "p" ); 524 | result.id = "qunit-testresult"; 525 | result.className = "result"; 526 | tests.parentNode.insertBefore( result, tests ); 527 | result.innerHTML = 'Running...
     '; 528 | } 529 | }, 530 | 531 | /** 532 | * Resets the test setup. Useful for tests that modify the DOM. 533 | * 534 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 535 | */ 536 | reset: function() { 537 | if ( window.jQuery ) { 538 | jQuery( "#main, #qunit-fixture" ).html( config.fixture ); 539 | } else { 540 | var main = id( 'main' ) || id( 'qunit-fixture' ); 541 | if ( main ) { 542 | main.innerHTML = config.fixture; 543 | } 544 | } 545 | }, 546 | 547 | /** 548 | * Trigger an event on an element. 549 | * 550 | * @example triggerEvent( document.body, "click" ); 551 | * 552 | * @param DOMElement elem 553 | * @param String type 554 | */ 555 | triggerEvent: function( elem, type, event ) { 556 | if ( document.createEvent ) { 557 | event = document.createEvent("MouseEvents"); 558 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 559 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 560 | elem.dispatchEvent( event ); 561 | 562 | } else if ( elem.fireEvent ) { 563 | elem.fireEvent("on"+type); 564 | } 565 | }, 566 | 567 | // Safe object type checking 568 | is: function( type, obj ) { 569 | return QUnit.objectType( obj ) == type; 570 | }, 571 | 572 | objectType: function( obj ) { 573 | if (typeof obj === "undefined") { 574 | return "undefined"; 575 | 576 | // consider: typeof null === object 577 | } 578 | if (obj === null) { 579 | return "null"; 580 | } 581 | 582 | var type = Object.prototype.toString.call( obj ) 583 | .match(/^\[object\s(.*)\]$/)[1] || ''; 584 | 585 | switch (type) { 586 | case 'Number': 587 | if (isNaN(obj)) { 588 | return "nan"; 589 | } else { 590 | return "number"; 591 | } 592 | case 'String': 593 | case 'Boolean': 594 | case 'Array': 595 | case 'Date': 596 | case 'RegExp': 597 | case 'Function': 598 | return type.toLowerCase(); 599 | } 600 | if (typeof obj === "object") { 601 | return "object"; 602 | } 603 | return undefined; 604 | }, 605 | 606 | push: function(result, actual, expected, message) { 607 | var details = { 608 | result: result, 609 | message: message, 610 | actual: actual, 611 | expected: expected 612 | }; 613 | 614 | message = escapeHtml(message) || (result ? "okay" : "failed"); 615 | message = '' + message + ""; 616 | expected = escapeHtml(QUnit.jsDump.parse(expected)); 617 | actual = escapeHtml(QUnit.jsDump.parse(actual)); 618 | var output = message + ''; 619 | if (actual != expected) { 620 | output += ''; 621 | output += ''; 622 | } 623 | if (!result) { 624 | var source = sourceFromStacktrace(); 625 | if (source) { 626 | details.source = source; 627 | output += ''; 628 | } 629 | } 630 | output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + source +'
    "; 631 | 632 | QUnit.log(details); 633 | 634 | config.current.assertions.push({ 635 | result: !!result, 636 | message: output 637 | }); 638 | }, 639 | 640 | // Logging callbacks; all receive a single argument with the listed properties 641 | // run test/logs.html for any related changes 642 | begin: function() {}, 643 | // done: { failed, passed, total, runtime } 644 | done: function() {}, 645 | // log: { result, actual, expected, message } 646 | log: function() {}, 647 | // testStart: { name } 648 | testStart: function() {}, 649 | // testDone: { name, failed, passed, total } 650 | testDone: function() {}, 651 | // moduleStart: { name } 652 | moduleStart: function() {}, 653 | // moduleDone: { name, failed, passed, total } 654 | moduleDone: function() {} 655 | }); 656 | 657 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 658 | config.autorun = true; 659 | } 660 | 661 | addEvent(window, "load", function() { 662 | QUnit.begin({}); 663 | 664 | // Initialize the config, saving the execution queue 665 | var oldconfig = extend({}, config); 666 | QUnit.init(); 667 | extend(config, oldconfig); 668 | 669 | config.blocking = false; 670 | 671 | var userAgent = id("qunit-userAgent"); 672 | if ( userAgent ) { 673 | userAgent.innerHTML = navigator.userAgent; 674 | } 675 | var banner = id("qunit-header"); 676 | if ( banner ) { 677 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + 678 | '' + 679 | ''; 680 | addEvent( banner, "change", function( event ) { 681 | var params = {}; 682 | params[ event.target.name ] = event.target.checked ? true : undefined; 683 | window.location = QUnit.url( params ); 684 | }); 685 | } 686 | 687 | var toolbar = id("qunit-testrunner-toolbar"); 688 | if ( toolbar ) { 689 | var filter = document.createElement("input"); 690 | filter.type = "checkbox"; 691 | filter.id = "qunit-filter-pass"; 692 | addEvent( filter, "click", function() { 693 | var ol = document.getElementById("qunit-tests"); 694 | if ( filter.checked ) { 695 | ol.className = ol.className + " hidepass"; 696 | } else { 697 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 698 | ol.className = tmp.replace(/ hidepass /, " "); 699 | } 700 | if ( defined.sessionStorage ) { 701 | sessionStorage.setItem("qunit-filter-passed-tests", filter.checked ? "true" : ""); 702 | } 703 | }); 704 | if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 705 | filter.checked = true; 706 | var ol = document.getElementById("qunit-tests"); 707 | ol.className = ol.className + " hidepass"; 708 | } 709 | toolbar.appendChild( filter ); 710 | 711 | var label = document.createElement("label"); 712 | label.setAttribute("for", "qunit-filter-pass"); 713 | label.innerHTML = "Hide passed tests"; 714 | toolbar.appendChild( label ); 715 | } 716 | 717 | var main = id('main') || id('qunit-fixture'); 718 | if ( main ) { 719 | config.fixture = main.innerHTML; 720 | } 721 | 722 | if (config.autostart) { 723 | QUnit.start(); 724 | } 725 | }); 726 | 727 | function done() { 728 | config.autorun = true; 729 | 730 | // Log the last module results 731 | if ( config.currentModule ) { 732 | QUnit.moduleDone( { 733 | name: config.currentModule, 734 | failed: config.moduleStats.bad, 735 | passed: config.moduleStats.all - config.moduleStats.bad, 736 | total: config.moduleStats.all 737 | } ); 738 | } 739 | 740 | var banner = id("qunit-banner"), 741 | tests = id("qunit-tests"), 742 | runtime = +new Date - config.started, 743 | passed = config.stats.all - config.stats.bad, 744 | html = [ 745 | 'Tests completed in ', 746 | runtime, 747 | ' milliseconds.
    ', 748 | '', 749 | passed, 750 | ' tests of ', 751 | config.stats.all, 752 | ' passed, ', 753 | config.stats.bad, 754 | ' failed.' 755 | ].join(''); 756 | 757 | if ( banner ) { 758 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 759 | } 760 | 761 | if ( tests ) { 762 | id( "qunit-testresult" ).innerHTML = html; 763 | } 764 | 765 | QUnit.done( { 766 | failed: config.stats.bad, 767 | passed: passed, 768 | total: config.stats.all, 769 | runtime: runtime 770 | } ); 771 | } 772 | 773 | function validTest( name ) { 774 | var filter = config.filter, 775 | run = false; 776 | 777 | if ( !filter ) { 778 | return true; 779 | } 780 | 781 | not = filter.charAt( 0 ) === "!"; 782 | if ( not ) { 783 | filter = filter.slice( 1 ); 784 | } 785 | 786 | if ( name.indexOf( filter ) !== -1 ) { 787 | return !not; 788 | } 789 | 790 | if ( not ) { 791 | run = true; 792 | } 793 | 794 | return run; 795 | } 796 | 797 | // so far supports only Firefox, Chrome and Opera (buggy) 798 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 799 | function sourceFromStacktrace() { 800 | try { 801 | throw new Error(); 802 | } catch ( e ) { 803 | if (e.stacktrace) { 804 | // Opera 805 | return e.stacktrace.split("\n")[6]; 806 | } else if (e.stack) { 807 | // Firefox, Chrome 808 | return e.stack.split("\n")[4]; 809 | } 810 | } 811 | } 812 | 813 | function escapeHtml(s) { 814 | if (!s) { 815 | return ""; 816 | } 817 | s = s + ""; 818 | return s.replace(/[\&"<>\\]/g, function(s) { 819 | switch(s) { 820 | case "&": return "&"; 821 | case "\\": return "\\\\"; 822 | case '"': return '\"'; 823 | case "<": return "<"; 824 | case ">": return ">"; 825 | default: return s; 826 | } 827 | }); 828 | } 829 | 830 | function synchronize( callback ) { 831 | config.queue.push( callback ); 832 | 833 | if ( config.autorun && !config.blocking ) { 834 | process(); 835 | } 836 | } 837 | 838 | function process() { 839 | var start = (new Date()).getTime(); 840 | 841 | while ( config.queue.length && !config.blocking ) { 842 | if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { 843 | config.queue.shift()(); 844 | } else { 845 | window.setTimeout( process, 13 ); 846 | break; 847 | } 848 | } 849 | if (!config.blocking && !config.queue.length) { 850 | done(); 851 | } 852 | } 853 | 854 | function saveGlobal() { 855 | config.pollution = []; 856 | 857 | if ( config.noglobals ) { 858 | for ( var key in window ) { 859 | config.pollution.push( key ); 860 | } 861 | } 862 | } 863 | 864 | function checkPollution( name ) { 865 | var old = config.pollution; 866 | saveGlobal(); 867 | 868 | var newGlobals = diff( old, config.pollution ); 869 | if ( newGlobals.length > 0 ) { 870 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 871 | config.current.expected++; 872 | } 873 | 874 | var deletedGlobals = diff( config.pollution, old ); 875 | if ( deletedGlobals.length > 0 ) { 876 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 877 | config.current.expected++; 878 | } 879 | } 880 | 881 | // returns a new Array with the elements that are in a but not in b 882 | function diff( a, b ) { 883 | var result = a.slice(); 884 | for ( var i = 0; i < result.length; i++ ) { 885 | for ( var j = 0; j < b.length; j++ ) { 886 | if ( result[i] === b[j] ) { 887 | result.splice(i, 1); 888 | i--; 889 | break; 890 | } 891 | } 892 | } 893 | return result; 894 | } 895 | 896 | function fail(message, exception, callback) { 897 | if ( typeof console !== "undefined" && console.error && console.warn ) { 898 | console.error(message); 899 | console.error(exception); 900 | console.warn(callback.toString()); 901 | 902 | } else if ( window.opera && opera.postError ) { 903 | opera.postError(message, exception, callback.toString); 904 | } 905 | } 906 | 907 | function extend(a, b) { 908 | for ( var prop in b ) { 909 | if ( b[prop] === undefined ) { 910 | delete a[prop]; 911 | } else { 912 | a[prop] = b[prop]; 913 | } 914 | } 915 | 916 | return a; 917 | } 918 | 919 | function addEvent(elem, type, fn) { 920 | if ( elem.addEventListener ) { 921 | elem.addEventListener( type, fn, false ); 922 | } else if ( elem.attachEvent ) { 923 | elem.attachEvent( "on" + type, fn ); 924 | } else { 925 | fn(); 926 | } 927 | } 928 | 929 | function id(name) { 930 | return !!(typeof document !== "undefined" && document && document.getElementById) && 931 | document.getElementById( name ); 932 | } 933 | 934 | // Test for equality any JavaScript type. 935 | // Discussions and reference: http://philrathe.com/articles/equiv 936 | // Test suites: http://philrathe.com/tests/equiv 937 | // Author: Philippe Rathé 938 | QUnit.equiv = function () { 939 | 940 | var innerEquiv; // the real equiv function 941 | var callers = []; // stack to decide between skip/abort functions 942 | var parents = []; // stack to avoiding loops from circular referencing 943 | 944 | // Call the o related callback with the given arguments. 945 | function bindCallbacks(o, callbacks, args) { 946 | var prop = QUnit.objectType(o); 947 | if (prop) { 948 | if (QUnit.objectType(callbacks[prop]) === "function") { 949 | return callbacks[prop].apply(callbacks, args); 950 | } else { 951 | return callbacks[prop]; // or undefined 952 | } 953 | } 954 | } 955 | 956 | var callbacks = function () { 957 | 958 | // for string, boolean, number and null 959 | function useStrictEquality(b, a) { 960 | if (b instanceof a.constructor || a instanceof b.constructor) { 961 | // to catch short annotaion VS 'new' annotation of a declaration 962 | // e.g. var i = 1; 963 | // var j = new Number(1); 964 | return a == b; 965 | } else { 966 | return a === b; 967 | } 968 | } 969 | 970 | return { 971 | "string": useStrictEquality, 972 | "boolean": useStrictEquality, 973 | "number": useStrictEquality, 974 | "null": useStrictEquality, 975 | "undefined": useStrictEquality, 976 | 977 | "nan": function (b) { 978 | return isNaN(b); 979 | }, 980 | 981 | "date": function (b, a) { 982 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 983 | }, 984 | 985 | "regexp": function (b, a) { 986 | return QUnit.objectType(b) === "regexp" && 987 | a.source === b.source && // the regex itself 988 | a.global === b.global && // and its modifers (gmi) ... 989 | a.ignoreCase === b.ignoreCase && 990 | a.multiline === b.multiline; 991 | }, 992 | 993 | // - skip when the property is a method of an instance (OOP) 994 | // - abort otherwise, 995 | // initial === would have catch identical references anyway 996 | "function": function () { 997 | var caller = callers[callers.length - 1]; 998 | return caller !== Object && 999 | typeof caller !== "undefined"; 1000 | }, 1001 | 1002 | "array": function (b, a) { 1003 | var i, j, loop; 1004 | var len; 1005 | 1006 | // b could be an object literal here 1007 | if ( ! (QUnit.objectType(b) === "array")) { 1008 | return false; 1009 | } 1010 | 1011 | len = a.length; 1012 | if (len !== b.length) { // safe and faster 1013 | return false; 1014 | } 1015 | 1016 | //track reference to avoid circular references 1017 | parents.push(a); 1018 | for (i = 0; i < len; i++) { 1019 | loop = false; 1020 | for(j=0;j= 0) { 1165 | type = "array"; 1166 | } else { 1167 | type = typeof obj; 1168 | } 1169 | return type; 1170 | }, 1171 | separator:function() { 1172 | return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; 1173 | }, 1174 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1175 | if ( !this.multiline ) 1176 | return ''; 1177 | var chr = this.indentChar; 1178 | if ( this.HTML ) 1179 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1180 | return Array( this._depth_ + (extra||0) ).join(chr); 1181 | }, 1182 | up:function( a ) { 1183 | this._depth_ += a || 1; 1184 | }, 1185 | down:function( a ) { 1186 | this._depth_ -= a || 1; 1187 | }, 1188 | setParser:function( name, parser ) { 1189 | this.parsers[name] = parser; 1190 | }, 1191 | // The next 3 are exposed so you can use them 1192 | quote:quote, 1193 | literal:literal, 1194 | join:join, 1195 | // 1196 | _depth_: 1, 1197 | // This is the list of parsers, to modify them, use jsDump.setParser 1198 | parsers:{ 1199 | window: '[Window]', 1200 | document: '[Document]', 1201 | error:'[ERROR]', //when no parser is found, shouldn't happen 1202 | unknown: '[Unknown]', 1203 | 'null':'null', 1204 | 'undefined':'undefined', 1205 | 'function':function( fn ) { 1206 | var ret = 'function', 1207 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1208 | if ( name ) 1209 | ret += ' ' + name; 1210 | ret += '('; 1211 | 1212 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1213 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1214 | }, 1215 | array: array, 1216 | nodelist: array, 1217 | arguments: array, 1218 | object:function( map ) { 1219 | var ret = [ ]; 1220 | QUnit.jsDump.up(); 1221 | for ( var key in map ) 1222 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); 1223 | QUnit.jsDump.down(); 1224 | return join( '{', ret, '}' ); 1225 | }, 1226 | node:function( node ) { 1227 | var open = QUnit.jsDump.HTML ? '<' : '<', 1228 | close = QUnit.jsDump.HTML ? '>' : '>'; 1229 | 1230 | var tag = node.nodeName.toLowerCase(), 1231 | ret = open + tag; 1232 | 1233 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1234 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1235 | if ( val ) 1236 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1237 | } 1238 | return ret + close + open + '/' + tag + close; 1239 | }, 1240 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1241 | var l = fn.length; 1242 | if ( !l ) return ''; 1243 | 1244 | var args = Array(l); 1245 | while ( l-- ) 1246 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1247 | return ' ' + args.join(', ') + ' '; 1248 | }, 1249 | key:quote, //object calls it internally, the key part of an item in a map 1250 | functionCode:'[code]', //function calls it internally, it's the content of the function 1251 | attribute:quote, //node calls it internally, it's an html attribute value 1252 | string:quote, 1253 | date:quote, 1254 | regexp:literal, //regex 1255 | number:literal, 1256 | 'boolean':literal 1257 | }, 1258 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1259 | id:'id', 1260 | name:'name', 1261 | 'class':'className' 1262 | }, 1263 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1264 | indentChar:' ',//indentation unit 1265 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1266 | }; 1267 | 1268 | return jsDump; 1269 | })(); 1270 | 1271 | // from Sizzle.js 1272 | function getText( elems ) { 1273 | var ret = "", elem; 1274 | 1275 | for ( var i = 0; elems[i]; i++ ) { 1276 | elem = elems[i]; 1277 | 1278 | // Get the text from text nodes and CDATA nodes 1279 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1280 | ret += elem.nodeValue; 1281 | 1282 | // Traverse everything else, except comment nodes 1283 | } else if ( elem.nodeType !== 8 ) { 1284 | ret += getText( elem.childNodes ); 1285 | } 1286 | } 1287 | 1288 | return ret; 1289 | }; 1290 | 1291 | /* 1292 | * Javascript Diff Algorithm 1293 | * By John Resig (http://ejohn.org/) 1294 | * Modified by Chu Alan "sprite" 1295 | * 1296 | * Released under the MIT license. 1297 | * 1298 | * More Info: 1299 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1300 | * 1301 | * Usage: QUnit.diff(expected, actual) 1302 | * 1303 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1304 | */ 1305 | QUnit.diff = (function() { 1306 | function diff(o, n){ 1307 | var ns = new Object(); 1308 | var os = new Object(); 1309 | 1310 | for (var i = 0; i < n.length; i++) { 1311 | if (ns[n[i]] == null) 1312 | ns[n[i]] = { 1313 | rows: new Array(), 1314 | o: null 1315 | }; 1316 | ns[n[i]].rows.push(i); 1317 | } 1318 | 1319 | for (var i = 0; i < o.length; i++) { 1320 | if (os[o[i]] == null) 1321 | os[o[i]] = { 1322 | rows: new Array(), 1323 | n: null 1324 | }; 1325 | os[o[i]].rows.push(i); 1326 | } 1327 | 1328 | for (var i in ns) { 1329 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1330 | n[ns[i].rows[0]] = { 1331 | text: n[ns[i].rows[0]], 1332 | row: os[i].rows[0] 1333 | }; 1334 | o[os[i].rows[0]] = { 1335 | text: o[os[i].rows[0]], 1336 | row: ns[i].rows[0] 1337 | }; 1338 | } 1339 | } 1340 | 1341 | for (var i = 0; i < n.length - 1; i++) { 1342 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1343 | n[i + 1] == o[n[i].row + 1]) { 1344 | n[i + 1] = { 1345 | text: n[i + 1], 1346 | row: n[i].row + 1 1347 | }; 1348 | o[n[i].row + 1] = { 1349 | text: o[n[i].row + 1], 1350 | row: i + 1 1351 | }; 1352 | } 1353 | } 1354 | 1355 | for (var i = n.length - 1; i > 0; i--) { 1356 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1357 | n[i - 1] == o[n[i].row - 1]) { 1358 | n[i - 1] = { 1359 | text: n[i - 1], 1360 | row: n[i].row - 1 1361 | }; 1362 | o[n[i].row - 1] = { 1363 | text: o[n[i].row - 1], 1364 | row: i - 1 1365 | }; 1366 | } 1367 | } 1368 | 1369 | return { 1370 | o: o, 1371 | n: n 1372 | }; 1373 | } 1374 | 1375 | return function(o, n){ 1376 | o = o.replace(/\s+$/, ''); 1377 | n = n.replace(/\s+$/, ''); 1378 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1379 | 1380 | var str = ""; 1381 | 1382 | var oSpace = o.match(/\s+/g); 1383 | if (oSpace == null) { 1384 | oSpace = [" "]; 1385 | } 1386 | else { 1387 | oSpace.push(" "); 1388 | } 1389 | var nSpace = n.match(/\s+/g); 1390 | if (nSpace == null) { 1391 | nSpace = [" "]; 1392 | } 1393 | else { 1394 | nSpace.push(" "); 1395 | } 1396 | 1397 | if (out.n.length == 0) { 1398 | for (var i = 0; i < out.o.length; i++) { 1399 | str += '' + out.o[i] + oSpace[i] + ""; 1400 | } 1401 | } 1402 | else { 1403 | if (out.n[0].text == null) { 1404 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1405 | str += '' + out.o[n] + oSpace[n] + ""; 1406 | } 1407 | } 1408 | 1409 | for (var i = 0; i < out.n.length; i++) { 1410 | if (out.n[i].text == null) { 1411 | str += '' + out.n[i] + nSpace[i] + ""; 1412 | } 1413 | else { 1414 | var pre = ""; 1415 | 1416 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1417 | pre += '' + out.o[n] + oSpace[n] + ""; 1418 | } 1419 | str += " " + out.n[i].text + nSpace[i] + pre; 1420 | } 1421 | } 1422 | } 1423 | 1424 | return str; 1425 | }; 1426 | })(); 1427 | 1428 | })(this); 1429 | --------------------------------------------------------------------------------