UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

599 lines (500 loc) 15.4 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2008 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: * Fabian Jakobs (fjakobs) * Sebastian Werner (wpbasti) * Andreas Ecker (ecker) * Derrell Lipman (derrell) * Christian Hagendorn (chris_schmidt) * Daniel Wagner (d_wagner) ************************************************************************ */ /** * The Tree class implements a tree widget, with collapsible and expandable * container nodes and terminal leaf nodes. You instantiate a Tree object and * then assign the tree a root folder using the {@link #root} property. * * If you don't want to show the root item, you can hide it with the * {@link #hideRoot} property. * * The handling of <b>selections</b> within a tree is somewhat distributed * between the root tree object and the attached {@link qx.ui.tree.selection.SelectionManager}. * To get the currently selected element of a tree use the tree {@link #getSelection} * method and tree {@link #setSelection} to set it. The TreeSelectionManager * handles more coarse-grained issues like providing {@link #selectAll} and * {@link #resetSelection} methods. */ qx.Class.define("qx.ui.tree.Tree", { extend : qx.ui.core.scroll.AbstractScrollArea, implement : [ qx.ui.core.IMultiSelection, qx.ui.form.IModelSelection, qx.ui.form.IField, qx.ui.form.IForm ], include : [ qx.ui.core.MMultiSelectionHandling, qx.ui.core.MContentPadding, qx.ui.form.MModelSelection, qx.ui.form.MForm ], /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ construct : function() { this.base(arguments); this.__content = new qx.ui.container.Composite(new qx.ui.layout.VBox()).set({ allowShrinkY: false, allowGrowX: true }); this.getChildControl("pane").add(this.__content); this.initOpenMode(); this.initRootOpenClose(); this.addListener("changeSelection", this._onChangeSelection, this); this.addListener("keypress", this._onKeyPress, this); }, /* ***************************************************************************** EVENTS ***************************************************************************** */ events : { /** * This event is fired after a tree item was added to the tree. The * {@link qx.event.type.Data#getData} method of the event returns the * added item. */ addItem : "qx.event.type.Data", /** * This event is fired after a tree item has been removed from the tree. * The {@link qx.event.type.Data#getData} method of the event returns the * removed item. */ removeItem : "qx.event.type.Data" }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties : { /** * Control whether tap or double tap should open or close the tapped * folder. */ openMode : { check : ["tap", "dbltap", "none"], init : "dbltap", apply : "_applyOpenMode", event : "changeOpenMode", themeable : true }, /** * The root tree item of the tree to display */ root : { check : "qx.ui.tree.core.AbstractTreeItem", init : null, nullable : true, event : "changeRoot", apply : "_applyRoot" }, /** * Hide the root (Tree) node. This differs from the visibility property in * that this property hides *only* the root node, not the node's children. */ hideRoot : { check : "Boolean", init : false, apply :"_applyHideRoot" }, /** * Whether the Root should have an open/close button. This may also be * used in conjunction with the hideNode property to provide for virtual root * nodes. In the latter case, be very sure that the virtual root nodes are * expanded programmatically, since there will be no open/close button for the * user to open them. */ rootOpenClose : { check : "Boolean", init : false, apply : "_applyRootOpenClose" }, // overridden appearance : { refine: true, init: "tree" }, // overridden focusable : { refine : true, init : true } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { __content : null, /** @type {Class} Pointer to the selection manager to use */ SELECTION_MANAGER : qx.ui.tree.selection.SelectionManager, /* --------------------------------------------------------------------------- WIDGET API --------------------------------------------------------------------------- */ /** * Get the widget, which contains the root tree item. This widget must * have a vertical box layout. * * @return {qx.ui.core.Widget} the children container */ getChildrenContainer : function() { return this.__content; }, // property apply _applyRoot : function(value, old) { var container = this.getChildrenContainer(); if (old && !old.isDisposed()) { container.remove(old); if (old.hasChildren()) { container.remove(old.getChildrenContainer()); } } if (value) { container.add(value); if (value.hasChildren()) { container.add(value.getChildrenContainer()); } value.setVisibility(this.getHideRoot() ? "excluded" : "visible"); value.recursiveAddToWidgetQueue(); } }, // property apply _applyHideRoot : function(value, old) { var root = this.getRoot(); if (!root) { return; } root.setVisibility(value ? "excluded" : "visible"); root.recursiveAddToWidgetQueue(); }, // property apply _applyRootOpenClose : function(value, old) { var root = this.getRoot(); if (!root) { return; } root.recursiveAddToWidgetQueue(); }, /** * Returns the element, to which the content padding should be applied. * * @return {qx.ui.core.Widget} The content padding target. */ _getContentPaddingTarget : function() { return this.__content; }, /* --------------------------------------------------------------------------- SELECTION MANAGER API --------------------------------------------------------------------------- */ /** * Get the tree item following the given item in the tree hierarchy. * * @param treeItem {qx.ui.tree.core.AbstractTreeItem} The tree item to get the item after * @param invisible {Boolean?true} Whether invisible/closed tree items * should be returned as well. * * @return {qx.ui.tree.core.AbstractTreeItem?null} The item after the given item. May be * <code>null</code> if the item is the last item. */ getNextNodeOf : function(treeItem, invisible) { if ((invisible !== false || treeItem.isOpen()) && treeItem.hasChildren()) { return treeItem.getChildren()[0]; } while (treeItem) { var parent = treeItem.getParent(); if (!parent) { return null; } var parentChildren = parent.getChildren(); var index = parentChildren.indexOf(treeItem); if (index > -1 && index < parentChildren.length-1) { return parentChildren[index+1]; } treeItem = parent; } return null; }, /** * Get the tree item preceding the given item in the tree hierarchy. * * @param treeItem {qx.ui.tree.core.AbstractTreeItem} The tree item to get the item before * @param invisible {Boolean?true} Whether invisible/closed tree items * should be returned as well. * * @return {qx.ui.tree.core.AbstractTreeItem?null} The item before the given item. May be * <code>null</code> if the given item is the tree's root. */ getPreviousNodeOf : function(treeItem, invisible) { var parent = treeItem.getParent(); if (!parent) { return null; } if (this.getHideRoot()) { if (parent == this.getRoot()) { if (parent.getChildren()[0] == treeItem) { return null; } } } else { if (treeItem == this.getRoot()) { return null; } } var parentChildren = parent.getChildren(); var index = parentChildren.indexOf(treeItem); if (index > 0) { var folder = parentChildren[index-1]; while ((invisible !== false || folder.isOpen()) && folder.hasChildren()) { var children = folder.getChildren(); folder = children[children.length-1]; } return folder; } else { return parent; } }, /** * Get the tree item's next sibling. * * @param treeItem {qx.ui.tree.core.AbstractTreeItem} The tree item to get the following * sibling of. * * @return {qx.ui.tree.core.AbstractTreeItem?null} The item following the given item. May be * <code>null</code> if the given item is the last in it's nesting * level. */ getNextSiblingOf : function(treeItem) { if (treeItem == this.getRoot()) { return null; } var parent = treeItem.getParent(); var siblings = parent.getChildren(); var index = siblings.indexOf(treeItem); if (index < siblings.length-1) { return siblings[index+1]; } return null; }, /** * Get the tree item's previous sibling. * * @param treeItem {qx.ui.tree.core.AbstractTreeItem} The tree item to get the previous * sibling of. * * @return {qx.ui.tree.core.AbstractTreeItem?null} The item preceding the given item. May be * <code>null</code> if the given item is the first in it's nesting * level. */ getPreviousSiblingOf : function(treeItem) { if (treeItem == this.getRoot()) { return null; } var parent = treeItem.getParent(); var siblings = parent.getChildren(); var index = siblings.indexOf(treeItem); if (index > 0) { return siblings[index-1]; } return null; }, /** * Returns all children of the tree. * * @param recursive {Boolean ? false} whether children of subfolder should be * included * @param invisible {Boolean ? true} whether invisible children should be * included * @return {qx.ui.tree.core.AbstractTreeItem[]} list of children */ getItems : function(recursive, invisible) { if (this.getRoot() != null) { return this.getRoot().getItems(recursive, invisible, this.getHideRoot()); } else { return []; } }, /** * Returns the tree's only "external" child, namely the root node. * * @return {qx.ui.tree.core.AbstractTreeItem[]} Array containing the root node */ getChildren : function() { if (this.getRoot() != null) { return [this.getRoot()]; } else { return []; } }, /* --------------------------------------------------------------------------- POINTER EVENT HANDLER --------------------------------------------------------------------------- */ /** * Returns the tree item, which contains the given widget. * * @param widget {qx.ui.core.Widget} The widget to get the containing tree * item for. * @return {qx.ui.tree.core.AbstractTreeItem|null} The tree item containing the widget. If the * widget is not inside of any tree item <code>null</code> is returned. */ getTreeItem : function(widget) { while (widget) { if (widget == this) { return null; } if (widget instanceof qx.ui.tree.core.AbstractTreeItem) { return widget; } widget = widget.getLayoutParent(); } return null; }, // property apply _applyOpenMode : function(value, old) { if (old == "tap") { this.removeListener("tap", this._onOpen, this); } else if (old == "dbltap") { this.removeListener("dbltap", this._onOpen, this); } if (value == "tap") { this.addListener("tap", this._onOpen, this); } else if (value == "dbltap") { this.addListener("dbltap", this._onOpen, this); } }, /** * Event handler for tap events, which could change a tree item's open * state. * * @param e {qx.event.type.Pointer} The tap event object */ _onOpen : function(e) { var treeItem = this.getTreeItem(e.getTarget()); if (!treeItem ||!treeItem.isOpenable()) { return; } treeItem.setOpen(!treeItem.isOpen()); e.stopPropagation(); }, /** * Event handler for changeSelection events, which opens all parent folders * of the selected folders. * * @param e {qx.event.type.Data} The selection data event. */ _onChangeSelection : function(e) { var selection = e.getData(); // for every selected folder for (var i = 0; i < selection.length; i++) { var folder = selection[i]; // go up all parents and open them while (folder.getParent() != null) { folder = folder.getParent(); folder.setOpen(true); } } }, /** * Event handler for key press events. Open and close the current selected * item on key left and right press. Jump to parent on key left if already * closed. * * @param e {qx.event.type.KeySequence} key event. */ _onKeyPress : function(e) { var item = this._getLeadItem(); if (item !== null) { switch(e.getKeyIdentifier()) { case "Left": if (item.isOpenable() && item.isOpen()) { item.setOpen(false); } else if (item.getParent()) { this.setSelection([item.getParent()]); } break; case "Right": if (item.isOpenable() && !item.isOpen()) { item.setOpen(true); } break; case "Enter": case "Space": if (item.isOpenable()) { item.toggleOpen(); } break; } } } }, /* ***************************************************************************** DESTRUCTOR ***************************************************************************** */ destruct : function() { this._disposeObjects("__content"); } });