├── .editorconfig
├── .gitattributes
├── .gitignore
├── .jshintrc
├── .travis.yml
├── LICENSE.txt
├── README.md
├── bower.json
├── config
├── jshintReporter.js
└── webpack.js
├── docs
└── FAQ.md
├── gulpfile.js
├── package.json
└── src
├── app.js
├── assets
├── apple-touch-icon-precomposed.png
├── browserconfig.xml
├── crossdomain.xml
├── favicon.ico
├── humans.txt
├── robots.txt
├── tile-wide.png
└── tile.png
├── components
├── demos
│ ├── accordion.js
│ └── modal.js
├── experimental
│ ├── occlusionScroller.js
│ └── todosX
│ │ ├── app.js
│ │ ├── footer.msx
│ │ ├── index.js
│ │ ├── item.js
│ │ └── model.js
├── mithril.bootstrap
│ └── index.js
└── todos
│ ├── __tests__
│ ├── app-tests.js
│ └── model-tests.js
│ ├── app.js
│ ├── footer.js
│ ├── header.js
│ ├── index.js
│ ├── list-of-tasks.js
│ ├── model.js
│ ├── new-task.js
│ ├── storage.js
│ └── task.js
├── pages
├── 404.html
└── index.html
└── styles
├── .csscomb.json
├── .csslintrc
├── app.less
├── bootstrap.less
├── jumbotron.less
├── mixins.less
├── navbar.less
├── utilities.less
└── variables.less
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 |
9 | # Change these settings to your own preference
10 | indent_style = space
11 | indent_size = 2
12 |
13 | # We recommend you to keep these unchanged
14 | end_of_line = lf
15 | charset = utf-8
16 | trim_trailing_whitespace = true
17 | insert_final_newline = true
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # These attributes affect how the contents stored in the repository are copied
2 | # to the working tree files when commands such as git checkout and git merge run.
3 | # http://git-scm.com/docs/gitattributes
4 |
5 | * text=auto
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Git uses this file to determine which files and directories to ignore
2 | # https://help.github.com/articles/ignoring-files
3 |
4 | build
5 | bower_components
6 | node_modules
7 | npm-debug.log
8 | .DS_Store
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "camelcase": true,
3 | "immed": true,
4 | "indent": 2,
5 | "latedef": true,
6 | "newcap": true,
7 | "quotmark": "single",
8 |
9 | "esnext": true,
10 | "globalstrict": true,
11 |
12 | "browser": true,
13 | "node": true,
14 |
15 | "globals": {
16 | "m": false,
17 | "require": false,
18 | "__dirname": false
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '0.10'
4 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2014 Phil Toms (phil.toms@hotmail.co.uk).
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Mithril.Elements Starter Kit
2 | [Mithril.Elements] is a thin wrapper around the [Mithril] JavaScript framework that allows you to create composable custom element types:
3 | ```javascript
4 | m('greet', 'Bob')
5 | ```
6 | becomes:
7 | ```html
8 |
9 | HiBob!
10 |
11 | ```
12 |
13 | Custom elements are first class Mithril citizens and compose naturally with existing DOM elements:
14 | ```javascript
15 | m('accordion', [
16 | m('.item', ['Title 1','item line one']),
17 | m('.item', ['Title 2','item line two']),
18 | m('.item', ['Title 3','item line three']),
19 | m('.item', ['Title 4','item line four'])
20 | ])
21 | ```
22 | [view in plunkr](http://embed.plnkr.co/FuSEJtlhvv4yqKN8Ohjd/preview)
23 |
24 | Application element types lend themselves to a feature oriented program structure:
25 | ```javascript
26 | m('#todoapp',[
27 | m('header',[
28 | m('new-task')
29 | ]),
30 | m('list-of-tasks', [
31 | m('$task')
32 | ]),
33 | m('footer')
34 | ])
35 | ```
36 | [view in plunkr](http://embed.plnkr.co/WIOF43ObW3NL2nW2XPkr/preview)
37 |
38 | Overloading existing DOM tags works too. A huge table might be tamed this way:
39 | ```javascript
40 | m('table', [
41 | m('thead', ['Name','Posts','Last Topic']),
42 | m('tbody',{state:{rows:12, content:hugeArray}}, function(content){return [
43 | m('td', content.name),
44 | m('td', content.posts),
45 | m('td', content.lastTopic)
46 | ]})
47 | ])
48 | ```
49 | compiles to:
50 | ```html
51 |
52 |
53 | Name |
54 | Posts |
55 | Last Topic |
56 |
57 |
58 |
59 | Bob |
60 | 47 |
61 | About occlusion scrolling |
62 |
63 |
64 |
65 |
66 | ```
67 | [view in plunkr](http://embed.plnkr.co/TQuzWpBzP4AMN874gOfF/preview)
68 |
69 |
70 | ## Getting Started
71 | Three ways to use Mithril.Elements:
72 |
73 | 1. download [this project](
74 | https://github.com/philtoms/mithril.elements/archive/master.zip
75 | ) and link to mithril and mithril.elements in the head of your app
76 |
77 | ```html
78 |
79 |
80 |
81 |
82 |
83 | ```
84 |
85 | 2. easier - npm install mithril.elements into your current Mithril project and require in your app
86 |
87 | ```shell
88 | npm install --save mithril.elements
89 | ```
90 |
91 | ```javascript
92 | // (Broswerify or WebPack)
93 | var m = require('mithril.elements');
94 | ```
95 |
96 | 3. easiest - [clone] or [fork] this repro and start hacking
97 |
98 | ```shell
99 | $ git clone -o upstream https://github.com/philtoms/mithril-starter-kit.git MyApp
100 | $ cd MyApp
101 | $ npm install -g gulp # Install Gulp task runner globally
102 | $ npm install # Install Node.js components listed in ./package.json
103 | bower install # only required for todomvc-common
104 | ```
105 | shell commands:
106 | ```
107 | gulp build --release # minify and build to release folder
108 | gulp serve # open browser on port 3000
109 | gulp jest # single pass test runner
110 | gulp tdd # watch + test runner
111 | npm test # run tests in CI (e.g. travis)
112 | npm run-script debug-test # run tests in node-inspector
113 | ```
114 |
115 | ## Using Mithril.Elements
116 | Mithril.Elements are extended Mithril [components], bound to an element tag name and registered with the application, so that they can be used in-line with default element types in Mithril views.
117 |
118 | An element registration:
119 | ```javascript
120 | m.element('accordion', {
121 | controller: function() {
122 | this.toggle = function(id){
123 | this.open=id;
124 | }
125 | },
126 | view: function(ctrl, content) {
127 | display = function(id) {
128 | return 'display:'+(ctrl.open===id? 'block':'none')
129 | }
130 | return m('.accordion', content.map(function(line,id){
131 | var title = line.children[0], content = line.children[1]
132 | return m(line,{
133 | onclick:ctrl.toggle.bind(ctrl,id)
134 | },[
135 | title,
136 | m('div',{style:display(id)},content)
137 | ])
138 | }))
139 | }
140 | })
141 | ```
142 | In the view:
143 | ```javascript
144 | m('accordion', [
145 | m('.item', ['Title 1','item line one']),
146 | m('.item', ['Title 2','item line two']),
147 | m('.item', ['Title 3','item line three']),
148 | m('.item', ['Title 4','item line four'])
149 | ])
150 | ```
151 |
152 | Sometimes you don't need to use the controller part of an element. In this situation you can leave it out of the definition and Mithril.Elements will provide a default controller:
153 | ```javascript
154 | m.element('jumbotron', {
155 | view: function(ctrl,inner) {
156 | return m('.jumbotron',[
157 | m('.container',[
158 | inner
159 | ])
160 | ])
161 | }
162 | })
163 | ```
164 |
165 | Note that the view can still receive the controller instance and can therefore access any state passed to the view through the controller.state property:
166 | ```javascript
167 | view: function(ctrl) {
168 | var count = ctrl.state.count;
169 | }
170 | ```
171 | ### Element state and the Mithril page life-cycle
172 | All Mithril components have program state - encapsulated in controller logic and typically maintained hierarchically through [m.module] registration. A custom elements program state on the other hand is tied to the life-cycle of its own parent view. This is the main difference between a custom element component and a standard Mithril component: Mithril.Element life-cycle is consistent with DOM element life-cycle.
173 |
174 | Element state come into existence lazily when the element in the view is first created and is maintained until the view is discarded (on a route change for example). In Mithril terms, element state is tied to the ongoing [redraw strategy] so that:
175 |
176 | - **all** - creates element state via a *new* controller instance - *always*
177 | - **diff** - uses the state of the *current* controller instance
178 | - *if it exists*
179 | - *otherwise as* **all**
180 |
181 | ### Element identity
182 | In most cases, this extended state management strategy is silently implemented by Mithril.Elements. In all of the examples presented so far, explicit reference to state management is not mentioned. However there are some programming scenarios where this strategy will fail.
183 |
184 | Mithril.Elements does not attempt to track element state through dynamically changing page layouts and relies instead on view generated identity using the following logical sequence:
185 |
186 | - use the virtual Element key attribute if it exists:
187 |
188 | ```javascript
189 | m('greet',{key:'bob1'}, 'Bob') // component identity is bob1
190 | ```
191 |
192 | - use the virtual Element id attribute if it exists:
193 |
194 | ```javascript
195 | m('greet#bob2', 'Bob') // component identity is bob2
196 | m('greet',{id:'bob2'}, 'Bob') // component identity is bob2
197 | ```
198 | - use the element state.id attribute if it exists:
199 |
200 | ```javascript
201 | m('greet',{state:{id='bob3'}}) // component identity is bob3
202 | ```
203 |
204 | - Default: generate a sequential id, keyed on page refresh. This option is not suitable
205 | for sortable lists or for pages that are composed logically:
206 |
207 | ```javascript
208 | m('greet', 'Bob') // component identity is greet1
209 | ```
210 |
211 | ### Creating element singletons
212 | Mithril.Elements are designed to be composed in-line with the current view life-cycle. Nevertheless, there are situations where it can be useful to create an instance outside of the view life-cycle and feed the instance into the view directly:
213 |
214 | ```javascript
215 | {
216 | controller: function(){
217 | var page1 = page.instance('one')
218 | var page2 = page.instance('two')
219 | },
220 | view: function(){
221 | return m('tabset', {},
222 | function(){ return [
223 | m('tab', ['Page 1', m(page1)]),
224 | m('tab', ['Page 2', m(page2)])
225 | ]}
226 | )
227 | }
228 | }
229 | ```
230 |
231 | Note that this pattern effectivey emulates the standard Mithril component pattern, with the semantic difference being that the singleton instance can be composed directly and interchangebly with other element types.
232 |
233 | Element singletons are also a useful pattern to use when you need to expose an extended API:
234 |
235 | ```javascript
236 | var launcherFactory = m.element('unicornLauncher', {
237 | controller: function(){
238 | // borrowed from https://docs.angularjs.org/guide/providers
239 | var useTinfoilShielding=false
240 | this.useTinfoilShielding = function(value) {
241 | useTinfoilShielding = !!value;
242 | }
243 | this.launch = function(useTinfoilShielding){...}
244 | },
245 | view: function(){
246 | return m('button', {onclick:ctrl.launch})
247 | }
248 | }
249 | // tally ho!!
250 | launcherFactory.instance().useTinfoilShielding(true);
251 | ```
252 |
253 | ### Escaping element tag names
254 | Element tag names can be escaped by preceeding them with the **$** sign to prevent them from being compiled into components. There are two situations where this can be useful:
255 |
256 | Using custom elements as templates in a parent-child relationship:
257 | ```javascript
258 | m('#todoapp',[
259 | m('header',[
260 | m('new-task')
261 | ]),
262 | m('list-of-tasks', [
263 | m('$task') // use task as a template
264 | ]),
265 | m('footer')
266 | ])
267 | ```
268 |
269 | Preventing recursion when overriding native elements:
270 | ```javascript
271 | view:function(ctrl){
272 | return m('$table',{style:{ // escape table to prevent recursion
273 | display:'block',
274 | overflow:'scroll',
275 | height:ctrl.height
276 | },
277 | config:ctrl.setup})
278 | }
279 | ```
280 |
281 | ## Composability
282 | Mithril.Elements supports two composability patterns: lexical and parent-child.
283 |
284 | Lexical composability (the standard mithril pattern) means that sibling elements are compiled in order of definition, and child elements are compiled before parents:
285 | ```javascript
286 | m('.main', [ // order of compilation -->
287 | m('sib-1'), // sib-1 : :
288 | m('sib-2',[ // : : sib-2
289 | m('child-1') // : child-1 :
290 | ])
291 | ])
292 | ```
293 | Normally this does not matter because the elements are [orthogonal] and they all end up being compiled before the DOM build phase. However, when creating higher order custom elements, compilation order becomes an issue for parent-child relationships.
294 |
295 | Parent-child composibility uses the factory pattern to invert the compilation order so that the child is compiled in the context of the parent:
296 | ```javascript
297 | m('table', [ // : : : : table
298 | m('tbody', function(content){return [ // tbody : : : :
299 | m('td', content.name), // : td : : :
300 | m('td', content.posts), // : : td : :
301 | m('td', content.lastTopic) // : : : td :
302 | ]}
303 | ])
304 | ```
305 | In the parent-child pattern, the parent component is responsible for compiling the child. Given this pattern, the parent has the opportunity to pass context into the child:
306 | ```javascript
307 | view: function(ctrl,child) {
308 | return ctrl.data.map(function(rowData){
309 | return child(rowData)
310 | }
311 | }
312 | ```
313 |
314 | ## Mithril API extensions
315 |
316 | ### m.element
317 | Use the m.element API to register mithril components as custom element types:
318 | ```javascript
319 | m.element('accordion', {
320 | controller: function() {
321 | this.toggle = function(id){
322 | this.open=id;
323 | }
324 | },
325 | view: function(ctrl, content) {
326 | display = function(id) {
327 | return 'display:'+(ctrl.open===id? 'block':'none')
328 | }
329 | return m('.accordion', content.map(function(line,id){
330 | return m(line,{
331 | onclick:ctrl.toggle.bind(ctrl,id)
332 | },[
333 | line.children[0],
334 | m('div',{style:display(id)},line.children[1])
335 | ])
336 | }))
337 | }
338 | })
339 | ```
340 |
341 | The Mithril component signature has been modified for semantic components in the following ways:
342 |
343 | - **Controller** - the controller accepts an optional state argument. The state can be any valid JavaScript type and will be passed on to the controller constructor function at the start of the current page life-cycle.
344 |
345 | - **View** - the view accepts an optional inner argument. The inner argument can be one of:
346 | - Functor - a function callback that is used to provide context to complex element compositions.
347 | - Template - a virtual DOM element that will provide the component element structure.
348 | - Content - an array of virtual Elements that form the children of the component.
349 |
350 | - **Instance** - an additional component method that can be used to programatically create a component instance. The method returns a new element Controller instance that can be used inline in view composition.
351 |
352 | Signature:
353 |
354 | ```clike
355 | Module element(string elementName, Module module)
356 |
357 | where:
358 | Module :: Object { Controller, View, Instance }
359 | Controller :: void controller([State state])
360 | { prototype: void unload(UnloadEvent e) }
361 | State :: Object | Array | Literal | undefined
362 | View :: void view(Object controllerInstance [, Inner inner])
363 | Inner :: Functor | VirtualElement | Array | undefined
364 | UnloadEvent :: Object {void preventDefault()}
365 | Instance :: Controller instance(State state)
366 |
367 | ```
368 |
369 | ### m
370 | A thin wrapper around the mithril [m()] signature function that lets Mithril.Elements intercept semantically registered element tags and integrate components bound to these tags with the current Mithril page life-cycle.
371 |
372 | The signature has been modified in the following ways:
373 |
374 | - **tag** - the tag argument can be any HTML5 tag name or a semantically registered component name, or a pre-compiled component instance:
375 |
376 | ```javascript
377 | m('greet', 'Bob') // hi Bob!
378 |
379 | var greeter = greet.instance('hola')
380 | m(greeter, 'Bob') // hola Bob!
381 | ```
382 |
383 | Mithril.Elements will use the tag name to look up registered components. If a component has been registered under a tag name, one of two behaviours will occur depending on the current redraw strategy:
384 |
385 | - **all** - creates element state via a *new* controller instance. Optional initial state will be passed on to the controller constructor function.
386 |
387 | - **diff** - uses the state of the *current* controller instance. Therefore does not pass state.
388 |
389 | In both cases, the component view will be called on every redraw, but only after the controller has been invoked.
390 |
391 | - **attrs** - The attrs argument accepts a special property named **state**. The value of the state property will be passed unchanged to the controller constructor:
392 |
393 | ```javascript
394 | m('tbody', {state:{rows:12, content:hugeArray}}, function(content){return [
395 | m('td', content.name),
396 | m('td', content.posts),
397 | m('td', content.lastTopic)
398 | ]})
399 | ```
400 |
401 | - **children** - the children argument can be overloaded with a functor that provides a context for composing complex elements.
402 |
403 | ```javascript
404 | m('shopping-cart-item',function(ctx){ return
405 | m('item', ctx.name),
406 | m('price', ctx.price),
407 | m('qnty', ctx.quantity])
408 | ]})
409 | ```
410 |
411 | Signature:
412 |
413 | ```clike
414 | VirtualElement m(String selector [, Attributes attributes] [, Children... children])
415 |
416 | where:
417 | VirtualElement :: Object { String tag, Attributes attributes, Children children }
418 | Attributes :: Object
419 | Children :: String text | VirtualElement virtualElement | SubtreeDirective directive | Functor | Array
420 | SubtreeDirective :: Object { String subtree }
421 | Functor :: Function definition
422 | ```
423 |
424 | ## and finally
425 | A special thanks to:
426 |
427 | - Konstantin Tarkus - This starter kit owes a lot to [React Starter Kit](https://github.com/kriasoft/react-starter-kit)
428 | - Sean Adkinson - [npm-debug / node-inspector integration](http://stackoverflow.com/a/26415442/2708419)
429 | - Barney Carroll - [Ideas and encouragement for this project](https://groups.google.com/forum/#!topic/mithriljs/kt3JburQb1o)
430 |
431 | ### Copyright
432 |
433 | Source code is licensed under the MIT License (MIT). See [LICENSE.txt](./LICENSE.txt)
434 | file in the project root. Documentation to the project is licensed under the
435 | [CC BY 4.0](http://creativecommons.org/licenses/by/4.0/) license.
436 |
437 |
438 | [Mithril]: http://lhorie.github.io/mithril/index.html
439 | [Mithril.Elements]:https://github.com/philtoms/mithril.elements
440 | [m()]:http://lhorie.github.io/mithril/mithril.html
441 | [components]: http://lhorie.github.io/mithril/components.html
442 | [redraw strategy]:http://lhorie.github.io/mithril/mithril.redraw.html#strategy
443 | [m.module]:http://lhorie.github.io/mithril/mithril.module.html
444 | [referential integrity]:http://lhorie.github.io/mithril/mithril.html#dealing-with-sorting-and-deleting-in-lists
445 | [semantic]: http://html5doctor.com/lets-talk-about-semantics/
446 | [orthogonal]:http://stackoverflow.com/questions/1527393/what-is-orthogonality
447 | [custom elements]: http://w3c.github.io/webcomponents/spec/custom/
448 | [idiomatically scripted]: http://lhorie.github.io/mithril/getting-started.html
449 | [clone]: github-windows://openRepo/https://github.com/philtoms/mithril-starter-kit
450 | [fork]: https://github.com/philtoms/mithril-starter-kit/fork
451 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-starter-kit",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "todomvc-common": "~0.1.4"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/config/jshintReporter.js:
--------------------------------------------------------------------------------
1 | /**
2 | * JSHint-Loader reporter function
3 | *
4 | * @param {Array} data Array of JSHint error objects.
5 | */
6 | var chalk = require('chalk');
7 |
8 | module.exports = function (errors) {
9 |
10 | var emitErrors = this.options.jshint.emitErrors;
11 |
12 | var hints = [];
13 | if(errors) errors.forEach(function(error) {
14 | if(!error) return;
15 | var message = chalk.gray(' line ') + chalk.blue(error.line) + chalk.gray(' char ') + chalk.blue(error.character) + ': ' + chalk.red(error.reason) + "\n " + chalk.gray(error.evidence);
16 | hints.push(message);
17 | }, this);
18 | var message = hints.join("\n\n");
19 | var emitter = emitErrors ? this.emitError : this.emitWarning;
20 | if(emitter)
21 | emitter("jshint results in errors\n" + message);
22 | else
23 | throw new Error("Your module system doesn't support emitWarning. Update availible? \n" + message);
24 | };
--------------------------------------------------------------------------------
/config/webpack.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Mithril.Elements Starter Kit | https://github.com/philtoms/mithril-starter-kit
3 | * Copyright (c) Phil Toms, LLC. All rights reserved. See LICENSE.txt
4 | */
5 |
6 | 'use strict';
7 |
8 | var webpack = require('webpack');
9 |
10 | /**
11 | * Get configuration for Webpack
12 | *
13 | * @see http://webpack.github.io/docs/configuration
14 | * https://github.com/petehunt/webpack-howto
15 | *
16 | * @param {boolean} release True if configuration is intended to be used in
17 | * a release mode, false otherwise
18 | * @return {object} Webpack configuration
19 | */
20 | module.exports = function(release,watch) {
21 | return {
22 | cache: !release,
23 | debug: !release,
24 | devtool: 'eval',
25 | watch:watch,
26 |
27 | output: {
28 | filename: "bundle.js"
29 | },
30 |
31 | stats: {
32 | colors: true,
33 | reasons: !release
34 | },
35 |
36 | plugins: release ? [
37 | new webpack.DefinePlugin({'process.env.NODE_ENV': '"production"'}),
38 | new webpack.optimize.DedupePlugin(),
39 | new webpack.optimize.UglifyJsPlugin(),
40 | new webpack.optimize.OccurenceOrderPlugin(),
41 | new webpack.optimize.AggressiveMergingPlugin()
42 | ] : [],
43 |
44 | resolve: {
45 | modulesDirectories: [
46 | 'node_modules',
47 | 'bower_components'
48 | ],
49 | // alias: {
50 | // "mithril": "../../node_modules/mithril/mithril.js",
51 | // "mithril.elements": "../node_modules/mithril.elements/mithril.elements.js"
52 | // },
53 | extensions: ['', '.webpack.js', '.web.js', '.js', '.msx']
54 | },
55 |
56 | module: {
57 | preLoaders: [
58 | {
59 | test: /\.js$/,
60 | exclude: /node_modules/,
61 | loader: 'jshint'
62 | }
63 | ],
64 |
65 | loaders: [
66 | {
67 | test: /\.msx$/,
68 | loader: 'sweetjs?modules[]=msx-reader/macros/msx-macro,readers[]=msx-reader'
69 | },
70 | {
71 | test: /\.css$/,
72 | loader: 'style!css'
73 | },
74 | {
75 | test: /\.less$/,
76 | loader: 'style!css!less'
77 | },
78 | {
79 | test: /\.gif/,
80 | loader: 'url-loader?limit=10000&mimetype=image/gif'
81 | },
82 | {
83 | test: /\.jpg/,
84 | loader: 'url-loader?limit=10000&mimetype=image/jpg'
85 | },
86 | {
87 | test: /\.png/,
88 | loader: 'url-loader?limit=10000&mimetype=image/png'
89 | }
90 | ]
91 | },
92 |
93 | // more options in the optional jshint object
94 | // see: http://jshint.com/docs/ for more details
95 | jshint: {
96 | // any jshint option http://www.jshint.com/docs/options/
97 | // i. e.
98 | camelcase: true,
99 |
100 | // any globals that should be suppressed
101 | globals: ['m'],
102 |
103 | // jshint errors are displayed by default as warnings
104 | // set emitErrors to true to display them as errors
105 | emitErrors: false,
106 |
107 | // jshint to not interrupt the compilation
108 | // if you want any file with jshint errors to fail
109 | // set failOnHint to true
110 | failOnHint: true,
111 |
112 | // custom reporter function
113 | reporter: require('./jshintReporter.js')
114 | }
115 | };
116 | };
117 |
118 |
--------------------------------------------------------------------------------
/docs/FAQ.md:
--------------------------------------------------------------------------------
1 | FAQ
2 | ===
3 |
4 | ...
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mithril.js Starter Kit
3 | * Copyright (c) 2014 Phil Toms (@philtoms)
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE.txt file in the root directory of this source tree.
7 | */
8 |
9 | 'use strict';
10 |
11 | // Include Gulp and other build automation tools and utilities
12 | // See: https://github.com/gulpjs/gulp/blob/master/docs/API.md
13 |
14 | var gulp = require('gulp');
15 | var $ = require('gulp-load-plugins')();
16 | var del = require('del');
17 | var path = require('path');
18 | var merge = require('merge-stream');
19 | var runSequence = require('run-sequence');
20 | var browserSync = require('browser-sync');
21 | var pagespeed = require('psi');
22 | var extend = require('extend');
23 | var fs = require('fs');
24 | var url = require('url');
25 | var argv = require('minimist')(process.argv.slice(2));
26 |
27 | // Settings
28 | var DEST = './build'; // The build output folder
29 | var RELEASE = !!argv.release; // Minimize and optimize during a build?
30 | var GOOGLE_ANALYTICS_ID = 'UA-XXXXX-X'; // https://www.google.com/analytics/web/
31 | var AUTOPREFIXER_BROWSERS = [ // https://github.com/ai/autoprefixer
32 | 'ie >= 10',
33 | 'ie_mob >= 10',
34 | 'ff >= 30',
35 | 'chrome >= 34',
36 | 'safari >= 7',
37 | 'opera >= 23',
38 | 'ios >= 7',
39 | 'android >= 4.4',
40 | 'bb >= 10'
41 | ];
42 |
43 | var src = {};
44 | var watch = false;
45 | var pkgs = (function() {
46 | var pkgs = {};
47 | var map = function(source) {
48 | for (var key in source) {
49 | pkgs[key.replace(/[^a-z0-9]/gi, '')] = source[key].substring(1);
50 | }
51 | };
52 | map(require('./package.json').dependencies);
53 | return pkgs;
54 | }());
55 |
56 | // The default task
57 | gulp.task('default', ['serve']);
58 |
59 | // Clean up
60 | gulp.task('clean', del.bind(null, [DEST]));
61 |
62 | // 3rd party libraries
63 | gulp.task('vendor', function() {
64 | src.vendor = [
65 | 'bower_components/todomvc-common/base.{js,css}',
66 | 'bower_components/todomvc-common/bg.png'
67 | ];
68 | return merge(
69 | gulp.src(src.vendor)
70 | .pipe(gulp.dest(DEST + '/vendor')),
71 | gulp.src('./node_modules/bootstrap/dist/fonts/**')
72 | .pipe(gulp.dest(DEST + '/fonts'))
73 | );
74 | });
75 |
76 | // Static files
77 | gulp.task('assets', function() {
78 | src.assets = [
79 | 'src/assets/**',
80 | 'src/pages/index.html'
81 | ];
82 | return gulp.src(src.assets)
83 | .pipe($.changed(DEST))
84 | .pipe(gulp.dest(DEST))
85 | .pipe($.size({title: 'assets'}));
86 | });
87 |
88 | // Images
89 | gulp.task('images', function() {
90 | src.images = 'src/images/**';
91 | return gulp.src(src.images)
92 | .pipe($.changed(DEST + '/images'))
93 | .pipe($.imagemin({
94 | progressive: true,
95 | interlaced: true
96 | }))
97 | .pipe(gulp.dest(DEST + '/images'))
98 | .pipe($.size({title: 'images'}));
99 | });
100 |
101 | // HTML pages
102 | gulp.task('pages', function() {
103 | src.pages = ['src/pages/**/*.js', 'src/pages/index.html', 'src/pages/404.html'];
104 |
105 | return gulp.src(src.pages)
106 | .pipe($.changed(DEST, {extension: '.html'}))
107 | .pipe($.replace('UA-XXXXX-X', GOOGLE_ANALYTICS_ID))
108 | .pipe($.if(!RELEASE, $.replace('.min.js', '.js')))
109 | .pipe($.if(RELEASE, $.htmlmin({
110 | removeComments: true,
111 | collapseWhitespace: true,
112 | minifyJS: true
113 | }), $.jsbeautifier()))
114 | .pipe(gulp.dest(DEST))
115 | .pipe($.size({title: 'pages'}));
116 | });
117 |
118 | // CSS style sheets
119 | gulp.task('styles', function() {
120 | src.styles = 'src/styles/**/*.{css,less}';
121 | return gulp.src('src/styles/app.less')
122 | .pipe($.plumber())
123 | .pipe($.less({
124 | sourceMap: !RELEASE,
125 | sourceMapBasepath: __dirname
126 | }))
127 | .on('error', console.error.bind(console))
128 | .pipe($.autoprefixer({browsers: AUTOPREFIXER_BROWSERS}))
129 | .pipe($.if(RELEASE, $.minifyCss()))
130 | .pipe(gulp.dest(DEST + '/css'))
131 | .pipe($.size({title: 'styles'}));
132 | });
133 |
134 | // Bundle
135 | gulp.task('bundle', function(cb) {
136 | var options = require('./config/webpack.js')(RELEASE,watch);
137 | gulp.src('./src/app.js')
138 | .pipe($.webpack(options))
139 | .pipe(gulp.dest('./build/'));
140 | cb(null);
141 | });
142 |
143 | // Build the app from source code
144 | gulp.task('build', ['clean'], function(cb) {
145 | runSequence(['vendor', 'assets', 'images', 'styles', 'bundle'], cb);
146 | });
147 |
148 | // Launch a lightweight HTTP Server
149 | gulp.task('serve', function(cb) {
150 |
151 | watch = true;
152 |
153 | runSequence('build', function() {
154 | browserSync({
155 | notify: false,
156 | // Customize the BrowserSync console logging prefix
157 | logPrefix: 'MSK',
158 | // Run as an https by uncommenting 'https: true'
159 | // Note: this uses an unsigned certificate which on first access
160 | // will present a certificate warning in the browser.
161 | // https: true,
162 | server: {
163 | baseDir: DEST,
164 | // Allow web page requests without .html file extension in URLs
165 | middleware: function(req, res, cb) {
166 | var uri = url.parse(req.url);
167 | if (uri.pathname.length > 1 &&
168 | path.extname(uri.pathname) === '' &&
169 | fs.existsSync(DEST + uri.pathname + '.html')) {
170 | req.url = uri.pathname + '.html' + (uri.search || '');
171 | }
172 | cb();
173 | }
174 | }
175 | });
176 |
177 | gulp.watch(src.vendor, ['vendor']);
178 | gulp.watch(src.assets, ['assets']);
179 | gulp.watch(src.images, ['images']);
180 | gulp.watch(src.pages, ['pages']);
181 | gulp.watch(src.styles, ['styles']);
182 | gulp.watch(DEST + '/**/*.*', function(file) {
183 | browserSync.reload(path.relative(__dirname, file.path));
184 | });
185 | cb();
186 | });
187 | });
188 |
189 | // run jest tests
190 | // gulp.task('jest', function () {
191 | // return gulp.src('src/**/__tests__').pipe($.jest({
192 | // unmockedModulePathPatterns: [
193 | // ],
194 | // testDirectoryName: "src",
195 | // testPathIgnorePatterns: [
196 | // "node_modules",
197 | // "spec/support"
198 | // ],
199 | // moduleFileExtensions: [
200 | // "js",
201 | // "json",
202 | // "msx"
203 | // ]
204 | // }));
205 | // });
206 |
207 | var jest = require('jest-cli');
208 | var chalk = require('chalk');
209 | gulp.task('jest', function (callback) {
210 | var onComplete = function (result) {
211 | // if (result) {
212 | // } else {
213 | // console.log(chalk.bgYellow('!!! Jest tests failed! You should fix them soon. !!!'));
214 | // }
215 | callback();
216 | }
217 | jest.runCLI({}, __dirname, onComplete);
218 | });
219 |
220 | gulp.task('tdd', function () {
221 | gulp.watch('src/**/*.js', ['jest']);
222 | });
223 |
224 | gulp.task('bdd', function () {
225 | gulp.watch('src/**/*.js', ['jest']);
226 | });
227 |
228 | // Deploy to GitHub Pages
229 | gulp.task('deploy', function() {
230 |
231 | // Remove temp folder
232 | if (argv.clean) {
233 | var os = require('os');
234 | var path = require('path');
235 | var repoPath = path.join(os.tmpdir(), 'tmpRepo');
236 | $.util.log('Delete ' + $.util.colors.magenta(repoPath));
237 | del.sync(repoPath, {force: true});
238 | }
239 |
240 | return gulp.src(DEST + '/**/*')
241 | .pipe($.if('**/robots.txt', !argv.production ? $.replace('Disallow:', 'Disallow: /') : $.util.noop()))
242 | .pipe($.ghPages({
243 | remoteUrl: 'https://github.com/{name}/{name}.github.io.git',
244 | branch: 'master'
245 | }));
246 | });
247 |
248 | // Run PageSpeed Insights
249 | // Update `url` below to the public URL for your site
250 | gulp.task('pagespeed', pagespeed.bind(null, {
251 | // By default, we use the PageSpeed Insights
252 | // free (no API key) tier. You can use a Google
253 | // Developer API key if you have one. See
254 | // http://goo.gl/RkN0vE for info key: 'YOUR_API_KEY'
255 | url: 'https://example.com',
256 | strategy: 'mobile'
257 | }));
258 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mithril-starter-kit",
3 | "private": true,
4 | "version": "0.1.0",
5 | "description": "Mithril Starter Kit",
6 | "repository": "https://github.com/philtoms/mithril-starter-kit",
7 | "license": "MIT",
8 | "dependencies": {
9 | "bootstrap": "^3.3.1",
10 | "mithril": "^0.1.28",
11 | "mithril.elements": "^0.1.1",
12 | "object-assign": "^1.0.0"
13 | },
14 | "devDependencies": {
15 | "browser-sync": "^1.6.5",
16 | "del": "^0.1.3",
17 | "gulp": "^3.8.10",
18 | "gulp-autoprefixer": "^1.0.1",
19 | "gulp-cache": "^0.2.4",
20 | "gulp-changed": "^1.0.0",
21 | "gulp-csscomb": "^3.0.3",
22 | "gulp-gh-pages": "^0.4.0",
23 | "gulp-htmlmin": "^0.2.0",
24 | "gulp-if": "^1.2.5",
25 | "gulp-imagemin": "^1.2.1",
26 | "gulp-jsbeautifier": "^0.0.3",
27 | "gulp-jshint": "^1.9.0",
28 | "gulp-less": "^1.3.6",
29 | "gulp-load-plugins": "^0.7.1",
30 | "gulp-minify-css": "^0.3.11",
31 | "gulp-plumber": "^0.6.6",
32 | "gulp-render": "^0.2.0",
33 | "gulp-replace": "^0.5.0",
34 | "gulp-size": "^1.1.0",
35 | "gulp-uglify": "^1.0.1",
36 | "gulp-util": "^3.0.1",
37 | "jest-cli": "~0.2.0",
38 | "jshint": "^2.5.10",
39 | "jshint-loader": "^0.8.0",
40 | "jshint-stylish": "^1.0.0",
41 | "jsx-loader": "^0.12.1",
42 | "merge-stream": "^0.1.6",
43 | "minimist": "^1.1.0",
44 | "protractor": "^1.4.0",
45 | "psi": "^0.1.5",
46 | "run-sequence": "^1.0.1",
47 | "url-loader": "^0.5.5",
48 | "webpack": "^1.4.13",
49 | "webpack-dev-server": "^1.6.5",
50 | "sweetjs-loader": "~0.1.0",
51 | "sweet.js": "~0.7.2",
52 | "msx-reader": "git+https://github.com/sdemjanenko/msx-reader.git",
53 | "gulp-webpack": "~1.1.0",
54 | "colors": "~1.0.3",
55 | "chalk": "~0.5.1",
56 | "extend": "~2.0.0",
57 | "css-loader": "~0.9.0",
58 | "less": "~1.7.5",
59 | "less-loader": "~0.7.8",
60 | "style-loader": "~0.8.2",
61 | "gulp-jest": "~0.2.2"
62 | },
63 | "jest": {
64 | "rootDir": "src",
65 | "unmockedModulePathPatterns": []
66 | },
67 | "scripts": {
68 | "start": "gulp",
69 | "test": "jest",
70 | "test-debug": "node-debug --nodejs --harmony ./node_modules/jest-cli/bin/jest.js"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mithril.Elements Starter Kit
3 | * Copyright (c) 2014 Phil Toms (@PhilToms3).
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE.txt file in the root directory of this source tree.
7 | */
8 |
9 | 'use strict';
10 |
11 | // global mithril.elements (alternatively, local require in each module)
12 | window.m = require('mithril.elements');
13 |
14 | // experimental - will probably be npm'd in next version
15 | require('./components/mithril.bootstrap');
16 |
17 | // tab routes
18 | var ACCORDION1 = 0;
19 | var ACCORDION2 = 1;
20 | var MODAL = 2;
21 | var TODOS = 3;
22 | var XP = 4;
23 |
24 | var app = function(tabNumber){
25 | return {
26 | controller: function() {
27 | // initialize the pages as singletons
28 | this.todos = require('./components/todos').instance();
29 | this.accordion1 = require('./components/demos/accordion').instance();
30 | this.accordion2 = require('./components/demos/accordion').instance({toggle:true});
31 | this.modal = require('./components/demos/modal').instance();
32 | this.experimental= require('./components/experimental/todosX').instance();
33 | },
34 |
35 | view: function(ctrl) {
36 | return [
37 | m('jumbotron',[
38 | m('h1','Mithril Starter Kit'),
39 | m('h3','with Mithril.Elements v0.1.1')
40 | ]),
41 | m('h2.text-center', 'Click on any of the tab pills below'),
42 | m('h4.text-center','to reveal some custom elements in action'),
43 | m('tabset', {state:{active:tabNumber, style:'pills'}},
44 | // provide routng to the tabs to engage route history
45 | function(){ return [
46 | m('tab', {state:{href:'/accordion-1'}}, ['Accordion 1', m(ctrl.accordion1)]),
47 | m('tab', {state:{href:'/accordion-2'}}, ['Accordion 2', m(ctrl.accordion2)]),
48 | m('tab', {state:{href:'/modal'}}, ['Modal dialog', m(ctrl.modal)]),
49 | m('tab', {state:{href:'/todos'}}, ['Todo List', m(ctrl.todos)]),
50 | m('tab', {state:{href:'/todos-xp'}}, ['Experimental', m(ctrl.experimental)])
51 | ];
52 | })
53 | ];
54 | }
55 | };
56 | };
57 |
58 | m.route(document.getElementById('app'), '/', {
59 | '/': app(),
60 | '/accordion-1': app(ACCORDION1),
61 | '/accordion-2': app(ACCORDION2),
62 | '/modal': app(MODAL),
63 | '/todos': app(TODOS),
64 | '/todos/:filter': app(TODOS),
65 | '/todos-xp': app(XP),
66 | '/todos-xp/:filter': app(XP)
67 | });
68 |
--------------------------------------------------------------------------------
/src/assets/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philtoms/mithril-starter-kit/1dfab67887e2bac74b3202afa1e475b4342cf9bf/src/assets/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/src/assets/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/crossdomain.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philtoms/mithril-starter-kit/1dfab67887e2bac74b3202afa1e475b4342cf9bf/src/assets/favicon.ico
--------------------------------------------------------------------------------
/src/assets/humans.txt:
--------------------------------------------------------------------------------
1 | # humanstxt.org/
2 | # The humans responsible & technology colophon
3 |
4 | # TEAM
5 |
6 | -- --
7 |
8 | # THANKS
9 |
10 |
11 |
12 | # TECHNOLOGY COLOPHON
13 |
14 | HTML5, CSS3, JavaScript
15 | Mithril, Bootstrap
16 |
--------------------------------------------------------------------------------
/src/assets/robots.txt:
--------------------------------------------------------------------------------
1 | # www.robotstxt.org/
2 |
3 | # Allow crawling of all content
4 | User-agent: *
5 | Disallow:
6 |
--------------------------------------------------------------------------------
/src/assets/tile-wide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philtoms/mithril-starter-kit/1dfab67887e2bac74b3202afa1e475b4342cf9bf/src/assets/tile-wide.png
--------------------------------------------------------------------------------
/src/assets/tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/philtoms/mithril-starter-kit/1dfab67887e2bac74b3202afa1e475b4342cf9bf/src/assets/tile.png
--------------------------------------------------------------------------------
/src/components/demos/accordion.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = m.element('accordion-demo', {
4 | controller: function(options) {
5 | this.options=options||{};
6 | },
7 | view: function(ctrl, content) {
8 | return [
9 | m('h3', ctrl.options.toggle? 'Accordion with toggle state':'Single item accordion'),
10 | m('accordion', {state:ctrl.options}, [
11 | m('.item', ['Title 1','item line one']),
12 | m('.item', ['Title 2','item line two']),
13 | m('.item', ['Title 3','item line three']),
14 | m('.item', ['Title 4','item line four'])
15 | ])
16 | ];
17 | }
18 | });
--------------------------------------------------------------------------------
/src/components/demos/modal.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = m.element('modal-demo', {
4 | controller: function() {
5 | // provide a boolean trigger for the dialog to
6 | // read open / closed state
7 | this.trigger = m.prop(false);
8 |
9 | this.save = function(){
10 | setTimeout(function(){window.alert('saved');},100);
11 | };
12 | },
13 | view: function(ctrl, content) {
14 | return [
15 | m('button.btn.btn-primary.btn-lg[type="button"]', {onclick:ctrl.trigger.bind(ctrl,true)}, 'Launch demo modal'),
16 | m('modal', {state:{trigger:ctrl.trigger}}, function(){ return {
17 | title:'A Modal Title',
18 | body: ['Another fine example...',
19 | m('p',' of a work in progress')
20 | ],
21 | cancel: 'Cancel',
22 | ok: m('.save', {onclick:ctrl.save.bind(ctrl)}, 'Save Changes')
23 | };
24 | })];
25 | }
26 | });
--------------------------------------------------------------------------------
/src/components/experimental/occlusionScroller.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = m.element('occlusionScroller',{
4 |
5 | // Component controllers are instanced with optional data.
6 | // This data is part of the interface definition that users
7 | // of a component are expected to comply with. In this case
8 | // the data must contain:
9 | // items: function returning a set of scrollable items
10 | // page: the number of line to display on a page
11 | controller: function(model) {
12 |
13 | this.items = model.items;
14 | this.page = model.page;
15 | this.itemHeight=58;
16 | var scroller={scrollTop:0};
17 |
18 | this.setup = function(element,done){
19 | if (!done){
20 | scroller = element;
21 | element.addEventListener('scroll', function(e) {
22 | m.redraw(); //notify view
23 | });
24 | }
25 | };
26 |
27 | this.pageY = function(){
28 | return scroller.scrollTop;
29 | };
30 |
31 | },
32 |
33 | // Component views accept a controller and an optional inner argument.
34 | //
35 | //
36 | view: function(ctrl, template) {
37 |
38 | // fetch the item list into local scope. Typically this
39 | // controller method will be bound to the component controller
40 | // throuth the model interface and will return a reference to an
41 | // external list.
42 | var items = typeof ctrl.items === 'function'? ctrl.items():ctrl.items;
43 |
44 | // calculate the begin and end indicies of the scrollable section
45 | var begin = ctrl.pageY() / ctrl.itemHeight | 0;
46 |
47 | // Add 2 so that the top and bottom of the page are filled with
48 | // next/prev item, not just whitespace if item not in full view
49 | var end = begin + ctrl.page + 2;
50 |
51 | var offset = ctrl.pageY % ctrl.itemHeight;
52 | var height = Math.min(items.length,ctrl.page) * ctrl.itemHeight + 'px';
53 |
54 | // add our own identity and style to the element. Note that any values
55 | // created here may be overridden by the component instance
56 | return m('.occlusionScroller', {style:{overflow:'scroll', height: height},config:ctrl.setup}, [
57 |
58 | m('.list', {style: {height: items.length * ctrl.itemHeight + 'px', position: 'relative', top: -offset + 'px'}}, [
59 | m('ul', {style: {paddingTop: ctrl.pageY() + 'px'}}, [
60 |
61 | // merge the page content into the flow with a standard map
62 | items.slice(begin, end).map(function(item, idx) {
63 |
64 | // register the child template. Notice that we
65 | // are passing it as an object and not as a string tagname.
66 | // Mithril.Element can distinguish between compiled components
67 | // and precompiled cells
68 | //
69 | return m(template, {id:idx+begin,state:item});
70 | })
71 |
72 | ])
73 | ])
74 | ]);
75 | }
76 | });
77 |
--------------------------------------------------------------------------------
/src/components/experimental/todosX/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var ENTER_KEY = 13;
4 | var ESC_KEY = 27;
5 |
6 | module.exports = {
7 | todoCount: 0,
8 | watchInput: function watchInput(ontype, onenter, onescape) {
9 | return function(e) {
10 | ontype(e);
11 | if (e.keyCode === ENTER_KEY) onenter();
12 | if (e.keyCode === ESC_KEY) onescape();
13 | };
14 | }
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/experimental/todosX/footer.msx:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var app = require('./app');
4 |
5 | module.exports = function(ctrl) {
6 |
7 | var completed = ctrl.amountCompleted();
8 |
9 | return <$footer id="footer">
10 | {app.todoCount} item{app.todoCount > 1 ? 's ' : ' '}left
11 |
31 | {completed == 0 ? '' : }
34 | $footer>
35 | }
--------------------------------------------------------------------------------
/src/components/experimental/todosX/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // todo modules
4 | var app = require('./app');
5 | var model = require('./model');
6 | var item = require('./item');
7 | var footer = require('./footer');
8 |
9 | // mithril.elements
10 | require('../occlusionScroller');
11 |
12 | module.exports = m.element('todosX-demo',{
13 |
14 | controller: function() {
15 |
16 | this.title = m.prop(''); // Temp title placeholder
17 | this.filter = m.prop(m.route.param('filter') || ''); // TodoList filter
18 | this.completeAll = model.completeAll;
19 | this.allCompleted = model.allCompleted;
20 | this.clearCompleted = model.clearCompleted;
21 |
22 | // Add a Todo
23 | this.add = function(title) {
24 | if(this.title()) {
25 | model.add(title());
26 | this.title('');
27 | }
28 | };
29 |
30 | //check whether a todo is visible
31 | this.isVisible = function(filter,todo) {
32 | if(filter === '')
33 | return true;
34 | if (filter === 'active')
35 | return !todo.completed();
36 | if (filter === 'completed')
37 | return todo.completed();
38 | }.bind(this,this.filter());
39 |
40 | this.clearTitle = function() {
41 | this.title('');
42 | };
43 |
44 | // Total amount of Todos completed
45 | this.amountCompleted = function() {
46 | var amount = 0;
47 | for(var i = 0, len=list.length; i < len; i++)
48 | if(list[i].completed())
49 | amount++;
50 |
51 | return amount;
52 | };
53 |
54 | // Todo collection - lazily filtered
55 | var list = [], filtered=[];
56 | app.todoCount = -1;
57 | this.list = function(){
58 | list = model.TodoList();
59 | if (app.todoCount !== list.length) {
60 | app.todoCount = list.length;
61 | filtered = list.filter(this.isVisible);
62 | }
63 | return filtered;
64 | }.bind(this);
65 | },
66 |
67 | view: function(ctrl) {
68 | return m('section#todoapp',[
69 | m('$header#header', [
70 | m('h1', 'too many todos'),
71 | m('input#new-todo[placeholder="What needs to be done?"]', {
72 | onkeypress: app.watchInput(
73 | m.withAttr('value', ctrl.title),
74 | ctrl.add.bind(ctrl, ctrl.title),
75 | ctrl.clearTitle.bind(ctrl)
76 | ),
77 | value: ctrl.title()
78 | })
79 | ]),
80 | m('section#main', [
81 | m('input#toggle-all[type=checkbox]',{
82 | onclick: ctrl.completeAll,
83 | checked: ctrl.allCompleted()
84 | }),
85 | m('occlusionScroller#todo-list', {state:{items:ctrl.list,page:6}},[
86 | m('$todosX-item')
87 | ])
88 | ]),
89 | app.todoCount === 0 ? '' : footer(ctrl)
90 | ]);
91 | }
92 | });
93 |
--------------------------------------------------------------------------------
/src/components/experimental/todosX/item.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var app = require('./app');
4 |
5 | module.exports = m.element('todosX-item',{
6 |
7 | controller: function(task) {
8 |
9 | var state = {
10 | editing: false,
11 | task: task,
12 | setClass: function() {
13 | var cls = '' + (task.completed() ? 'completed ' : '') + (state.editing ? 'editing': '');
14 | return cls? { class: cls}:'';
15 | },
16 |
17 | setEdit: function() {
18 | state.editing=task.title();
19 | },
20 |
21 | update: function() {
22 | state.editing=false;
23 | },
24 |
25 | remove: function() {
26 | task.remove();
27 | m.redraw.strategy('all');
28 | },
29 |
30 | reset: function() {
31 | task.title(state.editing);
32 | state.editing=false;
33 | }
34 | };
35 |
36 | return state;
37 | },
38 |
39 | view: function(ctrl) {
40 | var task = ctrl.task;
41 | return m('li', ctrl.setClass(), [
42 | m('.view', [
43 | m('input.toggle[type=checkbox]', {
44 | onclick: m.withAttr('checked', task.completed),
45 | checked: task.completed()
46 | }),
47 | m('label', {ondblclick:ctrl.setEdit}, task.title()),
48 | m('button.destroy', { onclick: ctrl.remove})
49 | ]),
50 | m('input.edit', {
51 | value:task.title(),
52 | onkeyup: app.watchInput(
53 | m.withAttr('value', task.title),
54 | ctrl.update,
55 | ctrl.reset
56 | ),
57 | onblur: ctrl.update,
58 | config: function (element) {
59 | if (ctrl.editing){
60 | element.focus();
61 | }
62 | }
63 | })
64 | ]);
65 | }
66 | });
67 |
--------------------------------------------------------------------------------
/src/components/experimental/todosX/model.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var list = [];
4 |
5 |
6 | // Todo Model
7 | function Todo(data){
8 | this.title = m.prop(data.title);
9 | this.completed = m.prop(false);
10 | this.remove = function(){
11 | var _item=this;
12 | list = list.filter(function(item){
13 | return (item!==_item);
14 | });
15 | };
16 | }
17 |
18 | var model = {
19 |
20 | // List of Todos
21 | TodoList: function () {
22 | return list;
23 | },
24 |
25 | // Add a Todo
26 | add: function(title) {
27 | list.push(new Todo({title: title }));
28 | },
29 |
30 | // Remove all Todos where Completed == true
31 | clearCompleted: function() {
32 | for(var i = 0; i < list.length; i++) {
33 | if(list[i].completed())
34 | list.splice(i, 1);
35 | }
36 | },
37 |
38 | completeAll: function () {
39 | var complete = model.allCompleted();
40 | for (var i = 0; i < list.length; i++) {
41 | list[i].completed(!complete);
42 | }
43 | },
44 |
45 | allCompleted: function () {
46 | for (var i = 0; i < list.length; i++) {
47 | if (!list[i].completed()) {
48 | return false;
49 | }
50 | }
51 | return true;
52 | }
53 |
54 | };
55 |
56 | for (var i = 0; i < 50000; i++){
57 | model.add('List item no ' + (i+1));
58 | }
59 |
60 | module.exports = model;
61 |
--------------------------------------------------------------------------------
/src/components/mithril.bootstrap/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var scrollbarWidth = (function () {
4 | var scrollbarWidth;
5 | return function(){
6 | if (scrollbarWidth===undefined){
7 | var scrollDiv = document.createElement('div');
8 | scrollDiv.className = 'modal-scrollbar-measure';
9 | document.body.appendChild(scrollDiv);
10 | scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
11 | document.body.removeChild(scrollDiv);
12 | }
13 | return scrollbarWidth;
14 | };
15 | })();
16 |
17 | var accordion = m.element('accordion', {
18 | controller: function(options) {
19 | options = options || {};
20 | var open=[];
21 | this.toggle = function(id){
22 | if (options.toggle){
23 | open[id]=!open[id];
24 | }
25 | else {
26 | open = id;
27 | }
28 | };
29 | this.isOpen = function(id){
30 | return id === open || options.toggle && open[id];
31 | };
32 | },
33 | view: function(ctrl, content) {
34 | return m('.accordian.panel.panel-default', content.map(function(line,id){
35 | var title=line.children[0],content=line.children[1];
36 | return [
37 | m(line,{
38 | class:'panel-heading',
39 | onclick:ctrl.toggle.bind(ctrl,id)
40 | },
41 | m('.panel-title',title)),
42 | m('div.panel-body',{style:'display:'+(ctrl.isOpen(id)? 'block':'none')},content)
43 | ];
44 | }));
45 | }
46 | });
47 |
48 |
49 | var jumbotron = m.element('jumbotron', {
50 |
51 | view: function(ctrl,inner) {
52 | return m('.jumbotron',[
53 | m('.container',[
54 | inner
55 | ])
56 | ]);
57 | }
58 | });
59 |
60 | var modal = m.element('modal', {
61 |
62 | controller: function(options) {
63 | var open, backdrop, saveBodyClass='';
64 | function close(e){
65 | open = false;
66 | options.trigger(false);
67 | document.body.className=saveBodyClass;
68 | if (e) m.redraw();
69 | }
70 | this.close = {onclick:function(){close();}};
71 | this.state = options.trigger;
72 | this.bind = function(element){
73 | if (!open && options.trigger()){
74 | open=element;
75 | saveBodyClass = document.body.className;
76 | document.body.className += ' modal-open';
77 | backdrop = element.getElementsByClassName('modal-backdrop')[0];
78 | backdrop.setAttribute('style', 'height:'+document.documentElement.clientHeight+'px');
79 | backdrop.addEventListener('click', close);
80 | }
81 | };
82 | },
83 |
84 | view: function(ctrl,inner) {
85 | inner = inner();
86 | var isOpen = ctrl.state();
87 | return m((isOpen? '.is-open':'.modal.fade'), {config:ctrl.bind}, [
88 | (isOpen? m('.modal-backdrop.fade.in'):''),
89 | m('.modal-dialog', [
90 | m('.modal-content', [
91 | m('.modal-header', [
92 | m('button.close[type="button" data-dismiss="modal" aria-label="Close"]',
93 | m('span[aria-hidden=true]', ctrl.close, m.trust('×'))),
94 | m('h4.modal-title', inner.title)
95 | ]),
96 | m('.modal-body', inner.body),
97 | m('.modal-footer', [
98 | m('button.btn.btn-default[type="button" data-dismiss="modal"]', ctrl.close, inner.cancel || 'Close'),
99 | inner.ok? m('button.btn.btn-primary[type="button"]', ctrl.close, inner.ok):''
100 | ])
101 | ])
102 | ])
103 | ]);
104 | }
105 | });
106 |
107 | // tabset based on bootstrap navs markup.
108 | // Options =
109 | // active: current (default) tab
110 | // style: 'tabs' | 'pills'
111 |
112 | var tabset = m.element('tabset', {
113 |
114 | controller: function(options){
115 |
116 | var currentTab = options.active;
117 | var count = 0;
118 | var tabs = this.tabs = [];
119 | var content = this.content = [];
120 |
121 | this.style=options.style || 'tabs';
122 |
123 | function Select(){
124 | currentTab = this.tabIdx;
125 | }
126 |
127 | function active(tabIdx) {
128 | return tabIdx===currentTab? 'active':'';
129 | }
130 |
131 | function display(tabIdx) {
132 | return {display: (tabIdx===currentTab? 'block':'none')};
133 | }
134 |
135 | m.element('tab', {
136 | controller: function(options){
137 | this.tabIdx=count++;
138 | this.href=function(){
139 | return options.href? {config: m.route,href:options.href}:{href:'#'};
140 | };
141 | },
142 |
143 | view: function(ctrl,inner) {
144 | var tabName=inner[0], tabContent=inner[1];
145 | tabs[ctrl.tabIdx] = m('li.tab', {onclick:Select.bind(ctrl),class:active(ctrl.tabIdx)}, m('a', ctrl.href(), tabName));
146 | content[ctrl.tabIdx] = m('.tabcontent', {style:display(ctrl.tabIdx)}, tabContent);
147 | }
148 | });
149 |
150 | },
151 |
152 | view: function(ctrl,tabs) {
153 | // tabs needs to be a factory in order to compile
154 | // directly into ctrl (ie parent / child) context
155 | tabs();
156 | return m('.tabset',[
157 | m('ul.nav.nav-'+ctrl.style, ctrl.tabs),
158 | m('div',ctrl.content)
159 | ]);
160 | }
161 |
162 | });
163 |
164 | module.exports = {
165 | accordion:accordion,
166 | jumbotron:jumbotron,
167 | modal:modal,
168 | tabset:tabset
169 | };
170 |
--------------------------------------------------------------------------------
/src/components/todos/__tests__/app-tests.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mithral Starter Kit
3 | * Copyright (c) 2014 Phil Toms (@PhilToms3).
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE.txt file in the root directory of this source tree.
7 | */
8 |
9 | /* global jest, describe, it, expect */
10 |
11 | 'use strict';
12 |
13 | jest.dontMock('../app');
14 |
15 | describe('app.watchInput', function() {
16 |
17 | var appstate = require('../app');
18 |
19 | var onType;
20 | var onEnter;
21 | var onEscape;
22 |
23 | var watchInput;
24 | var ENTER_KEY = 13;
25 | var ESC_KEY = 27;
26 |
27 | beforeEach(function(){
28 | onEnter = jasmine.createSpy('onEnter');
29 | onEscape = jasmine.createSpy('onEscape');
30 |
31 | watchInput = appstate.watchInput(onEnter,onEscape);
32 |
33 | });
34 |
35 | it('calls event handler on CR', function() {
36 | watchInput({keyCode:ENTER_KEY});
37 | expect(onEnter).toHaveBeenCalled();
38 | });
39 |
40 | it('calls escape handler on ESC', function() {
41 | watchInput({keyCode:ESC_KEY});
42 | expect(onEscape).toHaveBeenCalled();
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/src/components/todos/__tests__/model-tests.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Mithral Starter Kit
3 | * Copyright (c) 2014 Phil Toms (@PhilToms3).
4 | *
5 | * This source code is licensed under the MIT license found in the
6 | * LICENSE.txt file in the root directory of this source tree.
7 | */
8 |
9 | /* global jest, describe, it, expect */
10 |
11 | 'use strict';
12 |
13 | jest.dontMock('../model');
14 |
15 | describe('model.persistence', function() {
16 |
17 | var model = require('../model');
18 | var storage = require('../storage');
19 |
20 | beforeEach(function(){
21 | });
22 |
23 | it('saves to local storage', function() {
24 | var task = new model.Todo({title:'a task'})
25 | expect (storage.put).toBeCalled();
26 | });
27 |
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/todos/app.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | var ENTER_KEY = 13;
5 | var ESC_KEY = 27;
6 |
7 | var app = {
8 | watchInput: function (onenter, onescape) {
9 | return function(e) {
10 | if (e.keyCode === ENTER_KEY) onenter();
11 | if (e.keyCode === ESC_KEY) onescape();
12 | };
13 | },
14 | isVisible: function (todo) {
15 | switch (app.filter()) {
16 | case 'active':
17 | return !todo.completed();
18 | case 'completed':
19 | return todo.completed();
20 | default:
21 | return true;
22 | }
23 | }
24 | };
25 |
26 | module.exports = app;
27 |
--------------------------------------------------------------------------------
/src/components/todos/footer.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | var app = require('./app');
5 |
6 | module.exports = m.element('footer', {
7 |
8 | controller:function(){
9 |
10 | this.clearCompleted = app.todos.clearCompleted;
11 | this.amountCompleted = app.todos.amountCompleted;
12 |
13 | },
14 |
15 | view: function(ctrl){
16 | if (app.todos.list.length===0){
17 | return '';
18 | }
19 | var amountCompleted = ctrl.amountCompleted();
20 | var amountActive = app.todos.list.length - amountCompleted;
21 |
22 | return m('$footer#footer', [
23 | m('span#todo-count', [
24 | m('strong', amountActive), ' item' + (amountActive !== 1 ? 's' : '') + ' left'
25 | ]),
26 | m('ul#filters', [
27 | m('li', [
28 | m('a[href=/todos]', {
29 | config: m.route,
30 | class: app.filter() === '' ? 'selected' : ''
31 | }, 'All')
32 | ]),
33 | m('li', [
34 | m('a[href=/todos/active]', {
35 | config: m.route,
36 | class: app.filter() === 'active' ? 'selected' : ''
37 | }, 'Active')
38 | ]),
39 | m('li', [
40 | m('a[href=/todos/completed]', {
41 | config: m.route,
42 | class: app.filter() === 'completed' ? 'selected' : ''
43 | }, 'Completed')
44 | ])
45 | ]), amountCompleted === 0 ? '' : m('button#clear-completed', {
46 | onclick: ctrl.clearCompleted
47 | }, 'Clear completed (' + amountCompleted + ')')
48 | ]);
49 | }
50 | });
51 |
--------------------------------------------------------------------------------
/src/components/todos/header.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | module.exports = m.element('header',{
5 | view: function(ctrl,content){
6 | return m('$header#header', [
7 | m('h1', 'todos'),
8 | content
9 | ]);
10 | }
11 | });
12 |
--------------------------------------------------------------------------------
/src/components/todos/index.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 | /*global m */
4 |
5 | // todo modules
6 | var app = require('./app');
7 | var model = require('./model');
8 |
9 | require('./header');
10 | require('./new-task');
11 | require('./list-of-tasks');
12 | require('./task');
13 | require('./footer');
14 |
15 | module.exports = m.element('todos-demo', {
16 | controller: function(){
17 |
18 | // Todo collection
19 | app.todos = new model.Todos();
20 |
21 | // Todo list filter
22 | app.filter = m.prop(m.route.param('filter') || '');
23 |
24 | },
25 | view: function(){
26 | return m('#todoapp',[
27 | m('header',[
28 | m('new-task')
29 | ]),
30 | m('list-of-tasks', [
31 | m('$task')
32 | ]),
33 | m('footer')
34 | ]);
35 | }
36 | });
37 |
--------------------------------------------------------------------------------
/src/components/todos/list-of-tasks.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | var app = require('./app');
5 |
6 | module.exports = m.element('list-of-tasks',{
7 | controller:function(){
8 | this.completeAll = app.todos.completeAll;
9 | this.allCompleted = app.todos.allCompleted;
10 | },
11 | view: function(ctrl,template){
12 | return m('section#main', {
13 | style: {
14 | display: app.todos.list.length ? '' : 'none'
15 | }
16 | }, [
17 | m('input#toggle-all[type=checkbox]', {
18 | onclick: ctrl.completeAll,
19 | checked: ctrl.allCompleted()
20 | }),
21 | m('ul#todo-list', [
22 | app.todos.list.filter(app.isVisible).map(function (task) {
23 | return m(template,{id:task.id,state:task});
24 | })
25 | ])
26 | ]);
27 | }
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/todos/model.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | var storage = require('./storage');
5 |
6 | var list = [];
7 | var topId = 0;
8 |
9 | // prop utility
10 | function gettersetter(store,cb) {
11 | var prop = function() {
12 | if (arguments.length) {
13 | store = arguments[0];
14 | if (cb){
15 | cb.call(null,store);
16 | }
17 | }
18 | return store;
19 | };
20 |
21 | prop.toJSON = function() {
22 | return store;
23 | };
24 |
25 | if (cb){
26 | cb.call(null,store);
27 | }
28 | return prop;
29 | }
30 |
31 | function prop(store,cb) {
32 | return gettersetter(store,cb);
33 | }
34 |
35 | var model = {
36 | Todo: function (data) {
37 |
38 | var that = this;
39 |
40 | this.id = data.id || ++topId;
41 | this.title = prop(data.title);
42 |
43 | this.completed = prop(data.completed || false,function(){
44 | storage.put(list);
45 | });
46 |
47 | this.editing = prop(data.editing || false, function(){
48 | that.title(that.title().trim());
49 | if (!that.title()) {
50 | that.remove();
51 | }
52 | storage.put(list);
53 | });
54 |
55 | this.remove = function () {
56 | list.splice(list.indexOf(that), 1);
57 | storage.put(list);
58 | };
59 |
60 | },
61 |
62 | Todos: function(){
63 |
64 | list = storage.get();
65 |
66 | // Update with props
67 | list = list.map(function(item) {
68 | topId = Math.max(item.id,topId);
69 | return new model.Todo(item);
70 | });
71 |
72 | this.add = function(title){
73 | list.push(new model.Todo({title: title}));
74 | storage.put(list);
75 | };
76 |
77 | this.completeAll = function () {
78 | var complete = allCompleted();
79 | for (var i = 0; i < list.length; i++) {
80 | list[i].completed(!complete);
81 | }
82 | storage.put(list);
83 | };
84 |
85 | var allCompleted = this.allCompleted = function () {
86 | for (var i = 0; i < list.length; i++) {
87 | if (!list[i].completed()) {
88 | return false;
89 | }
90 | }
91 | return true;
92 | };
93 |
94 | this.clearCompleted = function () {
95 | for (var i = list.length - 1; i >= 0; i--) {
96 | if (list[i].completed()) {
97 | list.splice(i, 1);
98 | }
99 | }
100 | storage.put(list);
101 | };
102 |
103 | this.amountCompleted = function () {
104 | var amount = 0;
105 | for (var i = 0; i < list.length; i++) {
106 | if (list[i].completed()) {
107 | amount++;
108 | }
109 | }
110 | return amount;
111 | };
112 |
113 | this.list = list;
114 |
115 | }
116 | };
117 |
118 | module.exports = model;
119 |
--------------------------------------------------------------------------------
/src/components/todos/new-task.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | var app = require('./app');
5 |
6 | module.exports = m.element('new-task',{
7 | controller:function(){
8 |
9 | // Temp title placeholder
10 | this.title = m.prop('');
11 |
12 | this.add = function () {
13 | var title = this.title().trim();
14 | if (title) {
15 | app.todos.add(title);
16 | }
17 | this.title('');
18 | };
19 |
20 | this.clearTitle = function () {
21 | this.title('');
22 | };
23 |
24 | this.editing=false;
25 | },
26 | view: function(ctrl){
27 | return m('input#new-todo[placeholder="What needs to be done?"]', {
28 | onkeyup: app.watchInput(ctrl.add.bind(ctrl),
29 | ctrl.clearTitle.bind(ctrl)),
30 | value: ctrl.title(),
31 | oninput: m.withAttr('value', ctrl.title),
32 | config:function(element){
33 | if (!ctrl.editing){
34 | ctrl.editing = true;
35 | element.focus();
36 | }
37 | }});
38 | }
39 | });
40 |
41 |
--------------------------------------------------------------------------------
/src/components/todos/storage.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | var STORAGE_ID = 'todos-mithril';
5 |
6 | module.exports = {
7 | get: function () {
8 | return JSON.parse(localStorage.getItem(STORAGE_ID) || '[]');
9 | },
10 | put: function (todos) {
11 | localStorage.setItem(STORAGE_ID, JSON.stringify(todos));
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/todos/task.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | var app = require('./app');
5 |
6 | module.exports = m.element('task',{
7 | controller:function(task){
8 |
9 | this.classes = function(){
10 | var classes = '';
11 | classes += task.completed() ? 'completed' : '';
12 | classes += task.editing() ? ' editing' : '';
13 | return classes;
14 | };
15 |
16 | var previousTitle;
17 | this.title = task.title;
18 | this.completed = task.completed.bind(task);
19 | this.remove = task.remove.bind(task);
20 | this.editing = task.editing.bind(task);
21 |
22 | this.edit = function () {
23 | previousTitle = task.title();
24 | task.editing(true);
25 | };
26 |
27 | this.complete = function() {
28 | var state = !task.completed();
29 | task.completed(state);
30 | };
31 |
32 | this.doneEditing = function () {
33 | task.editing(false);
34 | };
35 |
36 | this.cancelEditing = function () {
37 | task.title(previousTitle);
38 | task.editing(false);
39 | };
40 |
41 | },
42 | view: function(ctrl){
43 | return m('li', { class: ctrl.classes()}, [
44 | m('.view', [
45 | m('input.toggle[type=checkbox]', {
46 | onclick: m.withAttr('checked', ctrl.complete),
47 | checked: ctrl.completed()
48 | }),
49 | m('label', {
50 | ondblclick: ctrl.edit
51 | }, ctrl.title()),
52 | m('button.destroy', {
53 | onclick: ctrl.remove
54 | })
55 | ]),
56 | m('input.edit', {
57 | value: ctrl.title(),
58 | onkeyup: app.watchInput(ctrl.doneEditing, ctrl.cancelEditing),
59 | oninput: m.withAttr('value', ctrl.title),
60 | config: function (element) {
61 | if (ctrl.editing()) {
62 | element.focus();
63 | }
64 | },
65 | onblur: ctrl.doneEditing
66 | })
67 | ]);
68 | }
69 | });
70 |
71 |
--------------------------------------------------------------------------------
/src/pages/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Page Not Found
6 |
7 |
53 |
54 |
55 | Page Not Found
56 | Sorry, but the page you were trying to view does not exist.
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/pages/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Mithril.Elements • Starter Kit
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/styles/.csscomb.json:
--------------------------------------------------------------------------------
1 | {
2 | "always-semicolon": true,
3 | "block-indent": 2,
4 | "colon-space": [0, 1],
5 | "color-case": "lower",
6 | "color-shorthand": true,
7 | "combinator-space": true,
8 | "element-case": "lower",
9 | "eof-newline": true,
10 | "leading-zero": false,
11 | "remove-empty-rulesets": true,
12 | "rule-indent": 2,
13 | "stick-brace": " ",
14 | "strip-spaces": true,
15 | "unitless-zero": true,
16 | "vendor-prefix-align": true,
17 | "sort-order": [
18 | [
19 | "position",
20 | "top",
21 | "right",
22 | "bottom",
23 | "left",
24 | "z-index",
25 | "display",
26 | "float",
27 | "width",
28 | "min-width",
29 | "max-width",
30 | "height",
31 | "min-height",
32 | "max-height",
33 | "-webkit-box-sizing",
34 | "-moz-box-sizing",
35 | "box-sizing",
36 | "-webkit-appearance",
37 | "padding",
38 | "padding-top",
39 | "padding-right",
40 | "padding-bottom",
41 | "padding-left",
42 | "margin",
43 | "margin-top",
44 | "margin-right",
45 | "margin-bottom",
46 | "margin-left",
47 | "overflow",
48 | "overflow-x",
49 | "overflow-y",
50 | "-webkit-overflow-scrolling",
51 | "-ms-overflow-x",
52 | "-ms-overflow-y",
53 | "-ms-overflow-style",
54 | "clip",
55 | "clear",
56 | "font",
57 | "font-family",
58 | "font-size",
59 | "font-style",
60 | "font-weight",
61 | "font-variant",
62 | "font-size-adjust",
63 | "font-stretch",
64 | "font-effect",
65 | "font-emphasize",
66 | "font-emphasize-position",
67 | "font-emphasize-style",
68 | "font-smooth",
69 | "-webkit-hyphens",
70 | "-moz-hyphens",
71 | "hyphens",
72 | "line-height",
73 | "color",
74 | "text-align",
75 | "-webkit-text-align-last",
76 | "-moz-text-align-last",
77 | "-ms-text-align-last",
78 | "text-align-last",
79 | "text-emphasis",
80 | "text-emphasis-color",
81 | "text-emphasis-style",
82 | "text-emphasis-position",
83 | "text-decoration",
84 | "text-indent",
85 | "text-justify",
86 | "text-outline",
87 | "-ms-text-overflow",
88 | "text-overflow",
89 | "text-overflow-ellipsis",
90 | "text-overflow-mode",
91 | "text-shadow",
92 | "text-transform",
93 | "text-wrap",
94 | "-webkit-text-size-adjust",
95 | "-ms-text-size-adjust",
96 | "letter-spacing",
97 | "-ms-word-break",
98 | "word-break",
99 | "word-spacing",
100 | "-ms-word-wrap",
101 | "word-wrap",
102 | "-moz-tab-size",
103 | "-o-tab-size",
104 | "tab-size",
105 | "white-space",
106 | "vertical-align",
107 | "list-style",
108 | "list-style-position",
109 | "list-style-type",
110 | "list-style-image",
111 | "pointer-events",
112 | "cursor",
113 | "visibility",
114 | "zoom",
115 | "flex-direction",
116 | "flex-order",
117 | "flex-pack",
118 | "flex-align",
119 | "table-layout",
120 | "empty-cells",
121 | "caption-side",
122 | "border-spacing",
123 | "border-collapse",
124 | "content",
125 | "quotes",
126 | "counter-reset",
127 | "counter-increment",
128 | "resize",
129 | "-webkit-user-select",
130 | "-moz-user-select",
131 | "-ms-user-select",
132 | "-o-user-select",
133 | "user-select",
134 | "nav-index",
135 | "nav-up",
136 | "nav-right",
137 | "nav-down",
138 | "nav-left",
139 | "background",
140 | "background-color",
141 | "background-image",
142 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient",
143 | "filter:progid:DXImageTransform.Microsoft.gradient",
144 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
145 | "filter",
146 | "background-repeat",
147 | "background-attachment",
148 | "background-position",
149 | "background-position-x",
150 | "background-position-y",
151 | "-webkit-background-clip",
152 | "-moz-background-clip",
153 | "background-clip",
154 | "background-origin",
155 | "-webkit-background-size",
156 | "-moz-background-size",
157 | "-o-background-size",
158 | "background-size",
159 | "border",
160 | "border-color",
161 | "border-style",
162 | "border-width",
163 | "border-top",
164 | "border-top-color",
165 | "border-top-style",
166 | "border-top-width",
167 | "border-right",
168 | "border-right-color",
169 | "border-right-style",
170 | "border-right-width",
171 | "border-bottom",
172 | "border-bottom-color",
173 | "border-bottom-style",
174 | "border-bottom-width",
175 | "border-left",
176 | "border-left-color",
177 | "border-left-style",
178 | "border-left-width",
179 | "border-radius",
180 | "border-top-left-radius",
181 | "border-top-right-radius",
182 | "border-bottom-right-radius",
183 | "border-bottom-left-radius",
184 | "-webkit-border-image",
185 | "-moz-border-image",
186 | "-o-border-image",
187 | "border-image",
188 | "-webkit-border-image-source",
189 | "-moz-border-image-source",
190 | "-o-border-image-source",
191 | "border-image-source",
192 | "-webkit-border-image-slice",
193 | "-moz-border-image-slice",
194 | "-o-border-image-slice",
195 | "border-image-slice",
196 | "-webkit-border-image-width",
197 | "-moz-border-image-width",
198 | "-o-border-image-width",
199 | "border-image-width",
200 | "-webkit-border-image-outset",
201 | "-moz-border-image-outset",
202 | "-o-border-image-outset",
203 | "border-image-outset",
204 | "-webkit-border-image-repeat",
205 | "-moz-border-image-repeat",
206 | "-o-border-image-repeat",
207 | "border-image-repeat",
208 | "outline",
209 | "outline-width",
210 | "outline-style",
211 | "outline-color",
212 | "outline-offset",
213 | "-webkit-box-shadow",
214 | "-moz-box-shadow",
215 | "box-shadow",
216 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
217 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha",
218 | "opacity",
219 | "-ms-interpolation-mode",
220 | "-webkit-transition",
221 | "-moz-transition",
222 | "-ms-transition",
223 | "-o-transition",
224 | "transition",
225 | "-webkit-transition-delay",
226 | "-moz-transition-delay",
227 | "-ms-transition-delay",
228 | "-o-transition-delay",
229 | "transition-delay",
230 | "-webkit-transition-timing-function",
231 | "-moz-transition-timing-function",
232 | "-ms-transition-timing-function",
233 | "-o-transition-timing-function",
234 | "transition-timing-function",
235 | "-webkit-transition-duration",
236 | "-moz-transition-duration",
237 | "-ms-transition-duration",
238 | "-o-transition-duration",
239 | "transition-duration",
240 | "-webkit-transition-property",
241 | "-moz-transition-property",
242 | "-ms-transition-property",
243 | "-o-transition-property",
244 | "transition-property",
245 | "-webkit-transform",
246 | "-moz-transform",
247 | "-ms-transform",
248 | "-o-transform",
249 | "transform",
250 | "-webkit-transform-origin",
251 | "-moz-transform-origin",
252 | "-ms-transform-origin",
253 | "-o-transform-origin",
254 | "transform-origin",
255 | "-webkit-animation",
256 | "-moz-animation",
257 | "-ms-animation",
258 | "-o-animation",
259 | "animation",
260 | "-webkit-animation-name",
261 | "-moz-animation-name",
262 | "-ms-animation-name",
263 | "-o-animation-name",
264 | "animation-name",
265 | "-webkit-animation-duration",
266 | "-moz-animation-duration",
267 | "-ms-animation-duration",
268 | "-o-animation-duration",
269 | "animation-duration",
270 | "-webkit-animation-play-state",
271 | "-moz-animation-play-state",
272 | "-ms-animation-play-state",
273 | "-o-animation-play-state",
274 | "animation-play-state",
275 | "-webkit-animation-timing-function",
276 | "-moz-animation-timing-function",
277 | "-ms-animation-timing-function",
278 | "-o-animation-timing-function",
279 | "animation-timing-function",
280 | "-webkit-animation-delay",
281 | "-moz-animation-delay",
282 | "-ms-animation-delay",
283 | "-o-animation-delay",
284 | "animation-delay",
285 | "-webkit-animation-iteration-count",
286 | "-moz-animation-iteration-count",
287 | "-ms-animation-iteration-count",
288 | "-o-animation-iteration-count",
289 | "animation-iteration-count",
290 | "-webkit-animation-direction",
291 | "-moz-animation-direction",
292 | "-ms-animation-direction",
293 | "-o-animation-direction",
294 | "animation-direction"
295 | ]
296 | ]
297 | }
298 |
--------------------------------------------------------------------------------
/src/styles/.csslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "adjoining-classes": false,
3 | "box-sizing": false,
4 | "box-model": false,
5 | "compatible-vendor-prefixes": false,
6 | "floats": false,
7 | "font-sizes": false,
8 | "gradients": false,
9 | "important": false,
10 | "known-properties": false,
11 | "outline-none": false,
12 | "qualified-headings": false,
13 | "regex-selectors": false,
14 | "shorthand": false,
15 | "text-indent": false,
16 | "unique-headings": false,
17 | "universal-selector": false,
18 | "unqualified-attributes": false
19 | }
20 |
--------------------------------------------------------------------------------
/src/styles/app.less:
--------------------------------------------------------------------------------
1 | @import "bootstrap.less";
2 |
3 | .jumbotron .container {
4 | text-align:center;
5 | }
6 |
7 | /* todo-mvc overrides */
8 |
9 | body {
10 | width:768px !important;
11 | }
12 |
13 | .tabset {
14 | width:550px;
15 | margin: 0 auto;
16 | }
17 |
18 | #todoapp {
19 | label {
20 | font-weight:normal;
21 | margin-bottom:0;
22 | }
23 |
24 | #todo-list {
25 | margin: 0;
26 | padding: 0;
27 | list-style: none;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/styles/bootstrap.less:
--------------------------------------------------------------------------------
1 | // =============================================================================
2 | // Bootstrap CSS + Custom styles and overrides
3 | // =============================================================================
4 |
5 | // Core variables and mixins
6 | @import "variables.less";
7 | @import "mixins.less";
8 |
9 | // Reset and dependencies
10 | @import "../../node_modules/bootstrap/less/normalize.less";
11 | @import "../../node_modules/bootstrap/less/print.less";
12 | @import "../../node_modules/bootstrap/less/glyphicons.less";
13 |
14 | // Core CSS
15 | @import "../../node_modules/bootstrap/less/scaffolding.less";
16 | @import "../../node_modules/bootstrap/less/type.less";
17 | @import "../../node_modules/bootstrap/less/code.less";
18 | @import "../../node_modules/bootstrap/less/grid.less";
19 | @import "../../node_modules/bootstrap/less/tables.less";
20 | @import "../../node_modules/bootstrap/less/forms.less";
21 | @import "../../node_modules/bootstrap/less/buttons.less";
22 |
23 | // Components
24 | @import "../../node_modules/bootstrap/less/component-animations.less";
25 | @import "../../node_modules/bootstrap/less/dropdowns.less";
26 | @import "../../node_modules/bootstrap/less/button-groups.less";
27 | @import "../../node_modules/bootstrap/less/input-groups.less";
28 | @import "../../node_modules/bootstrap/less/navs.less";
29 | @import "navbar.less";
30 | @import "../../node_modules/bootstrap/less/breadcrumbs.less";
31 | @import "../../node_modules/bootstrap/less/pagination.less";
32 | @import "../../node_modules/bootstrap/less/pager.less";
33 | @import "../../node_modules/bootstrap/less/labels.less";
34 | @import "../../node_modules/bootstrap/less/badges.less";
35 | @import "jumbotron.less";
36 | @import "../../node_modules/bootstrap/less/thumbnails.less";
37 | @import "../../node_modules/bootstrap/less/alerts.less";
38 | @import "../../node_modules/bootstrap/less/progress-bars.less";
39 | @import "../../node_modules/bootstrap/less/media.less";
40 | @import "../../node_modules/bootstrap/less/list-group.less";
41 | @import "../../node_modules/bootstrap/less/panels.less";
42 | @import "../../node_modules/bootstrap/less/responsive-embed.less";
43 | @import "../../node_modules/bootstrap/less/wells.less";
44 | @import "../../node_modules/bootstrap/less/close.less";
45 |
46 | // Components w/ JavaScript
47 | @import "../../node_modules/bootstrap/less/modals.less";
48 | @import "../../node_modules/bootstrap/less/tooltip.less";
49 | @import "../../node_modules/bootstrap/less/popovers.less";
50 | @import "../../node_modules/bootstrap/less/carousel.less";
51 |
52 | // Utility classes
53 | @import "utilities.less";
54 | @import "../../node_modules/bootstrap/less/responsive-utilities.less";
55 |
--------------------------------------------------------------------------------
/src/styles/jumbotron.less:
--------------------------------------------------------------------------------
1 | // =============================================================================
2 | // Jumbotron
3 | // =============================================================================
4 |
5 | @import "../../node_modules/bootstrap/less/jumbotron.less";
6 |
7 | .jumbotron {
8 | p {
9 | text-transform: uppercase;
10 | letter-spacing: 1px;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/styles/mixins.less:
--------------------------------------------------------------------------------
1 | // =============================================================================
2 | // Mixins
3 | // =============================================================================
4 |
5 | @import "../../node_modules/bootstrap/less/mixins.less";
6 |
--------------------------------------------------------------------------------
/src/styles/navbar.less:
--------------------------------------------------------------------------------
1 | // =============================================================================
2 | // Navigation Bar
3 | // =============================================================================
4 |
5 | @import "../../node_modules/bootstrap/less/navbar.less";
6 |
7 | body {
8 | padding: @navbar-height 0;
9 | overflow-y: scroll;
10 | }
11 |
12 | .navbar-top {
13 | &:extend(.navbar);
14 | &:extend(.navbar-inverse);
15 | &:extend(.navbar-fixed-top);
16 | }
17 |
18 | .navbar-brand {
19 | padding-top: 8px;
20 | padding-bottom: 8px;
21 | color: #00d8ff !important;
22 | font-size: 24px;
23 |
24 | img {
25 | margin-right: 10px;
26 | display: inline;
27 | }
28 | }
29 |
30 | //
31 | // Navigation Footer
32 | // -----------------------------------------------------------------------------
33 |
34 | .navbar-footer {
35 | &:extend(.navbar);
36 | &:extend(.navbar-fixed-bottom);
37 |
38 | background-color: darken(@body-bg, 2%);
39 |
40 | .text-muted {
41 | margin-top: 1em;
42 |
43 | span + span:before {
44 | content: "\2022\00a0"; // Unicode space added since inline-block means non-collapsing white-space
45 | padding: 0 3px 0 7px;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/utilities.less:
--------------------------------------------------------------------------------
1 | // =============================================================================
2 | // Utility classes
3 | // =============================================================================
4 |
5 | @import "../../node_modules/bootstrap/less/utilities.less";
6 |
7 | //
8 | // Browse Happy prompt
9 | // -----------------------------------------------------------------------------
10 |
11 | .browsehappy {
12 | margin: 0.2em 0;
13 | background: #ccc;
14 | color: #000;
15 | padding: 0.2em 0;
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/variables.less:
--------------------------------------------------------------------------------
1 | // =============================================================================
2 | // Variables
3 | // =============================================================================
4 |
5 | @import "../../node_modules/bootstrap/less/variables.less";
6 |
7 | //
8 | // Scaffolding
9 | // -----------------------------------------------------------------------------
10 |
11 | // Background color for ``
12 | @body-bg: #f9f9f9;
13 | // Global text color on ``
14 | @text-color: #484848;
15 | // Global textual link color
16 | @link-color: #c05b4d;
17 |
18 | //
19 | // Jumbotron
20 | // -----------------------------------------------------------------------------
21 |
22 | @jumbotron-heading-color: #61DAFB;
23 | @jumbotron-color: #E9E9E9;
24 | @jumbotron-bg: #2d2d2d;
25 | @jumbotron-font-size: @font-size-base;
26 |
--------------------------------------------------------------------------------