11 |
17 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/browser-tests/README.md:
--------------------------------------------------------------------------------
1 | Maquette Browser tests
2 | =========
3 |
4 | This directory contains a test-suite of browser tests. They can be run using the following commands:
5 |
6 | ### mocha
7 |
8 | Assumes you have a webdriver running on http://localhost:4444/hub/wd.
9 | This can be achieved using one of the following commands:
10 |
11 | - `phantomjs --webdriver=127.0.0.1:4444`
12 | - `java -jar selenium-server-standalone-2.44.0.jar -role hub -Dwebdriver.chrome.driver=chromedriver` (You can call mocha with a `--browserName=` argument to select a specific browser.)
13 |
14 | ### node sauce [desiredKey]
15 |
16 | Runs the tests in sauce. If desiredKey is not specified, all browsers specified in desireds.js are run sequentially.
17 | If desiredKey is specified, only that entry from desireds.js is run.
18 |
19 | You need to make sure the following exports have the right values:
20 |
21 | - `export SAUCE_USERNAME=`
22 | - `export SAUCE_ACCESS_KEY=`
23 |
--------------------------------------------------------------------------------
/test/animations.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | var maquette = require("../src/maquette.js");
3 | var assert = require("assert");
4 | var jsdom = require('mocha-jsdom');
5 | var chai = require('chai');
6 | chai.use(require('sinon-chai'));
7 | var expect = chai.expect;
8 | var sinon = require('sinon');
9 |
10 | var h = maquette.h;
11 |
12 | describe('Maquette', function () {
13 | describe('animations', function () {
14 |
15 | describe('updateAnimation', function () {
16 |
17 | jsdom();
18 |
19 | it('is invoked when a node contains only text and that text changes', function() {
20 | var updateAnimation = sinon.stub();
21 | var projection = maquette.dom.create(h("div", {updateAnimation: updateAnimation}, ["text"]));
22 | projection.update(h("div", {updateAnimation: updateAnimation}, ["text2"]));
23 | expect(updateAnimation).to.have.been.calledOnce;
24 | expect(projection.domNode.outerHTML).to.equal("
text2
");
25 | });
26 |
27 | });
28 | });
29 | });
30 |
31 |
--------------------------------------------------------------------------------
/examples/todomvc/js/components/todoRouter.js:
--------------------------------------------------------------------------------
1 | window.createRouter = function (model) {
2 | // This router renders a in which the current page is rendered. The current page is based on the hash (#) part of the url.
3 |
4 | 'use strict';
5 |
6 | var h = window.maquette.h;
7 |
8 | var currentHash = null;
9 | var currentPage = null;
10 |
11 | var todoRouter = {
12 |
13 | renderMaquette: function () {
14 | var hash = document.location.hash;
15 |
16 | if(hash !== currentHash) {
17 | switch(hash) {
18 | case "#/active":
19 | currentPage = createListComponent("active", model);
20 | break;
21 | case "#/completed":
22 | currentPage = createListComponent("completed", model);
23 | break;
24 | default:
25 | currentPage = createListComponent("all", model);
26 | }
27 | currentHash = hash;
28 | }
29 |
30 | return h("main", [
31 | currentPage.renderMaquette()
32 | ]);
33 | }
34 | };
35 |
36 | return todoRouter;
37 | };
38 |
--------------------------------------------------------------------------------
/browser-tests/sauce.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var Mocha = require("mocha");
4 |
5 | var desiredName = process.argv[2];
6 |
7 | var desireds = require("./desireds");
8 |
9 | var setup = require("./test/setup");
10 | setup.sauce = true;
11 |
12 | if (desiredName) {
13 | var desired = desireds[desiredName];
14 | if(!desired) {
15 | throw new Error("Desired browser not found in desireds.js: " + desiredName);
16 | }
17 | desireds = {};
18 | desireds[desiredName] = desired;
19 | }
20 |
21 | var desiredNames = Object.keys(desireds);
22 | var desiredIndex = 0;
23 |
24 | var totalErrors = 0;
25 |
26 | var mochaInstance = new Mocha({ timeout: 60000 });
27 | mochaInstance.addFile("test/todomvc-specs.js");
28 |
29 | var next = function () {
30 | if(desiredIndex < desiredNames.length) {
31 | setup.browserCapabilities = desireds[desiredNames[desiredIndex++]];
32 | mochaInstance.run(function (errCount) {
33 | totalErrors += errCount;
34 | next();
35 | });
36 | } else {
37 | console.log("Total errors: " + totalErrors);
38 | process.exit(0);
39 | }
40 | };
41 |
42 | next();
43 |
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Maquette contributors
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/examples/helloworld/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Hello world
5 |
6 |
36 |
37 |
38 |
Hello world
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # useful file for debugging a mocha unit test from cloud9, file contents: var Mocha = require("mocha");var mocha = new Mocha();mocha.addFile("test/createDom");mocha.run();
2 | sandbox.js
3 |
4 | bower_components
5 | node_modules
6 | coverage
7 | *.log
8 |
9 | # Linux gedit temp files
10 | *~
11 |
12 | # Windows image file caches
13 | Thumbs.db
14 | ehthumbs.db
15 |
16 | # Folder config file
17 | Desktop.ini
18 |
19 | # Recycle Bin used on file shares
20 | $RECYCLE.BIN/
21 |
22 | # Windows Installer files
23 | *.cab
24 | *.msi
25 | *.msm
26 | *.msp
27 |
28 | # Visual Studio
29 | Web.config
30 |
31 | # IntelliJ IDEA
32 | /.idea
33 | /*.iml
34 |
35 | # OSX
36 |
37 | .DS_Store
38 | .AppleDouble
39 | .LSOverride
40 |
41 | # Icon must end with two \r
42 | Icon
43 |
44 |
45 | # Thumbnails
46 | ._*
47 |
48 | # Files that might appear on external disk
49 | .Spotlight-V100
50 | .Trashes
51 |
52 | # Directories potentially created on remote AFP share
53 | .AppleDB
54 | .AppleDesktop
55 | Network Trash Folder
56 | Temporary Items
57 | .apdisk
58 | *.cmd
59 |
60 | # Files for NodeJS plugin for visual studio
61 | maquette.njsproj
62 | .ntvs_analysis.dat
63 | bin
64 | obj
65 |
66 | # Other
67 | .vscode
68 | docs
69 | .c9
--------------------------------------------------------------------------------
/examples/todomvc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Maquette - TodoMVC
6 |
7 |
8 |
9 |
10 |
11 |
12 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/test/cache.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | var maquette = require("../src/maquette.js");
3 | var assert = require("assert");
4 |
5 | describe('Maquette', function () {
6 | describe('#createCache()', function () {
7 | it('should execute calculate() on the first invocation', function () {
8 | var cache = maquette.createCache();
9 | var calculationCalled = false;
10 | var calculate = function () {
11 | calculationCalled = true;
12 | return "calculation result";
13 | };
14 | var result = cache.result([1], calculate);
15 | assert.equal(true, calculationCalled);
16 | assert.equal("calculation result", result);
17 | });
18 |
19 | it('should only execute calculate() on next invocations when the inputs are equal', function () {
20 | var cache = maquette.createCache();
21 | var calculationCount = 0;
22 | var calculate = function () {
23 | calculationCount++;
24 | return "calculation result";
25 | };
26 | cache.result([1], calculate);
27 | assert.equal(1, calculationCount);
28 | var result = cache.result([1], calculate);
29 | assert.equal(1, calculationCount);
30 | assert.equal("calculation result", result);
31 | });
32 |
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/examples/todomvc/readme.md:
--------------------------------------------------------------------------------
1 | # Framework Name TodoMVC Example
2 |
3 | > Short description of the framework provided by the official website.
4 |
5 | > _[Framework Name - framework.com](link-to-framework)_
6 |
7 |
8 | ## Learning Framework Name
9 |
10 | The [Framework Name website]() is a great resource for getting started.
11 |
12 | Here are some links you may find helpful:
13 |
14 | * [Documentation]()
15 | * [API Reference]()
16 | * [Applications built with Framework Name]()
17 | * [Blog]()
18 | * [FAQ]()
19 | * [Framework Name on GitHub]()
20 |
21 | Articles and guides from the community:
22 |
23 | * [Article 1]()
24 | * [Article 2]()
25 |
26 | Get help from other Framework Name users:
27 |
28 | * [Framework Name on StackOverflow](http://stackoverflow.com/questions/tagged/____)
29 | * [Mailing list on Google Groups]()
30 | * [Framework Name on Twitter](http://twitter.com/____)
31 | * [Framework Name on Google +]()
32 |
33 | _If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
34 |
35 |
36 | ## Implementation
37 |
38 | How is the app structured? Are there deviations from the spec? If so, why?
39 |
40 |
41 | ## Running
42 |
43 | If there is a build step required to get the example working, explain it here.
44 |
45 | To run the app, spin up an HTTP server and visit http://localhost/.../myexample/.
46 |
47 |
48 | ## Credit
49 |
50 | This TodoMVC application was created by [you]().
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "maquette",
3 | "description": "Minimalistic Virtual DOM implementation with support for animated transitions.",
4 | "homepage": "http://maquettejs.org/",
5 | "keywords": [
6 | "virtual",
7 | "dom",
8 | "animation",
9 | "transitions"
10 | ],
11 | "version": "2.1.6",
12 | "author": "Johan Gorter ",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/AFASSoftware/maquette"
16 | },
17 | "main": "src/maquette.js",
18 | "typings": "maquette.d.ts",
19 | "scripts": {
20 | "test": "mocha && cd examples/todomvc && bower install && cd ../../browser-tests && npm install && node sauce.js",
21 | "coverage": "node node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha"
22 | },
23 | "license": "MIT",
24 | "devDependencies": {
25 | "Set": "^0.4.1",
26 | "browser-sync": "^2.5.2",
27 | "chai": "^2.2.0",
28 | "del": "^1.1.1",
29 | "gulp": "^3.8.10",
30 | "gulp-bump": "^0.1.11",
31 | "gulp-filter": "^2.0.0",
32 | "gulp-git": "^0.5.5",
33 | "gulp-prompt": "^0.1.1",
34 | "gulp-rename": "^1.2.0",
35 | "gulp-tag-version": "^1.2.1",
36 | "gulp-uglify": "^1.0.2",
37 | "ink-docstrap": "^0.5.2",
38 | "inquirer": "^0.8.0",
39 | "istanbul": "^0.3.13",
40 | "jsdoc": "^3.3.0",
41 | "jsdom": "^7.0.2",
42 | "mocha": "^2.3.3",
43 | "mocha-jsdom": "^1.0.0",
44 | "sinon": "^1.17.2",
45 | "sinon-chai": "^2.8.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/scripts/release.js:
--------------------------------------------------------------------------------
1 | var spawn = require('child_process').spawn;
2 | var exec = require('child_process').exec;
3 | var inquirer = require('inquirer');
4 |
5 |
6 |
7 | spawn("gulp", ["compress"], { stdio: 'inherit' }).on('close', function (code) {
8 | if(code !== 0) {
9 | process.exit(code);
10 | }
11 |
12 | exec("git status --porcelain", function (error, stdout, stderr) {
13 | if (error) {
14 | console.log(stdout);
15 | console.log(stderr);
16 | console.log("error: " + error);
17 | process.exit(error);
18 | }
19 | if (stdout) {
20 | console.log(stdout);
21 | console.log("There are uncommitted changes");
22 | process.exit(1);
23 | }
24 |
25 | inquirer.prompt({
26 | type: 'list',
27 | name: 'bump',
28 | message: 'What type of bump would you like to do?',
29 | choices: ['patch', 'minor', 'major']
30 | }, function (importance) {
31 | spawn("gulp", ["bump-" + importance.bump], { stdio: 'inherit' }).on("close", function (code2) {
32 | if(code2 !== 0) { process.exit(code2); }
33 | spawn("git", ["push"], { stdio: 'inherit' }).on("close", function (code3) {
34 | if(code3 !== 0) { process.exit(code3); }
35 | spawn("git", ["push", "--tags"], { stdio: 'inherit' }).on("close", function (code4) {
36 | if(code4 !== 0) {
37 | process.exit(code4);
38 | }
39 | spawn('npm', ['publish'], { stdio: 'inherit' }).on('close', function (code5) {
40 | process.exit(code5);
41 | });
42 | });
43 | });
44 | });
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/examples/enhancement/index.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var h = maquette.h;
3 |
4 | var newTodoText = "Your next todo";
5 | var todos = [
6 | createTodoComponent("TODO item after enhancement")
7 | ];
8 |
9 | // Event handlers
10 |
11 | var handleNewTodoInput = function (evt) {
12 | newTodoText = evt.target.value;
13 | };
14 |
15 | var handleNewTodoButtonClick = function (evt) {
16 | evt.preventDefault();
17 | if(newTodoText) {
18 | todos.splice(0, 0, createTodoComponent(newTodoText));
19 | newTodoText = "";
20 | // ... and imagine we post the new todo to the server using XHR
21 | }
22 | };
23 |
24 | // Enhance functions
25 |
26 | var enhanceNewTodoText = function () {
27 | return h("input", { value: newTodoText, oninput: handleNewTodoInput });
28 | };
29 |
30 | var enhanceNewTodoButton = function () {
31 | return h("input", { type: "submit", onclick: handleNewTodoButtonClick });
32 | };
33 |
34 | var enhanceTodoList = function () {
35 | return h("ul#todo-list", [
36 | todos.map(function (todo) {
37 | return h("li", {key:todo}, [
38 | todo.renderMaquette()
39 | ]);
40 | })
41 | ]);
42 | };
43 |
44 | // Put it all in motion
45 | document.addEventListener('DOMContentLoaded', function () {
46 | var projector = maquette.createProjector();
47 | projector.merge(document.getElementById("new-todo-text"), enhanceNewTodoText);
48 | projector.merge(document.getElementById("new-todo-button"), enhanceNewTodoButton);
49 | projector.replace(document.getElementById("todo-list"), enhanceTodoList);
50 | projector.evaluateHyperscript(document.body, { todos: todos });
51 | });
52 |
53 | })();
--------------------------------------------------------------------------------
/src/extras/maquette-extras.js:
--------------------------------------------------------------------------------
1 | (function (global) {
2 |
3 | "use strict";
4 |
5 | var maquette = global.maquette;
6 |
7 | var maquetteExtras = {
8 |
9 | // projector which executes rendering synchronously (immediately). Created to be able to run performance tests.
10 | createSyncProjector: function (element, renderFunction, options) {
11 | var patchedOptions = {};
12 | Object.keys(options).forEach(function (key) {
13 | patchedOptions[key] = options[key];
14 | });
15 | patchedOptions.eventHandlerInterceptor = function (propertyName, functionPropertyArgument) {
16 | return function () {
17 | var result = functionPropertyArgument.apply(this, arguments);
18 | doRender();
19 | return result;
20 | };
21 | };
22 | var mount = null;
23 | var doRender = function () {
24 | if (!mount) {
25 | var vnode = renderFunction();
26 | mount = maquette.mergeDom(element, vnode, patchedOptions);
27 | } else {
28 | var updatedVnode = renderFunction();
29 | mount.update(updatedVnode);
30 | }
31 | };
32 | doRender();
33 | return {
34 | scheduleRender: doRender
35 | };
36 | }
37 | };
38 |
39 | if (typeof module !== "undefined" && module.exports) {
40 | // Node and other CommonJS-like environments that support module.exports
41 | module.exports = maquetteExtras;
42 | } else if (typeof define === "function" && define.amd) {
43 | // AMD / RequireJS
44 | define(function () {
45 | return maquetteExtras;
46 | });
47 | } else {
48 | // Browser
49 | window.maquetteExtras = maquetteExtras;
50 | }
51 |
52 | })(this);
--------------------------------------------------------------------------------
/src/extras/makeh.js:
--------------------------------------------------------------------------------
1 | // The code below can be copy-pasted into the developer console to get a translation from html to hyperscript
2 |
3 | (function () {
4 |
5 | var lastKey = 0;
6 |
7 | window.makeh = function (element) {
8 | if (element.nodeValue) {
9 | if(element.nodeType !== 3 || element.nodeValue.indexOf("\"") > 0 || element.nodeValue.trim().length === 0) {
10 | return null;
11 | }
12 | return "\"" + element.nodeValue.trim() + "\"";
13 | }
14 | if(!element.tagName || element.style.display === "none") {
15 | return null;
16 | }
17 | var properties = [];
18 | var children = [];
19 | var classes = [];
20 | var selector = element.tagName.toLowerCase();
21 | if (selector !== "svg") {
22 | classes = element.className.split(" ");
23 | for(var i=0;i 0) {
35 | properties.push("classes:{" + classes.map(function (c) { return "\"" + c + "\":true"; }).join() + "}");
36 | }
37 | }
38 | if (!element.id) {
39 | properties.push("key:"+(++lastKey));
40 | }
41 | if(element.href) {
42 | properties.push("href:\""+element.href+"\"");
43 | }
44 | if(element.src) {
45 | properties.push("src:\"" + element.src + "\"");
46 | }
47 | if (element.value) {
48 | properties.push("value:\"" + element.value + "\"");
49 | }
50 | return "\n h(\"" + selector + "\", {" + properties.join() + "}, [" + children.filter(function (c) { return !!c; }).join() + "])";
51 | };
52 |
53 | console.log(makeh(document.body));
54 |
55 | })();
--------------------------------------------------------------------------------
/browser-tests/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _ = require('lodash');
4 |
5 | var desireds = require('./desireds');
6 |
7 | var gruntConfig = {
8 | env: {
9 | // dynamically filled
10 | },
11 | simplemocha: {
12 | sauce: {
13 | options: {
14 | timeout: 60000,
15 | reporter: 'spec'
16 | },
17 | src: ['test/**/*-specs.js']
18 | }
19 | },
20 | concurrent: {
21 | 'test-sauce': [], // dynamically filled
22 | },
23 | connect: {
24 | server: {
25 | options: {
26 | port: 8000,
27 | hostname: "*",
28 | base: ".."
29 | }
30 | }
31 | }
32 | };
33 |
34 | Object.keys(desireds).forEach(function(key) {
35 | gruntConfig.env[key] = {
36 | DESIRED: JSON.stringify(desireds[key])
37 | };
38 | gruntConfig.concurrent['test-sauce'].push('dotest:sauce:' + key);
39 | });
40 |
41 | //console.log(gruntConfig);
42 |
43 | module.exports = function(grunt) {
44 |
45 | // Project configuration.
46 | grunt.initConfig(gruntConfig);
47 |
48 | // These plugins provide necessary tasks.
49 | grunt.loadNpmTasks('grunt-contrib-connect');
50 | grunt.loadNpmTasks('grunt-env');
51 | grunt.loadNpmTasks('grunt-simple-mocha');
52 | grunt.loadNpmTasks('grunt-concurrent');
53 |
54 | // Default task.
55 | grunt.registerTask('default', ['test:sauce:' + _(desireds).keys().first()]);
56 |
57 | Object.keys(desireds).forEach(function(key) {
58 | grunt.registerTask('dotest:sauce:' + key, ['env:' + key, 'simplemocha:sauce']);
59 | });
60 |
61 | var serialTasks = ['connect'];
62 | Object.keys(desireds).forEach(function (key) {
63 | grunt.registerTask('test:sauce:' + key, ['connect', 'env:' + key, 'simplemocha:sauce']);
64 | serialTasks.push('dotest:sauce:' + key);
65 | });
66 |
67 | grunt.registerTask('test:sauce:parallel', ['connect', 'concurrent:test-sauce']);
68 |
69 | grunt.registerTask('test:sauce:serial', serialTasks);
70 | };
71 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp=require("gulp");
2 | var uglify=require("gulp-uglify");
3 | var rename = require('gulp-rename');
4 | var del = require('del');
5 |
6 | var git = require('gulp-git');
7 | var bump = require('gulp-bump');
8 | var filter = require('gulp-filter');
9 | var tag_version = require('gulp-tag-version');
10 |
11 | var browserSync = require('browser-sync');
12 | var reload = browserSync.reload;
13 |
14 | var BROWSERSYNC_PORT = parseInt(process.env.PORT) || 3002;
15 | var BROWSERSYNC_HOST = process.env.IP || "127.0.0.1";
16 |
17 | gulp.task("compress", function() {
18 | gulp.src("src/*.js")
19 | .pipe(uglify())
20 | .pipe(rename({ suffix: '.min' }))
21 | .pipe(gulp.dest("dist"));
22 | });
23 |
24 | gulp.task('clean', function(cb) {
25 | del(['dist'], cb);
26 | });
27 |
28 | gulp.task("default", ["compress"]);
29 |
30 | function inc(importance) {
31 | // get all the files to bump version in
32 | return gulp.src(['./package.json', './bower.json'])
33 | // bump the version number in those files
34 | .pipe(bump({ type: importance }))
35 | // save it back to filesystem
36 | .pipe(gulp.dest('./'))
37 | // commit the changed version number
38 | .pipe(git.commit('bumps package version'))
39 | // read only one file to get the version number
40 | .pipe(filter('package.json'))
41 | // **tag it in the repository**
42 | .pipe(tag_version());
43 | }
44 |
45 | // these tasks are called from scripts/release.js
46 | gulp.task('bump-patch', ["compress"], function () { return inc('patch'); });
47 | gulp.task('bump-minor', ["compress"], function () { return inc('minor'); });
48 | gulp.task('bump-major', ["compress"], function () { return inc('major'); });
49 |
50 | gulp.task('reload', reload);
51 |
52 | gulp.task('serve', ['default'], function () {
53 | browserSync({
54 | port: BROWSERSYNC_PORT,
55 | host: BROWSERSYNC_HOST,
56 | notify: false,
57 | server: '.'
58 | });
59 |
60 | gulp.watch('./src/**/*', ['compress', 'reload']);
61 | gulp.watch('./examples/**/*', ['reload']);
62 | gulp.watch('./browser-tests/**/*', ['reload']);
63 | });
64 |
--------------------------------------------------------------------------------
/test/h.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | var maquette = require("../src/maquette.js");
3 | var assert = require("assert");
4 |
5 | describe('Maquette', function () {
6 | describe('#h()', function () {
7 |
8 | var h = maquette.h;
9 |
10 | var toTextVNode = function (text) {
11 | return {
12 | vnodeSelector: "",
13 | properties: undefined,
14 | children: undefined,
15 | text: text,
16 | domNode: null
17 | };
18 | };
19 |
20 | it('should flatten nested arrays', function () {
21 |
22 | var vnode = h("div", [
23 | "text",
24 | null,
25 | [ /* empty nested array */],
26 | [null],
27 | ["nested text"],
28 | [h("span")],
29 | [h("button", ["click me"])],
30 | [[[["deep"], null], "here"]]
31 | ]);
32 |
33 | assert.deepEqual(vnode.children, [
34 | toTextVNode("text"),
35 | toTextVNode("nested text"),
36 | h("span"),
37 | h("button", ["click me"]),
38 | toTextVNode("deep"),
39 | toTextVNode("here")
40 | ]);
41 |
42 | });
43 |
44 | it("Should be very flexible when accepting arguments", function() {
45 |
46 | var vnode = h("div",
47 | "text",
48 | h("span", [
49 | [
50 | "in array"
51 | ]
52 | ]),
53 | h("img", {src: "x.png"}),
54 | "text2",
55 | undefined,
56 | null,
57 | [
58 | undefined,
59 | h("button", "click me"),
60 | h("button", undefined, "click me")
61 | ]
62 | );
63 |
64 | assert.deepEqual(vnode.children, [
65 | toTextVNode("text"),
66 | h("span", "in array", undefined),
67 | h("img", {src:"x.png"}),
68 | toTextVNode("text2"),
69 | h("button", "click me"),
70 | h("button", "click me", undefined)
71 | ]);
72 |
73 | });
74 |
75 | it("Should render a number as text", function(){
76 | assert.deepEqual(h("div", 1), {vnodeSelector:"div", properties:undefined, text: undefined, children:[toTextVNode("1")], domNode: null});
77 | });
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/examples/transitions/demo-velocity.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Virtual DOM test page
5 |
16 |
17 |
18 |
19 |
20 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/test/styles.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | var maquette = require("../src/maquette.js");
3 | var assert = require("assert");
4 | var jsdom = require('mocha-jsdom');
5 | var expect = require('chai').expect;
6 |
7 | var h = maquette.h;
8 |
9 | describe('Maquette', function () {
10 | describe('styles', function () {
11 |
12 | jsdom();
13 |
14 | it("should not allow non-string values", function () {
15 | try {
16 | maquette.dom.create(h("div", { styles: { height: 20 } }));
17 | assert.fail();
18 | } catch(e) {
19 | expect(e.message.indexOf("strings") >= 0).to.be.true;
20 | }
21 | });
22 |
23 | it("should add styles to the real DOM", function () {
24 | var projection = maquette.dom.create(h("div", { styles: { height: "20px" } }));
25 | expect(projection.domNode.outerHTML).to.equal("");
26 | });
27 |
28 | it("should update styles", function () {
29 | var projection = maquette.dom.create(h("div", { styles: { height: "20px" } }));
30 | projection.update(h("div", { styles: { height: "30px" } }));
31 | expect(projection.domNode.outerHTML).to.equal("");
32 | });
33 |
34 | it("should remove styles", function () {
35 | var projection = maquette.dom.create(h("div", { styles: { height: "20px" } }));
36 | projection.update(h("div", { styles: { height: null } }));
37 | expect(projection.domNode.outerHTML).to.equal("");
38 | });
39 |
40 | it("should use the provided styleApplyer", function() {
41 | var styleApplyer = function(domNode, styleName, value) {
42 | // Useless styleApplyer which transforms height to minHeight
43 | domNode.style["min" + styleName.substr(0,1).toUpperCase() + styleName.substr(1)] = value;
44 | }
45 | var projection = maquette.dom.create(h("div", { styles: { height: "20px" } }), {styleApplyer: styleApplyer});
46 | expect(projection.domNode.outerHTML).to.equal("");
47 | projection.update(h("div", { styles: { height: "30px" } }));
48 | expect(projection.domNode.outerHTML).to.equal("");
49 | });
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/examples/transitions/demo-css.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Virtual DOM test page
5 |
17 |
18 |
39 |
40 |
41 |
42 |
43 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/dist/maquette-polyfills.min.js:
--------------------------------------------------------------------------------
1 | !function(t){"use strict";var n=function(n){return t.requestAnimationFrame&&t.cancelAnimationFrame||(t.requestAnimationFrame=t[n+"RequestAnimationFrame"])&&(t.cancelAnimationFrame=t[n+"CancelAnimationFrame"]||t[n+"CancelRequestAnimationFrame"])};if(!n("webkit")&&!n("moz")||/iP(ad|hone|od).*OS 6/.test(t.navigator.userAgent)){var e=Date.now||function(){return+new Date},r=0;t.requestAnimationFrame=function(t){var n=e(),o=Math.max(r+16,n);return setTimeout(function(){t(r=o)},o-n)},t.cancelAnimationFrame=clearTimeout}"classList"in document.documentElement||(!function(n,e){function r(t){if(/^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/.test(t))return String(t);throw new Error("InvalidCharacterError: DOM Exception 5")}function o(t){for(var n,e=-1,r={};n=t[++e];)r[n]=!0;return r}function a(t,n){var r,o=[];for(r in n)n[r]&&o.push(r);e.apply(t,[0,t.length].concat(o))}t.DOMTokenList=function(){},t.DOMTokenList.prototype={constructor:DOMTokenList,item:function(t){return this[parseFloat(t)]||null},length:Array.prototype.length,toString:function(){return n.call(this," ")},add:function(){for(var t,n=o(this),e=0;e in arguments;++e)t=r(arguments[e]),n[t]=!0;a(this,n)},contains:function(t){return t in o(this)},remove:function(){for(var t,n=o(this),e=0;e in arguments;++e)t=r(arguments[e]),n[t]=!1;a(this,n)},toggle:function(t){var n=o(this),e=1 in arguments?!arguments[1]:r(t)in n;return n[t]=!e,a(this,n),!e}}}(Array.prototype.join,Array.prototype.splice),function(n){Object.defineProperty(Element.prototype,"classList",{get:function(){function e(){n.apply(o,[0,o.length].concat((a.className||"").replace(/^\s+|\s+$/g,"").split(/\s+/)))}function r(){a.attachEvent&&a.detachEvent("onpropertychange",e),a.className=c.toString.call(o),a.attachEvent&&a.attachEvent("onpropertychange",e)}var o,a=this,i=t.DOMTokenList,c=i.prototype,u=function(){};return u.prototype=new i,u.prototype.item=function(t){return e(),c.item.apply(o,arguments)},u.prototype.toString=function(){return e(),c.toString.apply(o,arguments)},u.prototype.add=function(){return e(),c.add.apply(o,arguments),r()},u.prototype.contains=function(t){return e(),c.contains.apply(o,arguments)},u.prototype.remove=function(){return e(),c.remove.apply(o,arguments),r()},u.prototype.toggle=function(t){return e(),t=c.toggle.apply(o,arguments),r(),t},o=new u,a.attachEvent&&a.attachEvent("onpropertychange",e),o}})}(Array.prototype.splice))}(this);
--------------------------------------------------------------------------------
/test/mapping.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | var expect = require("chai").expect;
3 |
4 | var createMapping = require("../src/maquette").createMapping;
5 |
6 | var addAllPermutations = function(results, result, unusedNumbers, numbersToAdd) {
7 | if (numbersToAdd === 0) {
8 | results.push(result);
9 | }
10 | for (var i=0;i ", permutations[i], permutations[j]);
64 | mapping.map(permutations[j]);
65 | checkMapping(mapping, permutations[j]);
66 | }
67 | }
68 | });
69 | });
70 | })
--------------------------------------------------------------------------------
/test/createDom.js:
--------------------------------------------------------------------------------
1 | /* globals describe,it */
2 | var maquette = require("../src/maquette.js");
3 | var assert = require("assert");
4 | var jsdom = require('mocha-jsdom');
5 | var expect = require('chai').expect;
6 |
7 | var h = maquette.h;
8 |
9 | describe('Maquette', function () {
10 | describe('#createDom()', function () {
11 |
12 | jsdom();
13 |
14 | it("should create and update single textnodes", function () {
15 | var projection = maquette.dom.create(h("div", ["text"]));
16 | expect(projection.domNode.outerHTML).to.equal("
To run an execution time test on this page, run the profiler from your browser's developer tools and measure the running time of a page refresh. (Lower is better)
7 |
8 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/examples/todomvc/js/components/todoComponent.js:
--------------------------------------------------------------------------------
1 | window.createTodoComponent = function (todoList, id, title) {
2 |
3 | 'use strict';
4 |
5 | // Think of a component as being a View (the renderMaquette() function) combined with a ViewModel (the rest).
6 |
7 | var h = window.maquette.h;
8 | var ENTER_KEY = 13;
9 | var ESC_KEY = 27;
10 |
11 | // State
12 |
13 | var renderCache = window.maquette.createCache(); // We use a cache here just for demonstration purposes, performance is usually not an issue at all.
14 | var editingTitle = null;
15 |
16 | // Helper functions
17 |
18 | var acceptEdit = function () {
19 | todoComponent.title = editingTitle.trim();
20 | if(!todoComponent.title) {
21 | todoList.editTodo(null);
22 | todoList.removeTodo(todoComponent);
23 | } else {
24 | todoList.todoTitleUpdated(todoComponent);
25 | todoList.editTodo(null);
26 | editingTitle = null;
27 | }
28 | };
29 |
30 | var focusEdit = function (domNode) {
31 | if(window.setImmediate) {
32 | window.setImmediate(function () { // IE weirdness
33 | domNode.focus();
34 | domNode.selectionStart = 0;
35 | domNode.selectionEnd = domNode.value.length;
36 | });
37 | } else {
38 | domNode.focus();
39 | domNode.selectionStart = 0;
40 | domNode.selectionEnd = domNode.value.length;
41 | }
42 | };
43 |
44 | // Event handlers
45 |
46 | var handleDestroyClick = function (evt) {
47 | evt.preventDefault();
48 | todoList.removeTodo(todoComponent);
49 | };
50 |
51 | var handleToggleClick = function (evt) {
52 | evt.preventDefault();
53 | todoComponent.completed = !todoComponent.completed;
54 | todoList.todoCompletedUpdated(todoComponent, todoComponent.completed);
55 | };
56 |
57 | var handleLabelDoubleClick = function (evt) {
58 | editingTitle = todoComponent.title;
59 | todoList.editTodo(todoComponent);
60 | evt.preventDefault();
61 | };
62 |
63 | var handleEditInput = function (evt) {
64 | editingTitle = evt.target.value;
65 | };
66 |
67 | var handleEditKeyUp = function (evt) {
68 | if (evt.keyCode == ENTER_KEY) {
69 | acceptEdit();
70 | }
71 | if (evt.keyCode == ESC_KEY) {
72 | todoList.editTodo(null);
73 | editingTitle = null;
74 | }
75 | };
76 |
77 | var handleEditBlur = function (evt) {
78 | if (todoList.editingTodo === todoComponent) {
79 | acceptEdit();
80 | }
81 | };
82 |
83 | // Public API of this component
84 |
85 | var todoComponent = {
86 | id: id,
87 | title: title,
88 | completed: false,
89 |
90 | renderMaquette: function () {
91 | var editing = todoList.editingTodo === todoComponent;
92 |
93 | return renderCache.result([todoComponent.completed, todoComponent.title, editing], function () {
94 | return h("li", { key: todoComponent, classes: { completed: todoComponent.completed, editing: editing } },
95 | editing ? [
96 | h("input.edit", { value: editingTitle, oninput: handleEditInput, onkeyup: handleEditKeyUp, onblur: handleEditBlur, afterCreate: focusEdit })
97 | ] : [
98 | h("div.view",
99 | h("input.toggle", { type: "checkbox", checked: todoComponent.completed, onclick: handleToggleClick }),
100 | h("label", { ondblclick: handleLabelDoubleClick }, todoComponent.title),
101 | h("button.destroy", { onclick: handleDestroyClick })
102 | )
103 | ]
104 | );
105 | });
106 | }
107 | };
108 |
109 | return todoComponent;
110 | };
111 |
--------------------------------------------------------------------------------
/examples/todomvc/benchmark.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Template • TodoMVC
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/browser-tests/test/setup.js:
--------------------------------------------------------------------------------
1 | var Q = require('q');
2 | var expect = require("chai").expect;
3 | var wd = require("wd");
4 | var connect = require("connect");
5 |
6 | // double click is not 'natively' supported, so we need to send the
7 | // event direct to the element see:
8 | // http://stackoverflow.com/questions/3982442/selenium-2-webdriver-how-to-double-click-a-table-row-which-opens-a-new-window
9 | var doubleClickScript = 'var evt = document.createEvent("MouseEvents");' +
10 | 'evt.initMouseEvent("dblclick",true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0,null);' +
11 | 'document.querySelectorAll("#todo-list li label")[arguments[0]].dispatchEvent(evt);';
12 |
13 | var finalhandler = require('finalhandler');
14 | var http = require('http');
15 | var serveStatic = require('serve-static');
16 |
17 | // Serve up public/ftp folder
18 | var serve = serveStatic('..', {});
19 |
20 | // Create server
21 | var server = http.createServer(function (req, res) {
22 | var done = finalhandler(req, res);
23 | serve(req, res, done);
24 | });
25 |
26 | // Listen
27 | console.log("starting server on port 8000");
28 | server.listen(8000);
29 |
30 | // http configuration, not needed for simple runs
31 | wd.configureHttp({
32 | timeout: 60000,
33 | retryDelay: 15000,
34 | retries: 5
35 | });
36 |
37 | var createBrowser = function () {
38 | var desired = {};
39 | Object.keys(setup.browserCapabilities).forEach(function (key) {
40 | desired[key] = setup.browserCapabilities[key];
41 | });
42 | desired.tags = ['maquette'];
43 | if (process.env.TRAVIS_BUILD_NUMBER) {
44 | desired.build = "build-" + process.env.TRAVIS_BUILD_NUMBER;
45 | }
46 | if (process.env.TRAVIS_JOB_NUMBER) {
47 | desired["tunnel-identifier"] = process.env.TRAVIS_JOB_NUMBER;
48 | }
49 | var browser;
50 | if (setup.sauce) {
51 | if(!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
52 | throw new Error(
53 | 'Sauce credentials were not configured, configure your sauce credential as follows:\n\n' +
54 | 'export SAUCE_USERNAME=\n' +
55 | 'export SAUCE_ACCESS_KEY=\n\n'
56 | );
57 | }
58 | var username = process.env.SAUCE_USERNAME;
59 | var accessKey = process.env.SAUCE_ACCESS_KEY;
60 | browser = wd.promiseChainRemote("localhost", 4445, username, accessKey);
61 | } else {
62 | browser = wd.promiseChainRemote("localhost", 4444, null, null);
63 | }
64 | if (true || process.env.VERBOSE) {
65 | // optional logging
66 | browser.on('status', function (info) {
67 | console.log(info.cyan);
68 | });
69 | browser.on('command', function (meth, path, data) {
70 | console.log(' > ' + meth.yellow, path.grey, data || '');
71 | });
72 | }
73 | return browser
74 | .init(desired)
75 | .setAsyncScriptTimeout(3000)
76 | .then(function () {
77 | if(process.platform === "win32" && setup.rootUrl.indexOf("localhost") !== -1) {
78 | // Hack needed for sauce on windows
79 | var deferred = Q.defer();
80 | require('dns').lookup(require('os').hostname(), function (err, add, fam) {
81 | console.log('local ip: ' + add);
82 | setup.rootUrl = setup.rootUrl.replace("localhost", add);
83 | deferred.resolve(browser);
84 | });
85 | return deferred.promise;
86 | } else {
87 | return browser;
88 | }
89 | });
90 | };
91 |
92 | var quitBrowser = function (browser, allPassed) {
93 | if(browser) {
94 | browser = browser.quit();
95 | if(setup.sauce) {
96 | browser = browser.sauceJobStatus(allPassed);
97 | }
98 | }
99 | return browser;
100 | };
101 |
102 | var setup = {
103 | rootUrl: 'http://localhost:8000',
104 | server: server,
105 | browserCapabilities: { browserName: "chrome" },
106 | sauce: false,
107 | createBrowser: createBrowser, // returns a promise for a browser
108 | quitBrowser: quitBrowser
109 | };
110 |
111 | module.exports = setup;
--------------------------------------------------------------------------------
/examples/todomvc/js/models/model.js:
--------------------------------------------------------------------------------
1 | (function (window) {
2 | 'use strict';
3 |
4 | /**
5 | * Creates a new Model instance and hooks up the storage.
6 | *
7 | * @constructor
8 | * @param {object} storage A reference to the client side storage class
9 | */
10 | window.model = function (storage) {
11 |
12 | return {
13 |
14 | /**
15 | * Creates a new todo model
16 | *
17 | * @param {string} [title] The title of the task
18 | * @param {function} [callback] The callback to fire after the model is created
19 | */
20 | create: function (title, callback) {
21 | title = title || '';
22 | callback = callback || function () { };
23 |
24 | var newItem = {
25 | title: title.trim(),
26 | completed: false
27 | };
28 |
29 | storage.save(newItem, callback);
30 | },
31 |
32 | /**
33 | * Finds and returns a model in storage. If no query is given it'll simply
34 | * return everything. If you pass in a string or number it'll look that up as
35 | * the ID of the model to find. Lastly, you can pass it an object to match
36 | * against.
37 | *
38 | * @param {string|number|object} [query] A query to match models against
39 | * @param {function} [callback] The callback to fire after the model is found
40 | *
41 | * @example
42 | * model.read(1, func); // Will find the model with an ID of 1
43 | * model.read('1'); // Same as above
44 | * //Below will find a model with foo equalling bar and hello equalling world.
45 | * model.read({ foo: 'bar', hello: 'world' });
46 | */
47 | read: function (query, callback) {
48 | var queryType = typeof query;
49 | callback = callback || function () { };
50 |
51 | if (queryType === 'function') {
52 | callback = query;
53 | storage.findAll(callback);
54 | } else if (queryType === 'string' || queryType === 'number') {
55 | query = parseInt(query, 10);
56 | storage.find({ id: query }, callback);
57 | } else {
58 | storage.find(query, callback);
59 | }
60 | },
61 |
62 | /**
63 | * Updates a model by giving it an ID, data to update, and a callback to fire when
64 | * the update is complete.
65 | *
66 | * @param {number} id The id of the model to update
67 | * @param {object} data The properties to update and their new value
68 | * @param {function} callback The callback to fire when the update is complete.
69 | */
70 | update: function (id, data, callback) {
71 | storage.save(data, callback, id);
72 | },
73 |
74 | /**
75 | * Removes a model from storage
76 | *
77 | * @param {number} id The ID of the model to remove
78 | * @param {function} callback The callback to fire when the removal is complete.
79 | */
80 | remove: function (id, callback) {
81 | storage.remove(id, callback);
82 | },
83 |
84 | /**
85 | * WARNING: Will remove ALL data from storage.
86 | *
87 | * @param {function} callback The callback to fire when the storage is wiped.
88 | */
89 | removeAll: function (callback) {
90 | storage.drop(callback);
91 | },
92 |
93 | /**
94 | * Returns a count of all todos
95 | */
96 | getCount: function (callback) {
97 | var todos = {
98 | active: 0,
99 | completed: 0,
100 | total: 0
101 | };
102 |
103 | storage.findAll(function (data) {
104 | data.forEach(function (todo) {
105 | if (todo.completed) {
106 | todos.completed++;
107 | } else {
108 | todos.active++;
109 | }
110 |
111 | todos.total++;
112 | });
113 | callback(todos);
114 | });
115 | }
116 | };
117 | };
118 |
119 | })(window);
--------------------------------------------------------------------------------
/examples/todomvc/js/models/store.js:
--------------------------------------------------------------------------------
1 | // This is just a copy of the vanilla implementation, rewritten without prototype
2 | (function (window) {
3 |
4 | 'use strict';
5 |
6 | /**
7 | * Creates a new client side storage object and will create an empty
8 | * collection if no collection already exists.
9 | *
10 | * @param {string} name The name of our DB we want to use
11 | * NOTE: Our fake DB uses callbacks because in
12 | * real life you probably would be making AJAX calls
13 | */
14 | window.store = function (name) {
15 | var data;
16 | if (!localStorage[name]) {
17 | data = { todos: [] };
18 | localStorage[name] = JSON.stringify(data);
19 | } else {
20 | data = JSON.parse(localStorage[name]);
21 | }
22 |
23 | var flushTimeout = null;
24 | var flush = function () {
25 | if(!flushTimeout) {
26 | flushTimeout = setTimeout(function () {
27 | flushTimeout = null;
28 | localStorage[name] = JSON.stringify(data);
29 | });
30 | }
31 | };
32 |
33 |
34 | return {
35 |
36 | /**
37 | * Finds items based on a query given as a JS object
38 | *
39 | * @param {object} query The query to match against (i.e. {foo: 'bar'})
40 | * @param {function} callback The callback to fire when the query has
41 | * completed running
42 | *
43 | * @example
44 | * db.find({foo: 'bar', hello: 'world'}, function (data) {
45 | * // data will return any items that have foo: bar and
46 | * // hello: world in their properties
47 | * });
48 | */
49 | find: function (query, callback) {
50 | if (!callback) {
51 | return;
52 | }
53 |
54 | var todos = data.todos;
55 |
56 | callback.call(undefined, todos.filter(function (todo) {
57 | for (var q in query) {
58 | if (query[q] !== todo[q]) {
59 | return false;
60 | }
61 | }
62 | return true;
63 | }));
64 | },
65 |
66 | /**
67 | * Will retrieve all data from the collection
68 | *
69 | * @param {function} callback The callback to fire upon retrieving data
70 | */
71 | findAll: function (callback) {
72 | callback = callback || function () { };
73 | callback.call(undefined, data.todos);
74 | },
75 |
76 | /**
77 | * Will save the given data to the DB. If no item exists it will create a new
78 | * item, otherwise it'll simply update an existing item's properties
79 | *
80 | * @param {object} updateData The data to save back into the DB
81 | * @param {function} callback The callback to fire after saving
82 | * @param {number} id An optional param to enter an ID of an item to update
83 | */
84 | save: function (updateData, callback, id) {
85 |
86 | var todos = data.todos;
87 |
88 | callback = callback || function () { };
89 |
90 | // If an ID was actually given, find the item and update each property
91 | if (id) {
92 | for (var i = 0; i < todos.length; i++) {
93 | if (todos[i].id === id) {
94 | for (var key in updateData) {
95 | todos[i][key] = updateData[key];
96 | }
97 | break;
98 | }
99 | }
100 |
101 | flush();
102 | callback.call(undefined, data.todos);
103 | } else {
104 | // Generate an ID
105 | updateData.id = new Date().getTime();
106 |
107 | todos.push(updateData);
108 | flush();
109 | callback.call(undefined, [updateData]);
110 | }
111 | },
112 |
113 | /**
114 | * Will remove an item from the Store based on its ID
115 | *
116 | * @param {number} id The ID of the item you want to remove
117 | * @param {function} callback The callback to fire after saving
118 | */
119 | remove: function (id, callback) {
120 | var todos = data.todos;
121 |
122 | for (var i = 0; i < todos.length; i++) {
123 | if (todos[i].id == id) {
124 | todos.splice(i, 1);
125 | break;
126 | }
127 | }
128 |
129 | flush();
130 | callback.call(undefined, data.todos);
131 | },
132 |
133 | /**
134 | * Will drop all storage and start fresh
135 | *
136 | * @param {function} callback The callback to fire after dropping the data
137 | */
138 | drop: function (callback) {
139 | data = { todos: [] };
140 | flush();
141 | callback.call(undefined, data.todos);
142 | }
143 |
144 | };
145 | };
146 | })(window);
--------------------------------------------------------------------------------
/test/jsdom-classlist-polyfill.js:
--------------------------------------------------------------------------------
1 | // classList is not supported by jsdom, so we need to add this one ourselves.
2 | // Copied from/inspired by
3 | // https://github.com/tmpvar/jsdom/issues/510
4 |
5 | /*
6 | * classList.js: Cross-browser full element.classList implementation.
7 | * 2012-11-15
8 | *
9 | * By Eli Grey, http://eligrey.com
10 | * Public Domain.
11 | * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
12 | */
13 |
14 | /*global self, document, DOMException */
15 |
16 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js*/
17 |
18 | module.exports = function (view) {
19 |
20 | "use strict";
21 |
22 | if (!('HTMLElement' in view) && !('Element' in view)) return;
23 |
24 | var
25 | classListProp = "classList"
26 | , protoProp = "prototype"
27 | , elemCtrProto = (view.HTMLElement || view.Element)[protoProp]
28 | , objCtr = Object
29 | , strTrim = String[protoProp].trim || function () {
30 | return this.replace(/^\s+|\s+$/g, "");
31 | }
32 | , arrIndexOf = Array[protoProp].indexOf || function (item) {
33 | var
34 | i = 0
35 | , len = this.length
36 | ;
37 | for (; i < len; i++) {
38 | if (i in this && this[i] === item) {
39 | return i;
40 | }
41 | }
42 | return -1;
43 | }
44 | // Vendors: please allow content code to instantiate DOMExceptions
45 | , DOMEx = function (type, message) {
46 | this.name = type;
47 | this.code = DOMException[type];
48 | this.message = message;
49 | }
50 | , checkTokenAndGetIndex = function (classList, token) {
51 | if (token === "") {
52 | throw new DOMEx(
53 | "SYNTAX_ERR"
54 | , "An invalid or illegal string was specified"
55 | );
56 | }
57 | if (/\s/.test(token)) {
58 | throw new DOMEx(
59 | "INVALID_CHARACTER_ERR"
60 | , "String contains an invalid character"
61 | );
62 | }
63 | return arrIndexOf.call(classList, token);
64 | }
65 | , ClassList = function (elem) {
66 | var
67 | trimmedClasses = strTrim.call(elem.className)
68 | , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
69 | , i = 0
70 | , len = classes.length
71 | ;
72 | for (; i < len; i++) {
73 | this.push(classes[i]);
74 | }
75 | this._updateClassName = function () {
76 | elem.className = this.toString();
77 | };
78 | }
79 | , classListProto = ClassList[protoProp] = []
80 | , classListGetter = function () {
81 | return new ClassList(this);
82 | }
83 | ;
84 | // Most DOMException implementations don't allow calling DOMException's toString()
85 | // on non-DOMExceptions. Error's toString() is sufficient here.
86 | DOMEx[protoProp] = Error[protoProp];
87 | classListProto.item = function (i) {
88 | return this[i] || null;
89 | };
90 | classListProto.contains = function (token) {
91 | token += "";
92 | return checkTokenAndGetIndex(this, token) !== -1;
93 | };
94 | classListProto.add = function () {
95 | var
96 | tokens = arguments
97 | , i = 0
98 | , l = tokens.length
99 | , token
100 | , updated = false
101 | ;
102 | do {
103 | token = tokens[i] + "";
104 | if (checkTokenAndGetIndex(this, token) === -1) {
105 | this.push(token);
106 | updated = true;
107 | }
108 | }
109 | while (++i < l);
110 |
111 | if (updated) {
112 | this._updateClassName();
113 | }
114 | };
115 | classListProto.remove = function () {
116 | var
117 | tokens = arguments
118 | , i = 0
119 | , l = tokens.length
120 | , token
121 | , updated = false
122 | ;
123 | do {
124 | token = tokens[i] + "";
125 | var index = checkTokenAndGetIndex(this, token);
126 | if (index !== -1) {
127 | this.splice(index, 1);
128 | updated = true;
129 | }
130 | }
131 | while (++i < l);
132 |
133 | if (updated) {
134 | this._updateClassName();
135 | }
136 | };
137 | classListProto.toggle = function (token, forse) {
138 | token += "";
139 |
140 | var
141 | result = this.contains(token)
142 | , method = result ?
143 | forse !== true && "remove"
144 | :
145 | forse !== false && "add"
146 | ;
147 |
148 | if (method) {
149 | this[method](token);
150 | }
151 |
152 | return !result;
153 | };
154 | classListProto.toString = function () {
155 | return this.join(" ");
156 | };
157 |
158 | if (objCtr.defineProperty) {
159 | var classListPropDesc = {
160 | get: classListGetter
161 | , enumerable: true
162 | , configurable: true
163 | };
164 | try {
165 | objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
166 | } catch (ex) { // IE 8 doesn't support enumerable:true
167 | if (ex.number === -0x7FF5EC54) {
168 | classListPropDesc.enumerable = false;
169 | objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
170 | }
171 | }
172 | } else if (objCtr[protoProp].__defineGetter__) {
173 | elemCtrProto.__defineGetter__(classListProp, classListGetter);
174 | }
175 |
176 | }
--------------------------------------------------------------------------------
/src/maquette-polyfills.js:
--------------------------------------------------------------------------------
1 | (function (global) {
2 |
3 | "use strict";
4 |
5 | // polyfill for window.requestAnimationFrame
6 | var haveraf = function(vendor) {
7 | return global.requestAnimationFrame && global.cancelAnimationFrame ||
8 | (
9 | (global.requestAnimationFrame = global[vendor + 'RequestAnimationFrame']) &&
10 | (global.cancelAnimationFrame = (global[vendor + 'CancelAnimationFrame'] ||
11 | global[vendor + 'CancelRequestAnimationFrame']))
12 | );
13 | };
14 |
15 | if (!haveraf('webkit') && !haveraf('moz') ||
16 | /iP(ad|hone|od).*OS 6/.test(global.navigator.userAgent)) { // buggy iOS6
17 |
18 | // Closures
19 | var now = Date.now || function() { return +new Date(); }; // pre-es5
20 | var lastTime = 0;
21 |
22 | // Polyfills
23 | global.requestAnimationFrame = function(callback) {
24 | var nowTime = now();
25 | var nextTime = Math.max(lastTime + 16, nowTime);
26 | return setTimeout(function() {
27 | callback(lastTime = nextTime);
28 | }, nextTime - nowTime);
29 | };
30 | global.cancelAnimationFrame = clearTimeout;
31 | }
32 |
33 | // polyfill for DOMTokenList and classList
34 | if(!("classList" in document.documentElement)) {
35 |
36 | (function (join, splice) {
37 | function tokenize(token) {
38 | if (/^-?[_a-zA-Z]+[_a-zA-Z0-9-]*$/.test(token)) {
39 | return String(token);
40 | } else {
41 | throw new Error('InvalidCharacterError: DOM Exception 5');
42 | }
43 | }
44 |
45 | function toObject(self) {
46 | for (var index = -1, object = {}, element; element = self[++index];) {
47 | object[element] = true;
48 | }
49 |
50 | return object;
51 | }
52 |
53 | function fromObject(self, object) {
54 | var array = [], token;
55 |
56 | for (token in object) {
57 | if (object[token]) {
58 | array.push(token);
59 | }
60 | }
61 |
62 | splice.apply(self, [0, self.length].concat(array));
63 | }
64 |
65 | // .DOMTokenlist
66 | global.DOMTokenList = function DOMTokenList() { };
67 |
68 | global.DOMTokenList.prototype = {
69 | constructor: DOMTokenList,
70 | item: function item(index) {
71 | return this[parseFloat(index)] || null;
72 | },
73 | length: Array.prototype.length,
74 | toString: function toString() {
75 | return join.call(this, ' ');
76 | },
77 |
78 | add: function add() {
79 | for (var object = toObject(this), index = 0, token; index in arguments; ++index) {
80 | token = tokenize(arguments[index]);
81 |
82 | object[token] = true;
83 | }
84 |
85 | fromObject(this, object);
86 | },
87 | contains: function contains(token) {
88 | return token in toObject(this);
89 | },
90 | remove: function remove() {
91 | for (var object = toObject(this), index = 0, token; index in arguments; ++index) {
92 | token = tokenize(arguments[index]);
93 |
94 | object[token] = false;
95 | }
96 |
97 | fromObject(this, object);
98 | },
99 | toggle: function toggle(token) {
100 | var
101 | object = toObject(this),
102 | contains = 1 in arguments ? !arguments[1] : tokenize(token) in object;
103 |
104 | object[token] = !contains;
105 |
106 | fromObject(this, object);
107 |
108 | return !contains;
109 | }
110 | };
111 | })(Array.prototype.join, Array.prototype.splice);
112 |
113 | //polyfill for classList
114 | (function (splice) {
115 | Object.defineProperty(Element.prototype, 'classList', {
116 | get: function () {
117 |
118 | function pull() {
119 | splice.apply(classList, [0, classList.length].concat((element.className || '').replace(/^\s+|\s+$/g, '').split(/\s+/)));
120 | }
121 |
122 | function push() {
123 | if(element.attachEvent) {
124 | element.detachEvent('onpropertychange', pull);
125 | }
126 |
127 | element.className = original.toString.call(classList);
128 |
129 | if(element.attachEvent) {
130 | element.attachEvent('onpropertychange', pull);
131 | }
132 | }
133 |
134 | var
135 | element = this,
136 | NativeDOMTokenList = global.DOMTokenList,
137 | original = NativeDOMTokenList.prototype,
138 | ClassList = function DOMTokenList() {},
139 | classList;
140 |
141 | ClassList.prototype = new NativeDOMTokenList;
142 |
143 | ClassList.prototype.item = function item(index) {
144 | return pull(), original.item.apply(classList, arguments);
145 | };
146 |
147 | ClassList.prototype.toString = function toString() {
148 | return pull(), original.toString.apply(classList, arguments);
149 | };
150 |
151 | ClassList.prototype.add = function add() {
152 | return pull(), original.add.apply(classList, arguments), push();
153 | };
154 |
155 | ClassList.prototype.contains = function contains(token) {
156 | return pull(), original.contains.apply(classList, arguments);
157 | };
158 |
159 | ClassList.prototype.remove = function remove() {
160 | return pull(), original.remove.apply(classList, arguments), push();
161 | };
162 |
163 | ClassList.prototype.toggle = function toggle(token) {
164 | return pull(), token = original.toggle.apply(classList, arguments), push(), token;
165 | };
166 |
167 | classList = new ClassList;
168 |
169 | if(element.attachEvent) {
170 | element.attachEvent('onpropertychange', pull);
171 | }
172 |
173 | return classList;
174 | }
175 | });
176 | })(Array.prototype.splice);
177 |
178 | }
179 |
180 | })(this);
181 |
--------------------------------------------------------------------------------
/examples/todomvc/js/components/todoListComponent.js:
--------------------------------------------------------------------------------
1 | window.createListComponent = function (mode, model) {
2 |
3 | 'use strict';
4 |
5 | // Think of a component as being a View (the renderMaquette() function) combined with a ViewModel (the rest).
6 |
7 | var h = window.maquette.h;
8 |
9 | // State
10 |
11 | // TODO: make functions of these 3:
12 | var checkedAll = true;
13 | var completedCount = 0;
14 | var itemsLeft = 0;
15 |
16 | var newTodoTitle = "";
17 | var todos = [];
18 |
19 | // Helper functions
20 |
21 | var addTodo = function () {
22 | var title = newTodoTitle.trim();
23 | if (title) {
24 | model.create(newTodoTitle, function (results) {
25 | var todo = createTodoComponent(listComponent, results[0].id, results[0].title);
26 | todos.push(todo);
27 | itemsLeft++;
28 | checkedAll = false;
29 | });
30 | }
31 | };
32 |
33 | var visibleInMode = function (todo) {
34 | switch(mode) {
35 | case "completed":
36 | return todo.completed === true;
37 | case "active":
38 | return todo.completed !== true;
39 | default:
40 | return true;
41 | }
42 | };
43 |
44 | var focus = function (element) {
45 | element.focus();
46 | };
47 |
48 | // event handlers
49 |
50 | var handleNewTodoKeypress = function (evt) {
51 | newTodoTitle = evt.target.value;
52 | if(evt.keyCode === 13 /* Enter */) {
53 | addTodo();
54 | newTodoTitle = "";
55 | evt.preventDefault();
56 | } else if(evt.keyCode === 27 /* Esc */) {
57 | newTodoTitle = "";
58 | evt.preventDefault();
59 | }
60 | };
61 |
62 | var handleNewTodoInput = function (evt) {
63 | newTodoTitle = evt.target.value;
64 | };
65 |
66 | var handleToggleAllClick = function (evt) {
67 | evt.preventDefault();
68 | checkedAll = !checkedAll;
69 | todos.forEach(function (todo) {
70 | if(todo.completed !== checkedAll) {
71 | todo.completed = checkedAll;
72 | model.update(todo.id, { title: todo.title, completed: checkedAll });
73 | }
74 | });
75 | if(checkedAll) {
76 | itemsLeft = 0;
77 | completedCount = todos.length;
78 | } else {
79 | itemsLeft = todos.length;
80 | completedCount = 0;
81 | }
82 | };
83 |
84 | var handleClearCompletedClick = function (evt) {
85 | for(var i = todos.length - 1; i >= 0; i--) {
86 | if(todos[i].completed) {
87 | listComponent.removeTodo(todos[i]);
88 | }
89 | }
90 | };
91 |
92 | // public interface (accessible from both app and todoComponent)
93 |
94 | var listComponent = {
95 | mode: mode,
96 | editingTodo: undefined, // the todoComponent currently being edited
97 | removeTodo: function (todo) {
98 | model.remove(todo.id, function () {
99 | todos.splice(todos.indexOf(todo), 1);
100 | if (todo.completed) {
101 | completedCount--;
102 | } else {
103 | itemsLeft--;
104 | checkedAll = completedCount === todos.length;
105 | }
106 | });
107 | },
108 |
109 | editTodo: function (todo) {
110 | listComponent.editingTodo = todo;
111 | },
112 |
113 | todoCompletedUpdated: function (todo, completed) {
114 | if(completed) {
115 | completedCount++;
116 | checkedAll = completedCount === todos.length;
117 | itemsLeft--;
118 | } else {
119 | completedCount--;
120 | checkedAll = false;
121 | itemsLeft++;
122 | }
123 | model.update(todo.id, { title: todo.title, completed: completed });
124 | },
125 |
126 | todoTitleUpdated: function (todo) {
127 | model.update(todo.id, { title: todo.title, completed: todo.completed });
128 | },
129 |
130 | renderMaquette: function () {
131 | var anyTodos = todos.length > 0;
132 |
133 | return h("section#todoapp", {key: listComponent},
134 | h("header#header",
135 | h("h1", "todos"),
136 | h("input#new-todo", {
137 | autofocus: true,
138 | placeholder: "What needs to be done?",
139 | onkeypress: handleNewTodoKeypress, oninput: handleNewTodoInput,
140 | value: newTodoTitle, afterCreate: focus
141 | })
142 | ),
143 | anyTodos ? [
144 | h("section#main", { key: mode },
145 | h("input#toggle-all", { type: "checkbox", checked: checkedAll, onclick: handleToggleAllClick }),
146 | h("label", { "for": "toggle-all" }, "Mark all as complete"),
147 | h("ul#todo-list",
148 | todos.filter(visibleInMode).map(function (todo) {
149 | return todo.renderMaquette();
150 | })
151 | )
152 | ),
153 | h("footer#footer",
154 | h("span#todo-count", {},
155 | h("strong", itemsLeft), itemsLeft === 1 ? " item left" : " items left"
156 | ),
157 | h("ul#filters", {},
158 | h("li", { key: "all" },
159 | h("a", { classes: {selected: mode === "all"}, href: "#/all" }, "All")
160 | ),
161 | h("li", { key: "active" },
162 | h("a", { classes: { selected: mode === "active" }, href: "#/active" }, "Active")
163 | ),
164 | h("li", { key: "completed" },
165 | h("a", { classes: { selected: mode === "completed" }, href: "#/completed" }, "Completed")
166 | )
167 | ),
168 | completedCount > 0 ? h("button#clear-completed", { onclick: handleClearCompletedClick }, "Clear completed (" + completedCount + ")") : null
169 | )
170 | ] : null
171 | );
172 | }
173 | };
174 |
175 | // Initializes the component by reading from the model
176 |
177 | model.read(function (data) {
178 | data.forEach(function (dataItem) {
179 | var todo = createTodoComponent(listComponent, dataItem.id, dataItem.title);
180 | todos.push(todo);
181 | if(dataItem.completed) {
182 | todo.completed = true;
183 | completedCount++;
184 | } else {
185 | itemsLeft++;
186 | checkedAll = false;
187 | }
188 | });
189 | });
190 |
191 | return listComponent;
192 | };
193 |
--------------------------------------------------------------------------------
/dist/maquette.min.js:
--------------------------------------------------------------------------------
1 | !function(e){"use strict";var r=[],t=function(e,r){var t={};return Object.keys(e).forEach(function(r){t[r]=e[r]}),r&&Object.keys(r).forEach(function(e){t[e]=r[e]}),t},n=function(e,r,t){for(var i=0;im;){var N=d>u?n[u]:void 0,S=o[m];if(void 0!==N&&f(N,S))g=w(N,S,i)||g,u++;else{var x=c(n,S,u+1);if(x>=0){for(a=u;x>a;a++)v(n[a],p),h(n,a,e,"removed");g=w(n[x],S,i)||g,u=x+1}else y(S,t,d>u?n[u].domNode:void 0,i),l(S,p),h(o,m,e,"added")}m++}if(d>u)for(a=u;d>a;a++)v(n[a],p),h(n,a,e,"removed");return g},y=function(e,r,n,o){var i,a,d,s,p,u=0,f=e.vnodeSelector;if(""===f)i=e.domNode=document.createTextNode(e.text),void 0!==n?r.insertBefore(i,n):r.appendChild(i);else{for(a=0;a<=f.length;++a)d=f.charAt(a),(a===f.length||"."===d||"#"===d)&&(s=f.charAt(u-1),p=f.slice(u,a),"."===s?i.classList.add(p):"#"===s?i.id=p:("svg"===p&&(o=t(o,{namespace:"http://www.w3.org/2000/svg"})),i=e.domNode=void 0!==o.namespace?document.createElementNS(o.namespace,p):document.createElement(p),void 0!==n?r.insertBefore(i,n):r.appendChild(i)),u=a+1);g(i,e,o)}},g=function(e,r,t){u(e,r.children,t),r.text&&(e.textContent=r.text),s(e,r.properties,t),r.properties&&r.properties.afterCreate&&r.properties.afterCreate(e,t,r.vnodeSelector,r.properties,r.children)},w=function(e,r,n){var o=e.domNode;if(!o)throw new Error("previous node was not rendered");var i=!1;if(e===r)return i;var a=!1;return""===r.vnodeSelector?r.text!==e.text&&(o.nodeValue=r.text,i=!0):(0===r.vnodeSelector.lastIndexOf("svg",0)&&(n=t(n,{namespace:"http://www.w3.org/2000/svg"})),e.text!==r.text&&(a=!0,void 0===r.text?o.removeChild(o.firstChild):o.textContent=r.text),a=m(r,o,e.children,r.children,n)||a,a=p(o,e.properties,r.properties,n)||a,r.properties&&r.properties.afterUpdate&&r.properties.afterUpdate(o,n,r.vnodeSelector,r.properties,r.children)),a&&r.properties&&r.properties.updateAnimation&&r.properties.updateAnimation(o,r.properties,e.properties),r.domNode=e.domNode,i},N=function(e,r){if(!e.vnodeSelector)throw new Error("Invalid vnode argument");return{update:function(t){if(e.vnodeSelector!==t.vnodeSelector)throw new Error("The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)");w(e,t,r),e=t},domNode:e.domNode}},S={h:function(e,r,t){if("string"!=typeof e)throw new Error;var i=1;!r||r.hasOwnProperty("vnodeSelector")||Array.isArray(r)||"object"!=typeof r?r=void 0:i=2;var a=void 0,d=void 0,s=arguments.length;if(s===i+1){var p=arguments[i];"string"==typeof p?a=p:1===p.length&&"string"==typeof p[0]&&(a=p[0])}if(void 0===a)for(d=[];i
4 | // Definitions: https://github.com/johan-gorter/DefinitelyTyped
5 |
6 | export interface VNodeProperties {
7 | enterAnimation?: ((element: Element, properties?: VNodeProperties) => void) | string;
8 | exitAnimation?: ((element: Element, removeElement: () => void, properties?: VNodeProperties) => void) | string;
9 | updateAnimation?: (element: Element, properties?: VNodeProperties, previousProperties?: VNodeProperties) => void;
10 | afterCreate?: (element: Element, projectionOptions: ProjectionOptions, vnodeSelector: string, properties: VNodeProperties,
11 | children: VNode[]) => void;
12 | afterUpdate?: (element: Element, projectionOptions: ProjectionOptions, vnodeSelector: string, properties: VNodeProperties,
13 | children: VNode[]) => void;
14 | key?: Object;
15 | classes?: {[index:string]: boolean};
16 | styles?: {[index:string]: string};
17 |
18 | // From Element
19 | ontouchcancel?: (ev?: TouchEvent) => boolean|void;
20 | ontouchend?: (ev?: TouchEvent) => boolean|void;
21 | ontouchmove?: (ev?: TouchEvent) => boolean|void;
22 | ontouchstart?: (ev?: TouchEvent) => boolean|void;
23 | // From HTMLFormElement
24 | action?: string;
25 | encoding?: string;
26 | enctype?: string;
27 | method?: string;
28 | name?: string;
29 | target?: string;
30 | // From HTMLElement
31 | onblur?: (ev?: FocusEvent) => boolean|void;
32 | onchange?: (ev?: Event) => boolean|void;
33 | onclick?: (ev?: MouseEvent) => boolean|void;
34 | ondblclick?: (ev?: MouseEvent) => boolean|void;
35 | onfocus?: (ev?: FocusEvent) => boolean|void;
36 | oninput?: (ev?: Event) => boolean|void;
37 | onkeydown?: (ev?: KeyboardEvent) => boolean|void;
38 | onkeypress?: (ev?: KeyboardEvent) => boolean|void;
39 | onkeyup?: (ev?: KeyboardEvent) => boolean|void;
40 | onload?: (ev?: Event) => boolean|void;
41 | onmousedown?: (ev?: MouseEvent) => boolean|void;
42 | onmouseenter?: (ev?: MouseEvent) => boolean|void;
43 | onmouseleave?: (ev?: MouseEvent) => boolean|void;
44 | onmousemove?: (ev?: MouseEvent) => boolean|void;
45 | onmouseout?: (ev?: MouseEvent) => boolean|void;
46 | onmouseover?: (ev?: MouseEvent) => boolean|void;
47 | onmouseup?: (ev?: MouseEvent) => boolean|void;
48 | onmousewheel?: (ev?: MouseWheelEvent) => boolean|void;
49 | onscroll?: (ev?: UIEvent) => boolean|void;
50 | onsubmit?: (ev?: Event) => boolean|void;
51 | spellcheck?: boolean;
52 | tabIndex?: number;
53 | title?: string;
54 | accessKey?: string;
55 | id?: string;
56 | // From HTMLInputElement
57 | autocomplete?: string;
58 | checked?: boolean;
59 | placeholder?: string;
60 | readOnly?: boolean;
61 | src?: string;
62 | value?: string;
63 | // From HTMLImageElement
64 | alt?: string;
65 | srcset?: string;
66 |
67 | // Everything else (uncommon or custom properties and attributes)
68 | [index: string]: Object;
69 | }
70 |
71 | export interface ProjectionOptions {
72 | transitions?: {
73 | enter: (element: Element, properties: VNodeProperties, enterAnimation: string) => void;
74 | exit: (element: Element, properties: VNodeProperties, exitAnimation: string, removeElement: () => void) => void;
75 | }
76 | }
77 |
78 | // The following line is not possible in Typescript, hence the workaround in the two lines below
79 | // export type VNodeChild = string|VNode|Array
80 | export interface VNodeChildren extends Array {} // A bit of a hack to create a recursive type
81 | export type VNodeChild = string|VNode|VNodeChildren;
82 |
83 | export var dom: MaquetteDom;
84 |
85 | /**
86 | * Creates a {@link CalculationCache} object, useful for caching {@link VNode} trees.
87 | * In practice, caching of {@link VNode} trees is not needed, because achieving 60 frames per second is almost never a problem.
88 | * @returns {CalculationCache}
89 | */
90 | export function createCache(): CalculationCache;
91 |
92 | /**
93 | * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
94 | * @param {function} getSourceKey - `function(source)` that must return a key to identify each source object. The result must eather be a string or a number.
95 | * @param {function} createResult - `function(source, index)` that must create a new result object from a given source. This function is identical argument of `Array.map`.
96 | * @param {function} updateResult - `function(source, target, index)` that updates a result to an updated source.
97 | * @returns {Mapping}
98 | */
99 | export function createMapping(
100 | getSourceKey: (source: Source) => (string|number),
101 | createResult: (source: Source, index:number) => Target,
102 | updateResult: (source: Source, target: Target, index: number) => void): Mapping;
103 |
104 | /**
105 | * Creates a {@link Projector} instance using the provided projectionOptions.
106 | * @param {Object} projectionOptions - Options that influence how the DOM is rendered and updated.
107 | * projectionOptions.transitions - A transition strategy to invoke when
108 | * enterAnimation and exitAnimation properties are provided as strings.
109 | * The module `cssTransitions` in the provided `css-transitions.js` file provides such a strategy.
110 | * A transition strategy is not needed when enterAnimation and exitAnimation properties are provided as functions.
111 | * @returns {Projector}
112 | */
113 | export function createProjector(projectionOptions? : ProjectionOptions) : Projector;
114 |
115 | /**
116 | * Creates a virtual DOM node, used to render a real DOM later.
117 | * The `h` stands for (virtual) hyperscript.
118 | *
119 | * @param {string} selector - Contains the tagName, id and fixed css classnames in CSS selector format. It is formatted as follows: `tagname.cssclass1.cssclass2#id`.
120 | * @param {Object} properties - An object literal containing attributes, properties, event handlers and more be placed on the DOM node.
121 | * @param {Array} children - An array of virtual DOM nodes and strings to add as child nodes. May contain nested arrays, null or undefined.
122 | *
123 | * @returns {VNode} A VNode object, used to render a real DOM later.
124 | */
125 | export function h(selector: string, properties: VNodeProperties, children: Array): VNode;
126 | /**
127 | * Creates a virtual DOM node, used to render a real DOM later.
128 | * The `h` stands for (virtual) hyperscript.
129 | *
130 | * @param {string} selector - Contains the tagName, id and fixed css classnames in CSS selector format. It is formatted as follows: `tagname.cssclass1.cssclass2#id`.
131 | * @param {Array} children - An array of virtual DOM nodes and strings to add as child nodes. May contain nested arrays, null or undefined.
132 | *
133 | * @returns {VNode} A VNode object, used to render a real DOM later.
134 | */
135 | export function h(selector: string, children: Array): VNode;
136 | /**
137 | * Creates a virtual DOM node, used to render a real DOM later.
138 | * The `h` stands for (virtual) hyperscript.
139 | *
140 | * @param {string} selector - Contains the tagName, id and fixed css classnames in CSS selector format. It is formatted as follows: `tagname.cssclass1.cssclass2#id`.
141 | * @param {Object} properties - An object literal containing attributes, properties, event handlers and more be placed on the DOM node.
142 | *
143 | * @returns {VNode} A VNode object, used to render a real DOM later.
144 | */
145 | export function h(selector: string, properties: VNodeProperties): VNode;
146 | /**
147 | * Creates a virtual DOM node, used to render a real DOM later.
148 | * The `h` stands for (virtual) hyperscript.
149 | *
150 | * @param {string} selector - Contains the tagName, id and fixed css classnames in CSS selector format. It is formatted as follows: `tagname.cssclass1.cssclass2#id`.
151 | *
152 | * @returns {VNode} A VNode object, used to render a real DOM later.
153 | */
154 | export function h(selector: string): VNode;
155 |
156 | /**
157 | * A virtual representation of a DOM Node. Maquette assumes that {@link VNode} objects are never modified externally.
158 | * Instances of {@link VNode} can be created using {@link module:maquette.h}.
159 | */
160 | export interface VNode {
161 | vnodeSelector: string;
162 | properties: VNodeProperties;
163 | children: Array;
164 | }
165 |
166 | // Not used anywhere in the maquette sourcecode, but it is a widely used pattern.
167 | export interface Component {
168 | renderMaquette() : VNode;
169 | }
170 |
171 | /**
172 | * A CalculationCache object remembers the previous outcome of a calculation along with the inputs.
173 | * On subsequent calls the previous outcome is returned if the inputs are identical.
174 | * This object can be used to bypass both rendering and diffing of a virtual DOM subtree.
175 | * Instances of {@link CalculationCache} can be created using {@link module:maquette.createCache}.
176 | */
177 | export interface CalculationCache {
178 | /**
179 | * Manually invalidates the cached outcome.
180 | */
181 | invalidate(): void;
182 | /**
183 | * If the inputs array matches the inputs array from the previous invocation, this method returns the result of the previous invocation.
184 | * Otherwise, the calculation function is invoked and its result is cached and returned.
185 | * Objects in the inputs array are compared using ===.
186 | * @param {Object[]} inputs - Array of objects that are to be compared using === with the inputs from the previous invocation.
187 | * These objects are assumed to be immutable primitive values.
188 | * @param {function} calculation - Function that takes zero arguments and returns an object (A {@link VNode} assumably) that can be cached.
189 | */
190 | result(inputs: Array