├── src ├── img │ └── ajax-loader-dark.gif ├── test │ ├── bdd │ │ ├── main.js │ │ ├── vendor │ │ │ ├── jquery.simulate.ext.js │ │ │ ├── jquery.simulate.js │ │ │ ├── jquery.simulate.key-sequence.js │ │ │ └── bililiteRange.js │ │ ├── consulting-tasks.js │ │ ├── adding-tasks.js │ │ ├── page-objects.js │ │ └── doing-tasks.js │ └── unit │ │ ├── field-tests.js │ │ ├── app-widget-tests.js │ │ ├── event-tests.js │ │ ├── create-task-controller-tests.js │ │ ├── task-tests.js │ │ ├── app-controller-tests.js │ │ ├── task-list-tests.js │ │ ├── create-task-widget-tests.js │ │ ├── test-doubles.js │ │ └── task-widget-tests.js ├── lib │ ├── knockout │ │ ├── main.js │ │ └── viewmodels.js │ ├── zepto_jquery │ │ ├── main.js │ │ └── viewmodels.js │ └── core │ │ ├── controller.js │ │ ├── model.js │ │ ├── utils.js │ │ ├── store.js │ │ └── widgets.js ├── css │ ├── meyer_reset.css │ └── todo.css ├── todo_with_knockout.html ├── initial_design.html ├── todo_with_zepto.html └── todo_with_jquery.html ├── .gitignore ├── .travis.yml ├── MIT-LICENSE ├── package.json ├── karma.conf.js ├── initial_design.html ├── README.md ├── Gruntfile.js └── vendor ├── almond.js └── zepto-1.0.min.js /src/img/ajax-loader-dark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eamodeorubio/explore-the-todo-list-app/HEAD/src/img/ajax-loader-dark.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | notes.txt 2 | js/todo*.min.js 3 | *.xml 4 | coverage.html 5 | node_modules/ 6 | .idea/ 7 | dist/* 8 | .project 9 | .classpath -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_install: 5 | - npm install -g grunt-cli 6 | - export DISPLAY=:99.0 7 | - sh -e /etc/init.d/xvfb start 8 | notifications: 9 | email: 10 | - eamodeorubio@gmail.com -------------------------------------------------------------------------------- /src/test/bdd/main.js: -------------------------------------------------------------------------------- 1 | ["knockout", "zepto", "jquery"].forEach(function (tech) { 2 | var expect = chai.expect; 3 | 4 | function newUI() { 5 | return new bdd.UI(tech, $); 6 | } 7 | 8 | bdd.consultingTasks(newUI, expect); 9 | bdd.addingTasks(newUI, expect); 10 | bdd.doingTasks(newUI, expect); 11 | }); -------------------------------------------------------------------------------- /src/lib/knockout/main.js: -------------------------------------------------------------------------------- 1 | // Assemble MVC and start application 2 | "use strict"; 3 | 4 | var log = console.log.bind(console), 5 | LocalStorage = require('../core/store').LocalStorage, 6 | utils = require('../core/utils')(log), 7 | Event = utils.Event, 8 | AppViewModel = require('./viewmodels')(window.ko, Event), 9 | Tasks = require('../core/model').Tasks, 10 | AppWidget = require('../core/widgets')(Event).AppWidget, 11 | AppController = require('../core/controller').AppController, 12 | app = new AppController( 13 | new Tasks(new LocalStorage()), 14 | new AppWidget(new AppViewModel()) 15 | ); 16 | 17 | app.start(); -------------------------------------------------------------------------------- /src/lib/zepto_jquery/main.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | "use strict"; 3 | var log = console.log.bind(console), 4 | LocalStorage = require('../core/store').LocalStorage, 5 | utils = require('../core/utils')(log), 6 | Event = utils.Event, 7 | AppViewModel = require('./viewmodels')($, Event, utils.field), 8 | Tasks = require('../core/model').Tasks, 9 | AppWidget = require('../core/widgets')(Event).AppWidget, 10 | AppController = require('../core/controller').AppController, 11 | app = new AppController( 12 | new Tasks(new LocalStorage()), 13 | new AppWidget(new AppViewModel({ 14 | 'tasksList': $('.task-list-widget .task-list .task'), 15 | 'newTaskDescription': $('.add-task-widget .txt'), 16 | 'addTaskBtn': $('.add-task-widget .btn') 17 | })) 18 | ); 19 | app.start(); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 Enrique Amodeo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "explore-the-todo-list-app", 3 | "description": "A demo application showing practices like TDD/BDD, CI and hexagonal architecture", 4 | "author": { 5 | "name": "Enrique Amodeo", 6 | "email": "eamodeorubio@gmail.com", 7 | "url": "http://eamodeorubio.wordpress.com" 8 | }, 9 | "version": "0.4.0", 10 | "scripts": { 11 | "test": "grunt build", 12 | "watch": "grunt", 13 | "dist": "grunt dist", 14 | "unit-test": "grunt dev" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:eamodeorubio/explore-the-todo-list-app.git" 19 | }, 20 | "engines": { 21 | "node": ">=0.10.0" 22 | }, 23 | "devDependencies": { 24 | "grunt": "~0.4.1", 25 | "grunt-contrib-jshint": "~0.4.3", 26 | "grunt-contrib-csslint": "~0.1.2", 27 | "grunt-contrib-watch": "~0.3.1", 28 | "grunt-contrib-copy": "~0.4.1", 29 | "grunt-contrib-clean": "~0.4.0", 30 | "grunt-contrib-requirejs": "~0.4.0", 31 | "grunt-contrib-cssmin": "~0.6.0", 32 | "grunt-simple-mocha": "~0.4.0", 33 | "grunt-karma": "~0.4.3", 34 | "chai": "~1.5.0", 35 | "sinon": "~1.6.0", 36 | "sinon-chai": "~2.3.1", 37 | "mocha-specxunitcov-reporter": "0.0.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | //logLevel = LOG_DEBUG; 2 | var BASE_DIR = 'src/test/bdd/'; 3 | files = [ 4 | MOCHA, 5 | MOCHA_ADAPTER, 6 | {pattern: BASE_DIR + "vendor/jquery-1.9.1.min.js", watched: false, included: true, served: true}, 7 | {pattern: BASE_DIR + "vendor/bililiteRange.js", watched: false, included: true, served: true}, 8 | {pattern: BASE_DIR + "vendor/jquery.simulate.js", watched: false, included: true, served: true}, 9 | {pattern: BASE_DIR + "vendor/jquery.simulate.ext.js", watched: false, included: true, served: true}, 10 | {pattern: BASE_DIR + "vendor/jquery.simulate.key-sequence.js", watched: false, included: true, served: true}, 11 | {pattern: 'node_modules/chai/chai.js', watched: false, included: true, served: true}, 12 | {pattern: BASE_DIR + 'consulting-tasks.js', watched: false, included: true, served: true}, 13 | {pattern: BASE_DIR + 'adding-tasks.js', watched: false, included: true, served: true}, 14 | {pattern: BASE_DIR + 'doing-tasks.js', watched: false, included: true, served: true}, 15 | {pattern: BASE_DIR + 'page-objects.js', watched: false, included: true, served: true}, 16 | {pattern: BASE_DIR + 'main.js', watched: false, included: true, served: true}, 17 | {pattern: 'dist/**', watched: false, included: false, served: true} 18 | ]; -------------------------------------------------------------------------------- /src/css/meyer_reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /src/test/bdd/vendor/jquery.simulate.ext.js: -------------------------------------------------------------------------------- 1 | /*jshint camelcase:true, plusplus:true, forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, browser:true, devel:true, maxerr:100, white:false, onevar:false */ 2 | /*jslint white: true vars: true browser: true todo: true */ 3 | /*global jQuery:true $:true */ 4 | 5 | /* jQuery Simulate Extended Plugin 1.1.4 6 | * http://github.com/j-ulrich/jquery-simulate-ext 7 | * 8 | * Copyright (c) 2013 Jochen Ulrich 9 | * Licensed under the MIT license (MIT-LICENSE.txt). 10 | */ 11 | 12 | ;(function( $ ) { 13 | "use strict"; 14 | 15 | /* Overwrite the $.simulate.prototype.mouseEvent function 16 | * to convert pageX/Y to clientX/Y 17 | */ 18 | var originalMouseEvent = $.simulate.prototype.mouseEvent, 19 | rdocument = /\[object (?:HTML)?Document\]/; 20 | 21 | $.simulate.prototype.mouseEvent = function(type, options) { 22 | if (options.pageX || options.pageY) { 23 | var doc = rdocument.test(Object.prototype.toString.call(this.target))? this.target : (this.target.ownerDocument || document); 24 | options.clientX = (options.pageX || 0) - $(doc).scrollLeft(); 25 | options.clientY = (options.pageY || 0) - $(doc).scrollTop(); 26 | } 27 | return originalMouseEvent.apply(this, [type, options]); 28 | }; 29 | 30 | 31 | })( jQuery ); 32 | -------------------------------------------------------------------------------- /src/lib/core/controller.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A simple controller layer for todo list application 3 | * Binds the "active record" models to the application level widgets 4 | */ 5 | "use strict"; 6 | 7 | function createAndBindWidgetForTask(task, taskListWidget) { 8 | var taskWidget = taskListWidget.newWidgetForTask(task.toDTO()); 9 | taskWidget.onToggleDoneRequest(function (newDone) { 10 | task.done(newDone); 11 | task.save(function (task) { 12 | taskWidget.done(task.done()); 13 | }); 14 | }); 15 | return taskWidget; 16 | } 17 | 18 | module.exports = { 19 | createAndBindWidgetForTask: createAndBindWidgetForTask, 20 | AppController: function (taskList, taskListWidget, taskWidgetFactory) { 21 | taskWidgetFactory = taskWidgetFactory || createAndBindWidgetForTask; 22 | 23 | // Public 24 | this.start = function (callback) { 25 | taskListWidget.onNewTaskRequest(function (description) { 26 | var task, taskWidget; 27 | task = taskList.newTask(description, function () { 28 | taskWidget.working(false); 29 | }); 30 | taskWidget = taskWidgetFactory(task, taskListWidget); 31 | taskWidget.working(true); 32 | }); 33 | taskListWidget.attachToDOM(); 34 | taskList.forEach(function (task) { 35 | taskWidgetFactory(task, taskListWidget); 36 | }, callback); 37 | }; 38 | } 39 | }; -------------------------------------------------------------------------------- /src/todo_with_knockout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TODO list using knockout.js 5 | 6 | 7 | 8 |

Your personal task list

9 |
10 | 12 | 15 |
16 |
17 |

Tasks »

18 | 29 |
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/test/bdd/consulting-tasks.js: -------------------------------------------------------------------------------- 1 | var bdd = bdd || {}; 2 | bdd.consultingTasks = function (newUI, expect) { 3 | "use strict"; 4 | 5 | describe("The todo list apps allows to consult the tasks when the user enters the app", function () { 6 | var ui; 7 | 8 | beforeEach(function () { 9 | ui = newUI(); 10 | }); 11 | 12 | afterEach(function () { 13 | if (ui) { 14 | ui.dispose(); 15 | ui = undefined; 16 | } 17 | }); 18 | 19 | describe("Given the task list is empty", function () { 20 | beforeEach(function (done) { 21 | ui.emptyTheTaskList(done); 22 | }); 23 | 24 | describe("and the page starts", function () { 25 | beforeEach(function (done) { 26 | ui.startApp(done); 27 | }); 28 | 29 | it("it shows no tasks", function () { 30 | expect(ui.displayedTasks().length).to.be.equal(0); 31 | }); 32 | }); 33 | }); 34 | 35 | describe("Given the task list is not empty", function () { 36 | var expectedTasks; 37 | beforeEach(function (done) { 38 | expectedTasks = [ 39 | {text: 'task 1', done: false, inProgress: false}, 40 | {text: 'task 2', done: true, inProgress: false}, 41 | {text: 'task 3', done: false, inProgress: false} 42 | ]; 43 | ui.setupTheTaskList(expectedTasks, done); 44 | }); 45 | 46 | describe("and the page starts", function () { 47 | beforeEach(function (done) { 48 | ui.startApp(done); 49 | }); 50 | 51 | it("it shows the saved tasks", function () { 52 | expect(ui.displayedTasks()).to.be.eql(expectedTasks); 53 | }); 54 | }); 55 | }); 56 | }); 57 | }; -------------------------------------------------------------------------------- /src/lib/core/model.js: -------------------------------------------------------------------------------- 1 | /* 2 | * An classic model layer for todo list application 3 | * The Store is dummy, and really backed in memory, but simulates asynch access to backend 4 | */ 5 | "use strict"; 6 | 7 | function Task(initialData, store) { 8 | var id = initialData.id; 9 | var description = initialData.description; 10 | var done = initialData.done; 11 | this.toDTO = function () { 12 | return { 13 | 'description': description, 14 | 'id': id, 15 | 'done': done 16 | }; 17 | }; 18 | this.done = function (optIsDone) { 19 | if (typeof optIsDone === 'boolean' && optIsDone !== done) 20 | done = !done; 21 | return done; 22 | }; 23 | this.save = function (callback) { 24 | var self = this; 25 | store.save(self.toDTO(), function (dto) { 26 | id = dto.id; 27 | callback(self); 28 | }); 29 | }; 30 | } 31 | 32 | module.exports = { 33 | Tasks: function (store, optTaskFactory) { 34 | var makeTask = optTaskFactory || function (dto) { 35 | return new Task(dto, store); 36 | }; 37 | var allTasks = function (callback) { 38 | store.all(function (dtos) { 39 | callback(dtos.map(makeTask)); 40 | }); 41 | }; 42 | 43 | this.newTask = function (description, callback) { 44 | var task = makeTask({ 45 | description: description, 46 | done: false 47 | }); 48 | task.save(function (task) { 49 | callback(task); 50 | }); 51 | return task; 52 | }; 53 | this.forEach = function (callback, optEndCallback) { 54 | allTasks(function (tasks) { 55 | var numberOfTasks = tasks.length; 56 | for (var i = 0; i < numberOfTasks; i++) 57 | callback(tasks[i]); 58 | if (typeof optEndCallback === 'function') 59 | optEndCallback(); 60 | }); 61 | }; 62 | }, 63 | Task: Task 64 | }; 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/lib/core/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // TODO: fixme -- do this properly using YepNope or similar 4 | if (!Function.prototype.bind) { 5 | Function.prototype.bind = function (oThis) { 6 | if (typeof this !== "function") 7 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); 8 | 9 | var aArgs = Array.prototype.slice.call(arguments, 1), 10 | fToBind = this, 11 | FNOP = function () { 12 | }, 13 | FBound = function () { 14 | return fToBind.apply(this instanceof FNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); 15 | }; 16 | 17 | FNOP.prototype = this.prototype; 18 | FBound.prototype = new FNOP(); 19 | 20 | return FBound; 21 | }; 22 | } 23 | 24 | module.exports = function (log) { 25 | function Event() { 26 | var subscriptors = []; 27 | 28 | this.subscribe = function (subscriptor) { 29 | if (typeof subscriptor !== 'function') 30 | return; 31 | if (subscriptors.indexOf(subscriptor) === -1) 32 | subscriptors.push(subscriptor); 33 | }; 34 | this.publish = function (data) { 35 | subscriptors.forEach(function (subscriptor) { 36 | try { 37 | subscriptor(data); 38 | } catch (err) { 39 | log(err.toString(), err.stack); 40 | } 41 | }); 42 | }; 43 | } 44 | 45 | return { 46 | Event: Event, 47 | field: function (initialValue, optEvent) { 48 | var value = initialValue; 49 | var change = optEvent || new Event(); 50 | var fieldAccessor = function (optNewValue) { 51 | if (typeof optNewValue !== 'undefined') { 52 | var oldValue = value; 53 | value = optNewValue; 54 | if (optNewValue !== oldValue) 55 | change.publish(value); 56 | return fieldAccessor; 57 | } 58 | return value; 59 | }; 60 | fieldAccessor.subscribe = function (subscriptor) { 61 | change.subscribe(subscriptor); 62 | return fieldAccessor; 63 | }; 64 | return fieldAccessor; 65 | } 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /src/test/unit/field-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function nullLog() { 4 | 5 | } 6 | 7 | var newField = require('../../lib/core/utils')(nullLog).field, 8 | test = require('./test-doubles'), 9 | chai = require('chai'), 10 | expect = chai.expect; 11 | 12 | chai.use(require('sinon-chai')); 13 | 14 | describe("A field, initialized with an initial value and a change event", function () { 15 | var aField, initialValue, event; 16 | beforeEach(function () { 17 | initialValue = "field's initial value"; 18 | event = test.doubleFor('event'); 19 | 20 | aField = newField(initialValue, event); 21 | }); 22 | it("when called without parameters, will return its value", function () { 23 | expect(aField()).to.be.equal(initialValue); 24 | }); 25 | describe("when called with a parameter", function () { 26 | it("will change the value of the field", function () { 27 | var newValue = "new field's value"; 28 | 29 | aField(newValue); 30 | 31 | expect(aField()).to.be.equal(newValue); 32 | }); 33 | it("will return itself", function () { 34 | var newValue = "new field's value"; 35 | 36 | var result = aField(newValue); 37 | 38 | expect(result).to.be.equal(aField); 39 | }); 40 | it("will publish the new value", function () { 41 | var newValue = "new field's value"; 42 | 43 | aField(newValue); 44 | 45 | expect(event.publish).to.have.been.calledWith(newValue); 46 | }); 47 | it("won't publish anything if the new value is the same as the old one", function () { 48 | var newValue = initialValue + ""; // a copy 49 | 50 | aField(newValue); 51 | 52 | expect(event.publish).not.to.have.been.called; 53 | }); 54 | }); 55 | describe("has a subscribe method that when called with a callback", function () { 56 | var result, callback; 57 | beforeEach(function () { 58 | result = aField.subscribe(callback); 59 | }); 60 | it("will subscribe the callback into the event", function () { 61 | expect(event.subscribe).to.have.been.calledWith(callback); 62 | }); 63 | it("will return the field itself", function () { 64 | expect(result).to.be.equal(aField); 65 | }); 66 | }); 67 | }); -------------------------------------------------------------------------------- /src/test/unit/app-widget-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var AppWidget = require('../../lib/core/widgets')(null).AppWidget, 3 | test = require('./test-doubles'), 4 | chai = require('chai'), 5 | expect = chai.expect, 6 | sinon = require('sinon'); 7 | 8 | chai.use(require('sinon-chai')); 9 | 10 | describe("An AppWidget, initialized with a view model and a widget factory", function () { 11 | var widget, factory, viewModel, createTaskWidget; 12 | beforeEach(function () { 13 | createTaskWidget = test.doubleFor('createTaskWidget'); 14 | 15 | factory = test.spy('factory', ['newCreateTaskWidget', 'newTaskWidget']); 16 | factory.newCreateTaskWidget.returns(createTaskWidget); 17 | 18 | viewModel = test.doubleFor('appViewModel'); 19 | 20 | widget = new AppWidget(viewModel, factory); 21 | }); 22 | it("will ask the factory to create a CreateTaskWidget", function () { 23 | expect(factory.newCreateTaskWidget).to.have.been.calledWith(viewModel.newTaskDescription, viewModel.addTaskBtn); 24 | }); 25 | it("has a newWidgetForTask method that ask the factory to create a new TaskWidget", function () { 26 | var taskDTO = test.spy('task dto'); 27 | var taskWidget = test.spy('task widget'); 28 | var taskViewModel = test.spy('task view model'); 29 | viewModel.newViewForTask.returns(taskViewModel); 30 | factory.newTaskWidget.returns(taskWidget); 31 | 32 | var result = widget.newWidgetForTask(taskDTO); 33 | 34 | expect(viewModel.newViewForTask).to.have.been.calledWith(taskDTO); 35 | expect(result).to.be.equal(taskWidget); 36 | }); 37 | it("has an onNewTaskRequest that will register a callback into the 'NewTaskRequest' event of createTaskWidget", function () { 38 | var callback = sinon.stub(); 39 | 40 | widget.onNewTaskRequest(callback); 41 | 42 | expect(createTaskWidget.onNewTaskRequest).to.have.been.calledWith(callback); 43 | }); 44 | describe("has an attachToDOM method, that when called", function () { 45 | beforeEach(function () { 46 | widget.attachToDOM(); 47 | }); 48 | it("will call attachToDOM on the view model", function () { 49 | expect(viewModel.attachToDOM).to.have.been.called; 50 | }); 51 | it("will call put focus on the createTaskWidget", function () { 52 | expect(createTaskWidget.focus).to.have.been.calledWith(true); 53 | }); 54 | }); 55 | }); -------------------------------------------------------------------------------- /src/test/unit/event-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function nullLog() { 4 | 5 | } 6 | 7 | var Event = require('../../lib/core/utils')(nullLog).Event, 8 | chai = require('chai'), 9 | expect = chai.expect, 10 | sinon = require('sinon'); 11 | 12 | chai.use(require('sinon-chai')); 13 | 14 | describe("An Event", function () { 15 | var event; 16 | 17 | beforeEach(function () { 18 | event = new Event(); 19 | }); 20 | 21 | describe("given there are registered subscriptors", function () { 22 | var subscriptor1, subscriptor2, subscriptor3; 23 | beforeEach(function () { 24 | subscriptor1 = sinon.stub(); 25 | subscriptor2 = sinon.stub(); 26 | subscriptor3 = sinon.stub(); 27 | 28 | event.subscribe(subscriptor1); 29 | event.subscribe(subscriptor2); 30 | event.subscribe(subscriptor3); 31 | }); 32 | 33 | it("when publish is called with an object, it notifies all the subscriptors about it", function () { 34 | var data = "the data to publish"; 35 | 36 | event.publish(data); 37 | 38 | expect(subscriptor1).to.have.been.calledWith(data); 39 | expect(subscriptor2).to.have.been.calledWith(data); 40 | expect(subscriptor3).to.have.been.calledWith(data); 41 | }); 42 | 43 | it("when publish is called with an object, it notifies all the subscriptors, even if one fails", function () { 44 | var data = "the data to publish"; 45 | subscriptor2.throws("I'm a broken test subscriptor, don't panic if you see this message on console, ;-) !"); 46 | 47 | event.publish(data); 48 | 49 | expect(subscriptor1).to.have.been.calledWith(data); 50 | expect(subscriptor2).to.have.been.calledWith(data); 51 | expect(subscriptor3).to.have.been.calledWith(data); 52 | }); 53 | 54 | it("when publish is called with an object, it notifies all the subscriptors only once, even if they are registered several times", function () { 55 | var data = "the data to publish"; 56 | event.subscribe(subscriptor2); 57 | event.subscribe(subscriptor2); 58 | event.subscribe(subscriptor2); 59 | event.subscribe(subscriptor2); 60 | 61 | event.publish(data); 62 | 63 | expect(subscriptor1).to.have.been.calledExactlyOnce; 64 | expect(subscriptor2).to.have.been.calledExactlyOnce; 65 | expect(subscriptor3).to.have.been.calledExactlyOnce; 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /initial_design.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TODO list as defined by designer 5 | 6 | 7 | 8 | 9 |
10 |

Your personal task list

11 |
12 | 13 | 14 |
15 |
16 |

Tasks »

17 | 59 |
60 |
61 | 62 | -------------------------------------------------------------------------------- /src/lib/knockout/viewmodels.js: -------------------------------------------------------------------------------- 1 | /* 2 | * An knockout.js based ultrathin view layer for todo list application 3 | * The views translates DOM concepts to a plain JS Object in a cross browser fashion. 4 | * This way they decouples the rest of the application from the DOM and the presentation framework used. 5 | * In this sense you can think about them as a "DOM Access Layer". 6 | * In general they implement either a "Passive View" or a "View Model" pattern, 7 | * so they should have no logic. Main responsabilities: 8 | * - Sincronize JavaScript with DOM 9 | * - Capture low level user gestures (DOM events) 10 | * - Present a simple cross browser API to the upper layers 11 | */ 12 | module.exports = function (ko, Event) { 13 | "use strict"; 14 | 15 | function TextFieldViewModel(text, isFocused) { 16 | var keyUp = new Event(); 17 | 18 | this.text = ko.observable(text); 19 | this.onKeyUp = keyUp.subscribe.bind(keyUp); 20 | this.focus = ko.observable(isFocused); 21 | 22 | this.fireKeyUp = function (self, event) { 23 | keyUp.publish(event); 24 | }; 25 | } 26 | 27 | function ButtonViewModel(enabled) { 28 | var click = new Event(); 29 | 30 | this.enabled = ko.observable(enabled); 31 | this.onClick = click.subscribe.bind(click); 32 | 33 | this.fireOnClick = click.publish.bind(click); 34 | } 35 | 36 | function CheckboxViewModel(checked, enabled) { 37 | this.enabled = ko.observable(enabled); 38 | this.checked = ko.observable(checked); 39 | } 40 | 41 | function TaskViewModel(dto, even) { 42 | this.descriptionClicked = new Event(); 43 | 44 | this.even = ko.observable(even); 45 | this.description = ko.observable(dto.description); 46 | 47 | this.done = ko.observable(dto.done); 48 | this.working = ko.observable(false); 49 | this.doneChk = new CheckboxViewModel(dto.done, !this.working()); 50 | this.onDescriptionClicked = this.descriptionClicked.subscribe.bind(this.descriptionClicked); 51 | } 52 | 53 | return function () { 54 | this.taskViews = ko.observableArray([]); 55 | this.newTaskDescription = new TextFieldViewModel('', false); 56 | this.addTaskBtn = new ButtonViewModel(false); 57 | 58 | this.newViewForTask = function (dto) { 59 | var isEven = this.taskViews().length % 2 === 0; 60 | var viewForTask = new TaskViewModel(dto, isEven); 61 | this.taskViews.push(viewForTask); 62 | return viewForTask; 63 | }; 64 | this.attachToDOM = function () { 65 | ko.applyBindings(this); 66 | }; 67 | }; 68 | }; -------------------------------------------------------------------------------- /src/initial_design.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TODO list as defined by designer 5 | 6 | 7 | 8 | 9 |
10 |

Your personal task list

11 |
12 | 13 | 14 |
15 |
16 |

Tasks »

17 | 59 |
60 |
61 | 62 | -------------------------------------------------------------------------------- /src/test/unit/create-task-controller-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var createAndBindWidgetForTask = require('../../lib/core/controller').createAndBindWidgetForTask, 3 | test = require('./test-doubles'), 4 | chai = require('chai'), 5 | expect = chai.expect; 6 | 7 | chai.use(require('sinon-chai')); 8 | 9 | describe("The function createAndBindWidgetForTask, when invoked with a task and a task list widget", function () { 10 | var task, taskListWidget, taskDTO, result, taskWidget; 11 | beforeEach(function () { 12 | taskDTO = test.spy('task dto'); 13 | 14 | task = test.doubleFor('task'); 15 | task.toDTO.returns(taskDTO); 16 | 17 | taskWidget = test.doubleFor('taskWidget'); 18 | taskListWidget = test.doubleFor('appWidget'); 19 | taskListWidget.newWidgetForTask.returns(taskWidget); 20 | 21 | result = createAndBindWidgetForTask(task, taskListWidget); 22 | }); 23 | it("will ask the app widget to create a new widget for the task", function () { 24 | expect(taskListWidget.newWidgetForTask).to.have.been.calledWith(taskDTO); 25 | }); 26 | it("will return the new task widget returned by the app widget", function () { 27 | expect(result).to.be.equal(taskWidget); 28 | }); 29 | describe("will register on the event 'ToggleDoneRequest' of the new task widget", function () { 30 | it("a callback", function () { 31 | expect(taskWidget.onToggleDoneRequest).to.have.been.called; 32 | expect(taskWidget.callbackForLastOnToggleDoneRequestCall()).to.be.a('function'); 33 | }); 34 | describe("that when invoked with the new done state requested by the user", function () { 35 | var newDoneRequested; 36 | beforeEach(function () { 37 | newDoneRequested = true; 38 | taskWidget.callbackForLastOnToggleDoneRequestCall()(newDoneRequested); 39 | }); 40 | it("will modify the task done state with the new done state requested", function () { 41 | expect(task.done).to.have.been.calledWith(newDoneRequested); 42 | }); 43 | describe("and will save the task", function () { 44 | it("registering a callback", function () { 45 | expect(task.save).to.have.been.called; 46 | expect(task.callbackForLastSaveCall()).to.be.a('function'); 47 | }); 48 | it("that when invoked, will update the taskWidget done status", function () { 49 | task.done.returns(newDoneRequested); 50 | 51 | task.callbackForLastSaveCall()(task); 52 | 53 | expect(taskWidget.done).to.have.been.calledWith(newDoneRequested); 54 | }); 55 | }); 56 | }); 57 | }); 58 | }); -------------------------------------------------------------------------------- /src/todo_with_zepto.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TODO list using Zepto 5 | 6 | 7 | 8 |
9 |

Your personal task list

10 |
11 | 12 | 13 |
14 |
15 |

Tasks »

16 | 58 |
59 | 60 | 61 |
62 | 63 | -------------------------------------------------------------------------------- /src/test/bdd/adding-tasks.js: -------------------------------------------------------------------------------- 1 | var bdd = bdd || {}; 2 | bdd.addingTasks = function (newUI, expect) { 3 | "use strict"; 4 | 5 | function describeAddingANewTask(getUI, newTaskDescription) { 6 | var expectedNewTask; 7 | 8 | beforeEach(function () { 9 | expectedNewTask = { 10 | text: newTaskDescription, 11 | done: false 12 | }; 13 | }); 14 | 15 | it("will see that the new task is being created", function () { 16 | expectedNewTask.inProgress = true; 17 | expect(getUI().displayedTasks()).to.be.eql([expectedNewTask]); 18 | }); 19 | 20 | it("will see that the new task is created after a short period of time", function (done) { 21 | expectedNewTask.inProgress = false; 22 | getUI().executeWhen(function () { 23 | var tasks = getUI().displayedTasks(); 24 | return tasks.length === 1 && tasks[0].inProgress === false; 25 | }, function () { 26 | expect(getUI().displayedTasks()).to.be.eql([expectedNewTask]); 27 | done(); 28 | }); 29 | }); 30 | } 31 | 32 | describe("The todo list apps allows to add new tasks", function () { 33 | context("Given the application has been started and there are no tasks yet", function () { 34 | var ui; 35 | 36 | beforeEach(function (done) { 37 | ui = newUI(); 38 | ui.emptyTheTaskList(function (err) { 39 | if (err) 40 | return done(err); 41 | ui.startApp(done); 42 | }); 43 | }); 44 | 45 | afterEach(function () { 46 | if (ui) { 47 | ui.dispose(); 48 | ui = undefined; 49 | } 50 | }); 51 | 52 | it("the app has started", function () { 53 | expect(ui.isStarted()).to.be.ok; 54 | }); 55 | 56 | function getUI() { 57 | return ui; 58 | } 59 | 60 | context("Given the user has specified the new task description as 'Hola'", function () { 61 | var newTaskDescription = 'Hola'; 62 | beforeEach(function (done) { 63 | ui.fillNewTaskDescription(newTaskDescription, done); 64 | }); 65 | describe("When the user request the task to be added using the keyboard", function () { 66 | beforeEach(function (done) { 67 | ui.requestNewTaskUsingKeyboard(done); 68 | }); 69 | 70 | describeAddingANewTask(getUI, newTaskDescription); 71 | }); 72 | describe("When the user request the task to be added without using the keyboard", function () { 73 | beforeEach(function (done) { 74 | ui.requestNewTaskWithoutKeyboard(done); 75 | }); 76 | 77 | describeAddingANewTask(getUI, newTaskDescription); 78 | }); 79 | }); 80 | }); 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /src/todo_with_jquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TODO list using jQuery 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Your personal task list

13 |
14 | 15 | 16 |
17 |
18 |

Tasks »

19 | 61 |
62 | 63 | 64 |
65 | 66 | -------------------------------------------------------------------------------- /src/css/todo.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #354045; 3 | font: normal 100% Cambria, Georgia, serif; 4 | line-height: 1.4em; 5 | width: 60%; 6 | margin: 0 auto; 7 | background: white; 8 | -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; 9 | -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; 10 | -o-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; 11 | box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0; 12 | } 13 | 14 | h1 { 15 | width: 90%; 16 | font-weight: bold; 17 | font-size: 3em; 18 | letter-spacing: 0.15em; 19 | line-height: 1.4em; 20 | text-align: center; 21 | margin: 0 auto; 22 | } 23 | 24 | .btn, .txt { 25 | color: #354045; 26 | } 27 | 28 | .btn:disabled { 29 | color: #d3d3d3; 30 | } 31 | 32 | .add-task-widget { 33 | font-size: 2em; 34 | margin: 0 auto; 35 | width: 90%; 36 | } 37 | 38 | .add-task-widget .txt { 39 | font-size: 1em; 40 | margin: 1.738% 0 1.738% 0; 41 | width: 74%; 42 | height: 70%; 43 | } 44 | 45 | .add-task-widget .btn { 46 | font-size: 1em; 47 | margin: 1.738% 0 1.738% 0; 48 | height: 72%; 49 | width: 23%; 50 | cursor: pointer; 51 | } 52 | 53 | .task-list-widget { 54 | border-top: #bdb76b solid 0.1em; 55 | padding: 0; 56 | margin: 0 auto; 57 | width: 95%; 58 | font-size: 0.75em; 59 | } 60 | 61 | .task-list-widget header { 62 | width: 97%; 63 | margin: 0 auto; 64 | border-bottom: #bdb76b dotted 1px; 65 | } 66 | 67 | .task-list-widget .task-list { 68 | font-size: 1.7em; 69 | line-height: 2em; 70 | width: 97%; 71 | margin: 0 auto; 72 | } 73 | 74 | .task-list-widget .task { 75 | cursor: pointer; 76 | padding-left: 7.5%; 77 | border-bottom: #bdb76b dotted 1px; 78 | } 79 | 80 | .task-list-widget .task .txt { 81 | margin-left: 1%; 82 | } 83 | 84 | .task-list-widget .task img { 85 | display: none; 86 | } 87 | 88 | .task-list-widget .working > img { 89 | display: inline; 90 | } 91 | 92 | .task-list-widget .task .working img { 93 | margin: auto 6px auto -30px; 94 | } 95 | 96 | .task-list-widget .task .chk { 97 | cursor: pointer; 98 | } 99 | 100 | .task-list-widget .todo { 101 | font-weight: bold; 102 | } 103 | 104 | .task-list-widget .todo .working .txt { 105 | font-weight: lighter; 106 | text-decoration: none; 107 | color: gray; 108 | font-style: italic; 109 | } 110 | 111 | .task-list-widget .done .working .txt { 112 | font-weight: lighter; 113 | text-decoration: none; 114 | color: gray; 115 | font-style: normal; 116 | } 117 | 118 | .task-list-widget .done .txt { 119 | color: gray; 120 | font-style: italic; 121 | text-decoration: line-through; 122 | } 123 | 124 | .task-list-widget .even { 125 | background-color: #fff0f0; 126 | } 127 | 128 | .task-list-widget .odd { 129 | background-color: #f5f5f5; 130 | } -------------------------------------------------------------------------------- /src/lib/core/store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The Data Access Layer of the application 3 | * Currently only a naive InMemoryStorage is implemented 4 | * It simulates asynchronous data access 5 | */ 6 | "use strict"; 7 | 8 | var newId = function () { 9 | return 'id' + Math.round(Math.random() * 100000000000000000); 10 | }; 11 | var marshal = function (obj) { 12 | return JSON.stringify(obj); 13 | }; 14 | var unmarshal = function (json) { 15 | try { 16 | if (!json) 17 | return null; 18 | return JSON.parse(json); 19 | } catch (e) { 20 | throw json + ":" + (typeof json) + ":" + e; 21 | } 22 | }; 23 | var scheduleCallback = function (callback, param) { 24 | // Simulate asynchronous processing, perhaps and AJAX request 25 | setTimeout(function () { 26 | if (param) 27 | callback(param); 28 | else 29 | callback(); 30 | }, 0); 31 | }; 32 | 33 | var STORE_ID = 'todo.store'; 34 | 35 | module.exports = { 36 | MemoryStorage: function () { 37 | var objects = [], 38 | objectsById = {}; 39 | var create = function (dto, callback) { 40 | dto.id = newId(); 41 | objectsById[dto.id] = objects.length; 42 | objects.push(marshal(dto)); 43 | scheduleCallback(callback, dto); 44 | }; 45 | this.removeAll = function (callback) { 46 | scheduleCallback(callback); 47 | }; 48 | this.all = function (callback) { 49 | scheduleCallback(callback, objects.map(unmarshal)); 50 | }; 51 | this.save = function (dto, callback) { 52 | var index = objectsById[dto.id]; 53 | if (typeof index !== 'number') 54 | create(dto, callback); 55 | else { 56 | objects[index] = marshal(dto); 57 | scheduleCallback(callback, dto); 58 | } 59 | }; 60 | }, 61 | LocalStorage: function () { 62 | // In a real application we should use WebSQL or IndexedDB or AJAX 63 | var objects = unmarshal(localStorage.getItem(STORE_ID)) || [], 64 | objectsById = {}; 65 | objects.forEach(function (dto, i) { 66 | objectsById[unmarshal(dto).id] = i; 67 | }); 68 | var create = function (dto, callback) { 69 | dto.id = newId(); 70 | objectsById[dto.id] = objects.length; 71 | objects.push(marshal(dto)); 72 | localStorage.setItem(STORE_ID, marshal(objects)); 73 | scheduleCallback(callback, dto); 74 | }; 75 | this.removeAll = function (callback) { 76 | objects = []; 77 | localStorage.removeItem(STORE_ID); 78 | scheduleCallback(callback); 79 | }; 80 | this.all = function (callback) { 81 | scheduleCallback(callback, objects.map(unmarshal)); 82 | }; 83 | this.save = function (dto, callback) { 84 | var index = objectsById[dto.id]; 85 | if (typeof index !== 'number') 86 | create(dto, callback); 87 | else { 88 | objects[index] = marshal(dto); 89 | localStorage.setItem(STORE_ID, marshal(objects)); 90 | scheduleCallback(callback, dto); 91 | } 92 | }; 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /src/test/unit/task-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Task = require('../../lib/core/model').Task, 3 | test = require('./test-doubles'), 4 | chai = require('chai'), 5 | expect = chai.expect, 6 | sinon = require('sinon'); 7 | 8 | chai.use(require('sinon-chai')); 9 | 10 | describe("A Task object, initialized with a store and initial data", function () { 11 | var task, store, initialContents; 12 | beforeEach(function () { 13 | store = test.doubleFor('store'); 14 | initialContents = { 15 | description: 'a task description', 16 | done: false 17 | }; 18 | task = new Task(initialContents, store); 19 | }); 20 | 21 | describe("has a toDTO method, that when invoked, an object is returned", function () { 22 | it("with the same contents as the task", function () { 23 | var dto = task.toDTO(); 24 | 25 | expect(dto).to.exist; 26 | expect(dto.description).to.be.equal(initialContents.description); 27 | expect(dto.done).to.be.equal(initialContents.done); 28 | }); 29 | it("which is a copy of the contents as the task", function () { 30 | var dto1 = task.toDTO(); 31 | dto1.description = "XX"; 32 | dto1.done = true; 33 | var dto = task.toDTO(); 34 | 35 | expect(dto.description).to.be.equal(initialContents.description); 36 | expect(dto.done).to.be.equal(initialContents.done); 37 | }); 38 | }); 39 | 40 | describe("has a done method, that when invoked", function () { 41 | it("without parameters will return if it is done", function () { 42 | expect(task.done()).not.to.be.ok; 43 | }); 44 | describe("with parameters will change the done state of the task", function () { 45 | beforeEach(function () { 46 | task.done(true); 47 | }); 48 | it("so done() will return the new value", function () { 49 | expect(task.done()).to.be.ok; 50 | }); 51 | it("toDTO() will reflect the new done state", function () { 52 | expect(task.toDTO().done).to.be.ok; 53 | }); 54 | }); 55 | }); 56 | 57 | describe("has a save method, that when invoked with a callback", function () { 58 | var callback; 59 | beforeEach(function () { 60 | callback = sinon.stub(); 61 | task.save(callback); 62 | }); 63 | it("will call store.save(dto, storeCallback)", function () { 64 | expect(store.save).to.have.been.called; 65 | expect(store.save.lastCall.args.length).to.be.equal(2); 66 | var dto = store.dataForLastSaveCall(); 67 | expect(dto.description).to.be.equal(initialContents.description); 68 | expect(dto.done).to.be.equal(initialContents.done); 69 | expect(store.callbackForLastSaveCall()).to.be.a('function'); 70 | }); 71 | it("when the store finished saving the task it will assign an id to the task", function () { 72 | var taskId = 'just assigned task id'; 73 | 74 | store.callbackForLastSaveCall()({ 75 | id: taskId 76 | }); 77 | 78 | expect(task.toDTO().id).to.be.equal(taskId); 79 | }); 80 | it("when the store finished saving the callback will be invoked with the task itself", function () { 81 | store.callbackForLastSaveCall()({}); 82 | 83 | expect(callback).to.have.been.calledWith(task); 84 | }); 85 | }); 86 | }); -------------------------------------------------------------------------------- /src/test/bdd/page-objects.js: -------------------------------------------------------------------------------- 1 | var bdd = bdd || {}; 2 | bdd.UI = function (tech, $) { 3 | "use strict"; 4 | var childDOC, 5 | STORE_ID = 'todo.store'; 6 | 7 | this.startApp = function (done) { 8 | $('body').append(''); 9 | $('#fr1').load(function () { 10 | childDOC = this.contentDocument; 11 | // TODO: fixme -- we should find a proper way of waiting, maybe with an started callack/event? 12 | setTimeout(done, 500); // Wait for KO and STORE 13 | }); 14 | $('#fr1').attr('src', "/base/dist/todo_with_" + tech + ".html"); 15 | }; 16 | 17 | this.isStarted = function () { 18 | return !!childDOC; 19 | }; 20 | 21 | this.executeWhen = function (condition, action) { 22 | function check() { 23 | if (condition()) 24 | return action(); 25 | setTimeout(check, 10); 26 | } 27 | 28 | check(); 29 | }; 30 | 31 | this.dispose = function () { 32 | childDOC = null; 33 | $('#fr1').contents().remove(); 34 | $('#fr1').remove(); 35 | }; 36 | 37 | this.emptyTheTaskList = function (done) { 38 | localStorage.removeItem(STORE_ID); 39 | done(); 40 | }; 41 | 42 | this.setupTheTaskList = function (initialTasks, done) { 43 | localStorage.setItem(STORE_ID, JSON.stringify(initialTasks.map(function (task) { 44 | return JSON.stringify({ 45 | id: task.id, 46 | description: task.text, 47 | inProgress: task.inProgress, 48 | done: task.done 49 | }); 50 | }))); 51 | done(); 52 | }; 53 | 54 | this.displayedTasks = function () { 55 | if (!childDOC) 56 | throw 'UI not started!'; 57 | var tasks = []; 58 | $('.task-list > .task', childDOC).each(function () { 59 | var el = $(this), task = {}; 60 | task.text = el.find('.txt').first().text(); 61 | task.done = el.find('.chk').first().prop('checked'); 62 | task.inProgress = el.hasClass('working'); 63 | tasks.push(task); 64 | }); 65 | return tasks; 66 | }; 67 | 68 | this.requestToggleTaskUsingCheck = function (taskIndex, done) { 69 | if (!childDOC) 70 | return done('UI not started!'); 71 | $('.task-list > .task', childDOC).eq(taskIndex).find('.chk').simulate('click', {bubbles: true}); 72 | done(); 73 | }; 74 | 75 | this.requestToggleTaskNotUsingCheck = function (taskIndex, done) { 76 | if (!childDOC) 77 | return done('UI not started!'); 78 | $('.task-list > .task', childDOC).eq(taskIndex).find('.txt').simulate('click', {bubbles: true}); 79 | done(); 80 | }; 81 | 82 | this.fillNewTaskDescription = function (text, done) { 83 | if (!childDOC) 84 | return done('UI not started!'); 85 | $('.add-task-widget > .txt', childDOC).focus().val('').simulate("key-sequence", { 86 | sequence: text, 87 | callback: function () { 88 | // Remove argument! 89 | done(); 90 | } 91 | }); 92 | }; 93 | 94 | this.requestNewTaskUsingKeyboard = function (done) { 95 | if (!childDOC) 96 | return done('UI not started!'); 97 | $('.add-task-widget > .txt', childDOC).focus().simulate("key-sequence", { 98 | sequence: "{enter}", 99 | callback: function () { 100 | // Remove argument! 101 | done(); 102 | } 103 | }); 104 | }; 105 | 106 | this.requestNewTaskWithoutKeyboard = function (done) { 107 | if (!childDOC) 108 | return done('UI not started!'); 109 | $(".add-task-widget > .btn", childDOC).simulate('click', {bubbles: true}); 110 | done(); 111 | }; 112 | }; -------------------------------------------------------------------------------- /src/test/bdd/doing-tasks.js: -------------------------------------------------------------------------------- 1 | var bdd = bdd || {}; 2 | bdd.doingTasks = function (newUI, expect) { 3 | "use strict"; 4 | 5 | function describeChangingATaskTo(getUI, taskIndex, isDone, initialTasks) { 6 | var expectedToggledTask, expectedTasks; 7 | 8 | beforeEach(function () { 9 | expectedTasks = []; 10 | initialTasks().forEach(function (task) { 11 | var copy = {}; 12 | copy.done = task.done; 13 | copy.text = task.text; 14 | copy.inProgress = task.inProgress; 15 | expectedTasks.push(copy); 16 | }); 17 | expectedToggledTask = expectedTasks[taskIndex]; 18 | expectedToggledTask.done = isDone; 19 | }); 20 | 21 | it("will see that the new task is being " + (isDone ? 'done' : 'undone'), function () { 22 | expectedToggledTask.inProgress = true; 23 | expect(getUI().displayedTasks()).to.be.eql(expectedTasks); 24 | }); 25 | 26 | it("will see that the new task is " + (isDone ? 'done' : 'undone') + " after a short period of time", function (done) { 27 | getUI().executeWhen(function () { 28 | var tasks = getUI().displayedTasks(); 29 | return tasks.length === expectedTasks.length && tasks[taskIndex].inProgress === false; 30 | }, function () { 31 | expectedToggledTask.inProgress = false; 32 | expect(getUI().displayedTasks()).to.be.eql(expectedTasks); 33 | done(); 34 | }); 35 | }); 36 | } 37 | 38 | describe("The todo list apps allows to do/undo tasks", function () { 39 | describe("Given the application has been started and there are some tasks", function () { 40 | var ui, initialTasks; 41 | 42 | beforeEach(function (done) { 43 | initialTasks = [ 44 | {text: 'task 1', done: false, inProgress: false}, 45 | {text: 'task 2', done: true, inProgress: false}, 46 | {text: 'task 3', done: false, inProgress: false} 47 | ]; 48 | 49 | ui = newUI(); 50 | ui.setupTheTaskList(initialTasks, function (err) { 51 | if (err) 52 | return done(err); 53 | ui.startApp(done); 54 | }); 55 | }); 56 | 57 | afterEach(function () { 58 | if (ui) { 59 | ui.dispose(); 60 | ui = undefined; 61 | } 62 | }); 63 | 64 | it("the app has started", function () { 65 | expect(ui.isStarted()).to.be.ok; 66 | }); 67 | 68 | function getUI() { 69 | return ui; 70 | } 71 | 72 | function getInitialTasks() { 73 | return initialTasks; 74 | } 75 | 76 | describe("When the user request the 3rd task to be done", function () { 77 | describe("using the check", function () { 78 | beforeEach(function (done) { 79 | ui.requestToggleTaskUsingCheck(2, done); 80 | }); 81 | 82 | describeChangingATaskTo(getUI, 2, true, getInitialTasks); 83 | }); 84 | 85 | describe("without using the check", function () { 86 | beforeEach(function (done) { 87 | ui.requestToggleTaskNotUsingCheck(2, done); 88 | }); 89 | 90 | describeChangingATaskTo(getUI, 2, true, getInitialTasks); 91 | }); 92 | }); 93 | 94 | describe("When the user request the 2nd task to be undone", function () { 95 | describe("using the check", function () { 96 | beforeEach(function (done) { 97 | ui.requestToggleTaskUsingCheck(1, done); 98 | }); 99 | 100 | describeChangingATaskTo(getUI, 1, false, getInitialTasks); 101 | }); 102 | 103 | describe("without using the check", function () { 104 | beforeEach(function (done) { 105 | ui.requestToggleTaskNotUsingCheck(1, done); 106 | }); 107 | 108 | describeChangingATaskTo(getUI, 1, false, getInitialTasks); 109 | }); 110 | }); 111 | }); 112 | }); 113 | }; -------------------------------------------------------------------------------- /src/test/unit/app-controller-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var AppController = require('../../lib/core/controller').AppController, 3 | test = require('./test-doubles'), 4 | chai = require('chai'), 5 | expect = chai.expect, 6 | sinon = require('sinon'); 7 | 8 | chai.use(require('sinon-chai')); 9 | 10 | describe("The AppController, initialized with a task list model, a widget, a newTask and a taskWidgetFactory", function () { 11 | var controller, taskListModel, taskListWidget, taskWidgetFactory; 12 | beforeEach(function () { 13 | taskWidgetFactory = sinon.stub(); 14 | taskListModel = test.doubleFor('tasks', 'taskListModel'); 15 | taskListWidget = test.doubleFor('appWidget', 'taskListWidget'); 16 | controller = new AppController(taskListModel, taskListWidget, taskWidgetFactory); 17 | }); 18 | 19 | describe("has a start method, that when called", function () { 20 | var startcallback; 21 | beforeEach(function () { 22 | startcallback = sinon.stub(); 23 | 24 | controller.start(startcallback); 25 | }); 26 | 27 | describe("will call the forEach method of the model:", function () { 28 | it("with two callbacks", function () { 29 | expect(taskListModel.forEach).to.have.been.called; 30 | var lastCallArgs = taskListModel.forEach.lastCall.args; 31 | expect(lastCallArgs.length).to.be.equal(2); 32 | expect(lastCallArgs[0]).to.be.a('function'); 33 | expect(lastCallArgs[1]).to.be.a('function'); 34 | }); 35 | 36 | describe("the first callback that when called with a task", function () { 37 | var task, callback; 38 | beforeEach(function () { 39 | task = test.doubleFor('task'); 40 | callback = taskListModel.callbackForLastForEachCall(); 41 | }); 42 | 43 | it("will ask taskWidgetFactory to create a new widget for the task", function () { 44 | callback(task); 45 | 46 | expect(taskWidgetFactory).to.have.been.calledWith(task, taskListWidget); 47 | }); 48 | }); 49 | 50 | it("the second callback is the start application callback", function () { 51 | expect(taskListModel.forEach.lastCall.args[1]).to.be.equal(startcallback); 52 | }); 53 | }); 54 | 55 | describe("it will register on the event 'NewTaskRequest' of the widget", function () { 56 | it("a callback", function () { 57 | expect(taskListWidget.onNewTaskRequest).to.have.been.called; 58 | expect(taskListWidget.callbackForLastOnNewTaskRequestCall()).to.be.a('function'); 59 | }); 60 | describe("that when invoked with the requested task's description", function () { 61 | var description, taskWidget, task; 62 | beforeEach(function () { 63 | description = "requested task description"; 64 | 65 | taskWidget = test.doubleFor('taskWidget'); 66 | taskWidgetFactory.returns(taskWidget); 67 | 68 | task = test.doubleFor('task'); 69 | taskListModel.newTask.returns(task); 70 | 71 | taskListWidget.callbackForLastOnNewTaskRequestCall()(description); 72 | }); 73 | describe("will ask the model to create a new task", function () { 74 | it("with the description requested and a callback", function () { 75 | expect(taskListModel.newTask).to.have.been.called; 76 | expect(taskListModel.descriptionForLastNewTaskCall()).to.be.equal(description); 77 | expect(taskListModel.callbackForLastNewTaskCall()).to.be.a('function'); 78 | }); 79 | it("will ask taskWidgetFactory to create a new widget for the new task", function () { 80 | expect(taskWidgetFactory).to.have.been.calledWith(task, taskListWidget); 81 | }); 82 | it("will update the working state of the new task widget to true", function () { 83 | expect(taskWidget.working).to.have.been.calledWith(true); 84 | }); 85 | it("when the new task creation finished, the callback will update the task widget working state to false", function () { 86 | taskListModel.callbackForLastNewTaskCall()(); 87 | 88 | expect(taskWidget.working).to.have.been.calledWith(false); 89 | }); 90 | }); 91 | }); 92 | }); 93 | it("will call attachToDOM on the widget", function () { 94 | expect(taskListWidget.attachToDOM).to.have.been.called; 95 | }); 96 | }); 97 | }); -------------------------------------------------------------------------------- /src/test/unit/task-list-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var Tasks = require('../../lib/core/model').Tasks, 3 | test = require('./test-doubles'), 4 | chai = require('chai'), 5 | expect = chai.expect, 6 | sinon = require('sinon'); 7 | 8 | chai.use(require('sinon-chai')); 9 | 10 | describe("A Tasks object, initialized with a store and a task factory", function () { 11 | var tasks, store, taskFactory; 12 | beforeEach(function () { 13 | store = test.doubleFor('store'); 14 | taskFactory = sinon.stub(); 15 | tasks = new Tasks(store, taskFactory); 16 | }); 17 | 18 | describe("has a newTask method, that when invoked with a description and a callback", function () { 19 | var description, userCallback, newTask; 20 | beforeEach(function () { 21 | description = "new task description"; 22 | userCallback = sinon.stub(); 23 | newTask = test.doubleFor('task', 'new task'); 24 | taskFactory.returns(newTask); 25 | tasks.newTask(description, userCallback); 26 | }); 27 | it("will call the task factory with the description indicated as parameter and done state to false", function () { 28 | expect(taskFactory).to.have.been.calledWith({ 29 | description: description, 30 | done: false 31 | }); 32 | }); 33 | it("will call the save method on the task created by the task factory with a save callback", function () { 34 | expect(newTask.save).to.have.been.called; 35 | expect(newTask.callbackForLastSaveCall()).to.be.a('function'); 36 | }); 37 | it("will call the save callback with the new task when newTask.save() finished", function () { 38 | newTask.callbackForLastSaveCall()(newTask); 39 | 40 | expect(userCallback).to.have.been.calledWith(newTask); 41 | }); 42 | }); 43 | 44 | describe("has a forEach method, that when invoked with a callback", function () { 45 | var userCallback; 46 | beforeEach(function () { 47 | userCallback = sinon.stub(); 48 | tasks.forEach(userCallback); 49 | }); 50 | it("will call the all method on the store with callback", function () { 51 | expect(store.all).to.have.been.called; 52 | expect(store.callbackForLastAllCall()).to.be.a('function'); 53 | }); 54 | describe("when the store invokes the callback with the found task dtos ", function () { 55 | var foundDTOs, foundTasks; 56 | beforeEach(function () { 57 | foundDTOs = [test.spy('task dto 1'), test.spy('task dto 2')]; 58 | foundTasks = [test.doubleFor('task', 'found task 1'), test.doubleFor('task', 'found task 1')]; 59 | 60 | taskFactory.withArgs(foundDTOs[0]).returns(foundTasks[0]); 61 | taskFactory.withArgs(foundDTOs[1]).returns(foundTasks[1]); 62 | 63 | store.callbackForLastAllCall()(foundDTOs); 64 | }); 65 | it("the callback will call the task factory for each dto returned by the store", function () { 66 | expect(taskFactory.callCount).to.be.equal(2); 67 | 68 | expect(taskFactory.firstCall).to.have.been.calledWith(foundDTOs[0]); 69 | expect(taskFactory.secondCall).to.have.been.calledWith(foundDTOs[1]); 70 | }); 71 | it("the callback will invoke the user's callback once for each returned task", function () { 72 | expect(userCallback.callCount).to.be.equal(2); 73 | 74 | expect(userCallback.firstCall).to.have.been.calledWith(foundTasks[0]); 75 | expect(userCallback.secondCall).to.have.been.calledWith(foundTasks[1]); 76 | }); 77 | }); 78 | }); 79 | 80 | describe("has a forEach method, that can receive an optional second end callback", function () { 81 | var userCallback, endCallback; 82 | beforeEach(function () { 83 | userCallback = sinon.stub(); 84 | endCallback = sinon.stub(); 85 | tasks.forEach(userCallback, endCallback); 86 | }); 87 | it("if no data, the end callback will be invoked", function () { 88 | store.callbackForLastAllCall()([]); 89 | 90 | expect(endCallback).to.have.been.called; 91 | }); 92 | it("if data, the end callback will be invoked after the last task is provided", function () { 93 | var foundDTOs = [test.spy('task dto 1'), test.spy('task dto 2')]; 94 | var foundTasks = [test.doubleFor('task', 'found task 1'), test.doubleFor('task', 'found task 1')]; 95 | 96 | taskFactory.withArgs(foundDTOs[0]).returns(foundTasks[0]); 97 | taskFactory.withArgs(foundDTOs[1]).returns(foundTasks[1]); 98 | 99 | store.callbackForLastAllCall()(foundDTOs); 100 | 101 | expect(userCallback.callCount).to.be.equal(2); 102 | expect(endCallback).to.have.been.calledOnce; 103 | expect(endCallback).to.have.been.calledAfter(userCallback); 104 | }); 105 | }); 106 | }); -------------------------------------------------------------------------------- /src/lib/zepto_jquery/viewmodels.js: -------------------------------------------------------------------------------- 1 | /* 2 | * An Zepto/jQuery based ultrathin view layer for todo list application 3 | * The views translates DOM concepts to a plain JS Object in a cross browser fashion. 4 | * This way they decouples the rest of the application from the DOM and the presentation framework used. 5 | * In this sense you can think about them as a "DOM Access Layer". 6 | * In general they implement either a "Passive View" or a "View Model" pattern, so 7 | * they should have no logic. Main responsabilities: 8 | * - Sincronize JavaScript with DOM 9 | * - Capture low level user gestures (DOM events) 10 | * - Present a simple cross browser API to the upper layers 11 | */ 12 | module.exports = function ($, Event, field) { 13 | "use strict"; 14 | function TextFieldViewModel($self, text, isFocused) { 15 | var keyUp = new Event(); 16 | var textChanged = function (newText) { 17 | if (newText !== $self.val()) 18 | $self.val(newText); 19 | }; 20 | var focusChanged = function (isFocused) { 21 | if (isFocused) 22 | $self.focus(); 23 | }; 24 | 25 | var txt = this.text = field(text).subscribe(textChanged); 26 | this.onKeyUp = keyUp.subscribe.bind(keyUp); 27 | this.focus = field(isFocused).subscribe(focusChanged); 28 | 29 | // Init 30 | $self.keyup(function (ev) { 31 | txt($self.val()); 32 | keyUp.publish(ev); 33 | }); 34 | textChanged(txt()); 35 | focusChanged(this.focus()); 36 | } 37 | 38 | function ButtonViewModel($self, enabled) { 39 | var click = new Event(); 40 | var enabledChanged = function (isEnabled) { 41 | if (isEnabled) 42 | $self.removeAttr("disabled"); 43 | else 44 | $self.attr("disabled", true); 45 | }; 46 | 47 | this.enabled = field(enabled).subscribe(enabledChanged); 48 | this.onClick = click.subscribe.bind(click); 49 | 50 | // Init 51 | $self.click(click.publish.bind(click)); 52 | enabledChanged(this.enabled()); 53 | } 54 | 55 | function CheckboxViewModel(isChecked, enabled, $self) { 56 | $self.removeAttr("checked"); 57 | var enabledChanged = function (isEnabled) { 58 | if (isEnabled) 59 | $self.removeAttr("disabled"); 60 | else 61 | $self.attr("disabled", true); 62 | }; 63 | var checkedChanged = function (isChecked) { 64 | $self.prop("checked", isChecked); 65 | }; 66 | 67 | this.enabled = field(enabled).subscribe(enabledChanged); 68 | var checked = this.checked = field(isChecked).subscribe(checkedChanged); 69 | 70 | // Init 71 | $self.change(function () { 72 | checked($self.prop('checked')); 73 | }); 74 | enabledChanged(this.enabled()); 75 | checkedChanged(checked()); 76 | } 77 | 78 | function TaskViewModel(dto, $self) { 79 | var self = this, 80 | descriptionClicked = new Event(), 81 | $txt = $self.find('.txt'); 82 | 83 | function doneChanged(isDone) { 84 | $self.toggleClass("done", isDone); 85 | $self.toggleClass("todo", !isDone); 86 | } 87 | 88 | function workingChanged(isWorking) { 89 | $self.toggleClass("working", isWorking); 90 | } 91 | 92 | function initAndBind() { 93 | $txt.click(descriptionClicked.publish.bind(descriptionClicked)); 94 | $txt.text(dto.description); 95 | doneChanged(self.done()); 96 | workingChanged(self.working()); 97 | } 98 | 99 | self.done = field(dto.done).subscribe(doneChanged); 100 | self.working = field(false).subscribe(workingChanged); 101 | self.doneChk = new CheckboxViewModel(dto.done, !self.working(), $self.find('.chk')); 102 | self.onDescriptionClicked = descriptionClicked.subscribe.bind(descriptionClicked); 103 | self.appendToDOM = function ($container) { 104 | $container.append($self); 105 | $self.show(); 106 | }; 107 | 108 | initAndBind(); 109 | } 110 | 111 | function TasksListViewModel($taskExamples) { 112 | var attached = false; 113 | var $self = $taskExamples.parent(); 114 | var evenTaskExample = $taskExamples.filter(".even").first(); 115 | var oddTaskExample = $taskExamples.filter(".odd").first(); 116 | var taskViewModels = []; 117 | this.activate = function () { 118 | taskViewModels.forEach(function (viewForTask) { 119 | viewForTask.appendToDOM($self); 120 | }); 121 | attached = true; 122 | }; 123 | this.newViewForTask = function (dto) { 124 | var $example = taskViewModels.length % 2 === 0 ? evenTaskExample : oddTaskExample; 125 | var viewForTask = new TaskViewModel(dto, $example.clone()); 126 | taskViewModels.push(viewForTask); 127 | if (attached) 128 | viewForTask.appendToDOM($self); 129 | return viewForTask; 130 | }; 131 | 132 | $taskExamples.remove(); 133 | } 134 | 135 | return function ($elementsMap) { 136 | this.newTaskDescription = new TextFieldViewModel($elementsMap.newTaskDescription, '', false); 137 | this.addTaskBtn = new ButtonViewModel($elementsMap.addTaskBtn, false); 138 | var tasksList = new TasksListViewModel($elementsMap.tasksList, ''); 139 | 140 | this.newViewForTask = tasksList.newViewForTask.bind(tasksList); 141 | this.attachToDOM = tasksList.activate.bind(tasksList); 142 | }; 143 | }; -------------------------------------------------------------------------------- /src/lib/core/widgets.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Widgets are resusable low level controllers. 3 | * You can also think about widgets as application level views. 4 | * Their main responsability is convert low level UI concepts to application level concepts, 5 | * but they are decoupled from DOM and presentation details through a DOM acces layer or low level views. 6 | * Common functionality implemented in them are: 7 | * - Convert DOM events to application level events 8 | * - Formatting & Validation logic 9 | * - Map application level fields to low level fields in views 10 | * - Decouples the rest of the application from the specific presentation framework/technology 11 | * - Act as low level controller, coordinating user gestures with the DOM views. 12 | */ 13 | /* 14 | * In the case of the todo list application we have three widgets 15 | * - A "AppWidget" that represents the full todo list UI. It is a container of: 16 | * a) A "CreateTaskWidget" 17 | * c) A list of "TaskWidget"s 18 | * - A "CreateTaskWidget" that allows the user to create new tasks. Its functionality is: 19 | * a) Allows the user to create new tasks 20 | * b) Emit a "NewTaskRequest" event whenever the user requests the creation of a new task 21 | * c) Control the enable/disable logic of the DOM 22 | * - A "TaskWidget" that represents a concrete task to the user. Its responsabilities are: 23 | * a) Show the information of a task to the user 24 | * b) Allow the user to mark a task a 'done' or 'todo' 25 | * c) Emit a "ToggleDoneRequest" event whenever the user requests changing the task to 'done' or 'todo' 26 | * d) Control the enable/disable logic of the DOM 27 | * e) Give feedback to the user about the progress of the operations (create a task and toggle it are asynchronous and not instantaneous) 28 | */ 29 | "use strict"; 30 | 31 | function trim(str) { 32 | if (!str) 33 | return str; 34 | return str.replace(/^(\s*)([^\s]*)/g, '$2').replace(/([^\s]*)(\s*)$/g, '$1'); 35 | } 36 | 37 | module.exports = function (Event) { 38 | 39 | function CreateTaskWidget(newTaskDescription, addTaskBtn, optNewTaskRequestEvent) { 40 | // PRIVATE 41 | var newTaskRequest = optNewTaskRequestEvent || new Event(); 42 | var updateButtonEnabled = function (newText) { 43 | addTaskBtn.enabled(!!newText); 44 | }; 45 | var fireNewTaskRequest = function () { 46 | var taskDescription = trim(newTaskDescription.text()); 47 | if (!taskDescription) 48 | return; 49 | newTaskDescription.text(''); 50 | newTaskRequest.publish(taskDescription); 51 | }; 52 | 53 | // PUBLIC 54 | this.onNewTaskRequest = newTaskRequest.subscribe.bind(newTaskRequest); 55 | this.focus = newTaskDescription.focus.bind(newTaskDescription); 56 | 57 | // INIT & BIND 58 | newTaskDescription.onKeyUp(function (event) { 59 | if (event.keyCode === 13) 60 | fireNewTaskRequest(); 61 | }); 62 | newTaskDescription.text.subscribe(updateButtonEnabled); 63 | addTaskBtn.onClick(fireNewTaskRequest); 64 | updateButtonEnabled(newTaskDescription.text()); 65 | } 66 | 67 | function TaskWidget(ui, optToggleDoneRequestEvent) { 68 | // PRIVATE 69 | var toggleDoneRequest = optToggleDoneRequestEvent || new Event(); 70 | 71 | function fireToggleRequest() { 72 | if (ui.working()) 73 | return; 74 | var done = !ui.done(); 75 | ui.doneChk.checked(done); 76 | ui.working(true); 77 | toggleDoneRequest.publish(done); 78 | } 79 | 80 | function doneChanged() { 81 | ui.working(false); 82 | } 83 | 84 | function workingChanged(isWorking) { 85 | ui.doneChk.enabled(!isWorking); 86 | } 87 | 88 | function initAndBind() { 89 | ui.done.subscribe(doneChanged); 90 | ui.working.subscribe(workingChanged); 91 | ui.doneChk.checked.subscribe(fireToggleRequest); 92 | ui.onDescriptionClicked(fireToggleRequest); 93 | } 94 | 95 | // PUBLIC 96 | this.done = ui.done.bind(ui); 97 | this.working = ui.working.bind(ui); 98 | this.onToggleDoneRequest = toggleDoneRequest.subscribe.bind(toggleDoneRequest); 99 | 100 | initAndBind(); 101 | } 102 | 103 | function factoryOrElseDefault(optFactory) { 104 | return optFactory || { 105 | newCreateTaskWidget: function (txtField, btn) { 106 | return new CreateTaskWidget(txtField, btn); 107 | }, 108 | newTaskWidget: function (taskViewModel) { 109 | return new TaskWidget(taskViewModel); 110 | } 111 | }; 112 | } 113 | 114 | return { 115 | TaskWidget: TaskWidget, 116 | CreateTaskWidget: CreateTaskWidget, 117 | AppWidget: function (ui, optFactory) { 118 | // PRIVATE 119 | var factory = factoryOrElseDefault(optFactory); 120 | 121 | var createTaskWidget = factory.newCreateTaskWidget(ui.newTaskDescription, ui.addTaskBtn); 122 | 123 | // PUBLIC 124 | this.newWidgetForTask = function (dto) { 125 | return factory.newTaskWidget(ui.newViewForTask(dto)); 126 | }; 127 | this.onNewTaskRequest = createTaskWidget.onNewTaskRequest.bind(createTaskWidget); 128 | this.attachToDOM = function () { 129 | ui.attachToDOM(); 130 | createTaskWidget.focus(true); 131 | }; 132 | } 133 | }; 134 | }; -------------------------------------------------------------------------------- /src/test/unit/create-task-widget-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var CreateTaskWidget = require('../../lib/core/widgets')(null).CreateTaskWidget, 3 | test = require('./test-doubles'), 4 | chai = require('chai'), 5 | expect = chai.expect, 6 | sinon = require('sinon'); 7 | 8 | chai.use(require('sinon-chai')); 9 | 10 | describe("A CreateTaskWidget, initialized with a task description text field, an add task button and a 'NewTaskRequest' event", function () { 11 | var widget, descriptionTxtField, addBtn, newTaskRequestEvent; 12 | beforeEach(function () { 13 | descriptionTxtField = test.doubleFor('textFieldViewModel'); 14 | descriptionTxtField.text.returns(''); 15 | addBtn = test.doubleFor('buttonViewModel'); 16 | newTaskRequestEvent = test.doubleFor('event'); 17 | 18 | widget = new CreateTaskWidget(descriptionTxtField, addBtn, newTaskRequestEvent); 19 | }); 20 | 21 | var testWillClearTheTextFieldAndPublishNewTaskRequestEvent = function (callback) { 22 | it("if the task description text is empty, nothing will happen", function () { 23 | descriptionTxtField.text.returns(''); 24 | 25 | callback(); 26 | 27 | expect(newTaskRequestEvent.publish).not.to.have.been.called; 28 | }); 29 | describe("if the task description is not empty", function () { 30 | var newTaskDescriptionText; 31 | beforeEach(function () { 32 | newTaskDescriptionText = "new task to do"; 33 | descriptionTxtField.text.returns(newTaskDescriptionText); 34 | 35 | callback(); 36 | }); 37 | it("the task description text will be cleared", function () { 38 | expect(descriptionTxtField.text).to.have.been.calledWith(''); 39 | }); 40 | it("a 'NewTaskRequest' event will be published", function () { 41 | expect(newTaskRequestEvent.publish).to.have.been.calledWith(newTaskDescriptionText); 42 | }); 43 | }); 44 | }; 45 | 46 | it("has a onNewTaskRequest method that register a callback into 'NewTaskRequest' event", function () { 47 | var callback = sinon.stub(); 48 | 49 | widget.onNewTaskRequest(callback); 50 | 51 | expect(newTaskRequestEvent.subscribe).to.have.been.calledWith(callback); 52 | }); 53 | it("has a focus method that is equivalent to calling focus on the task description field", function () { 54 | var newFocusValue = true; 55 | var expectedResult = "expected focus result"; 56 | descriptionTxtField.focus.returns(expectedResult); 57 | 58 | var result = widget.focus(newFocusValue); 59 | 60 | expect(descriptionTxtField.focus).to.have.been.calledWith(newFocusValue); 61 | expect(result).to.be.equal(expectedResult); 62 | }); 63 | it("will enable the add task btn if the description text field is not empty", function () { 64 | expect(addBtn.enabled).to.have.been.calledWith(!!descriptionTxtField.text()); 65 | }); 66 | describe("will register a callback on the description text field's text change event", function () { 67 | it("the callback is a valid function", function () { 68 | expect(descriptionTxtField.text.subscribe).to.have.been.called; 69 | 70 | expect(descriptionTxtField.text.callbackForLastSubscribeCall()).to.be.a('function'); 71 | }); 72 | it("when the text changes to an empty string, it will disable the add task button", function () { 73 | descriptionTxtField.text.callbackForLastSubscribeCall()(''); 74 | 75 | expect(addBtn.enabled).to.have.been.calledWith(false); 76 | }); 77 | it("when the text changes to an empty string, it will disable the add task button", function () { 78 | descriptionTxtField.text.callbackForLastSubscribeCall()('not empty'); 79 | 80 | expect(addBtn.enabled).to.have.been.calledWith(true); 81 | }); 82 | }); 83 | describe("will register a callback on the click event of add task button", function () { 84 | it("the callback is a valid function", function () { 85 | expect(addBtn.onClick).to.have.been.called; 86 | 87 | expect(addBtn.callbackForLastOnClickCall()).to.be.a('function'); 88 | }); 89 | describe("when the button is clicked, the text field will be cleared and an event may be published", function () { 90 | testWillClearTheTextFieldAndPublishNewTaskRequestEvent(function () { 91 | return addBtn.callbackForLastOnClickCall()(); 92 | }); 93 | }); 94 | }); 95 | describe("will register a callback on the key up event of task description text field", function () { 96 | it("the callback is a valid function", function () { 97 | expect(descriptionTxtField.onKeyUp).to.have.been.called; 98 | 99 | expect(descriptionTxtField.callbackForLastOnKeyUpCall()).to.be.a('function'); 100 | }); 101 | describe("when the user do a keystroke on the text field", function () { 102 | it("won't happend anything if the key stroked is not enter", function () { 103 | descriptionTxtField.text.returns('not empty'); 104 | 105 | descriptionTxtField.callbackForLastOnKeyUpCall()({keyCode: 23}); 106 | 107 | expect(newTaskRequestEvent.publish).not.to.have.been.called; 108 | }); 109 | describe("if the key stroked is enter, the text field will be cleared and an event may be published", function () { 110 | testWillClearTheTextFieldAndPublishNewTaskRequestEvent(function () { 111 | return descriptionTxtField.callbackForLastOnKeyUpCall()({keyCode: 13}); 112 | }); 113 | }); 114 | }); 115 | }); 116 | }); -------------------------------------------------------------------------------- /src/test/unit/test-doubles.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var sinon = require('sinon'); 4 | 5 | function spy(name, methodNames) { 6 | methodNames = methodNames || []; 7 | var r = { _description: name }, 8 | numberOfMethods = methodNames.length, 9 | methodName; 10 | for (var i = 0; i < numberOfMethods; i++) { 11 | methodName = methodNames[i]; 12 | r[methodName] = sinon.stub(); 13 | } 14 | r.toString = function () { 15 | return "test double <" + name + ">"; 16 | }; 17 | return r; 18 | } 19 | 20 | var doubleFactories = { 21 | task: function (name) { 22 | var taskDouble = spy(name, ['save', 'toDTO', 'done']); 23 | taskDouble.callbackForLastSaveCall = function () { 24 | return this.save.lastCall.args[0]; 25 | }; 26 | return taskDouble; 27 | }, 28 | tasks: function (name) { 29 | var taskListDouble = spy(name, ['newTask', 'forEach']); 30 | taskListDouble.descriptionForLastNewTaskCall = function () { 31 | return this.newTask.lastCall.args[0]; 32 | }; 33 | taskListDouble.callbackForLastNewTaskCall = function () { 34 | return this.newTask.lastCall.args[1]; 35 | }; 36 | taskListDouble.callbackForLastForEachCall = function () { 37 | return this.forEach.lastCall.args[0]; 38 | }; 39 | return taskListDouble; 40 | }, 41 | store: function (name) { 42 | var storeDouble = spy(name, ['save', 'all']); 43 | storeDouble.dataForLastSaveCall = function () { 44 | return this.save.lastCall.args[0]; 45 | }; 46 | storeDouble.callbackForLastSaveCall = function () { 47 | return this.save.lastCall.args[1]; 48 | }; 49 | storeDouble.callbackForLastAllCall = function () { 50 | return this.all.lastCall.args[0]; 51 | }; 52 | return storeDouble; 53 | }, 54 | appWidget: function (name) { 55 | var appWidgetDouble = spy(name, ['attachToDOM', 'onNewTaskRequest', 'newWidgetForTask']); 56 | appWidgetDouble.callbackForLastOnNewTaskRequestCall = function () { 57 | return this.onNewTaskRequest.lastCall.args[0]; 58 | }; 59 | return appWidgetDouble; 60 | }, 61 | taskWidget: function (name) { 62 | var taskWidgetDouble = spy(name, ['onToggleDoneRequest', 'working', 'done']); 63 | taskWidgetDouble.callbackForLastOnToggleDoneRequestCall = function () { 64 | return this.onToggleDoneRequest.lastCall.args[0]; 65 | }; 66 | return taskWidgetDouble; 67 | }, 68 | createTaskWidget: function (name) { 69 | var createTaskWidgetDouble = spy(name, ['onNewTaskRequest']); 70 | createTaskWidgetDouble.focus = doubleFor('field', name + '.focus'); 71 | createTaskWidgetDouble.callbackForLastOnNewTaskRequestCall = function () { 72 | return this.onNewTaskRequest.lastCall.args[0]; 73 | }; 74 | return createTaskWidgetDouble; 75 | }, 76 | textFieldViewModel: function (name) { 77 | var textFieldViewModelDouble = spy(name, ['onKeyUp']); 78 | textFieldViewModelDouble.text = doubleFor('field', name + '.text'); 79 | textFieldViewModelDouble.focus = doubleFor('field', name + '.focus'); 80 | textFieldViewModelDouble.callbackForLastOnKeyUpCall = function () { 81 | return this.onKeyUp.lastCall.args[0]; 82 | }; 83 | return textFieldViewModelDouble; 84 | }, 85 | taskViewModel: function (name) { 86 | var taskViewModelDouble = spy(name, ['onDescriptionClicked']); 87 | taskViewModelDouble.done = doubleFor('field', name + '.done'); 88 | taskViewModelDouble.working = doubleFor('field', name + '.working'); 89 | taskViewModelDouble.doneChk = doubleFor('checkboxViewModel', name + '.doneChk'); 90 | taskViewModelDouble.callbackForLastOnDescriptionClickedCall = function () { 91 | return this.onDescriptionClicked.lastCall.args[0]; 92 | }; 93 | return taskViewModelDouble; 94 | }, 95 | checkboxViewModel: function (name) { 96 | var checkboxViewModelDouble = spy(name); 97 | checkboxViewModelDouble.enabled = doubleFor('field', name + '.enabled'); 98 | checkboxViewModelDouble.checked = doubleFor('field', name + '.checked'); 99 | return checkboxViewModelDouble; 100 | }, 101 | buttonViewModel: function (name) { 102 | var buttonViewModelDouble = spy(name, ['onClick']); 103 | buttonViewModelDouble.enabled = doubleFor('field', name + '.enabled'); 104 | buttonViewModelDouble.callbackForLastOnClickCall = function () { 105 | return this.onClick.lastCall.args[0]; 106 | }; 107 | return buttonViewModelDouble; 108 | }, 109 | appViewModel: function (name) { 110 | var viewModelDouble = spy(name, ['newViewForTask', 'attachToDOM']); 111 | viewModelDouble.newTaskDescription = doubleFor('textFieldViewModel', name + '.newTaskDescription'); 112 | viewModelDouble.addTaskBtn = doubleFor('buttonViewModel', name + ".addTaskBtn"); 113 | return viewModelDouble; 114 | }, 115 | event: function (name) { 116 | var eventDouble = spy(name, ['subscribe', 'publish']); 117 | eventDouble.callbackForLastSubscribeCall = function () { 118 | return this.subscribe.lastCall.args[0]; 119 | }; 120 | return eventDouble; 121 | }, 122 | field: function () { 123 | var fieldDouble = sinon.stub(); 124 | fieldDouble.subscribe = sinon.stub(); 125 | fieldDouble.callbackForLastSubscribeCall = function () { 126 | return this.subscribe.lastCall.args[0]; 127 | }; 128 | return fieldDouble; 129 | } 130 | }; 131 | 132 | function doubleFor(objectType, optName) { 133 | var doubleFactory = doubleFactories[objectType]; 134 | if (typeof doubleFactory !== 'function') 135 | throw new Error('No test double found for ' + objectType); 136 | return doubleFactory(optName || objectType); 137 | } 138 | 139 | module.exports = { 140 | spy: spy, 141 | doubleFor: doubleFor 142 | }; 143 | -------------------------------------------------------------------------------- /src/test/unit/task-widget-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var TaskWidget = require('../../lib/core/widgets')(null).TaskWidget, 3 | test = require('./test-doubles'), 4 | chai = require('chai'), 5 | expect = chai.expect, 6 | sinon = require('sinon'); 7 | 8 | chai.use(require('sinon-chai')); 9 | 10 | describe("A TaskWidget, initialized with a task view model and a 'ToggleDoneRequest' event", function () { 11 | var widget, taskViewModel, toggleDoneRequestEvent; 12 | beforeEach(function () { 13 | taskViewModel = test.doubleFor('taskViewModel'); 14 | toggleDoneRequestEvent = test.doubleFor('event'); 15 | 16 | widget = new TaskWidget(taskViewModel, toggleDoneRequestEvent); 17 | }); 18 | 19 | it("has a onToggleDoneRequest method that register a callback into 'ToggleDoneRequest' event", function () { 20 | var callback = sinon.stub(); 21 | 22 | widget.onToggleDoneRequest(callback); 23 | 24 | expect(toggleDoneRequestEvent.subscribe).to.have.been.calledWith(callback); 25 | }); 26 | it("has a working method that is equivalent to calling working on the task view model", function () { 27 | var newWorkingValue = true; 28 | var expectedResult = "expected working result"; 29 | taskViewModel.working.returns(expectedResult); 30 | 31 | var result = widget.working(newWorkingValue); 32 | 33 | expect(taskViewModel.working).to.have.been.calledWith(newWorkingValue); 34 | expect(result).to.be.equal(expectedResult); 35 | }); 36 | it("has a done method that is equivalent to calling done on the task view model", function () { 37 | var newDoneValue = true; 38 | var expectedResult = "expected done result"; 39 | taskViewModel.done.returns(expectedResult); 40 | 41 | var result = widget.done(newDoneValue); 42 | 43 | expect(taskViewModel.done).to.have.been.calledWith(newDoneValue); 44 | expect(result).to.be.equal(expectedResult); 45 | }); 46 | describe("will register a callback on the task view model working change event", function () { 47 | it("the callback is a valid function", function () { 48 | expect(taskViewModel.working.subscribe).to.have.been.called; 49 | 50 | expect(taskViewModel.working.callbackForLastSubscribeCall()).to.be.a('function'); 51 | }); 52 | it("when working changes to true, it will disable the checkbox of the task view model", function () { 53 | taskViewModel.working.callbackForLastSubscribeCall()(true); 54 | 55 | expect(taskViewModel.doneChk.enabled).to.have.been.calledWith(false); 56 | }); 57 | it("when working changes to false, it will enable the checkbox of the task view model", function () { 58 | taskViewModel.working.callbackForLastSubscribeCall()(false); 59 | 60 | expect(taskViewModel.doneChk.enabled).to.have.been.calledWith(true); 61 | }); 62 | }); 63 | describe("will register a callback on the task view model done change event", function () { 64 | it("the callback is a valid function", function () { 65 | expect(taskViewModel.done.subscribe).to.have.been.called; 66 | 67 | expect(taskViewModel.done.callbackForLastSubscribeCall()).to.be.a('function'); 68 | }); 69 | it("when done changes to any value, it will stop working", function () { 70 | var anyValue = true; 71 | 72 | taskViewModel.done.callbackForLastSubscribeCall()(anyValue); 73 | 74 | expect(taskViewModel.working).to.have.been.calledWith(false); 75 | }); 76 | }); 77 | describe("will register a callback on the click event of the task description and the check change of the task checkbox", function () { 78 | it("the callback for the checkbox is a valid function", function () { 79 | expect(taskViewModel.doneChk.checked.subscribe).to.have.been.called; 80 | 81 | expect(taskViewModel.doneChk.checked.callbackForLastSubscribeCall()).to.be.a('function'); 82 | }); 83 | it("the callback for the task description's click event is a valid function", function () { 84 | expect(taskViewModel.onDescriptionClicked).to.have.been.called; 85 | 86 | expect(taskViewModel.callbackForLastOnDescriptionClickedCall()).to.be.a('function'); 87 | }); 88 | it("both callbacks are the same", function () { 89 | expect(taskViewModel.callbackForLastOnDescriptionClickedCall()) 90 | .to.be.equal(taskViewModel.doneChk.checked.callbackForLastSubscribeCall()); 91 | }); 92 | describe("when the user click on the task description or changes its checkbox", function () { 93 | it("won't do anything if the task widget is working", function () { 94 | taskViewModel.working.returns(true); 95 | 96 | taskViewModel.callbackForLastOnDescriptionClickedCall()(); 97 | 98 | expect(toggleDoneRequestEvent.publish).not.to.have.been.called; 99 | }); 100 | describe("if the task widget is not working", function () { 101 | var newDesiredDoneState; 102 | beforeEach(function () { 103 | newDesiredDoneState = true; 104 | taskViewModel.done.returns(!newDesiredDoneState); 105 | taskViewModel.working.returns(false); 106 | 107 | taskViewModel.callbackForLastOnDescriptionClickedCall()(); 108 | }); 109 | it("will update the checkbox with the desired done state", function () { 110 | expect(taskViewModel.doneChk.checked).to.have.been.calledWith(newDesiredDoneState); 111 | }); 112 | it("will show the user her request is being processed (working=true)", function () { 113 | expect(taskViewModel.working).to.have.been.calledWith(true); 114 | }); 115 | it("will publish a 'ToggleDoneRequest' event with the desired done state", function () { 116 | expect(toggleDoneRequestEvent.publish).to.have.been.calledWith(newDesiredDoneState); 117 | }); 118 | }); 119 | }); 120 | }); 121 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Another todo list application? Why bother...? 2 | ============================================= 3 | 4 | [![Build Status](https://secure.travis-ci.org/eamodeorubio/explore-the-todo-list-app.png)](http://travis-ci.org/eamodeorubio/explore-the-todo-list-app) 5 | 6 | All the todo list applications I've seen have the purpose of showing the "easy to use" a framework are. In this case 7 | I simply don't want to proof that a framework is better than other. 8 | 9 | Since I teach TDD/BDD, Javascript and OO, I'd like to show to my students best practices to develop rich 10 | internet applications, and the todo list application is simple enough to show key concepts. So I think it's a good 11 | idea publishing a full sample project showing this concepts. 12 | 13 | **Of course I'm not perfect, the application can have errors or something could have been done in a better way**. This 14 | is my second motivation, to learn. I expect to get some feedback to enhance this sample application ! I can lear too 15 | new ideas and techniches. 16 | 17 | Finally I'd like to explore some frameworks and how to use them in an efective way. 18 | 19 | Framework agnostic architecture 20 | =============================== 21 | 22 | A well designed architecture should give you the freedom of switching the framework used without impacting most of 23 | your code. In this project I'd tried to decouple most of the code from any framework. 24 | 25 | DOM Access Layer 26 | ---------------- 27 | 28 | The DOM Access Layer decouple the core of the application from the specifics of DOM manipulation. In this case I use 29 | "view models" to implement this layer. There is an implementation of these "view models" using knockout.js, 30 | and other using Zepto/jQuery. Switching from one framework to another is only matter of switching the "view 31 | model" library used. If in the future we wish to change the presentation framework we only need to implement a new 32 | "view model" library. 33 | 34 | It's interesting comparing both implementations of the DOM Access Layer. Clearly knockout.js seems more powerful, 35 | the view layer is very simple to code. With Zepto/jQuery the view layer is more complex, 36 | but I don't need to modify the HTML and I can use the same document as provided by the web designer. Another advantage of 37 | Zepto/jQuery is that the final result has less footprint and less start up time, specially in the case of Zepto. 38 | 39 | Data Access Layer 40 | ----------------- 41 | 42 | The Data Access Layer decouples the models from the specifics storage 43 | mechanism used. Currently there is only support for local storage. I plan to 44 | add more storage systems (AJAX, WebSockets...) only need to implement a new 45 | storage library. The rest of the code won't need to be changed. 46 | 47 | TDD/BDD 48 | ======= 49 | 50 | The core of the application code is fully unit tested. Furthermore there exists end to end integration tests for most of the use cases scenarios of the application. I didn't use any fancy BDD framework, but just Jasmine and PhantomJS. 51 | 52 | The tests are integrated in a build script and generate JUnit XML report files, so I hope it will be easy to integrate with a CI server in the future. 53 | 54 | If you have never seen TDD in practice in a full rich internet application, I hope this project helps you to 55 | illustrate how to apply TDD in a web application based in javascript and HTML5. 56 | 57 | Build 58 | ===== 59 | 60 | Standalone build 61 | ---------------- 62 | 63 | To build this project follow these steps: 64 | 65 | 1. If not on your system, [install Node.js](http://nodejs.org/#download). It has already installed NPM. 66 | 2. If you plan to run the BDD integration tests install [PhantomJS] 67 | (http://phantomjs.org/download.html) 68 | 3 Uninstall old version of grunt `npm uninstall -g grunt` 69 | 4. Install grunt-cli: `npm install -g grunt-cli` 70 | 5. Download this project to your computer 71 | 6. Install all the packages needed to build this application, it's easy, 72 | simply run the command ``npm install`` in the root of the project 73 | 7. Execute the following command ``npm test`` from the root folder of this 74 | project. 75 | 76 | Travis CI 77 | --------- 78 | 79 | This project is now integrated with travis CI. Have a look at the ``.travis.yml`` file and the ``package.json`` file at the root of the project 80 | 81 | Execute 82 | ======= 83 | 84 | After building the project, simply open in your browser either 85 | ``todo_with_knockout.html`` or ``todo_with_zepto.html`` or ``todo_with_jquery 86 | .html``. Do this from the `dist/` directory. 87 | 88 | Proyect layout 89 | ============== 90 | 91 | The project is structured in the following directories: 92 | 93 | * ```package.json``` The node information used by npm and Travis-CI when you want to build this project with them 94 | * ```.travis.yml``` The Travis-CI configuration file for this project 95 | * ```karma.conf.js``` The karma configuration file (for BDD test suite) 96 | * ```Gruntfile.js``` The grunt configuration file 97 | * ```dist/``` The output directory 98 | * ```src/``` The source code for the application 99 | * ```css/``` Simple CSS for the applications 100 | * ```img/``` Some images (the ajax loader) 101 | * ```lib/``` The source code for the runtime 102 | * ```common/``` The source code of the core of the application, decoupled from the DOM/Presentation framework 103 | used. The unit tests cover this files, except store.js. 104 | * ```utils.js``` Event & observable fields (whenever I do not have knockout) 105 | * ```model.js``` A Task & Tasks (task list) model. Decoupled of the specific persistence/storage mechanism 106 | * ```controller.js``` The high level application controller 107 | * ```widgets.js``` Reusable controllers/widgets. Decoupled from the specific presentation framework 108 | * ```store.js``` A simple in memory storage. 109 | * ```knockout/``` The view models implemented with [Knockout.js](http://knockoutjs.com/) 110 | * ```zepto_jquery/``` The view models implemented with [Zepto](http://zeptojs.com/)/[jQuery](http://docs.jquery 111 | .com/Downloading_jQuery) 112 | * ```tests/``` The source code for the tests 113 | * ```bdd/``` End to end tests of the application (BDD test suite) 114 | * ```vendor/``` Third party libs used for testing (jQuery, 115 | jQuery-simulate & jQuery-simulate.key-sequence) 116 | * ```main.js``` The entry point for the tdd 117 | * ```adding-tasks.js``` The BDD test suite about creating new tasks in the application 118 | * ```consulting-tasks.js``` The BDD test suite about consulting the existing tasks in the application 119 | * ```doing-tasks.js``` The BDD test suite about marking tasks as done or undone. 120 | * ```page-objects.js``` A wrapper around the application's UI 121 | using jQuery, jQuery simulate and jQuery simulate key-sequece 122 | * ```unit/``` Unit tests 123 | * ```test-doubles.js``` A test double (mocks/spies/stubs) library for the application 124 | * ```*-tests.js``` The unit test suites 125 | 126 | 127 | Not yet done (in the roadmap) 128 | ============================= 129 | 130 | There are a lot of things left to do ! 131 | 132 | * Use bower for client-side dependencies 133 | * Use application cache manifest 134 | * Edit the description of the task (click to edit) 135 | * Task filters (view only done/todo) 136 | * Task filters (matching description) 137 | * A better web design (somebody help me with those damned CSS!) 138 | * Responsive design 139 | * An storage based in a remote REST service using AJAX 140 | * An storage based in a remote service using WebSockets 141 | * An robust storage able to switch to local storage when there is no connectivity and resync with the server when it 142 | is online again 143 | * Explore other presentation frameworks (ember.js, jquerymobile...?) 144 | 145 | Feel free to experiment 146 | ======================= 147 | 148 | If you are learning JavaScript and about building a rich web application, this is your project. Feel free to read the 149 | code, modify it and experiment! This is the main purpose of this project ! -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (grunt) { 4 | var GRUNT_FILE = 'Gruntfile.js', 5 | OUTPUT_DIR = 'dist/', 6 | SRC_DIR = 'src/', 7 | SCRIPTS_DIR = SRC_DIR + 'lib/', 8 | TESTS_DIR = SRC_DIR + 'test/', 9 | CORE_SOURCES = SCRIPTS_DIR + 'core/**/*.js', 10 | KO_SOURCES = SCRIPTS_DIR + 'knockout/**/*.js', 11 | ZEPTO_JQUERY_SOURCES = SCRIPTS_DIR + '/zepto_jquery/**/*.js', 12 | UNIT_TESTS_SOURCES = TESTS_DIR + 'unit/**/*.js', 13 | STYLES_SOURCES = SRC_DIR + 'css/**/*.css', 14 | BDD_SOURCES = TESTS_DIR + 'bdd/*.js'; 15 | 16 | grunt.initConfig({ 17 | pkg: grunt.file.readJSON('package.json'), 18 | jshint: { 19 | options: { 20 | browser: false, 21 | node: false, 22 | jquery: false, 23 | bitwise: true, 24 | camelcase: true, 25 | curly: false, 26 | eqeqeq: true, 27 | forin: true, 28 | immed: true, 29 | latedef: true, 30 | newcap: true, 31 | noarg: true, 32 | nonew: true, 33 | plusplus: false, 34 | undef: true, 35 | unused: true, 36 | white: false, 37 | strict: true, 38 | globalstrict: true, // The files will be wrapped in a IIFE 39 | trailing: true, 40 | maxparams: 3, 41 | maxdepth: 2, 42 | maxstatements: 10, 43 | maxcomplexity: 5 44 | }, 45 | core: { 46 | options: { 47 | globals: { 48 | module: true, 49 | setTimeout: true, 50 | localStorage: true 51 | } 52 | }, 53 | files: { 54 | src: [CORE_SOURCES] 55 | } 56 | }, 57 | gruntfile: { 58 | options: { 59 | node: true, 60 | maxstatements: false 61 | }, 62 | files: { 63 | src: [GRUNT_FILE] 64 | } 65 | }, 66 | knockout: { 67 | options: { 68 | browser: true, 69 | globals: { 70 | console: true, 71 | module: true, 72 | require: true 73 | } 74 | }, 75 | files: { 76 | src: [KO_SOURCES] 77 | } 78 | }, 79 | jquery: { 80 | options: { 81 | browser: true, 82 | jquery: true, 83 | globals: { 84 | console: true, 85 | module: true, 86 | require: true 87 | } 88 | }, 89 | files: { 90 | src: [ZEPTO_JQUERY_SOURCES] 91 | } 92 | }, 93 | bdd: { 94 | options: { 95 | browser: true, 96 | jquery: true, 97 | expr: true, 98 | maxparams: 5, 99 | maxdepth: 3, 100 | maxstatements: 20, 101 | globals: { 102 | describe: true, 103 | beforeEach: true, 104 | afterEach: true, 105 | xdescribe: true, 106 | context: true, 107 | it: true, 108 | xit: true, 109 | chai: true, 110 | bdd: true, 111 | console: true 112 | } 113 | }, 114 | files: { 115 | src: [BDD_SOURCES] 116 | } 117 | }, 118 | tests: { 119 | options: { 120 | node: true, 121 | expr: true, 122 | latedef: false, // Sic! Not working with function declarations 123 | globals: { 124 | describe: true, 125 | beforeEach: true, 126 | afterEach: true, 127 | xdescribe: true, 128 | context: true, 129 | it: true, 130 | xit: true 131 | } 132 | }, 133 | files: { 134 | src: [UNIT_TESTS_SOURCES] 135 | } 136 | } 137 | }, 138 | csslint: { 139 | options: { 140 | formatters: [ 141 | {id: 'junit-xml', dest: 'csslint_junit.xml'} 142 | ] 143 | }, 144 | main: { 145 | src: [STYLES_SOURCES] 146 | } 147 | }, 148 | simplemocha: { 149 | options: { 150 | timeout: 500, // They are *unit* tests! 151 | ui: 'bdd' 152 | }, 153 | dev: { 154 | options: { 155 | reporter: 'dot' 156 | }, 157 | files: { 158 | src: [UNIT_TESTS_SOURCES] 159 | } 160 | }, 161 | ci: { 162 | options: { 163 | reporter: 'mocha-specxunitcov-reporter' 164 | }, 165 | files: { 166 | src: [UNIT_TESTS_SOURCES] 167 | } 168 | } 169 | }, 170 | watch: { 171 | gruntfile: { 172 | files: [GRUNT_FILE], 173 | tasks: ['jshint:gruntfile'] 174 | }, 175 | bdd: { 176 | files: [BDD_SOURCES], 177 | tasks: ['jshint:bdd'] 178 | }, 179 | tests: { 180 | files: [UNIT_TESTS_SOURCES], 181 | tasks: ['jshint:tests', 'simplemocha:dev'] 182 | }, 183 | core: { 184 | files: [CORE_SOURCES], 185 | tasks: ['jshint:core', 'simplemocha:dev'] 186 | }, 187 | zeptoJQuery: { 188 | files: [ZEPTO_JQUERY_SOURCES], 189 | tasks: ['jshint:jquery', 'simplemocha:dev'] 190 | }, 191 | knockout: { 192 | files: [KO_SOURCES], 193 | tasks: ['jshint:ko', 'simplemocha:dev'] 194 | }, 195 | styles: { 196 | files: [STYLES_SOURCES], 197 | tasks: ['csslint'] 198 | } 199 | }, 200 | clean: [OUTPUT_DIR], 201 | copy: { 202 | main: { 203 | files: [ 204 | {expand: true, cwd: SRC_DIR, src: ['img/**', '**/!(initial_*).html'], dest: OUTPUT_DIR}, 205 | {expand: true, cwd: './', src: ['vendor/**/!(almond.js)'], dest: OUTPUT_DIR + 'js/'} 206 | ] 207 | } 208 | }, 209 | cssmin: { 210 | options: { 211 | report: 'gzip' 212 | }, 213 | minify: { 214 | src: [STYLES_SOURCES], 215 | dest: "dist/css/<%= pkg.name %>.min.css" 216 | } 217 | }, 218 | requirejs: { 219 | options: { 220 | baseUrl: SCRIPTS_DIR, 221 | cjsTranslate: true, 222 | useStrict: true, 223 | preserveLicenseComments: false, 224 | generateSourceMaps: true, 225 | optimize: 'uglify2', 226 | include: ['../../vendor/almond.js'] 227 | }, 228 | zeptoJQuery: { 229 | options: { 230 | name: 'zepto_jquery/main', 231 | insertRequire: ['zepto_jquery/main'], 232 | out: "dist/js/<%= pkg.name %>-zepto_jquery.min.js", 233 | uglify2: { 234 | report: 'gzip', 235 | mangle: { 236 | except: ['jQuery', 'Zepto'] 237 | } 238 | } 239 | } 240 | }, 241 | ko: { 242 | options: { 243 | name: 'knockout/main', 244 | insertRequire: ['knockout/main'], 245 | out: "dist/js/<%= pkg.name %>-ko.min.js", 246 | uglify2: { 247 | report: 'gzip', 248 | mangle: { 249 | except: ['ko'] 250 | } 251 | } 252 | } 253 | } 254 | }, 255 | karma: { 256 | options: { 257 | configFile: 'karma.conf.js', 258 | runnerPort: 9999 259 | }, 260 | dev: { 261 | singleRun: false, 262 | reporters: ['dots'], 263 | browsers: ['Chrome'] 264 | }, 265 | ci: { 266 | singleRun: true, 267 | reporters: ['dots', 'junit'], 268 | browsers: ['PhantomJS', 'Firefox'] 269 | } 270 | } 271 | }); 272 | 273 | grunt.loadNpmTasks('grunt-contrib-jshint'); 274 | grunt.loadNpmTasks('grunt-contrib-csslint'); 275 | grunt.loadNpmTasks('grunt-contrib-watch'); 276 | grunt.loadNpmTasks('grunt-simple-mocha'); 277 | grunt.loadNpmTasks('grunt-contrib-clean'); 278 | grunt.loadNpmTasks('grunt-contrib-copy'); 279 | grunt.loadNpmTasks('grunt-contrib-requirejs'); 280 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 281 | grunt.loadNpmTasks('grunt-karma'); 282 | 283 | grunt.registerTask('lint', [ 284 | 'csslint', 285 | 'jshint' 286 | ]); 287 | 288 | grunt.registerTask('dev', [ 289 | 'lint', 290 | 'simplemocha:dev' 291 | ]); 292 | 293 | grunt.registerTask('dist', [ 294 | 'clean', 295 | 'cssmin', 296 | 'requirejs', 297 | 'copy' 298 | ]); 299 | 300 | grunt.registerTask('build', [ 301 | 'lint', 302 | 'simplemocha:ci', 303 | 'dist', 304 | 'karma:ci' 305 | ]); 306 | 307 | grunt.registerTask('default', ['watch']); 308 | }; -------------------------------------------------------------------------------- /src/test/bdd/vendor/jquery.simulate.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Simulate v0.0.1 - simulate browser mouse and keyboard events 3 | * https://github.com/jquery/jquery-simulate 4 | * 5 | * Copyright 2012 jQuery Foundation and other contributors 6 | * Released under the MIT license. 7 | * http://jquery.org/license 8 | * 9 | * Date: Sun Dec 9 12:15:33 2012 -0500 10 | */ 11 | 12 | ;(function( $, undefined ) { 13 | 14 | var rkeyEvent = /^key/, 15 | rmouseEvent = /^(?:mouse|contextmenu)|click/; 16 | 17 | $.fn.simulate = function( type, options ) { 18 | return this.each(function() { 19 | new $.simulate( this, type, options ); 20 | }); 21 | }; 22 | 23 | $.simulate = function( elem, type, options ) { 24 | var method = $.camelCase( "simulate-" + type ); 25 | 26 | this.target = elem; 27 | this.options = options; 28 | 29 | if ( this[ method ] ) { 30 | this[ method ](); 31 | } else { 32 | this.simulateEvent( elem, type, options ); 33 | } 34 | }; 35 | 36 | $.extend( $.simulate, { 37 | 38 | keyCode: { 39 | BACKSPACE: 8, 40 | COMMA: 188, 41 | DELETE: 46, 42 | DOWN: 40, 43 | END: 35, 44 | ENTER: 13, 45 | ESCAPE: 27, 46 | HOME: 36, 47 | LEFT: 37, 48 | NUMPAD_ADD: 107, 49 | NUMPAD_DECIMAL: 110, 50 | NUMPAD_DIVIDE: 111, 51 | NUMPAD_ENTER: 108, 52 | NUMPAD_MULTIPLY: 106, 53 | NUMPAD_SUBTRACT: 109, 54 | PAGE_DOWN: 34, 55 | PAGE_UP: 33, 56 | PERIOD: 190, 57 | RIGHT: 39, 58 | SPACE: 32, 59 | TAB: 9, 60 | UP: 38 61 | }, 62 | 63 | buttonCode: { 64 | LEFT: 0, 65 | MIDDLE: 1, 66 | RIGHT: 2 67 | } 68 | }); 69 | 70 | $.extend( $.simulate.prototype, { 71 | 72 | simulateEvent: function( elem, type, options ) { 73 | var event = this.createEvent( type, options ); 74 | this.dispatchEvent( elem, type, event, options ); 75 | }, 76 | 77 | createEvent: function( type, options ) { 78 | if ( rkeyEvent.test( type ) ) { 79 | return this.keyEvent( type, options ); 80 | } 81 | 82 | if ( rmouseEvent.test( type ) ) { 83 | return this.mouseEvent( type, options ); 84 | } 85 | }, 86 | 87 | mouseEvent: function( type, options ) { 88 | var event, eventDoc, doc, body; 89 | options = $.extend({ 90 | bubbles: true, 91 | cancelable: (type !== "mousemove"), 92 | view: window, 93 | detail: 0, 94 | screenX: 0, 95 | screenY: 0, 96 | clientX: 1, 97 | clientY: 1, 98 | ctrlKey: false, 99 | altKey: false, 100 | shiftKey: false, 101 | metaKey: false, 102 | button: 0, 103 | relatedTarget: undefined 104 | }, options ); 105 | 106 | if ( document.createEvent ) { 107 | event = document.createEvent( "MouseEvents" ); 108 | event.initMouseEvent( type, options.bubbles, options.cancelable, 109 | options.view, options.detail, 110 | options.screenX, options.screenY, options.clientX, options.clientY, 111 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 112 | options.button, options.relatedTarget || document.body.parentNode ); 113 | 114 | // IE 9+ creates events with pageX and pageY set to 0. 115 | // Trying to modify the properties throws an error, 116 | // so we define getters to return the correct values. 117 | if ( event.pageX === 0 && event.pageY === 0 && Object.defineProperty ) { 118 | eventDoc = event.relatedTarget.ownerDocument || document; 119 | doc = eventDoc.documentElement; 120 | body = eventDoc.body; 121 | 122 | Object.defineProperty( event, "pageX", { 123 | get: function() { 124 | return options.clientX + 125 | ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - 126 | ( doc && doc.clientLeft || body && body.clientLeft || 0 ); 127 | } 128 | }); 129 | Object.defineProperty( event, "pageY", { 130 | get: function() { 131 | return options.clientY + 132 | ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - 133 | ( doc && doc.clientTop || body && body.clientTop || 0 ); 134 | } 135 | }); 136 | } 137 | } else if ( document.createEventObject ) { 138 | event = document.createEventObject(); 139 | $.extend( event, options ); 140 | // standards event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ff974877(v=vs.85).aspx 141 | // old IE event.button uses constants defined here: http://msdn.microsoft.com/en-us/library/ie/ms533544(v=vs.85).aspx 142 | // so we actually need to map the standard back to oldIE 143 | event.button = { 144 | 0: 1, 145 | 1: 4, 146 | 2: 2 147 | }[ event.button ] || event.button; 148 | } 149 | 150 | return event; 151 | }, 152 | 153 | keyEvent: function( type, options ) { 154 | var event; 155 | options = $.extend({ 156 | bubbles: true, 157 | cancelable: true, 158 | view: window, 159 | ctrlKey: false, 160 | altKey: false, 161 | shiftKey: false, 162 | metaKey: false, 163 | keyCode: 0, 164 | charCode: undefined 165 | }, options ); 166 | 167 | if ( document.createEvent ) { 168 | try { 169 | event = document.createEvent( "KeyEvents" ); 170 | event.initKeyEvent( type, options.bubbles, options.cancelable, options.view, 171 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 172 | options.keyCode, options.charCode ); 173 | // initKeyEvent throws an exception in WebKit 174 | // see: http://stackoverflow.com/questions/6406784/initkeyevent-keypress-only-works-in-firefox-need-a-cross-browser-solution 175 | // and also https://bugs.webkit.org/show_bug.cgi?id=13368 176 | // fall back to a generic event until we decide to implement initKeyboardEvent 177 | } catch( err ) { 178 | event = document.createEvent( "Events" ); 179 | event.initEvent( type, options.bubbles, options.cancelable ); 180 | $.extend( event, { 181 | view: options.view, 182 | ctrlKey: options.ctrlKey, 183 | altKey: options.altKey, 184 | shiftKey: options.shiftKey, 185 | metaKey: options.metaKey, 186 | keyCode: options.keyCode, 187 | charCode: options.charCode 188 | }); 189 | } 190 | } else if ( document.createEventObject ) { 191 | event = document.createEventObject(); 192 | $.extend( event, options ); 193 | } 194 | 195 | if ( !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ) || (({}).toString.call( window.opera ) === "[object Opera]") ) { 196 | event.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode; 197 | event.charCode = undefined; 198 | } 199 | 200 | return event; 201 | }, 202 | 203 | dispatchEvent: function( elem, type, event ) { 204 | if ( elem.dispatchEvent ) { 205 | elem.dispatchEvent( event ); 206 | } else if ( elem.fireEvent ) { 207 | elem.fireEvent( "on" + type, event ); 208 | } 209 | }, 210 | 211 | simulateFocus: function() { 212 | var focusinEvent, 213 | triggered = false, 214 | element = $( this.target ); 215 | 216 | function trigger() { 217 | triggered = true; 218 | } 219 | 220 | element.bind( "focus", trigger ); 221 | element[ 0 ].focus(); 222 | 223 | if ( !triggered ) { 224 | focusinEvent = $.Event( "focusin" ); 225 | focusinEvent.preventDefault(); 226 | element.trigger( focusinEvent ); 227 | element.triggerHandler( "focus" ); 228 | } 229 | element.unbind( "focus", trigger ); 230 | }, 231 | 232 | simulateBlur: function() { 233 | var focusoutEvent, 234 | triggered = false, 235 | element = $( this.target ); 236 | 237 | function trigger() { 238 | triggered = true; 239 | } 240 | 241 | element.bind( "blur", trigger ); 242 | element[ 0 ].blur(); 243 | 244 | // blur events are async in IE 245 | setTimeout(function() { 246 | // IE won't let the blur occur if the window is inactive 247 | if ( element[ 0 ].ownerDocument.activeElement === element[ 0 ] ) { 248 | element[ 0 ].ownerDocument.body.focus(); 249 | } 250 | 251 | // Firefox won't trigger events if the window is inactive 252 | // IE doesn't trigger events if we had to manually focus the body 253 | if ( !triggered ) { 254 | focusoutEvent = $.Event( "focusout" ); 255 | focusoutEvent.preventDefault(); 256 | element.trigger( focusoutEvent ); 257 | element.triggerHandler( "blur" ); 258 | } 259 | element.unbind( "blur", trigger ); 260 | }, 1 ); 261 | } 262 | }); 263 | 264 | 265 | 266 | /** complex events **/ 267 | 268 | function findCenter( elem ) { 269 | var offset, 270 | document = $( elem.ownerDocument ); 271 | elem = $( elem ); 272 | offset = elem.offset(); 273 | 274 | return { 275 | x: offset.left + elem.outerWidth() / 2 - document.scrollLeft(), 276 | y: offset.top + elem.outerHeight() / 2 - document.scrollTop() 277 | }; 278 | } 279 | 280 | function findCorner( elem ) { 281 | var offset, 282 | document = $( elem.ownerDocument ); 283 | elem = $( elem ); 284 | offset = elem.offset(); 285 | 286 | return { 287 | x: offset.left - document.scrollLeft(), 288 | y: offset.top - document.scrollTop() 289 | }; 290 | } 291 | 292 | $.extend( $.simulate.prototype, { 293 | simulateDrag: function() { 294 | var i = 0, 295 | target = this.target, 296 | options = this.options, 297 | center = options.handle === "corner" ? findCorner( target ) : findCenter( target ), 298 | x = Math.floor( center.x ), 299 | y = Math.floor( center.y ), 300 | coord = { clientX: x, clientY: y }, 301 | dx = options.dx || ( options.x !== undefined ? options.x - x : 0 ), 302 | dy = options.dy || ( options.y !== undefined ? options.y - y : 0 ), 303 | moves = options.moves || 3; 304 | 305 | this.simulateEvent( target, "mousedown", coord ); 306 | 307 | for ( ; i < moves ; i++ ) { 308 | x += dx / moves; 309 | y += dy / moves; 310 | 311 | coord = { 312 | clientX: Math.round( x ), 313 | clientY: Math.round( y ) 314 | }; 315 | 316 | this.simulateEvent( document, "mousemove", coord ); 317 | } 318 | 319 | if ( $.contains( document, target ) ) { 320 | this.simulateEvent( target, "mouseup", coord ); 321 | this.simulateEvent( target, "click", coord ); 322 | } else { 323 | this.simulateEvent( document, "mouseup", coord ); 324 | } 325 | } 326 | }); 327 | 328 | })( jQuery ); 329 | -------------------------------------------------------------------------------- /src/test/bdd/vendor/jquery.simulate.key-sequence.js: -------------------------------------------------------------------------------- 1 | /*jshint camelcase:true, plusplus:true, forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, browser:true, devel:true, maxerr:100, white:false, onevar:false */ 2 | /*jslint white: true vars: true browser: true todo: true */ 3 | /*global jQuery:true $:true bililiteRange:true */ 4 | 5 | /* jQuery Simulate Key-Sequence Plugin 1.1.4 6 | * http://github.com/j-ulrich/jquery-simulate-ext 7 | * 8 | * Copyright (c) 2013 Jochen Ulrich 9 | * Licensed under the MIT license (MIT-LICENSE.txt). 10 | * 11 | * The plugin is an extension and modification of the jQuery sendkeys plugin by Daniel Wachsstock. 12 | * Therefore, the original copyright notice and license follow below. 13 | */ 14 | 15 | // insert characters in a textarea or text input field 16 | // special characters are enclosed in {}; use {{} for the { character itself 17 | // documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/ 18 | // Version: 2.0 19 | // Copyright (c) 2010 Daniel Wachsstock 20 | // MIT license: 21 | // Permission is hereby granted, free of charge, to any person 22 | // obtaining a copy of this software and associated documentation 23 | // files (the "Software"), to deal in the Software without 24 | // restriction, including without limitation the rights to use, 25 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 26 | // copies of the Software, and to permit persons to whom the 27 | // Software is furnished to do so, subject to the following 28 | // conditions: 29 | // 30 | // The above copyright notice and this permission notice shall be 31 | // included in all copies or substantial portions of the Software. 32 | // 33 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 34 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 35 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 36 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 37 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 38 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 39 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 40 | // OTHER DEALINGS IN THE SOFTWARE. 41 | 42 | ;(function($){ 43 | "use strict"; 44 | 45 | $.extend($.simulate.prototype, 46 | 47 | /** 48 | * @lends $.simulate.prototype 49 | */ 50 | { 51 | 52 | /** 53 | * Simulates sequencial key strokes. 54 | * 55 | * @see https://github.com/j-ulrich/jquery-simulate-ext/blob/master/doc/key-sequence.md 56 | * @public 57 | * @author Daniel Wachsstock, julrich 58 | * @since 1.0 59 | */ 60 | simulateKeySequence: function() { 61 | var target = this.target, 62 | opts = $.extend({ 63 | sequence: "", 64 | triggerKeyEvents: true, 65 | delay: 0, 66 | callback: undefined 67 | }, this.options), 68 | sequence = opts.sequence; 69 | 70 | opts.delay = parseInt(opts.delay,10); 71 | 72 | var localkeys = $.extend({}, opts, $(target).data('simulate-keySequence')); // allow for element-specific key functions 73 | // most elements to not keep track of their selection when they lose focus, so we have to do it for them 74 | var rng = $.data (target, 'simulate-keySequence.selection'); 75 | if (!rng){ 76 | rng = bililiteRange(target).bounds('selection'); 77 | $.data(target, 'simulate-keySequence.selection', rng); 78 | $(target).bind('mouseup.simulate-keySequence', function(){ 79 | // we have to update the saved range. The routines here update the bounds with each press, but actual keypresses and mouseclicks do not 80 | $.data(target, 'simulate-keySequence.selection').bounds('selection'); 81 | }).bind('keyup.simulate-keySequence', function(evt){ 82 | // restore the selection if we got here with a tab (a click should select what was clicked on) 83 | if (evt.which === 9){ 84 | // there's a flash of selection when we restore the focus, but I don't know how to avoid that. 85 | $.data(target, 'simulate-keySequence.selection').select(); 86 | }else{ 87 | $.data(target, 'simulate-keySequence.selection').bounds('selection'); 88 | } 89 | }); 90 | } 91 | target.focus(); 92 | if (typeof sequence === 'undefined') { // no string, so we just set up the event handlers 93 | return; 94 | } 95 | sequence = sequence.replace(/\n/g, '{enter}'); // turn line feeds into explicit break insertions 96 | 97 | /** 98 | * Informs the rest of the world that the sequences is finished. 99 | * @fires simulate-keySequence 100 | * @requires target 101 | * @requires sequence 102 | * @requires opts 103 | * @inner 104 | * @author julrich 105 | * @since 1.0 106 | */ 107 | function sequenceFinished() { 108 | $(target).trigger({type: 'simulate-keySequence', sequence: sequence}); 109 | if ($.isFunction(opts.callback)) { 110 | opts.callback.apply(target, [{ 111 | sequence: sequence 112 | }]); 113 | } 114 | } 115 | 116 | /** 117 | * Simulates the key stroke for one character (or special sequence) and sleeps for 118 | * opts.delay milliseconds. 119 | * @requires lastTime 120 | * @requires now() 121 | * @requires tokenRegExp 122 | * @requires opts 123 | * @requires rng 124 | * @inner 125 | * @author julrich 126 | * @since 1.0 127 | */ 128 | function processNextToken() { 129 | var timeElapsed = now() - lastTime; // Work-around for Firefox "bug": setTimeout can fire before the timeout 130 | if (timeElapsed >= opts.delay) { 131 | var match = tokenRegExp.exec(sequence); 132 | if ( match !== null ) { 133 | var s = match[0]; 134 | (localkeys[s] || $.simulate.prototype.simulateKeySequence.defaults[s] || $.simulate.prototype.simulateKeySequence.defaults.simplechar)(rng, s, opts); 135 | setTimeout(processNextToken, opts.delay); 136 | } 137 | else { 138 | sequenceFinished(); 139 | } 140 | lastTime = now(); 141 | } 142 | else { 143 | setTimeout(processNextToken, opts.delay - timeElapsed); 144 | } 145 | } 146 | 147 | if (!opts.delay || opts.delay <= 0) { 148 | // Run as fast as possible 149 | sequence.replace(/\{[^}]*\}|[^{]+/g, function(s){ 150 | (localkeys[s] || $.simulate.prototype.simulateKeySequence.defaults[s] || $.simulate.prototype.simulateKeySequence.defaults.simplechar)(rng, s, opts); 151 | }); 152 | sequenceFinished(); 153 | } 154 | else { 155 | var tokenRegExp = /\{[^}]*\}|[^{]/g; // This matches curly bracket expressions or single characters 156 | var now = Date.now || function() { return new Date().getTime(); }, 157 | lastTime = now(); 158 | 159 | processNextToken(); 160 | } 161 | 162 | } 163 | }); 164 | 165 | $.extend($.simulate.prototype.simulateKeySequence.prototype, 166 | 167 | /** 168 | * @lends $.simulate.prototype.simulateKeySequence.prototype 169 | */ 170 | { 171 | 172 | /** 173 | * Maps special character char codes to IE key codes (covers IE and Webkit) 174 | * @author julrich 175 | * @since 1.0 176 | */ 177 | IEKeyCodeTable: { 178 | 33: 49, // ! -> 1 179 | 64: 50, // @ -> 2 180 | 35: 51, // # -> 3 181 | 36: 52, // $ -> 4 182 | 37: 53, // % -> 5 183 | 94: 54, // ^ -> 6 184 | 38: 55, // & -> 7 185 | 42: 56, // * -> 8 186 | 40: 57, // ( -> 9 187 | 41: 48, // ) -> 0 188 | 189 | 59: 186, // ; -> 186 190 | 58: 186, // : -> 186 191 | 61: 187, // = -> 187 192 | 43: 187, // + -> 187 193 | 44: 188, // , -> 188 194 | 60: 188, // < -> 188 195 | 45: 189, // - -> 189 196 | 95: 189, // _ -> 189 197 | 46: 190, // . -> 190 198 | 62: 190, // > -> 190 199 | 47: 191, // / -> 191 200 | 63: 191, // ? -> 191 201 | 96: 192, // ` -> 192 202 | 126: 192, // ~ -> 192 203 | 91: 219, // [ -> 219 204 | 123: 219, // { -> 219 205 | 92: 220, // \ -> 220 206 | 124: 220, // | -> 220 207 | 93: 221, // ] -> 221 208 | 125: 221, // } -> 221 209 | 39: 222, // ' -> 222 210 | 34: 222 // " -> 222 211 | }, 212 | 213 | /** 214 | * Tries to convert character codes to key codes. 215 | * @param {Numeric} character - A character code 216 | * @returns {Numeric} The key code corresponding to the given character code, 217 | * based on the key code table of InternetExplorer. If no corresponding key code 218 | * could be found (which will be the case for all special characters except the common 219 | * ones), the character code itself is returned. However, keyCode === charCode 220 | * does not imply that no key code was found because some key codes are identical to the 221 | * character codes (e.g. for uppercase characters). 222 | * @requires $.simulate.prototype.simulateKeySequence.prototype.IEKeyCodeTable 223 | * @see $.simulate.prototype.simulateKeySequence.prototype.IEKeyCodeTable 224 | * @author julrich 225 | * @since 1.0 226 | */ 227 | charToKeyCode: function(character) { 228 | var specialKeyCodeTable = $.simulate.prototype.simulateKeySequence.prototype.IEKeyCodeTable; 229 | var charCode = character.charCodeAt(0); 230 | 231 | if (charCode >= 64 && charCode <= 90 || charCode >= 48 && charCode <= 57) { 232 | // A-Z and 0-9 233 | return charCode; 234 | } 235 | else if (charCode >= 97 && charCode <= 122) { 236 | // a-z -> A-Z 237 | return character.toUpperCase().charCodeAt(0); 238 | } 239 | else if (specialKeyCodeTable[charCode] !== undefined) { 240 | return specialKeyCodeTable[charCode]; 241 | } 242 | else { 243 | return charCode; 244 | } 245 | } 246 | }); 247 | 248 | // add the functions publicly so they can be overridden 249 | $.simulate.prototype.simulateKeySequence.defaults = { 250 | 251 | /** 252 | * Simulates key strokes of "normal" characters (i.e. non-special sequences). 253 | * @param {Object} rng - bililiteRange object of the simulation target element. 254 | * @param {String} s - String of (simple) characters to be simulated. 255 | * @param {Object} opts - The key-sequence options. 256 | * @author Daniel Wachsstock, julrich 257 | * @since 1.0 258 | */ 259 | simplechar: function (rng, s, opts){ 260 | rng.text(s, 'end'); 261 | if (opts.triggerKeyEvents) { 262 | for (var i =0; i < s.length; i += 1){ 263 | var charCode = s.charCodeAt(i); 264 | var keyCode = $.simulate.prototype.simulateKeySequence.prototype.charToKeyCode(s.charAt(i)); 265 | // a bit of cheating: rng._el is the element associated with rng. 266 | $(rng._el).simulate('keydown', {keyCode: keyCode}); 267 | $(rng._el).simulate('keypress', {keyCode: charCode, which: charCode, charCode: charCode}); 268 | $(rng._el).simulate('keyup', {keyCode: keyCode}); 269 | } 270 | } 271 | }, 272 | 273 | /** 274 | * Simulates key strokes of a curly opening bracket. 275 | * @param {Object} rng - bililiteRange object of the simulation target element. 276 | * @param {String} s - Ignored. 277 | * @param {Object} opts - The key-sequence options. 278 | * @author Daniel Wachsstock, julrich 279 | * @since 1.0 280 | */ 281 | '{{}': function (rng, s, opts){ 282 | $.simulate.prototype.simulateKeySequence.defaults.simplechar(rng, '{', opts); 283 | }, 284 | 285 | /** 286 | * Simulates hitting the enter button. 287 | * @param {Object} rng - bililiteRange object of the simulation target element. 288 | * @param {String} s - Ignored. 289 | * @param {Object} opts - The key-sequence options. 290 | * @author Daniel Wachsstock, julrich 291 | * @since 1.0 292 | */ 293 | '{enter}': function (rng, s, opts){ 294 | rng.insertEOL(); 295 | rng.select(); 296 | if (opts.triggerKeyEvents === true) { 297 | $(rng._el).simulate('keydown', {keyCode: 13}); 298 | $(rng._el).simulate('keypress', {keyCode: 13, which: 13, charCode: 13}); 299 | $(rng._el).simulate('keyup', {keyCode: 13}); 300 | } 301 | }, 302 | 303 | /** 304 | * Simulates hitting the backspace button. 305 | * @param {Object} rng - bililiteRange object of the simulation target element. 306 | * @param {String} s - Ignored. 307 | * @param {Object} opts - The key-sequence options. 308 | * @author Daniel Wachsstock, julrich 309 | * @since 1.0 310 | */ 311 | '{backspace}': function (rng, s, opts){ 312 | var b = rng.bounds(); 313 | if (b[0] === b[1]) { rng.bounds([b[0]-1, b[0]]); } // no characters selected; it's just an insertion point. Remove the previous character 314 | rng.text('', 'end'); // delete the characters and update the selection 315 | if (opts.triggerKeyEvents === true) { 316 | $(rng._el).simulate('keydown', {keyCode: 8}); 317 | $(rng._el).simulate('keyup', {keyCode: 8}); 318 | } 319 | }, 320 | 321 | /** 322 | * Simulates hitting the delete button. 323 | * @param {Object} rng - bililiteRange object of the simulation target element. 324 | * @param {String} s - Ignored. 325 | * @param {Object} opts - The key-sequence options. 326 | * @author Daniel Wachsstock, julrich 327 | * @since 1.0 328 | */ 329 | '{del}': function (rng, s, opts){ 330 | var b = rng.bounds(); 331 | if (b[0] === b[1]) { rng.bounds([b[0], b[0]+1]); } // no characters selected; it's just an insertion point. Remove the next character 332 | rng.text('', 'end'); // delete the characters and update the selection 333 | if (opts.triggerKeyEvents === true) { 334 | $(rng._el).simulate('keydown', {keyCode: 46}); 335 | $(rng._el).simulate('keyup', {keyCode: 46}); 336 | } 337 | }, 338 | 339 | /** 340 | * Simulates hitting the right arrow button. 341 | * @param {Object} rng - bililiteRange object of the simulation target element. 342 | * @param {String} s - Ignored. 343 | * @param {Object} opts - The key-sequence options. 344 | * @author Daniel Wachsstock, julrich 345 | * @since 1.0 346 | */ 347 | '{rightarrow}': function (rng, s, opts){ 348 | var b = rng.bounds(); 349 | if (b[0] === b[1]) { b[1] += 1; } // no characters selected; it's just an insertion point. Move to the right 350 | rng.bounds([b[1], b[1]]).select(); 351 | if (opts.triggerKeyEvents === true) { 352 | $(rng._el).simulate('keydown', {keyCode: 39}); 353 | $(rng._el).simulate('keyup', {keyCode: 39}); 354 | } 355 | }, 356 | 357 | /** 358 | * Simulates hitting the left arrow button. 359 | * @param {Object} rng - bililiteRange object of the simulation target element. 360 | * @param {String} s - Ignored. 361 | * @param {Object} opts - The key-sequence options. 362 | * @author Daniel Wachsstock, julrich 363 | * @since 1.0 364 | */ 365 | '{leftarrow}': function (rng, s, opts){ 366 | var b = rng.bounds(); 367 | if (b[0] === b[1]) { b[0] -= 1; } // no characters selected; it's just an insertion point. Move to the left 368 | rng.bounds([b[0], b[0]]).select(); 369 | if (opts.triggerKeyEvents === true) { 370 | $(rng._el).simulate('keydown', {keyCode: 37}); 371 | $(rng._el).simulate('keyup', {keyCode: 37}); 372 | } 373 | }, 374 | 375 | /** 376 | * Selects all characters in the target element. 377 | * @param {Object} rng - bililiteRange object of the simulation target element. 378 | * @author Daniel Wachsstock, julrich 379 | * @since 1.0 380 | */ 381 | '{selectall}' : function (rng){ 382 | rng.bounds('all').select(); 383 | } 384 | }; 385 | 386 | })(jQuery); -------------------------------------------------------------------------------- /vendor/almond.js: -------------------------------------------------------------------------------- 1 | /** 2 | * almond 0.2.5 Copyright (c) 2011-2012, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/jrburke/almond for details 5 | */ 6 | //Going sloppy to avoid 'use strict' string cost, but strict practices should 7 | //be followed. 8 | /*jslint sloppy: true */ 9 | /*global setTimeout: false */ 10 | 11 | var requirejs, require, define; 12 | (function (undef) { 13 | var main, req, makeMap, handlers, 14 | defined = {}, 15 | waiting = {}, 16 | config = {}, 17 | defining = {}, 18 | hasOwn = Object.prototype.hasOwnProperty, 19 | aps = [].slice; 20 | 21 | function hasProp(obj, prop) { 22 | return hasOwn.call(obj, prop); 23 | } 24 | 25 | /** 26 | * Given a relative module name, like ./something, normalize it to 27 | * a real name that can be mapped to a path. 28 | * @param {String} name the relative name 29 | * @param {String} baseName a real name that the name arg is relative 30 | * to. 31 | * @returns {String} normalized name 32 | */ 33 | function normalize(name, baseName) { 34 | var nameParts, nameSegment, mapValue, foundMap, 35 | foundI, foundStarMap, starI, i, j, part, 36 | baseParts = baseName && baseName.split("/"), 37 | map = config.map, 38 | starMap = (map && map['*']) || {}; 39 | 40 | //Adjust any relative paths. 41 | if (name && name.charAt(0) === ".") { 42 | //If have a base name, try to normalize against it, 43 | //otherwise, assume it is a top-level require that will 44 | //be relative to baseUrl in the end. 45 | if (baseName) { 46 | //Convert baseName to array, and lop off the last part, 47 | //so that . matches that "directory" and not name of the baseName's 48 | //module. For instance, baseName of "one/two/three", maps to 49 | //"one/two/three.js", but we want the directory, "one/two" for 50 | //this normalization. 51 | baseParts = baseParts.slice(0, baseParts.length - 1); 52 | 53 | name = baseParts.concat(name.split("/")); 54 | 55 | //start trimDots 56 | for (i = 0; i < name.length; i += 1) { 57 | part = name[i]; 58 | if (part === ".") { 59 | name.splice(i, 1); 60 | i -= 1; 61 | } else if (part === "..") { 62 | if (i === 1 && (name[2] === '..' || name[0] === '..')) { 63 | //End of the line. Keep at least one non-dot 64 | //path segment at the front so it can be mapped 65 | //correctly to disk. Otherwise, there is likely 66 | //no path mapping for a path starting with '..'. 67 | //This can still fail, but catches the most reasonable 68 | //uses of .. 69 | break; 70 | } else if (i > 0) { 71 | name.splice(i - 1, 2); 72 | i -= 2; 73 | } 74 | } 75 | } 76 | //end trimDots 77 | 78 | name = name.join("/"); 79 | } else if (name.indexOf('./') === 0) { 80 | // No baseName, so this is ID is resolved relative 81 | // to baseUrl, pull off the leading dot. 82 | name = name.substring(2); 83 | } 84 | } 85 | 86 | //Apply map config if available. 87 | if ((baseParts || starMap) && map) { 88 | nameParts = name.split('/'); 89 | 90 | for (i = nameParts.length; i > 0; i -= 1) { 91 | nameSegment = nameParts.slice(0, i).join("/"); 92 | 93 | if (baseParts) { 94 | //Find the longest baseName segment match in the config. 95 | //So, do joins on the biggest to smallest lengths of baseParts. 96 | for (j = baseParts.length; j > 0; j -= 1) { 97 | mapValue = map[baseParts.slice(0, j).join('/')]; 98 | 99 | //baseName segment has config, find if it has one for 100 | //this name. 101 | if (mapValue) { 102 | mapValue = mapValue[nameSegment]; 103 | if (mapValue) { 104 | //Match, update name to the new value. 105 | foundMap = mapValue; 106 | foundI = i; 107 | break; 108 | } 109 | } 110 | } 111 | } 112 | 113 | if (foundMap) { 114 | break; 115 | } 116 | 117 | //Check for a star map match, but just hold on to it, 118 | //if there is a shorter segment match later in a matching 119 | //config, then favor over this star map. 120 | if (!foundStarMap && starMap && starMap[nameSegment]) { 121 | foundStarMap = starMap[nameSegment]; 122 | starI = i; 123 | } 124 | } 125 | 126 | if (!foundMap && foundStarMap) { 127 | foundMap = foundStarMap; 128 | foundI = starI; 129 | } 130 | 131 | if (foundMap) { 132 | nameParts.splice(0, foundI, foundMap); 133 | name = nameParts.join('/'); 134 | } 135 | } 136 | 137 | return name; 138 | } 139 | 140 | function makeRequire(relName, forceSync) { 141 | return function () { 142 | //A version of a require function that passes a moduleName 143 | //value for items that may need to 144 | //look up paths relative to the moduleName 145 | return req.apply(undef, aps.call(arguments, 0).concat([relName, forceSync])); 146 | }; 147 | } 148 | 149 | function makeNormalize(relName) { 150 | return function (name) { 151 | return normalize(name, relName); 152 | }; 153 | } 154 | 155 | function makeLoad(depName) { 156 | return function (value) { 157 | defined[depName] = value; 158 | }; 159 | } 160 | 161 | function callDep(name) { 162 | if (hasProp(waiting, name)) { 163 | var args = waiting[name]; 164 | delete waiting[name]; 165 | defining[name] = true; 166 | main.apply(undef, args); 167 | } 168 | 169 | if (!hasProp(defined, name) && !hasProp(defining, name)) { 170 | throw new Error('No ' + name); 171 | } 172 | return defined[name]; 173 | } 174 | 175 | //Turns a plugin!resource to [plugin, resource] 176 | //with the plugin being undefined if the name 177 | //did not have a plugin prefix. 178 | function splitPrefix(name) { 179 | var prefix, 180 | index = name ? name.indexOf('!') : -1; 181 | if (index > -1) { 182 | prefix = name.substring(0, index); 183 | name = name.substring(index + 1, name.length); 184 | } 185 | return [prefix, name]; 186 | } 187 | 188 | /** 189 | * Makes a name map, normalizing the name, and using a plugin 190 | * for normalization if necessary. Grabs a ref to plugin 191 | * too, as an optimization. 192 | */ 193 | makeMap = function (name, relName) { 194 | var plugin, 195 | parts = splitPrefix(name), 196 | prefix = parts[0]; 197 | 198 | name = parts[1]; 199 | 200 | if (prefix) { 201 | prefix = normalize(prefix, relName); 202 | plugin = callDep(prefix); 203 | } 204 | 205 | //Normalize according 206 | if (prefix) { 207 | if (plugin && plugin.normalize) { 208 | name = plugin.normalize(name, makeNormalize(relName)); 209 | } else { 210 | name = normalize(name, relName); 211 | } 212 | } else { 213 | name = normalize(name, relName); 214 | parts = splitPrefix(name); 215 | prefix = parts[0]; 216 | name = parts[1]; 217 | if (prefix) { 218 | plugin = callDep(prefix); 219 | } 220 | } 221 | 222 | //Using ridiculous property names for space reasons 223 | return { 224 | f: prefix ? prefix + '!' + name : name, //fullName 225 | n: name, 226 | pr: prefix, 227 | p: plugin 228 | }; 229 | }; 230 | 231 | function makeConfig(name) { 232 | return function () { 233 | return (config && config.config && config.config[name]) || {}; 234 | }; 235 | } 236 | 237 | handlers = { 238 | require: function (name) { 239 | return makeRequire(name); 240 | }, 241 | exports: function (name) { 242 | var e = defined[name]; 243 | if (typeof e !== 'undefined') { 244 | return e; 245 | } else { 246 | return (defined[name] = {}); 247 | } 248 | }, 249 | module: function (name) { 250 | return { 251 | id: name, 252 | uri: '', 253 | exports: defined[name], 254 | config: makeConfig(name) 255 | }; 256 | } 257 | }; 258 | 259 | main = function (name, deps, callback, relName) { 260 | var cjsModule, depName, ret, map, i, 261 | args = [], 262 | usingExports; 263 | 264 | //Use name if no relName 265 | relName = relName || name; 266 | 267 | //Call the callback to define the module, if necessary. 268 | if (typeof callback === 'function') { 269 | 270 | //Pull out the defined dependencies and pass the ordered 271 | //values to the callback. 272 | //Default to [require, exports, module] if no deps 273 | deps = !deps.length && callback.length ? ['require', 'exports', 'module'] : deps; 274 | for (i = 0; i < deps.length; i += 1) { 275 | map = makeMap(deps[i], relName); 276 | depName = map.f; 277 | 278 | //Fast path CommonJS standard dependencies. 279 | if (depName === "require") { 280 | args[i] = handlers.require(name); 281 | } else if (depName === "exports") { 282 | //CommonJS module spec 1.1 283 | args[i] = handlers.exports(name); 284 | usingExports = true; 285 | } else if (depName === "module") { 286 | //CommonJS module spec 1.1 287 | cjsModule = args[i] = handlers.module(name); 288 | } else if (hasProp(defined, depName) || 289 | hasProp(waiting, depName) || 290 | hasProp(defining, depName)) { 291 | args[i] = callDep(depName); 292 | } else if (map.p) { 293 | map.p.load(map.n, makeRequire(relName, true), makeLoad(depName), {}); 294 | args[i] = defined[depName]; 295 | } else { 296 | throw new Error(name + ' missing ' + depName); 297 | } 298 | } 299 | 300 | ret = callback.apply(defined[name], args); 301 | 302 | if (name) { 303 | //If setting exports via "module" is in play, 304 | //favor that over return value and exports. After that, 305 | //favor a non-undefined return value over exports use. 306 | if (cjsModule && cjsModule.exports !== undef && 307 | cjsModule.exports !== defined[name]) { 308 | defined[name] = cjsModule.exports; 309 | } else if (ret !== undef || !usingExports) { 310 | //Use the return value from the function. 311 | defined[name] = ret; 312 | } 313 | } 314 | } else if (name) { 315 | //May just be an object definition for the module. Only 316 | //worry about defining if have a module name. 317 | defined[name] = callback; 318 | } 319 | }; 320 | 321 | requirejs = require = req = function (deps, callback, relName, forceSync, alt) { 322 | if (typeof deps === "string") { 323 | if (handlers[deps]) { 324 | //callback in this case is really relName 325 | return handlers[deps](callback); 326 | } 327 | //Just return the module wanted. In this scenario, the 328 | //deps arg is the module name, and second arg (if passed) 329 | //is just the relName. 330 | //Normalize module name, if it contains . or .. 331 | return callDep(makeMap(deps, callback).f); 332 | } else if (!deps.splice) { 333 | //deps is a config object, not an array. 334 | config = deps; 335 | if (callback.splice) { 336 | //callback is an array, which means it is a dependency list. 337 | //Adjust args if there are dependencies 338 | deps = callback; 339 | callback = relName; 340 | relName = null; 341 | } else { 342 | deps = undef; 343 | } 344 | } 345 | 346 | //Support require(['a']) 347 | callback = callback || function () {}; 348 | 349 | //If relName is a function, it is an errback handler, 350 | //so remove it. 351 | if (typeof relName === 'function') { 352 | relName = forceSync; 353 | forceSync = alt; 354 | } 355 | 356 | //Simulate async callback; 357 | if (forceSync) { 358 | main(undef, deps, callback, relName); 359 | } else { 360 | //Using a non-zero value because of concern for what old browsers 361 | //do, and latest browsers "upgrade" to 4 if lower value is used: 362 | //http://www.whatwg.org/specs/web-apps/current-work/multipage/timers.html#dom-windowtimers-settimeout: 363 | //If want a value immediately, use require('id') instead -- something 364 | //that works in almond on the global level, but not guaranteed and 365 | //unlikely to work in other AMD implementations. 366 | setTimeout(function () { 367 | main(undef, deps, callback, relName); 368 | }, 4); 369 | } 370 | 371 | return req; 372 | }; 373 | 374 | /** 375 | * Just drops the config on the floor, but returns req in case 376 | * the config return value is used. 377 | */ 378 | req.config = function (cfg) { 379 | config = cfg; 380 | if (config.deps) { 381 | req(config.deps, config.callback); 382 | } 383 | return req; 384 | }; 385 | 386 | define = function (name, deps, callback) { 387 | 388 | //This module may not have dependencies 389 | if (!deps.splice) { 390 | //deps is not an array, so probably means 391 | //an object literal or factory function for 392 | //the value. Adjust args. 393 | callback = deps; 394 | deps = []; 395 | } 396 | 397 | if (!hasProp(defined, name) && !hasProp(waiting, name)) { 398 | waiting[name] = [name, deps, callback]; 399 | } 400 | }; 401 | 402 | define.amd = { 403 | jQuery: true 404 | }; 405 | }()); 406 | -------------------------------------------------------------------------------- /src/test/bdd/vendor/bililiteRange.js: -------------------------------------------------------------------------------- 1 | // Cross-broswer implementation of text ranges and selections 2 | // documentation: http://bililite.com/blog/2011/01/17/cross-browser-text-ranges-and-selections/ 3 | // Version: 1.1 4 | // Copyright (c) 2010 Daniel Wachsstock 5 | // MIT license: 6 | // Permission is hereby granted, free of charge, to any person 7 | // obtaining a copy of this software and associated documentation 8 | // files (the "Software"), to deal in the Software without 9 | // restriction, including without limitation the rights to use, 10 | // copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the 12 | // Software is furnished to do so, subject to the following 13 | // conditions: 14 | 15 | // The above copyright notice and this permission notice shall be 16 | // included in all copies or substantial portions of the Software. 17 | 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | // OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | (function () { 28 | bililiteRange = function (el, debug) { 29 | var ret; 30 | if (debug) { 31 | ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser 32 | } else if (document.selection) { 33 | // Internet Explorer 34 | ret = new IERange(); 35 | } else if (window.getSelection && el.setSelectionRange) { 36 | // Standards. Element is an input or textarea 37 | ret = new InputRange(); 38 | } else if (window.getSelection) { 39 | // Standards, with any other kind of element 40 | ret = new W3CRange(); 41 | } else { 42 | // doesn't support selection 43 | ret = new NothingRange(); 44 | } 45 | ret._el = el; 46 | ret._textProp = textProp(el); 47 | ret._bounds = [0, ret.length()]; 48 | return ret; 49 | }; 50 | 51 | function textProp(el) { 52 | // returns the property that contains the text of the element 53 | if (typeof el.value != 'undefined') return 'value'; 54 | if (typeof el.text != 'undefined') return 'text'; 55 | if (typeof el.textContent != 'undefined') return 'textContent'; 56 | return 'innerText'; 57 | } 58 | 59 | // base class 60 | function Range() { 61 | } 62 | 63 | Range.prototype = { 64 | length: function () { 65 | return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness 66 | }, 67 | bounds: function (s) { 68 | if (s === 'all') { 69 | this._bounds = [0, this.length()]; 70 | } else if (s === 'start') { 71 | this._bounds = [0, 0]; 72 | } else if (s === 'end') { 73 | this._bounds = [this.length(), this.length()]; 74 | } else if (s === 'selection') { 75 | this.bounds('all'); // first select the whole thing for constraining 76 | this._bounds = this._nativeSelection(); 77 | } else if (s) { 78 | this._bounds = s; // don't error check now; the element may change at any moment, so constrain it when we need it. 79 | } else { 80 | var b = [ 81 | Math.max(0, Math.min(this.length(), this._bounds[0])), 82 | Math.max(0, Math.min(this.length(), this._bounds[1])) 83 | ]; 84 | return b; // need to constrain it to fit 85 | } 86 | return this; // allow for chaining 87 | }, 88 | select: function () { 89 | this._nativeSelect(this._nativeRange(this.bounds())); 90 | return this; // allow for chaining 91 | }, 92 | text: function (text, select) { 93 | if (arguments.length) { 94 | this._nativeSetText(text, this._nativeRange(this.bounds())); 95 | if (select == 'start') { 96 | this.bounds([this._bounds[0], this._bounds[0]]); 97 | this.select(); 98 | } else if (select == 'end') { 99 | this.bounds([this._bounds[0] + text.length, this._bounds[0] + text.length]); 100 | this.select(); 101 | } else if (select == 'all') { 102 | this.bounds([this._bounds[0], this._bounds[0] + text.length]); 103 | this.select(); 104 | } 105 | return this; // allow for chaining 106 | } else { 107 | return this._nativeGetText(this._nativeRange(this.bounds())); 108 | } 109 | }, 110 | insertEOL: function () { 111 | this._nativeEOL(); 112 | this._bounds = [this._bounds[0] + 1, this._bounds[0] + 1]; // move past the EOL marker 113 | return this; 114 | } 115 | }; 116 | 117 | function IERange() { 118 | } 119 | 120 | IERange.prototype = new Range(); 121 | IERange.prototype._nativeRange = function (bounds) { 122 | var rng; 123 | if (this._el.tagName == 'INPUT') { 124 | // IE 8 is very inconsistent; textareas have createTextRange but it doesn't work 125 | rng = this._el.createTextRange(); 126 | } else { 127 | rng = document.body.createTextRange(); 128 | rng.moveToElementText(this._el); 129 | } 130 | if (bounds) { 131 | if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds 132 | if (bounds[0] > this.length()) bounds[0] = this.length(); 133 | if (bounds[1] < rng.text.replace(/\r/g, '').length) { // correct for IE's CrLf wierdness 134 | // block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range 135 | rng.moveEnd('character', -1); 136 | rng.moveEnd('character', bounds[1] - rng.text.replace(/\r/g, '').length); 137 | } 138 | if (bounds[0] > 0) rng.moveStart('character', bounds[0]); 139 | } 140 | return rng; 141 | }; 142 | IERange.prototype._nativeSelect = function (rng) { 143 | rng.select(); 144 | }; 145 | IERange.prototype._nativeSelection = function () { 146 | // returns [start, end] for the selection constrained to be in element 147 | var rng = this._nativeRange(); // range of the element to constrain to 148 | var len = this.length(); 149 | if (document.selection.type != 'Text') return [len, len]; // append to the end 150 | var sel = document.selection.createRange(); 151 | try { 152 | return [ 153 | iestart(sel, rng), 154 | ieend(sel, rng) 155 | ]; 156 | } catch (e) { 157 | // IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess 158 | return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0, 0] : [len, len]; 159 | } 160 | }; 161 | IERange.prototype._nativeGetText = function (rng) { 162 | return rng.text.replace(/\r/g, ''); // correct for IE's CrLf weirdness 163 | }; 164 | IERange.prototype._nativeSetText = function (text, rng) { 165 | rng.text = text; 166 | }; 167 | IERange.prototype._nativeEOL = function () { 168 | if (typeof this._el.value != 'undefined') { 169 | this.text('\n'); // for input and textarea, insert it straight 170 | } else { 171 | this._nativeRange(this.bounds()).pasteHTML('
'); 172 | } 173 | }; 174 | // IE internals 175 | function iestart(rng, constraint) { 176 | // returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after 177 | var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness 178 | if (rng.compareEndPoints('StartToStart', constraint) <= 0) return 0; // at or before the beginning 179 | if (rng.compareEndPoints('StartToEnd', constraint) >= 0) return len; 180 | for (var i = 0; rng.compareEndPoints('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1)); 181 | return i; 182 | } 183 | 184 | function ieend(rng, constraint) { 185 | // returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after 186 | var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf wierdness 187 | if (rng.compareEndPoints('EndToEnd', constraint) >= 0) return len; // at or after the end 188 | if (rng.compareEndPoints('EndToStart', constraint) <= 0) return 0; 189 | for (var i = 0; rng.compareEndPoints('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1)); 190 | return i; 191 | } 192 | 193 | // an input element in a standards document. "Native Range" is just the bounds array 194 | function InputRange() { 195 | } 196 | 197 | InputRange.prototype = new Range(); 198 | InputRange.prototype._nativeRange = function (bounds) { 199 | return bounds || [0, this.length()]; 200 | }; 201 | InputRange.prototype._nativeSelect = function (rng) { 202 | this._el.setSelectionRange(rng[0], rng[1]); 203 | }; 204 | InputRange.prototype._nativeSelection = function () { 205 | return [this._el.selectionStart, this._el.selectionEnd]; 206 | }; 207 | InputRange.prototype._nativeGetText = function (rng) { 208 | return this._el.value.substring(rng[0], rng[1]); 209 | }; 210 | InputRange.prototype._nativeSetText = function (text, rng) { 211 | var val = this._el.value; 212 | this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]); 213 | }; 214 | InputRange.prototype._nativeEOL = function () { 215 | this.text('\n'); 216 | }; 217 | 218 | function W3CRange() { 219 | } 220 | 221 | W3CRange.prototype = new Range(); 222 | W3CRange.prototype._nativeRange = function (bounds) { 223 | var rng = document.createRange(); 224 | rng.selectNodeContents(this._el); 225 | if (bounds) { 226 | w3cmoveBoundary(rng, bounds[0], true, this._el); 227 | rng.collapse(true); 228 | w3cmoveBoundary(rng, bounds[1] - bounds[0], false, this._el); 229 | } 230 | return rng; 231 | }; 232 | W3CRange.prototype._nativeSelect = function (rng) { 233 | window.getSelection().removeAllRanges(); 234 | window.getSelection().addRange(rng); 235 | }; 236 | W3CRange.prototype._nativeSelection = function () { 237 | // returns [start, end] for the selection constrained to be in element 238 | var rng = this._nativeRange(); // range of the element to constrain to 239 | if (window.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end 240 | var sel = window.getSelection().getRangeAt(0); 241 | return [ 242 | w3cstart(sel, rng), 243 | w3cend(sel, rng) 244 | ]; 245 | }; 246 | W3CRange.prototype._nativeGetText = function (rng) { 247 | return rng.toString(); 248 | }; 249 | W3CRange.prototype._nativeSetText = function (text, rng) { 250 | rng.deleteContents(); 251 | rng.insertNode(document.createTextNode(text)); 252 | this._el.normalize(); // merge the text with the surrounding text 253 | }; 254 | W3CRange.prototype._nativeEOL = function () { 255 | var rng = this._nativeRange(this.bounds()); 256 | rng.deleteContents(); 257 | var br = document.createElement('br'); 258 | br.setAttribute('_moz_dirty', ''); // for Firefox 259 | rng.insertNode(br); 260 | rng.insertNode(document.createTextNode('\n')); 261 | rng.collapse(false); 262 | }; 263 | // W3C internals 264 | function nextnode(node, root) { 265 | // in-order traversal 266 | // we've already visited node, so get kids then siblings 267 | if (node.firstChild) return node.firstChild; 268 | if (node.nextSibling) return node.nextSibling; 269 | if (node === root) return null; 270 | while (node.parentNode) { 271 | // get uncles 272 | node = node.parentNode; 273 | if (node == root) return null; 274 | if (node.nextSibling) return node.nextSibling; 275 | } 276 | return null; 277 | } 278 | 279 | function w3cmoveBoundary(rng, n, bStart, el) { 280 | // move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only! 281 | // if the start is moved after the end, then an exception is raised 282 | if (n <= 0) return; 283 | var node = rng[bStart ? 'startContainer' : 'endContainer']; 284 | if (node.nodeType == 3) { 285 | // we may be starting somewhere into the text 286 | n += rng[bStart ? 'startOffset' : 'endOffset']; 287 | } 288 | while (node) { 289 | if (node.nodeType == 3) { 290 | if (n <= node.nodeValue.length) { 291 | rng[bStart ? 'setStart' : 'setEnd'](node, n); 292 | // special case: if we end next to a
, include that node. 293 | if (n == node.nodeValue.length) { 294 | // skip past zero-length text nodes 295 | for (var next = nextnode(node, el); next && next.nodeType == 3 && next.nodeValue.length == 0; next = nextnode(next, el)) { 296 | rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); 297 | } 298 | if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); 299 | } 300 | return; 301 | } else { 302 | rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one 303 | n -= node.nodeValue.length; // and eat these characters 304 | } 305 | } 306 | node = nextnode(node, el); 307 | } 308 | } 309 | 310 | var START_TO_START = 0; // from the w3c definitions 311 | var START_TO_END = 1; 312 | var END_TO_END = 2; 313 | var END_TO_START = 3; 314 | // from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange) 315 | // -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange. 316 | // * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range. 317 | // * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range. 318 | // * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range. 319 | // * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range. 320 | function w3cstart(rng, constraint) { 321 | if (rng.compareBoundaryPoints(START_TO_START, constraint) <= 0) return 0; // at or before the beginning 322 | if (rng.compareBoundaryPoints(END_TO_START, constraint) >= 0) return constraint.toString().length; 323 | rng = rng.cloneRange(); // don't change the original 324 | rng.setEnd(constraint.endContainer, constraint.endOffset); // they now end at the same place 325 | return constraint.toString().length - rng.toString().length; 326 | } 327 | 328 | function w3cend(rng, constraint) { 329 | if (rng.compareBoundaryPoints(END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end 330 | if (rng.compareBoundaryPoints(START_TO_END, constraint) <= 0) return 0; 331 | rng = rng.cloneRange(); // don't change the original 332 | rng.setStart(constraint.startContainer, constraint.startOffset); // they now start at the same place 333 | return rng.toString().length; 334 | } 335 | 336 | function NothingRange() { 337 | } 338 | NothingRange.prototype = new Range(); 339 | NothingRange.prototype._nativeRange = function (bounds) { 340 | return bounds || [0, this.length()]; 341 | }; 342 | NothingRange.prototype._nativeSelect = function (rng) { // do nothing 343 | }; 344 | NothingRange.prototype._nativeSelection = function () { 345 | return [0, 0]; 346 | }; 347 | NothingRange.prototype._nativeGetText = function (rng) { 348 | return this._el[this._textProp].substring(rng[0], rng[1]); 349 | }; 350 | NothingRange.prototype._nativeSetText = function (text, rng) { 351 | var val = this._el[this._textProp]; 352 | this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]); 353 | }; 354 | NothingRange.prototype._nativeEOL = function () { 355 | this.text('\n'); 356 | }; 357 | })(); -------------------------------------------------------------------------------- /vendor/zepto-1.0.min.js: -------------------------------------------------------------------------------- 1 | /* Zepto v1.0-1-ga3cab6c - polyfill zepto detect event ajax form fx - zeptojs.com/license */ 2 | (function(a){String.prototype.trim===a&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),Array.prototype.reduce===a&&(Array.prototype.reduce=function(b){if(this===void 0||this===null)throw new TypeError;var c=Object(this),d=c.length>>>0,e=0,f;if(typeof b!="function")throw new TypeError;if(d==0&&arguments.length==1)throw new TypeError;if(arguments.length>=2)f=arguments[1];else do{if(e in c){f=c[e++];break}if(++e>=d)throw new TypeError}while(!0);while(e0?c.fn.concat.apply([],a):a}function O(a){return a.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function P(a){return a in j?j[a]:j[a]=new RegExp("(^|\\s)"+a+"(\\s|$)")}function Q(a,b){return typeof b=="number"&&!l[O(a)]?b+"px":b}function R(a){var b,c;return i[a]||(b=h.createElement(a),h.body.appendChild(b),c=k(b,"").getPropertyValue("display"),b.parentNode.removeChild(b),c=="none"&&(c="block"),i[a]=c),i[a]}function S(a){return"children"in a?f.call(a.children):c.map(a.childNodes,function(a){if(a.nodeType==1)return a})}function T(c,d,e){for(b in d)e&&(J(d[b])||K(d[b]))?(J(d[b])&&!J(c[b])&&(c[b]={}),K(d[b])&&!K(c[b])&&(c[b]=[]),T(c[b],d[b],e)):d[b]!==a&&(c[b]=d[b])}function U(b,d){return d===a?c(b):c(b).filter(d)}function V(a,b,c,d){return F(b)?b.call(a,c,d):b}function W(a,b,c){c==null?a.removeAttribute(b):a.setAttribute(b,c)}function X(b,c){var d=b.className,e=d&&d.baseVal!==a;if(c===a)return e?d.baseVal:d;e?d.baseVal=c:b.className=c}function Y(a){var b;try{return a?a=="true"||(a=="false"?!1:a=="null"?null:isNaN(b=Number(a))?/^[\[\{]/.test(a)?c.parseJSON(a):a:b):a}catch(d){return a}}function Z(a,b){b(a);for(var c in a.childNodes)Z(a.childNodes[c],b)}var a,b,c,d,e=[],f=e.slice,g=e.filter,h=window.document,i={},j={},k=h.defaultView.getComputedStyle,l={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},m=/^\s*<(\w+|!)[^>]*>/,n=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,o=/^(?:body|html)$/i,p=["val","css","html","text","data","width","height","offset"],q=["after","prepend","before","append"],r=h.createElement("table"),s=h.createElement("tr"),t={tr:h.createElement("tbody"),tbody:r,thead:r,tfoot:r,td:s,th:s,"*":h.createElement("div")},u=/complete|loaded|interactive/,v=/^\.([\w-]+)$/,w=/^#([\w-]*)$/,x=/^[\w-]+$/,y={},z=y.toString,A={},B,C,D=h.createElement("div");return A.matches=function(a,b){if(!a||a.nodeType!==1)return!1;var c=a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.matchesSelector;if(c)return c.call(a,b);var d,e=a.parentNode,f=!e;return f&&(e=D).appendChild(a),d=~A.qsa(e,b).indexOf(a),f&&D.removeChild(a),d},B=function(a){return a.replace(/-+(.)?/g,function(a,b){return b?b.toUpperCase():""})},C=function(a){return g.call(a,function(b,c){return a.indexOf(b)==c})},A.fragment=function(b,d,e){b.replace&&(b=b.replace(n,"<$1>")),d===a&&(d=m.test(b)&&RegExp.$1),d in t||(d="*");var g,h,i=t[d];return i.innerHTML=""+b,h=c.each(f.call(i.childNodes),function(){i.removeChild(this)}),J(e)&&(g=c(h),c.each(e,function(a,b){p.indexOf(a)>-1?g[a](b):g.attr(a,b)})),h},A.Z=function(a,b){return a=a||[],a.__proto__=c.fn,a.selector=b||"",a},A.isZ=function(a){return a instanceof A.Z},A.init=function(b,d){if(!b)return A.Z();if(F(b))return c(h).ready(b);if(A.isZ(b))return b;var e;if(K(b))e=M(b);else if(I(b))e=[J(b)?c.extend({},b):b],b=null;else if(m.test(b))e=A.fragment(b.trim(),RegExp.$1,d),b=null;else{if(d!==a)return c(d).find(b);e=A.qsa(h,b)}return A.Z(e,b)},c=function(a,b){return A.init(a,b)},c.extend=function(a){var b,c=f.call(arguments,1);return typeof a=="boolean"&&(b=a,a=c.shift()),c.forEach(function(c){T(a,c,b)}),a},A.qsa=function(a,b){var c;return H(a)&&w.test(b)?(c=a.getElementById(RegExp.$1))?[c]:[]:a.nodeType!==1&&a.nodeType!==9?[]:f.call(v.test(b)?a.getElementsByClassName(RegExp.$1):x.test(b)?a.getElementsByTagName(b):a.querySelectorAll(b))},c.contains=function(a,b){return a!==b&&a.contains(b)},c.type=E,c.isFunction=F,c.isWindow=G,c.isArray=K,c.isPlainObject=J,c.isEmptyObject=function(a){var b;for(b in a)return!1;return!0},c.inArray=function(a,b,c){return e.indexOf.call(b,a,c)},c.camelCase=B,c.trim=function(a){return a.trim()},c.uuid=0,c.support={},c.expr={},c.map=function(a,b){var c,d=[],e,f;if(L(a))for(e=0;e=0?b:b+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){this.parentNode!=null&&this.parentNode.removeChild(this)})},each:function(a){return e.every.call(this,function(b,c){return a.call(b,c,b)!==!1}),this},filter:function(a){return F(a)?this.not(this.not(a)):c(g.call(this,function(b){return A.matches(b,a)}))},add:function(a,b){return c(C(this.concat(c(a,b))))},is:function(a){return this.length>0&&A.matches(this[0],a)},not:function(b){var d=[];if(F(b)&&b.call!==a)this.each(function(a){b.call(this,a)||d.push(this)});else{var e=typeof b=="string"?this.filter(b):L(b)&&F(b.item)?f.call(b):c(b);this.forEach(function(a){e.indexOf(a)<0&&d.push(a)})}return c(d)},has:function(a){return this.filter(function(){return I(a)?c.contains(this,a):c(this).find(a).size()})},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){var a=this[0];return a&&!I(a)?a:c(a)},last:function(){var a=this[this.length-1];return a&&!I(a)?a:c(a)},find:function(a){var b,d=this;return typeof a=="object"?b=c(a).filter(function(){var a=this;return e.some.call(d,function(b){return c.contains(b,a)})}):this.length==1?b=c(A.qsa(this[0],a)):b=this.map(function(){return A.qsa(this,a)}),b},closest:function(a,b){var d=this[0],e=!1;typeof a=="object"&&(e=c(a));while(d&&!(e?e.indexOf(d)>=0:A.matches(d,a)))d=d!==b&&!H(d)&&d.parentNode;return c(d)},parents:function(a){var b=[],d=this;while(d.length>0)d=c.map(d,function(a){if((a=a.parentNode)&&!H(a)&&b.indexOf(a)<0)return b.push(a),a});return U(b,a)},parent:function(a){return U(C(this.pluck("parentNode")),a)},children:function(a){return U(this.map(function(){return S(this)}),a)},contents:function(){return this.map(function(){return f.call(this.childNodes)})},siblings:function(a){return U(this.map(function(a,b){return g.call(S(b.parentNode),function(a){return a!==b})}),a)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(a){return c.map(this,function(b){return b[a]})},show:function(){return this.each(function(){this.style.display=="none"&&(this.style.display=null),k(this,"").getPropertyValue("display")=="none"&&(this.style.display=R(this.nodeName))})},replaceWith:function(a){return this.before(a).remove()},wrap:function(a){var b=F(a);if(this[0]&&!b)var d=c(a).get(0),e=d.parentNode||this.length>1;return this.each(function(f){c(this).wrapAll(b?a.call(this,f):e?d.cloneNode(!0):d)})},wrapAll:function(a){if(this[0]){c(this[0]).before(a=c(a));var b;while((b=a.children()).length)a=b.first();c(a).append(this)}return this},wrapInner:function(a){var b=F(a);return this.each(function(d){var e=c(this),f=e.contents(),g=b?a.call(this,d):a;f.length?f.wrapAll(g):e.append(g)})},unwrap:function(){return this.parent().each(function(){c(this).replaceWith(c(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(b){return this.each(function(){var d=c(this);(b===a?d.css("display")=="none":b)?d.show():d.hide()})},prev:function(a){return c(this.pluck("previousElementSibling")).filter(a||"*")},next:function(a){return c(this.pluck("nextElementSibling")).filter(a||"*")},html:function(b){return b===a?this.length>0?this[0].innerHTML:null:this.each(function(a){var d=this.innerHTML;c(this).empty().append(V(this,b,a,d))})},text:function(b){return b===a?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=b})},attr:function(c,d){var e;return typeof c=="string"&&d===a?this.length==0||this[0].nodeType!==1?a:c=="value"&&this[0].nodeName=="INPUT"?this.val():!(e=this[0].getAttribute(c))&&c in this[0]?this[0][c]:e:this.each(function(a){if(this.nodeType!==1)return;if(I(c))for(b in c)W(this,b,c[b]);else W(this,c,V(this,d,a,this.getAttribute(c)))})},removeAttr:function(a){return this.each(function(){this.nodeType===1&&W(this,a)})},prop:function(b,c){return c===a?this[0]&&this[0][b]:this.each(function(a){this[b]=V(this,c,a,this[b])})},data:function(b,c){var d=this.attr("data-"+O(b),c);return d!==null?Y(d):a},val:function(b){return b===a?this[0]&&(this[0].multiple?c(this[0]).find("option").filter(function(a){return this.selected}).pluck("value"):this[0].value):this.each(function(a){this.value=V(this,b,a,this.value)})},offset:function(a){if(a)return this.each(function(b){var d=c(this),e=V(this,a,b,d.offset()),f=d.offsetParent().offset(),g={top:e.top-f.top,left:e.left-f.left};d.css("position")=="static"&&(g.position="relative"),d.css(g)});if(this.length==0)return null;var b=this[0].getBoundingClientRect();return{left:b.left+window.pageXOffset,top:b.top+window.pageYOffset,width:Math.round(b.width),height:Math.round(b.height)}},css:function(a,c){if(arguments.length<2&&typeof a=="string")return this[0]&&(this[0].style[B(a)]||k(this[0],"").getPropertyValue(a));var d="";if(E(a)=="string")!c&&c!==0?this.each(function(){this.style.removeProperty(O(a))}):d=O(a)+":"+Q(a,c);else for(b in a)!a[b]&&a[b]!==0?this.each(function(){this.style.removeProperty(O(b))}):d+=O(b)+":"+Q(b,a[b])+";";return this.each(function(){this.style.cssText+=";"+d})},index:function(a){return a?this.indexOf(c(a)[0]):this.parent().children().indexOf(this[0])},hasClass:function(a){return e.some.call(this,function(a){return this.test(X(a))},P(a))},addClass:function(a){return this.each(function(b){d=[];var e=X(this),f=V(this,a,b,e);f.split(/\s+/g).forEach(function(a){c(this).hasClass(a)||d.push(a)},this),d.length&&X(this,e+(e?" ":"")+d.join(" "))})},removeClass:function(b){return this.each(function(c){if(b===a)return X(this,"");d=X(this),V(this,b,c,d).split(/\s+/g).forEach(function(a){d=d.replace(P(a)," ")}),X(this,d.trim())})},toggleClass:function(b,d){return this.each(function(e){var f=c(this),g=V(this,b,e,X(this));g.split(/\s+/g).forEach(function(b){(d===a?!f.hasClass(b):d)?f.addClass(b):f.removeClass(b)})})},scrollTop:function(){if(!this.length)return;return"scrollTop"in this[0]?this[0].scrollTop:this[0].scrollY},position:function(){if(!this.length)return;var a=this[0],b=this.offsetParent(),d=this.offset(),e=o.test(b[0].nodeName)?{top:0,left:0}:b.offset();return d.top-=parseFloat(c(a).css("margin-top"))||0,d.left-=parseFloat(c(a).css("margin-left"))||0,e.top+=parseFloat(c(b[0]).css("border-top-width"))||0,e.left+=parseFloat(c(b[0]).css("border-left-width"))||0,{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||h.body;while(a&&!o.test(a.nodeName)&&c(a).css("position")=="static")a=a.offsetParent;return a})}},c.fn.detach=c.fn.remove,["width","height"].forEach(function(b){c.fn[b]=function(d){var e,f=this[0],g=b.replace(/./,function(a){return a[0].toUpperCase()});return d===a?G(f)?f["inner"+g]:H(f)?f.documentElement["offset"+g]:(e=this.offset())&&e[b]:this.each(function(a){f=c(this),f.css(b,V(this,d,a,f[b]()))})}}),q.forEach(function(a,b){var d=b%2;c.fn[a]=function(){var a,e=c.map(arguments,function(b){return a=E(b),a=="object"||a=="array"||b==null?b:A.fragment(b)}),f,g=this.length>1;return e.length<1?this:this.each(function(a,h){f=d?h:h.parentNode,h=b==0?h.nextSibling:b==1?h.firstChild:b==2?h:null,e.forEach(function(a){if(g)a=a.cloneNode(!0);else if(!f)return c(a).remove();Z(f.insertBefore(a,h),function(a){a.nodeName!=null&&a.nodeName.toUpperCase()==="SCRIPT"&&(!a.type||a.type==="text/javascript")&&!a.src&&window.eval.call(window,a.innerHTML)})})})},c.fn[d?a+"To":"insert"+(b?"Before":"After")]=function(b){return c(b)[a](this),this}}),A.Z.prototype=c.fn,A.uniq=C,A.deserializeValue=Y,c.zepto=A,c}();window.Zepto=Zepto,"$"in window||(window.$=Zepto),function(a){function b(a){var b=this.os={},c=this.browser={},d=a.match(/WebKit\/([\d.]+)/),e=a.match(/(Android)\s+([\d.]+)/),f=a.match(/(iPad).*OS\s([\d_]+)/),g=!f&&a.match(/(iPhone\sOS)\s([\d_]+)/),h=a.match(/(webOS|hpwOS)[\s\/]([\d.]+)/),i=h&&a.match(/TouchPad/),j=a.match(/Kindle\/([\d.]+)/),k=a.match(/Silk\/([\d._]+)/),l=a.match(/(BlackBerry).*Version\/([\d.]+)/),m=a.match(/(BB10).*Version\/([\d.]+)/),n=a.match(/(RIM\sTablet\sOS)\s([\d.]+)/),o=a.match(/PlayBook/),p=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),q=a.match(/Firefox\/([\d.]+)/);if(c.webkit=!!d)c.version=d[1];e&&(b.android=!0,b.version=e[2]),g&&(b.ios=b.iphone=!0,b.version=g[2].replace(/_/g,".")),f&&(b.ios=b.ipad=!0,b.version=f[2].replace(/_/g,".")),h&&(b.webos=!0,b.version=h[2]),i&&(b.touchpad=!0),l&&(b.blackberry=!0,b.version=l[2]),m&&(b.bb10=!0,b.version=m[2]),n&&(b.rimtabletos=!0,b.version=n[2]),o&&(c.playbook=!0),j&&(b.kindle=!0,b.version=j[1]),k&&(c.silk=!0,c.version=k[1]),!k&&b.android&&a.match(/Kindle Fire/)&&(c.silk=!0),p&&(c.chrome=!0,c.version=p[1]),q&&(c.firefox=!0,c.version=q[1]),b.tablet=!!(f||o||e&&!a.match(/Mobile/)||q&&a.match(/Tablet/)),b.phone=!b.tablet&&!!(e||g||h||l||m||p&&a.match(/Android/)||p&&a.match(/CriOS\/([\d.]+)/)||q&&a.match(/Mobile/))}b.call(a,navigator.userAgent),a.__detect=b}(Zepto),function(a){function g(a){return a._zid||(a._zid=d++)}function h(a,b,d,e){b=i(b);if(b.ns)var f=j(b.ns);return(c[g(a)]||[]).filter(function(a){return a&&(!b.e||a.e==b.e)&&(!b.ns||f.test(a.ns))&&(!d||g(a.fn)===g(d))&&(!e||a.sel==e)})}function i(a){var b=(""+a).split(".");return{e:b[0],ns:b.slice(1).sort().join(" ")}}function j(a){return new RegExp("(?:^| )"+a.replace(" "," .* ?")+"(?: |$)")}function k(b,c,d){a.type(b)!="string"?a.each(b,d):b.split(/\s/).forEach(function(a){d(a,c)})}function l(a,b){return a.del&&(a.e=="focus"||a.e=="blur")||!!b}function m(a){return f[a]||a}function n(b,d,e,h,j,n){var o=g(b),p=c[o]||(c[o]=[]);k(d,e,function(c,d){var e=i(c);e.fn=d,e.sel=h,e.e in f&&(d=function(b){var c=b.relatedTarget;if(!c||c!==this&&!a.contains(this,c))return e.fn.apply(this,arguments)}),e.del=j&&j(d,c);var g=e.del||d;e.proxy=function(a){var c=g.apply(b,[a].concat(a.data));return c===!1&&(a.preventDefault(),a.stopPropagation()),c},e.i=p.length,p.push(e),b.addEventListener(m(e.e),e.proxy,l(e,n))})}function o(a,b,d,e,f){var i=g(a);k(b||"",d,function(b,d){h(a,b,d,e).forEach(function(b){delete c[i][b.i],a.removeEventListener(m(b.e),b.proxy,l(b,f))})})}function t(b){var c,d={originalEvent:b};for(c in b)!r.test(c)&&b[c]!==undefined&&(d[c]=b[c]);return a.each(s,function(a,c){d[a]=function(){return this[c]=p,b[a].apply(b,arguments)},d[c]=q}),d}function u(a){if(!("defaultPrevented"in a)){a.defaultPrevented=!1;var b=a.preventDefault;a.preventDefault=function(){this.defaultPrevented=!0,b.call(this)}}}var b=a.zepto.qsa,c={},d=1,e={},f={mouseenter:"mouseover",mouseleave:"mouseout"};e.click=e.mousedown=e.mouseup=e.mousemove="MouseEvents",a.event={add:n,remove:o},a.proxy=function(b,c){if(a.isFunction(b)){var d=function(){return b.apply(c,arguments)};return d._zid=g(b),d}if(typeof c=="string")return a.proxy(b[c],b);throw new TypeError("expected function")},a.fn.bind=function(a,b){return this.each(function(){n(this,a,b)})},a.fn.unbind=function(a,b){return this.each(function(){o(this,a,b)})},a.fn.one=function(a,b){return this.each(function(c,d){n(this,a,b,null,function(a,b){return function(){var c=a.apply(d,arguments);return o(d,b,a),c}})})};var p=function(){return!0},q=function(){return!1},r=/^([A-Z]|layer[XY]$)/,s={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};a.fn.delegate=function(b,c,d){return this.each(function(e,f){n(f,c,d,b,function(c){return function(d){var e,g=a(d.target).closest(b,f).get(0);if(g)return e=a.extend(t(d),{currentTarget:g,liveFired:f}),c.apply(g,[e].concat([].slice.call(arguments,1)))}})})},a.fn.undelegate=function(a,b,c){return this.each(function(){o(this,b,c,a)})},a.fn.live=function(b,c){return a(document.body).delegate(this.selector,b,c),this},a.fn.die=function(b,c){return a(document.body).undelegate(this.selector,b,c),this},a.fn.on=function(b,c,d){return!c||a.isFunction(c)?this.bind(b,c||d):this.delegate(c,b,d)},a.fn.off=function(b,c,d){return!c||a.isFunction(c)?this.unbind(b,c||d):this.undelegate(c,b,d)},a.fn.trigger=function(b,c){if(typeof b=="string"||a.isPlainObject(b))b=a.Event(b);return u(b),b.data=c,this.each(function(){"dispatchEvent"in this&&this.dispatchEvent(b)})},a.fn.triggerHandler=function(b,c){var d,e;return this.each(function(f,g){d=t(typeof b=="string"?a.Event(b):b),d.data=c,d.target=g,a.each(h(g,b.type||b),function(a,b){e=b.proxy(d);if(d.isImmediatePropagationStopped())return!1})}),e},"focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(b){a.fn[b]=function(a){return a?this.bind(b,a):this.trigger(b)}}),["focus","blur"].forEach(function(b){a.fn[b]=function(a){return a?this.bind(b,a):this.each(function(){try{this[b]()}catch(a){}}),this}}),a.Event=function(a,b){typeof a!="string"&&(b=a,a=b.type);var c=document.createEvent(e[a]||"Events"),d=!0;if(b)for(var f in b)f=="bubbles"?d=!!b[f]:c[f]=b[f];return c.initEvent(a,d,!0,null,null,null,null,null,null,null,null,null,null,null,null),c.isDefaultPrevented=function(){return this.defaultPrevented},c}}(Zepto),function($){function triggerAndReturn(a,b,c){var d=$.Event(b);return $(a).trigger(d,c),!d.defaultPrevented}function triggerGlobal(a,b,c,d){if(a.global)return triggerAndReturn(b||document,c,d)}function ajaxStart(a){a.global&&$.active++===0&&triggerGlobal(a,null,"ajaxStart")}function ajaxStop(a){a.global&&!--$.active&&triggerGlobal(a,null,"ajaxStop")}function ajaxBeforeSend(a,b){var c=b.context;if(b.beforeSend.call(c,a,b)===!1||triggerGlobal(b,c,"ajaxBeforeSend",[a,b])===!1)return!1;triggerGlobal(b,c,"ajaxSend",[a,b])}function ajaxSuccess(a,b,c){var d=c.context,e="success";c.success.call(d,a,e,b),triggerGlobal(c,d,"ajaxSuccess",[b,c,a]),ajaxComplete(e,b,c)}function ajaxError(a,b,c,d){var e=d.context;d.error.call(e,c,b,a),triggerGlobal(d,e,"ajaxError",[c,d,a]),ajaxComplete(b,c,d)}function ajaxComplete(a,b,c){var d=c.context;c.complete.call(d,b,a),triggerGlobal(c,d,"ajaxComplete",[b,c]),ajaxStop(c)}function empty(){}function mimeToDataType(a){return a&&(a=a.split(";",2)[0]),a&&(a==htmlType?"html":a==jsonType?"json":scriptTypeRE.test(a)?"script":xmlTypeRE.test(a)&&"xml")||"text"}function appendQuery(a,b){return(a+"&"+b).replace(/[&?]{1,2}/,"?")}function serializeData(a){a.processData&&a.data&&$.type(a.data)!="string"&&(a.data=$.param(a.data,a.traditional)),a.data&&(!a.type||a.type.toUpperCase()=="GET")&&(a.url=appendQuery(a.url,a.data))}function parseArguments(a,b,c,d){var e=!$.isFunction(b);return{url:a,data:e?b:undefined,success:e?$.isFunction(c)?c:undefined:b,dataType:e?d||c:c}}function serialize(a,b,c,d){var e,f=$.isArray(b);$.each(b,function(b,g){e=$.type(g),d&&(b=c?d:d+"["+(f?"":b)+"]"),!d&&f?a.add(g.name,g.value):e=="array"||!c&&e=="object"?serialize(a,g,c,b):a.add(b,g)})}var jsonpID=0,document=window.document,key,name,rscript=/)<[^<]*)*<\/script>/gi,scriptTypeRE=/^(?:text|application)\/javascript/i,xmlTypeRE=/^(?:text|application)\/xml/i,jsonType="application/json",htmlType="text/html",blankRE=/^\s*$/;$.active=0,$.ajaxJSONP=function(a){if("type"in a){var b="jsonp"+ ++jsonpID,c=document.createElement("script"),d=function(){clearTimeout(g),$(c).remove(),delete window[b]},e=function(c){d();if(!c||c=="timeout")window[b]=empty;ajaxError(null,c||"abort",f,a)},f={abort:e},g;return ajaxBeforeSend(f,a)===!1?(e("abort"),!1):(window[b]=function(b){d(),ajaxSuccess(b,f,a)},c.onerror=function(){e("error")},c.src=a.url.replace(/=\?/,"="+b),$("head").append(c),a.timeout>0&&(g=setTimeout(function(){e("timeout")},a.timeout)),f)}return $.ajax(a)},$.ajaxSettings={type:"GET",beforeSend:empty,success:empty,error:empty,complete:empty,context:null,global:!0,xhr:function(){return new window.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript",json:jsonType,xml:"application/xml, text/xml",html:htmlType,text:"text/plain"},crossDomain:!1,timeout:0,processData:!0,cache:!0},$.ajax=function(options){var settings=$.extend({},options||{});for(key in $.ajaxSettings)settings[key]===undefined&&(settings[key]=$.ajaxSettings[key]);ajaxStart(settings),settings.crossDomain||(settings.crossDomain=/^([\w-]+:)?\/\/([^\/]+)/.test(settings.url)&&RegExp.$2!=window.location.host),settings.url||(settings.url=window.location.toString()),serializeData(settings),settings.cache===!1&&(settings.url=appendQuery(settings.url,"_="+Date.now()));var dataType=settings.dataType,hasPlaceholder=/=\?/.test(settings.url);if(dataType=="jsonp"||hasPlaceholder)return hasPlaceholder||(settings.url=appendQuery(settings.url,"callback=?")),$.ajaxJSONP(settings);var mime=settings.accepts[dataType],baseHeaders={},protocol=/^([\w-]+:)\/\//.test(settings.url)?RegExp.$1:window.location.protocol,xhr=settings.xhr(),abortTimeout;settings.crossDomain||(baseHeaders["X-Requested-With"]="XMLHttpRequest"),mime&&(baseHeaders.Accept=mime,mime.indexOf(",")>-1&&(mime=mime.split(",",2)[0]),xhr.overrideMimeType&&xhr.overrideMimeType(mime));if(settings.contentType||settings.contentType!==!1&&settings.data&&settings.type.toUpperCase()!="GET")baseHeaders["Content-Type"]=settings.contentType||"application/x-www-form-urlencoded";settings.headers=$.extend(baseHeaders,settings.headers||{}),xhr.onreadystatechange=function(){if(xhr.readyState==4){xhr.onreadystatechange=empty,clearTimeout(abortTimeout);var result,error=!1;if(xhr.status>=200&&xhr.status<300||xhr.status==304||xhr.status==0&&protocol=="file:"){dataType=dataType||mimeToDataType(xhr.getResponseHeader("content-type")),result=xhr.responseText;try{dataType=="script"?(1,eval)(result):dataType=="xml"?result=xhr.responseXML:dataType=="json"&&(result=blankRE.test(result)?null:$.parseJSON(result))}catch(e){error=e}error?ajaxError(error,"parsererror",xhr,settings):ajaxSuccess(result,xhr,settings)}else ajaxError(null,xhr.status?"error":"abort",xhr,settings)}};var async="async"in settings?settings.async:!0;xhr.open(settings.type,settings.url,async);for(name in settings.headers)xhr.setRequestHeader(name,settings.headers[name]);return ajaxBeforeSend(xhr,settings)===!1?(xhr.abort(),!1):(settings.timeout>0&&(abortTimeout=setTimeout(function(){xhr.onreadystatechange=empty,xhr.abort(),ajaxError(null,"timeout",xhr,settings)},settings.timeout)),xhr.send(settings.data?settings.data:null),xhr)},$.get=function(a,b,c,d){return $.ajax(parseArguments.apply(null,arguments))},$.post=function(a,b,c,d){var e=parseArguments.apply(null,arguments);return e.type="POST",$.ajax(e)},$.getJSON=function(a,b,c){var d=parseArguments.apply(null,arguments);return d.dataType="json",$.ajax(d)},$.fn.load=function(a,b,c){if(!this.length)return this;var d=this,e=a.split(/\s/),f,g=parseArguments(a,b,c),h=g.success;return e.length>1&&(g.url=e[0],f=e[1]),g.success=function(a){d.html(f?$("
").html(a.replace(rscript,"")).find(f):a),h&&h.apply(d,arguments)},$.ajax(g),this};var escape=encodeURIComponent;$.param=function(a,b){var c=[];return c.add=function(a,b){this.push(escape(a)+"="+escape(b))},serialize(c,a,b),c.join("&").replace(/%20/g,"+")}}(Zepto),function(a){a.fn.serializeArray=function(){var b=[],c;return a(Array.prototype.slice.call(this.get(0).elements)).each(function(){c=a(this);var d=c.attr("type");this.nodeName.toLowerCase()!="fieldset"&&!this.disabled&&d!="submit"&&d!="reset"&&d!="button"&&(d!="radio"&&d!="checkbox"||this.checked)&&b.push({name:c.attr("name"),value:c.val()})}),b},a.fn.serialize=function(){var a=[];return this.serializeArray().forEach(function(b){a.push(encodeURIComponent(b.name)+"="+encodeURIComponent(b.value))}),a.join("&")},a.fn.submit=function(b){if(b)this.bind("submit",b);else if(this.length){var c=a.Event("submit");this.eq(0).trigger(c),c.defaultPrevented||this.get(0).submit()}return this}}(Zepto),function(a,b){function s(a){return t(a.replace(/([a-z])([A-Z])/,"$1-$2"))}function t(a){return a.toLowerCase()}function u(a){return d?d+a:t(a)}var c="",d,e,f,g={Webkit:"webkit",Moz:"",O:"o",ms:"MS"},h=window.document,i=h.createElement("div"),j=/^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,k,l,m,n,o,p,q,r={};a.each(g,function(a,e){if(i.style[a+"TransitionProperty"]!==b)return c="-"+t(a)+"-",d=e,!1}),k=c+"transform",r[l=c+"transition-property"]=r[m=c+"transition-duration"]=r[n=c+"transition-timing-function"]=r[o=c+"animation-name"]=r[p=c+"animation-duration"]=r[q=c+"animation-timing-function"]="",a.fx={off:d===b&&i.style.transitionProperty===b,speeds:{_default:400,fast:200,slow:600},cssPrefix:c,transitionEnd:u("TransitionEnd"),animationEnd:u("AnimationEnd")},a.fn.animate=function(b,c,d,e){return a.isPlainObject(c)&&(d=c.easing,e=c.complete,c=c.duration),c&&(c=(typeof c=="number"?c:a.fx.speeds[c]||a.fx.speeds._default)/1e3),this.anim(b,c,d,e)},a.fn.anim=function(c,d,e,f){var g,h={},i,t="",u=this,v,w=a.fx.transitionEnd;d===b&&(d=.4),a.fx.off&&(d=0);if(typeof c=="string")h[o]=c,h[p]=d+"s",h[q]=e||"linear",w=a.fx.animationEnd;else{i=[];for(g in c)j.test(g)?t+=g+"("+c[g]+") ":(h[g]=c[g],i.push(s(g)));t&&(h[k]=t,i.push(k)),d>0&&typeof c=="object"&&(h[l]=i.join(", "),h[m]=d+"s",h[n]=e||"linear")}return v=function(b){if(typeof b!="undefined"){if(b.target!==b.currentTarget)return;a(b.target).unbind(w,v)}a(this).css(r),f&&f.call(this)},d>0&&this.bind(w,v),this.size()&&this.get(0).clientLeft,this.css(h),d<=0&&setTimeout(function(){u.each(function(){v.call(this)})},0),this},i=null}(Zepto) --------------------------------------------------------------------------------