├── .jshintrc ├── examples ├── todomvc │ ├── bower.json │ ├── js │ │ ├── app.js │ │ ├── components │ │ │ ├── todoRouter.js │ │ │ ├── todoComponent.js │ │ │ └── todoListComponent.js │ │ └── models │ │ │ ├── model.js │ │ │ └── store.js │ ├── index.html │ ├── readme.md │ └── benchmark.html ├── enhancement │ ├── todoComponent.js │ ├── index.html │ └── index.js ├── helloworld │ └── index.html ├── transitions │ ├── demo-velocity.html │ └── demo-css.html └── benchmarks │ └── loadAndRender.html ├── CONTRIBUTING.md ├── jsdoc.json ├── browser-tests ├── desireds.js ├── package.json ├── README.md ├── sauce.js ├── Gruntfile.js └── test │ ├── setup.js │ ├── todoPage.js │ └── todomvc-specs.js ├── .travis.yml ├── .gitattributes ├── README.md ├── bower.json ├── index.html ├── dist ├── css-transitions.min.js ├── maquette-polyfills.min.js └── maquette.min.js ├── test ├── animations.js ├── cache.js ├── h.js ├── styles.js ├── mapping.js ├── createDom.js └── jsdom-classlist-polyfill.js ├── LICENSE ├── .gitignore ├── package.json ├── scripts └── release.js ├── src ├── extras │ ├── maquette-extras.js │ └── makeh.js ├── css-transitions.js ├── maquette-polyfills.js └── maquette.js ├── gulpfile.js └── maquette.d.ts /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "predef": [ "define" ], 5 | "unused": "var", 6 | "undef": true 7 | } 8 | -------------------------------------------------------------------------------- /examples/todomvc/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-template", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "todomvc-common": "~0.3.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Before you may contribute you need to sign our Contributor License Agreement. 4 | 5 | Thanks in advance. -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc","closure"] 5 | }, 6 | "source": { 7 | "include": ["src/maquette.js"] 8 | }, 9 | "plugins": ["plugins/markdown"], 10 | "templates": { 11 | "default" : { 12 | "outputSourceFiles": false, 13 | "includeDate": false 14 | }, 15 | "cleverLinks": false, 16 | "monospaceLinks": false 17 | }, 18 | "opts": { 19 | "package": "./package.json", 20 | "destination": "./docs/" 21 | } 22 | } -------------------------------------------------------------------------------- /browser-tests/desireds.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | chrome: {browserName: 'chrome'}, 5 | // firefox: {browserName: 'firefox', platform: "windows 8.1"}, 6 | // ie11: { browserName: 'internet explorer', version: "11" }, 7 | // ie10: { browserName: 'internet explorer', version: "10" }, 8 | // ie9: { browserName: 'internet explorer', version: "9" }, 9 | // iphone: {browserName: 'iphone', version: '7.1'}, 10 | // android: {browserName: 'android', version: '4.4'}, 11 | // safari: {browserName: 'safari', version: '7'} 12 | }; 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - secure: L7CXe36PJw0l4PxEGK0erpHxi4fHXf43S9tUOLH7HblbJn+Jl6WhUFlFHizMQ2cvHLj1djv3gRem0FhL2hzLMN88pmpziXDBcPQXvBg1f/EAs0AZrBH62hqile9zvyebK3dY4O1SsHR6DK54VSGBByQYbQIbADL7vEmG1mwP9fc= 4 | - secure: AxnDnt9Sr7CwzHbFEB20GJm5GTweCGlyIhaN9dmHqUJs//UmskqLDkIczCLpgmV2vFbh2YXw6Nwy8YQThz6HhZTOLL0U5SQsyan1vSKTuOMzz949SPIghes7sOeP4sE7vN5vm51PFjMBsNF5fDyPV+K5wIyGH7u4rMVTbv4R0Ds= 5 | 6 | addons: 7 | sauce_connect: true 8 | 9 | language: node_js 10 | node_js: 11 | - "4.1" 12 | 13 | before_install: npm install -g grunt-cli bower 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/AFASSoftware/maquette.png?branch=master)](https://travis-ci.org/AFASSoftware/maquette) 2 | 3 | Maquette 4 | ========= 5 | 6 | Maquette is a Javascript utility which makes it easy to synchronize the DOM tree in the browser with your data. 7 | It uses a technique called 'Virtual DOM'. 8 | Compared to other virtual DOM implementations, maquette has 3 advantages: 9 | 10 | * It is very lightweight (3Kb gzipped) 11 | * It allows changes to be animated 12 | * It is optimized for speed 13 | 14 | Visit the [website](http://maquettejs.org) for more information. 15 | -------------------------------------------------------------------------------- /examples/enhancement/todoComponent.js: -------------------------------------------------------------------------------- 1 | window.createTodoComponent = function (message) { 2 | var h = maquette.h; 3 | 4 | var completed = false; 5 | 6 | var handleCompletedClick = function (evt) { 7 | evt.preventDefault(); 8 | completed = !completed; 9 | }; 10 | 11 | return { 12 | isCompleted: function () { 13 | return completed; 14 | }, 15 | renderMaquette: function () { 16 | return h("todo-component", [ 17 | h("div.message", [message]), 18 | h("input", { type: "checkbox", checked: completed, onclick: handleCompletedClick }) 19 | ]); 20 | } 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /examples/todomvc/js/app.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | 3 | 'use strict'; 4 | 5 | var maquette = window.maquette; 6 | 7 | // Using the vanilla JS implementation for Model and Store, nothing special here 8 | var model = new window.model(new window.store("todomvc-maquette")); 9 | 10 | var router = window.createRouter(model); 11 | 12 | document.addEventListener('DOMContentLoaded', function () { 13 | var projector = maquette.createProjector(); 14 | projector.merge(document.getElementsByTagName("main")[0], router.renderMaquette); 15 | window.onhashchange = function (evt) { 16 | projector.scheduleRender(); 17 | }; 18 | }); 19 | 20 | })(window); 21 | -------------------------------------------------------------------------------- /browser-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maquette-browser-tests", 3 | "description": "An automated test suite for maquette", 4 | "private": true, 5 | "devDependencies": { 6 | "chai": "latest", 7 | "chai-as-promised": "latest", 8 | "colors": "latest", 9 | "connect": "^2.27.1", 10 | "grunt": "^0.4.5", 11 | "grunt-cli": "^0.1.13", 12 | "grunt-concurrent": "latest", 13 | "grunt-contrib-connect": "^0.9.0", 14 | "grunt-env": "latest", 15 | "grunt-simple-mocha": "latest", 16 | "lodash": "^3.10.1", 17 | "q": "^1.4.1", 18 | "wd": "^0.3.12" 19 | }, 20 | "dependencies": { 21 | "finalhandler": "^0.3.3", 22 | "mocha": "^2.1.0", 23 | "serve-static": "^1.8.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maquette", 3 | "main": "maquette.js", 4 | "version": "2.1.5", 5 | "homepage": "http://maquettejs.org/", 6 | "authors": [ 7 | "Johan Gorter " 8 | ], 9 | "description": "Minimalistic Virtual DOM implementation with support for animated transitions.", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "virtual", 17 | "dom", 18 | "animation", 19 | "transitions" 20 | ], 21 | "license": "MIT", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests", 28 | "examples", 29 | "scripts", 30 | "browser-tests" 31 | ], 32 | "dependencies": { 33 | "velocity": "~1.1.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Maquette development page 5 | 6 | 7 |

