UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

1,889 lines (1,516 loc) 47.7 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 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: * Sebastian Werner (wpbasti) ************************************************************************ */ /** * Generic selection manager to bring rich desktop like selection behavior * to widgets and low-level interactive controls. * * The selection handling supports both Shift and Ctrl/Meta modifies like * known from native applications. */ qx.Class.define("qx.ui.core.selection.Abstract", { type : "abstract", extend : qx.core.Object, /* ***************************************************************************** CONSTRUCTOR ***************************************************************************** */ construct : function() { this.base(arguments); // {Map} Internal selection storage this.__selection = {}; }, /* ***************************************************************************** EVENTS ***************************************************************************** */ events : { /** Fires after the selection was modified. Contains the selection under the data property. */ "changeSelection" : "qx.event.type.Data" }, /* ***************************************************************************** PROPERTIES ***************************************************************************** */ properties : { /** * Selects the selection mode to use. * * * single: One or no element is selected * * multi: Multi items could be selected. Also allows empty selections. * * additive: Easy Web-2.0 selection mode. Allows multiple selections without modifier keys. * * one: If possible always exactly one item is selected */ mode : { check : [ "single", "multi", "additive", "one" ], init : "single", apply : "_applyMode" }, /** * Enable drag selection (multi selection of items through * dragging the pointer in pressed states). * * Only possible for the modes <code>multi</code> and <code>additive</code> */ drag : { check : "Boolean", init : false }, /** * Enable quick selection mode, where no tap is needed to change the selection. * * Only possible for the modes <code>single</code> and <code>one</code>. */ quick : { check : "Boolean", init : false } }, /* ***************************************************************************** MEMBERS ***************************************************************************** */ members : { __scrollStepX : 0, __scrollStepY : 0, __scrollTimer : null, __frameScroll : null, __lastRelX : null, __lastRelY : null, __frameLocation : null, __dragStartX : null, __dragStartY : null, __inCapture : null, __pointerX : null, __pointerY : null, __moveDirectionX : null, __moveDirectionY : null, __selectionModified : null, __selectionContext : null, __leadItem : null, __selection : null, __anchorItem : null, __pointerDownOnSelected : null, // A flag that signals an user interaction, which means the selection change // was triggered by pointer or keyboard [BUG #3344] _userInteraction : false, __oldScrollTop : null, /* --------------------------------------------------------------------------- USER APIS --------------------------------------------------------------------------- */ /** * Returns the selection context. One of <code>tap</code>, * <code>quick</code>, <code>drag</code> or <code>key</code> or * <code>null</code>. * * @return {String} One of <code>tap</code>, <code>quick</code>, * <code>drag</code> or <code>key</code> or <code>null</code> */ getSelectionContext : function() { return this.__selectionContext; }, /** * Selects all items of the managed object. * */ selectAll : function() { var mode = this.getMode(); if (mode == "single" || mode == "one") { throw new Error("Can not select all items in selection mode: " + mode); } this._selectAllItems(); this._fireChange(); }, /** * Selects the given item. Replaces current selection * completely with the new item. * * Use {@link #addItem} instead if you want to add new * items to an existing selection. * * @param item {Object} Any valid item */ selectItem : function(item) { this._setSelectedItem(item); var mode = this.getMode(); if (mode !== "single" && mode !== "one") { this._setLeadItem(item); this._setAnchorItem(item); } this._scrollItemIntoView(item); this._fireChange(); }, /** * Adds the given item to the existing selection. * * Use {@link #selectItem} instead if you want to replace * the current selection. * * @param item {Object} Any valid item */ addItem : function(item) { var mode = this.getMode(); if (mode === "single" || mode === "one") { this._setSelectedItem(item); } else { if (this._getAnchorItem() == null) { this._setAnchorItem(item); } this._setLeadItem(item); this._addToSelection(item); } this._scrollItemIntoView(item); this._fireChange(); }, /** * Removes the given item from the selection. * * Use {@link #clearSelection} when you want to clear * the whole selection at once. * * @param item {Object} Any valid item */ removeItem : function(item) { this._removeFromSelection(item); if (this.getMode() === "one" && this.isSelectionEmpty()) { var selected = this._applyDefaultSelection(); // Do not fire any event in this case. if (selected == item) { return; } } if (this.getLeadItem() == item) { this._setLeadItem(null); } if (this._getAnchorItem() == item) { this._setAnchorItem(null); } this._fireChange(); }, /** * Selects an item range between two given items. * * @param begin {Object} Item to start with * @param end {Object} Item to end at */ selectItemRange : function(begin, end) { var mode = this.getMode(); if (mode == "single" || mode == "one") { throw new Error("Can not select multiple items in selection mode: " + mode); } this._selectItemRange(begin, end); this._setAnchorItem(begin); this._setLeadItem(end); this._scrollItemIntoView(end); this._fireChange(); }, /** * Clears the whole selection at once. Also * resets the lead and anchor items and their * styles. * */ clearSelection : function() { if (this.getMode() == "one") { var selected = this._applyDefaultSelection(true); if (selected != null) { return; } } this._clearSelection(); this._setLeadItem(null); this._setAnchorItem(null); this._fireChange(); }, /** * Replaces current selection with given array of items. * * Please note that in single selection scenarios it is more * efficient to directly use {@link #selectItem}. * * @param items {Array} Items to select */ replaceSelection : function(items) { var mode = this.getMode(); if (mode == "one" || mode === "single") { if (items.length > 1) { throw new Error("Could not select more than one items in mode: " + mode + "!"); } if (items.length == 1) { this.selectItem(items[0]); } else { this.clearSelection(); } return; } else { this._replaceMultiSelection(items); } }, /** * Get the selected item. This method does only work in <code>single</code> * selection mode. * * @return {Object} The selected item. */ getSelectedItem : function() { var mode = this.getMode(); if (mode === "single" || mode === "one") { var result = this._getSelectedItem(); return result != undefined ? result : null; } throw new Error("The method getSelectedItem() is only supported in 'single' and 'one' selection mode!"); }, /** * Returns an array of currently selected items. * * Note: The result is only a set of selected items, so the order can * differ from the sequence in which the items were added. * * @return {Object[]} List of items. */ getSelection : function() { return Object.values(this.__selection); }, /** * Returns the selection sorted by the index in the * container of the selection (the assigned widget) * * @return {Object[]} Sorted list of items */ getSortedSelection : function() { var children = this.getSelectables(); var sel = Object.values(this.__selection); sel.sort(function(a, b) { return children.indexOf(a) - children.indexOf(b); }); return sel; }, /** * Detects whether the given item is currently selected. * * @param item {var} Any valid selectable item * @return {Boolean} Whether the item is selected */ isItemSelected : function(item) { var hash = this._selectableToHashCode(item); return this.__selection[hash] !== undefined; }, /** * Whether the selection is empty * * @return {Boolean} Whether the selection is empty */ isSelectionEmpty : function() { return qx.lang.Object.isEmpty(this.__selection); }, /** * Invert the selection. Select the non selected and deselect the selected. */ invertSelection: function() { var mode = this.getMode(); if (mode === "single" || mode === "one") { throw new Error("The method invertSelection() is only supported in 'multi' and 'additive' selection mode!"); } var selectables = this.getSelectables(); for (var i = 0; i < selectables.length; i++) { this._toggleInSelection(selectables[i]); } this._fireChange(); }, /* --------------------------------------------------------------------------- LEAD/ANCHOR SUPPORT --------------------------------------------------------------------------- */ /** * Sets the lead item. Generally the item which was last modified * by the user (tapped on etc.) * * @param value {Object} Any valid item or <code>null</code> */ _setLeadItem : function(value) { var old = this.__leadItem; if (old !== null) { this._styleSelectable(old, "lead", false); } if (value !== null) { this._styleSelectable(value, "lead", true); } this.__leadItem = value; }, /** * Returns the current lead item. Generally the item which was last modified * by the user (tapped on etc.) * * @return {Object} The lead item or <code>null</code> */ getLeadItem : function() { return this.__leadItem; }, /** * Sets the anchor item. This is the item which is the starting * point for all range selections. Normally this is the item which was * tapped on the last time without any modifier keys pressed. * * @param value {Object} Any valid item or <code>null</code> */ _setAnchorItem : function(value) { var old = this.__anchorItem; if (old != null) { this._styleSelectable(old, "anchor", false); } if (value != null) { this._styleSelectable(value, "anchor", true); } this.__anchorItem = value; }, /** * Returns the current anchor item. This is the item which is the starting * point for all range selections. Normally this is the item which was * tapped on the last time without any modifier keys pressed. * * @return {Object} The anchor item or <code>null</code> */ _getAnchorItem : function() { return this.__anchorItem !== null ? this.__anchorItem : null; }, /* --------------------------------------------------------------------------- BASIC SUPPORT --------------------------------------------------------------------------- */ /** * Whether the given item is selectable. * * @param item {var} Any item * @return {Boolean} <code>true</code> when the item is selectable */ _isSelectable : function(item) { throw new Error("Abstract method call: _isSelectable()"); }, /** * Finds the selectable instance from a pointer event * * @param event {qx.event.type.Pointer} The pointer event * @return {Object|null} The resulting selectable */ _getSelectableFromPointerEvent : function(event) { var target = event.getTarget(); // check for target (may be null when leaving the viewport) [BUG #4378] if (target && this._isSelectable(target)) { return target; } return null; }, /** * Returns an unique hashcode for the given item. * * @param item {var} Any item * @return {String} A valid hashcode */ _selectableToHashCode : function(item) { throw new Error("Abstract method call: _selectableToHashCode()"); }, /** * Updates the style (appearance) of the given item. * * @param item {var} Item to modify * @param type {String} Any of <code>selected</code>, <code>anchor</code> or <code>lead</code> * @param enabled {Boolean} Whether the given style should be added or removed. */ _styleSelectable : function(item, type, enabled) { throw new Error("Abstract method call: _styleSelectable()"); }, /** * Enables capturing of the container. * */ _capture : function() { throw new Error("Abstract method call: _capture()"); }, /** * Releases capturing of the container * */ _releaseCapture : function() { throw new Error("Abstract method call: _releaseCapture()"); }, /* --------------------------------------------------------------------------- DIMENSION AND LOCATION --------------------------------------------------------------------------- */ /** * Returns the location of the container * * @return {Map} Map with the keys <code>top</code>, <code>right</code>, * <code>bottom</code> and <code>left</code>. */ _getLocation : function() { throw new Error("Abstract method call: _getLocation()"); }, /** * Returns the dimension of the container (available scrolling space). * * @return {Map} Map with the keys <code>width</code> and <code>height</code>. */ _getDimension : function() { throw new Error("Abstract method call: _getDimension()"); }, /** * Returns the relative (to the container) horizontal location of the given item. * * @param item {var} Any item * @return {Map} A map with the keys <code>left</code> and <code>right</code>. */ _getSelectableLocationX : function(item) { throw new Error("Abstract method call: _getSelectableLocationX()"); }, /** * Returns the relative (to the container) horizontal location of the given item. * * @param item {var} Any item * @return {Map} A map with the keys <code>top</code> and <code>bottom</code>. */ _getSelectableLocationY : function(item) { throw new Error("Abstract method call: _getSelectableLocationY()"); }, /* --------------------------------------------------------------------------- SCROLL SUPPORT --------------------------------------------------------------------------- */ /** * Returns the scroll position of the container. * * @return {Map} Map with the keys <code>left</code> and <code>top</code>. */ _getScroll : function() { throw new Error("Abstract method call: _getScroll()"); }, /** * Scrolls by the given offset * * @param xoff {Integer} Horizontal offset to scroll by * @param yoff {Integer} Vertical offset to scroll by */ _scrollBy : function(xoff, yoff) { throw new Error("Abstract method call: _scrollBy()"); }, /** * Scrolls the given item into the view (make it visible) * * @param item {var} Any item */ _scrollItemIntoView : function(item) { throw new Error("Abstract method call: _scrollItemIntoView()"); }, /* --------------------------------------------------------------------------- QUERY SUPPORT --------------------------------------------------------------------------- */ /** * Returns all selectable items of the container. * * @param all {Boolean} true for all selectables, false for the * selectables the user can interactively select * @return {Array} A list of items */ getSelectables : function(all) { throw new Error("Abstract method call: getSelectables()"); }, /** * Returns all selectable items between the two given items. * * The items could be given in any order. * * @param item1 {var} First item * @param item2 {var} Second item * @return {Array} List of items */ _getSelectableRange : function(item1, item2) { throw new Error("Abstract method call: _getSelectableRange()"); }, /** * Returns the first selectable item. * * @return {var} The first selectable item */ _getFirstSelectable : function() { throw new Error("Abstract method call: _getFirstSelectable()"); }, /** * Returns the last selectable item. * * @return {var} The last selectable item */ _getLastSelectable : function() { throw new Error("Abstract method call: _getLastSelectable()"); }, /** * Returns a selectable item which is related to the given * <code>item</code> through the value of <code>relation</code>. * * @param item {var} Any item * @param relation {String} A valid relation: <code>above</code>, * <code>right</code>, <code>under</code> or <code>left</code> * @return {var} The related item */ _getRelatedSelectable : function(item, relation) { throw new Error("Abstract method call: _getRelatedSelectable()"); }, /** * Returns the item which should be selected on pageUp/pageDown. * * May also scroll to the needed position. * * @param lead {var} The current lead item * @param up {Boolean?false} Which page key was pressed: * <code>up</code> or <code>down</code>. */ _getPage : function(lead, up) { throw new Error("Abstract method call: _getPage()"); }, /* --------------------------------------------------------------------------- PROPERTY APPLY ROUTINES --------------------------------------------------------------------------- */ // property apply _applyMode : function(value, old) { this._setLeadItem(null); this._setAnchorItem(null); this._clearSelection(); // Mode "one" requires one selected item if (value === "one") { this._applyDefaultSelection(true); } this._fireChange(); }, /* --------------------------------------------------------------------------- POINTER SUPPORT --------------------------------------------------------------------------- */ /** * This method should be connected to the <code>pointerover</code> event * of the managed object. * * @param event {qx.event.type.Pointer} A valid pointer event */ handlePointerOver : function(event) { // All browsers (except Opera) fire a native "mouseover" event when a scroll appears // by keyboard interaction. We have to ignore the event to avoid a selection for // "pointerover" (quick selection). For more details see [BUG #4225] if(this.__oldScrollTop != null && this.__oldScrollTop != this._getScroll().top) { this.__oldScrollTop = null; return; } // quick select should only work on mouse events if (event.getPointerType() != "mouse") { return; } // this is a method invoked by an user interaction, so be careful to // set / clear the mark this._userInteraction [BUG #3344] this._userInteraction = true; if (!this.getQuick()) { this._userInteraction = false; return; } var mode = this.getMode(); if (mode !== "one" && mode !== "single") { this._userInteraction = false; return; } var item = this._getSelectableFromPointerEvent(event); if (item === null) { this._userInteraction = false; return; } this._setSelectedItem(item); // Be sure that item is in view // This does not feel good when pointerover is used // this._scrollItemIntoView(item); // Fire change event as needed this._fireChange("quick"); this._userInteraction = false; }, /** * This method should be connected to the <code>pointerdown</code> event * of the managed object. * * @param event {qx.event.type.Pointer} A valid pointer event */ handlePointerDown : function(event) { // this is a method invoked by an user interaction, so be careful to // set / clear the mark this._userInteraction [BUG #3344] this._userInteraction = true; var item = this._getSelectableFromPointerEvent(event); if (item === null) { this._userInteraction = false; return; } // Read in keyboard modifiers var isCtrlPressed = event.isCtrlPressed() || (qx.core.Environment.get("os.name") == "osx" && event.isMetaPressed()); var isShiftPressed = event.isShiftPressed(); // tapping on selected items deselect on pointerup, not on pointerdown if (this.isItemSelected(item) && !isShiftPressed && !isCtrlPressed && !this.getDrag()) { this.__pointerDownOnSelected = item; this._userInteraction = false; return; } else { this.__pointerDownOnSelected = null; } // Be sure that item is in view this._scrollItemIntoView(item); // Drag selection var mode = this.getMode(); if ( this.getDrag() && mode !== "single" && mode !== "one" && !isShiftPressed && !isCtrlPressed && event.getPointerType() == "mouse" ) { this._setAnchorItem(item); this._setLeadItem(item); // Cache location/scroll data this.__frameLocation = this._getLocation(); this.__frameScroll = this._getScroll(); // Store position at start this.__dragStartX = event.getDocumentLeft() + this.__frameScroll.left; this.__dragStartY = event.getDocumentTop() + this.__frameScroll.top; // Switch to capture mode this.__inCapture = true; this._capture(); } // Fire change event as needed this._fireChange("tap"); this._userInteraction = false; }, /** * This method should be connected to the <code>tap</code> event * of the managed object. * * @param event {qx.event.type.Tap} A valid pointer event */ handleTap : function(event) { // this is a method invoked by an user interaction, so be careful to // set / clear the mark this._userInteraction [BUG #3344] this._userInteraction = true; // Read in keyboard modifiers var isCtrlPressed = event.isCtrlPressed() || (qx.core.Environment.get("os.name") == "osx" && event.isMetaPressed()); var isShiftPressed = event.isShiftPressed(); if (!isCtrlPressed && !isShiftPressed && this.__pointerDownOnSelected != null) { this._userInteraction = false; var item = this._getSelectableFromPointerEvent(event); if (item === null || !this.isItemSelected(item)) { return; } } var item = this._getSelectableFromPointerEvent(event); if (item === null) { this._userInteraction = false; return; } // Action depends on selected mode switch(this.getMode()) { case "single": case "one": this._setSelectedItem(item); break; case "additive": this._setLeadItem(item); this._setAnchorItem(item); this._toggleInSelection(item); break; case "multi": // Update lead item this._setLeadItem(item); // Create/Update range selection if (isShiftPressed) { var anchor = this._getAnchorItem(); if (anchor === null) { anchor = this._getFirstSelectable(); this._setAnchorItem(anchor); } this._selectItemRange(anchor, item, isCtrlPressed); } // Toggle in selection else if (isCtrlPressed) { this._setAnchorItem(item); this._toggleInSelection(item); } // Replace current selection else { this._setAnchorItem(item); this._setSelectedItem(item); } break; } // Cleanup operation this._cleanup(); }, /** * This method should be connected to the <code>losecapture</code> event * of the managed object. * * @param event {qx.event.type.Pointer} A valid pointer event */ handleLoseCapture : function(event) { this._cleanup(); }, /** * This method should be connected to the <code>pointermove</code> event * of the managed object. * * @param event {qx.event.type.Pointer} A valid pointer event */ handlePointerMove : function(event) { // Only relevant when capturing is enabled if (!this.__inCapture) { return; } // Update pointer position cache this.__pointerX = event.getDocumentLeft(); this.__pointerY = event.getDocumentTop(); // this is a method invoked by an user interaction, so be careful to // set / clear the mark this._userInteraction [BUG #3344] this._userInteraction = true; // Detect move directions var dragX = this.__pointerX + this.__frameScroll.left; if (dragX > this.__dragStartX) { this.__moveDirectionX = 1; } else if (dragX < this.__dragStartX) { this.__moveDirectionX = -1; } else { this.__moveDirectionX = 0; } var dragY = this.__pointerY + this.__frameScroll.top; if (dragY > this.__dragStartY) { this.__moveDirectionY = 1; } else if (dragY < this.__dragStartY) { this.__moveDirectionY = -1; } else { this.__moveDirectionY = 0; } // Update scroll steps var location = this.__frameLocation; if (this.__pointerX < location.left) { this.__scrollStepX = this.__pointerX - location.left; } else if (this.__pointerX > location.right) { this.__scrollStepX = this.__pointerX - location.right; } else { this.__scrollStepX = 0; } if (this.__pointerY < location.top) { this.__scrollStepY = this.__pointerY - location.top; } else if (this.__pointerY > location.bottom) { this.__scrollStepY = this.__pointerY - location.bottom; } else { this.__scrollStepY = 0; } // Dynamically create required timer instance if (!this.__scrollTimer) { this.__scrollTimer = new qx.event.Timer(100); this.__scrollTimer.addListener("interval", this._onInterval, this); } // Start interval this.__scrollTimer.start(); // Auto select based on new cursor position this._autoSelect(); event.stopPropagation(); this._userInteraction = false; }, /** * This method should be connected to the <code>addItem</code> event * of the managed object. * * @param e {qx.event.type.Data} The event object */ handleAddItem : function(e) { var item = e.getData(); if (this.getMode() === "one" && this.isSelectionEmpty()) { this.addItem(item); } }, /** * This method should be connected to the <code>removeItem</code> event * of the managed object. * * @param e {qx.event.type.Data} The event object */ handleRemoveItem : function(e) { this.removeItem(e.getData()); }, /* --------------------------------------------------------------------------- POINTER SUPPORT INTERNALS --------------------------------------------------------------------------- */ /** * Stops all timers, release capture etc. to cleanup drag selection */ _cleanup : function() { if (!this.getDrag() && this.__inCapture) { return; } // Fire change event if needed if (this.__selectionModified) { this._fireChange("tap"); } // Remove flags delete this.__inCapture; delete this.__lastRelX; delete this.__lastRelY; // Stop capturing this._releaseCapture(); // Stop timer if (this.__scrollTimer) { this.__scrollTimer.stop(); } }, /** * Event listener for timer used by drag selection * * @param e {qx.event.type.Event} Timer event */ _onInterval : function(e) { // Scroll by defined block size this._scrollBy(this.__scrollStepX, this.__scrollStepY); // Update scroll cache this.__frameScroll = this._getScroll(); // Auto select based on new scroll position and cursor this._autoSelect(); }, /** * Automatically selects items based on the pointer movement during a drag selection */ _autoSelect : function() { var inner = this._getDimension(); // Get current relative Y position and compare it with previous one var relX = Math.max(0, Math.min(this.__pointerX - this.__frameLocation.left, inner.width)) + this.__frameScroll.left; var relY = Math.max(0, Math.min(this.__pointerY - this.__frameLocation.top, inner.height)) + this.__frameScroll.top; // Compare old and new relative coordinates (for performance reasons) if (this.__lastRelX === relX && this.__lastRelY === relY) { return; } this.__lastRelX = relX; this.__lastRelY = relY; // Cache anchor var anchor = this._getAnchorItem(); var lead = anchor; // Process X-coordinate var moveX = this.__moveDirectionX; var nextX, locationX; while (moveX !== 0) { // Find next item to process depending on current scroll direction nextX = moveX > 0 ? this._getRelatedSelectable(lead, "right") : this._getRelatedSelectable(lead, "left"); // May be null (e.g. first/last item) if (nextX !== null) { locationX = this._getSelectableLocationX(nextX); // Continue when the item is in the visible area if ( (moveX > 0 && locationX.left <= relX) || (moveX < 0 && locationX.right >= relX) ) { lead = nextX; continue; } } // Otherwise break break; } // Process Y-coordinate var moveY = this.__moveDirectionY; var nextY, locationY; while (moveY !== 0) { // Find next item to process depending on current scroll direction nextY = moveY > 0 ? this._getRelatedSelectable(lead, "under") : this._getRelatedSelectable(lead, "above"); // May be null (e.g. first/last item) if (nextY !== null) { locationY = this._getSelectableLocationY(nextY); // Continue when the item is in the visible area if ( (moveY > 0 && locationY.top <= relY) || (moveY < 0 && locationY.bottom >= relY) ) { lead = nextY; continue; } } // Otherwise break break; } // Differenciate between the two supported modes var mode = this.getMode(); if (mode === "multi") { // Replace current selection with new range this._selectItemRange(anchor, lead); } else if (mode === "additive") { // Behavior depends on the fact whether the // anchor item is selected or not if (this.isItemSelected(anchor)) { this._selectItemRange(anchor, lead, true); } else { this._deselectItemRange(anchor, lead); } // Improve performance. This mode does not rely // on full ranges as it always extend the old // selection/deselection. this._setAnchorItem(lead); } // Fire change event as needed this._fireChange("drag"); }, /* --------------------------------------------------------------------------- KEYBOARD SUPPORT --------------------------------------------------------------------------- */ /** * @type {Map} All supported navigation keys * * @lint ignoreReferenceField(__navigationKeys) */ __navigationKeys : { Home : 1, Down : 1 , Right : 1, PageDown : 1, End : 1, Up : 1, Left : 1, PageUp : 1 }, /** * This method should be connected to the <code>keypress</code> event * of the managed object. * * @param event {qx.event.type.KeySequence} A valid key sequence event */ handleKeyPress : function(event) { // this is a method invoked by an user interaction, so be careful to // set / clear the mark this._userInteraction [BUG #3344] this._userInteraction = true; var current, next; var key = event.getKeyIdentifier(); var mode = this.getMode(); // Support both control keys on Mac var isCtrlPressed = event.isCtrlPressed() || (qx.core.Environment.get("os.name") == "osx" && event.isMetaPressed()); var isShiftPressed = event.isShiftPressed(); var consumed = false; if (key === "A" && isCtrlPressed) { if (mode !== "single" && mode !== "one") { this._selectAllItems(); consumed = true; } } else if (key === "Escape") { if (mode !== "single" && mode !== "one") { this._clearSelection(); consumed = true; } } else if (key === "Space") { var lead = this.getLeadItem(); if (lead != null && !isShiftPressed) { if (isCtrlPressed || mode === "additive") { this._toggleInSelection(lead); } else { this._setSelectedItem(lead); } consumed = true; } } else if (this.__navigationKeys[key]) { consumed = true; if (mode === "single" || mode == "one") { current = this._getSelectedItem(); } else { current = this.getLeadItem(); } if (current !== null) { switch(key) { case "Home": next = this._getFirstSelectable(); break; case "End": next = this._getLastSelectable(); break; case "Up": next = this._getRelatedSelectable(current, "above"); break; case "Down": next = this._getRelatedSelectable(current, "under"); break; case "Left": next = this._getRelatedSelectable(current, "left"); break; case "Right": next = this._getRelatedSelectable(current, "right"); break; case "PageUp": next = this._getPage(current, true); break; case "PageDown": next = this._getPage(current, false); break; } } else { switch(key) { case "Home": case "Down": case "Right": case "PageDown": next = this._getFirstSelectable(); break; case "End": case "Up": case "Left": case "PageUp": next = this._getLastSelectable(); break; } } // Process result if (next !== null) { switch(mode) { case "single": case "one": this._setSelectedItem(next); break; case "additive": this._setLeadItem(next); break; case "multi": if (isShiftPressed) { var anchor = this._getAnchorItem(); if (anchor === null) { this._setAnchorItem(anchor = this._getFirstSelectable()); } this._setLeadItem(next); this._selectItemRange(anchor, next, isCtrlPressed); } else { this._setAnchorItem(next); this._setLeadItem(next); if (!isCtrlPressed) { this._setSelectedItem(next); } } break; } this.__oldScrollTop = this._getScroll().top; this._scrollItemIntoView(next); } } if (consumed) { // Stop processed events event.stop(); // Fire change event as needed this._fireChange("key"); } this._userInteraction = false; }, /* --------------------------------------------------------------------------- SUPPORT FOR ITEM RANGES --------------------------------------------------------------------------- */ /** * Adds all items to the selection */ _selectAllItems : function() { var range = this.getSelectables(); for (var i=0, l=range.length; i<l; i++) { this._addToSelection(range[i]); } }, /** * Clears current selection */ _clearSelection : function() { var selection = this.__selection; for (var hash in selection) { this._removeFromSelection(selection[hash]); } this.__selection = {}; }, /** * Select a range from <code>item1</code> to <code>item2</code>. * * @param item1 {Object} Start with this item * @param item2 {Object} End with this item * @param extend {Boolean?false} Whether the current * selection should be replaced or extended. */ _selectItemRange : function(item1, item2, extend) { var range = this._getSelectableRange(item1, item2); // Remove items which are not in the detected range if (!extend) { var selected = this.__selection; var mapped = this.__rangeToMap(range); for (var hash in selected) { if (!mapped[hash]) { this._removeFromSelection(selected[hash]); } } } // Add new items to the selection for (var i=0, l=range.length; i<l; i++) { this._addToSelection(range[i]); } }, /** * Deselect all items between <code>item1</code> and <code>item2</code>. * * @param item1 {Object} Start with this item * @param item2 {Object} End with this item */ _deselectItemRange : function(item1, item2) { var range = this._getSelectableRange(item1, item2); for (var i=0, l=range.length; i<l; i++) { this._removeFromSelection(range[i]); } }, /** * Internal method to convert a range to a map of hash * codes for faster lookup during selection compare routines. * * @param range {Array} List of selectable items */ __rangeToMap : function(range) { var mapped = {}; var item; for (var i=0, l=range.length; i<l; i++) { item = range[i]; mapped[this._selectableToHashCode(item)] = item; } return mapped; }, /* --------------------------------------------------------------------------- SINGLE ITEM QUERY AND MODIFICATION --------------------------------------------------------------------------- */ /** * Returns the first selected item. Only makes sense * when using manager in single selection mode. * * @return {var} The selected item (or <code>null</code>) */ _getSelectedItem : function() { for (var hash in this.__selection) { return this.__selection[hash]; } return null; }, /** * Replace current selection with given item. * * @param item {var} Any valid selectable item */ _setSelectedItem : function(item) { if (this._isSelectable(item)) { // If already selected try to find out if this is the only item var current = this.__selection; var hash = this._selectableToHashCode(item); if (!current[hash] || (current.length >= 2)) { this._clearSelection(); this._addToSelection(item); } } }, /* --------------------------------------------------------------------------- MODIFY ITEM SELECTION --------------------------------------------------------------------------- */ /** * Adds an item to the current selection. * * @param item {Object} Any item */ _addToSelection : function(item) { var hash = this._selectableToHashCode(item); if (this.__selection[hash] == null && this._isSelectable(item)) { this.__selection[hash] = item; this._styleSelectable(item, "selected", true); this.__selectionModified = true; } }, /** * Toggles the item e.g. remove it when already selected * or select it when currently not. * * @param item {Object} Any item */ _toggleInSelection : function(item) { var hash = this._selectableToHashCode(item); if (this.__selection[hash] == null) { this.__selection[hash] = item; this._styleSelectable(item, "selected", true); } else { delete this.__selection[hash]; this._styleSelectable(item, "selected", false); } this.__selectionModified = true; }, /** * Removes the given item from the current selection. * * @param item {Object} Any item */ _removeFromSelection : function(item) { var hash = this._selectableToHashCode(item); if (this.__selection[hash] != null) { delete this.__selection[hash]; this._styleSelectable(item, "selected", false); this.__selectionModified = true; } }, /** * Replaces current selection with items from given array. * * @param items {Array} List of items to select */ _replaceMultiSelection : function(items) { if (items.length === 0) { this.clearSelection(); return; } var modified = false; // Build map from hash codes and filter non-selectables var selectable, hash; var incoming = {}; for (var i=0, l=items.length; i<l; i++) { selectable = items[i]; if (this._isSelectable(selectable)) { hash = this._selectableToHashCode(selectable); incoming[hash] = selectable; } } // Remember last var first = items[0]; var last = selectable; // Clear old entries from map var current = this.__selection; for (var hash in current) { if (incoming[hash]) { // Reduce map to make next loop faster delete incoming[hash]; } else { // update internal map selectable = current[hash]; delete current[hash]; // apply styling this._styleSelectable(selectable, "selected", false); // remember that the selection has been modified modified = true; } } // Add remaining selectables to selection for (var hash in incoming) { // update internal map selectable = current[hash] = incoming[hash]; // apply styling this._styleSelectable(selectable, "selected", true); // remember that the selection has been modified modified = true; } // Do not do anything if selection is equal to previous one if (!modified) { return false; } // Scroll last incoming item into view this._scrollItemIntoView(last); // Reset anchor and lead item this._setLeadItem(first); this._setAnchorItem(first); // Finally fire change event this.__selectionModified = true; this._fireChange(); }, /** * Fires the selection change event if the selection has * been modified. * * @param context {String} One of <code>tap</code>, <code>quick</code>, * <code>drag</code> or <code>key</code> or <code>null</code> */ _fireChange : function(context) { if (this.__selectionModified) { // Store context this.__selectionContext = context || null; // Fire data event which contains the current selection this.fireDataEvent("changeSelection", this.getSelection()); delete this.__selectionModified; } }, /** * Applies the default selection. The default item is the first item. * * @param force {Boolean} Whether the default selection should be forced. * * @return {var} The selected item. */ _applyDefaultSelection : function(force) { if (force === true || this.getMode() === "one" && this.isSelectionEmpty()) { var first = this._getFirstSelectable(); if (first != null) { this.selectItem(first); } return first; } return null; } }, /* ***************************************************************************** DESTRUCTOR ***************************************************************************** */ destruct : function() { this._disposeObjects("__scrollTimer"); this.__selection = this.__pointerDownOnSelected = this.__anchorItem = null; this.__leadItem = null; } });