1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | var path = require('path');
|
10 | var EventEmitter = require('events').EventEmitter;
|
11 | var tracks = require('tracks');
|
12 | var util = require('racer/lib/util');
|
13 | var derbyTemplates = require('derby-templates');
|
14 | var templates = derbyTemplates.templates;
|
15 | var documentListeners = require('./documentListeners');
|
16 | var components = require('./components');
|
17 | var Page = require('./Page');
|
18 | var serializedViews = require('./_views');
|
19 |
|
20 | module.exports = App;
|
21 |
|
22 | function App(derby, name, filename, options) {
|
23 | EventEmitter.call(this);
|
24 | this.derby = derby;
|
25 | this.name = name;
|
26 | this.filename = filename;
|
27 | this.scriptHash = '{{DERBY_SCRIPT_HASH}}';
|
28 | this.bundledAt = '{{DERBY_BUNDLED_AT}}';
|
29 | this.Page = createAppPage();
|
30 | this.proto = this.Page.prototype;
|
31 | this.views = new templates.Views();
|
32 | this.tracksRoutes = tracks.setup(this);
|
33 | this.model = null;
|
34 | this.page = null;
|
35 | this._init(options);
|
36 | }
|
37 |
|
38 | function createAppPage() {
|
39 |
|
40 |
|
41 | function AppPage() {
|
42 | Page.apply(this, arguments);
|
43 | }
|
44 | AppPage.prototype = Object.create(Page.prototype);
|
45 | return AppPage;
|
46 | }
|
47 |
|
48 | util.mergeInto(App.prototype, EventEmitter.prototype);
|
49 |
|
50 |
|
51 | App.prototype._init = function() {
|
52 | this._waitForAttach = true;
|
53 | this._cancelAttach = false;
|
54 | this.model = new this.derby.Model();
|
55 | serializedViews(derbyTemplates, this.views);
|
56 |
|
57 |
|
58 | this._contentReady();
|
59 | };
|
60 | App.prototype._finishInit = function() {
|
61 | var script = this._getScript();
|
62 | var data = JSON.parse(script.nextSibling.innerHTML);
|
63 | this.model.createConnection(data);
|
64 | this.emit('model', this.model);
|
65 | util.isProduction = data.nodeEnv === 'production';
|
66 | if (!util.isProduction) this._autoRefresh();
|
67 | this.model.unbundle(data);
|
68 | var page = this.createPage();
|
69 | page.params = this.model.get('$render.params');
|
70 | this.emit('ready', page);
|
71 | this._waitForAttach = false;
|
72 |
|
73 |
|
74 | if (this._cancelAttach) {
|
75 | this.history.refresh();
|
76 | return;
|
77 | }
|
78 |
|
79 |
|
80 |
|
81 | if (util.isProduction) {
|
82 | try {
|
83 | page.attach();
|
84 | } catch (err) {
|
85 | this.history.refresh();
|
86 | console.warn('attachment error', err.stack);
|
87 | }
|
88 | } else {
|
89 | page.attach();
|
90 | }
|
91 | this.emit('load', page);
|
92 | };
|
93 |
|
94 | App.prototype._contentReady = function() {
|
95 |
|
96 | var isReady = false;
|
97 | var app = this;
|
98 |
|
99 |
|
100 | function onDOMContentLoaded() {
|
101 | if (document.addEventListener) {
|
102 | document.removeEventListener('DOMContentLoaded', onDOMContentLoaded, false);
|
103 | } else {
|
104 |
|
105 |
|
106 | document.detachEvent('onreadystatechange', onDOMContentLoaded);
|
107 | }
|
108 | onDOMReady();
|
109 | }
|
110 |
|
111 |
|
112 | function onDOMReady() {
|
113 |
|
114 | if (isReady) return;
|
115 |
|
116 | if (!document.body) return setTimeout(onDOMReady, 0);
|
117 |
|
118 | isReady = true;
|
119 |
|
120 | setTimeout(function() {
|
121 | app._finishInit();
|
122 | }, 0);
|
123 | }
|
124 |
|
125 |
|
126 | function doScrollCheck() {
|
127 | if (isReady) return;
|
128 | try {
|
129 |
|
130 |
|
131 | document.documentElement.doScroll('left');
|
132 | } catch (err) {
|
133 | setTimeout(doScrollCheck, 0);
|
134 | return;
|
135 | }
|
136 |
|
137 | onDOMReady();
|
138 | }
|
139 |
|
140 |
|
141 | if (document.readyState !== 'loading') return onDOMReady();
|
142 |
|
143 |
|
144 | if (document.addEventListener) {
|
145 |
|
146 | document.addEventListener('DOMContentLoaded', onDOMContentLoaded, false);
|
147 |
|
148 | window.addEventListener('load', onDOMContentLoaded, false);
|
149 |
|
150 | } else if (document.attachEvent) {
|
151 |
|
152 |
|
153 | document.attachEvent('onreadystatechange', onDOMContentLoaded);
|
154 |
|
155 | window.attachEvent('onload', onDOMContentLoaded);
|
156 |
|
157 |
|
158 | var toplevel;
|
159 | try {
|
160 | toplevel = window.frameElement == null;
|
161 | } catch (err) {}
|
162 | if (document.documentElement.doScroll && toplevel) {
|
163 | doScrollCheck();
|
164 | }
|
165 | }
|
166 | };
|
167 |
|
168 | App.prototype._getScript = function() {
|
169 | return document.querySelector('script[data-derby-app]');
|
170 | };
|
171 |
|
172 | App.prototype.use = util.use;
|
173 | App.prototype.serverUse = util.serverUse;
|
174 |
|
175 | App.prototype.loadViews = function() {};
|
176 |
|
177 | App.prototype.loadStyles = function() {};
|
178 |
|
179 | App.prototype.component = function(viewName, constructor) {
|
180 | if (typeof viewName === 'function') {
|
181 | constructor = viewName;
|
182 | viewName = null;
|
183 | }
|
184 |
|
185 | // Inherit from Component
|
186 | components.extendComponent(constructor);
|
187 |
|
188 | // Load template view from filename
|
189 | if (constructor.prototype.view) {
|
190 | var viewFilename = constructor.prototype.view;
|
191 | viewName = constructor.prototype.name || path.basename(viewFilename, '.html');
|
192 | this.loadViews(viewFilename, viewName);
|
193 |
|
194 | } else if (!viewName) {
|
195 | if (constructor.prototype.name) {
|
196 | viewName = constructor.prototype.name;
|
197 | var view = this.views.register(viewName);
|
198 | view.template = templates.emptyTemplate;
|
199 | } else {
|
200 | throw new Error('No view name specified for component');
|
201 | }
|
202 | }
|
203 |
|
204 |
|
205 | var view = this.views.find(viewName);
|
206 | if (!view) {
|
207 | var message = this.views.findErrorMessage(viewName);
|
208 | throw new Error(message);
|
209 | }
|
210 | view.componentFactory = components.createFactory(constructor);
|
211 |
|
212 | // Make chainable
|
213 | return this;
|
214 | };
|
215 |
|
216 | App.prototype.createPage = function() {
|
217 | if (this.page) {
|
218 | this.emit('destroyPage', this.page);
|
219 | this.page.destroy();
|
220 | }
|
221 | var page = new this.Page(this, this.model);
|
222 | this.page = page;
|
223 | return page;
|
224 | };
|
225 |
|
226 | App.prototype.onRoute = function(callback, page, next, done) {
|
227 | if (this._waitForAttach) {
|
228 |
|
229 |
|
230 | this._cancelAttach = true;
|
231 | return;
|
232 | }
|
233 | this.emit('route', page);
|
234 |
|
235 | page.model.set('$render.params', page.params);
|
236 | page.model.set('$render.url', page.params.url);
|
237 | page.model.set('$render.query', page.params.query);
|
238 |
|
239 | if (done) {
|
240 | var app = this;
|
241 | var _done = function() {
|
242 | app.emit('routeDone', page, 'transition');
|
243 | done();
|
244 | };
|
245 | callback.call(page, page, page.model, page.params, next, _done);
|
246 | return;
|
247 | }
|
248 | callback.call(page, page, page.model, page.params, next);
|
249 | };
|
250 |
|
251 | App.prototype._autoRefresh = function() {
|
252 | var app = this;
|
253 | var connection = this.model.connection;
|
254 | connection.on('connected', function() {
|
255 | connection.send({
|
256 | derby: 'app',
|
257 | name: app.name,
|
258 | hash: app.scriptHash
|
259 | });
|
260 | });
|
261 | connection.on('receive', function(request) {
|
262 | if (request.data.derby) {
|
263 | var message = request.data;
|
264 | request.data = null;
|
265 | app._handleMessage(message.derby, message);
|
266 | }
|
267 | });
|
268 | };
|
269 |
|
270 | App.prototype._handleMessage = function(action, message) {
|
271 | if (action === 'refreshViews') {
|
272 | var fn = new Function('return ' + message.views)();
|
273 | fn(derbyTemplates, this.views);
|
274 | var ns = this.model.get('$render.ns');
|
275 | this.page.render(ns);
|
276 |
|
277 | } else if (action === 'refreshStyles') {
|
278 | var styleElement = document.querySelector('style[data-filename="' +
|
279 | message.filename + '"]');
|
280 | if (styleElement) styleElement.innerHTML = message.css;
|
281 |
|
282 | } else if (action === 'reload') {
|
283 | this.model.whenNothingPending(function() {
|
284 | window.location = window.location;
|
285 | });
|
286 | }
|
287 | };
|
288 |
|
289 | util.serverRequire(module, './App.server');
|