UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

481 lines (417 loc) 13.8 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2008 Derrell Lipman License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Derrell Lipman (derrell) ************************************************************************ */ /** * <i>Progressive</i>. * * Follow progressive instructions provided by a data model. A variable * number of instructions are executed at one time, after which control is * returned briefly to the browser. This allows browser rendering between * batches of instructions, improving the visual experience. * * <i>Progressive</i> may be used for various purposes. Two predefined * purposes for which "renderers" are provided, are a progressively-rendered * table which allows variable row height, and a program load/initialization * renderer with progress bar. (Note that the term "renderer" is interpreted * quite broadly. A renderer needn't actually render; rather it is just some * set of activities that takes place at one time, e.g a row of table data or * a single widget added to the document or a sending a request to a server, * etc.) */ qx.Class.define("qx.ui.progressive.Progressive", { extend : qx.ui.container.Composite, /** * @param structure {qx.ui.progressive.structure.Abstract} * The structure of the Progressive pane. */ construct : function(structure) { this.base(arguments, new qx.ui.layout.VBox()); // Create an object in which we'll track renderers that have been added this.__renderer = { }; // Prepare to have our pane structure added to us. this.set( { backgroundColor : "white" }); // If no structure is provided... if (! structure) { // ... then create a default one. structure = new qx.ui.progressive.structure.Default(); } // Prepare our pane structure this.__structure = structure; structure.applyStructure(this); // We've not yet done our initial render this.__bInitialRenderComplete = false; // We're not currently rendering this.__bRendering = false; // Number of elements available to be rendered. Useful for progress // handlers, e.g. a progress bar or status counter. this.__initialNumElements = 0; }, events : { /** * Event fired when rendering begins. * * The event data is an object with the following members: * <dl> * <dt>state</dt> * <dd> * The state object. * </dd> * * <dt>initial</dt> * The number of elements that are available to be rendered * <dd> * </dd> * </dl> */ "renderStart" : "qx.event.type.Data", /** * Event fired when rendering ends. The data is the state object. */ "renderEnd" : "qx.event.type.Data", /** * This event is fired after each batch of elements is rendered, and * control is about to be yielded to the browser. This is an appropriate * event to listen for, to implement a progress bar. * * The event data is an object with the following members: * <dl> * <dt>initial</dt> * <dd> * The number of elements that were available at the start of this * rendering request. * </dd> * * <dt>remaining</dt> * <dd> * The number of elements still remaining to be rendered. * </dd> * </dl> */ "progress" : "qx.event.type.Data", /** * This event is fired after each element is rendered. * * The event data is an object with the following members: * <dl> * <dt>initial</dt> * <dd> * The number of elements that were available at the start of this * rendering request. * </dd> * * <dt>remaining</dt> * <dd> * The number of elements still remaining to be rendered. * </dd> * * <dt>element</dt> * <dd> * The object, returned by the data model's getNextElement() method, * that was just rendered. * </dd> * </dl> * * Note: Unless batchSize is set to 1 or we happen to be at the end of a * batch, widgets will not be rendered at this time. Use this event * for programmatically processing rendered elements, but not for * such things as progress bars. Instead, where only user-visible * changes such as progress bars are being updated, use the * "progress" event. */ "progressDetail" : "qx.event.type.Data" }, properties : { /** The data model. */ dataModel : { check : "qx.ui.progressive.model.Abstract", apply : "_applyDataModel" }, /** * Number of elements to render at one time. After this number of * elements has been rendered, control will be yielded to the browser * allowing the elements to actually be displayed. A short-interval timer * will be set, to regain control to render the next batch of elements. */ batchSize : { check : "Integer", init : 20 }, /** * Flush the widget queue after each batch is rendered. This is * particularly relevant for such things as progressive loading, where * the whole purpose is to be able to see the loading progressing. */ flushWidgetQueueAfterBatch : { check : "Boolean", init : false }, /** * Delay between rendering elements. Zero is normally adequate, but * there are times that the user wants more time between rendering * some elements. */ interElementTimeout : { check: "Integer", init : 0 } }, members : { __renderer : null, __bRendering : null, __t1 : null, __initialNumElements : null, __bInitialRenderComplete : null, __structure : null, /** * Return the structure object * * @return {qx.ui.progressive.structure.Abstract} The structure object */ getStructure : function() { return this.__structure; }, /** * Add a renderer that can be referenced by the data model. * * @param name {String} * Name referenced in the data model when this renderer is to be used. * * @param renderer {qx.ui.progressive.renderer.Abstract} * Renderer object used if the data model references the specified name. * */ addRenderer : function(name, renderer) { this.__renderer[name] = renderer; renderer.join(this, name); }, /** * Remove a previously added renderer. * * @param name {String} * Remove the renderer which was assigned this name. * */ removeRenderer : function(name) { if (! this.__renderer[name]) { throw new Error("No existing renderer named " + name); } delete this.__renderer[name]; }, /** * Render the elements available from the data model. Elements are * rendered in batches of size {@link #batchSize}. After each batch of * elements are rendered, control is returned temporarily to the * browser, so that actual screen updates can take place. A timer is * used to regain control a short while later, in order to render the * next batch of element. * */ render : function() { // Prevent render calls while we're already rendering if (this.__bRendering) { return; } this.__bRendering = true; var state = new qx.ui.progressive.State( { progressive : this, model : this.getDataModel(), pane : this.__structure.getPane(), batchSize : this.getBatchSize(), rendererData : this.__createStateRendererData(), userData : { } }); // Record render start time this.__t1 = new Date(); // Render the first batch of elements. Subsequent batches will be via // timer started from this.__renderElementBatch(). if (this.__bInitialRenderComplete) { // Get the starting number of elements this.__initialNumElements = state.getModel().getElementCount(); // Let listeners know we're beginning to render this.fireDataEvent("renderStart", { state : state, initial : this.__initialNumElements }); // Begin rendering this.__renderElementBatch(state); } else { // Ensure we leave enough time that 'this' has been rendered, so that // this.getContentElement().getDomElement() is valid and has // properties. It's needed by some renderers. // // FIXME: Why isn't an event listener for "appear" an adequate delay??? // (It's done with a timer like this in Table's Pane too.) qx.event.Timer.once(function() { this.__initialNumElements = state.getModel().getElementCount(); this.fireDataEvent( "renderStart", { state : state, initial : this.__initialNumElements }); this.__renderElementBatch(state); this.__bInitialRenderComplete = true; }, this, 10); } }, /** * Called when the dataModel property is changed. * * @param value {qx.ui.progressive.model.Abstract} * The new data model. * * @param old {qx.ui.progressive.model.Abstract} * The old data model. * */ _applyDataModel : function(value, old) { if (old) { // Remove the old event listener old.removeListener("dataAvailable", this.__dataAvailable, this); // Dispose the old model old.dispose(); } // Add an event listener so we know when data is available in the model value.addListener("dataAvailable", this.__dataAvailable, this); }, /** * Render a batch of elements. The batch size is determined by the * Progressive's batch size at the time that rendering began. That batch * size was copied into the {@link qx.ui.progressive.State} object and is * used herein. * * @param state {qx.ui.progressive.State} * The current state of rendering. * */ __renderElementBatch : function(state) { var current; var element; var renderer; for (var i = state.getBatchSize(); i > 0; i--) { // Retrieve the current element current = state.getModel().getNextElement(); if (! current) { // No more elements. We're done. this.debug("Render time: " + (new Date() - this.__t1) + "ms"); this.__bRendering = false; // Notify any progress handlers that are listening this.fireDataEvent("renderEnd", state); // We don't need our render state any longer state.dispose(); // See ya! return; } // Get the element member element = current.element; // Get the element's renderer renderer = this.__renderer[element.renderer]; // Render this element renderer.render(state, element); // Notify any progress detail handlers that are listening this.fireDataEvent("progressDetail", { initial : this.__initialNumElements, remaining : current.remaining, element : element }); } // Notify any progress handlers that are listening this.fireDataEvent("progress", { initial : this.__initialNumElements, remaining : current.remaining }); // Flush the widget queue if (this.getFlushWidgetQueueAfterBatch()) { qx.ui.core.queue.Manager.flush(); } // Set a timer to render the next element qx.event.Timer.once(function() { this.__renderElementBatch(state); }, this, this.getInterElementTimeout()); }, /** * Create the map of empty objects for use by the renderers. * @return {Map} renderer data map */ __createStateRendererData : function() { var rendererData = { }; for (var name in this.__renderer) { rendererData[name] = { }; } return rendererData; }, /** * Event callback for the "dataAvailable" event. * * @param e {qx.event.type.Data} * A "dataAvailable" event's data contains the initial number of elements * */ __dataAvailable : function(e) { this.__initialNumElements = e.getData(); this.render(); } }, /** */ destruct : function() { // For each renderer... for (var name in this.__renderer) { // ... dispose it this.__renderer[name].dispose(); } // Clean up references this.__t1 = this.__renderer = this.__structure = null; } });