UNPKG

11.4 kBJavaScriptView Raw
1var derbyTemplates = require('derby-templates');
2var contexts = derbyTemplates.contexts;
3var expressions = derbyTemplates.expressions;
4var templates = derbyTemplates.templates;
5var DependencyOptions = derbyTemplates.options.DependencyOptions;
6var util = require('racer/lib/util');
7var components = require('./components');
8var EventModel = require('./eventmodel');
9var textDiff = require('./textDiff');
10var Controller = require('./Controller');
11var documentListeners = require('./documentListeners');
12
13module.exports = Page;
14
15function 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
28util.mergeInto(Page.prototype, Controller.prototype);
29
30Page.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
41Page.prototype.$preventDefault = function(e) {
42 e.preventDefault();
43};
44
45Page.prototype.$stopPropagation = function(e) {
46 e.stopPropagation();
47};
48
49Page.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
56Page.prototype._setRenderPrefix = function(ns) {
57 var prefix = (ns) ? ns + ':' : '';
58 this.model.set('$render.prefix', prefix);
59};
60
61Page.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
67Page.prototype.getFragment = function(viewName, ns) {
68 this._setRenderPrefix(ns);
69 var view = this.getView(viewName, ns);
70 return view.getFragment(this.context);
71};
72
73Page.prototype.getView = function(viewName, ns) {
74 return this.app.views.find(viewName, ns);
75};
76
77Page.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
91Page.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
103Page.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
112Page.prototype._addListeners = function() {
113 var eventModel = this._eventModel = new EventModel();
114 this._addModelListeners(eventModel);
115 this._addContextListeners(eventModel);
116};
117
118Page.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 // Remove all data, refs, listeners, and reactive functions
126 // for the previous page
127 var silentModel = this.model.silent();
128 silentModel.destroy('_page');
129 silentModel.destroy('$components');
130 // Unfetch and unsubscribe from all queries and documents
131 silentModel.unloadAll && silentModel.unloadAll();
132};
133
134Page.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 // The pass parameter is passed in for special handling of updates
142 // resulting from stringInsert or stringRemove
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
176Page.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 // TODO
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
223function addDependencies(eventModel, expression, binding) {
224 var bindingWrapper = new BindingWrapper(eventModel, expression, binding);
225 bindingWrapper.updateDependencies();
226}
227
228// The code here uses object-based set pattern where objects are keyed using
229// sequentially generated IDs.
230var nextId = 1;
231function 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}
250BindingWrapper.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 // Do nothing if dependencies haven't changed
259 if (equalDependencies(this.dependencies, dependencies)) return;
260 // Otherwise, remove current dependencies
261 this.eventModel.removeBinding(this);
262 }
263 // Add new dependencies
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};
271BindingWrapper.prototype.update = function(previous, pass) {
272 this.binding.update(previous, pass);
273 this.updateDependencies();
274};
275BindingWrapper.prototype.insert = function(index, howMany) {
276 this.binding.insert(index, howMany);
277 this.updateDependencies();
278};
279BindingWrapper.prototype.remove = function(index, howMany) {
280 this.binding.remove(index, howMany);
281 this.updateDependencies();
282};
283BindingWrapper.prototype.move = function(from, to, howMany) {
284 this.binding.move(from, to, howMany);
285 this.updateDependencies();
286};
287
288function 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
305function 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
317function textInputUpdate(previous, pass) {
318 textUpdate(this, this.element, previous, pass);
319}
320function 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
343util.serverRequire(module, './Page.server');