@qooxdoo/framework
Version:
The JS Framework for Coders
463 lines (396 loc) • 12.5 kB
JavaScript
/* ************************************************************************
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:
* Martin Wittemann (martinwittemann)
* Sebastian Werner (wpbasti)
* Jonathan Weiß (jonathan_rass)
************************************************************************ */
/**
* Basically a text fields which allows a selection from a list of
* preconfigured options. Allows custom user input. Public API is value
* oriented.
*
* To work with selections without custom input the ideal candidates are
* the {@link SelectBox} or the {@link RadioGroup}.
*
* @childControl textfield {qx.ui.form.TextField} textfield component of the combobox
* @childControl button {qx.ui.form.Button} button to open the list popup
* @childControl list {qx.ui.form.List} list inside the popup
*/
qx.Class.define("qx.ui.form.ComboBox", {
extend: qx.ui.form.AbstractSelectBox,
implement: [qx.ui.form.IStringForm],
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
construct() {
super();
var textField = this._createChildControl("textfield");
this._createChildControl("button");
// ARIA attrs
this.getContentElement().setAttribute("role", "combobox");
this.addListener("tap", this._onTap);
// forward the focusin and focusout events to the textfield. The textfield
// is not focusable so the events need to be forwarded manually.
this.addListener("focusin", e => {
textField.fireNonBubblingEvent("focusin", qx.event.type.Focus);
});
this.addListener("focusout", e => {
textField.fireNonBubblingEvent("focusout", qx.event.type.Focus);
});
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties: {
// overridden
appearance: {
refine: true,
init: "combobox"
},
/**
* String value which will be shown as a hint if the field is all of:
* unset, unfocused and enabled. Set to null to not show a placeholder
* text.
*/
placeholder: {
check: "String",
nullable: true,
apply: "_applyPlaceholder"
}
},
/*
*****************************************************************************
EVENTS
*****************************************************************************
*/
events: {
/** Whenever the value is changed this event is fired
*
* Event data: The new text value of the field.
*/
changeValue: "qx.event.type.Data"
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
/* eslint-disable @qooxdoo/qx/no-refs-in-members */
members: {
__preSelectedItem: null,
__onInputId: null,
// property apply
_applyPlaceholder(value, old) {
this.getChildControl("textfield").setPlaceholder(value);
},
/*
---------------------------------------------------------------------------
WIDGET API
---------------------------------------------------------------------------
*/
// overridden
_createChildControlImpl(id, hash) {
var control;
switch (id) {
case "textfield":
control = new qx.ui.form.TextField();
control.setFocusable(false);
control.addState("inner");
control.addListener(
"changeValue",
this._onTextFieldChangeValue,
this
);
control.addListener("blur", this.close, this);
this._add(control, { flex: 1 });
break;
case "button":
control = new qx.ui.form.Button();
control.setFocusable(false);
control.setKeepActive(true);
control.addState("inner");
control.addListener("execute", this.toggle, this);
this._add(control);
break;
case "list":
// Get the list from the AbstractSelectBox
control = super._createChildControlImpl(id);
// Change selection mode
control.setSelectionMode("single");
break;
}
return control || super._createChildControlImpl(id);
},
// overridden
/**
* @lint ignoreReferenceField(_forwardStates)
*/
_forwardStates: {
focused: true,
invalid: true
},
// overridden
tabFocus() {
var field = this.getChildControl("textfield");
field.getFocusElement().focus();
field.selectAllText();
},
// overridden
focus() {
super.focus();
this.getChildControl("textfield").getFocusElement().focus();
},
// interface implementation
setValue(value) {
var textfield = this.getChildControl("textfield");
if (textfield.getValue() == value) {
return;
}
// Apply to text field
textfield.setValue(value);
},
// interface implementation
getValue() {
return this.getChildControl("textfield").getValue();
},
// interface implementation
resetValue() {
this.getChildControl("textfield").setValue(null);
},
/*
---------------------------------------------------------------------------
EVENT LISTENERS
---------------------------------------------------------------------------
*/
// overridden
_onKeyPress(e) {
var popup = this.getChildControl("popup");
var iden = e.getKeyIdentifier();
if (popup.isVisible()) {
const listIdentifier = [
"Up",
"Down",
"PageUp",
"PageDown",
"Escape",
"Tab"
];
if (iden == "Enter") {
this._setPreselectedItem();
this.resetAllTextSelection();
this.close();
e.stop();
} else if (listIdentifier.includes(iden)) {
super._onKeyPress(e);
} else {
this.close();
}
} else {
if (iden == "Down") {
this.getChildControl("button").addState("selected");
this.open();
e.stop();
}
}
},
/**
* Toggles the popup's visibility.
*
* @param e {qx.event.type.Pointer} Pointer tap event
*/
_onTap(e) {
this.close();
},
// overridden
_onListPointerDown(e) {
this._setPreselectedItem();
},
/**
* Apply pre-selected item
*/
_setPreselectedItem() {
if (this.__preSelectedItem) {
var label = this.__preSelectedItem.getLabel();
if (this.getFormat() != null) {
label = this.getFormat().call(this, this.__preSelectedItem);
}
// check for translation
if (label && label.translate) {
label = label.translate();
}
this.setValue(label);
this.__preSelectedItem = null;
}
},
// overridden
_onListChangeSelection(e) {
var current = e.getData();
if (current.length > 0) {
// Ignore quick context (e.g. pointerover)
// and configure the new value when closing the popup afterwards
var list = this.getChildControl("list");
var ctx = list.getSelectionContext();
if (ctx == "quick" || ctx == "key") {
this.__preSelectedItem = current[0];
} else {
var label = current[0].getLabel();
if (this.getFormat() != null) {
label = this.getFormat().call(this, current[0]);
}
// check for translation
if (label && label.translate) {
label = label.translate();
}
this.setValue(label);
this.__preSelectedItem = null;
}
}
// Set aria-activedescendant
const textFieldContentEl =
this.getChildControl("textfield").getContentElement();
if (!textFieldContentEl) {
return;
}
const currentContentEl =
current && current[0] ? current[0].getContentElement() : null;
if (currentContentEl) {
textFieldContentEl.setAttribute(
"aria-activedescendant",
currentContentEl.getAttribute("id")
);
} else {
textFieldContentEl.removeAttribute("aria-activedescendant");
}
},
// overridden
_onPopupChangeVisibility(e) {
super._onPopupChangeVisibility(e);
// Synchronize the list with the current value on every
// opening of the popup. This is useful because through
// the quick selection mode, the list may keep an invalid
// selection on close or the user may enter text while
// the combobox is closed and reopen it afterwards.
var popup = this.getChildControl("popup");
if (popup.isVisible()) {
var list = this.getChildControl("list");
var value = this.getValue();
var item = null;
if (value) {
item = list.findItem(value);
}
if (item) {
list.setSelection([item]);
} else {
list.resetSelection();
}
}
// In all cases: Remove focused state from button
this.getChildControl("button").removeState("selected");
},
/**
* Reacts on value changes of the text field and syncs the
* value to the combobox.
*
* @param e {qx.event.type.Data} Change event
*/
_onTextFieldChangeValue(e) {
var value = e.getData();
var list = this.getChildControl("list");
let current = null;
if (value != null) {
// Select item when possible
current = list.findItem(value, false);
if (current) {
list.setSelection([current]);
} else {
list.resetSelection();
}
} else {
list.resetSelection();
}
// ARIA attrs
const old = e.getOldData() ? list.findItem(e.getOldData(), false) : null;
if (old && old !== current) {
old.getContentElement().setAttribute("aria-selected", false);
}
if (current) {
current.getContentElement().setAttribute("aria-selected", true);
}
// Fire event
this.fireDataEvent("changeValue", value, e.getOldData());
},
/*
---------------------------------------------------------------------------
TEXTFIELD SELECTION API
---------------------------------------------------------------------------
*/
/**
* Returns the current selection.
* This method only works if the widget is already created and
* added to the document.
*
* @return {String|null}
*/
getTextSelection() {
return this.getChildControl("textfield").getTextSelection();
},
/**
* Returns the current selection length.
* This method only works if the widget is already created and
* added to the document.
*
* @return {Integer|null}
*/
getTextSelectionLength() {
return this.getChildControl("textfield").getTextSelectionLength();
},
/**
* Set the selection to the given start and end (zero-based).
* If no end value is given the selection will extend to the
* end of the textfield's content.
* This method only works if the widget is already created and
* added to the document.
*
* @param start {Integer} start of the selection (zero-based)
* @param end {Integer} end of the selection
*/
setTextSelection(start, end) {
this.getChildControl("textfield").setTextSelection(start, end);
},
/**
* Clears the current selection.
* This method only works if the widget is already created and
* added to the document.
*
*/
clearTextSelection() {
this.getChildControl("textfield").clearTextSelection();
},
/**
* Selects the whole content
*
*/
selectAllText() {
this.getChildControl("textfield").selectAllText();
},
/**
* Clear any text selection, then select all text
*
*/
resetAllTextSelection() {
this.clearTextSelection();
this.selectAllText();
}
}
});