UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

897 lines (765 loc) 25.3 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2007 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) ************************************************************************ */ /** * A "virtual" tree * <p> * A number of convenience methods are available in the following mixins: * <ul> * <li>{@link qx.ui.treevirtual.MNode}</li> * <li>{@link qx.ui.treevirtual.MFamily}</li> * </ul> * </p> */ qx.Class.define("qx.ui.treevirtual.TreeVirtual", { extend : qx.ui.table.Table, /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ /** * @param headings {Array | String} * An array containing a list of strings, one for each column, representing * the headings for each column. As a special case, if only one column is * to exist, the string representing its heading need not be enclosed in an * array. * * @param custom {Map ? null} * A map provided (typically) by subclasses, to override the various * supplemental classes allocated within this constructor. For normal * usage, this parameter may be omitted. Each property must be an object * instance or a function which returns an object instance, as indicated by * the defaults listed here: * * <dl> * <dt>initiallyHiddenColumns</dt> * <dd> * {Array?} * A list of column numbers that should be initially invisible. Any * column not mentioned will be initially visible, and if no array * is provided, all columns will be initially visible. * </dd> * <dt>dataModel</dt> * <dd>new qx.ui.treevirtual.SimpleTreeDataModel()</dd> * <dt>treeDataCellRenderer</dt> * <dd> * Instance of {@link qx.ui.treevirtual.SimpleTreeDataCellRenderer}. * Custom data cell renderer for the tree column. * </dd> * <dt>treeColumn</dt> * <dd> * The column number in which the tree is to reside, i.e., which * column uses the SimpleTreeDataCellRenderer or a subclass of it. * </dd> * <dt>defaultDataCellRenderer</dt> * <dd> * Instance of {@link qx.ui.treevirtual.DefaultDataCellRenderer}. * Custom data cell renderer for all columns other than the tree * column. * </dd> * <dt>dataRowRenderer</dt> * <dd>new qx.ui.treevirtual.SimpleTreeDataRowRenderer()</dd> * <dt>selectionManager</dt> * <dd><pre class='javascript'> * function(obj) * { * return new qx.ui.treevirtual.SelectionManager(obj); * } * </pre></dd> * <dt>tableColumnModel</dt> * <dd><pre class='javascript'> * function(obj) * { * return new qx.ui.table.columnmodel.Resize(obj); * } * </pre></dd> * </dl> */ construct : function(headings, custom) { // // Allocate default objects if custom objects are not specified // if (! custom) { custom = { }; } if (! custom.dataModel) { custom.dataModel = new qx.ui.treevirtual.SimpleTreeDataModel(); } if (custom.treeColumn === undefined) { custom.treeColumn = 0; custom.dataModel.setTreeColumn(custom.treeColumn); } if (! custom.treeDataCellRenderer) { custom.treeDataCellRenderer = new qx.ui.treevirtual.SimpleTreeDataCellRenderer(); } if (! custom.defaultDataCellRenderer) { custom.defaultDataCellRenderer = new qx.ui.treevirtual.DefaultDataCellRenderer(); } if (! custom.dataRowRenderer) { custom.dataRowRenderer = new qx.ui.treevirtual.SimpleTreeDataRowRenderer(); } if (! custom.selectionManager) { custom.selectionManager = function(obj) { return new qx.ui.treevirtual.SelectionManager(obj); }; } if (! custom.tableColumnModel) { custom.tableColumnModel = function(obj) { return new qx.ui.table.columnmodel.Resize(obj); }; } // Specify the column headings. We accept a single string (one single // column) or an array of strings (one or more columns). if (qx.lang.Type.isString(headings)) { headings = [ headings ]; } custom.dataModel.setColumns(headings); custom.dataModel.setTreeColumn(custom.treeColumn); // Save a reference to the tree with the data model custom.dataModel.setTree(this); // Call our superclass constructor this.base(arguments, custom.dataModel, custom); // Arrange to redisplay edited data following editing this.addListener("dataEdited", function(e) { this.getDataModel().setData(); }, this); // By default, present the column visibility button only if there are // multiple columns. this.setColumnVisibilityButtonVisible(headings.length > 1); // Set sizes this.setRowHeight(16); this.setMetaColumnCounts(headings.length > 1 ? [ 1, -1 ] : [ 1 ]); // Overflow on trees is always hidden. The internal elements scroll. this.setOverflow("hidden"); // Set the data cell render. We use the SimpleTreeDataCellRenderer for the // tree column, and our DefaultDataCellRenderer for all other columns. var stdcr = custom.treeDataCellRenderer; var ddcr = custom.defaultDataCellRenderer; var tcm = this.getTableColumnModel(); var treeCol = this.getDataModel().getTreeColumn(); for (var i=0; i<headings.length; i++) { tcm.setDataCellRenderer(i, i == treeCol ? stdcr : ddcr); } // Set the data row renderer. this.setDataRowRenderer(custom.dataRowRenderer); // Move the focus with the mouse. This controls the ROW focus indicator. this.setFocusCellOnPointerMove(true); // In a tree we don't typically want a visible cell focus indicator this.setShowCellFocusIndicator(false); // Get the list of pane scrollers var scrollers = this._getPaneScrollerArr(); // For each scroller... for (var i=0; i<scrollers.length; i++) { // Set the pane scrollers to handle the selection before // displaying the focus, so we can manipulate the selected icon. scrollers[i].setSelectBeforeFocus(true); } }, /* ***************************************************************************** EVENTS ***************************************************************************** */ events : { /** * Fired when a tree branch which already has content is opened. * * Event data: the node object from the data model (of the node * being opened) as described in * {@link qx.ui.treevirtual.SimpleTreeDataModel} */ "treeOpenWithContent" : "qx.event.type.Data", /** * Fired when an empty tree branch is opened. * * Event data: the node object from the data model (of the node * being opened) as described in * {@link qx.ui.treevirtual.SimpleTreeDataModel} */ "treeOpenWhileEmpty" : "qx.event.type.Data", /** * Fired when a tree branch is closed. * * Event data: the node object from the data model (of the node * being closed) as described in * {@link qx.ui.treevirtual.SimpleTreeDataModel} */ "treeClose" : "qx.event.type.Data", /** * Fired when the selected rows change. * * Event data: An array of node objects (the selected rows' nodes) * from the data model. Each node object is described in * {@link qx.ui.treevirtual.SimpleTreeDataModel} */ "changeSelection" : "qx.event.type.Data" }, /* ***************************************************************************** STATICS ***************************************************************************** */ statics : { /** * Selection Modes {int} * * NONE * Nothing can ever be selected. * * SINGLE * Allow only one selected item. * * SINGLE_INTERVAL * Allow one contiguous interval of selected items. * * MULTIPLE_INTERVAL * Allow any set of selected items, whether contiguous or not. * * MULTIPLE_INTERVAL_TOGGLE * Like MULTIPLE_INTERVAL, but clicking on an item toggles its selection state. */ SelectionMode : { NONE : qx.ui.table.selection.Model.NO_SELECTION, SINGLE : qx.ui.table.selection.Model.SINGLE_SELECTION, SINGLE_INTERVAL : qx.ui.table.selection.Model.SINGLE_INTERVAL_SELECTION, MULTIPLE_INTERVAL : qx.ui.table.selection.Model.MULTIPLE_INTERVAL_SELECTION, MULTIPLE_INTERVAL_TOGGLE : qx.ui.table.selection.Model.MULTIPLE_INTERVAL_SELECTION_TOGGLE } }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties : { /** * Whether a click on the open/close button should also cause selection of * the row. */ openCloseClickSelectsRow : { check : "Boolean", init : false }, appearance : { refine : true, init : "treevirtual" } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { /** * Return the data model for this tree. * * @return {qx.ui.table.ITableModel} The data model. */ getDataModel : function() { return this.getTableModel(); }, /** * Set whether lines linking tree children shall be drawn on the tree. * Note that not all themes support tree lines. As of the time of this * writing, the Classic theme supports tree lines (and uses +/- icons * which lend themselves to tree lines), while the Modern theme, which * uses right-facing and downward-facing arrows instead of +/-, does not. * * @param b {Boolean} * <i>true</i> if tree lines should be shown; <i>false</i> otherwise. * */ setUseTreeLines : function(b) { var dataModel = this.getDataModel(); var treeCol = dataModel.getTreeColumn(); var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol); dcr.setUseTreeLines(b); // Inform the listeners if (dataModel.hasListener("dataChanged")) { var data = { firstRow : 0, lastRow : dataModel.getRowCount() - 1, firstColumn : 0, lastColumn : dataModel.getColumnCount() - 1 }; dataModel.fireDataEvent("dataChanged", data); } }, /** * Get whether lines linking tree children shall be drawn on the tree. * * @return {Boolean} * <i>true</i> if tree lines are in use; * <i>false</i> otherwise. */ getUseTreeLines : function() { var treeCol = this.getDataModel().getTreeColumn(); var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol); return dcr.getUseTreeLines(); }, /** * Set whether the open/close button should be displayed on a branch, * even if the branch has no children. * * @param b {Boolean} * <i>true</i> if the open/close button should be shown; * <i>false</i> otherwise. * */ setAlwaysShowOpenCloseSymbol : function(b) { var dataModel = this.getDataModel(); var treeCol = dataModel.getTreeColumn(); var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol); dcr.setAlwaysShowOpenCloseSymbol(b); // Inform the listeners if (dataModel.hasListener("dataChanged")) { var data = { firstRow : 0, lastRow : dataModel.getRowCount() - 1, firstColumn : 0, lastColumn : dataModel.getColumnCount() - 1 }; dataModel.fireDataEvent("dataChanged", data); } }, /** * Set whether drawing of first-level tree-node lines are disabled even * if drawing of tree lines is enabled. * * @param b {Boolean} * <i>true</i> if first-level tree lines should be disabled; * <i>false</i> for normal operation. * */ setExcludeFirstLevelTreeLines : function(b) { var dataModel = this.getDataModel(); var treeCol = dataModel.getTreeColumn(); var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol); dcr.setExcludeFirstLevelTreeLines(b); // Inform the listeners if (dataModel.hasListener("dataChanged")) { var data = { firstRow : 0, lastRow : dataModel.getRowCount() - 1, firstColumn : 0, lastColumn : dataModel.getColumnCount() - 1 }; dataModel.fireDataEvent("dataChanged", data); } }, /** * Get whether drawing of first-level tree lines should be disabled even * if drawing of tree lines is enabled. * (See also {@link #getUseTreeLines}) * * @return {Boolean} * <i>true</i> if tree lines are in use; * <i>false</i> otherwise. */ getExcludeFirstLevelTreeLines : function() { var treeCol = this.getDataModel().getTreeColumn(); var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol); return dcr.getExcludeFirstLevelTreeLines(); }, /** * Set whether the open/close button should be displayed on a branch, * even if the branch has no children. * * @return {Boolean} * <i>true</i> if tree lines are in use; * <i>false</i> otherwise. */ getAlwaysShowOpenCloseSymbol : function() { var treeCol = this.getDataModel().getTreeColumn(); var dcr = this.getTableColumnModel().getDataCellRenderer(treeCol); return dcr.getAlwaysShowOpenCloseSymbol(); }, /** * Set the selection mode. * * @param mode {Integer} * The selection mode to be used. It may be any of: * <pre> * qx.ui.treevirtual.TreeVirtual.SelectionMode.NONE: * Nothing can ever be selected. * * qx.ui.treevirtual.TreeVirtual.SelectionMode.SINGLE * Allow only one selected item. * * qx.ui.treevirtual.TreeVirtual.SelectionMode.SINGLE_INTERVAL * Allow one contiguous interval of selected items. * * qx.ui.treevirtual.TreeVirtual.SelectionMode.MULTIPLE_INTERVAL * Allow any selected items, whether contiguous or not. * </pre> * */ setSelectionMode : function(mode) { this.getSelectionModel().setSelectionMode(mode); }, /** * Get the selection mode currently in use. * * @return {Integer} * One of the values documented in {@link #setSelectionMode} */ getSelectionMode : function() { return this.getSelectionModel().getSelectionMode(); }, /** * Obtain the entire hierarchy of labels from the root down to the * specified node. * * @param nodeReference {Object | Integer} * The node for which the hierarchy is desired. The node can be * represented either by the node object, or the node id (as would have * been returned by addBranch(), addLeaf(), etc.) * * @return {Array} * The returned array contains one string for each label in the * hierarchy of the node specified by the parameter. Element 0 of the * array contains the label of the root node, element 1 contains the * label of the node immediately below root in the specified node's * hierarchy, etc., down to the last element in the array contain the * label of the node referenced by the parameter. */ getHierarchy : function(nodeReference) { var _this = this; var components = []; var node; var nodeId; if (typeof(nodeReference) == "object") { node = nodeReference; nodeId = node.nodeId; } else if (typeof(nodeReference) == "number") { nodeId = nodeReference; } else { throw new Error("Expected node object or node id"); } function addHierarchy(nodeId) { // If we're at the root... if (! nodeId) { // ... then we're done return ; } // Get the requested node var node = _this.getDataModel().getData()[nodeId]; // Add its label to the hierarchy components components.unshift(node.label); // Call recursively to our parent node. addHierarchy(node.parentNodeId); } addHierarchy(nodeId); return components; }, /** * Return the nodes that are currently selected. * * @return {Array} * An array containing the nodes that are currently selected. */ getSelectedNodes : function() { return this.getDataModel().getSelectedNodes(); }, /** * Event handler. Called when a key was pressed. * * We handle the Enter key to toggle opened/closed tree state. All * other keydown events are passed to our superclass. * * @param evt {Map} * The event. * */ _onKeyPress : function(evt) { if (!this.getEnabled()) { return; } var identifier = evt.getKeyIdentifier(); var consumed = false; var modifiers = evt.getModifiers(); if (modifiers == 0) { switch(identifier) { case "Enter": // Get the data model var dm = this.getDataModel(); var focusedCol = this.getFocusedColumn(); var treeCol = dm.getTreeColumn(); if (focusedCol == treeCol) { // Get the focused node var focusedRow = this.getFocusedRow(); var node = dm.getNode(focusedRow); if (! node.bHideOpenClose && node.type != qx.ui.treevirtual.SimpleTreeDataModel.Type.LEAF) { dm.setState(node, { bOpened : ! node.bOpened }); } consumed = true; } break; case "Left": this.moveFocusedCell(-1, 0); break; case "Right": this.moveFocusedCell(1, 0); break; } } else if (modifiers == qx.event.type.Dom.CTRL_MASK) { switch(identifier) { case "Left": // Get the data model var dm = this.getDataModel(); // Get the focused node var focusedRow = this.getFocusedRow(); var treeCol = dm.getTreeColumn(); var node = dm.getNode(focusedRow); // If it's an open branch and open/close is allowed... if ((node.type == qx.ui.treevirtual.SimpleTreeDataModel.Type.BRANCH) && ! node.bHideOpenClose && node.bOpened) { // ... then close it dm.setState(node, { bOpened : ! node.bOpened }); } // Reset the focus to the current node this.setFocusedCell(treeCol, focusedRow, true); consumed = true; break; case "Right": // Get the data model var dm = this.getDataModel(); // Get the focused node focusedRow = this.getFocusedRow(); treeCol = dm.getTreeColumn(); node = dm.getNode(focusedRow); // If it's a closed branch and open/close is allowed... if ((node.type == qx.ui.treevirtual.SimpleTreeDataModel.Type.BRANCH) && ! node.bHideOpenClose && ! node.bOpened) { // ... then open it dm.setState(node, { bOpened : ! node.bOpened }); } // Reset the focus to the current node this.setFocusedCell(treeCol, focusedRow, true); consumed = true; break; } } else if (modifiers == qx.event.type.Dom.SHIFT_MASK) { switch(identifier) { case "Left": // Get the data model var dm = this.getDataModel(); // Get the focused node var focusedRow = this.getFocusedRow(); var treeCol = dm.getTreeColumn(); var node = dm.getNode(focusedRow); // If we're not at the top-level already... if (node.parentNodeId) { // Find out what rendered row our parent node is at var rowIndex = dm.getRowFromNodeId(node.parentNodeId); // Set the focus to our parent this.setFocusedCell(this._focusedCol, rowIndex, true); } consumed = true; break; case "Right": // Get the data model var dm = this.getDataModel(); // Get the focused node focusedRow = this.getFocusedRow(); treeCol = dm.getTreeColumn(); node = dm.getNode(focusedRow); // If we're on a branch and open/close is allowed... if ((node.type == qx.ui.treevirtual.SimpleTreeDataModel.Type.BRANCH) && ! node.bHideOpenClose) { // ... then first ensure the branch is open if (! node.bOpened) { dm.setState(node, { bOpened : ! node.bOpened }); } // If this node has children... if (node.children.length > 0) { // ... then move the focus to the first child this.moveFocusedCell(0, 1); } } consumed = true; break; } } // Was this one of our events that we handled? if (consumed) { // Yup. Don't propagate it. evt.preventDefault(); evt.stopPropagation(); } else { // It's not one of ours. Let our superclass handle this event this.base(arguments, evt); } }, /** * Event handler. Called when the selection has changed. * * @param evt {Map} * The event. * */ _onSelectionChanged : function(evt) { // Clear the old list of selected nodes this.getDataModel()._clearSelections(); // If selections are allowed, pass an event to our listeners if (this.getSelectionMode() != qx.ui.treevirtual.TreeVirtual.SelectionMode.NONE) { var selectedNodes = this._calculateSelectedNodes(); // Get the now-focused this.fireDataEvent("changeSelection", selectedNodes); } // Call the superclass method this.base(arguments, evt); }, /** * Calculate and return the set of nodes which are currently selected by * the user, on the screen. In the process of calculating which nodes * are selected, the nodes corresponding to the selected rows on the * screen are marked as selected by setting their <i>bSelected</i> * property to true, and all previously-selected nodes have their * <i>bSelected</i> property reset to false. * * @return {Array} * An array of nodes matching the set of rows which are selected on the * screen. */ _calculateSelectedNodes : function() { // Create an array of nodes that are now selected var stdcm = this.getDataModel(); var selectedRanges = this.getSelectionModel().getSelectedRanges(); var selectedNodes = []; var node; for (var i=0; i<selectedRanges.length; i++) { for (var j=selectedRanges[i].minIndex; j<=selectedRanges[i].maxIndex; j++) { node = stdcm.getNode(j); stdcm.setState(node, { bSelected : true }); selectedNodes.push(node); } } return selectedNodes; }, /** * Set the overflow mode. * * @param s {String} * Overflow mode. The only allowable mode is "hidden". * * * @throws {Error} * Error if tree overflow mode is other than "hidden" */ setOverflow : function(s) { if (s != "hidden") { throw new Error("Tree overflow must be hidden. " + "The internal elements of it will scroll."); } } } });