Commit e4786809 authored by Stephen McKamey's avatar Stephen McKamey Committed by Sindre Sorhus

Closes #136: DUEL app

parent 8990986b
TodoMVC implemented in DUEL
===========================
Prerequisites
-------------
This example requires [Apache Maven 3](http://maven.apache.org/download.html) to build.
About DUEL
----------
[DUEL](http://duelengine.org) is a duel-sided template engine. Views written as markup get precompiled into both
JavaScript (client-side templates) and Java (server-side templates).
The client-side templates are executed as functions directly from JavaScript. The result
can be rendered as either text markup or as DOM objects. This example generates DOM objects for views.
This particular example only uses the server-side templates for debugging. They have been generated into
the `target/generated-sources/duel/` directory.
How to build
------------
Run a standard Maven build command in the directory that contains the `pom.xml`:
mvn clean package
Maven will download any dependencies, clean out any previously built files, and generate a new static app in the `www/` directory.
How to run debug version
------------------------
To run a debug-able version using Tomcat 7 as the web server, use this Maven command:
mvn tomcat7:run
Then navigate your browser to: http://127.0.0.1:8080/
<?xml version="1.0" encoding="UTF-8"?>
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.todos</groupId>
<artifactId>todomvc</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
<name>TodoMVC</name>
<description>TodoMVC example written in DUEL</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<resourcesDir>${project.basedir}/src/main/resources</resourcesDir>
<staticapps.version>0.8.5</staticapps.version>
<merge.version>0.5.2</merge.version>
<duel.version>0.8.2</duel.version>
<slf4j.version>1.6.4</slf4j.version>
<javac.version>1.6</javac.version>
<duel.clientPrefix>todos.views</duel.clientPrefix>
<duel.serverPrefix>com.example.todos.views</duel.serverPrefix>
<duel.sourceDir>${resourcesDir}/views/</duel.sourceDir>
<duel.clientPath>/js/</duel.clientPath>
<merge.cdnMapFile>/cdn.properties</merge.cdnMapFile>
<merge.cdnRoot>/cdn/</merge.cdnRoot>
<merge.cdnFiles>.ico .png .jpg .gif .eot .woff .ttf .svg .svgz</merge.cdnFiles>
<staticapps.config>${project.basedir}/staticapp.json</staticapps.config>
</properties>
<dependencies>
<!-- DUEL runtime -->
<dependency>
<groupId>org.duelengine</groupId>
<artifactId>duel-runtime</artifactId>
<version>${duel.version}</version>
</dependency>
<!-- DUEL staticapps -->
<dependency>
<groupId>org.duelengine</groupId>
<artifactId>duel-staticapps</artifactId>
<version>${staticapps.version}</version>
</dependency>
<!-- SLF4J runtime -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<resources>
<resource>
<directory>${resourcesDir}</directory>
<excludes>
<!-- exclude DUEL sources from target output -->
<exclude>**/*.duel</exclude>
</excludes>
</resource>
</resources>
<plugins>
<!-- Tomcat 7 config -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.0-beta-1</version>
<configuration>
<path>/</path>
<port>8080</port>
<warSourceDirectory>${project.build.directory}/${project.build.finalName}/</warSourceDirectory>
</configuration>
</plugin>
<!-- DUEL Compiler -->
<plugin>
<groupId>org.duelengine</groupId>
<artifactId>duel-maven-plugin</artifactId>
<version>${duel.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<clientPrefix>${duel.clientPrefix}</clientPrefix>
<serverPrefix>${duel.serverPrefix}</serverPrefix>
<inputDir>${duel.sourceDir}</inputDir>
<outputClientPath>${duel.clientPath}</outputClientPath>
</configuration>
</execution>
</executions>
</plugin>
<!-- Merge Builder -->
<plugin>
<groupId>org.duelengine</groupId>
<artifactId>merge-maven-plugin</artifactId>
<version>${merge.version}</version>
<executions>
<execution>
<goals>
<goal>merge</goal>
</goals>
<configuration>
<cdnMapFile>${merge.cdnMapFile}</cdnMapFile>
<cdnRoot>${merge.cdnRoot}</cdnRoot>
<cdnFiles>${merge.cdnFiles}</cdnFiles>
</configuration>
</execution>
</executions>
</plugin>
<!-- Static App Builder -->
<plugin>
<groupId>org.duelengine</groupId>
<artifactId>duel-staticapps-maven-plugin</artifactId>
<version>${staticapps.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<configPath>${staticapps.config}</configPath>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<!-- Java compiler -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${javac.version}</source>
<target>${javac.version}</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
<view name="HomePage"><!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>DUEL &bull; TodoMVC</title>
<link rel="stylesheet" href="/css/styles.merge">
<!--[if lt IE 9]><script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
</head>
<body>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<p>Ported to <a href="http://duelengine.org">DUEL</a> by <a href="http://mck.me">Stephen McKamey</a></p>
</footer>
<script src="/js/scripts.merge"></script>
</body>
</html>
<view name="Stats">
<%-- this footer should hidden by default and shown when there are todos --%>
<footer id="footer" if="<%= data.total %>">
<span id="todo-count"><strong><%= data.active %></strong> <%= (data.active === 1) ? 'item' : 'items' %> left</span>
<%-- TODO: implement routing
<ul id="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
--%>
<button id="clear-completed"
if="<%= data.completed %>"
onclick="<%= todos.actions.clear_click %>">Clear completed (<%= data.completed %>)</button>
</footer>
<view name="Task">
<%-- could have embedded in 'tasks' for-loop, but this allows us to add single tasks --%>
<li class="<%= data.completed ? 'complete' : '' %>"
ondblclick="<%= todos.actions.content_dblclick(data.id) %>">
<div class="view">
<input class="toggle" type="checkbox" checked="<%= data.completed %>"
onchange="<%= todos.actions.completed_change(data.id) %>">
<label><%= data.title %></label>
<button class="destroy" onclick="<%= todos.actions.remove_click(data.id) %>"></button>
</div>
<input class="edit" type="text" value="<%= data.title %>"
onblur="<%= todos.actions.edit_blur(data.id) %>"
onkeypress="<%= todos.actions.edit_keypress(data.id) %>">
</li>
<view name="Tasks">
<%-- this section should hidden by default and shown when there are todos --%>
<section id="main" if="<%= data.tasks.length %>">
<input id="toggle-all" type="checkbox" checked="<%= !data.stats.active %>"
onchange="<%= todos.actions.toggle_change %>">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<for each="<%= data.tasks %>">
<call view="Task">
</for>
</ul>
</section>
<view name="TodoApp">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo"
placeholder="What needs to be done?"
autofocus
onblur="<%= todos.actions.add_blur %>"
onkeypress="<%= todos.actions.add_keypress %>">
</header>
<call view="Tasks" data="data" />
<call view="Stats" data="data.stats" />
</section>
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<servlet>
<servlet-name>routing-servlet</servlet-name>
<servlet-class>org.duelengine.duel.staticapps.RoutingServlet</servlet-class>
<init-param>
<param-name>config-path</param-name>
<param-value>staticapp.json</param-value>
</init-param>
<init-param>
<param-name>dev-mode-override</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>routing-servlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eeeeee url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#todoapp header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-radius: inherit;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input:-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#new-todo,
.edit {
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
position: relative;
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: 12px;
text-align: center;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
-webkit-transform: rotate(90deg);
/*-moz-transform: rotate(90deg);*/
-ms-transform: rotate(90deg);
/*-o-transform: rotate(90deg);*/
transform: rotate(90deg);
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#toggle-all:checked:before {
color: #737373;
}
/* Need this ugly hack, since only
WebKit supports styling of inputs */
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all {
top: -52px;
left: -11px;
}
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px dotted #ccc;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 35px;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
font-size: 18px;
content: '✔';
line-height: 40px;
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
word-break: break-word;
margin: 20px 15px;
display: inline-block;
-webkit-transition: color 0.4s;
-moz-transition: color 0.4s;
-ms-transition: color 0.4s;
-o-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.complete label {
color: #a9a9a9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 10px;
right: 10px;
width: 40px;
height: 40px;
font-size: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-ms-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-moz-transform: scale(1.3);
-ms-transform: scale(1.3);
-o-transform: scale(1.3);
transform: scale(1.3);
}
#todo-list li .destroy:after {
content: '✖';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 100px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 42px 0 -6px rgba(255, 255, 255, 0.8),
0 43px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
position: relative;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
\ No newline at end of file
# template styles
/css/base.css
# app overrides (not used)
#/css/app.css
\ No newline at end of file
/*global window */
/**
* @license DUEL v0.8.2 http://duelengine.org
* Copyright (c)2006-2012 Stephen M. McKamey.
* Licensed under The MIT License.
*/
/**
* @public
* @param {Array|Object|string|number|function(*,number,number):Array|Object|string} view The view template
* @return {function(*)}
*/
var duel = (
/**
* @param {Document} document Document reference
* @param {function()} scriptEngine script engine version
* @param {*=} undef undefined
*/
function(document, scriptEngine, undef) {
'use strict';
/* types.js --------------------*/
/**
* @private
* @const
* @type {number}
*/
var NUL = 0;
/**
* @private
* @const
* @type {number}
*/
var FUN = 1;
/**
* @private
* @const
* @type {number}
*/
var ARY = 2;
/**
* @private
* @const
* @type {number}
*/
var OBJ = 3;
/**
* @private
* @const
* @type {number}
*/
var VAL = 4;
/**
* @private
* @const
* @type {number}
*/
var RAW = 5;
/**
* Wraps a data value to maintain as raw markup in output
*
* @private
* @this {Markup}
* @param {string} value The value
* @constructor
*/
function Markup(value) {
/**
* @type {string}
* @const
* @protected
*/
this.value = value;
}
/**
* Renders the value
*
* @public
* @override
* @this {Markup}
* @return {string} value
*/
Markup.prototype.toString = function() {
return this.value;
};
/**
* Determines if the value is an Array
*
* @private
* @param {*} val the object being tested
* @return {boolean}
*/
var isArray = Array.isArray || function(val) {
return (val instanceof Array);
};
/**
* Determines the type of the value
*
* @private
* @param {*} val the object being tested
* @return {number}
*/
function getType(val) {
switch (typeof val) {
case 'object':
return !val ? NUL : (isArray(val) ? ARY : ((val instanceof Markup) ? RAW : ((val instanceof Date) ? VAL : OBJ)));
case 'function':
return FUN;
case 'undefined':
return NUL;
default:
return VAL;
}
}
/**
* Determines if the value is a string
*
* @private
* @param {*} val the object being tested
* @return {boolean}
*/
function isString(val) {
return (typeof val === 'string');
}
/**
* Determines if the value is a function
*
* @private
* @param {*} val the object being tested
* @return {boolean}
*/
function isFunction(val) {
return (typeof val === 'function');
}
/**
* String buffer
*
* @private
* @this {Buffer}
* @constructor
*/
function Buffer() {
/**
* @type {Array|string}
* @private
*/
this.value = Buffer.FAST ? '' : [];
}
/**
* Only IE<9 benefits from Array.join()
*
* @private
* @constant
* @type {boolean}
*/
Buffer.FAST = !(scriptEngine && scriptEngine() < 9);
/**
* Appends to the internal value
*
* @public
* @this {Buffer}
* @param {null|string} v1
* @param {null|string=} v2
* @param {null|string=} v3
*/
Buffer.prototype.append = function(v1, v2, v3) {
if (Buffer.FAST) {
if (v1 !== null) {
this.value += v1;
if (v2 !== null && v2 !== undef) {
this.value += v2;
if (v3 !== null && v3 !== undef) {
this.value += v3;
}
}
}
} else {
this.value.push.apply(
// Closure Compiler type cast
/** @type{Array} */(this.value),
arguments);
}
};
/**
* Clears the internal value
*
* @public
* @this {Buffer}
*/
Buffer.prototype.clear = function() {
this.value = Buffer.FAST ? '' : [];
};
/**
* Renders the value
*
* @public
* @override
* @this {Buffer}
* @return {string} value
*/
Buffer.prototype.toString = function() {
return Buffer.FAST ?
// Closure Compiler type cast
/** @type{string} */(this.value) :
this.value.join('');
};
function digits(n) {
return (n < 10) ? '0'+n : n;
}
/**
* Formats the value as a string
*
* @private
* @param {*} val the object being rendered
* @return {string|null}
*/
function asString(val) {
var buffer, needsDelim;
switch (getType(val)) {
case VAL:
return ''+val;
case NUL:
return '';
case ARY:
// flatten into simple list
buffer = new Buffer();
for (var i=0, length=val.length; i<length; i++) {
if (needsDelim) {
buffer.append(', ');
} else {
needsDelim = true;
}
buffer.append(asString(val[i]));
}
return buffer.toString();
case OBJ:
// format JSON-like
buffer = new Buffer();
buffer.append('{');
for (var key in val) {
if (val.hasOwnProperty(key)) {
if (needsDelim) {
buffer.append(', ');
} else {
needsDelim = true;
}
buffer.append(key, '=', asString(val[key]));
}
}
buffer.append('}');
return buffer.toString();
}
// Closure Compiler type cast
return /** @type{string} */(val);
}
/**
* Wraps a binding result with rendering methods
*
* @private
* @this {Result}
* @param {Array|Object|string|number} view The result tree
* @constructor
*/
function Result(view) {
if (!isArray(view)) {
// ensure is rooted element
view = ['', view];
}
/**
* @type {Array}
* @const
* @protected
*/
// Closure Compiler type cast
this.value = /** @type {Array} */(view);
}
/* bind.js --------------------*/
/**
* @private
* @constant
* @type {string}
*/
var FOR = '$for';
/**
* @private
* @constant
* @type {string}
*/
var XOR = '$xor';
/**
* @private
* @constant
* @type {string}
*/
var IF = '$if';
/**
* @private
* @constant
* @type {string}
*/
var CALL = '$call';
/**
* @private
* @constant
* @type {string}
*/
var PART = '$part';
/**
* @private
* @constant
* @type {string}
*/
var TEST = 'test';
/**
* @private
* @constant
* @type {string}
*/
var EACH = 'each';
/**
* @private
* @constant
* @type {string}
*/
var IN = 'in';
/**
* @private
* @constant
* @type {string}
*/
var VIEW = 'view';
/**
* @private
* @constant
* @type {string}
*/
var DATA = 'data';
/**
* @private
* @constant
* @type {string}
*/
var INDEX = 'index';
/**
* @private
* @constant
* @type {string}
*/
var COUNT = 'count';
/**
* @private
* @constant
* @type {string}
*/
var KEY = 'key';
/**
* @private
* @constant
* @type {string}
*/
var NAME = 'name';
var bind;
/**
* Appends a node to a parent
*
* @private
* @param {Array} parent The parent node
* @param {Array|Object|string|number} child The child node
*/
function append(parent, child) {
switch (getType(child)) {
case ARY:
if (child[0] === '') {
// child is documentFragment
// directly append children, skip fragment identifier
for (var i=1, length=child.length; i<length; i++) {
append(parent, child[i]);
}
} else {
// child is an element array
parent.push(child);
}
break;
case OBJ:
// child is attributes object
if (parent.length === 1) {
parent.push(child);
} else {
var old = parent[1];
if (getType(old) === OBJ) {
// merge attribute objects
for (var key in child) {
if (child.hasOwnProperty(key)) {
old[key] = child[key];
}
}
} else {
// insert attributes object
parent.splice(1, 0, child);
}
}
break;
case VAL:
if (child !== '') {
// coerce primitive to string literal
child = '' + child;
var last = parent.length-1;
if (last > 0 && getType(parent[last]) === VAL) {
// combine string literals
parent[last] += child;
} else {
// append
parent.push(child);
}
}
break;
case NUL:
// cull empty values
break;
default:
// append others
parent.push(child);
break;
}
}
/**
* Binds the child nodes ignoring parent element and attributes
*
* @private
* @param {Array} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
function bindContent(node, data, index, count, key, parts) {
// second item might be attributes object
var hasAttr = (getType(node[1]) === OBJ);
if (node.length === (hasAttr ? 3 : 2)) {
// unwrap single nodes
return bind(node[node.length-1], data, index, count, key, parts);
}
// element array, make a doc frag
var result = [''];
for (var i=hasAttr ? 2 : 1, length=node.length; i<length; i++) {
append(result, bind(node[i], data, index, count, key, parts));
}
return result;
}
/**
* Binds the content once for each item in data
*
* @private
* @param {Array|Object|string|number|function(*,number,number):*} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
function loop(node, data, index, count, key, parts) {
var args = node[1] || {},
result = [''],
items, i, length;
if (args.hasOwnProperty(COUNT)) {
// evaluate for-count loop
length = args[COUNT];
if (isFunction(length)) {
// execute code block
length = length(data, index, count, key);
}
var d;
if (args.hasOwnProperty(DATA)) {
d = args[DATA];
if (isFunction(d)) {
// execute code block
d = d(data, index, count, key);
}
} else {
d = data;
}
// iterate over the items
for (i=0; i<length; i++) {
// Closure Compiler type cast
append(result, bindContent(/** @type {Array} */(node), d, i, length, null, parts));
}
return result;
}
if (args.hasOwnProperty(IN)) {
// convert for-in loop to for-each loop
var obj = args[IN];
if (isFunction(obj)) {
// execute code block
obj = obj(data, index, count, key);
}
if (getType(obj) === OBJ) {
// accumulate the property keys to get count
items = [];
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
items.push(k);
}
}
// iterate over the keys
for (i=0, length=items.length; i<length; i++) {
// Closure Compiler type cast
append(result, bindContent(/** @type {Array} */(node), obj[items[i]], i, length, items[i], parts));
}
return result;
}
// just bind to single value
items = obj;
} else {
// evaluate for-each loop
items = args[EACH];
if (isFunction(items)) {
// execute code block
items = items(data, index, count, key);
}
}
var type = getType(items);
if (type === ARY) {
// iterate over the items
for (i=0, length=items.length; i<length; i++) {
// Closure Compiler type cast
append(result, bindContent(/** @type {Array} */(node), items[i], i, length, null, parts));
}
} else if (type !== NUL) {
// just bind the single value
// Closure Compiler type cast
result = bindContent(/** @type {Array} */(node), items, 0, 1, null, parts);
}
return result;
}
/**
* Binds the node to the first conditional block that evaluates to true
*
* @private
* @param {Array|Object|string|number|function(*,number,number):Array|Object|string} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
function xor(node, data, index, count, key, parts) {
for (var i=1, length=node.length; i<length; i++) {
var block = node[i],
args = block[1],
test = args[TEST];
if (getType(block[1]) === OBJ && test) {
// execute test if exists
if (isFunction(test)) {
test = test(data, index, count, key);
}
if (!test) {
continue;
}
}
// process block contents
return bindContent(block, data, index, count, key, parts);
}
return null;
}
/**
* Calls into another view
*
* @private
* @param {Array|Object|string|number|function(*,*,*,*):(Object|null)} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @return {Array|Object|string|number}
*/
function call(node, data, index, count, key) {
var args = node[1] || {};
if (!args[VIEW]) {
return null;
}
// evaluate the arguments
var v = bind(args[VIEW], data, index, count, key),
d = args.hasOwnProperty(DATA) ? bind(args[DATA], data, index, count, key) : data,
i = args.hasOwnProperty(INDEX) ? bind(args[INDEX], data, index, count, key) : index,
c = args.hasOwnProperty(COUNT) ? bind(args[COUNT], data, index, count, key) : count,
k = args.hasOwnProperty(KEY) ? bind(args[KEY], data, index, count, key) : key,
p = {};
// check for view parts
for (var j=node.length-1; j>=2; j--) {
var block = node[j];
args = block[1] || {};
if (args.hasOwnProperty(NAME)) {
p[args[NAME]] = block;
}
}
return (v && isFunction(v.getView)) ?
// Closure Compiler type cast
bind(v.getView(), d, /** @type {number} */i, /** @type {number} */c, /** @type {string} */k, p) : null;
}
/**
* Replaces a place holder part with the named part from the calling view
*
* @private
* @param {Array|Object|string|number|function(*,*,*,*):(Object|null)} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
function part(node, data, index, count, key, parts) {
var args = node[1] || {},
block = args[NAME] || '';
block = parts && parts.hasOwnProperty(block) ? parts[block] : node;
return bindContent(block, data, index, count, key);
}
/**
* Binds the node to data
*
* @private
* @param {Array|Object|string|number|function(*,*,*,*):(Object|null)} node The template subtree root
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @param {Object=} parts Named replacement partial views
* @return {Array|Object|string|number}
*/
bind = function(node, data, index, count, key, parts) {
switch (getType(node)) {
case FUN:
// execute code block
// Closure Compiler type cast
return (/** @type {function(*,*,*,*):(Object|null)} */ (node))(data, index, count, key);
case ARY:
// inspect element name for template commands
/**
* @type {string}
*/
var tag = node[0] || '';
switch (tag) {
case FOR:
return loop(node, data, index, count, key, parts);
case XOR:
return xor(node, data, index, count, key, parts);
case IF:
return xor([XOR, node], data, index, count, key, parts);
case CALL:
// parts not needed when calling another view
return call(node, data, index, count, key);
case PART:
return part(node, data, index, count, key, parts);
}
// element array, first item is name
var elem = [tag];
for (var i=1, length=node.length; i<length; i++) {
append(elem, bind(node[i], data, index, count, key, parts));
}
return elem;
case OBJ:
// attribute map
var attr = {};
for (var k in node) {
if (node.hasOwnProperty(k)) {
// parts not needed when binding attributes
attr[k] = bind(node[k], data, index, count, key);
}
}
return attr;
}
// literal values
return node;
};
/* factory.js --------------------*/
/**
* Renders an error as text
*
* @private
* @param {Error} ex The exception
* @return {string}
*/
function onError(ex) {
return '['+ex+']';
}
/**
* Wraps a view definition with binding method
*
* @private
* @param {Array|Object|string|number} view The template definition
* @return {function(*)}
*/
function factory(view) {
if (getType(view) !== ARY) {
// ensure is rooted element
view = ['', view];
}
/**
* Binds and wraps the result
*
* @public
* @param {*} data The data item being bound
* @param {number} index The index of the current data item
* @param {number} count The total number of data items
* @param {string|null} key The current property name
* @return {Result}
*/
var self = function(data, index, count, key) {
try {
var result = bind(
// Closure Compiler type cast
/** @type {Array} */(view),
data,
isFinite(index) ? index : 0,
isFinite(count) ? count : 1,
isString(key) ? key : null);
return new Result(result);
} catch (ex) {
// handle error with context
return new Result(onError(ex));
}
};
/**
* Gets the internal view definition
*
* @private
* @return {Array}
*/
self.getView = function() {
// Closure Compiler type cast
return /** @type {Array} */(view);
};
return self;
}
/**
* @public
* @param {Array|Object|string|number|function(*,number,number):Array|Object|string} view The view template
* @return {Array|Object|string|number}
*/
var duel = function(view) {
return (isFunction(view) && isFunction(view.getView)) ? view : factory(view);
};
/**
* @public
* @param {string} value Markup text
* @return {Markup}
*/
duel.raw = function(value) {
return new Markup(value);
};
/* render.js --------------------*/
/**
* Void tag lookup
*
* @private
* @constant
* @type {Object.<boolean>}
*/
var VOID_TAGS = {
'area' : true,
'base' : true,
'basefont' : true,
'br' : true,
'col' : true,
'frame' : true,
'hr' : true,
'img' : true,
'input' : true,
'isindex' : true,
'keygen' : true,
'link' : true,
'meta' : true,
'param' : true,
'source' : true,
'wbr' : true
};
/**
* Boolean attribute map
*
* @private
* @constant
* @type {Object.<number>}
*/
var ATTR_BOOL = {
'async': 1,
'checked': 1,
'defer': 1,
'disabled': 1,
'hidden': 1,
'novalidate': 1,
'formnovalidate': 1
// can add more attributes here as needed
};
/**
* Encodes invalid literal characters in strings
*
* @private
* @param {Array|Object|string|number} val The value
* @return {Array|Object|string|number}
*/
function htmlEncode(val) {
if (!isString(val)) {
return val;
}
return val.replace(/[&<>]/g,
function(ch) {
switch(ch) {
case '&':
return '&amp;';
case '<':
return '&lt;';
case '>':
return '&gt;';
default:
return ch;
}
});
}
/**
* Encodes invalid attribute characters in strings
*
* @private
* @param {Array|Object|string|number} val The value
* @return {Array|Object|string|number}
*/
function attrEncode(val) {
if (!isString(val)) {
return val;
}
return val.replace(/[&<>"]/g,
function(ch) {
switch(ch) {
case '&':
return '&amp;';
case '<':
return '&lt;';
case '>':
return '&gt;';
case '"':
return '&quot;';
default:
return ch;
}
});
}
/**
* Renders the comment as a string
*
* @private
* @param {Buffer} buffer The output buffer
* @param {Array} node The result tree
*/
function renderComment(buffer, node) {
if (node[0] === '!DOCTYPE') {
// emit doctype
buffer.append('<!DOCTYPE ', node[1], '>');
} else {
// emit HTML comment
buffer.append('<!--', node[1], '-->');
}
}
/**
* Renders the element as a string
*
* @private
* @param {Buffer} buffer The output buffer
* @param {Array} node The result tree
*/
function renderElem(buffer, node) {
var tag = node[0] || '',
length = node.length,
i = 1,
child,
isVoid = VOID_TAGS[tag];
if (tag.charAt(0) === '!') {
renderComment(buffer, node);
return;
}
if (tag) {
// emit open tag
buffer.append('<', tag);
child = node[i];
if (getType(child) === OBJ) {
// emit attributes
for (var name in child) {
if (child.hasOwnProperty(name)) {
var val = child[name];
if (ATTR_BOOL[name]) {
if (val) {
val = name;
} else {
// falsey boolean attributes must not be present
continue;
}
}
buffer.append(' ', name);
if (getType(val) !== NUL) {
// Closure Compiler type cast
buffer.append('="', /** @type{string} */(attrEncode(val)), '"');
}
}
}
i++;
}
if (isVoid) {
buffer.append(' /');
}
buffer.append('>');
}
// emit children
for (; i<length; i++) {
child = node[i];
if (isArray(child)) {
renderElem(buffer, child);
} else {
// encode string literals
// Closure Compiler type cast
buffer.append(/** @type{string} */(htmlEncode(child)));
}
}
if (tag && !isVoid) {
// emit close tag
buffer.append('</', tag, '>');
}
}
/**
* Renders the result as a string
*
* @private
* @param {Array} view The compiled view
* @return {string}
*/
function render(view) {
try {
var buffer = new Buffer();
renderElem(buffer, view);
return buffer.toString();
} catch (ex) {
// handle error with context
return onError(ex);
}
}
/**
* Returns result as HTML text
*
* @public
* @override
* @this {Result}
* @return {string}
*/
Result.prototype.toString = function() {
return render(this.value);
};
/**
* Immediately writes the resulting value to the document
*
* @public
* @this {Result}
* @param {Document} doc optional Document reference
*/
Result.prototype.write = function(doc) {
/*jslint evil:true*/
(doc||document).write(''+this);
/*jslint evil:false*/
};
/* dom.js --------------------*/
/**
* @private
* @constant
* @type {string}
*/
var INIT = '$init';
/**
* @private
* @constant
* @type {string}
*/
var LOAD = '$load';
/**
* Attribute name map
*
* @private
* @constant
* @type {Object.<string>}
*/
var ATTR_MAP = {
'rowspan': 'rowSpan',
'colspan': 'colSpan',
'cellpadding': 'cellPadding',
'cellspacing': 'cellSpacing',
'tabindex': 'tabIndex',
'accesskey': 'accessKey',
'hidefocus': 'hideFocus',
'usemap': 'useMap',
'maxlength': 'maxLength',
'readonly': 'readOnly',
'contenteditable': 'contentEditable'
// can add more attributes here as needed
};
/**
* Attribute duplicates map
*
* @private
* @constant
* @type {Object.<string>}
*/
var ATTR_DUP = {
'enctype': 'encoding',
'onscroll': 'DOMMouseScroll',
'checked': 'defaultChecked'
// can add more attributes here as needed
};
/**
* Leading SGML line ending pattern
*
* @private
* @constant
* @type {RegExp}
*/
var LEADING = /^[\r\n]+/;
/**
* Trailing SGML line ending pattern
*
* @private
* @constant
* @type {RegExp}
*/
var TRAILING = /[\r\n]+$/;
/**
* Creates a DOM element
*
* @private
* @param {string} tag The element's tag name
* @return {Node}
*/
function createElement(tag) {
if (!tag) {
// create a document fragment to hold multiple-root elements
if (document.createDocumentFragment) {
return document.createDocumentFragment();
}
tag = '';
} else if (tag.charAt(0) === '!') {
return document.createComment(tag === '!' ? '' : tag.substr(1)+' ');
}
if (tag.toLowerCase() === 'style' && document.createStyleSheet) {
// IE requires this interface for styles
return document.createStyleSheet();
}
return document.createElement(tag);
}
/**
* Appends a child to an element
*
* @private
* @param {Node} elem The parent element
* @param {Node} child The child
*/
function appendDOM(elem, child) {
if (child) {
var tag = (elem.tagName||'').toLowerCase();
if (elem.nodeType === 8) { // comment
if (child.nodeType === 3) { // text node
elem.nodeValue += child.nodeValue;
}
} else if (tag === 'table' && elem.tBodies) {
if (!child.tagName) {
// must unwrap documentFragment for tables
if (child.nodeType === 11) {
while (child.firstChild) {
appendDOM(elem, child.removeChild(child.firstChild));
}
}
return;
}
// in IE must explicitly nest TRs in TBODY
var childTag = child.tagName.toLowerCase();// child tagName
if (childTag && childTag !== 'tbody' && childTag !== 'thead') {
// insert in last tbody
var tBody = elem.tBodies.length > 0 ? elem.tBodies[elem.tBodies.length-1] : null;
if (!tBody) {
tBody = createElement(childTag === 'th' ? 'thead' : 'tbody');
elem.appendChild(tBody);
}
tBody.appendChild(child);
} else if (elem.canHaveChildren !== false) {
elem.appendChild(child);
}
} else if (tag === 'style' && document.createStyleSheet) {
// IE requires this interface for styles
elem.cssText = child;
} else if (elem.canHaveChildren !== false) {
elem.appendChild(child);
} else if (tag === 'object' &&
child.tagName && child.tagName.toLowerCase() === 'param') {
// IE-only path
try {
elem.appendChild(child);
} catch (ex1) {}
try {
if (elem.object) {
elem.object[child.name] = child.value;
}
} catch (ex2) {}
}
}
}
/**
* Adds an event handler to an element
*
* @private
* @param {Node} elem The element
* @param {string} name The event name
* @param {function(Event)} handler The event handler
*/
function addHandler(elem, name, handler) {
switch (typeof handler) {
case 'function':
if (elem.addEventListener) {
// DOM Level 2
elem.addEventListener((name.substr(0,2) === 'on') ? name.substr(2) : name, handler, false);
} else {
// DOM Level 0
elem[name] = handler;
}
break;
case 'string':
// inline functions are DOM Level 0
/*jslint evil:true */
elem[name] = new Function('event', handler);
/*jslint evil:false */
break;
}
}
/**
* Appends a child to an element
*
* @private
* @param {Node} elem The element
* @param {Object} attr Attributes object
* @return {Node}
*/
function addAttributes(elem, attr) {
if (attr.name && document.attachEvent && !elem.parentNode) {
try {
// IE fix for not being able to programatically change the name attribute
var alt = createElement('<'+elem.tagName+' name="'+attr.name+'">');
// fix for Opera 8.5 and Netscape 7.1 creating malformed elements
if (elem.tagName === alt.tagName) {
elem = alt;
}
} catch (ex) { }
}
// for each attributeName
for (var name in attr) {
if (attr.hasOwnProperty(name)) {
// attributeValue
var value = attr[name],
type = getType(value);
if (name) {
if (type === NUL) {
value = '';
type = VAL;
}
name = ATTR_MAP[name.toLowerCase()] || name;
if (ATTR_BOOL[name]) {
elem[name] = !!value;
// also set duplicated attributes
if (ATTR_DUP[name]) {
elem[ATTR_DUP[name]] = !!value;
}
} else if (name === 'style') {
if (typeof elem.style.cssText !== 'undefined') {
elem.style.cssText = value;
} else {
elem.style = value;
}
} else if (name === 'class') {
elem.className = value;
} else if (name.substr(0,2) === 'on') {
addHandler(elem, name, value);
// also set duplicated events
if (ATTR_DUP[name]) {
addHandler(elem, ATTR_DUP[name], value);
}
} else if (type === VAL && name.charAt(0) !== '$') {
elem.setAttribute(name, value);
// also set duplicated attributes
if (ATTR_DUP[name]) {
elem.setAttribute(ATTR_DUP[name], value);
}
} else {
// allow direct setting of complex properties
elem[name] = value;
// also set duplicated attributes
if (ATTR_DUP[name]) {
elem[ATTR_DUP[name]] = value;
}
}
}
}
}
return elem;
}
/**
* Tests a node for whitespace
*
* @private
* @param {Node} node The node
* @return {boolean}
*/
function isWhitespace(node) {
return !!node && (node.nodeType === 3) && (!node.nodeValue || !/\S/.exec(node.nodeValue));
}
/**
* Trims whitespace pattern from the text node
*
* @private
* @param {Node} node The node
*/
function trimPattern(node, pattern) {
if (!!node && (node.nodeType === 3) && pattern.exec(node.nodeValue)) {
node.nodeValue = node.nodeValue.replace(pattern, '');
}
}
/**
* Removes leading and trailing whitespace nodes
*
* @private
* @param {Node} elem The node
*/
function trimWhitespace(elem) {
if (elem) {
while (isWhitespace(elem.firstChild)) {
// trim leading whitespace text nodes
elem.removeChild(elem.firstChild);
}
// trim leading whitespace text
trimPattern(elem.firstChild, LEADING);
while (isWhitespace(elem.lastChild)) {
// trim trailing whitespace text nodes
elem.removeChild(elem.lastChild);
}
// trim trailing whitespace text
trimPattern(elem.lastChild, TRAILING);
}
}
/**
* Converts the markup to DOM nodes
*
* @private
* @param {string|Markup} value The node
* @return {Node}
*/
function toDOM(value) {
var wrapper = createElement('div');
wrapper.innerHTML = ''+value;
// trim extraneous whitespace
trimWhitespace(wrapper);
// eliminate wrapper for single nodes
if (wrapper.childNodes.length === 1) {
return wrapper.firstChild;
}
// create a document fragment to hold elements
var frag = createElement('');
while (wrapper.firstChild) {
frag.appendChild(wrapper.firstChild);
}
return frag;
}
/**
* Retrieve and remove method
*
* @private
* @param {Node} elem The element
* @param {string} key The callback name
* @return {function(Node)}
*/
function popCallback(elem, key) {
var method = elem[key];
if (method) {
try {
delete elem[key];
} catch (ex) {
// sometimes IE doesn't like deleting from DOM
elem[key] = undef;
}
if (!isFunction(method)) {
try {
/*jslint evil:true */
method = new Function(''+method);
/*jslint evil:false */
} catch (ex2) {
// filter
method = null;
}
}
}
return method;
}
/**
* Executes oninit/onload callbacks
*
* @private
* @param {Node} elem The element
*/
function callbacks(elem) {
if (!elem) {
return;
}
// execute and remove oninit method
var method = popCallback(elem, INIT);
if (method) {
// execute in context of element
method.call(elem);
}
// execute and remove onload method
method = popCallback(elem, LOAD);
if (method) {
// queue up to execute after insertion into parentNode
setTimeout(function() {
// execute in context of element
method.call(elem);
method = elem = null;
}, 0);
} else {
method = elem = null;
}
}
/**
* Applies node to DOM
*
* @private
* @param {Node} elem The element to append
* @param {Array} node The node to populate
* @return {Node}
*/
function patchDOM(elem, node) {
for (var i=1, length=node.length; i<length; i++) {
var child = node[i];
switch (getType(child)) {
case ARY:
// build child element
var childTag = child[0];
child = patchDOM(createElement(childTag), child);
if (childTag === 'html') {
// trim extraneous whitespace
trimWhitespace(child);
// trigger callbacks
callbacks(child);
// unwrap HTML root, to simplify insertion
return child;
}
// append child element
appendDOM(elem, child);
break;
case VAL:
if (child !== '') {
// append child value as text
appendDOM(elem, document.createTextNode(''+child));
}
break;
case OBJ:
if (elem.nodeType === 1) {
// add attributes
elem = addAttributes(elem, child);
}
break;
case RAW:
appendDOM(elem, toDOM(child));
break;
}
}
// trim extraneous whitespace
trimWhitespace(elem);
// trigger callbacks
callbacks(elem);
// eliminate wrapper for single nodes
if (elem.nodeType === 11 && elem.childNodes.length === 1) {
elem = elem.firstChild;
}
return elem;
}
/**
* Renders an error as a text node
*
* @private
* @param {Error} ex The exception
* @return {Node}
*/
function onErrorDOM(ex) {
return document.createTextNode(onError(ex));
}
/**
* Returns result as DOM objects
*
* @public
* @this {Result}
* @param {Node|string=} elem An optional element or element ID to be replaced or merged
* @param {boolean=} merge Optionally merge result into elem
* @return {Node|null}
*/
Result.prototype.toDOM = function(elem, merge) {
// resolve the element ID
if (getType(elem) === VAL) {
elem = document.getElementById(
// Closure Compiler type cast
/** @type{string} */(elem));
}
var view;
try {
if (merge) {
view = elem;
elem = null;
}
// Closure Compiler type cast
view = patchDOM(/** @type{Node} */(view) || createElement(this.value[0]), this.value);
} catch (ex) {
// handle error with context
view = onErrorDOM(ex);
}
if (elem && elem.parentNode) {
// replace existing element with result
// Closure Compiler type cast
elem.parentNode.replaceChild(view, /** @type{Node} */(elem));
}
return view;
};
/**
* Replaces entire document with this Result
*
* @public
* @this {Result}
*/
Result.prototype.reload = function() {
// http://stackoverflow.com/questions/4297877
var doc = document;
try {
var newRoot = this.toDOM();
doc.replaceChild(newRoot, doc.documentElement);
if (doc.createStyleSheet) {
// IE requires link repair
var head = newRoot.firstChild;
while (head && (head.tagName||'') !== 'HEAD') {
head = head.nextSibling;
}
var link = head && head.firstChild;
while (link) {
if ((link.tagName||'') === 'LINK') {
// this seems to repair the link
link.href = link.href;
}
link = link.nextSibling;
}
}
} catch (ex) {
/*jslint evil:true*/
doc = doc.open('text/html');
doc.write(this.toString());
doc.close();
/*jslint evil:false*/
}
};
return duel;
})(document, window.ScriptEngineMajorVersion);
# libraries
/js/lib/duel.js
# model
/js/todos/model.js
# views
/js/todos/views/Stats.js
/js/todos/views/Task.js
/js/todos/views/Tasks.js
/js/todos/views/TodoApp.js
# controller
/js/todos/controller.js
var todos = todos || {};
(function( todos, document ) {
/*-- private members -------------------------------*/
var ENTER_KEY = 13,
STATS_ID = 'footer',
TODOAPP_ID = 'todoapp',
TASKS_ID = 'main',
LIST_ID = 'todo-list',
EDITING_CSS = 'editing';
function getById( id ) {
return document.getElementById( id );
}
function refreshStats( stats ) {
// get the data
var data = stats || todos.model.stats();
// build the view
var view = todos.views.Stats( data ).toDOM();
// replace old stats
var old = getById( STATS_ID );
if ( old ) {
old.parentNode.replaceChild( view, old );
} else {
getById( TODOAPP_ID ).appendChild( view );
}
}
function refreshAll() {
// get the data
var data = {
tasks: todos.model.tasks(),
stats: todos.model.stats()
};
// build the view
var view = todos.views.Tasks( data ).toDOM();
// replace old task list
var old = getById( TASKS_ID );
if ( old ) {
old.parentNode.replaceChild( view, old );
} else {
getById( TODOAPP_ID ).appendChild( view );
}
refreshStats( data.stats );
}
function add( input ) {
var title = (input.value || '').trim();
input.value = '';
if ( !title ) {
return;
}
var task = todos.model.add( title );
var list = getById( LIST_ID );
if ( list ) {
// add new at the top
list.appendChild( todos.views.Task( task ).toDOM() );
refreshStats();
} else {
refreshAll();
}
}
function edit( input, id ) {
var title = (input.value || '').trim();
input.value = title;
if ( title ) {
todos.model.edit( id, title );
} else {
todos.model.remove( id );
}
refreshAll();
}
/*-- export public interface -------------------------------*/
// event handlers
todos.actions = {
add_blur: function( e ) {
add( this );
},
add_keypress: function( e ) {
if ( e.keyCode === ENTER_KEY ) {
add( this );
}
},
edit_blur: function( id ) {
// create a closure around the ID
return function( e ) {
edit( this, id );
};
},
edit_keypress: function( id ) {
// create a closure around the ID
return function(e) {
if ( e.keyCode === ENTER_KEY ) {
// just blur so doesn't get triggered twice
this.blur();
}
};
},
remove_click: function( id ) {
// create a closure around the ID
return function( e ) {
todos.model.remove( id );
refreshAll();
};
},
clear_click: function() {
todos.model.expunge();
refreshAll();
},
content_dblclick: function( id ) {
// create a closure around the ID
return function( e ) {
var li = this;
li.className = EDITING_CSS;
li.getElementsByTagName( 'input' )[1].focus();
};
},
completed_change: function( id ) {
// create a closure around the ID
return function( e ) {
var checkbox = this;
todos.model.toggle( id, checkbox.checked );
refreshAll();
};
},
toggle_change: function( e ) {
var checkbox = this;
todos.model.toggleAll( checkbox.checked );
refreshAll();
}
};
/*-- init task list -------------------------------*/
(function( body ) {
// build out task list
var view = todos.views.TodoApp({
tasks: todos.model.tasks(),
stats: todos.model.stats()
}).toDOM();
// insert at top
body.insertBefore( view, body.firstChild );
})( document.body );
})( todos, document );
var todos = todos || {};
(function( todos, localStorage, KEY ) {
/*-- private members -------------------------------*/
var tasks;
// model uses localStorage as the underlying data store
// this creates a poor man's localStorage polyfill
localStorage = localStorage || (function() {
var storage = {};
return {
getItem: function( key ) {
return storage[ key ];
},
setItem: function( key, value ) {
storage[ key ] = value;
}
};
})();
function create( title, completed ) {
return {
// fast, compact, non-repeating, unique ID: e.g., 'c2wwu0vz.pz4zpvi'
id: (new Date().getTime() + Math.random()).toString( 36 ),
title: title,
completed: !!completed
};
}
function save() {
// if doesn't support JSON then will be directly stored in polyfill
var value = typeof JSON !== 'undefined' ? JSON.stringify( tasks ) : tasks;
localStorage.setItem( KEY, value );
}
// initialize storage
var value = localStorage.getItem( KEY );
if ( value ) {
// if doesn't support JSON then will be directly stored in polyfill
tasks = typeof JSON !== 'undefined' ? JSON.parse( value ) : value;
} else {
tasks = [];
}
/*-- export public interface -------------------------------*/
todos.model = {
tasks: function() {
return tasks;
},
stats: function() {
var stats = {
total: tasks.length,
active: tasks.length,
completed: 0
};
var i = tasks.length;
while ( i-- ) {
if ( tasks[i].completed ) {
stats.completed++;
}
}
stats.active -= stats.completed;
return stats;
},
add: function( title ) {
var task = create( title, false );
tasks.push( task );
save();
return task;
},
edit: function( id, title ) {
var i = tasks.length;
while ( i-- ) {
if ( tasks[i].id === id ) {
tasks[i].title = title;
save();
return;
}
}
},
// toggle completion of task
toggle: function( id, completed ) {
var i = tasks.length;
while ( i-- ) {
if ( tasks[i].id === id ) {
tasks[i].completed = completed;
save();
return;
}
}
},
// toggle completion of all tasks
toggleAll: function( completed ) {
var i = tasks.length;
while ( i-- ) {
tasks[i].completed = completed;
}
save();
},
remove: function( id ) {
var i = tasks.length;
while ( i-- ) {
if ( tasks[i].id === id ) {
tasks.splice( i, 1 );
save();
return;
}
}
},
expunge: function() {
var i = tasks.length;
while ( i-- ) {
if ( tasks[i].completed ) {
tasks.splice( i, 1 );
}
}
save();
}
};
})( todos, window.localStorage, 'todos-duel' );
{
"targetDir": "www/",
"sourceDir": "target/todomvc/",
"serverPrefix" : "com.example.todos.views",
"cdnMap": "cdn",
"cdnLinksMap": "cdnLinks",
"cdnHost": "./",
"isDevMode": false,
"views": {
"index.html":
{
"view": "HomePage",
"data": {},
"extras": {}
}
},
"files": [
"robots.txt",
"favicon.ico"
]
}
/*
DUEL v0.8.2 http://duelengine.org
Copyright (c)2006-2012 Stephen M. McKamey.
Licensed under The MIT License.
*/
var duel=function(m,u,w){function x(a){this.value=a}function l(a){switch(typeof a){case "object":return!a?0:y(a)?2:a instanceof x?5:a instanceof Date?4:3;case "function":return 1;case "undefined":return 0;default:return 4}}function n(a){return"function"===typeof a}function o(){this.value=o.FAST?"":[]}function q(a){y(a)||(a=["",a]);this.value=a}function r(a,b){switch(l(b)){case 2:if(""===b[0])for(var c=1,f=b.length;c<f;c++)r(a,b[c]);else a.push(b);break;case 3:if(1===a.length)a.push(b);else if(c=a[1],
3===l(c))for(f in b)b.hasOwnProperty(f)&&(c[f]=b[f]);else a.splice(1,0,b);break;case 4:""!==b&&(b=""+b,c=a.length-1,0<c&&4===l(a[c])?a[c]+=b:a.push(b));break;case 0:break;default:a.push(b)}}function s(a,b,c,f,d,e){var g=3===l(a[1]);if(a.length===(g?3:2))return k(a[a.length-1],b,c,f,d,e);for(var h=[""],g=g?2:1,j=a.length;g<j;g++)r(h,k(a[g],b,c,f,d,e));return h}function A(a,b,c,f,d,e){for(var g=1,h=a.length;g<h;g++){var j=a[g],i=j[1].test;if(3===l(j[1])&&i&&(n(i)&&(i=i(b,c,f,d)),!i))continue;return s(j,
b,c,f,d,e)}return null}function J(a){2!==l(a)&&(a=["",a]);var b=function(b,f,d,e){try{var g=k(a,b,isFinite(f)?f:0,isFinite(d)?d:1,"string"===typeof e?e:null);return new q(g)}catch(h){return new q("["+h+"]")}};b.getView=function(){return a};return b}function K(a){return"string"!==typeof a?a:a.replace(/[&<>]/g,function(a){switch(a){case "&":return"&amp;";case "<":return"&lt;";case ">":return"&gt;";default:return a}})}function L(a){return"string"!==typeof a?a:a.replace(/[&<>"]/g,function(a){switch(a){case "&":return"&amp;";
case "<":return"&lt;";case ">":return"&gt;";case '"':return"&quot;";default:return a}})}function B(a,b){var c=b[0]||"",f=b.length,d=1,e,g=M[c];if("!"===c.charAt(0))"!DOCTYPE"===b[0]?a.append("<!DOCTYPE ",b[1],">"):a.append("<\!--",b[1],"--\>");else{if(c){a.append("<",c);e=b[d];if(3===l(e)){for(var h in e)if(e.hasOwnProperty(h)){var j=e[h];if(C[h])if(j)j=h;else continue;a.append(" ",h);0!==l(j)&&a.append('="',L(j),'"')}d++}g&&a.append(" /");a.append(">")}for(;d<f;d++)e=b[d],y(e)?B(a,e):a.append(K(e));
c&&!g&&a.append("</",c,">")}}function t(a){if(a){if("!"===a.charAt(0))return m.createComment("!"===a?"":a.substr(1)+" ")}else{if(m.createDocumentFragment)return m.createDocumentFragment();a=""}return"style"===a.toLowerCase()&&m.createStyleSheet?m.createStyleSheet():m.createElement(a)}function v(a,b){if(b){var c=(a.tagName||"").toLowerCase();if(8===a.nodeType)3===b.nodeType&&(a.nodeValue+=b.nodeValue);else if("table"===c&&a.tBodies)if(b.tagName)if((c=b.tagName.toLowerCase())&&"tbody"!==c&&"thead"!==
c){var f=0<a.tBodies.length?a.tBodies[a.tBodies.length-1]:null;f||(f=t("th"===c?"thead":"tbody"),a.appendChild(f));f.appendChild(b)}else!1!==a.canHaveChildren&&a.appendChild(b);else{if(11===b.nodeType)for(;b.firstChild;)v(a,b.removeChild(b.firstChild))}else if("style"===c&&m.createStyleSheet)a.cssText=b;else if(!1!==a.canHaveChildren)a.appendChild(b);else if("object"===c&&b.tagName&&"param"===b.tagName.toLowerCase()){try{a.appendChild(b)}catch(d){}try{if(a.object)a.object[b.name]=b.value}catch(e){}}}}
function D(a,b,c){switch(typeof c){case "function":a.addEventListener?a.addEventListener("on"===b.substr(0,2)?b.substr(2):b,c,!1):a[b]=c;break;case "string":a[b]=new Function("event",c)}}function E(a){return!!a&&3===a.nodeType&&(!a.nodeValue||!/\S/.exec(a.nodeValue))}function F(a,b){if(a&&3===a.nodeType&&b.exec(a.nodeValue))a.nodeValue=a.nodeValue.replace(b,"")}function z(a){if(a){for(;E(a.firstChild);)a.removeChild(a.firstChild);for(F(a.firstChild,N);E(a.lastChild);)a.removeChild(a.lastChild);F(a.lastChild,
O)}}function G(a,b){var c=a[b];if(c){try{delete a[b]}catch(f){a[b]=w}if(!n(c))try{c=new Function(""+c)}catch(d){c=null}}return c}function H(a){if(a){var b=G(a,"$init");b&&b.call(a);(b=G(a,"$load"))?setTimeout(function(){b.call(a);b=a=null},0):b=a=null}}function I(a,b){for(var c=1,f=b.length;c<f;c++){var d=b[c];switch(l(d)){case 2:var e=d[0],d=I(t(e),d);if("html"===e)return z(d),H(d),d;v(a,d);break;case 4:""!==d&&v(a,m.createTextNode(""+d));break;case 3:if(1===a.nodeType){var e=a,g=d;if(g.name&&m.attachEvent&&
!e.parentNode)try{var h=t("<"+e.tagName+' name="'+g.name+'">');e.tagName===h.tagName&&(e=h)}catch(j){}d=void 0;for(d in g)if(g.hasOwnProperty(d)){var i=g[d],k=l(i);if(d)0===k&&(i="",k=4),d=P[d.toLowerCase()]||d,C[d]?(e[d]=!!i,p[d]&&(e[p[d]]=!!i)):"style"===d?"undefined"!==typeof e.style.cssText?e.style.cssText=i:e.style=i:"class"===d?e.className=i:"on"===d.substr(0,2)?(D(e,d,i),p[d]&&D(e,p[d],i)):4===k&&"$"!==d.charAt(0)?(e.setAttribute(d,i),p[d]&&e.setAttribute(p[d],i)):(e[d]=i,p[d]&&(e[p[d]]=i))}a=
e}break;case 5:e=v;g=a;i=d;d=t("div");d.innerHTML=""+i;z(d);if(1===d.childNodes.length)d=d.firstChild;else{for(i=t("");d.firstChild;)i.appendChild(d.firstChild);d=i}e(g,d)}}z(a);H(a);if(11===a.nodeType&&1===a.childNodes.length)a=a.firstChild;return a}x.prototype.toString=function(){return this.value};var y=Array.isArray||function(a){return a instanceof Array};o.FAST=!(u&&9>u());o.prototype.append=function(a,b,c){o.FAST?null!==a&&(this.value+=a,null!==b&&b!==w&&(this.value+=b,null!==c&&c!==w&&(this.value+=
c))):this.value.push.apply(this.value,arguments)};o.prototype.clear=function(){this.value=o.FAST?"":[]};o.prototype.toString=function(){return o.FAST?this.value:this.value.join("")};var k;k=function(a,b,c,f,d,e){switch(l(a)){case 1:return a(b,c,f,d);case 2:var g=a[0]||"";switch(g){case "$for":a:{var h=a[1]||{},g=[""],j;if(h.hasOwnProperty("count")){j=h.count;n(j)&&(j=j(b,c,f,d));h.hasOwnProperty("data")?(h=h.data,n(h)&&(h=h(b,c,f,d))):h=b;for(b=0;b<j;b++)r(g,s(a,h,b,j,null,e))}else{if(h.hasOwnProperty("in")){var i=
h["in"];n(i)&&(i=i(b,c,f,d));if(3===l(i)){h=[];for(j in i)i.hasOwnProperty(j)&&h.push(j);for(b=0,j=h.length;b<j;b++)r(g,s(a,i[h[b]],b,j,h[b],e));a=g;break a}h=i}else h=h.each,n(h)&&(h=h(b,c,f,d));b=l(h);if(2===b)for(b=0,j=h.length;b<j;b++)r(g,s(a,h[b],b,j,null,e));else 0!==b&&(g=s(a,h,0,1,null,e))}a=g}return a;case "$xor":return A(a,b,c,f,d,e);case "$if":return A(["$xor",a],b,c,f,d,e);case "$call":e=a[1]||{};if(e.view){g=k(e.view,b,c,f,d);h=e.hasOwnProperty("data")?k(e.data,b,c,f,d):b;j=e.hasOwnProperty("index")?
k(e.index,b,c,f,d):c;i=e.hasOwnProperty("count")?k(e.count,b,c,f,d):f;b=e.hasOwnProperty("key")?k(e.key,b,c,f,d):d;c={};for(f=a.length-1;2<=f;f--)d=a[f],e=d[1]||{},e.hasOwnProperty("name")&&(c[e.name]=d);a=g&&n(g.getView)?k(g.getView(),h,j,i,b,c):null}else a=null;return a;case "$part":return g=(a[1]||{}).name||"",g=e&&e.hasOwnProperty(g)?e[g]:a,s(g,b,c,f,d)}g=[g];h=1;for(j=a.length;h<j;h++)r(g,k(a[h],b,c,f,d,e));return g;case 3:e={};for(g in a)a.hasOwnProperty(g)&&(e[g]=k(a[g],b,c,f,d));return e}return a};
u=function(a){return n(a)&&n(a.getView)?a:J(a)};u.raw=function(a){return new x(a)};var M={area:!0,base:!0,basefont:!0,br:!0,col:!0,frame:!0,hr:!0,img:!0,input:!0,isindex:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,wbr:!0},C={async:1,checked:1,defer:1,disabled:1,hidden:1,novalidate:1,formnovalidate:1};q.prototype.toString=function(){var a;var b=this.value;try{var c=new o;B(c,b);a=c.toString()}catch(f){a="["+f+"]"}return a};q.prototype.write=function(a){(a||m).write(""+this)};var P={rowspan:"rowSpan",
colspan:"colSpan",cellpadding:"cellPadding",cellspacing:"cellSpacing",tabindex:"tabIndex",accesskey:"accessKey",hidefocus:"hideFocus",usemap:"useMap",maxlength:"maxLength",readonly:"readOnly",contenteditable:"contentEditable"},p={enctype:"encoding",onscroll:"DOMMouseScroll",checked:"defaultChecked"},N=/^[\r\n]+/,O=/[\r\n]+$/;q.prototype.toDOM=function(a,b){4===l(a)&&(a=m.getElementById(a));var c;try{b&&(c=a,a=null),c=I(c||t(this.value[0]),this.value)}catch(f){c=m.createTextNode("["+f+"]")}a&&a.parentNode&&
a.parentNode.replaceChild(c,a);return c};q.prototype.reload=function(){var a=m;try{var b=this.toDOM();a.replaceChild(b,a.documentElement);if(a.createStyleSheet){for(var c=b.firstChild;c&&"HEAD"!==(c.tagName||"");)c=c.nextSibling;for(var f=c&&c.firstChild;f;){if("LINK"===(f.tagName||""))f.href=f.href;f=f.nextSibling}}}catch(d){a=a.open("text/html"),a.write(this.toString()),a.close()}};return u}(document,window.ScriptEngineMajorVersion);var todos=todos||{};
(function(i,f,h){function e(){var b="undefined"!==typeof JSON?JSON.stringify(a):a;f.setItem(h,b)}var a,f=f||function(){var b={};return{getItem:function(a){return b[a]},setItem:function(a,d){b[a]=d}}}(),g=f.getItem(h);a=g?"undefined"!==typeof JSON?JSON.parse(g):g:[];i.model={tasks:function(){return a},stats:function(){for(var b={total:a.length,active:a.length,completed:0},c=a.length;c--;)a[c].completed&&b.completed++;b.active-=b.completed;return b},add:function(b){b={id:((new Date).getTime()+Math.random()).toString(36),
title:b,completed:!1};a.push(b);e();return b},edit:function(b,c){for(var d=a.length;d--;)if(a[d].id===b){a[d].title=c;e();break}},toggle:function(b,c){for(var d=a.length;d--;)if(a[d].id===b){a[d].completed=c;e();break}},toggleAll:function(b){for(var c=a.length;c--;)a[c].completed=b;e()},remove:function(b){for(var c=a.length;c--;)if(a[c].id===b){a.splice(c,1);e();break}},expunge:function(){for(var b=a.length;b--;)a[b].completed&&a.splice(b,1);e()}}})(todos,window.localStorage,"todos-duel");var todos=todos||{};todos.views=todos.views||{};todos.views.Stats=duel([""," ",["$if",{test:function(a){return a.total}},["footer",{id:"footer"}," ",["span",{id:"todo-count"},["strong",function(a){return a.active}]," ",function(a){return 1===a.active?"item":"items"}," left"]," "," ",["$if",{test:function(a){return a.completed}},["button",{id:"clear-completed",onclick:function(){return todos.actions.clear_click}},"Clear completed (",function(a){return a.completed},")"]]]]]);var todos=todos||{};todos.views=todos.views||{};
todos.views.Task=duel([""," ",["li",{"class":function(a){return a.completed?"complete":""},ondblclick:function(a){return todos.actions.content_dblclick(a.id)}}," ",["div",{"class":"view"}," ",["input",{"class":"toggle",type:"checkbox",checked:function(a){return a.completed},onchange:function(a){return todos.actions.completed_change(a.id)}}]," ",["label",function(a){return a.title}]," ",["button",{"class":"destroy",onclick:function(a){return todos.actions.remove_click(a.id)}}]," "]," ",["input",{"class":"edit",
type:"text",value:function(a){return a.title},onblur:function(a){return todos.actions.edit_blur(a.id)},onkeypress:function(a){return todos.actions.edit_keypress(a.id)}}]]]);var todos=todos||{};todos.views=todos.views||{};todos.views.Tasks=duel([""," ",["$if",{test:function(a){return a.tasks.length}},["section",{id:"main"}," ",["input",{id:"toggle-all",type:"checkbox",checked:function(a){return!a.stats.active},onchange:function(){return todos.actions.toggle_change}}]," ",["label",{"for":"toggle-all"},"Mark all as complete"]," ",["ul",{id:"todo-list"}," ",["$for",{each:function(a){return a.tasks}}," ",["$call",{view:function(){return todos.views.Task}}]]," "]]]]);var todos=todos||{};todos.views=todos.views||{};todos.views.TodoApp=duel(["section",{id:"todoapp"}," ",["header",{id:"header"}," ",["h1","todos"]," ",["input",{id:"new-todo",placeholder:"What needs to be done?",autofocus:null,onblur:function(){return todos.actions.add_blur},onkeypress:function(){return todos.actions.add_keypress}}]," "]," ",["$call",{view:function(){return todos.views.Tasks},data:function(a){return a}}]," ",["$call",{view:function(){return todos.views.Stats},data:function(a){return a.stats}}]]);var todos=todos||{};
(function(b,e){function f(a){var a=a||b.model.stats(),a=b.views.Stats(a).toDOM(),c=e.getElementById(i);c?c.parentNode.replaceChild(a,c):e.getElementById(g).appendChild(a)}function d(){var a={tasks:b.model.tasks(),stats:b.model.stats()},c=b.views.Tasks(a).toDOM(),d=e.getElementById(j);d?d.parentNode.replaceChild(c,d):e.getElementById(g).appendChild(c);f(a.stats)}function h(a){var c=(a.value||"").trim();a.value="";c&&(a=b.model.add(c),(c=e.getElementById(k))?(c.appendChild(b.views.Task(a).toDOM()),f()):
d())}var i="footer",g="todoapp",j="main",k="todo-list";b.actions={add_blur:function(){h(this)},add_keypress:function(a){13===a.keyCode&&h(this)},edit_blur:function(a){return function(){var c=(this.value||"").trim();(this.value=c)?b.model.edit(a,c):b.model.remove(a);d()}},edit_keypress:function(){return function(a){13===a.keyCode&&this.blur()}},remove_click:function(a){return function(){b.model.remove(a);d()}},clear_click:function(){b.model.expunge();d()},content_dblclick:function(){return function(){this.className=
"editing";this.getElementsByTagName("input")[1].focus()}},completed_change:function(a){return function(){b.model.toggle(a,this.checked);d()}},toggle_change:function(){b.model.toggleAll(this.checked);d()}};(function(a){var c=b.views.TodoApp({tasks:b.model.tasks(),stats:b.model.stats()}).toDOM();a.insertBefore(c,a.firstChild)})(e.body)})(todos,document);
\ No newline at end of file
html,body{margin:0;padding:0;}button{margin:0;padding:0;border:0;background:none;font-size:100%;vertical-align:baseline;font-family:inherit;color:inherit;-webkit-appearance:none;-ms-appearance:none;-o-appearance:none;appearance:none;}body{font:14px 'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4em;background:#EEE url("0c975f0bb536597038b63a8cb0d85cff2222b0c9.png");color:#4d4d4d;width:550px;margin:0 auto;-webkit-font-smoothing:antialiased;-moz-font-smoothing:antialiased;-ms-font-smoothing:antialiased;-o-font-smoothing:antialiased;font-smoothing:antialiased;}#todoapp{background:#fff;background:rgba(255,255,255,0.9);margin:130px 0 40px 0;border:1px solid #ccc;position:relative;border-top-left-radius:2px;border-top-right-radius:2px;box-shadow:0 2px 6px 0 rgba(0,0,0,0.2),0 25px 50px 0 rgba(0,0,0,0.15);}#todoapp:before{content:'';border-left:1px solid #f5d6d6;border-right:1px solid #f5d6d6;width:2px;position:absolute;top:0;left:40px;height:100%;}#todoapp h1{position:absolute;top:-120px;width:100%;font-size:70px;font-weight:bold;text-align:center;color:#b3b3b3;color:rgba(255,255,255,0.3);text-shadow:-1px -1px rgba(0,0,0,0.2);-webkit-text-rendering:optimizeLegibility;-moz-text-rendering:optimizeLegibility;-ms-text-rendering:optimizeLegibility;-o-text-rendering:optimizeLegibility;text-rendering:optimizeLegibility;}#header{padding-top:15px;border-radius:inherit;}#todoapp header:before{content:'';position:absolute;top:0;right:0;left:0;height:15px;z-index:2;border-bottom:1px solid #6c615c;background:#8d7d77;background:-webkit-gradient(linear,left top,left bottom,from(rgba(132,110,100,0.8)),to(rgba(101,84,76,0.8)));background:-webkit-linear-gradient(top,rgba(132,110,100,0.8),rgba(101,84,76,0.8));background:-moz-linear-gradient(top,rgba(132,110,100,0.8),rgba(101,84,76,0.8));background:-o-linear-gradient(top,rgba(132,110,100,0.8),rgba(101,84,76,0.8));background:-ms-linear-gradient(top,rgba(132,110,100,0.8),rgba(101,84,76,0.8));background:linear-gradient(top,rgba(132,110,100,0.8),rgba(101,84,76,0.8));filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83',EndColorStr='#847670');border-radius:inherit;}#todoapp input::-webkit-input-placeholder{font-style:italic;}#todoapp input:-moz-placeholder{font-style:italic;color:#a9a9a9;}#new-todo,.edit{margin:0;width:100%;font-size:24px;font-family:inherit;line-height:1.4em;border:0;outline:none;color:inherit;padding:6px;border:1px solid #999;box-shadow:inset 0 -1px 5px 0 rgba(0,0,0,0.2);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;-o-box-sizing:border-box;box-sizing:border-box;-webkit-font-smoothing:antialiased;-moz-font-smoothing:antialiased;-ms-font-smoothing:antialiased;-o-font-smoothing:antialiased;font-smoothing:antialiased;}#new-todo{padding:16px 16px 16px 60px;border:none;background:rgba(0,0,0,0.02);position:relative;z-index:2;box-shadow:none;}#main{position:relative;z-index:2;border-top:1px dotted #adadad;}label[for='toggle-all']{display:none;}#toggle-all{position:absolute;top:-42px;left:12px;text-align:center;-webkit-appearance:none;-ms-appearance:none;-o-appearance:none;appearance:none;-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);}#toggle-all:before{content:'»';font-size:28px;color:#d9d9d9;padding:0 25px 7px;}#toggle-all:checked:before{color:#737373;}@media screen and (-webkit-min-device-pixel-ratio:0){#toggle-all{top:-52px;left:-11px;}}#todo-list{margin:0;padding:0;list-style:none;}#todo-list li{position:relative;font-size:24px;border-bottom:1px dotted #ccc;}#todo-list li:last-child{border-bottom:none;}#todo-list li.editing{border-bottom:none;padding:0;}#todo-list li.editing .edit{display:block;width:506px;padding:13px 17px 12px 17px;margin:0 0 0 43px;}#todo-list li.editing .view{display:none;}#todo-list li .toggle{text-align:center;width:35px;-webkit-appearance:none;-ms-appearance:none;-o-appearance:none;appearance:none;}#todo-list li .toggle:after{font-size:18px;content:'✔';line-height:40px;font-size:20px;color:#d9d9d9;text-shadow:0 -1px 0 #bfbfbf;}#todo-list li .toggle:checked:after{color:#85ada7;text-shadow:0 1px 0 #669991;bottom:1px;position:relative;}#todo-list li label{word-break:break-word;margin:20px 15px;display:inline-block;-webkit-transition:color 0.4s;-moz-transition:color 0.4s;-ms-transition:color 0.4s;-o-transition:color 0.4s;transition:color 0.4s;}#todo-list li.complete label{color:#a9a9a9;text-decoration:line-through;}#todo-list li .destroy{display:none;position:absolute;top:10px;right:10px;width:40px;height:40px;font-size:22px;color:#a88a8a;-webkit-transition:all 0.2s;-moz-transition:all 0.2s;-ms-transition:all 0.2s;-o-transition:all 0.2s;transition:all 0.2s;}#todo-list li .destroy:hover{text-shadow:0 0 1px #000,0 0 10px rgba(199,107,107,0.8);-webkit-transform:scale(1.3);-moz-transform:scale(1.3);-ms-transform:scale(1.3);-o-transform:scale(1.3);transform:scale(1.3);}#todo-list li .destroy:after{content:'✖';}#todo-list li:hover .destroy{display:block;}#todo-list li .edit{display:none;}#todo-list li.editing:last-child{margin-bottom:-1px;}#footer{color:#777;padding:0 15px;position:absolute;right:0;bottom:-31px;left:0;z-index:1;text-align:center;}#footer:before{content:'';position:absolute;right:0;bottom:31px;left:0;height:100px;z-index:-1;box-shadow:0 1px 1px rgba(0,0,0,0.3),0 6px 0 -3px rgba(255,255,255,0.8),0 7px 1px -3px rgba(0,0,0,0.3),0 42px 0 -6px rgba(255,255,255,0.8),0 43px 2px -6px rgba(0,0,0,0.2);}#todo-count{float:left;text-align:left;}#filters{margin:0;padding:0;list-style:none;position:absolute;right:0;left:0;}#filters li{display:inline;}#filters li a{color:#83756f;margin:2px;text-decoration:none;}#filters li a.selected{font-weight:bold;}#clear-completed{float:right;line-height:20px;text-decoration:none;background:rgba(0,0,0,0.1);font-size:11px;padding:0 10px;position:relative;border-radius:3px;box-shadow:0 -1px 0 0 rgba(0,0,0,0.2);}#clear-completed:hover{background:rgba(0,0,0,0.15);box-shadow:0 -1px 0 0 rgba(0,0,0,0.3);}#info{margin:65px auto 0;color:#a6a6a6;font-size:12px;text-shadow:0 1px 0 rgba(255,255,255,0.7);text-align:center;}#info a{color:inherit;}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>DUEL &bull; TodoMVC</title>
<link rel="stylesheet" href="./cdn/502117dcda6b1a71004e818c348c9de5fc80966a.css" />
<!--[if lt IE 9]><script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
</head>
<body>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<p>Ported to <a href="http://duelengine.org">DUEL</a> by <a href="http://mck.me">Stephen McKamey</a></p>
</footer>
<script src="./cdn/1de7392f10ea2465d68b839144090d158ba7730f.js"></script>
</body>
</html>
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment