/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. Neither the name of "Scrivo" nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * * $Id: ListView.js 616 2013-04-22 23:48:38Z geert $ */ "use strict"; SUI.ListView = SUI.defineClass( /** @lends SUI.ListView.prototype */{ /** @ignore */ baseClass: SUI.Box, /** * @class * SUI.ListView is a component to display data in a list with columns. The * list can be used to select one multiple rows. To aid the user's * selection the rows can be sorted per column. * * @augments SUI.Box * * @description * Construct a SUI.ListView object. A large number of variables can be set * to customize the listview to your specific needs. * * @constructs * @param {Object} object in which the follow entries can be set * @param see base class * @param {boolean} arg.multiselect Enable multiple selection * @param {object[]} arg.data The data array of objects in which each * object contains column (key) / values pairs * @param {String} arg.sort The column key for which the data should be * sorted initially * @param {int|int[]} arg.selected Array of indexes for the initial row * selection * @param {int} arg.focussed Index of the row to focus initially * @param {object[]} arg.cols The column definition: an array with objects * which can contain the following fields: * @param {String} arg.cols[].title The header title * @param {String} arg.cols[].key Key that corresponds with a key in the * data array * @param {int} arg.cols[].width Width of the header * @param {int} arg.cols[].minWidth Minimum width of the header * @param {int} arg.cols[].maxWidth Maximum width of the header * @param {String} arg.cols[].align Alignment of the header, options are * "left" (default), "center" and "right" * @param {string|function} arg.cols[].icon false (default) if no icon is * needed or a location of an icon file or a function that returns one. * This function takes to parameters, the first is a reference to the * row and the second is key of the column to generate an icon * location for. * @param {string|function} arg.cols[].sort a build in sort method or a * user defined one. "text" (default) and "number" are the build in * sort methods. The sort function take three parameters: the first * is a reference to the data array, the second is the key on which to * sort and the third is the search direction (1 or -1). * @param {Function} arg.cols[].format_func A user defined format * function, this function takes to parameters, the first is a * reference to the row and the second is key of the column that * needs to be formatted. */ initializer: function(arg) { // by default anchor to all sides if (!arg.anchor) { arg.anchor = { right:true,left:true,top:true,bottom:true }; } SUI.ListView.initializeBase(this, arg); // do the heavy stuff this._buildControl(arg); }, /** * Top padding of a cell */ CELL_PADDING_TOP: 1, /** * Left and right padding of a cell */ CELL_PADDING_SIDES: 5, /** * Header border (only below the header). */ HEADER_BORDER_BOTTOM_WIDTH: 1, /** * Header height (including bottom border) */ HEADER_HEIGHT: 21, /** * Padding top of the headers */ HEADER_PADDING_TOP: 2, /** * Side padding for a header if a no direction indicator needs to be drawn * at that side. */ HEADER_PADDING_SHORT: 2, /** * Side padding for a header if a direction indicator needs to be drawn * at that side. */ HEADER_PADDING_LONG: 16, /** * Top margin of the sort direction indicator. */ HEADER_SORT_ICON_MARGIN_TOP: 2, /** * Size (width/height) of the icon for the sort direction indicator. */ HEADER_SORT_ICON_SIZE: 16, /** * The half-width of the spacer */ HEADER_SPACER_WIDTH: 3, /** * Size (width/height) of the row icons. */ ICON_SIZE: 16, /** * The vertical margin for the separator (determines the length of the * separator). */ SEPARATOR_MARGIN_V: 3, /** * Width of the separator on the spacer between the headers. */ SEPARATOR_WIDTH: 2, /** * Row border width (top and bottom) */ ROW_BORDER_WIDTH: 1, /** * Row height (including top and bottom border) */ ROW_HEIGHT: 20, /** * Display the listview control. Set the CSS size and position of the list * and all the headers. Then render the rows. */ display: function() { this.setDim(); // set the CSS dimensions of the header this.headervpt.setDim(); this.header.setDim(); // set the CSS dimensions of the column headers for (var i=0; i<this.colums.length; i++) { this.colums[i].header.setDim(); this.colums[i].headerTitle.setDim(); this.colums[i].spacer.setDim(); this.colums[i].separator.setDim(); } // set the CSS of the list this.listvpt.setDim(); this.list.setDim(); // render the rows, only deal with the focussed row if the list was // not yet drawn if (this._firstDisplay && this._focussedRow) { if (!this._scrollIntoView(this._focussedRow)) { this._renderRows(); } } else { this._renderRows(); } this._firstDisplay = false; }, /** * Lay out the listview control. Calculate the size and position of the * list and headers. */ layOut: function() { var hdrInnerH = this.HEADER_HEIGHT - this.HEADER_BORDER_BOTTOM_WIDTH; // set the size of the header viewport this.headervpt.setRect(0, 0, this.width(), this.HEADER_HEIGHT); // and create a very wide header bar (so we can set the scroll offset) this.header.setRect(0, 0, 10000, hdrInnerH); // current left position for the header var l = 0; // loop through all the columns for (var i=0; i<this.colums.length; i++) { // set the off the column to the current left this.colums[i].left = l; // get the width of an header var w = this.colums[i].width - this.HEADER_SPACER_WIDTH*2; // set the dimensions for the header this.colums[i].header.setRect( 0, l+this.HEADER_SPACER_WIDTH, w, hdrInnerH); // set the dimensions for the spacer between the header this.colums[i].spacer.setRect(0, l+this.HEADER_SPACER_WIDTH+w, this.HEADER_SPACER_WIDTH*2, hdrInnerH); // set the dimensions for the separator line in the spacer this.colums[i].separator.setRect(this.SEPARATOR_MARGIN_V, this.HEADER_SPACER_WIDTH - this.SEPARATOR_WIDTH/2, this.SEPARATOR_WIDTH, hdrInnerH - 2*this.SEPARATOR_MARGIN_V); // move current left to the next column l += this.colums[i].width; var pl = 0, pr = 0; if (this.colums[i].align === "right") { // set the parameters for a right aligned header pl = this.HEADER_PADDING_LONG; pr = this.HEADER_PADDING_SHORT; this.colums[i].header.addClass("sui-lv-header-sort-left"); this.colums[i].header.el().style.backgroundPosition = "0px " + this.HEADER_SORT_ICON_MARGIN_TOP + "px"; } else { // set the parameters for a left aligned header pl = this.HEADER_PADDING_SHORT; pr = this.HEADER_PADDING_LONG; this.colums[i].header.addClass("sui-lv-header-sort-right"); this.colums[i].header.el().style.backgroundPosition = (this.colums[i].width-this.HEADER_SORT_ICON_SIZE- this.HEADER_SPACER_WIDTH*2) + "px " + this.HEADER_SORT_ICON_MARGIN_TOP + "px"; } // set the dimensions of the text in the header this.colums[i].headerTitle.setRect(this.HEADER_PADDING_TOP, pl, this.colums[i].header.width() - pr - pl, this.colums[i].header.height()-this.HEADER_PADDING_TOP); } // set the dimensions of the viewport this.listvpt.setRect(this.HEADER_HEIGHT, 0, this.width(), this.height() - this.HEADER_HEIGHT); // and the dimensions of the list this.list.setRect(0, 0, l, this.list.height()); // we want to (re) draw all the rows, so set a new row drawing phase this._rowDraw++; }, /** * (Re)load data into the list view. * @param {object[]} data The data array of objects in which each object * contains column (key) / values pairs * @param {int/int[]} selected Array of indexes for the initial row * selection * @param {int} focussed Index of the row to focus initially */ loadData: function(data, selected, focussed) { var w = this.list.width(); // remove what's in the list (other data or loading image) ... this.list.removeBox(); // and create a new box this.list = new SUI.Box({parent: this.listvpt}); // we want the zebra pattern to continue further than the last column this.list.el().style.overflow = "visible"; // remove the "loading" image this.listvpt.el().style.backgroundImage = "none"; this.listvpt.removeClass("sui-lv-loading"); // and set our data to the data given this.data = data ? data : []; // we keep the last width but the height setting may be different this.list.setRect(0, 0, w, this.data.length * this.ROW_HEIGHT); // create an entry for the data we need (references to the cells and // rows f.i.) in each data row. for (var r=0; r<this.data.length; r++) { this.data[r].rowPtr = { "row": null, "drw" : 0, sel : false, "cells" : []}; } // 'normalize' the selected parameter into tmpsel (undefined to [], // and int to [int] and [] to [] var tmpsel = []; if (selected !== undefined) { if (selected) { if (this._multiselect) { tmpsel = selected; } else { tmpsel = [selected[0]]; } } } // get the focussed row from the index var f = focussed !== undefined ? this.data[focussed] : null; this._focussedRow = null; // set the selected rows this.selectedRows = []; // the first selected row is always the focussed one if (f) { this.data[focussed].rowPtr.sel = true; this.selectedRows.push(this.data[focussed]); } // set selected rows (focussed row might be selected already) if (this._multiselect || this.selectedRows.length !== 1) { for (r=0; r<tmpsel.length; r++) { if (r !== focussed) { this.data[tmpsel[r]].rowPtr.sel = true; this.selectedRows.push(this.data[tmpsel[r]]); } } } // sort the data if required if (this._sortCol) { this._sort(); } // and set the focussed row after sorting (leave it for the display // function) this._focussedRow = f; }, /** * onClick event handler: is executed when the user clicks on a row. * @param {Object} row A reference to the selected row in the data table */ onClick: function(row) { }, /** * onDblClick event handler: is executed when the user double clicks on a * row. * @param {Object} row A reference to the selected row in the data table */ onDblClick: function(row) { }, /** * onContextMenu event handler: is executed when the user uses the context * menu click on a row. * @param {int} x The x location of the click * @param {int} y The y location of the click */ onContextMenu: function(x, y) { }, /** * onSelectionChange event handler: is executed when the (set of) row * selection(s) changes. * TODO: this event handler is not called properly from the code (f.i. * id not called when rows are deselected). */ onSelectionChange: function() { }, /** * Replace the listview contents with an "is loading" image. Recommended * when data is loaded from an external resource. */ setIsLoadingImage: function() { // get the width and height of the current box var w = this.list.width(); var h = this.list.height(); // remove the data in the list ... this.list.removeBox(); // ... and create a new box ... this.list = new SUI.Box({parent: this.listvpt}); // .. and set the width an height to those of the old box this.list.setRect(0, 0, w, h); // now set the CSS for the loading image this.listvpt.addClass("sui-lv-loading"); this.listvpt.el().style.backgroundImage = "url("+SUI.imgDir+"/"+SUI.resource.lvLoading+")"; }, // flag to indicate if the display functions was called for the first time _firstDisplay: true, // the row that has the focus _focussedRow: null, // is the list focussed or not _isFocussed: false, // allow multiple selection _multiselect: true, // first row of a multiple selection _multiSelStartRow: null, // indicate a row drawing phase: increment to redraw current rows (to // invalidate the currently drawn rows) _rowDraw: 0, // flag to prevent re-entrancy in the renderRows function _rowDrawing: false, // width of the system scrollbar (needed for size corrections) _scrollBarWidth: 0, // local storage for current left scroll offset of the list _scrollOffsetLeft: 0, // local storage for current top scroll offset of the list _scrollOffsetTop: 0, // column that is currently sorted _sortCol: null, // flag to indicate if we're sorting up or down _sortUp: true, /* Add the focus CSS class name to the focussed row */ _addFocusRectangle: function(e) { this._isFocussed = true; if (this._focussedRow && this._focussedRow.rowPtr.row) { this._focussedRow.rowPtr.row.addClass("sui-lv-row-focus"); } }, /* Set event handlers of a header: * click => _sortColumn; * mousedown => _highlightHeader; * mouseout, mouseup => _restoreHeader; * mousedown (of a spacer) => _resizeColumn */ _addHeaderEventHandlers: function(column) { // 'that' and 'column' are two closure variables var that = this; // Do _sortColumn on the click event of a header. SUI.browser.addEventListener(column.header.el(), "click", function(e) { if (!that._sortColumn(column)) { SUI.browser.noPropagation(e); } } ); // Do _highlightHeader on the mousedown event of a header. SUI.browser.addEventListener(column.header.el(), "mousedown", function(e) { if (!that._highlightHeader(column.header)) { SUI.browser.noPropagation(e); } } ); // Do _resizeColumn on the onmousedown event of a header spacer. SUI.browser.addEventListener(column.spacer.el(), "mousedown", function(e) { if (!that._resizeColumn(new SUI.Event(this, e), column)) { SUI.browser.noPropagation(e); } } ); // Do _restoreHeader on the mouseout event of a header. SUI.browser.addEventListener(column.header.el(), "mouseout", function(e) { if (!that._restoreHeader(column.header)) { SUI.browser.noPropagation(e); } } ); // Do _restoreHeader on the mouseup event of a header. SUI.browser.addEventListener(column.header.el(), "mouseup", function(e) { if (!that._restoreHeader(column.header)) { SUI.browser.noPropagation(e); } } ); }, /* Set event handlers the list: * focus => _addFocusRectangle; * blur => _removeFocusRectangle; * keydown/keypress => _handleKeyStroke; * scroll => handleScroll */ _addListEventHandlers: function() { var that = this; SUI.browser.addEventListener(this.el(), "focus", function(e) { if (!that._addFocusRectangle(new SUI.Event(this, e))) { SUI.browser.noPropagation(e); } } ); SUI.browser.addEventListener(this.el(), "blur", function(e) { if (!that._removeFocusRectangle(new SUI.Event(this, e))) { SUI.browser.noPropagation(e); } } ); SUI.browser.addEventListener(this.el(), "keydown", function(e) { // problems with Gecko's keydown use so keypress for Gecko if (!SUI.browser.isGecko) { if (!that._handleKeyStroke(new SUI.Event(this, e))) { SUI.browser.noPropagation(e); } } } ); SUI.browser.addEventListener(this.listvpt.el(), "keypress", function(e) { // problems with gecko's keydown use so keypress if (SUI.browser.isGecko) { if (!that._handleKeyStroke(new SUI.Event(this, e))) { SUI.browser.noPropagation(e); } } } ); SUI.browser.addEventListener(this.listvpt.el(), "scroll", function(e) { if (!that._handleScroll(new SUI.Event(this, e))) { SUI.browser.noPropagation(e); } } ); }, /* Set event handlers of a row: * click => _handleRowClick; * contextmenu => _handleRowContextMenu; * dblclick => _handleRowDblClick */ _addRowEventHandlers: function(row) { // 'that' and 'row' are two closure variables var that = this; // Do _handleRowClick on the click event of a row. SUI.browser.addEventListener(row.rowPtr.row.el(), "click", function(e) { if (!that._handleRowClick(new SUI.Event(this, e), row)) { SUI.browser.noPropagation(e); } } ); // Do _handleRowContextMenu on the contextmenu event of a row. SUI.browser.addEventListener(row.rowPtr.row.el(), "contextmenu", function(e) { if (!that._handleRowContextMenu(new SUI.Event(this, e), row)) { SUI.browser.noPropagation(e); } } ); // Do _handleRowDblClick on the dblclick event of a row. SUI.browser.addEventListener(row.rowPtr.row.el(), "dblclick", function(e) { if (!that._handleRowDblClick(new SUI.Event(this, e), row)) { SUI.browser.noPropagation(e); } } ); }, /* Create the columns for the listview */ _buildColumns: function(arg) { // get the profile for each column for (var i=0; i<arg.cols.length; i++) { // start with a default profile var def = { title: "Kolom "+i, key: i, width: 100, minWidth: 10, maxWidth: 1000, icon: false, align: "left", sort: "text", format_func: null }; for (var prop in arg.cols[i]) { // and overwrite the default profile with the entries set // in the arguments if (arg.cols[i].hasOwnProperty(prop)) { def[prop] = arg.cols[i][prop]; } } this.colums.push(def); } // loop through all the columns for (i=0; i<this.colums.length; i++) { // create the column header this.colums[i].header = new SUI.Box({parent: this.header}); // and a box for the text in the header this.colums[i].headerTitle = new SUI.TextBox({parent: this.colums[i].header}); this.colums[i].headerTitle.text(this.colums[i].title); this.colums[i].headerTitle.el().style.overflow = "hidden"; this.colums[i].headerTitle.el().style.whiteSpace = "nowrap"; this.colums[i].headerTitle.el().style.textAlign = this.colums[i].align; // end all headers with a spacer this.colums[i].spacer = new SUI.Box({parent: this.header}); this.colums[i].spacer.addClass("sui-lv-header-spacer"); this.colums[i].spacer.el().style.cursor = "col-resize"; // each spacer also get a separator line this.colums[i].separator = new SUI.Box({parent: this.colums[i].spacer}); this.colums[i].separator.border( new SUI.Border(0, this.SEPARATOR_WIDTH / 2 | 0)); this.colums[i].separator.addClass("sui-lv-header-separator"); // set the event handlers for the row this._addHeaderEventHandlers(this.colums[i]); // and if date is sorted ... if (arg.sort) { // ... set the sort direction indicator for the sorted column if (this.colums[i].key === arg.sort) { this._sortCol = this.colums[i]; this.colums[i].header.el().style.backgroundImage = "url("+SUI.imgDir+"/"+this.ICON_SORT_UP+")"; } } } }, /* Make all required boxes for the control, set event handlers and load * the data. */ _buildControl: function(arg) { // initialize the selectedRows array this.selectedRows = []; // get the width of the system scrollbar this._scrollBarWidth = SUI.style.scrollbarWidth(); // initialize the columns array this.colums = []; // if multiselect was set multiple selection on if (arg.multiselect !== undefined) { this._multiselect = arg.multiselect; } // incorporate this the listview's main element in the tab flow this.el().tabIndex = 1; // but do not allow it to be focusable (one row will be focusable) this.addClass("no-focus"); // create a viewport for the header: a box in which the header can // scroll to the left or right. this.headervpt = new SUI.Box({parent: this}); this.headervpt.border( new SUI.Border(0, 0, this.HEADER_BORDER_BOTTOM_WIDTH, 0)); this.headervpt.addClass("sui-lv-header"); // don't want scroll bars, its scrolling is dependent on the scrolling // of the list this.headervpt.el().style.overflow = "hidden"; // create the header box, fill it with headers later this.header = new SUI.Box({parent: this.headervpt}); // create a viewport for the list: a box in which the list can // scroll to the left or right, or up or down. this.listvpt = new SUI.Box({parent: this}); this.listvpt.addClass("no-focus"); this.listvpt.addClass("sui-lv-viewport"); // allow for scrolling in two dimensions this.listvpt.el().style.overflow = "auto"; // take it out of the tab flow this.listvpt.el().tabIndex = -1; // create the list and add it to the viewport this.list = new SUI.Box({parent: this.listvpt}); // add the general event handlers this._addListEventHandlers(); // create the columns this._buildColumns(arg); // and load the data into the listview this.loadData(arg.data, arg.selected, arg.focussed); }, /* Create a row cell */ _createCell: function(rowPtr, dataPtr, col, c) { // create the cell var cell = new SUI.TextBox({parent: rowPtr.row}); // set the content for cell, using a format function if appropriate cell.text(col.format_func ? col.format_func(dataPtr, col.key) : (dataPtr[col.key] ? dataPtr[col.key] : "")); // set the cell's alignment cell.el().style.textAlign = col.align; // we don't want overflow and warp on these cells cell.el().style.overflow = "hidden"; cell.el().style.whiteSpace = "nowrap"; var that = this; // but we do want to show the content if it is truncated // that and cell are the tow closure variables SUI.browser.addEventListener(cell.el(), "mouseover", function(e) { if (!that._setTitleOnOverflow(cell)) { SUI.browser.noPropagation(e); } } ); // if there is an icon in this column ... if (col.icon) { // ... get the icon, using a function if appropriate var icn = col.icon instanceof Function ? col.icon(dataPtr, col.key) : dataPtr[col.icon]; // ... and set the icon as background image ... cell.el().style.backgroundImage = "url("+SUI.imgDir+"/"+icn+")"; // ... now set the padding for the cell cell.padding(new SUI.Padding( this.CELL_PADDING_TOP, this.CELL_PADDING_SIDES, 0, (this.ICON_SIZE+this.CELL_PADDING_SIDES))); } else { // ... else set normal padding for the cell cell.padding(new SUI.Padding( this.CELL_PADDING_TOP, this.CELL_PADDING_SIDES, 0)); } // store a reference to the cell rowPtr.cells[c]=cell; }, /* Deselect a given or all selected rows. */ _deSelectRows: function(dataPtr) { // if a row as argument was given ... if (dataPtr !== undefined) { // ... then deselect that row, remove the CSS classname ... if (dataPtr.rowPtr.row) { // ... if the row was rendered ... dataPtr.rowPtr.row.removeClass("sui-lv-row-selected"); } // ... set its selection marker to false ... dataPtr.rowPtr.sel = false; // ... and remove it from the selectedRows array var i = this.selectedRows.indexOf(dataPtr); if(i!==-1) { this.selectedRows.splice(i, 1); } } else { // ... else deselect all selected rows ... for (i=0; i<this.selectedRows.length; i++) { // ... remove the CSS classname if (this.selectedRows[i].rowPtr.row) { // ... if the row was rendered ... this.selectedRows[i].rowPtr.row.removeClass( "sui-lv-row-selected"); } // ... and set its selection marker to false this.selectedRows[i].rowPtr.sel = false; } // now clear the selectedRows array this.selectedRows = []; } }, /* Handle a click on a row */ _handleRowClick: function(e, row) { this._selectAndFocusRows(row, e.event.ctrlKey, e.event.shiftKey); this.callListener("onClick", row); }, /* Handle a right-click (context menu request) on a row */ _handleRowContextMenu: function(e, row) { if (this.selectedRows.indexOf(row) === -1) { this._selectAndFocusRows(row, e.event.ctrlKey, e.event.shiftKey); } this.callListener("onContextMenu", SUI.browser.getX(e.event), SUI.browser.getY(e.event)); }, /* Handle a double-click (context menu request) on a row */ _handleRowDblClick: function(e, row) { this._selectAndFocusRows(row, e.event.ctrlKey, e.event.shiftKey); this.callListener("onDblClick", row); }, /* Handle a scroll event: save the scroll offsets and render the rows * in case of vertical scroll. */ _handleScroll: function(e) { // if scrolling in horizontal direction ... if (this._scrollOffsetLeft !== e.target.scrollLeft) { // ... store left scroll distance ... this._scrollOffsetLeft = e.target.scrollLeft; // ... and set the scroll offset for the header this.headervpt.el().scrollLeft = this._scrollOffsetLeft; } // if scrolling in vertical direction ... if (this._scrollOffsetTop !== e.target.scrollTop) { // ... store left scroll distance ... this._scrollOffsetTop = e.target.scrollTop; // ... and render the rows this._renderRows(); } }, /* End dragging of a columnheader: remove dragger and resize column. */ _endDrag: function(dragger, column) { // remove the dragger form the document tree dragger.removeBox(); // calculate new column width column.width = dragger.left() - column.left + this.HEADER_SPACER_WIDTH; // and redraw the list view this.draw(); }, /* Set the currently focussed row */ _focusRow: function(dataPtr) { // Only if row focus changes if (this._focussedRow !== dataPtr) { // Remove CSS class name if the row was rendered ... if (this._focussedRow && this._focussedRow.rowPtr.row) { this._focussedRow.rowPtr.row.removeClass("sui-lv-row-focus"); } // ... set the new focussed row ... this._focussedRow = dataPtr; // ... and ddd the CSS class name if the row was rendered if (this._focussedRow && this._focussedRow.rowPtr.row) { this._focussedRow.rowPtr.row.addClass("sui-lv-row-focus"); } } }, /* Find the next row if we're scrolling with page up or page down key */ _getRowIndexNextPage: function(keyCode) { // get the index of the current row var i = this.data.indexOf(this._focussedRow); // and calculate the page size in rows var ps = this.listvpt.height() / this.ROW_HEIGHT | 0; // if page down was pressed if (keyCode === 34) { // increase index with page size i += ps; // but not further than the length of the data array if (i > this.data.length-1) { i = this.data.length-1; } } // if page down was pressed if (keyCode === 33) { // decrease index with page size i -= ps; // but not further that zero if (i < 0) { i = 0; } } // return the index for new row to focus/select return i; }, /* Find the next row if we're scrolling with up or down key */ _getRowIndexNextRow: function(keyCode) { // get the index of the current row var i = this.data.indexOf(this._focussedRow); // if key down was pressed an we're not at the last row already ... if (keyCode === 40 && i < this.data.length-1) { // increase the index i++; } // if key up was pressed an we're not at the first row already ... if (keyCode === 38 && i > 0) { // decrease the index i--; } // return the index for new row to focus/select return i; }, /* Set a header to the pressed state. */ _highlightHeader: function(header) { header.addClass("sui-lv-header-button-pressed"); }, /* Process a key stroke */ _handleKeyStroke: function(e) { // enable key processing by the browser var r = true; // set _multiSelStartRow if necessary this._setMultiSelStartRow(e.event.ctrlKey, e.event.shiftKey); // keycode madness: there is a difference between keypress and // mousedown event var keyCode = e.event.keyCode ? e.event.keyCode : e.event.charCode; // dispatch to the correct keystroke processing function switch(keyCode) { case 40: case 38: // up or down r = this._selectNextRow(keyCode, e.event.ctrlKey, e.event.shiftKey); break; case 32: // (ctrl) space r = this._toggleSelection(keyCode, e.event.ctrlKey); break; case 34: case 33: // page up or down r = this._selectNextPageRow(keyCode); break; case 65: case 97: // (ctrl) a r = this._selectAll(keyCode, e.event.ctrlKey); break; default: break; } // it the keystroke was processed (note: !r) then prevent default // action if (!r) { if (SUI.browser.isIE) { e.event.returnValue = false; } else { e.event.preventDefault(); } } // likewise prevent event propagation if the keystroke was processed return r; }, /* Remove the focus CSS class name from the focussed row */ _removeFocusRectangle: function(e) { this._isFocussed = false; if (this._focussedRow && this._focussedRow.rowPtr.row) { this._focussedRow.rowPtr.row.removeClass("sui-lv-row-focus"); } }, /* Render the rows currently in the viewport, create the rows if necessary */ _renderRows: function() { // don't re-enter if (this._rowDrawing) { return; } this._rowDrawing = true; // get the first and number of rows to render ... var t = this.listvpt.el().scrollTop / this.ROW_HEIGHT | 0; var n = this.height() / this.ROW_HEIGHT | 0; // and loop over these rows for (var i=t; i<t+n && i<this.data.length; i++) { // the current left var l = 0; // shortcuts: dataPtr is the current row in the data set ... var dataPtr = this.data[i]; // ... and rowPtr point to the DOM row var rowPtr = dataPtr.rowPtr; // is the row already drawn in the current drawing phase ... if (rowPtr.drw !== this._rowDraw) { // ... no the draw it // if there is no row yet, create it if (rowPtr.drw === 0) { // create a box for the row rowPtr.row = new SUI.Box({parent: this.list}); // temporary hide its contents rowPtr.row.el().style.display="none"; // add CSS class and border rowPtr.row.addClass("sui-lv-row"); rowPtr.row.border( new SUI.Border(this.ROW_BORDER_WIDTH, 0)); // store reference dataPtr.rowPtr = rowPtr; // an add the event handlers to the row this._addRowEventHandlers(dataPtr); } // loop over the columns for (var c=0; c<this.colums.length; c++) { // if the row is not drawn yet add the cells if (rowPtr.drw === 0) { this._createCell(rowPtr, dataPtr, this.colums[c], c); } // set the size of the cells var w = this.colums[c].width; rowPtr.cells[c].setRect(0, l, w, this.ROW_HEIGHT - 2 * this.ROW_BORDER_WIDTH - this.CELL_PADDING_TOP); // increase the current left l += w; // set the CSS dimensions of the cell rowPtr.cells[c].setDim(); } // now we're sure that we have a row and all cells have the // proper dimensions draw the row, first set the proper CSS // class name rowPtr.row.removeClass("sui-lv-row-even"); rowPtr.row.removeClass("sui-lv-row-odd"); rowPtr.row.addClass( "sui-lv-row sui-lv-row-" + (i%2 ? "even" : "odd")); // if the row is not as long as the viewport extend it so the // zebra pattern is not truncated (and correct for the width of // the scroll bar) if (l < this.width()) { l = this.width() - (this.listvpt.height() < this.list.height() ? this._scrollBarWidth : 0); } // set the size of the row and the CSS dimensions rowPtr.row.setRect(this.ROW_HEIGHT*i, 0, l, this.ROW_HEIGHT); rowPtr.row.setDim(); // if it is the focussed row add CSS class if (this._focussedRow && this._isFocussed && rowPtr === this._focussedRow.rowPtr) { rowPtr.row.addClass("sui-lv-row-focus"); } // if it is a selected row add CSS class if (rowPtr.sel) { rowPtr.row.addClass("sui-lv-row-selected"); } // store the drawing phase into the row rowPtr.drw = this._rowDraw; // now show the row rowPtr.row.el().style.display = "block"; } } // unlock this function this._rowDrawing = false; }, /* Start sizing a column: create and initialize a dragger. */ _resizeColumn: function(event, column) { var left = column.left - this.HEADER_SPACER_WIDTH; // find minimum x for dragging var minx = left + column.minWidth; if (minx < this.headervpt.el().scrollLeft) { minx = this.headervpt.el().scrollLeft; } // find maximum x for dragging var maxx = left + column.maxWidth; if (maxx > this.headervpt.width() + this.headervpt.el().scrollLeft - this.HEADER_SPACER_WIDTH * 2) { maxx = this.headervpt.width() + this.headervpt.el().scrollLeft - this.HEADER_SPACER_WIDTH * 2; } // Create dragger ... var dragger = new SUI.Dragger({parent: this.header}); // ... width the same dimensions as the column spacer dragger.setRect(column.spacer); // style the dragger dragger.addClass("sui-lv-header-dragger"); dragger.el().style.cursor = column.spacer.el().style.cursor; // set the dragging restrictions dragger.direction(dragger.HORIZONTAL); dragger.xMin(minx); dragger.xMax(maxx); // set the CSS dimensions of the dragger dragger.setDim(); // set the onEndDrag event handler var that = this; dragger.addListener("onEndDrag", function() { that._endDrag(dragger, column); }); // and start dragging dragger.start(event, this); }, /* Set a header to the normal state. */ _restoreHeader: function(header) { header.removeClass("sui-lv-header-button-pressed"); }, /* Scroll the given row into view. */ _scrollIntoView: function(row) { // assume no need to scroll var res = false; if (row) { // at what position is the row? var rownum = this.data.indexOf(row); // if calculated distance from top is larger than the viewport // height plus top overflow ... if (((rownum + 1) * this.ROW_HEIGHT) > (this._scrollOffsetTop + this.listvpt.el().clientHeight)) { // ... then calculate the new top overflow en set it this._scrollOffsetTop = (rownum + 1) * this.ROW_HEIGHT - this.listvpt.el().clientHeight; this.listvpt.el().scrollTop = this._scrollOffsetTop; // render rows into the viewport this._renderRows(); res = true; } // if calculated distance from top is smaller than the // top overflow ... if (rownum * this.ROW_HEIGHT < this._scrollOffsetTop) { // ... then set the top overflow to that value this._scrollOffsetTop = rownum * this.ROW_HEIGHT; this.listvpt.el().scrollTop = this._scrollOffsetTop; // render rows into the viewport this._renderRows(); res = true; } } return res; }, /* Process letter A key. */ _selectAll: function(keyCode, ctrl) { // Ctrl-A: select all, So only relevant when multiple selection is on // and ctrl is pressed if (this._multiselect && ctrl) { // select all rows for (var i=0; i<this.data.length; i++) { this._selectRow(this.data[i]); } // invalidate rows region, so rows will be redrawn this._rowDraw++; // and redraw the rows this._renderRows(); // disable key processing by the browser return false; } // enable key processing by the browser return true; }, /* Handle a click on a row */ _selectAndFocusRows: function(dataPtr, ctrl, shift) { // set _multiSelStartRow if necessary this._setMultiSelStartRow(ctrl, shift); if (this._multiselect && shift && this._multiSelStartRow) { // multiple selection is on, shift key is pressed and there is a // start row: do a range selection. Find the start and end var startno = this.data.indexOf(this._multiSelStartRow); var endno = this.data.indexOf(dataPtr); // swap if necessary if (startno > endno) { var t = startno; startno = endno; endno = t; } // deselect the rows this._deSelectRows(); // and create the new selection for (var i=startno; i<=endno; i++) { this._selectRow(this.data[i]); } } else if (this._multiselect && ctrl) { // multiple selection is on and the ctrl key was pressed, if // current row was selected ... if (dataPtr.rowPtr.sel) { // ... de-select it ... this._deSelectRows(dataPtr); } else { // ... else select it. this._selectRow(dataPtr); } } else { // no fancy keys, or no start of multiple selection set yet: // deselect all rows an select the requested. this._deSelectRows(); this._selectRow(dataPtr); } // focus the row was clicked upon this._focusRow(dataPtr); }, /* Process page up or page down key. */ _selectNextPageRow: function(keyCode) { // only relevant if there is a focussed row to start if (this._focussedRow) { // find the index of the new row, ... var i = this._getRowIndexNextPage(keyCode); // ... deselect the rows and select the new one, ... this._deSelectRows(); this._selectRow(this.data[i]); // ... and focus the row, ... this._focusRow(this.data[i]); // ... and scroll it into view this._scrollIntoView(this._focussedRow); // disable key processing by the browser return false; } // enable key processing by the browser return true; }, /* Process up or down key. */ _selectNextRow: function(keyCode, ctrl, shift) { // if there is no focussed row ... if (!this._focussedRow) { // ... then use that keystroke to focus the first row // TODO: is this a real case? this._focusRow(this.data[0]); } else { // ... else if there is no selected row ... if (this.selectedRows.length === 0) { // ... the use that keystroke to select the row this._selectRow(this._focussedRow); } else { var n = this._getRowIndexNextRow(keyCode); if (this._multiselect && shift) { // multiple selection is on, shift key is pressed and // there is a start row: do a range selection. Find the // start and end var s = this.data.indexOf(this._multiSelStartRow); var e = this.data.indexOf(this.data[n]); // swap if necessary if (s > e) { var t = s; s = e; e = t; } // deselect the rows this._deSelectRows(); // and create the new selection for (var i=s; i<=e; i++) { this._selectRow(this.data[i]); } } else if (!this._multiselect || !ctrl) { // multiple selection is off or ctrl and shift were not // used, deselect all rows an select the requested one. this._deSelectRows(); this._selectRow(this.data[n]); } // now foucs the selected row this._focusRow(this.data[n]); } // and scroll the row into view this._scrollIntoView(this._focussedRow); } // disable key processing by the browser return false; }, /* Set the selected row */ _selectRow: function(dataPtr) { // don't add this row if it was just added? Seems like a stange bug fix if (this.selectedRows[this.selectedRows.length-1] !== dataPtr) { // add the row to the selected rows ... this.selectedRows.push(dataPtr); // ... set the CSS class name ... if (dataPtr.rowPtr.row) { // ... if the row was rendered ... dataPtr.rowPtr.row.addClass("sui-lv-row-selected"); } // ... call the onselection changed event handler ... this.callListener("onSelectionChange"); // ... and set the selection marker to true dataPtr.rowPtr.sel = true; } }, /* For a range selection with shift we need a to remember the first * selected row in a multiple selection. */ _setMultiSelStartRow: function(ctrl, shift) { // shift or ctrl key was pressed ... if (this._multiselect && (ctrl || shift)) { // ... and this._multiSelStartRow is not yet set ... if (!this._multiSelStartRow) { // ... remember the first row in a multiple selection, we use // that as the first row for a range selection with the shift // key this._multiSelStartRow = this._focussedRow; } } else { // ... no multiple select so clear the row this._multiSelStartRow = null; } }, /* Set the cell title if the contents of the cell did overflow */ _setTitleOnOverflow: function(cell) { var of = cell.el().clientWidth - cell.el().scrollWidth; cell.el().title = of >= 0 ? "" : cell.text().replace(/<[^>]+>/g,""); }, /* Set the sort direction and header icon and sort the data. */ _sortColumn: function(column) { // if the requested column to sort is different that the current // sorted column ... if (column !== this._sortCol) { // ... clear the sort icon from the current column header ... if (this._sortCol) { this._sortCol.header.el().style.backgroundImage = ""; } // ... set the sort direction to asc ... this._sortUp = true; // .. set the current sort column the requested one this._sortCol = column; } else { // ... else just reverse the search direction this._sortUp = !this._sortUp; } // now sort the data based on the requested column this._sort(); // and redraw the listview this.draw(); }, /* Set the sort icon on the header and sort the data */ _sort: function() { // get the key of the data-column var k = this._sortCol.key; // and the search direction var d = this._sortUp ? 1 : -1; // set the header icon for the sort this._sortCol.header.el().style.backgroundImage = this._sortUp ? "url("+SUI.imgDir+"/"+SUI.resource.lvSortUp+")" : "url("+SUI.imgDir+"/"+SUI.resource.lvSortDown+")"; // and hide all row data (if the row was rendered) for (var i=0; i<this.data.length; i++) { if (this.data[i].rowPtr.row) { this.data[i].rowPtr.row.el().style.display="none"; } } // now sort the data using the specified method if ("text" === this._sortCol.sort) { // text sort: sort data with a case insensitive comparison method this.data.sort( function(a, b) { var aa = String(a[k]); var bb = String(b[k]); return d * ( aa.toLowerCase() < bb.toLowerCase() ? -1 : aa.toLowerCase() > bb.toLowerCase() ? 1 : 0 ); } ); } else if ("number" === this._sortCol.sort) { // number sort: sort data with a number comparison method this.data.sort( function(a, b) { return d*(a[k] - b[k]); } ); } else { // user sort: sort data with a user provided comparison method this._sortCol.sort(this.data, k, d); } // scroll the currently focussed row into view this._scrollIntoView(this._focussedRow); }, /* Process space key. */ _toggleSelection: function(keyCode, ctrl) { // only relevant when multiple selection is on and there is a focussed // row and ctrl is pressed if (this._multiselect && this._focussedRow && ctrl) { // if the focussed row is selected ... if (this._focussedRow.rowPtr.sel) { // ... then deselect it ... this._deSelectRows(this._focussedRow); } else { // ... else select it this._selectRow(this._focussedRow); } // disable key processing by the browser return false; } // enable key processing by the browser return true; } });