derby
Version:
MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.
350 lines (349 loc) • 15 kB
JavaScript
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.BindingWrapper = exports.PageForClient = exports.Page = void 0;
var racer_1 = require("racer");
var components = require("./components");
var Controller_1 = require("./Controller");
var eventmodel_1 = require("./eventmodel");
var derbyTemplates = require("./templates");
var documentListeners = require("./documentListeners");
var textDiff = require("./textDiff");
var contexts = derbyTemplates.contexts, DependencyOptions = derbyTemplates.DependencyOptions, expressions = derbyTemplates.expressions, templates = derbyTemplates.templates;
var Page = /** @class */ (function (_super) {
__extends(Page, _super);
function Page(app, model) {
var _this = _super.call(this, app, null, model) || this;
_this._removeModelListeners = function () { };
_this.params = null;
_this._eventModel = null;
_this._removeModelListeners = function () { };
_this._components = {};
if (_this.init)
_this.init(model);
_this.context = _this._createContext();
_this.page = _this;
return _this;
}
Page.prototype.$bodyClass = function (ns) {
if (!ns)
return;
var classNames = [];
var segments = ns.split(':');
for (var i = 0, len = segments.length; i < len; i++) {
var className = segments.slice(0, i + 1).join('-');
classNames.push(className);
}
return classNames.join(' ');
};
Page.prototype.get = function (viewName, ns, unescaped) {
this._setRenderPrefix(ns);
var view = this.getView(viewName, ns);
return view.get(this.context, unescaped);
};
Page.prototype.getFragment = function (viewName, ns) {
this._setRenderPrefix(ns);
var view = this.getView(viewName, ns);
return view.getFragment(this.context);
};
Page.prototype.getView = function (viewName, ns) {
return this.app.views.find(viewName, ns);
};
Page.prototype.destroy = function () {
this.emit('destroy');
this._removeModelListeners();
for (var id in this._components) {
var component = this._components[id];
component.destroy();
}
// Remove all data, refs, listeners, and reactive functions
// for the previous page
var silentModel = this.model.silent();
silentModel.destroy('_page');
silentModel.destroy('$components');
// Unfetch and unsubscribe from all queries and documents
if (silentModel.unloadAll) {
silentModel.unloadAll();
}
};
Page.prototype.render = function (_ns, _status) { };
Page.prototype._createContext = function () {
var contextMeta = new contexts.ContextMeta();
contextMeta.views = this.app && this.app.views;
var context = new contexts.Context(contextMeta, this);
context.expression = new expressions.PathExpression([]);
context.alias = '#root';
return context;
};
Page.prototype._setRenderPrefix = function (ns) {
var prefix = (ns) ? ns + ':' : '';
this.model.set('$render.prefix', prefix);
};
Page.prototype._setRenderParams = function (ns) {
this.model.set('$render.ns', ns);
this.model.set('$render.params', this.params);
this.model.set('$render.url', this.params && this.params.url);
this.model.set('$render.query', this.params && this.params.query);
};
return Page;
}(Controller_1.Controller));
exports.Page = Page;
var PageForClient = /** @class */ (function (_super) {
__extends(PageForClient, _super);
function PageForClient(app, model) {
var _this = _super.call(this, app, model) || this;
_this._addListeners();
return _this;
}
PageForClient.prototype.$preventDefault = function (e) {
e.preventDefault();
};
PageForClient.prototype.$stopPropagation = function (e) {
e.stopPropagation();
};
PageForClient.prototype.attach = function () {
this.context.pause();
var ns = this.model.get('$render.ns');
var titleView = this.getView('TitleElement', ns);
var bodyView = this.getView('BodyElement', ns);
var titleElement = document.getElementsByTagName('title')[0];
titleView.attachTo(titleElement.parentNode, titleElement, this.context);
bodyView.attachTo(document.body.parentNode, document.body, this.context);
this.context.unpause();
if (this.create) {
this.create(this.model, this.dom);
}
};
PageForClient.prototype.render = function (ns, _status) {
this.app.emit('render', this);
this.context.pause();
this._setRenderParams(ns);
var titleFragment = this.getFragment('TitleElement', ns);
var bodyFragment = this.getFragment('BodyElement', ns);
var titleElement = document.getElementsByTagName('title')[0];
titleElement.parentNode.replaceChild(titleFragment, titleElement);
document.body.parentNode.replaceChild(bodyFragment, document.body);
this.context.unpause();
if (this.create) {
this.create(this.model, this.dom);
}
this.app.emit('routeDone', this, 'render');
};
PageForClient.prototype._addListeners = function () {
var eventModel = this._eventModel = new eventmodel_1.EventModel();
this._addModelListeners(eventModel);
this._addContextListeners(eventModel);
};
PageForClient.prototype._addModelListeners = function (eventModel) {
var model = this.model;
if (!model)
return;
// Registering model listeners with the *Immediate events helps to prevent
// a bug with binding updates where a model listener causes a change to the
// path being listened on, directly or indirectly.
// `util.castSegments(segments)` is needed to cast string segments into
// numbers, since EventModel#child does typeof checks against segments. This
// could be done once in Racer's Model#emit, instead of in every listener.
var changeListener = model.on('changeImmediate', function onChange(segments, event) {
// The pass parameter is passed in for special handling of updates
// resulting from stringInsert or stringRemove
eventModel.set(racer_1.util.castSegments(segments), event.previous, event.passed);
});
var loadListener = model.on('loadImmediate', function onLoad(segments) {
eventModel.set(racer_1.util.castSegments(segments));
});
var unloadListener = model.on('unloadImmediate', function onUnload(segments, event) {
eventModel.set(racer_1.util.castSegments(segments), event.previous);
});
var insertListener = model.on('insertImmediate', function onInsert(segments, event) {
eventModel.insert(racer_1.util.castSegments(segments), event.index, event.values.length);
});
var removeListener = model.on('removeImmediate', function onRemove(segments, event) {
eventModel.remove(racer_1.util.castSegments(segments), event.index, event.values.length);
});
var moveListener = model.on('moveImmediate', function onMove(segments, event) {
eventModel.move(racer_1.util.castSegments(segments), event.from, event.to, event.howMany);
});
this._removeModelListeners = function () {
model.removeListener('changeImmediate', changeListener);
model.removeListener('loadImmediate', loadListener);
model.removeListener('unloadImmediate', unloadListener);
model.removeListener('insertImmediate', insertListener);
model.removeListener('removeImmediate', removeListener);
model.removeListener('moveImmediate', moveListener);
};
};
PageForClient.prototype._addContextListeners = function (eventModel) {
this.context.meta.addBinding = addBinding;
this.context.meta.removeBinding = removeBinding;
this.context.meta.removeNode = removeNode;
this.context.meta.addItemContext = addItemContext;
this.context.meta.removeItemContext = removeItemContext;
function addItemContext(context) {
var segments = context.expression.resolve(context);
eventModel.addItemContext(segments, context);
}
function removeItemContext(_context) {
// TODO
}
function addBinding(binding) {
patchTextBinding(binding);
var expressions = binding.template.expressions;
if (expressions) {
for (var i = 0, len = expressions.length; i < len; i++) {
addDependencies(eventModel, expressions[i], binding);
}
}
else {
var expression = binding.template.expression;
addDependencies(eventModel, expression, binding);
}
}
function removeBinding(binding) {
var bindingWrappers = binding.meta;
if (!bindingWrappers)
return;
for (var i = bindingWrappers.length; i--;) {
eventModel.removeBinding(bindingWrappers[i]);
}
}
function removeNode(node) {
var component = node.$component;
if (component)
component.destroy();
var destroyListeners = node.$destroyListeners;
if (destroyListeners) {
for (var i = 0; i < destroyListeners.length; i++) {
destroyListeners[i]();
}
}
}
};
return PageForClient;
}(Page));
exports.PageForClient = PageForClient;
function addDependencies(eventModel, expression, binding) {
var bindingWrapper = new BindingWrapper(eventModel, expression, binding);
bindingWrapper.updateDependencies();
}
// The code here uses object-based set pattern where objects are keyed using
// sequentially generated IDs.
var nextId = 1;
var BindingWrapper = /** @class */ (function () {
function BindingWrapper(eventModel, expression, binding) {
this.updateDependencies = function () {
var dependencyOptions;
if (this.ignoreTemplateDependency && this.binding.condition instanceof templates.Template) {
dependencyOptions = new DependencyOptions();
dependencyOptions.setIgnoreTemplate(this.binding.condition);
}
var dependencies = this.expression.dependencies(this.binding.context, dependencyOptions);
if (this.dependencies) {
// Do nothing if dependencies haven't changed
if (equalDependencies(this.dependencies, dependencies))
return;
// Otherwise, remove current dependencies
this.eventModel.removeBinding(this);
}
// Add new dependencies
if (!dependencies)
return;
this.dependencies = dependencies;
for (var i = 0, len = dependencies.length; i < len; i++) {
var dependency = dependencies[i];
if (dependency)
this.eventModel.addBinding(dependency, this);
}
};
this.update = function (previous, pass) {
this.binding.update(previous, pass);
this.updateDependencies();
};
this.insert = function (index, howMany) {
this.binding.insert(index, howMany);
this.updateDependencies();
};
this.remove = function (index, howMany) {
this.binding.remove(index, howMany);
this.updateDependencies();
};
this.move = function (from, to, howMany) {
this.binding.move(from, to, howMany);
this.updateDependencies();
};
this.eventModel = eventModel;
this.expression = expression;
this.binding = binding;
this.id = nextId++;
this.eventModels = null;
this.dependencies = null;
this.ignoreTemplateDependency = (binding instanceof components.ComponentAttributeBinding) || ((binding.template instanceof templates.DynamicText) &&
(binding instanceof templates.RangeBinding));
if (binding.meta) {
binding.meta.push(this);
}
else {
binding.meta = [this];
}
}
return BindingWrapper;
}());
exports.BindingWrapper = BindingWrapper;
function equalDependencies(a, b) {
var lenA = a ? a.length : -1;
var lenB = b ? b.length : -1;
if (lenA !== lenB)
return false;
for (var i = 0; i < lenA; i++) {
var itemA = a[i];
var itemB = b[i];
var lenItemA = itemA ? itemA.length : -1;
var lenItemB = itemB ? itemB.length : -1;
if (lenItemA !== lenItemB)
return false;
for (var j = 0; j < lenItemB; j++) {
if (itemA[j] !== itemB[j])
return false;
}
}
return true;
}
function patchTextBinding(binding) {
if (binding instanceof templates.AttributeBinding &&
binding.name === 'value' &&
(binding.element.tagName === 'INPUT' || binding.element.tagName === 'TEXTAREA') &&
documentListeners.inputSupportsSelection(binding.element) &&
binding.template.expression.resolve(binding.context)) {
binding.update = textInputUpdate;
}
}
function textInputUpdate(previous, pass) {
textUpdate(this, this.element, previous, pass);
}
function textUpdate(binding, element, previous, pass) {
if (pass) {
if (pass.$event && pass.$event.target === element) {
return;
}
else if (pass.$stringInsert) {
return textDiff.onStringInsert(element, previous, pass.$stringInsert.index, pass.$stringInsert.text);
}
else if (pass.$stringRemove) {
return textDiff.onStringRemove(element, previous, pass.$stringRemove.index, pass.$stringRemove.howMany);
}
}
binding.template.update(binding.context, binding);
}