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 | var context = this.context;
|
139 | var changeListener = model.on('change', '**', function onChange(path, value, previous, pass) {
|
140 | var segments = util.castSegments(path.split('.'));
|
141 |
|
142 |
|
143 | eventModel.set(segments, previous, pass);
|
144 | });
|
145 | var loadListener = model.on('load', '**', function onLoad(path) {
|
146 | var segments = util.castSegments(path.split('.'));
|
147 | eventModel.set(segments);
|
148 | });
|
149 | var unloadListener = model.on('unload', '**', function onUnload(path) {
|
150 | var segments = util.castSegments(path.split('.'));
|
151 | eventModel.set(segments);
|
152 | });
|
153 | var insertListener = model.on('insert', '**', function onInsert(path, index, values) {
|
154 | var segments = util.castSegments(path.split('.'));
|
155 | eventModel.insert(segments, index, values.length);
|
156 | });
|
157 | var removeListener = model.on('remove', '**', function onRemove(path, index, values) {
|
158 | var segments = util.castSegments(path.split('.'));
|
159 | eventModel.remove(segments, index, values.length);
|
160 | });
|
161 | var moveListener = model.on('move', '**', function onMove(path, from, to, howMany) {
|
162 | var segments = util.castSegments(path.split('.'));
|
163 | eventModel.move(segments, from, to, howMany);
|
164 | });
|
165 |
|
166 | this._removeModelListeners = function() {
|
167 | model.removeListener('change', changeListener);
|
168 | model.removeListener('load', loadListener);
|
169 | model.removeListener('unload', unloadListener);
|
170 | model.removeListener('insert', insertListener);
|
171 | model.removeListener('remove', removeListener);
|
172 | model.removeListener('move', moveListener);
|
173 | };
|
174 | };
|
175 |
|
176 | Page.prototype._addContextListeners = function(eventModel) {
|
177 | this.context.meta.addBinding = addBinding;
|
178 | this.context.meta.removeBinding = removeBinding;
|
179 | this.context.meta.removeNode = removeNode;
|
180 | this.context.meta.addItemContext = addItemContext;
|
181 | this.context.meta.removeItemContext = removeItemContext;
|
182 |
|
183 | function addItemContext(context) {
|
184 | var segments = context.expression.resolve(context);
|
185 | eventModel.addItemContext(segments, context);
|
186 | }
|
187 | function removeItemContext(context) {
|
188 |
|
189 | }
|
190 | function addBinding(binding) {
|
191 | patchTextBinding(binding);
|
192 | var expressions = binding.template.expressions;
|
193 | if (expressions) {
|
194 | for (var i = 0, len = expressions.length; i < len; i++) {
|
195 | addDependencies(eventModel, expressions[i], binding);
|
196 | }
|
197 | } else {
|
198 | var expression = binding.template.expression;
|
199 | addDependencies(eventModel, expression, binding);
|
200 | }
|
201 | }
|
202 | function removeBinding(binding) {
|
203 | var bindingWrappers = binding.meta;
|
204 | if (!bindingWrappers) return;
|
205 | for (var i = bindingWrappers.length; i--;) {
|
206 | eventModel.removeBinding(bindingWrappers[i]);
|
207 | }
|
208 | }
|
209 | function removeNode(node) {
|
210 | var component = node.$component;
|
211 | if (component && !component.singleton) {
|
212 | component.destroy();
|
213 | }
|
214 | var destroyListeners = node.$destroyListeners;
|
215 | if (destroyListeners) {
|
216 | for (var i = 0; i < destroyListeners.length; i++) {
|
217 | destroyListeners[i]();
|
218 | }
|
219 | }
|
220 | }
|
221 | };
|
222 |
|
223 | function addDependencies(eventModel, expression, binding) {
|
224 | var bindingWrapper = new BindingWrapper(eventModel, expression, binding);
|
225 | bindingWrapper.updateDependencies();
|
226 | }
|
227 |
|
228 |
|
229 |
|
230 | var nextId = 1;
|
231 | function BindingWrapper(eventModel, expression, binding) {
|
232 | this.eventModel = eventModel;
|
233 | this.expression = expression;
|
234 | this.binding = binding;
|
235 | this.id = nextId++;
|
236 | this.eventModels = null;
|
237 | this.dependencies = null;
|
238 | this.ignoreTemplateDependency = (
|
239 | binding instanceof components.ComponentAttributeBinding
|
240 | ) || (
|
241 | (binding.template instanceof templates.DynamicText) &&
|
242 | (binding instanceof templates.RangeBinding)
|
243 | );
|
244 | if (binding.meta) {
|
245 | binding.meta.push(this);
|
246 | } else {
|
247 | binding.meta = [this];
|
248 | }
|
249 | }
|
250 | BindingWrapper.prototype.updateDependencies = function() {
|
251 | var dependencyOptions;
|
252 | if (this.ignoreTemplateDependency && this.binding.condition instanceof templates.Template) {
|
253 | dependencyOptions = new DependencyOptions();
|
254 | dependencyOptions.ignoreTemplate = this.binding.condition;
|
255 | }
|
256 | var dependencies = this.expression.dependencies(this.binding.context, dependencyOptions);
|
257 | if (this.dependencies) {
|
258 |
|
259 | if (equalDependencies(this.dependencies, dependencies)) return;
|
260 |
|
261 | this.eventModel.removeBinding(this);
|
262 | }
|
263 |
|
264 | if (!dependencies) return;
|
265 | this.dependencies = dependencies;
|
266 | for (var i = 0, len = dependencies.length; i < len; i++) {
|
267 | var dependency = dependencies[i];
|
268 | if (dependency) this.eventModel.addBinding(dependency, this);
|
269 | }
|
270 | };
|
271 | BindingWrapper.prototype.update = function(previous, pass) {
|
272 | this.binding.update(previous, pass);
|
273 | this.updateDependencies();
|
274 | };
|
275 | BindingWrapper.prototype.insert = function(index, howMany) {
|
276 | this.binding.insert(index, howMany);
|
277 | this.updateDependencies();
|
278 | };
|
279 | BindingWrapper.prototype.remove = function(index, howMany) {
|
280 | this.binding.remove(index, howMany);
|
281 | this.updateDependencies();
|
282 | };
|
283 | BindingWrapper.prototype.move = function(from, to, howMany) {
|
284 | this.binding.move(from, to, howMany);
|
285 | this.updateDependencies();
|
286 | };
|
287 |
|
288 | function equalDependencies(a, b) {
|
289 | var lenA = a ? a.length : -1;
|
290 | var lenB = b ? b.length : -1;
|
291 | if (lenA !== lenB) return false;
|
292 | for (var i = 0; i < lenA; i++) {
|
293 | var itemA = a[i];
|
294 | var itemB = b[i];
|
295 | var lenItemA = itemA ? itemA.length : -1;
|
296 | var lenItemB = itemB ? itemB.length : -1;
|
297 | if (lenItemA !== lenItemB) return false;
|
298 | for (var j = 0; j < lenItemB; j++) {
|
299 | if (itemA[j] !== itemB[j]) return false;
|
300 | }
|
301 | }
|
302 | return true;
|
303 | }
|
304 |
|
305 | function patchTextBinding(binding) {
|
306 | if (
|
307 | binding instanceof templates.AttributeBinding &&
|
308 | binding.name === 'value' &&
|
309 | (binding.element.tagName === 'INPUT' || binding.element.tagName === 'TEXTAREA') &&
|
310 | documentListeners.inputSupportsSelection(binding.element) &&
|
311 | binding.template.expression.resolve(binding.context)
|
312 | ) {
|
313 | binding.update = textInputUpdate;
|
314 | }
|
315 | }
|
316 |
|
317 | function textInputUpdate(previous, pass) {
|
318 | textUpdate(this, this.element, previous, pass);
|
319 | }
|
320 | function textUpdate(binding, element, previous, pass) {
|
321 | if (pass) {
|
322 | if (pass.$event && pass.$event.target === element) {
|
323 | return;
|
324 | } else if (pass.$stringInsert) {
|
325 | return textDiff.onStringInsert(
|
326 | element,
|
327 | previous,
|
328 | pass.$stringInsert.index,
|
329 | pass.$stringInsert.text
|
330 | );
|
331 | } else if (pass.$stringRemove) {
|
332 | return textDiff.onStringRemove(
|
333 | element,
|
334 | previous,
|
335 | pass.$stringRemove.index,
|
336 | pass.$stringRemove.howMany
|
337 | );
|
338 | }
|
339 | }
|
340 | binding.template.update(binding.context, binding);
|
341 | }
|
342 |
|
343 | util.serverRequire(module, './Page.server');
|