UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

638 lines (527 loc) 16.8 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2012 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: * Tino Butz (tbtz) * Christopher Zuendorf (czuendorf) ************************************************************************ */ /** * The list widget displays the data of a model in a list. * * *Example* * * Here is a little example of how to use the widget. * * <pre class='javascript'> * * // Data for the list * var data = [ * {title : "Row1", subtitle : "Sub1"}, * {title : "Row2", subtitle : "Sub2"}, * {title : "Row3", subtitle : "Sub3"} * ]; * * // Create the list with a delegate that * var list = new qx.ui.mobile.list.List({ * configureItem: function(item, data, row) * { * item.setImage("path/to/image.png"); * item.setTitle(data.title); * item.setSubtitle(data.subtitle); * }, * * configureGroupItem: function(item, data) { * item.setTitle(data.title); * }, * * group: function(data, row) { * return { * title: row < 2 ? "Selectable" : "Unselectable" * }; * } * }); * * // Set the model of the list * list.setModel(new qx.data.Array(data)); * * // Add an changeSelection event * list.addListener("changeSelection", function(evt) { * alert("Index: " + evt.getData()) * }, this); * * this.getRoot().add(list); * </pre> * * This example creates a list with a delegate that configures the list item with * the given data. A listener for the event {@link #changeSelection} is added. */ qx.Class.define("qx.ui.mobile.list.List", { extend : qx.ui.mobile.core.Widget, /** * @param delegate {qx.ui.mobile.list.IListDelegate?null} The {@link #delegate} to use */ construct : function(delegate) { this.base(arguments); this.__provider = new qx.ui.mobile.list.provider.Provider(this); this.addListener("tap", this._onTap, this); this.addListener("trackstart", this._onTrackStart, this); this.addListener("track", this._onTrack, this); this.addListener("trackend", this._onTrackEnd, this); if (delegate) { this.setDelegate(delegate); } else { this.setDelegate(this); } if (qx.core.Environment.get("qx.dynlocale")) { qx.locale.Manager.getInstance().addListener("changeLocale", this._onChangeLocale, this); } }, events : { /** * Fired when the selection is changed. */ changeSelection : "qx.event.type.Data", /** * Fired when the group selection is changed. */ changeGroupSelection : "qx.event.type.Data", /** * Fired when an item should be removed from list. */ removeItem : "qx.event.type.Data" }, properties : { // overridden defaultCssClass : { refine : true, init : "list" }, /** * Delegation object which can have one or more functions defined by the * {@link qx.ui.mobile.list.IListDelegate} interface. */ delegate : { apply: "_applyDelegate", event: "changeDelegate", init: null, nullable: true }, /** * The model to use to render the list. */ model : { check : "qx.data.Array", apply : "_applyModel", event: "changeModel", nullable : true, init : null }, /** * Number of items to display. Auto set by model. * Reset to limit the amount of data that should be displayed. */ itemCount : { check : "Integer", init : 0 }, /** * The height of a list item. */ itemHeight : { check : "Number", init : null, nullable : true } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { __provider : null, __minDeleteDistance : null, __isScrollingBlocked : null, __trackElement : null, // overridden _getTagName : function() { return "ul"; }, /** * Default list delegate. Expects a map which contains an image, a subtitle, and a title: * <code>{title : "Row1", subtitle : "Sub1", image : "path/to/image.png"}</code> * * @param item {qx.ui.mobile.list.renderer.Abstract} Instance of list item renderer to modify * @param data {var} The data of the row. Can be used to configure the given item. * @param row {Integer} The row index. */ configureItem: function(item, data, row) { if(typeof data.image != "undefined") { item.setImage(data.image); } if(typeof data.subtitle != "undefined") { item.setSubtitle(data.subtitle); } if(typeof data.title != "undefined") { item.setTitle(data.title); } if(typeof data.enabled != "undefined") { item.setEnabled(data.enabled); } if(typeof data.removable != "undefined") { item.setRemovable(data.removable); } if(typeof data.selectable != "undefined") { item.setSelectable(data.selectable); } if(typeof data.activatable != "undefined") { item.setActivatable(data.activatable); } if(typeof data.arrow != "undefined") { item.setShowArrow(data.arrow); } if(typeof data.selected != "undefined") { item.setSelected(data.selected); } }, /** * Event handler for the "tap" event. * * @param evt {qx.event.type.Tap} The tap event */ _onTap : function(evt) { var element = this._getElement(evt); if(!element) { return; } var row = -1; if (qx.bom.element.Class.has(element, "list-item")) { if (qx.bom.element.Attribute.get(element, "data-selectable") != "false" && qx.dom.Element.hasChild(this.getContainerElement(), element)) { row = parseInt(element.getAttribute("data-row"), 10); } if (row != -1) { this.fireDataEvent("changeSelection", row); } } else { var group = parseInt(element.getAttribute("data-group"), 10); if (qx.bom.element.Attribute.get(element, "data-selectable") != "false") { this.fireDataEvent("changeGroupSelection", group); } } }, /** * Event handler for <code>trackstart</code> event. * @param evt {qx.event.type.Track} the <code>trackstart</code> event */ _onTrackStart : function(evt) { this.__isScrollingBlocked = null; this.__trackElement = null; var element = this._getElement(evt); if (element && qx.bom.element.Class.has(element, "list-item") && qx.bom.element.Class.has(element, "removable")) { this.__trackElement = element; this.__minDeleteDistance = qx.bom.element.Dimension.getWidth(element) / 2; qx.bom.element.Class.add(element, "track"); } }, /** * Event handler for <code>track</code> event. * @param evt {qx.event.type.Track} the <code>track</code> event */ _onTrack : function(evt) { if (!this.__trackElement) { return; } var element = this.__trackElement; var delta = evt.getDelta(); var deltaX = Math.round(delta.x * 0.1) / 0.1; if(this.__isScrollingBlocked === null) { this.__isScrollingBlocked = (delta.axis == "x"); } if (!this.__isScrollingBlocked) { return; } var opacity = 1 - (Math.abs(deltaX) / this.__minDeleteDistance); opacity = Math.round(opacity * 100) / 100; qx.bom.element.Style.set(element, "transform", "translate3d(" + deltaX + "px,0,0)"); qx.bom.element.Style.set(element, "opacity", opacity); evt.preventDefault(); }, /** * Event handler for <code>trackend</code> event. * @param evt {qx.event.type.Track} the <code>trackend</code> event */ _onTrackEnd : function(evt) { if (!this.__trackElement) { return; } var element = this.__trackElement; if (Math.abs(evt.getDelta().x) > this.__minDeleteDistance) { var row = parseInt(element.getAttribute("data-row"), 10); this.fireDataEvent("removeItem", row); } else { qx.bom.AnimationFrame.request(function() { qx.bom.element.Style.set(element, "transform", "translate3d(0,0,0)"); qx.bom.element.Style.set(element, "opacity", "1"); qx.bom.element.Class.remove(element, "track"); }.bind(this)); } }, /** * Returns the target list item. * @param evt {Event} the input event * @return {Element} the target list item. */ _getElement : function(evt) { var element = evt.getOriginalTarget(); // Click on border: do nothing. if(element.tagName == "UL") { return null; } while (element.tagName != "LI") { element = element.parentNode; } return element; }, // property apply _applyModel : function(value, old) { if (old != null) { old.removeListener("changeBubble", this.__onModelChangeBubble, this); } if (value != null) { value.addListener("changeBubble", this.__onModelChangeBubble, this); } if (old != null) { old.removeListener("change", this.__onModelChange, this); } if (value != null) { value.addListener("change", this.__onModelChange, this); } if (old != null) { old.removeListener("changeLength", this.__onModelChangeLength, this); } if (value != null) { value.addListener("changeLength", this.__onModelChangeLength, this); } this.__render(); }, // property apply _applyDelegate : function(value, old) { this.__provider.setDelegate(value); }, /** * Listen on model 'changeLength' event. * @param evt {qx.event.type.Data} data event which contains model change data. */ __onModelChangeLength : function(evt) { this.__render(); }, /** * Locale change event handler * * @signature function(e) * @param e {Event} the change event */ _onChangeLocale : qx.core.Environment.select("qx.dynlocale", { "true" : function(e) { this.__render(); }, "false" : null }), /** * Reacts on model 'change' event. * @param evt {qx.event.type.Data} data event which contains model change data. */ __onModelChange : function(evt) { if(evt && evt.getData() && evt.getData().type == "order") { this.__render(); } }, /** * Reacts on model 'changeBubble' event. * @param evt {qx.event.type.Data} data event which contains model changeBubble data. */ __onModelChangeBubble : function(evt) { if(evt) { var data = evt.getData(); var isArray = (qx.lang.Type.isArray(data.old) && qx.lang.Type.isArray(data.value)); if (!isArray || (isArray && data.old.length == data.value.length)) { var rows = this._extractRowsToRender(data.name); for (var i = 0; i < rows.length; i++) { this.__renderRow(rows[i]); } } } }, /** * Extracts all rows, which should be rendered from "changeBubble" event's * data.name. * @param name {String} The 'data.name' String of the "changeBubble" event, * which contains the rows that should be rendered. * @return {Integer[]} An array with integer values, representing the rows which should * be rendered. */ _extractRowsToRender : function(name) { var rows = []; if(!name) { return rows; } // "[0-2].propertyName" | "[0].propertyName" | "0" var containsPoint = (name.indexOf(".")!=-1); if(containsPoint) { // "[0-2].propertyName" | "[0].propertyName" var candidate = name.split(".")[0]; // Normalize candidate = candidate.replace("[",""); candidate = candidate.replace("]",""); // "[0-2]" | "[0]" var isRange = (candidate.indexOf("-") != -1); if(isRange) { var rangeMembers = candidate.split("-"); // 0 var startRange = parseInt(rangeMembers[0],10); // 2 var endRange = parseInt(rangeMembers[1],10); for(var i = startRange; i <= endRange; i++) { rows.push(i); } } else { // "[0]" rows.push(parseInt(candidate.match(/\d+/)[0], 10)); } } else { // "0" var match = name.match(/\d+/); if(match.length == 1) { rows.push(parseInt(match[0], 10)); } } return rows; }, /** * Renders a specific row identified by its index. * @param index {Integer} index of the row which should be rendered. */ __renderRow : function(index) { var renderedItems = qx.bom.Selector.query(".list-item",this.getContentElement()); var oldNode = renderedItems[index]; var newNode = this.__provider.getItemElement(this.getModel().getItem(index), index); this.getContentElement().replaceChild(newNode, oldNode); this._domUpdated(); }, /** * @internal * Returns the height of one single list item. * @return {Integer} the height of a list item in px. */ getListItemHeight : function() { var listItemHeight = 0; if (this.getModel() != null && this.getModel().length > 0) { var listHeight = qx.bom.element.Style.get(this.getContentElement(), "height"); listItemHeight = parseInt(listHeight) / this.getModel().length; } return listItemHeight; }, /** * Renders the list. */ __render : function() { this._setHtml(""); var model = this.getModel(); this.setItemCount(model ? model.getLength() : 0); var groupIndex = 0; for (var index = 0; index < this.getItemCount(); index++) { if (this.__hasGroup()) { var groupElement = this._renderGroup(index, groupIndex); if (groupElement) { groupIndex++; this.getContentElement().appendChild(groupElement); } } var item = model.getItem(index); var itemElement = this.__provider.getItemElement(item, index); var itemHeight = null; if (this.getItemHeight() !== null) { itemHeight = this.getItemHeight() + "px"; } // Fixed height qx.bom.element.Style.set(itemElement, "minHeight", itemHeight); qx.bom.element.Style.set(itemElement, "maxHeight", itemHeight); this.getContentElement().appendChild(itemElement); } this._domUpdated(); }, /** * Triggers a re-rendering of this list. */ render : function() { this.__render(); }, /** * Renders a group header. * * @param itemIndex {Integer} the current list item index. * @param groupIndex {Integer} the group index. * @return {Element} the group element or <code>null</code> if no group was needed. */ _renderGroup: function(itemIndex, groupIndex) { var group = this.__getGroup(itemIndex); if (itemIndex === 0) { return this.__provider.getGroupElement(group, groupIndex); } else { var previousGroup = this.__getGroup(itemIndex - 1); if (!qx.lang.Object.equals(group, previousGroup)) { return this.__provider.getGroupElement(group, groupIndex); } } }, /** * Checks whether the delegate support group rendering. * @return {Boolean} true if the delegate object supports grouping function. */ __hasGroup : function() { return qx.util.Delegate.getMethod(this.getDelegate(), "group") !== null; }, /** * Returns the group for this item, identified by its index * @param index {Integer} the item index. * @return {Object} the group object, to which the item belongs to. */ __getGroup : function(index) { var item = this.getModel().getItem(index); var group = qx.util.Delegate.getMethod(this.getDelegate(), "group"); return group(item, index); } }, destruct : function() { this.__trackElement = null; this._disposeObjects("__provider"); if (qx.core.Environment.get("qx.dynlocale")) { qx.locale.Manager.getInstance().removeListener("changeLocale", this._onChangeLocale, this); } } });