/** * @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 | Controller} component */ Controller.prototype.add = function (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; }; /** * Request a reflow. The controller will schedule a reflow */ Controller.prototype.requestReflow = function () { 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 */ Controller.prototype.requestRepaint = function () { if (!this.repaintTimer) { var me = this; this.repaintTimer = setTimeout(function () { me.repaintTimer = undefined; me.repaint(); }, 0); } }; /** * Repaint all components */ Controller.prototype.repaint = function () { 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 () { 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 };