UNPKG

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
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); }