({}, _todos[id], updates);
77 | store.set(keyPrefix + id, _todos[id]);
78 | }
79 |
80 | /**
81 | * Update all of the TODO items with the same object.
82 | * the data to be updated. Used to mark all TODOs as completed.
83 | * @param {object} updates An object literal containing only the data to be
84 | * updated.
85 |
86 | */
87 | function updateAll(updates:TodoUpdateData): void {
88 | var id: string;
89 | for (id in _todos) {
90 | if(_todos.hasOwnProperty(id)) {
91 | update(id, updates);
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * Delete a TODO item.
98 | * @param {string} id
99 | */
100 | function destroy(id: string): void {
101 | delete _todos[id];
102 | store.remove(keyPrefix + id);
103 | }
104 |
105 | /**
106 | * Delete all the completed TODO items.
107 | */
108 | function destroyCompleted(): void {
109 | var id: string;
110 | for (id in _todos) {
111 | if (_todos[id].complete) {
112 | destroy(id);
113 | }
114 | }
115 | }
116 |
117 | class TodoStoreStatic extends EventEmitter {
118 |
119 | /**
120 | * Tests whether all the remaining TODO items are marked as completed.
121 | * @return {boolean}
122 | */
123 | public areAllComplete(): boolean {
124 | var id: string;
125 |
126 | for(id in _todos) {
127 | if(!_todos[id].complete) {
128 | return false;
129 | }
130 | }
131 | return true;
132 | }
133 |
134 | /**
135 | * Get the entire collection of TODOs.
136 | * @return {object}
137 | */
138 | public getAll(): TodoMap {
139 | return _todos;
140 | }
141 |
142 | public emitChange(): void {
143 | this.emit(CHANGE_EVENT);
144 | }
145 |
146 | /**
147 | * @param {function} callback
148 | */
149 | public addChangeListener(callback: () => void): void {
150 | this.on(CHANGE_EVENT, callback);
151 | }
152 |
153 | /**
154 | * @param {function} callback
155 | */
156 | public removeChangeListener(callback: () => void) {
157 | this.removeListener(CHANGE_EVENT, callback);
158 | }
159 | }
160 |
161 | var TodoStore: TodoStoreStatic = new TodoStoreStatic();
162 |
163 | // Register callback to handle all updates
164 | AppDispatcher.register( function(action:TodoAction): void {
165 | var text: string;
166 |
167 | switch(action.actionType) {
168 | case TodoActionID.TODO_CREATE:
169 | text = action.text.trim();
170 | if (text !== '') {
171 | create(text);
172 | }
173 | TodoStore.emitChange();
174 | break;
175 |
176 | case TodoActionID.TODO_TOGGLE_COMPLETE_ALL:
177 | if (TodoStore.areAllComplete()) {
178 | updateAll({complete: false});
179 | } else {
180 | updateAll({complete: true});
181 | }
182 | TodoStore.emitChange();
183 | break;
184 |
185 | case TodoActionID.TODO_UNDO_COMPLETE:
186 | update(action.id, {complete: false});
187 | TodoStore.emitChange();
188 | break;
189 |
190 | case TodoActionID.TODO_COMPLETE:
191 | update(action.id, {complete: true});
192 | TodoStore.emitChange();
193 | break;
194 |
195 | case TodoActionID.TODO_UPDATE_TEXT:
196 | text = action.text.trim();
197 | if (text !== '') {
198 | update(action.id, {text: text});
199 | }
200 | TodoStore.emitChange();
201 | break;
202 |
203 | case TodoActionID.TODO_DESTROY:
204 | destroy(action.id);
205 | TodoStore.emitChange();
206 | break;
207 |
208 | case TodoActionID.TODO_DESTROY_COMPLETED:
209 | destroyCompleted();
210 | TodoStore.emitChange();
211 | break;
212 |
213 | // default:
214 | // no op
215 | }
216 | });
217 |
218 | export = TodoStore;
219 |
--------------------------------------------------------------------------------
/src/react/ReactComponent.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | import React = require('react/addons');
5 |
6 | class ReactComponent extends React.Component
{
7 |
8 | public getDerivedInitialState(): S {
9 | return null;
10 | }
11 |
12 | public getInitialState = (): S => {
13 | return this.getDerivedInitialState();
14 | };
15 |
16 | /**
17 | * @see React.createClass
18 | */
19 | constructor(props:P, context:any) {
20 |
21 | super(props, context);
22 |
23 | this.props = props;
24 | this.context = context;
25 | this.state = this.getInitialState();
26 |
27 | // Nasty trick to avoid warnings.
28 | this.getInitialState = null;
29 | }
30 | }
31 |
32 | export = ReactComponent;
33 |
--------------------------------------------------------------------------------
/src/react/ReactJSX.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | import React = require('react/addons');
6 | import ReactTools = require('react-tools');
7 |
8 | var _react = React;
9 |
10 | interface MapStringTo {
11 | [key:string]: T
12 | }
13 |
14 | /*
15 | Horrible, horrible trick using evil eval in order to create a function from a
16 | template string that has been transformed by ReactTools.transform().
17 |
18 | That said, this technique is probably not known to many fellow devs.
19 | At least I didn't find it mentioned in this post that everybody refers to:
20 | http://perfectionkills.com/global-eval-what-are-the-options/
21 | */
22 |
23 | function ReactJSX( jsx: string, owner: any = null, imports: MapStringTo = {} ): React.ReactElement {
24 | var reactCode: string = 'function jsx(imports) { \n';
25 |
26 | // No need to let every call site add React to the imports.
27 | // That one comes for free, we'll add it here:
28 | reactCode += 'var React = imports.React;\n';
29 | imports['React'] = _react;
30 |
31 | for( var im in imports ) {
32 | reactCode += 'var ' + im + ' = imports.' + im + ';\n';
33 | }
34 |
35 | reactCode += 'return (' + ReactTools.transform(jsx, { harmony: false }) + '); }; jsx;';
36 |
37 | // I believe the discussion in http://perfectionkills.com/global-eval-what-are-the-options/
38 | // missed that you can eval code that creates a function.
39 | // If done right, that function can be used like any other function in the caller's domain
40 | // as long as all scope variables are captured in 'imports'.
41 | var fn: () => React.ReactElement = eval.call(null,reactCode);
42 | var element: React.ReactElement = fn.call(owner, imports);
43 | return element;
44 | }
45 |
46 | export = ReactJSX;
47 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.5.0-beta",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "commonjs",
6 | "declaration": false,
7 | "noImplicitAny": true,
8 | "removeComments": false,
9 | "noLib": false,
10 | "outDir": "./build/js/"
11 | },
12 | "filesGlob": [
13 | "./src/**/*.ts",
14 | "./build/src/**/*.ts",
15 | "!./node_modules/**/*.ts"
16 | ],
17 | "files": [
18 | "./build/src/actions/TodoActions.ts",
19 | "./build/src/app.ts",
20 | "./build/src/components/Footer.react.ts",
21 | "./build/src/components/Header.react.ts",
22 | "./build/src/components/MainSection.react.ts",
23 | "./build/src/components/TodoApp.react.ts",
24 | "./build/src/components/TodoItem.react.ts",
25 | "./build/src/components/TodoTextInput.react.ts",
26 | "./build/src/constants/TodoConstants.ts",
27 | "./build/src/dispatcher/AppDispatcher.ts",
28 | "./build/src/react/ReactComponent.ts",
29 | "./build/src/react/ReactJSX.ts",
30 | "./build/src/stores/TodoStore.ts",
31 | "./build/typings/browserify/browserify.d.ts",
32 | "./build/typings/flux/flux.d.ts",
33 | "./build/typings/node/node.d.ts",
34 | "./build/typings/object-assign/object-assign.d.ts",
35 | "./build/typings/react-jsx/react-jsx.d.ts",
36 | "./build/typings/react-tools/react-tools.d.ts",
37 | "./build/typings/react/react-addons.d.ts",
38 | "./build/typings/react/react-global.d.ts",
39 | "./build/typings/react/react.d.ts",
40 | "./build/typings/todomvc/todomvc.d.ts",
41 | "./build/typings/tsd.d.ts",
42 | "./build/typings/typings/tsd.d.ts"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/typings/object-assign/object-assign.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare module "object-assign" {
3 | function assign(target:U, source:T, ...args: U[]): T;
4 |
5 | export = assign;
6 | }
7 |
--------------------------------------------------------------------------------
/typings/react-jsx/react-jsx.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Source: https://github.com/jbrantly/reactconf/blob/master/src_ts/lib/react-jsx.d.ts
3 | */
4 |
5 | ///
6 |
7 | declare module 'react/addons' {
8 | function jsx(jsx?: string): React.ReactElement;
9 | }
10 |
11 | declare module 'react/lib/cx' {
12 | function cs(json:any): string;
13 | export = cs;
14 | }
15 |
--------------------------------------------------------------------------------
/typings/react-tools/react-tools.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare module 'react-tools' {
3 |
4 | interface TransformOptions
5 | {
6 | harmony?: boolean;
7 | }
8 |
9 | function transform(jsx: string, options: TransformOptions): string;
10 | }
11 |
--------------------------------------------------------------------------------
/typings/todomvc/todomvc.d.ts:
--------------------------------------------------------------------------------
1 | interface MapStringTo {
2 | [key:string]: T;
3 | }
4 |
5 | interface TodoData {
6 | id: string;
7 | complete: boolean;
8 | text: string;
9 | }
10 |
11 | interface TodoState {
12 | allTodos: MapStringTo;
13 | areAllComplete: boolean;
14 | }
15 |
16 | interface TodoItemProps {
17 | todo: TodoData;
18 | }
19 |
20 | interface TodoAction {
21 | actionType: number;
22 | id?: string;
23 | text?: string;
24 | }
25 |
26 | interface TodoTextInputElement {
27 | id: string;
28 | }
29 |
30 | declare module todomvc {
31 | type TodoMap = MapStringTo;
32 | }
33 |
--------------------------------------------------------------------------------
/typings/tsd.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 | ///
7 | ///
8 |
--------------------------------------------------------------------------------
/typings/tsd.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "v4",
3 | "repo": "borisyankov/DefinitelyTyped",
4 | "ref": "master",
5 | "path": ".",
6 | "bundle": "typings/tsd.d.ts",
7 | "installed": {
8 | "browserify/browserify.d.ts": {
9 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0"
10 | },
11 | "node/node.d.ts": {
12 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0"
13 | },
14 | "react/react.d.ts": {
15 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0"
16 | },
17 | "react/react-addons.d.ts": {
18 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0"
19 | },
20 | "react/react-global.d.ts": {
21 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0"
22 | },
23 | "flux/flux.d.ts": {
24 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0"
25 | },
26 | "eventemitter3/eventemitter3.d.ts": {
27 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0"
28 | },
29 | "storejs/storejs.d.ts": {
30 | "commit": "b3834d886a95789e6ab56e8244775ec10c5293d0"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/web/css/app.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2015, Facebook, Inc.
3 | * All rights reserved.
4 | *
5 | * This source code is licensed under the BSD-style license found in the
6 | * LICENSE file in the root directory of this source tree. An additional grant
7 | * of patent rights can be found in the PATENTS file in the same directory.
8 | *
9 | * base.css overrides
10 | */
11 |
12 | /**
13 | * We are not changing from display:none, but rather re-rendering instead.
14 | * Therefore this needs to be displayed normally by default.
15 | */
16 | #todo-list li .edit {
17 | display: inline;
18 | }
19 |
--------------------------------------------------------------------------------
/web/debug.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Flux • TodoMVC
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Flux • TodoMVC
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/web/todomvc-app-css/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-font-smoothing: antialiased;
21 | font-smoothing: antialiased;
22 | }
23 |
24 | body {
25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
26 | line-height: 1.4em;
27 | background: #f5f5f5;
28 | color: #4d4d4d;
29 | min-width: 230px;
30 | max-width: 550px;
31 | margin: 0 auto;
32 | -webkit-font-smoothing: antialiased;
33 | -moz-font-smoothing: antialiased;
34 | font-smoothing: antialiased;
35 | font-weight: 300;
36 | }
37 |
38 | button,
39 | input[type="checkbox"] {
40 | outline: none;
41 | }
42 |
43 | .hidden {
44 | display: none;
45 | }
46 |
47 | #todoapp {
48 | background: #fff;
49 | margin: 130px 0 40px 0;
50 | position: relative;
51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
53 | }
54 |
55 | #todoapp input::-webkit-input-placeholder {
56 | font-style: italic;
57 | font-weight: 300;
58 | color: #e6e6e6;
59 | }
60 |
61 | #todoapp input::-moz-placeholder {
62 | font-style: italic;
63 | font-weight: 300;
64 | color: #e6e6e6;
65 | }
66 |
67 | #todoapp input::input-placeholder {
68 | font-style: italic;
69 | font-weight: 300;
70 | color: #e6e6e6;
71 | }
72 |
73 | #todoapp h1 {
74 | position: absolute;
75 | top: -155px;
76 | width: 100%;
77 | font-size: 100px;
78 | font-weight: 100;
79 | text-align: center;
80 | color: rgba(175, 47, 47, 0.15);
81 | -webkit-text-rendering: optimizeLegibility;
82 | -moz-text-rendering: optimizeLegibility;
83 | text-rendering: optimizeLegibility;
84 | }
85 |
86 | #new-todo,
87 | .edit {
88 | position: relative;
89 | margin: 0;
90 | width: 100%;
91 | font-size: 24px;
92 | font-family: inherit;
93 | font-weight: inherit;
94 | line-height: 1.4em;
95 | border: 0;
96 | outline: none;
97 | color: inherit;
98 | padding: 6px;
99 | border: 1px solid #999;
100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
101 | box-sizing: border-box;
102 | -webkit-font-smoothing: antialiased;
103 | -moz-font-smoothing: antialiased;
104 | font-smoothing: antialiased;
105 | }
106 |
107 | #new-todo {
108 | padding: 16px 16px 16px 60px;
109 | border: none;
110 | background: rgba(0, 0, 0, 0.003);
111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
112 | }
113 |
114 | #main {
115 | position: relative;
116 | z-index: 2;
117 | border-top: 1px solid #e6e6e6;
118 | }
119 |
120 | label[for='toggle-all'] {
121 | display: none;
122 | }
123 |
124 | #toggle-all {
125 | position: absolute;
126 | top: -55px;
127 | left: -12px;
128 | width: 60px;
129 | height: 34px;
130 | text-align: center;
131 | border: none; /* Mobile Safari */
132 | }
133 |
134 | #toggle-all:before {
135 | content: '❯';
136 | font-size: 22px;
137 | color: #e6e6e6;
138 | padding: 10px 27px 10px 27px;
139 | }
140 |
141 | #toggle-all:checked:before {
142 | color: #737373;
143 | }
144 |
145 | #todo-list {
146 | margin: 0;
147 | padding: 0;
148 | list-style: none;
149 | }
150 |
151 | #todo-list li {
152 | position: relative;
153 | font-size: 24px;
154 | border-bottom: 1px solid #ededed;
155 | }
156 |
157 | #todo-list li:last-child {
158 | border-bottom: none;
159 | }
160 |
161 | #todo-list li.editing {
162 | border-bottom: none;
163 | padding: 0;
164 | }
165 |
166 | #todo-list li.editing .edit {
167 | display: block;
168 | width: 506px;
169 | padding: 13px 17px 12px 17px;
170 | margin: 0 0 0 43px;
171 | }
172 |
173 | #todo-list li.editing .view {
174 | display: none;
175 | }
176 |
177 | #todo-list li .toggle {
178 | text-align: center;
179 | width: 40px;
180 | /* auto, since non-WebKit browsers doesn't support input styling */
181 | height: auto;
182 | position: absolute;
183 | top: 0;
184 | bottom: 0;
185 | margin: auto 0;
186 | border: none; /* Mobile Safari */
187 | -webkit-appearance: none;
188 | appearance: none;
189 | }
190 |
191 | #todo-list li .toggle:after {
192 | content: url('data:image/svg+xml;utf8,');
193 | }
194 |
195 | #todo-list li .toggle:checked:after {
196 | content: url('data:image/svg+xml;utf8,');
197 | }
198 |
199 | #todo-list li label {
200 | white-space: pre;
201 | word-break: break-word;
202 | padding: 15px 60px 15px 15px;
203 | margin-left: 45px;
204 | display: block;
205 | line-height: 1.2;
206 | transition: color 0.4s;
207 | }
208 |
209 | #todo-list li.completed label {
210 | color: #d9d9d9;
211 | text-decoration: line-through;
212 | }
213 |
214 | #todo-list li .destroy {
215 | display: none;
216 | position: absolute;
217 | top: 0;
218 | right: 10px;
219 | bottom: 0;
220 | width: 40px;
221 | height: 40px;
222 | margin: auto 0;
223 | font-size: 30px;
224 | color: #cc9a9a;
225 | margin-bottom: 11px;
226 | transition: color 0.2s ease-out;
227 | }
228 |
229 | #todo-list li .destroy:hover {
230 | color: #af5b5e;
231 | }
232 |
233 | #todo-list li .destroy:after {
234 | content: '×';
235 | }
236 |
237 | #todo-list li:hover .destroy {
238 | display: block;
239 | }
240 |
241 | #todo-list li .edit {
242 | display: none;
243 | }
244 |
245 | #todo-list li.editing:last-child {
246 | margin-bottom: -1px;
247 | }
248 |
249 | #footer {
250 | color: #777;
251 | padding: 10px 15px;
252 | height: 20px;
253 | text-align: center;
254 | border-top: 1px solid #e6e6e6;
255 | }
256 |
257 | #footer:before {
258 | content: '';
259 | position: absolute;
260 | right: 0;
261 | bottom: 0;
262 | left: 0;
263 | height: 50px;
264 | overflow: hidden;
265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
266 | 0 8px 0 -3px #f6f6f6,
267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
268 | 0 16px 0 -6px #f6f6f6,
269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
270 | }
271 |
272 | #todo-count {
273 | float: left;
274 | text-align: left;
275 | }
276 |
277 | #todo-count strong {
278 | font-weight: 300;
279 | }
280 |
281 | #filters {
282 | margin: 0;
283 | padding: 0;
284 | list-style: none;
285 | position: absolute;
286 | right: 0;
287 | left: 0;
288 | }
289 |
290 | #filters li {
291 | display: inline;
292 | }
293 |
294 | #filters li a {
295 | color: inherit;
296 | margin: 3px;
297 | padding: 3px 7px;
298 | text-decoration: none;
299 | border: 1px solid transparent;
300 | border-radius: 3px;
301 | }
302 |
303 | #filters li a.selected,
304 | #filters li a:hover {
305 | border-color: rgba(175, 47, 47, 0.1);
306 | }
307 |
308 | #filters li a.selected {
309 | border-color: rgba(175, 47, 47, 0.2);
310 | }
311 |
312 | #clear-completed,
313 | html #clear-completed:active {
314 | float: right;
315 | position: relative;
316 | line-height: 20px;
317 | text-decoration: none;
318 | cursor: pointer;
319 | position: relative;
320 | }
321 |
322 | #clear-completed:hover {
323 | text-decoration: underline;
324 | }
325 |
326 | #info {
327 | margin: 65px auto 0;
328 | color: #bfbfbf;
329 | font-size: 10px;
330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
331 | text-align: center;
332 | }
333 |
334 | #info p {
335 | line-height: 1;
336 | }
337 |
338 | #info a {
339 | color: inherit;
340 | text-decoration: none;
341 | font-weight: 400;
342 | }
343 |
344 | #info a:hover {
345 | text-decoration: underline;
346 | }
347 |
348 | /*
349 | Hack to remove background from Mobile Safari.
350 | Can't use it globally since it destroys checkboxes in Firefox
351 | */
352 | @media screen and (-webkit-min-device-pixel-ratio:0) {
353 | #toggle-all,
354 | #todo-list li .toggle {
355 | background: none;
356 | }
357 |
358 | #todo-list li .toggle {
359 | height: 40px;
360 | }
361 |
362 | #toggle-all {
363 | -webkit-transform: rotate(90deg);
364 | transform: rotate(90deg);
365 | -webkit-appearance: none;
366 | appearance: none;
367 | }
368 | }
369 |
370 | @media (max-width: 430px) {
371 | #footer {
372 | height: 50px;
373 | }
374 |
375 | #filters {
376 | bottom: 10px;
377 | }
378 | }
379 |
--------------------------------------------------------------------------------
/web/todomvc-common/base.css:
--------------------------------------------------------------------------------
1 | hr {
2 | margin: 20px 0;
3 | border: 0;
4 | border-top: 1px dashed #c5c5c5;
5 | border-bottom: 1px dashed #f7f7f7;
6 | }
7 |
8 | .learn a {
9 | font-weight: normal;
10 | text-decoration: none;
11 | color: #b83f45;
12 | }
13 |
14 | .learn a:hover {
15 | text-decoration: underline;
16 | color: #787e7e;
17 | }
18 |
19 | .learn h3,
20 | .learn h4,
21 | .learn h5 {
22 | margin: 10px 0;
23 | font-weight: 500;
24 | line-height: 1.2;
25 | color: #000;
26 | }
27 |
28 | .learn h3 {
29 | font-size: 24px;
30 | }
31 |
32 | .learn h4 {
33 | font-size: 18px;
34 | }
35 |
36 | .learn h5 {
37 | margin-bottom: 0;
38 | font-size: 14px;
39 | }
40 |
41 | .learn ul {
42 | padding: 0;
43 | margin: 0 0 30px 25px;
44 | }
45 |
46 | .learn li {
47 | line-height: 20px;
48 | }
49 |
50 | .learn p {
51 | font-size: 15px;
52 | font-weight: 300;
53 | line-height: 1.3;
54 | margin-top: 0;
55 | margin-bottom: 0;
56 | }
57 |
58 | #issue-count {
59 | display: none;
60 | }
61 |
62 | .quote {
63 | border: none;
64 | margin: 20px 0 60px 0;
65 | }
66 |
67 | .quote p {
68 | font-style: italic;
69 | }
70 |
71 | .quote p:before {
72 | content: '“';
73 | font-size: 50px;
74 | opacity: .15;
75 | position: absolute;
76 | top: -20px;
77 | left: 3px;
78 | }
79 |
80 | .quote p:after {
81 | content: '”';
82 | font-size: 50px;
83 | opacity: .15;
84 | position: absolute;
85 | bottom: -42px;
86 | right: 3px;
87 | }
88 |
89 | .quote footer {
90 | position: absolute;
91 | bottom: -40px;
92 | right: 0;
93 | }
94 |
95 | .quote footer img {
96 | border-radius: 3px;
97 | }
98 |
99 | .quote footer a {
100 | margin-left: 5px;
101 | vertical-align: middle;
102 | }
103 |
104 | .speech-bubble {
105 | position: relative;
106 | padding: 10px;
107 | background: rgba(0, 0, 0, .04);
108 | border-radius: 5px;
109 | }
110 |
111 | .speech-bubble:after {
112 | content: '';
113 | position: absolute;
114 | top: 100%;
115 | right: 30px;
116 | border: 13px solid transparent;
117 | border-top-color: rgba(0, 0, 0, .04);
118 | }
119 |
120 | .learn-bar > .learn {
121 | position: absolute;
122 | width: 272px;
123 | top: 8px;
124 | left: -300px;
125 | padding: 10px;
126 | border-radius: 5px;
127 | background-color: rgba(255, 255, 255, .6);
128 | transition-property: left;
129 | transition-duration: 500ms;
130 | }
131 |
132 | @media (min-width: 899px) {
133 | .learn-bar {
134 | width: auto;
135 | padding-left: 300px;
136 | }
137 |
138 | .learn-bar > .learn {
139 | left: 8px;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/web/todomvc-common/base.js:
--------------------------------------------------------------------------------
1 | /* global _ */
2 | (function () {
3 | 'use strict';
4 |
5 | /* jshint ignore:start */
6 | // Underscore's Template Module
7 | // Courtesy of underscorejs.org
8 | var _ = (function (_) {
9 | _.defaults = function (object) {
10 | if (!object) {
11 | return object;
12 | }
13 | for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
14 | var iterable = arguments[argsIndex];
15 | if (iterable) {
16 | for (var key in iterable) {
17 | if (object[key] == null) {
18 | object[key] = iterable[key];
19 | }
20 | }
21 | }
22 | }
23 | return object;
24 | }
25 |
26 | // By default, Underscore uses ERB-style template delimiters, change the
27 | // following template settings to use alternative delimiters.
28 | _.templateSettings = {
29 | evaluate : /<%([\s\S]+?)%>/g,
30 | interpolate : /<%=([\s\S]+?)%>/g,
31 | escape : /<%-([\s\S]+?)%>/g
32 | };
33 |
34 | // When customizing `templateSettings`, if you don't want to define an
35 | // interpolation, evaluation or escaping regex, we need one that is
36 | // guaranteed not to match.
37 | var noMatch = /(.)^/;
38 |
39 | // Certain characters need to be escaped so that they can be put into a
40 | // string literal.
41 | var escapes = {
42 | "'": "'",
43 | '\\': '\\',
44 | '\r': 'r',
45 | '\n': 'n',
46 | '\t': 't',
47 | '\u2028': 'u2028',
48 | '\u2029': 'u2029'
49 | };
50 |
51 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
52 |
53 | // JavaScript micro-templating, similar to John Resig's implementation.
54 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
55 | // and correctly escapes quotes within interpolated code.
56 | _.template = function(text, data, settings) {
57 | var render;
58 | settings = _.defaults({}, settings, _.templateSettings);
59 |
60 | // Combine delimiters into one regular expression via alternation.
61 | var matcher = new RegExp([
62 | (settings.escape || noMatch).source,
63 | (settings.interpolate || noMatch).source,
64 | (settings.evaluate || noMatch).source
65 | ].join('|') + '|$', 'g');
66 |
67 | // Compile the template source, escaping string literals appropriately.
68 | var index = 0;
69 | var source = "__p+='";
70 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
71 | source += text.slice(index, offset)
72 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
73 |
74 | if (escape) {
75 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
76 | }
77 | if (interpolate) {
78 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
79 | }
80 | if (evaluate) {
81 | source += "';\n" + evaluate + "\n__p+='";
82 | }
83 | index = offset + match.length;
84 | return match;
85 | });
86 | source += "';\n";
87 |
88 | // If a variable is not specified, place data values in local scope.
89 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
90 |
91 | source = "var __t,__p='',__j=Array.prototype.join," +
92 | "print=function(){__p+=__j.call(arguments,'');};\n" +
93 | source + "return __p;\n";
94 |
95 | try {
96 | render = new Function(settings.variable || 'obj', '_', source);
97 | } catch (e) {
98 | e.source = source;
99 | throw e;
100 | }
101 |
102 | if (data) return render(data, _);
103 | var template = function(data) {
104 | return render.call(this, data, _);
105 | };
106 |
107 | // Provide the compiled function source as a convenience for precompilation.
108 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
109 |
110 | return template;
111 | };
112 |
113 | return _;
114 | })({});
115 |
116 | if (location.hostname === 'todomvc.com') {
117 | window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
118 | }
119 | /* jshint ignore:end */
120 |
121 | function redirect() {
122 | if (location.hostname === 'tastejs.github.io') {
123 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
124 | }
125 | }
126 |
127 | function findRoot() {
128 | var base = location.href.indexOf('examples/');
129 | return location.href.substr(0, base);
130 | }
131 |
132 | function getFile(file, callback) {
133 | if (!location.host) {
134 | return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
135 | }
136 |
137 | var xhr = new XMLHttpRequest();
138 |
139 | xhr.open('GET', findRoot() + file, true);
140 | xhr.send();
141 |
142 | xhr.onload = function () {
143 | if (xhr.status === 200 && callback) {
144 | callback(xhr.responseText);
145 | }
146 | };
147 | }
148 |
149 | function Learn(learnJSON, config) {
150 | if (!(this instanceof Learn)) {
151 | return new Learn(learnJSON, config);
152 | }
153 |
154 | var template, framework;
155 |
156 | if (typeof learnJSON !== 'object') {
157 | try {
158 | learnJSON = JSON.parse(learnJSON);
159 | } catch (e) {
160 | return;
161 | }
162 | }
163 |
164 | if (config) {
165 | template = config.template;
166 | framework = config.framework;
167 | }
168 |
169 | if (!template && learnJSON.templates) {
170 | template = learnJSON.templates.todomvc;
171 | }
172 |
173 | if (!framework && document.querySelector('[data-framework]')) {
174 | framework = document.querySelector('[data-framework]').dataset.framework;
175 | }
176 |
177 | this.template = template;
178 |
179 | if (learnJSON.backend) {
180 | this.frameworkJSON = learnJSON.backend;
181 | this.frameworkJSON.issueLabel = framework;
182 | this.append({
183 | backend: true
184 | });
185 | } else if (learnJSON[framework]) {
186 | this.frameworkJSON = learnJSON[framework];
187 | this.frameworkJSON.issueLabel = framework;
188 | this.append();
189 | }
190 |
191 | this.fetchIssueCount();
192 | }
193 |
194 | Learn.prototype.append = function (opts) {
195 | var aside = document.createElement('aside');
196 | aside.innerHTML = _.template(this.template, this.frameworkJSON);
197 | aside.className = 'learn';
198 |
199 | if (opts && opts.backend) {
200 | // Remove demo link
201 | var sourceLinks = aside.querySelector('.source-links');
202 | var heading = sourceLinks.firstElementChild;
203 | var sourceLink = sourceLinks.lastElementChild;
204 | // Correct link path
205 | var href = sourceLink.getAttribute('href');
206 | sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
207 | sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
208 | } else {
209 | // Localize demo links
210 | var demoLinks = aside.querySelectorAll('.demo-link');
211 | Array.prototype.forEach.call(demoLinks, function (demoLink) {
212 | if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
213 | demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
214 | }
215 | });
216 | }
217 |
218 | document.body.className = (document.body.className + ' learn-bar').trim();
219 | document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
220 | };
221 |
222 | Learn.prototype.fetchIssueCount = function () {
223 | var issueLink = document.getElementById('issue-count-link');
224 | if (issueLink) {
225 | var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
226 | var xhr = new XMLHttpRequest();
227 | xhr.open('GET', url, true);
228 | xhr.onload = function (e) {
229 | var parsedResponse = JSON.parse(e.target.responseText);
230 | if (parsedResponse instanceof Array) {
231 | var count = parsedResponse.length
232 | if (count !== 0) {
233 | issueLink.innerHTML = 'This app has ' + count + ' open issues';
234 | document.getElementById('issue-count').style.display = 'inline';
235 | }
236 | }
237 | };
238 | xhr.send();
239 | }
240 | };
241 |
242 | redirect();
243 | getFile('learn.json', Learn);
244 | })();
245 |
--------------------------------------------------------------------------------