├── .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 | Next
44 | Previous
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 | Previous
48 | Next
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 |
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 | Next
53 | Previous
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 + 'Expected: ' + expected + ' ';
619 | if (actual != expected) {
620 | output += 'Result: ' + actual + ' ';
621 | output += 'Diff: ' + QUnit.diff(expected, actual) +' ';
622 | }
623 | if (!result) {
624 | var source = sourceFromStacktrace();
625 | if (source) {
626 | details.source = source;
627 | output += 'Source: ' + source +' ';
628 | }
629 | }
630 | output += "
";
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 | ' noglobals ' +
679 | ' notrycatch ';
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 |
--------------------------------------------------------------------------------