UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

950 lines (765 loc) 22.4 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2010 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Christian Hagendorn (chris_schmidt) ************************************************************************ */ /** * The <code>qx.ui.list.List</code> is based on the virtual infrastructure and * supports filtering, sorting, grouping, single selection, multi selection, * data binding and custom rendering. * * Using the virtual infrastructure has considerable advantages when there is a * huge amount of model items to render because the virtual infrastructure only * creates widgets for visible items and reuses them. This saves both creation * time and memory. * * With the {@link qx.ui.list.core.IListDelegate} interface it is possible * to configure the list's behavior (item and group renderer configuration, * filtering, sorting, grouping, etc.). * * Here's an example of how to use the widget: * <pre class="javascript"> * //create the model data * var rawData = []; * for (var i = 0; i < 2500; i++) { * rawData[i] = "Item No " + i; * } * var model = qx.data.marshal.Json.createModel(rawData); * * //create the list * var list = new qx.ui.list.List(model); * * //configure the lists's behavior * var delegate = { * sorter : function(a, b) { * return a > b ? 1 : a < b ? -1 : 0; * } * }; * list.setDelegate(delegate); * * //Pre-Select "Item No 20" * list.getSelection().push(model.getItem(20)); * * //log selection changes * list.getSelection().addListener("change", function(e) { * this.debug("Selection: " + list.getSelection().getItem(0)); * }, this); * </pre> * * @childControl row-layer {qx.ui.virtual.layer.Row} layer for all rows */ qx.Class.define("qx.ui.list.List", { extend : qx.ui.virtual.core.Scroller, include : [qx.ui.virtual.selection.MModel], implement : qx.data.controller.ISelection, /** * Creates the <code>qx.ui.list.List</code> with the passed model. * * @param model {qx.data.IListData|null?} model for the list. */ construct : function(model) { this.base(arguments, 0, 1, 20, 100); this._init(); this.__defaultGroups = new qx.data.Array(); this.initGroups(this.__defaultGroups); if(model != null) { this.initModel(model); } this.initItemHeight(); }, events : { /** * Fired when the length of {@link #model} changes. */ "changeModelLength" : "qx.event.type.Data" }, properties : { // overridden appearance : { refine : true, init : "virtual-list" }, // overridden focusable : { refine : true, init : true }, // overridden width : { refine : true, init : 100 }, // overridden height : { refine : true, init : 200 }, /** Data array containing the data which should be shown in the list. */ model : { check : "qx.data.IListData", apply : "_applyModel", event: "changeModel", nullable : true, deferredInit : true }, /** Default item height */ itemHeight : { check : "Integer", init : 25, apply : "_applyRowHeight", themeable : true }, /** Group item height */ groupItemHeight : { check : "Integer", init : null, nullable : true, apply : "_applyGroupRowHeight", themeable : true }, /** * The path to the property which holds the information that should be * displayed as a label. This is only needed if objects are stored in the * model. */ labelPath : { check: "String", apply: "_applyLabelPath", nullable: true }, /** * The path to the property which holds the information that should be * displayed as an icon. This is only needed if objects are stored in the * model and icons should be displayed. */ iconPath : { check: "String", apply: "_applyIconPath", nullable: true }, /** * The path to the property which holds the information that should be * displayed as a group label. This is only needed if objects are stored in the * model. */ groupLabelPath : { check: "String", apply: "_applyGroupLabelPath", nullable: true }, /** * A map containing the options for the label binding. The possible keys * can be found in the {@link qx.data.SingleValueBinding} documentation. */ labelOptions : { apply: "_applyLabelOptions", nullable: true }, /** * A map containing the options for the icon binding. The possible keys * can be found in the {@link qx.data.SingleValueBinding} documentation. */ iconOptions : { apply: "_applyIconOptions", nullable: true }, /** * A map containing the options for the group label binding. The possible keys * can be found in the {@link qx.data.SingleValueBinding} documentation. */ groupLabelOptions : { apply: "_applyGroupLabelOptions", nullable: true }, /** * Delegation object which can have one or more functions defined by the * {@link qx.ui.list.core.IListDelegate} interface. */ delegate : { apply: "_applyDelegate", event: "changeDelegate", init: null, nullable: true }, /** * Indicates that the list is managing the {@link #groups} automatically. */ autoGrouping : { check: "Boolean", init: true }, /** * Contains all groups for data binding, but do only manipulate the array * when the {@link #autoGrouping} is set to <code>false</code>. */ groups : { check: "qx.data.Array", event: "changeGroups", nullable: false, deferredInit: true }, /** * Render list items with variable height, * calculated from the individual item size. */ variableItemHeight : { check : "Boolean", apply : "_applyVariableItemHeight", nullable : false, init : true } }, members : { /** @type {qx.ui.virtual.layer.Row} background renderer */ _background : null, /** @type {qx.ui.list.provider.IListProvider} provider for cell rendering */ _provider : null, /** @type {qx.ui.virtual.layer.Abstract} layer which contains the items. */ _layer : null, /** * @type {Array} lookup table to get the model index from a row. To get the * correct value after applying filter, sorter, group. * * Note the value <code>-1</code> indicates that the value is a group item. */ __lookupTable : null, /** @type {Array} lookup table for getting the group index from the row */ __lookupTableForGroup : null, /** * @type {Map} contains all groups with the items as children. The key is * the group name and the value is an <code>Array</code> containing each * item's model index. */ __groupHashMap : null, /** * @type {Boolean} indicates when one or more <code>String</code> are used for grouping. */ __groupStringsUsed : false, /** * @type {Boolean} indicates when one or more <code>Object</code> are used for grouping. */ __groupObjectsUsed : false, /** * @type {Boolean} indicates when a default group is used for grouping. */ __defaultGroupUsed : false, __defaultGroups : null, __deferredLayerUpdate : null, /** * Trigger a rebuild from the internal data structure. */ refresh : function() { this.__buildUpLookupTable(); }, // overridden _createChildControlImpl : function(id, hash) { var control; switch(id) { case "row-layer" : control = new qx.ui.virtual.layer.Row(null, null); break; } return control || this.base(arguments, id); }, /** * Initializes the virtual list. */ _init : function() { this._provider = new qx.ui.list.provider.WidgetProvider(this); this.__lookupTable = []; this.__lookupTableForGroup = []; this.__groupHashMap = {}; this.__groupStringsUsed = false; this.__groupObjectsUsed = false; this.__defaultGroupUsed = false; this.getPane().addListener("resize", this._onResize, this); this._initBackground(); this._initLayer(); }, /** * Initializes the background renderer. */ _initBackground : function() { this._background = this.getChildControl("row-layer"); this.getPane().addLayer(this._background); }, /** * Initializes the layer for rendering. */ _initLayer : function() { this._layer = this._provider.createLayer(); this._layer.addListener("updated", this._onLayerUpdated, this); this.getPane().addLayer(this._layer); }, /* --------------------------------------------------------------------------- INTERNAL API --------------------------------------------------------------------------- */ /** * Returns the model data for the given row. * * @param row {Integer} row to get data for. * @return {var|null} the row's model data. */ _getDataFromRow : function(row) { var data = null; var model = this.getModel(); if (model == null) { return null; } if (this._isGroup(row)) { data = this.getGroups().getItem(this._lookupGroup(row)); } else { data = model.getItem(this._lookup(row)); } if (data != null) { return data; } else { return null; } }, /** * Return the internal lookup table. But do not manipulate the * lookup table! * * @return {Array} The internal lookup table. */ _getLookupTable : function() { return this.__lookupTable; }, /** * Performs a lookup from row to model index. * * @param row {Number} The row to look at. * @return {Number} The model index or * <code>-1</code> if the row is a group item. */ _lookup : function(row) { return this.__lookupTable[row]; }, /** * Performs a lookup from row to group index. * * @param row {Number} The row to look at. * @return {Number} The group index or * <code>-1</code> if the row is a not a group item. */ _lookupGroup : function(row) { return this.__lookupTableForGroup.indexOf(row); }, /** * Performs a lookup from model index to row. * * @param index {Number} The index to look at. * @return {Number} The row or <code>-1</code> * if the index is not a model index. */ _reverseLookup : function(index) { if (index < 0) { return -1; } return this.__lookupTable.indexOf(index); }, /** * Checks if the passed row is a group or an item. * * @param row {Integer} row to check. * @return {Boolean} <code>true</code> if the row is a group element, * <code>false</code> if the row is an item element. */ _isGroup : function(row) { return this._lookup(row) == -1; }, /** * Returns the selectable model items. * * @return {qx.data.Array | null} The selectable items. */ _getSelectables : function() { return this.getModel(); }, /* --------------------------------------------------------------------------- APPLY ROUTINES --------------------------------------------------------------------------- */ // apply method _applyModel : function(value, old) { if (value != null) { value.addListener("changeLength", this._onModelChange, this); } if (old != null) { old.removeListener("changeLength", this._onModelChange, this); } this._onModelChange(); }, // apply method _applyRowHeight : function(value, old) { this.getPane().getRowConfig().setDefaultItemSize(value); }, // apply method _applyGroupRowHeight : function(value, old) { this.__updateGroupRowHeight(); }, // apply method _applyLabelPath : function(value, old) { this._provider.setLabelPath(value); }, // apply method _applyIconPath : function(value, old) { this._provider.setIconPath(value); }, // apply method _applyGroupLabelPath : function(value, old) { this._provider.setGroupLabelPath(value); }, // apply method _applyLabelOptions : function(value, old) { this._provider.setLabelOptions(value); }, // apply method _applyIconOptions : function(value, old) { this._provider.setIconOptions(value); }, // apply method _applyGroupLabelOptions : function(value, old) { this._provider.setGroupLabelOptions(value); }, // apply method _applyDelegate : function(value, old) { this._provider.setDelegate(value); this.__buildUpLookupTable(); }, // property apply _applyVariableItemHeight : function(value, old) { if(value) { this._setRowItemSize(); } else { this.getPane().getRowConfig().resetItemSizes(); this.getPane().fullUpdate(); } }, /* --------------------------------------------------------------------------- EVENT HANDLERS --------------------------------------------------------------------------- */ /** * Event handler for the resize event. * * @param e {qx.event.type.Data} resize event. */ _onResize : function(e) { this.getPane().getColumnConfig().setItemSize(0, e.getData().width); }, /** * Event handler for the model change event. * * @param e {qx.event.type.Data} model change event. */ _onModelChange : function(e) { // we have to remove the bindings before we rebuild the lookup table // otherwise bindings might be dispatched to wrong items // see: https://github.com/qooxdoo/qooxdoo/issues/196 this._provider.removeBindings(); this.__buildUpLookupTable(); this._applyDefaultSelection(); if (e instanceof qx.event.type.Data) { this.fireDataEvent("changeModelLength", e.getData(), e.getOldData()); } }, /** * Event handler for the updated event of the * qx.ui.virtual.layer.WidgetCell layer. * * Recalculates the item sizes in a deffered call, * which only happens if we have variable item heights */ _onLayerUpdated: function () { if (this.isVariableItemHeight() === false) { return; } if (this.__deferredLayerUpdate === null) { this.__deferredLayerUpdate = new qx.util.DeferredCall(function () { this._setRowItemSize(); }, this); } this.__deferredLayerUpdate.schedule(); }, /* --------------------------------------------------------------------------- HELPER ROUTINES --------------------------------------------------------------------------- */ /** * Helper method to update the row count. */ __updateRowCount : function() { this.getPane().getRowConfig().setItemCount(this.__lookupTable.length); this.getPane().fullUpdate(); }, /** * Helper method to update row heights. */ __updateGroupRowHeight : function() { var rc = this.getPane().getRowConfig(); var gh = this.getGroupItemHeight(); rc.resetItemSizes(); if (gh) { for (var i = 0,l = this.__lookupTable.length; i < l; ++i) { if (this.__lookupTable[i] == -1) { rc.setItemSize(i, gh); } } } }, /** * Internal method for building the lookup table. */ __buildUpLookupTable : function() { this.__lookupTable = []; this.__lookupTableForGroup = []; this.__groupHashMap = {}; if (this.isAutoGrouping()) { this.getGroups().removeAll(); } var model = this.getModel(); if (model != null) { this._runDelegateFilter(model); this._runDelegateSorter(model); this._runDelegateGroup(model); } this._updateSelection(); this.__updateGroupRowHeight(); this.__updateRowCount(); }, /** * Invokes filtering using the filter given in the delegate. * * @param model {qx.data.IListData} The model. */ _runDelegateFilter : function (model) { var filter = qx.util.Delegate.getMethod(this.getDelegate(), "filter"); for (var i = 0,l = model.length; i < l; ++i) { if (filter == null || filter(model.getItem(i))) { this.__lookupTable.push(i); } } }, /** * Invokes sorting using the sorter given in the delegate. * * @param model {qx.data.IListData} The model. */ _runDelegateSorter : function (model) { if (this.__lookupTable.length == 0) { return; } var sorter = qx.util.Delegate.getMethod(this.getDelegate(), "sorter"); if (sorter != null) { this.__lookupTable.sort(function(a, b) { return sorter(model.getItem(a), model.getItem(b)); }); } }, /** * Invokes grouping using the group result given in the delegate. * * @param model {qx.data.IListData} The model. */ _runDelegateGroup : function (model) { var groupMethod = qx.util.Delegate.getMethod(this.getDelegate(), "group"); if (groupMethod != null) { for (var i = 0,l = this.__lookupTable.length; i < l; ++i) { var index = this.__lookupTable[i]; var item = this.getModel().getItem(index); var group = groupMethod(item); this.__addGroup(group, index); } this.__lookupTable = this.__createLookupFromGroup(); } }, /** * Adds a model index the the group. * * @param group {String|Object|null} the group. * @param index {Integer} model index to add. */ __addGroup : function(group, index) { // if group is null add to default group if (group == null) { this.__defaultGroupUsed = true; group = "???"; } var name = this.__getUniqueGroupName(group); if (this.__groupHashMap[name] == null) { this.__groupHashMap[name] = []; if (this.isAutoGrouping()) { this.getGroups().push(group); } } this.__groupHashMap[name].push(index); }, /** * Creates a lookup table form the internal group hash map. * * @return {Array} the lookup table based on the internal group hash map. */ __createLookupFromGroup : function() { this.__checkGroupStructure(); var result = []; var row = 0; var groups = this.getGroups(); for (var i = 0; i < groups.getLength(); i++) { var group = groups.getItem(i); // indicate that the value is a group result.push(-1); this.__lookupTableForGroup.push(row); row++; var key = this.__getUniqueGroupName(group); var groupMembers = this.__groupHashMap[key]; if (groupMembers != null) { for (var k = 0; k < groupMembers.length; k++) { result.push(groupMembers[k]); row++; } } } return result; }, /** * Returns an unique group name for the passed group. * * @param group {String|Object} Group to find unique group name. * @return {String} Unique group name. */ __getUniqueGroupName : function(group) { var name = null; if (!qx.lang.Type.isString(group)) { var index = this.getGroups().indexOf(group); this.__groupObjectsUsed = true; name = "group"; if (index == -1) { name += this.getGroups().getLength(); } else { name += index; } } else { this.__groupStringsUsed = true; var name = group; } return name; }, /** * Checks that <code>Object</code> and <code>String</code> are not mixed * as group identifier, otherwise an exception occurs. */ __checkGroupStructure : function() { if (this.__groupObjectsUsed && this.__defaultGroupUsed || this.__groupObjectsUsed && this.__groupStringsUsed) { throw new Error("GroupingTypeError: You can't mix 'Objects' and 'Strings' as" + " group identifier!"); } }, /** * Get the height of each visible item and set it as the * row size */ _setRowItemSize : function() { var rowConfig = this.getPane().getRowConfig(); var layer = this._layer; var firstRow = layer.getFirstRow(); var lastRow = firstRow + layer.getRowSizes().length; for (var row = firstRow; row < lastRow; row++) { var widget = layer.getRenderedCellWidget(row, 0); if (widget !== null) { var height = widget.getSizeHint().height; rowConfig.setItemSize( row, height ); } } } }, destruct : function() { this._disposeObjects("__deferredLayerUpdate"); var model = this.getModel(); if (model != null) { model.removeListener("changeLength", this._onModelChange, this); } var pane = this.getPane(); if (pane != null) { pane.removeListener("resize", this._onResize, this); } this._background.dispose(); this._provider.dispose(); this._layer.dispose(); this._background = this._provider = this._layer = this.__lookupTable = this.__lookupTableForGroup = this.__groupHashMap = null; if (this.__defaultGroups) { this.__defaultGroups.dispose(); } } });