UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

463 lines (365 loc) 11.6 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2011 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: * Christian Hagendorn (chris_schmidt) ************************************************************************ */ /** * A form virtual widget which allows a single selection. Looks somewhat like * a normal button, but opens a virtual list of items to select when tapping * on it. * * @childControl spacer {qx.ui.core.Spacer} Flexible spacer widget. * @childControl atom {qx.ui.basic.Atom} Shows the text and icon of the content. * @childControl arrow {qx.ui.basic.Image} Shows the arrow to open the drop-down * list. */ qx.Class.define("qx.ui.form.VirtualSelectBox", { extend : qx.ui.form.core.AbstractVirtualBox, implement : [ qx.data.controller.ISelection, qx.ui.form.IField ], construct : function(model) { this.base(arguments, model); this._createChildControl("atom"); this._createChildControl("spacer"); this._createChildControl("arrow"); // Register listener this.addListener("pointerover", this._onPointerOver, this); this.addListener("pointerout", this._onPointerOut, this); this.__bindings = []; this.initSelection(this.getChildControl("dropdown").getSelection()); this.__searchTimer = new qx.event.Timer(500); this.__searchTimer.addListener("interval", this.__preselect, this); this.getSelection().addListener("change", this._updateSelectionValue, this); }, properties : { // overridden appearance : { refine : true, init : "virtual-selectbox" }, // overridden width : { refine : true, init : 120 }, /** Current selected items. */ selection : { check : "qx.data.Array", event : "changeSelection", apply : "_applySelection", nullable : false, deferredInit : true } }, events : { /** * This event is fired as soon as the content of the selection property changes, but * this is not equal to the change of the selection of the widget. If the selection * of the widget changes, the content of the array stored in the selection property * changes. This means you have to listen to the change event of the selection array * to get an event as soon as the user changes the selected item. * <pre class="javascript">obj.getSelection().addListener("change", listener, this);</pre> */ "changeSelection" : "qx.event.type.Data", /** Fires after the value was modified */ "changeValue" : "qx.event.type.Data" }, members : { /** @type {String} The search value to {@link #__preselect} an item. */ __searchValue : "", /** * @type {qx.event.Timer} The time which triggers the search for pre-selection. */ __searchTimer : null, /** @type {Array} Contains the id from all bindings. */ __bindings : null, /** * @param selected {var|null} Item to select as value. * @returns {null|TypeError} The status of this operation. */ setValue : function(selected) { if (null === selected) { this.getSelection().removeAll(); return null; } this.getSelection().setItem(0, selected); return null; }, /** * @returns {null|var} The currently selected item or null if there is none. */ getValue : function() { var s = this.getSelection(); return (s.length === 0 ? null : s.getItem(0)); }, resetValue : function() { this.setValue(null); }, // overridden syncWidget : function(jobs) { this._removeBindings(); this._addBindings(); }, /* --------------------------------------------------------------------------- INTERNAl API --------------------------------------------------------------------------- */ // overridden _createChildControlImpl : function(id, hash) { var control; switch(id) { case "spacer": control = new qx.ui.core.Spacer(); this._add(control, {flex: 1}); break; case "atom": control = new qx.ui.form.ListItem(""); control.setCenter(false); control.setAnonymous(true); this._add(control, {flex:1}); break; case "arrow": control = new qx.ui.basic.Image(); control.setAnonymous(true); this._add(control); break; } return control || this.base(arguments, id, hash); }, // overridden _getAction : function(event) { var keyIdentifier = event.getKeyIdentifier(); var isOpen = this.getChildControl("dropdown").isVisible(); var isModifierPressed = this._isModifierPressed(event); if ( !isOpen && !isModifierPressed && (keyIdentifier === "Enter" || keyIdentifier === "Space") ) { return "open"; } else if (isOpen && event.isPrintable()) { return "search"; } else { return this.base(arguments, event); } }, /** * This method is called when the binding can be added to the * widget. For e.q. bind the drop-down selection with the widget. */ _addBindings : function() { var atom = this.getChildControl("atom"); var modelPath = this._getBindPath("selection", ""); var id = this.bind(modelPath, atom, "model", null); this.__bindings.push(id); var labelSourcePath = this._getBindPath("selection", this.getLabelPath()); id = this.bind(labelSourcePath, atom, "label", this.getLabelOptions()); this.__bindings.push(id); if (this.getIconPath() != null) { var iconSourcePath = this._getBindPath("selection", this.getIconPath()); id = this.bind(iconSourcePath, atom, "icon", this.getIconOptions()); this.__bindings.push(id); } }, /** * This method is called when the binding can be removed from the * widget. For e.q. remove the bound drop-down selection. */ _removeBindings : function() { while (this.__bindings.length > 0) { var id = this.__bindings.pop(); this.removeBinding(id); } }, /* --------------------------------------------------------------------------- EVENT LISTENERS --------------------------------------------------------------------------- */ // overridden _handlePointer : function(event) { this.base(arguments, event); var type = event.getType(); if (type === "tap") { this.toggle(); } }, // overridden _handleKeyboard : function(event) { var action = this._getAction(event); switch(action) { case "search": this.__searchValue += this.__convertKeyIdentifier(event.getKeyIdentifier()); this.__searchTimer.restart(); break; default: this.base(arguments, event); break; } }, /** * Listener method for "pointerover" event. * * <ul> * <li>Adds state "hovered"</li> * <li>Removes "abandoned" and adds "pressed" state (if "abandoned" state * is set)</li> * </ul> * * @param event {qx.event.type.Pointer} Pointer event */ _onPointerOver : function(event) { if (!this.isEnabled() || event.getTarget() !== this) { return; } if (this.hasState("abandoned")) { this.removeState("abandoned"); this.addState("pressed"); } this.addState("hovered"); }, /** * Listener method for "pointerout" event. * * <ul> * <li>Removes "hovered" state</li> * <li>Adds "abandoned" and removes "pressed" state (if "pressed" state * is set)</li> * </ul> * * @param event {qx.event.type.Pointer} Pointer event */ _onPointerOut : function(event) { if (!this.isEnabled() || event.getTarget() !== this) { return; } this.removeState("hovered"); if (this.hasState("pressed")) { this.removeState("pressed"); this.addState("abandoned"); } }, /* --------------------------------------------------------------------------- APPLY ROUTINES --------------------------------------------------------------------------- */ // property apply _applySelection : function(value, old) { this.getChildControl("dropdown").setSelection(value); qx.ui.core.queue.Widget.add(this); }, /* --------------------------------------------------------------------------- HELPER METHODS --------------------------------------------------------------------------- */ /** * Preselects an item in the drop-down, when item starts with the * __searchValue value. */ __preselect : function() { this.__searchTimer.stop(); var searchValue = this.__searchValue; if (searchValue === null || searchValue === "") { return; } var model = this.getModel(); var list = this.getChildControl("dropdown").getChildControl("list"); var selection = list.getSelection(); var length = list._getLookupTable().length; var startIndex = model.indexOf(selection.getItem(0)); var startRow = list._reverseLookup(startIndex); for (var i = 1; i <= length; i++) { var row = (i + startRow) % length; var item = model.getItem(list._lookup(row)); if (!item) { // group items aren't in the model continue; } var value = item; if (this.getLabelPath()) { value = qx.data.SingleValueBinding.resolvePropertyChain(item, this.getLabelPath()); var labelOptions = this.getLabelOptions(); if (labelOptions) { var converter = qx.util.Delegate.getMethod(labelOptions, "converter"); if (converter) { value = converter(value, item); } } } if ( value.toLowerCase().startsWith(searchValue.toLowerCase()) ) { selection.push(item); break; } } this.__searchValue = ""; }, /** * Converts the keyIdentifier to a printable character e.q. <code>"Space"</code> * to <code>" "</code>. * * @param keyIdentifier {String} The keyIdentifier to convert. * @return {String} The converted keyIdentifier. */ __convertKeyIdentifier : function(keyIdentifier) { if (keyIdentifier === "Space") { return " "; } else { return keyIdentifier; } }, /** * Called when selection changes. * * @param event {qx.event.type.Data} {@link qx.data.Array} change event. */ _updateSelectionValue : function(event) { var d = event.getData(); var old = (d.removed.length ? d.removed[0] : null); this.fireDataEvent("changeValue", d.added[0], old); } }, destruct : function() { this._removeBindings(); this.getSelection().removeListener("change", this._updateSelectionValue, this); this.__searchTimer.removeListener("interval", this.__preselect, this); this.__searchTimer.dispose(); this.__searchTimer = null; } });