├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── app ├── app.js ├── routes.js └── tabs.js ├── client ├── data │ └── quotes.json ├── index.html └── styles │ └── app.css ├── node_modules ├── mithril-isomorphic │ ├── isomithril.js │ ├── mock.js │ ├── package.json │ └── reviver.js └── mithril │ ├── mithril.js │ └── package.json ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | express 26 | q 27 | q-io 28 | node_modules/mithril-isomorphic/node_modules/* 29 | __.js -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Settings 3 | "passfail" : false, // Stop on first error. 4 | "maxerr" : 100, // Maximum error before stopping. 5 | 6 | 7 | // Predefined globals whom JSHint will ignore. 8 | "browser" : true, // Standard browser globals e.g. `window`, `document`. 9 | 10 | "node" : false, 11 | "rhino" : false, 12 | "couch" : false, 13 | 14 | // Development. 15 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 16 | "devel" : true, // Allow developments statements e.g. `console.log();`. 17 | 18 | 19 | // ECMAScript 5. 20 | "es5" : false, // Allow ECMAScript 5 syntax. 21 | "strict" : false, // Require `use strict` pragma in every file. 22 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 23 | 24 | 25 | // The Good Parts. 26 | "asi" : true, // Tolerate Automatic Semicolon Insertion (no semicolons). 27 | "laxbreak" : true, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 28 | "bitwise" : false, // Prohibit bitwise operators (&, |, ^, etc.). 29 | "boss" : true, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 30 | "curly" : false, // Require {} for every new block or scope. 31 | "eqeqeq" : false, // Require triple equals i.e. `===`. 32 | "eqnull" : true, // Tolerate use of `== null`. 33 | "evil" : false, // Tolerate use of `eval`. 34 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 35 | "forin" : false, // Prohibit `for in` loops without `hasOwnPrototype`. 36 | "immed" : false, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 37 | "latedef" : false, // Prohipit variable use before definition. 38 | "loopfunc" : true, // Allow functions to be defined within loops. 39 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 40 | "regexp" : true, // Prohibit `.` and `[^...]` in regular expressions. 41 | "regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`. 42 | "scripturl" : true, // Tolerate script-targeted URLs. 43 | "shadow" : true, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 44 | "supernew" : true, // Tolerate `new function () { ... };` and `new Object;`. 45 | "undef" : false, // Require all non-global variables be declared before they are used. 46 | "-W053": false, // Hide errors about using `new` with primitive type constructors 47 | 48 | 49 | // Personal styling preferences. 50 | "newcap" : false, // Require capitalization of all constructor functions e.g. `new F()`. 51 | "noempty" : false, // Prohibit use of empty blocks. 52 | "nonew" : false, // Prohibit use of constructors for side-effects. 53 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 54 | "onevar" : false, // Allow only one `var` statement per function. 55 | "plusplus" : false, // Prohibit use of `++` & `--`. 56 | "sub" : true, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 57 | "trailing" : true, // Prohibit trailing whitespaces. 58 | "white" : false, // Check against strict whitespace and indentation rules. 59 | "indent" : 4 // Specify indentation spacing 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Daniel Barreiro 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsolete!!! 2 | 3 | (sorry) 4 | 5 | mithril-isomorphic 6 | ================== 7 | 8 | An attempt at making Mithril work both on the client and the server. 9 | 10 | Installation 11 | ============ 12 | 13 | Clone this repository or download the zip file and expand. 14 | 15 | Run `npm install` in the project folder to install the dependencies. 16 | 17 | So far.. 18 | ======== 19 | 20 | This is a work in progress. Sorry if at any point it gets unstable. 21 | 22 | So far: 23 | 24 | If you do `node server.js` and then browse to `localhost:8000` as the console message suggests 25 | you will be able to get to a sample application. 26 | 27 | The text on the application is all in uppercase letters. This is a clue to its origin. 28 | The contents generated on the server has been turned into uppercase to tell it apart 29 | from that generated on the client. 30 | 31 | As you move across the tabs you will see the contents on the new tab panels show in lowercase letters 32 | while the labels on the tabs themselves remain in uppercase as they are not refreshed. 33 | 34 | If you reload any of those pages, reloading them from the server, an uppercased version will show. 35 | 36 | All active elements, whether in the client or server side sections, are active and should respond as expected. 37 | As a matter of fact, if you switched tabs it was because the action associated with the tabs 38 | was revived. As expected of a single page application such as this, no trips to the server are required 39 | unless you explicitly reload the page. 40 | 41 | `server.js` launches an express server which uses middleware to run the Mithril application in the server. 42 | For the default configuration, you just need to add this to the express server script: 43 | 44 | app.use(require('mithril-isomorphic')()); 45 | 46 | When loading the middleware, an object with several configuration options can be given. 47 | You can see the configuration options documented [here](https://github.com/Satyam/mithril-isomorphic/blob/master/node_modules/mithril-isomorphic/isomithril.js#L125) 48 | 49 | The [`routes.js` file](https://github.com/Satyam/mithril-isomorphic/blob/master/app/routes.js) 50 | is slightly modified from the standard arguments to `m.route()`: 51 | 52 | module.exports = function (m) { 53 | m.route('/', { 54 | '/': 'app', 55 | '/:tab': 'app' 56 | }); 57 | }; 58 | 59 | * It should be exported as any node module. 60 | * The route configuration is missing the first argument, the document root, as there is no document yet. 61 | * The module name is given as a string (`'app'` instead of `app`) as there is no instance of it yet. 62 | 63 | The application can be made of any number of individual files. 64 | A [module](https://github.com/Satyam/mithril-isomorphic/blob/master/app/app.js) would look like this: 65 | 66 | module.exports = function (m, IsoModules) { 67 | var app = { 68 | // The regular application goes here 69 | }; 70 | IsoModules.app = app; 71 | }; 72 | 73 | The module is defined as normal Mithril one within the exported function which will be called with the instance of Mithril 74 | and an object which will be used to collect all the modules in the application. 75 | A single module can be split into several files or several of them placed in the same file, as long as a 76 | reference to each module, with its `controller` and `view` properties are in the `IsoModules` collection of modules. 77 | The name of the module within the collection is the one used in the `routes` file above. 78 | 79 | The [home page](https://github.com/Satyam/mithril-isomorphic/blob/master/client/index.html) 80 | `index.html` by default, should contain a placeholder `{{body}}` somewhere within the body. 81 | It will be replaced by the static version of the page plus the scripts to make it active, including Mithril itself. 82 | 83 | Only the minimum of Mithril itself has been modified to be able to access internal variables and member 84 | functions inaccessible to external applications. The `m.render()` method is the only one changed. 85 | Other methods have bene monkey-patched. 86 | The version of Mithril tagged *next* was used for this package. 87 | 88 | I have used the [mock DOM](https://github.com/lhorie/mithril.js/blob/master/tests/mock.js) 89 | used for testing with very slight modifications, instead of attempting to use a more 90 | comprehensive emulator such as PhantomJS. 91 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | module.exports = function (m, IsoModules) { 3 | 4 | var app = { 5 | //models 6 | 7 | // I keep a static copy of the list in the app 8 | // so it doesn't get reloaded each time the route changes 9 | // and a new app instance gets initialized 10 | list: null, 11 | 12 | //controller 13 | controller: function () { 14 | 15 | // model initialization, if does not already exists 16 | if (!app.list) { 17 | app.list = m.request({ 18 | method: 'GET', 19 | url: 'data/quotes.json' 20 | }); 21 | } 22 | 23 | this.title = m.prop("hello"); 24 | 25 | // app properties 26 | this.color = m.prop(''); 27 | this.bigFonts = m.prop(false); 28 | 29 | // event listeners 30 | this.reset = function () { 31 | this.bigFonts(false); 32 | this.color(''); 33 | }.bind(this); 34 | 35 | this.randomizeColor = function () { 36 | this.color("rgb(0, " + (Math.random() * 125 | 0) + ", 0)"); 37 | }.bind(this); 38 | 39 | this.tabs = new IsoModules.mc.Tabs.controller({ 40 | list: { 41 | view: app.listView, 42 | ctrl: this, 43 | label: 'List' 44 | }, 45 | settings: { 46 | label: 'Settings', 47 | view: app.settingsView 48 | }, 49 | about: app.aboutView 50 | }); 51 | }, 52 | 53 | 54 | //view 55 | view: function (ctrl) { 56 | return m( 57 | "div", { 58 | class: "app " + (ctrl.bigFonts() ? "big" : ""), 59 | style: { 60 | backgroundColor: ctrl.color() 61 | } 62 | }, 63 | IsoModules.mc.Tabs.view(ctrl.tabs, { 64 | _parent: '.tabs', 65 | _activeAnchor: '.selected' 66 | }) 67 | 68 | ); 69 | }, 70 | listView: function (ctrl) { 71 | return m( 72 | "ul.itemlist", 73 | app.list().map(function (item) { 74 | // I read the properties in both lower and uppercase due to a peculiarity of the demo. 75 | // For this application on its own, the lowercase properties should suffice. 76 | return m("li", (item.quote || item.QUOTE) + ' - ' + (item.author || item.AUTHOR)); 77 | }) 78 | ); 79 | }, 80 | settingsView: function (ctrl) { 81 | return m(".settings", [ 82 | m("div", [ 83 | m("input[type=checkbox]", { 84 | checked: ctrl.bigFonts(), 85 | onclick: m.withAttr('checked', ctrl.bigFonts) 86 | }), 87 | "big fonts" 88 | ]), 89 | m("div", [ 90 | m("button", { 91 | onclick: ctrl.randomizeColor 92 | }, "random color") 93 | ]), 94 | m("div", [ 95 | m("button", { 96 | onclick: ctrl.reset 97 | }, "reset") 98 | ]) 99 | ]); 100 | }, 101 | aboutView: function () { 102 | return m( 103 | ".about", [ 104 | "This is a sample demo", 105 | m("hr"), 106 | m( 107 | "textarea", { 108 | rows: 10, 109 | cols: 80, 110 | onchange: function () { 111 | app.list(JSON.parse(this.value)); 112 | console.log(app.list()); 113 | } 114 | }, 115 | JSON.stringify(app.list) 116 | ), 117 | m('p', 'If you go to the [list] tab, you will see the changes at once.') 118 | ] 119 | ); 120 | }, 121 | leoView: function (ctrl) { 122 | return m( 123 | ".leo", [ 124 | m("h1", ctrl.title()), 125 | m("input", { 126 | oninput: m.withAttr("value", ctrl.title), 127 | value: ctrl.title() 128 | }) 129 | ] 130 | 131 | ); 132 | } 133 | }; 134 | 135 | /* 136 | Receives a reference to the controller and 137 | an object with the labels to be shown as the keys and 138 | the content as functions returning the result of an m() call. 139 | 140 | I'm using a potentially localizable string as the key to the tabs. 141 | That is not a good idea but for the example it serves 142 | */ 143 | var tabs = function (ctrl, options) { 144 | 145 | // read the tab key from the URL or use the first key as default 146 | var tabKey = m.route.param('tab') || Object.keys(options)[0]; 147 | 148 | var tab = function (name) { 149 | return m("li", [ 150 | m("a", { 151 | class: tabKey == name ? "selected" : "", 152 | href: '/' + name, 153 | // let Mithril take care of the routing 154 | config: m.route 155 | }, name) 156 | ]); 157 | }; 158 | 159 | return [ 160 | // tabs: 161 | m('.tabs', [ 162 | m("ul", Object.keys(options).map(tab)) 163 | ]), 164 | // body: 165 | options[tabKey](ctrl) 166 | ]; 167 | }; 168 | 169 | IsoModules.app = app; 170 | }; -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function (m) { 2 | m.route('/', { 3 | '/': 'app', 4 | '/:tab': 'app' 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /app/tabs.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | /*global m:false */ 3 | module.exports = function (m, IsoModules) { 4 | // Select ====================================================================== 5 | var mc = mc || {}; 6 | 7 | mc.Tabs = { 8 | controller: function (tabs, activeTab) { 9 | this.tabs = tabs || []; 10 | this.activeTab = activeTab || ''; 11 | }, 12 | 13 | view: function (ctrl, selectors, attrs, overrides) { 14 | selectors = selectors || {}; 15 | attrs = attrs || {}; 16 | overrides = overrides || {}; 17 | 18 | var tabs = normalizeTabs(overrides.tabs || ctrl.tabs, ctrl), 19 | activeTab = overrides.activeTab || ctrl.activeTab || 20 | m.route.param('tab') || Object.keys(tabs)[0]; 21 | 22 | return [ 23 | m('div' + (selectors._parent || ''), attrs._parent || {}, 24 | m('ul', Object.keys(tabs).map(tab)) 25 | ), 26 | tabs[activeTab].view(tabs[activeTab].ctrl) 27 | ]; 28 | 29 | function tab(name) { 30 | var selected = activeTab === name, 31 | selector = (selected && selectors._activeAnchor ? 32 | selectors._activeAnchor : selectors._anchor) || '', 33 | attrParam = (selected && attrs._activeAnchor ? 34 | attrs._activeAnchor : attrs._anchor) || '', 35 | attr = {}; 36 | 37 | if (attrParam) { 38 | merge(attr, attrParam); 39 | } 40 | merge(attr, { 41 | href: '/' + name, 42 | config: m.route 43 | }); 44 | 45 | return m('li' + (selectors._item || ''), attrs._item || {}, 46 | m('a' + selector, attr, tabs[name].label || '') 47 | ); 48 | } 49 | 50 | function normalizeTabs(tabs, ctrl) { 51 | var norm = {}, 52 | lastCtrl = ctrl || {}; 53 | 54 | Object.keys(tabs).forEach(function (key) { // depends on .keys() returning keys in order defined 55 | var tab = tabs[key]; 56 | if (typeof tab === 'function') { 57 | norm[key] = { 58 | view: tab, 59 | ctrl: lastCtrl, 60 | label: key 61 | }; 62 | } else { 63 | if (tab.ctrl) { 64 | lastCtrl = tab.ctrl; 65 | } 66 | norm[key] = { 67 | view: tab.view, 68 | ctrl: lastCtrl, 69 | label: tab.label || key 70 | }; 71 | } 72 | }); 73 | 74 | return norm; 75 | } 76 | 77 | 78 | function merge(to, from) { 79 | for (var key in from) { 80 | to[key] = from[key]; 81 | } // jshint ignore:line 82 | } 83 | } 84 | }; 85 | 86 | IsoModules.mc = mc; 87 | }; -------------------------------------------------------------------------------- /client/data/quotes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "author": "Albert Camus", 4 | "quote": "Those who lack the courage will always find a philosophy to justify it." 5 | }, 6 | { 7 | "author": "Plato", 8 | "quote": "You can discover more about a person in an hour of play than in a year of conversation." 9 | }, 10 | { 11 | "author": "Ludwig Wittgenstein", 12 | "quote": "If people never did silly things nothing intelligent would ever get done." 13 | } 14 | ] -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Isomorphic Mithril Project 7 | 8 | 9 | 10 | {{body}} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/styles/app.css: -------------------------------------------------------------------------------- 1 | body{background-color:#466a5c;color:#fff}a,a:visited,a:active{color:#fff;text-decoration:none;cursor:pointer}ul{margin:0;padding:0;list-style:none}.app{margin:1.5em}.app.big{font-size:2em}.sidebar,.itemlist{color:#000;min-height:300px}.tabs{background-color:#222;margin-bottom:1em;overflow:hidden}.tabs li{float:left;margin:0 .25em;padding-top:4px;border-bottom:4px solid #BEAA8E}.tabs a{padding:.5em;display:block}.tabs a.selected{background-color:#BEAA8E}.tabs a:hover{background-color:#BEAA8E}.itemlist{color:#fff}ul.itemlist li{padding:1em;margin-bottom:1em;background-color:#222} -------------------------------------------------------------------------------- /node_modules/mithril-isomorphic/isomithril.js: -------------------------------------------------------------------------------- 1 | /* jshint node:true */ 2 | 'use strict'; 3 | 4 | var path = require('path'), 5 | FS = require('q-io/fs'), 6 | Q = require('q'), 7 | Mithril = require('mithril'), 8 | mock = require('./mock.js'); 9 | 10 | var page, 11 | routesFname, 12 | app, 13 | routes, 14 | def, 15 | mod, 16 | localRoot, 17 | remoteRoot, 18 | script, 19 | placeholder, 20 | rxApp, 21 | routeParams = {}, 22 | isoName, 23 | IsoModules = {}, 24 | modes = { 25 | pathname: "", 26 | hash: "#", 27 | search: "?" 28 | }, 29 | routeMode; 30 | 31 | 32 | // Attach the mock DOM to Mithril 33 | Mithril.deps(mock.window); 34 | 35 | // Redefine the original `m.route.param` 36 | // to read from my collection of parameters 37 | // collected on the server side. 38 | Mithril.route.param = function (key) { 39 | console.log('param', key, routeParams[key]); 40 | return routeParams[key]; 41 | }; 42 | 43 | // Monkey patch the original `m.render` method 44 | // to read the HTML from the mock DOM once it is done rendering. 45 | 46 | Mithril.render = (function (original) { 47 | 48 | var logNode = function (node) { 49 | var prop, val, 50 | style, styles = [], 51 | html = ''; 52 | 53 | if (!node.nodeName && node.nodeValue !== undefined) { 54 | // For text nodes, I return the uppercase text 55 | // so that you can tell the parts generated at the server 56 | // from the normal lowercase of the actual app when run on the client 57 | return node.nodeValue.toUpperCase(); 58 | } 59 | html += '<' + node.nodeName; 60 | for (prop in node) { 61 | val = node[prop]; 62 | 63 | // Ignore functions, those will be revived on the client side. 64 | if (typeof val == 'function') continue; 65 | switch (prop) { 66 | case 'nodeName': 67 | case 'parentNode': 68 | case 'childNodes': 69 | case 'pathname': 70 | case 'search': 71 | continue; 72 | case 'checked': 73 | if (val == 'false') continue; 74 | break; 75 | case 'href': 76 | val = node.pathname; 77 | break; 78 | case 'className': 79 | prop = 'class'; 80 | break; 81 | case 'style': 82 | if (val) { 83 | for (style in val) { 84 | if (val[style]) { 85 | styles.push(style + ': ' + val[style]); 86 | } 87 | } 88 | if (!styles.length) continue; 89 | val = styles.join(';'); 90 | } 91 | break; 92 | } 93 | html += ' ' + prop + '="' + val.replace('"', '\\"') + '"'; 94 | } 95 | 96 | if (node.childNodes.length) { 97 | html += '>' + node.childNodes.reduce(function (prev, node) { 98 | return prev + logNode(node); 99 | }, '') + ''; 100 | } else { 101 | // I don't know why Mithril assigns the content of textareas 102 | // to its value attribute instead of the innerHTML property. 103 | // Since it doesn't have children, the closing tag has to be forced. 104 | if (node.nodeName == 'TEXTAREA') { 105 | html += '>'; 106 | } else { 107 | html += '/>'; 108 | } 109 | } 110 | return html; 111 | }; 112 | 113 | 114 | 115 | 116 | // This is the monkey-patching part: 117 | return function () { 118 | 119 | var result = original.apply(this, arguments); 120 | mock.html = mock.window.document.body.childNodes.reduce(function (prev, node) { 121 | return prev + logNode(node); 122 | }, ''); 123 | return result; 124 | }; 125 | })(Mithril.render); 126 | 127 | 128 | //var wAttrList = []; 129 | //Mithril.withAttr = (function (original) { 130 | // return function () { 131 | // console.log('withAttr', arguments[0], arguments[1].toString()); 132 | // var result = original.apply(this, arguments); 133 | // wAttrList.push({ 134 | // attr: arguments[0], 135 | // fn: arguments[1], 136 | // wrap: result 137 | // }); 138 | // return result; 139 | // }; 140 | // 141 | //})(Mithril.withAttr); 142 | 143 | 144 | module.exports = function (config) { 145 | 146 | config = config || {}; 147 | // Folders 148 | // Where the application resides 149 | app = config.app || './app'; 150 | 151 | // The root for the static files 152 | localRoot = config.localRoot || 'client'; 153 | // The root as seen from the far side 154 | remoteRoot = config.remoteRoot || '/'; 155 | 156 | // Files: 157 | // The home page, located within `localRoot` 158 | page = config.page || 'index.html'; 159 | // The placeholder for the body within that home page 160 | placeholder = config.placeholder || '{{body}}'; 161 | // The file name for the routes configuration, located in `app` 162 | routesFname = config.routes || 'routes.js'; 163 | // The name of the script file where all the application code is collected, located in `app` 164 | script = config.script || '__.js'; 165 | 166 | // The name of the global object where all the modules are collected 167 | isoName = config.isoName || 'IsoModules'; 168 | // The route mode as per m.routes.mode 169 | routeMode = config.routeMode || 'search'; 170 | 171 | 172 | 173 | mock.window.location.pathname = localRoot; 174 | rxApp = new RegExp(path.join('/', app, '/') + '(.*)'); 175 | 176 | 177 | // Read the routes file for the modified call to 178 | // `m.route` or `m.module` 179 | 180 | var r = require(path.resolve(app, routesFname))({ 181 | route: function (d, r) { 182 | def = d; 183 | routes = r; 184 | }, 185 | module: function (m) { 186 | mod = m; 187 | } 188 | }); 189 | 190 | 191 | // Collect everything that needs to be sent to the client into one script. 192 | var out = path.resolve(app, script); 193 | 194 | // First, send Mithril itself 195 | FS.copy(require.resolve('mithril'), out).then(function () { 196 | // Add the reviver 197 | return FS.read(require.resolve('./reviver.js')).then(function (contents) { 198 | return FS.append(out, contents); 199 | }); 200 | }).then(function () { 201 | // Add the declaration of the variable that will hold the collection of modules: 202 | return FS.append(out, ';\n' + isoName + ' = {};\n'); 203 | }).then(function () { 204 | // Add all the modules declared 205 | // skipping over the routes file and the file where the scripts are being collected 206 | return FS.listTree(app, function (fullPath) { 207 | return !( 208 | path.extname(fullPath) != '.js' || 209 | path.basename(fullPath) == script || 210 | path.basename(fullPath) == routesFname 211 | ); 212 | }).then(function (files) { 213 | return Q.all(files.map(function (file) { 214 | var module = require(path.resolve('./', file)); 215 | module(Mithril, IsoModules); 216 | return FS.append(out, '\n;(' + module.toString() + ')(Mithril, ' + isoName + ')'); 217 | })); 218 | }); 219 | }).then(function () { 220 | // Add either the `m.module` or `m.route` call at the end. 221 | if (mod) { 222 | return FS.append(out, '\n;Mithril.module(document.body,' + isoName + '.' + mod + ');'); 223 | } 224 | if (routes) { 225 | var rs = [], 226 | r; 227 | for (r in routes) { 228 | rs.push('"' + r + '":' + isoName + '.' + routes[r]); 229 | } 230 | return FS.append(out, '\n;Mithril.route(document.body, "' + def + '", {' + rs.join(',') + '});'); 231 | } 232 | }); 233 | 234 | 235 | //Entry point to the middleware 236 | return function (req, res, next) { 237 | 238 | console.log('-----------------------'); 239 | console.log('req.url:', req.url); 240 | 241 | // Skip over the requests I don't need to handle 242 | var match = rxApp.exec(req.url); 243 | if (match) { 244 | if (match[1] == script) { 245 | console.log('rxmatch == script', match[1]); 246 | res.sendfile(path.join(app, script)); 247 | return; 248 | } 249 | console.log('rxmatch else', match[1]); 250 | res.send('(' + require(path.resolve(app, match[1])).toString() + ')(Mithril)'); 251 | } 252 | 253 | var accept = req.headers.accept; 254 | if ( 255 | req.method !== 'GET' || 256 | accept.indexOf('application/json') === 0 || !(accept.indexOf('text/html') !== -1 || accept.indexOf('*/*') !== -1) 257 | ) { 258 | console.log('first ignore'); 259 | return void next(); 260 | } 261 | 262 | if (req.url.indexOf('.') !== -1) { 263 | console.log('second ignore'); 264 | return void next(); 265 | } 266 | 267 | // This is a copy of the function by the same name 268 | // in Mithril which is inaccessible from outside. 269 | var routeByValue = function (router, path) { 270 | path = path.replace(/^\/\?/, ''); 271 | routeParams = {}; 272 | var replacer = function () { 273 | var keys = route.match(/:[^\/]+/g); 274 | var values = [].slice.call(arguments, 1, -2); 275 | for (var i = 0; i < keys.length; i++) routeParams[keys[i].replace(/:|\./g, "")] = decodeURIComponent(values[i]); 276 | return router[route]; 277 | }; 278 | for (var route in router) { 279 | if (route == path) return router[route]; 280 | 281 | var matcher = new RegExp("^" + route.replace(/:[^\/]+?\.{3}/g, "(.*?)").replace(/:[^\/]+/g, "([^\\/]+)") + "$"); 282 | 283 | if (matcher.test(path)) { 284 | return path.replace(matcher, replacer); 285 | } 286 | } 287 | }; 288 | 289 | // If the routes file contained a routes declaration 290 | // find the module to be called and extract the params. 291 | if (routes) { 292 | mod = routeByValue(routes, req.url) || routes[def]; 293 | console.log('rbv', mod, routeParams); 294 | } 295 | // If the routes file had a single call to `m.module` 296 | // then it would already be stored in `mod` 297 | if (!mod) { 298 | res.send(500, 'Missing routes or single module declaration'); 299 | } 300 | 301 | // Call that module, with the body of the fake DOM as its root. 302 | Mithril.module(mock.window.document.body, IsoModules[mod]); 303 | 304 | 305 | // Read the template for the page normally the `index.html` file. 306 | FS.read(path.join(localRoot, page)).then(function (data) { 307 | // Locate the placeholder for the server-side content 308 | var i = data.indexOf(placeholder); 309 | // If the placeholder is not found, return the page as-is 310 | if (i == -1) return void res.send(data); 311 | // avoid 304 - Not Modified 312 | res.setHeader('Last-Modified', (new Date()).toUTCString()); 313 | 314 | 315 | // console.log('\n\nvar cellCache = ', JSON.stringify(Mithril.$cellCache[0], function (key, value) { 316 | // if (key == 'nodes') value = value.length + ' [' + value.map(function (node) { return node.nodeName || node.nodeValue;}) + ']'; 317 | // return value; 318 | // })); 319 | // var inController = function (value) { 320 | // var m, mod = Mithril.$controller; 321 | // for (m in mod) { 322 | // if (mod[m] == value) { 323 | // return m; 324 | // } 325 | // } 326 | // }; 327 | // 328 | // console.log('\n\nvar cell = ', JSON.stringify(Mithril.$cell, function (key, value) { 329 | // var m, obj; 330 | // if (typeof value == 'function') { 331 | // 332 | // for (m in Mithril) { 333 | // if (Mithril[m] == value) { 334 | // value = '* Mithril: ' + m; 335 | // } 336 | // } 337 | // 338 | // wAttrList.forEach(function (entry) { 339 | // if (entry.wrap == value) { 340 | // m = inController(entry.fn); 341 | // value = '* withAttr ' + entry.attr + (m ? ', controller: ' + m : ''); 342 | // } 343 | // }); 344 | // 345 | // obj = IsoModules[mod]; 346 | // for (m in obj) { 347 | // if (obj[m] == value) { 348 | // value = '* IsoModules[' + mod + ']: ' + m; 349 | // } 350 | // } 351 | // m = inController(value); 352 | // if (m) value = '* controller: ' + m; 353 | // 354 | // value = value.toString(); 355 | // } 356 | // if (key == 'config' && value == Mithril.route) value = 'm.route()'; 357 | // return value; 358 | // })); 359 | 360 | // Send the HTML produced server-side 361 | // inserted where the placeholder should have been. 362 | res.send( 363 | data.substr(0, i) + 364 | // send the HTML 365 | mock.html + 366 | // send code to fix the URL on the client 367 | ( 368 | req.url.length > 2 && req.url.substr(0, 2) != '/?' ? 369 | '\n' : 371 | '' 372 | ) + 373 | // Send the script containing Mithril, the app, the reviver and the app. 374 | '\n' + 375 | data.substr(i + placeholder.length) 376 | ); 377 | 378 | }); 379 | }; 380 | }; -------------------------------------------------------------------------------- /node_modules/mithril-isomorphic/mock.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, lastsemic: true, -W033*/ 2 | var fs = require('fs'), 3 | urlParse = require('url').parse, 4 | path = require('path'); 5 | 6 | 7 | var mock = {} 8 | mock.used = {}; 9 | mock.window = new function() { 10 | var window = {} 11 | window.document = {} 12 | window.document.childNodes = [] 13 | window.document.createElement = function(tag) { 14 | return { 15 | style: {}, 16 | childNodes: [], 17 | nodeName: tag.toUpperCase(), 18 | appendChild: window.document.appendChild, 19 | removeChild: window.document.removeChild, 20 | replaceChild: window.document.replaceChild, 21 | insertBefore: function(node, reference) { 22 | node.parentNode = this 23 | var referenceIndex = this.childNodes.indexOf(reference) 24 | if (referenceIndex < 0) this.childNodes.push(node) 25 | else { 26 | var index = this.childNodes.indexOf(node) 27 | this.childNodes.splice(referenceIndex, index < 0 ? 0 : 1, node) 28 | } 29 | }, 30 | insertAdjacentHTML: function(position, html) { 31 | 32 | //todo: accept markup 33 | if (position == "beforebegin") { 34 | this.parentNode.insertBefore(window.document.createTextNode(html), this) 35 | } 36 | else if (position == "beforeend") { 37 | this.appendChild(window.document.createTextNode(html)) 38 | } 39 | }, 40 | setAttribute: function(name, value) { 41 | this[name] = value.toString() 42 | if (name == 'href') { 43 | var url = urlParse(value); 44 | this.pathname = url.pathname; 45 | if (url.search) this.search = url.search; 46 | } 47 | }, 48 | setAttributeNS: function(namespace, name, value) { 49 | this.namespaceURI = namespace 50 | this[name] = value.toString() 51 | }, 52 | getAttribute: function(name, value) { 53 | return this[name] 54 | }, 55 | addEventListener: window.document.addEventListener, 56 | removeEventListener: window.document.removeEventListener 57 | } 58 | } 59 | window.document.createElementNS = function(namespace, tag) { 60 | var element = window.document.createElement(tag) 61 | element.namespaceURI = namespace 62 | return element 63 | } 64 | window.document.createTextNode = function(text) { 65 | return {nodeValue: text.toString()} 66 | } 67 | window.document.documentElement = window.document.createElement("html") 68 | window.document.replaceChild = function(newChild, oldChild) { 69 | var index = this.childNodes.indexOf(oldChild) 70 | if (index > -1) this.childNodes.splice(index, 1, newChild) 71 | else this.childNodes.push(newChild) 72 | newChild.parentNode = this 73 | oldChild.parentNode = null 74 | } 75 | window.document.appendChild = function(child) { 76 | var index = this.childNodes.indexOf(child) 77 | if (index > -1) this.childNodes.splice(index, 1) 78 | this.childNodes.push(child) 79 | child.parentNode = this 80 | } 81 | window.document.removeChild = function(child) { 82 | var index = this.childNodes.indexOf(child) 83 | this.childNodes.splice(index, 1) 84 | child.parentNode = null 85 | } 86 | window.document.addEventListener = function () { 87 | }; 88 | window.document.removeEventListener = function () { 89 | }; 90 | window.performance = { 91 | now: function() { 92 | var hrt = process.hrtime(); 93 | return hrt[0] * 1000 + hrt[1] / 1e6; 94 | } 95 | } 96 | window.cancelAnimationFrame = function() {} 97 | window.requestAnimationFrame = function(callback) { 98 | process.nextTick(callback); 99 | } 100 | window.XMLHttpRequest = new function() { 101 | var request = function() { 102 | this.$headers = {} 103 | this.setRequestHeader = function(key, value) { 104 | this.$headers[key] = value 105 | } 106 | 107 | this.open = function(method, url) { 108 | this.method = method 109 | this.url = url 110 | } 111 | this.send = function() { 112 | var xhr = this; 113 | var r = ''; 114 | xhr.readyState = 4 115 | xhr.status = 200 116 | 117 | request.$instances.push(this); 118 | fs.createReadStream(path.resolve(window.location.pathname, this.url), {encoding:'utf8'}).on('data', function (chunk) { 119 | r += chunk; 120 | }).on('end', function () { 121 | xhr.responseText = r; 122 | xhr.onreadystatechange(); 123 | // xhr.onload({ 124 | // type: 'load', 125 | // target: xhr 126 | // }); 127 | }); 128 | } 129 | } 130 | request.$instances = [] 131 | return request 132 | } 133 | window.location = {search: "?/", pathname: "/", hash: ""}; 134 | window.history = {}; 135 | window.history.pushState = function(data, title, url) { 136 | window.location.pathname = window.location.search = window.location.hash = url 137 | }; 138 | window.history.replaceState = function(data, title, url) { 139 | window.location.pathname = window.location.search = window.location.hash = url 140 | }; 141 | var _body = window.document.createElement('body'); 142 | window.document.appendChild(_body); 143 | window.document.body = _body; 144 | 145 | return window 146 | } 147 | module.exports = mock; 148 | -------------------------------------------------------------------------------- /node_modules/mithril-isomorphic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mithril-iso-middleware", 3 | "description": "Turning Mithril into an isomorphic framework.", 4 | "repository": "", 5 | "main": "./isomithril.js", 6 | "dependencies": { 7 | "q": "^1.0.1", 8 | "q-io": "^1.11.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /node_modules/mithril-isomorphic/reviver.js: -------------------------------------------------------------------------------- 1 | /* jshint browser:true, devel:true */ 2 | /* global Mithril */ 3 | 4 | 5 | 6 | /* The $reviver function reads the processed view in `cell` that would normally 7 | be used to produce the HTML and matches it with the HTML sent from 8 | the server starting in root. 9 | The combination of the two is stored in `cellCache`. 10 | In order to revive the functions, the controller that this view belongs to 11 | is also needed. 12 | */ 13 | Mithril.$reviver = function (cellCache, root, cell, controller) { 14 | 15 | // Skips over pure whitespace nodes. 16 | // I am not sure which browsers support nextElementSibling so I do it in code. 17 | var nextEl = function (node) { 18 | if (node === null) return null; 19 | while (node.nodeName == "#text" && node.nodeValue.trim().length === 0) node = node.nextSibling; 20 | return node; 21 | }; 22 | 23 | 24 | 25 | // Revives the function `fn` that was set in the attribute `attr` 26 | // and attaches it to `node` 27 | var reviveFn = function (node, attrName, fn) { 28 | 29 | // Take care to properly redirect calls to m.route 30 | if (attrName == 'config' && fn == Mithril.route) { 31 | node.onclick = Mithril.$routeUnobtrusive; 32 | return; 33 | } 34 | 35 | // All the rest of the functions just need to be wrapped 36 | // by `autoredraw` so as to notfy Mithril that a refresh is in order 37 | node[attrName] = Mithril.$autoRedraw(fn); 38 | 39 | 40 | }; 41 | 42 | // Revives a single level of nodes in the hierarchy. 43 | // It is called recursively for each level down. 44 | // The `cell` argument is the processed node as returned by the view. 45 | // `parentNode` is an actual DOM node. 46 | var revive = function (cell, parentNode) { 47 | var i, cache, node = parentNode && nextEl(parentNode.firstChild); 48 | 49 | // If the cell contains nothing but a string, 50 | // return it with a reference to the matching node 51 | if (typeof cell == 'string') { 52 | cell = new ''.constructor(cell); 53 | cell.nodes = [node]; 54 | return cell; 55 | } 56 | 57 | // copies the attributes from a single `cell` as 58 | // would have been produced by a call to `m()` 59 | // and returns the corresponding level to be 60 | // stored in `cellCache` 61 | 62 | // It is mostly a matter of associating the actual DOM 63 | // node as produced from the HTML sent from the server 64 | // and reviving the functions associated to events. 65 | var copy = function (cell) { 66 | var prop, value, attrs = {}, attrN, attrV, ret = {}; 67 | 68 | if (typeof cell == 'string') { 69 | ret = new ''.constructor(cell); 70 | } else { 71 | for (prop in cell) { 72 | value = cell[prop]; 73 | switch (prop) { 74 | case 'children': 75 | // Recursively revive each level 76 | if (value) ret.children = revive(value, node); 77 | break; 78 | case 'attrs': 79 | for (attrN in value) { 80 | attrV = value[attrN]; 81 | // If any of the attributes is a function, revive it. 82 | if (typeof attrV == 'function') { 83 | attrV = reviveFn(node, attrN, attrV); 84 | } 85 | attrs[attrN] = attrV; 86 | } 87 | ret.attrs = attrs; 88 | break; 89 | default: 90 | ret[prop] = value; 91 | } 92 | } 93 | } 94 | // Associate the DOM node 95 | ret.nodes = [node]; 96 | // move to the next node 97 | node = node && nextEl(node.nextSibling); 98 | // returned the representation of this branch 99 | return ret; 100 | }; 101 | 102 | // handle either an array of cells or a single cell 103 | if (Array.isArray(cell)) { 104 | cache = cell.map(copy); 105 | // If it didn't get DOM nodes associated with it 106 | // (which normally would not) 107 | // collect the nodes associated with its children 108 | // which should 109 | if (!cache.nodes) { 110 | cache.nodes = cache.map(function (cell) { 111 | return cell.nodes[0]; 112 | }); 113 | } 114 | } else { 115 | cache = copy(cell); 116 | } 117 | return cache; 118 | }; 119 | var c = revive(cell, root); 120 | cellCache[0] = c; 121 | // console.log('\n\nvar cellCacheR = ', JSON.stringify(c, function (key, value) { 122 | // if (key == 'nodes') value = value.length + ' [' + value.map(function (node) { 123 | // return (node ? node.nodeName || node.nodeValue : 'null*node'); 124 | // }) + ']'; 125 | // return value; 126 | // })); 127 | }; -------------------------------------------------------------------------------- /node_modules/mithril/mithril.js: -------------------------------------------------------------------------------- 1 | /* jshint -W033 */ 2 | // Search for method `m.render` which is the only one changed in this file 3 | 4 | 5 | 6 | // This is based on the version of Mithril of Feb 18th, 2015: 7 | // https://github.com/lhorie/mithril.js/blob/550fe9871aede73bd065da7b08f315e224aeb873/mithril.js 8 | 9 | var Mithril = m = (function app(window, undefined) { 10 | var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function"; 11 | var type = {}.toString; 12 | var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; 13 | var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; 14 | 15 | // caching commonly used variables 16 | var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; 17 | 18 | // self invoking function needed because of the way mocks work 19 | function initialize(window){ 20 | $document = window.document; 21 | $location = window.location; 22 | $cancelAnimationFrame = window.cancelAnimationFrame || window.clearTimeout; 23 | $requestAnimationFrame = window.requestAnimationFrame || window.setTimeout; 24 | } 25 | 26 | initialize(window); 27 | 28 | 29 | /** 30 | * @typedef {String} Tag 31 | * A string that looks like -> div.classname#id[param=one][param2=two] 32 | * Which describes a DOM node 33 | */ 34 | 35 | /** 36 | * 37 | * @param {Tag} The DOM node tag 38 | * @param {Object=[]} optional key-value pairs to be mapped to DOM attrs 39 | * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional) 40 | * 41 | */ 42 | function m() { 43 | var args = [].slice.call(arguments); 44 | var hasAttrs = args[1] != null && type.call(args[1]) === OBJECT && !("tag" in args[1]) && !("subtree" in args[1]); 45 | var attrs = hasAttrs ? args[1] : {}; 46 | var classAttrName = "class" in attrs ? "class" : "className"; 47 | var cell = {tag: "div", attrs: {}}; 48 | var match, classes = []; 49 | if (type.call(args[0]) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string") 50 | while (match = parser.exec(args[0])) { 51 | if (match[1] === "" && match[2]) cell.tag = match[2]; 52 | else if (match[1] === "#") cell.attrs.id = match[2]; 53 | else if (match[1] === ".") classes.push(match[2]); 54 | else if (match[3][0] === "[") { 55 | var pair = attrParser.exec(match[3]); 56 | cell.attrs[pair[1]] = pair[3] || (pair[2] ? "" :true) 57 | } 58 | } 59 | if (classes.length > 0) cell.attrs[classAttrName] = classes.join(" "); 60 | 61 | 62 | var children = hasAttrs ? args.slice(2) : args.slice(1); 63 | if (children.length === 1 && type.call(children[0]) === ARRAY) { 64 | cell.children = children[0] 65 | } 66 | else { 67 | cell.children = children 68 | } 69 | 70 | for (var attrName in attrs) { 71 | if (attrName === classAttrName) { 72 | if (attrs[attrName] !== "") cell.attrs[attrName] = (cell.attrs[attrName] || "") + " " + attrs[attrName]; 73 | } 74 | else cell.attrs[attrName] = attrs[attrName] 75 | } 76 | return cell 77 | } 78 | function build(parentElement, parentTag, parentCache, parentIndex, data, cached, shouldReattach, index, editable, namespace, configs) { 79 | //`build` is a recursive function that manages creation/diffing/removal of DOM elements based on comparison between `data` and `cached` 80 | //the diff algorithm can be summarized as this: 81 | //1 - compare `data` and `cached` 82 | //2 - if they are different, copy `data` to `cached` and update the DOM based on what the difference is 83 | //3 - recursively apply this algorithm for every array and for the children of every virtual element 84 | 85 | //the `cached` data structure is essentially the same as the previous redraw's `data` data structure, with a few additions: 86 | //- `cached` always has a property called `nodes`, which is a list of DOM elements that correspond to the data represented by the respective virtual element 87 | //- in order to support attaching `nodes` as a property of `cached`, `cached` is *always* a non-primitive object, i.e. if the data was a string, then cached is a String instance. If data was `null` or `undefined`, cached is `new String("")` 88 | //- `cached also has a `configContext` property, which is the state storage object exposed by config(element, isInitialized, context) 89 | //- when `cached` is an Object, it represents a virtual element; when it's an Array, it represents a list of elements; when it's a String, Number or Boolean, it represents a text node 90 | 91 | //`parentElement` is a DOM element used for W3C DOM API calls 92 | //`parentTag` is only used for handling a corner case for textarea values 93 | //`parentCache` is used to remove nodes in some multi-node cases 94 | //`parentIndex` and `index` are used to figure out the offset of nodes. They're artifacts from before arrays started being flattened and are likely refactorable 95 | //`data` and `cached` are, respectively, the new and old nodes being diffed 96 | //`shouldReattach` is a flag indicating whether a parent node was recreated (if so, and if this node is reused, then this node must reattach itself to the new parent) 97 | //`editable` is a flag that indicates whether an ancestor is contenteditable 98 | //`namespace` indicates the closest HTML namespace as it cascades down from an ancestor 99 | //`configs` is a list of config functions to run after the topmost `build` call finishes running 100 | 101 | //there's logic that relies on the assumption that null and undefined data are equivalent to empty strings 102 | //- this prevents lifecycle surprises from procedural helpers that mix implicit and explicit return statements (e.g. function foo() {if (cond) return m("div")} 103 | //- it simplifies diffing code 104 | //data.toString() is null if data is the return value of Console.log in Firefox 105 | if (data == null || data.toString() == null) data = ""; 106 | if (data.subtree === "retain") return cached; 107 | var cachedType = type.call(cached), dataType = type.call(data); 108 | if (cached == null || cachedType !== dataType) { 109 | if (cached != null) { 110 | if (parentCache && parentCache.nodes) { 111 | var offset = index - parentIndex; 112 | var end = offset + (dataType === ARRAY ? data : cached.nodes).length; 113 | clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)) 114 | } 115 | else if (cached.nodes) clear(cached.nodes, cached) 116 | } 117 | cached = new data.constructor; 118 | if (cached.tag) cached = {}; //if constructor creates a virtual dom element, use a blank object as the base cached node instead of copying the virtual el (#277) 119 | cached.nodes = [] 120 | } 121 | 122 | if (dataType === ARRAY) { 123 | //recursively flatten array 124 | for (var i = 0, len = data.length; i < len; i++) { 125 | if (type.call(data[i]) === ARRAY) { 126 | data = data.concat.apply([], data); 127 | i-- //check current index again and flatten until there are no more nested arrays at that index 128 | len = data.length 129 | } 130 | } 131 | 132 | var nodes = [], intact = cached.length === data.length, subArrayCount = 0; 133 | 134 | //keys algorithm: sort elements without recreating them if keys are present 135 | //1) create a map of all existing keys, and mark all for deletion 136 | //2) add new keys to map and mark them for addition 137 | //3) if key exists in new list, change action from deletion to a move 138 | //4) for each key, handle its corresponding action as marked in previous steps 139 | //5) copy unkeyed items into their respective gaps 140 | var DELETION = 1, INSERTION = 2 , MOVE = 3; 141 | var existing = {}, unkeyed = [], shouldMaintainIdentities = false; 142 | for (var i = 0; i < cached.length; i++) { 143 | if (cached[i] && cached[i].attrs && cached[i].attrs.key != null) { 144 | shouldMaintainIdentities = true; 145 | existing[cached[i].attrs.key] = {action: DELETION, index: i} 146 | } 147 | } 148 | if (shouldMaintainIdentities) { 149 | if (data.indexOf(null) > -1) data = data.filter(function(x) {return x != null}) 150 | 151 | var keysDiffer = false 152 | if (data.length != cached.length) keysDiffer = true 153 | else for (var i = 0, cachedCell, dataCell; cachedCell = cached[i], dataCell = data[i]; i++) { 154 | if (cachedCell.attrs && dataCell.attrs && cachedCell.attrs.key != dataCell.attrs.key) { 155 | keysDiffer = true 156 | break 157 | } 158 | } 159 | 160 | if (keysDiffer) { 161 | for (var i = 0, len = data.length; i < len; i++) { 162 | if (data[i] && data[i].attrs) { 163 | if (data[i].attrs.key != null) { 164 | var key = data[i].attrs.key; 165 | if (!existing[key]) existing[key] = {action: INSERTION, index: i}; 166 | else existing[key] = { 167 | action: MOVE, 168 | index: i, 169 | from: existing[key].index, 170 | element: cached.nodes[existing[key].index] || $document.createElement("div") 171 | } 172 | } 173 | else unkeyed.push({index: i, element: parentElement.childNodes[i] || $document.createElement("div")}) 174 | } 175 | } 176 | var actions = [] 177 | for (var prop in existing) actions.push(existing[prop]) 178 | var changes = actions.sort(sortChanges); 179 | var newCached = new Array(cached.length) 180 | 181 | for (var i = 0, change; change = changes[i]; i++) { 182 | if (change.action === DELETION) { 183 | clear(cached[change.index].nodes, cached[change.index]); 184 | newCached.splice(change.index, 1) 185 | } 186 | if (change.action === INSERTION) { 187 | var dummy = $document.createElement("div"); 188 | dummy.key = data[change.index].attrs.key; 189 | parentElement.insertBefore(dummy, parentElement.childNodes[change.index] || null); 190 | newCached.splice(change.index, 0, {attrs: {key: data[change.index].attrs.key}, nodes: [dummy]}) 191 | } 192 | 193 | if (change.action === MOVE) { 194 | if (parentElement.childNodes[change.index] !== change.element && change.element !== null) { 195 | parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null) 196 | } 197 | newCached[change.index] = cached[change.from] 198 | } 199 | } 200 | for (var i = 0, len = unkeyed.length; i < len; i++) { 201 | var change = unkeyed[i]; 202 | parentElement.insertBefore(change.element, parentElement.childNodes[change.index] || null); 203 | newCached[change.index] = cached[change.index] 204 | } 205 | cached = newCached; 206 | cached.nodes = new Array(parentElement.childNodes.length); 207 | for (var i = 0, child; child = parentElement.childNodes[i]; i++) cached.nodes[i] = child 208 | } 209 | } 210 | //end key algorithm 211 | 212 | for (var i = 0, cacheCount = 0, len = data.length; i < len; i++) { 213 | //diff each item in the array 214 | var item = build(parentElement, parentTag, cached, index, data[i], cached[cacheCount], shouldReattach, index + subArrayCount || subArrayCount, editable, namespace, configs); 215 | if (item === undefined) continue; 216 | if (!item.nodes.intact) intact = false; 217 | if (item.$trusted) { 218 | //fix offset of next element if item was a trusted string w/ more than one html element 219 | //the first clause in the regexp matches elements 220 | //the second clause (after the pipe) matches text nodes 221 | subArrayCount += (item.match(/<[^\/]|\>\s*[^<]|&/g) || []).length 222 | } 223 | else subArrayCount += type.call(item) === ARRAY ? item.length : 1; 224 | cached[cacheCount++] = item 225 | } 226 | if (!intact) { 227 | //diff the array itself 228 | 229 | //update the list of DOM nodes by collecting the nodes from each item 230 | for (var i = 0, len = data.length; i < len; i++) { 231 | if (cached[i] != null) nodes.push.apply(nodes, cached[i].nodes) 232 | } 233 | //remove items from the end of the array if the new array is shorter than the old one 234 | //if errors ever happen here, the issue is most likely a bug in the construction of the `cached` data structure somewhere earlier in the program 235 | for (var i = 0, node; node = cached.nodes[i]; i++) { 236 | if (node.parentNode != null && nodes.indexOf(node) < 0) clear([node], [cached[i]]) 237 | } 238 | if (data.length < cached.length) cached.length = data.length; 239 | cached.nodes = nodes 240 | } 241 | } 242 | else if (data != null && dataType === OBJECT) { 243 | if (!data.attrs) data.attrs = {}; 244 | if (!cached.attrs) cached.attrs = {}; 245 | 246 | var dataAttrKeys = Object.keys(data.attrs) 247 | var hasKeys = dataAttrKeys.length > ("key" in data.attrs ? 1 : 0) 248 | //if an element is different enough from the one in cache, recreate it 249 | if (data.tag != cached.tag || dataAttrKeys.join() != Object.keys(cached.attrs).join() || data.attrs.id != cached.attrs.id) { 250 | if (cached.nodes.length) clear(cached.nodes); 251 | if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload() 252 | } 253 | if (type.call(data.tag) != STRING) return; 254 | 255 | var node, isNew = cached.nodes.length === 0; 256 | if (data.attrs.xmlns) namespace = data.attrs.xmlns; 257 | else if (data.tag === "svg") namespace = "http://www.w3.org/2000/svg"; 258 | else if (data.tag === "math") namespace = "http://www.w3.org/1998/Math/MathML"; 259 | if (isNew) { 260 | if (data.attrs.is) node = namespace === undefined ? $document.createElement(data.tag, data.attrs.is) : $document.createElementNS(namespace, data.tag, data.attrs.is); 261 | else node = namespace === undefined ? $document.createElement(data.tag) : $document.createElementNS(namespace, data.tag); 262 | cached = { 263 | tag: data.tag, 264 | //set attributes first, then create children 265 | attrs: hasKeys ? setAttributes(node, data.tag, data.attrs, {}, namespace) : data.attrs, 266 | children: data.children != null && data.children.length > 0 ? 267 | build(node, data.tag, undefined, undefined, data.children, cached.children, true, 0, data.attrs.contenteditable ? node : editable, namespace, configs) : 268 | data.children, 269 | nodes: [node] 270 | }; 271 | if (cached.children && !cached.children.nodes) cached.children.nodes = []; 272 | //edge case: setting value on