Maquette development page

8 |

Examples:

9 | 16 |

Code coverage result:

17 | Only available after running npm run-script coverage 18 |

19 | Open coverage 20 |

21 | 22 | 23 | -------------------------------------------------------------------------------- /dist/css-transitions.min.js: -------------------------------------------------------------------------------- 1 | !function(n){"use strict";var e=null,i=function(n){if("WebkitTransition"in n.style)e="webkitTransitionEnd";else if("transition"in n.style)e="transitionend";else{if(!("MozTransition"in n.style))throw new Error("Your browser is not supported");e="transitionend"}},t=function(n){null===e&&i(n)},s={exit:function(n,i,s,o){t(n);var r=!1,a=function(i){r||(r=!0,n.removeEventListener(e,a),o())};n.classList.add(s),n.addEventListener(e,a),requestAnimationFrame(function(){n.classList.add(s+"-active")})},enter:function(n,i,s){t(n);var o=!1,r=function(i){o||(o=!0,n.removeEventListener(e,r),n.classList.remove(s),n.classList.remove(s+"-active"))};n.classList.add(s),n.addEventListener(e,r),requestAnimationFrame(function(){n.classList.add(s+"-active")})}};void 0!==n.module&&n.module.exports?n.module.exports=s:"function"==typeof n.define&&n.define.amd&&n.define(function(){return s}),window&&(window.cssTransitions=s)}(this); -------------------------------------------------------------------------------- /examples/enhancement/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Progressive enhancement demo 5 | 6 | 7 | 8 | 9 | 10 |

Enhancement example

