1 | var derbyTemplates = require('derby-templates');
|
2 | var contexts = derbyTemplates.contexts;
|
3 | var expressions = derbyTemplates.expressions;
|
4 | var templates = derbyTemplates.templates;
|
5 | var DependencyOptions = derbyTemplates.options.DependencyOptions;
|
6 | var util = require('racer/lib/util');
|
7 | var components = require('./components');
|
8 | var EventModel = require('./eventmodel');
|
9 | var textDiff = require('./textDiff');
|
10 | var Controller = require('./Controller');
|
11 | var documentListeners = require('./documentListeners');
|
12 |
|
13 | module.exports = Page;
|
14 |
|
15 | function Page(app, model, req, res) {
|
16 | Controller.call(this, app, this, model);
|
17 | this.req = req;
|
18 | this.res = res;
|
19 | this.params = null;
|
20 | if (this.init) this.init(model);
|
21 | this.context = this._createContext();
|
22 | this._eventModel = null;
|
23 | this._removeModelListeners = null;
|
24 | this._components = {};
|
25 | this._addListeners();
|
26 | }
|
27 |
|
28 | util.mergeInto(Page.prototype, Controller.prototype);
|
29 |
|
30 | Page.prototype.$bodyClass = function(ns) {
|
31 | if (!ns) return;
|
32 | var classNames = [];
|
33 | var segments = ns.split(':');
|
34 | for (var i = 0, len = segments.length; i < len; i++) {
|
35 | var className = segments.slice(0, i + 1).join('-');
|
36 | classNames.push(className);
|
37 | }
|
38 | return classNames.join(' ');
|
39 | };
|
40 |
|
41 | Page.prototype.$preventDefault = function(e) {
|
42 | e.preventDefault();
|
43 | };
|
44 |
|
45 | Page.prototype.$stopPropagation = function(e) {
|
46 | e.stopPropagation();
|
47 | };
|
48 |
|
49 | Page.prototype._setRenderParams = function(ns) {
|
50 | this.model.set('$render.ns', ns);
|
51 | this.model.set('$render.params', this.params);
|
52 | this.model.set('$render.url', this.params && this.params.url);
|
53 | this.model.set('$render.query', this.params && this.params.query);
|
54 | };
|
55 |
|
56 | Page.prototype._setRenderPrefix = function(ns) {
|
57 | var prefix = (ns) ? ns + ':' : '';
|
58 | this.model.set('$render.prefix', prefix);
|
59 | };
|
60 |
|
61 | Page.prototype.get = function(viewName, ns, unescaped) {
|
62 | this._setRenderPrefix(ns);
|
63 | var view = this.getView(viewName, ns);
|
64 | return view.get(this.context, unescaped);
|
65 | };
|
66 |
|
67 | Page.prototype.getFragment = function(viewName, ns) {
|
68 | this._setRenderPrefix(ns);
|
69 | var view = this.getView(viewName, ns);
|
70 | return view.getFragment(this.context);
|
71 | };
|
72 |
|
73 | Page.prototype.getView = function(viewName, ns) {
|
74 | return this.app.views.find(viewName, ns);
|
75 | };
|
76 |
|
77 | Page.prototype.render = function(ns) {
|
78 | this.app.emit('render', this);
|
79 | this.context.pause();
|
80 | this._setRenderParams(ns);
|
81 | var titleFragment = this.getFragment('TitleElement', ns);
|
82 | var bodyFragment = this.getFragment('BodyElement', ns);
|
83 | var titleElement = document.getElementsByTagName('title')[0];
|
84 | titleElement.parentNode.replaceChild(titleFragment, titleElement);
|
85 | document.body.parentNode.replaceChild(bodyFragment, document.body);
|
86 | this.context.unpause();
|
87 | if (this.create) this.create(this.model, this.dom);
|
88 | this.app.emit('routeDone', this, 'render');
|
89 | };
|
90 |
|
91 | Page.prototype.attach = function() {
|
92 | this.context.pause();
|
93 | var ns = this.model.get('$render.ns');
|
94 | var titleView = this.getView('TitleElement', ns);
|
95 | var bodyView = this.getView('BodyElement', ns);
|
96 | var titleElement = document.getElementsByTagName('title')[0];
|
97 | titleView.attachTo(titleElement.parentNode, titleElement, this.context);
|
98 | bodyView.attachTo(document.body.parentNode, document.body, this.context);
|
99 | this.context.unpause();
|
100 | if (this.create) this.create(this.model, this.dom);
|
101 | };
|
102 |
|
103 | Page.prototype._createContext = function() {
|
104 | var contextMeta = new contexts.ContextMeta();
|
105 | contextMeta.views = this.app && this.app.views;
|
106 | var context = new contexts.Context(contextMeta, this);
|
107 | context.expression = new expressions.PathExpression([]);
|
108 | context.alias = '#root';
|
109 | return context;
|
110 | };
|
111 |
|
112 | Page.prototype._addListeners = function() {
|
113 | var eventModel = this._eventModel = new EventModel();
|
114 | this._addModelListeners(eventModel);
|
115 | this._addContextListeners(eventModel);
|
116 | };
|
117 |
|
118 | Page.prototype.destroy = function() {
|
119 | this.emit('destroy');
|
120 | this._removeModelListeners();
|
121 | for (var id in this._components) {
|
122 | var component = this._components[id];
|
123 | component.destroy();
|
124 | }
|
125 |
|
126 |
|
127 | var silentModel = this.model.silent();
|
128 | silentModel.destroy('_page');
|
129 | silentModel.destroy('$components');
|
130 |
|
131 | silentModel.unloadAll && silentModel.unloadAll();
|
132 | };
|
133 |
|
134 | Page.prototype._addModelListeners = function(eventModel) {
|
135 | var model = this.model;
|
136 | if (!model) return;
|
137 |
|
138 | if (model.get('$derbyFlags.immediateModelListeners')) {
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 | return this._addModelListenersImmediate(eventModel);
|
145 | }
|
146 |
|
147 | var context = this.context;
|
148 | var changeListener = model.on('change', '**', function onChange(path, value, previous, pass) {
|
149 | var segments = util.castSegments(path.split('.'));
|
150 |
|
151 |
|
152 | eventModel.set(segments, previous, pass);
|
153 | });
|
154 | var loadListener = model.on('load', '**', function onLoad(path) {
|
155 | var segments = util.castSegments(path.split('.'));
|
156 | eventModel.set(segments);
|
157 | });
|
158 | var unloadListener = model.on('unload', '**', function onUnload(path) {
|
159 | var segments = util.castSegments(path.split('.'));
|
160 | eventModel.set(segments);
|
161 | });
|
162 | var insertListener = model.on('insert', '**', function onInsert(path, index, values) {
|
163 | var segments = util.castSegments(path.split('.'));
|
164 | eventModel.insert(segments, index, values.length);
|
165 | });
|
166 | var removeListener = model.on('remove', '**', function onRemove(path, index, values) {
|
167 | var segments = util.castSegments(path.split('.'));
|
168 | eventModel.remove(segments, index, values.length);
|
169 | });
|
170 | var moveListener = model.on('move', '**', function onMove(path, from, to, howMany) {
|
171 | var segments = util.castSegments(path.split('.'));
|
172 | eventModel.move(segments, from, to, howMany);
|
173 | });
|
174 |
|
175 | this._removeModelListeners = function() {
|
176 | model.removeListener('change', changeListener);
|
177 | model.removeListener('load', loadListener);
|
178 | model.removeListener('unload', unloadListener);
|
179 | model.removeListener('insert', insertListener);
|
180 | model.removeListener('remove', removeListener);
|
181 | model.removeListener('move', moveListener);
|
182 | };
|
183 | };
|
184 | Page.prototype._addModelListenersImmediate = function(eventModel) {
|
185 | var model = this.model;
|
186 | if (!model) return;
|
187 |
|
188 |
|
189 |
|
190 |
|
191 | var changeListener = model.on('changeImmediate', function onChange(segments, eventArgs) {
|
192 |
|
193 | var previous = eventArgs[1];
|
194 |
|
195 |
|
196 | var pass = eventArgs[2];
|
197 | segments = util.castSegments(segments.slice());
|
198 | eventModel.set(segments, previous, pass);
|
199 | });
|
200 | var loadListener = model.on('loadImmediate', function onLoad(segments) {
|
201 | segments = util.castSegments(segments.slice());
|
202 | eventModel.set(segments);
|
203 | });
|
204 | var unloadListener = model.on('unloadImmediate', function onUnload(segments) {
|
205 | segments = util.castSegments(segments.slice());
|
206 | eventModel.set(segments);
|
207 | });
|
208 | var insertListener = model.on('insertImmediate', function onInsert(segments, eventArgs) {
|
209 | var index = eventArgs[0];
|
210 | var values = eventArgs[1];
|
211 | segments = util.castSegments(segments.slice());
|
212 | eventModel.insert(segments, index, values.length);
|
213 | });
|
214 | var removeListener = model.on('removeImmediate', function onRemove(segments, eventArgs) {
|
215 | var index = eventArgs[0];
|
216 | var values = eventArgs[1];
|
217 | segments = util.castSegments(segments.slice());
|
218 | eventModel.remove(segments, index, values.length);
|
219 | });
|
220 | var moveListener = model.on('moveImmediate', function onMove(segments, eventArgs) {
|
221 | var from = eventArgs[0];
|
222 | var to = eventArgs[1];
|
223 | var howMany = eventArgs[2];
|
224 | segments = util.castSegments(segments.slice());
|
225 | eventModel.move(segments, from, to, howMany);
|
226 | });
|
227 |
|
228 | this._removeModelListeners = function() {
|
229 | model.removeListener('changeImmediate', changeListener);
|
230 | model.removeListener('loadImmediate', loadListener);
|
231 | model.removeListener('unloadImmediate', unloadListener);
|
232 | model.removeListener('insertImmediate', insertListener);
|
233 | model.removeListener('removeImmediate', removeListener);
|
234 | model.removeListener('moveImmediate', moveListener);
|
235 | };
|
236 | };
|
237 |
|
238 | Page.prototype._addContextListeners = function(eventModel) {
|
239 | this.context.meta.addBinding = addBinding;
|
240 | this.context.meta.removeBinding = removeBinding;
|
241 | this.context.meta.removeNode = removeNode;
|
242 | this.context.meta.addItemContext = addItemContext;
|
243 | this.context.meta.removeItemContext = removeItemContext;
|
244 |
|
245 | function addItemContext(context) {
|
246 | var segments = context.expression.resolve(context);
|
247 | eventModel.addItemContext(segments, context);
|
248 | }
|
249 | function removeItemContext(context) {
|
250 |
|
251 | }
|
252 | function addBinding(binding) {
|
253 | patchTextBinding(binding);
|
254 | var expressions = binding.template.expressions;
|
255 | if (expressions) {
|
256 | for (var i = 0, len = expressions.length; i < len; i++) {
|
257 | addDependencies(eventModel, expressions[i], binding);
|
258 | }
|
259 | } else {
|
260 | var expression = binding.template.expression;
|
261 | addDependencies(eventModel, expression, binding);
|
262 | }
|
263 | }
|
264 | function removeBinding(binding) {
|
265 | var bindingWrappers = binding.meta;
|
266 | if (!bindingWrappers) return;
|
267 | for (var i = bindingWrappers.length; i--;) {
|
268 | eventModel.removeBinding(bindingWrappers[i]);
|
269 | }
|
270 | }
|
271 | function removeNode(node) {
|
272 | var component = node.$component;
|
273 | if (component && !component.singleton) {
|
274 | component.destroy();
|
275 | }
|
276 | var destroyListeners = node.$destroyListeners;
|
277 | if (destroyListeners) {
|
278 | for (var i = 0; i < destroyListeners.length; i++) {
|
279 | destroyListeners[i]();
|
280 | }
|
281 | }
|
282 | }
|
283 | };
|
284 |
|
285 | function addDependencies(eventModel, expression, binding) {
|
286 | var bindingWrapper = new BindingWrapper(eventModel, expression, binding);
|
287 | bindingWrapper.updateDependencies();
|
288 | }
|
289 |
|
290 |
|
291 |
|
292 | var nextId = 1;
|
293 | function BindingWrapper(eventModel, expression, binding) {
|
294 | this.eventModel = eventModel;
|
295 | this.expression = expression;
|
296 | this.binding = binding;
|
297 | this.id = nextId++;
|
298 | this.eventModels = null;
|
299 | this.dependencies = null;
|
300 | this.ignoreTemplateDependency = (
|
301 | binding instanceof components.ComponentAttributeBinding
|
302 | ) || (
|
303 | (binding.template instanceof templates.DynamicText) &&
|
304 | (binding instanceof templates.RangeBinding)
|
305 | );
|
306 | if (binding.meta) {
|
307 | binding.meta.push(this);
|
308 | } else {
|
309 | binding.meta = [this];
|
310 | }
|
311 | }
|
312 | BindingWrapper.prototype.updateDependencies = function() {
|
313 | var dependencyOptions;
|
314 | if (this.ignoreTemplateDependency && this.binding.condition instanceof templates.Template) {
|
315 | dependencyOptions = new DependencyOptions();
|
316 | dependencyOptions.setIgnoreTemplate(this.binding.condition);
|
317 | }
|
318 | var dependencies = this.expression.dependencies(this.binding.context, dependencyOptions);
|
319 | if (this.dependencies) {
|
320 |
|
321 | if (equalDependencies(this.dependencies, dependencies)) return;
|
322 |
|
323 | this.eventModel.removeBinding(this);
|
324 | }
|
325 |
|
326 | if (!dependencies) return;
|
327 | this.dependencies = dependencies;
|
328 | for (var i = 0, len = dependencies.length; i < len; i++) {
|
329 | var dependency = dependencies[i];
|
330 | if (dependency) this.eventModel.addBinding(dependency, this);
|
331 | }
|
332 | };
|
333 | BindingWrapper.prototype.update = function(previous, pass) {
|
334 | this.binding.update(previous, pass);
|
335 | this.updateDependencies();
|
336 | };
|
337 | BindingWrapper.prototype.insert = function(index, howMany) {
|
338 | this.binding.insert(index, howMany);
|
339 | this.updateDependencies();
|
340 | };
|
341 | BindingWrapper.prototype.remove = function(index, howMany) {
|
342 | this.binding.remove(index, howMany);
|
343 | this.updateDependencies();
|
344 | };
|
345 | BindingWrapper.prototype.move = function(from, to, howMany) {
|
346 | this.binding.move(from, to, howMany);
|
347 | this.updateDependencies();
|
348 | };
|
349 |
|
350 | function equalDependencies(a, b) {
|
351 | var lenA = a ? a.length : -1;
|
352 | var lenB = b ? b.length : -1;
|
353 | if (lenA !== lenB) return false;
|
354 | for (var i = 0; i < lenA; i++) {
|
355 | var itemA = a[i];
|
356 | var itemB = b[i];
|
357 | var lenItemA = itemA ? itemA.length : -1;
|
358 | var lenItemB = itemB ? itemB.length : -1;
|
359 | if (lenItemA !== lenItemB) return false;
|
360 | for (var j = 0; j < lenItemB; j++) {
|
361 | if (itemA[j] !== itemB[j]) return false;
|
362 | }
|
363 | }
|
364 | return true;
|
365 | }
|
366 |
|
367 | function patchTextBinding(binding) {
|
368 | if (
|
369 | binding instanceof templates.AttributeBinding &&
|
370 | binding.name === 'value' &&
|
371 | (binding.element.tagName === 'INPUT' || binding.element.tagName === 'TEXTAREA') &&
|
372 | documentListeners.inputSupportsSelection(binding.element) &&
|
373 | binding.template.expression.resolve(binding.context)
|
374 | ) {
|
375 | binding.update = textInputUpdate;
|
376 | }
|
377 | }
|
378 |
|
379 | function textInputUpdate(previous, pass) {
|
380 | textUpdate(this, this.element, previous, pass);
|
381 | }
|
382 | function textUpdate(binding, element, previous, pass) {
|
383 | if (pass) {
|
384 | if (pass.$event && pass.$event.target === element) {
|
385 | return;
|
386 | } else if (pass.$stringInsert) {
|
387 | return textDiff.onStringInsert(
|
388 | element,
|
389 | previous,
|
390 | pass.$stringInsert.index,
|
391 | pass.$stringInsert.text
|
392 | );
|
393 | } else if (pass.$stringRemove) {
|
394 | return textDiff.onStringRemove(
|
395 | element,
|
396 | previous,
|
397 | pass.$stringRemove.index,
|
398 | pass.$stringRemove.howMany
|
399 | );
|
400 | }
|
401 | }
|
402 | binding.template.update(binding.context, binding);
|
403 | }
|
404 |
|
405 | util.serverRequire(module, './Page.server');
|