/** * @constructor Controller * * A Controller controls the reflows and repaints of all visual components */ function Controller () { this.id = util.randomUUID(); this.components = {}; this.repaintTimer = undefined; this.reflowTimer = undefined; } /** * Add a component to the controller * @param {Component} component */ Controller.prototype.add = function add(component) { // validate the component if (component.id == undefined) { throw new Error('Component has no field id'); } if (!(component instanceof Component) && !(component instanceof Controller)) { throw new TypeError('Component must be an instance of ' + 'prototype Component or Controller'); } // add the component component.controller = this; this.components[component.id] = component; }; /** * Remove a component from the controller * @param {Component | String} component */ Controller.prototype.remove = function remove(component) { var id; for (id in this.components) { if (this.components.hasOwnProperty(id)) { if (id == component || this.components[id] == component) { break; } } } if (id) { delete this.components[id]; } }; /** * Request a reflow. The controller will schedule a reflow * @param {Boolean} [force] If true, an immediate reflow is forced. Default * is false. */ Controller.prototype.requestReflow = function requestReflow(force) { if (force) { this.reflow(); } else { if (!this.reflowTimer) { var me = this; this.reflowTimer = setTimeout(function () { me.reflowTimer = undefined; me.reflow(); }, 0); } } }; /** * Request a repaint. The controller will schedule a repaint * @param {Boolean} [force] If true, an immediate repaint is forced. Default * is false. */ Controller.prototype.requestRepaint = function requestRepaint(force) { if (force) { this.repaint(); } else { if (!this.repaintTimer) { var me = this; this.repaintTimer = setTimeout(function () { me.repaintTimer = undefined; me.repaint(); }, 0); } } }; /** * Repaint all components */ Controller.prototype.repaint = function repaint() { var changed = false; // cancel any running repaint request if (this.repaintTimer) { clearTimeout(this.repaintTimer); this.repaintTimer = undefined; } var done = {}; function repaint(component, id) { if (!(id in done)) { // first repaint the components on which this component is dependent if (component.depends) { component.depends.forEach(function (dep) { repaint(dep, dep.id); }); } if (component.parent) { repaint(component.parent, component.parent.id); } // repaint the component itself and mark as done changed = component.repaint() || changed; done[id] = true; } } util.forEach(this.components, repaint); // immediately reflow when needed if (changed) { this.reflow(); } // TODO: limit the number of nested reflows/repaints, prevent loop }; /** * Reflow all components */ Controller.prototype.reflow = function reflow() { var resized = false; // cancel any running repaint request if (this.reflowTimer) { clearTimeout(this.reflowTimer); this.reflowTimer = undefined; } var done = {}; function reflow(component, id) { if (!(id in done)) { // first reflow the components on which this component is dependent if (component.depends) { component.depends.forEach(function (dep) { reflow(dep, dep.id); }); } if (component.parent) { reflow(component.parent, component.parent.id); } // reflow the component itself and mark as done resized = component.reflow() || resized; done[id] = true; } } util.forEach(this.components, reflow); // immediately repaint when needed if (resized) { this.repaint(); } // TODO: limit the number of nested reflows/repaints, prevent loop };