11 |
12 | 13 | 16 |
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("
text
"); 17 | 18 | projection.update(h("div", ["text2"])); 19 | expect(projection.domNode.outerHTML).to.equal("
text2
"); 20 | 21 | projection.update(h("div", ["text2", h("span", ["a"])])); 22 | expect(projection.domNode.outerHTML).to.equal("
text2a
"); 23 | 24 | projection.update(h("div", ["text2"])); 25 | expect(projection.domNode.outerHTML).to.equal("
text2
"); 26 | 27 | projection.update(h("div", ["text"])); 28 | expect(projection.domNode.outerHTML).to.equal("
text
"); 29 | }); 30 | 31 | it("should work correctly with adjacent textnodes", function () { 32 | var projection = maquette.dom.create(h("div", ["", "1", ""])); 33 | expect(projection.domNode.outerHTML).to.equal("
1
"); 34 | 35 | projection.update(h("div", ["",""])); 36 | expect(projection.domNode.outerHTML).to.equal("
"); 37 | 38 | projection.update(h("div", ["", "1", ""])); 39 | expect(projection.domNode.outerHTML).to.equal("
1
"); 40 | }); 41 | 42 | it("should parse the selector", function () { 43 | 44 | require("./jsdom-classlist-polyfill")(window); 45 | 46 | var projection = maquette.dom.create(h("div")); 47 | expect(projection.domNode.outerHTML).to.equal("
"); 48 | 49 | projection = maquette.dom.create(h("div.class1")); 50 | expect(projection.domNode.outerHTML).to.equal("
"); 51 | 52 | projection = maquette.dom.create(h("div#id")); 53 | expect(projection.domNode.outerHTML).to.equal("
"); 54 | 55 | projection = maquette.dom.create(h("div.class1.class2")); 56 | expect(projection.domNode.outerHTML).to.equal("
"); 57 | 58 | projection = maquette.dom.create(h("div.class1.class2#id")); 59 | expect(projection.domNode.outerHTML).to.equal("
"); 60 | 61 | projection = maquette.dom.create(h("div#id.class1.class2")); 62 | expect(projection.domNode.outerHTML).to.equal("
"); 63 | }); 64 | 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/css-transitions.js: -------------------------------------------------------------------------------- 1 | (function (global) { 2 | 3 | "use strict"; 4 | 5 | var browserSpecificTransitionEndEventName = null; 6 | 7 | var determineBrowserSpecificStyleNames = function (element) { 8 | if ("WebkitTransition" in element.style) { 9 | browserSpecificTransitionEndEventName = "webkitTransitionEnd"; 10 | } else if ("transition" in element.style) { 11 | browserSpecificTransitionEndEventName = "transitionend"; 12 | } else if ("MozTransition" in element.style) { 13 | browserSpecificTransitionEndEventName = "transitionend"; 14 | } else { 15 | throw new Error("Your browser is not supported"); 16 | } 17 | }; 18 | 19 | var init = function (testElement) { 20 | if (browserSpecificTransitionEndEventName === null) { 21 | determineBrowserSpecificStyleNames(testElement); 22 | } 23 | }; 24 | 25 | var cssTransitions = { 26 | exit: function (node, properties, exitAnimation, removeNode) { 27 | init(node); 28 | var finished = false; 29 | var transitionEnd = function (evt) { 30 | if (!finished) { 31 | finished = true; 32 | node.removeEventListener(browserSpecificTransitionEndEventName, transitionEnd); 33 | removeNode(); 34 | } 35 | }; 36 | node.classList.add(exitAnimation); 37 | node.addEventListener(browserSpecificTransitionEndEventName, transitionEnd); 38 | requestAnimationFrame(function () { 39 | node.classList.add(exitAnimation + "-active"); 40 | }); 41 | }, 42 | enter: function (node, properties, enterAnimation) { 43 | init(node); 44 | var finished = false; 45 | var transitionEnd = function (evt) { 46 | if (!finished) { 47 | finished = true; 48 | node.removeEventListener(browserSpecificTransitionEndEventName, transitionEnd); 49 | node.classList.remove(enterAnimation); 50 | node.classList.remove(enterAnimation + "-active"); 51 | } 52 | }; 53 | node.classList.add(enterAnimation); 54 | node.addEventListener(browserSpecificTransitionEndEventName, transitionEnd); 55 | requestAnimationFrame(function () { 56 | node.classList.add(enterAnimation + "-active"); 57 | }); 58 | } 59 | }; 60 | 61 | if (global.module !== undefined && global.module.exports) { 62 | // Node and other CommonJS-like environments that support module.exports 63 | global.module.exports = cssTransitions; 64 | } else if (typeof global.define == 'function' && global.define.amd) { 65 | // AMD / RequireJS 66 | global.define(function () { 67 | return cssTransitions; 68 | }); 69 | } 70 | if (window) { 71 | // Browser 72 | window.cssTransitions = cssTransitions; 73 | } 74 | 75 | })(this); 76 | -------------------------------------------------------------------------------- /examples/benchmarks/loadAndRender.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

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