├── 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 |
9 |
10 |
12 |
15 |
16 |
17 |
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 |
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 |
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 |
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 |
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 | [](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>$2>")),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=/