@qooxdoo/framework
Version:
The JS Framework for Coders
521 lines (433 loc) • 13.9 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:
* Sebastian Werner (wpbasti)
* Andreas Ecker (ecker)
* Martin Wittemann (martinwittemann)
* Christian Hagendorn (chris_schmidt)
************************************************************************ */
/**
* A list of items. Displays an automatically scrolling list for all
* added {@link qx.ui.form.IListItem} instances (typically this would be instances of
* {@link qx.ui.form.ListItem} but can also be other Atoms, such as {@link qx.ui.form.CheckBox}).
* Supports various selection options: single, multi, ...
*/
qx.Class.define("qx.ui.form.List", {
extend: qx.ui.core.scroll.AbstractScrollArea,
implement: [
qx.ui.core.IMultiSelection,
qx.ui.form.IForm,
qx.ui.form.IField,
qx.ui.form.IModelSelection
],
include: [
qx.ui.core.MRemoteChildrenHandling,
qx.ui.core.MMultiSelectionHandling,
qx.ui.form.MForm,
qx.ui.form.MModelSelection
],
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param horizontal {Boolean?false} Whether the list should be horizontal.
*/
construct(horizontal) {
super();
// Create content
this.__content = this._createListItemContainer();
// Used to fire item add/remove events
this.__content.addListener("addChildWidget", this._onAddChild, this);
this.__content.addListener("removeChildWidget", this._onRemoveChild, this);
// Add to scrollpane
this.getChildControl("pane").add(this.__content);
// Apply orientation
if (horizontal) {
this.setOrientation("horizontal");
} else {
this.initOrientation();
}
// Add keypress listener
this.addListener("keypress", this._onKeyPress);
this.addListener("keyinput", this._onKeyInput);
// initialize the search string
this.__pressedString = "";
this.__childrenBindings = new Map();
},
/*
*****************************************************************************
EVENTS
*****************************************************************************
*/
events: {
/**
* This event is fired after a list item was added to the list. 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 list item has been removed from the list.
* The {@link qx.event.type.Data#getData} method of the event returns the
* removed item.
*/
removeItem: "qx.event.type.Data"
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties: {
// overridden
appearance: {
refine: true,
init: "list"
},
// overridden
focusable: {
refine: true,
init: true
},
// overridden
width: {
refine: true,
init: 100
},
// overridden
height: {
refine: true,
init: 200
},
/**
* Whether the list should be rendered horizontal or vertical.
*/
orientation: {
check: ["horizontal", "vertical"],
init: "vertical",
apply: "_applyOrientation"
},
/** Spacing between the items */
spacing: {
check: "Integer",
init: 0,
apply: "_applySpacing",
themeable: true
},
/** Controls whether the inline-find feature is activated or not */
enableInlineFind: {
check: "Boolean",
init: true
},
/** Whether the list is read only when enabled */
readOnly: {
check: "Boolean",
init: false,
event: "changeReadOnly",
apply: "_applyReadOnly"
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members: {
__pressedString: null,
__lastKeyPress: null,
/** @type {qx.ui.core.Widget} The children container */
__content: null,
/** @type {Class} Pointer to the selection manager to use */
SELECTION_MANAGER: qx.ui.core.selection.ScrollArea,
/*
---------------------------------------------------------------------------
WIDGET API
---------------------------------------------------------------------------
*/
// overridden
getChildrenContainer() {
return this.__content;
},
__childrenBindings: null,
/**
* Handle child widget adds on the content pane
*
* @param e {qx.event.type.Data} the event instance
*/
_onAddChild(e) {
const child = e.getData();
if (qx.Class.implementsInterface(child, qx.ui.form.IListItem)) {
this.__childrenBindings.set(
child.toHashCode(),
this.bind("readOnly", child, "readOnly")
);
}
this.fireDataEvent("addItem", child);
},
/**
* Handle child widget removes on the content pane
*
* @param e {qx.event.type.Data} the event instance
*/
_onRemoveChild(e) {
const child = e.getData();
const binding = this.__childrenBindings.get(child.toHashCode());
if (binding) {
child.removeBinding(binding);
this.__childrenBindings.delete(child.toHashCode());
}
this.fireDataEvent("removeItem", child);
},
/*
---------------------------------------------------------------------------
PUBLIC API
---------------------------------------------------------------------------
*/
/**
* Used to route external <code>keypress</code> events to the list
* handling (in fact the manager of the list)
*
* @param e {qx.event.type.KeySequence} KeyPress event
*/
handleKeyPress(e) {
if (!this._onKeyPress(e)) {
this._getManager().handleKeyPress(e);
}
},
/*
---------------------------------------------------------------------------
PROTECTED API
---------------------------------------------------------------------------
*/
/**
* This container holds the list item widgets.
*
* @return {qx.ui.container.Composite} Container for the list item widgets
*/
_createListItemContainer() {
return new qx.ui.container.Composite();
},
/*
---------------------------------------------------------------------------
PROPERTY APPLY ROUTINES
---------------------------------------------------------------------------
*/
// property apply
_applyOrientation(value, old) {
// ARIA attrs
this.getContentElement().setAttribute("aria-orientation", value);
var content = this.__content;
// save old layout for disposal
var oldLayout = content.getLayout();
// Create new layout
var horizontal = value === "horizontal";
var layout = horizontal
? new qx.ui.layout.HBox()
: new qx.ui.layout.VBox();
// Configure content
content.setLayout(layout);
content.setAllowGrowX(!horizontal);
content.setAllowGrowY(horizontal);
// Configure spacing
this._applySpacing(this.getSpacing());
// dispose old layout
if (oldLayout) {
oldLayout.dispose();
}
},
// property apply
_applySpacing(value, old) {
this.__content.getLayout().setSpacing(value);
},
// property readOnly
_applyReadOnly(value) {
this._getManager().setReadOnly(value);
if (value) {
this.addState("readonly");
this.addState("disabled");
// Remove draggable
if (this.isDraggable()) {
this._applyDraggable(false, true);
}
// Remove droppable
if (this.isDroppable()) {
this._applyDroppable(false, true);
}
} else {
this.removeState("readonly");
if (this.isEnabled()) {
this.removeState("disabled");
// Re-add draggable
if (this.isDraggable()) {
this._applyDraggable(true, false);
}
// Re-add droppable
if (this.isDroppable()) {
this._applyDroppable(true, false);
}
}
}
},
// override
_applyEnabled(value, old) {
super._applyEnabled(value, old);
// If editable has just been turned on, we need to correct for readOnly status
if (value && this.isReadOnly()) {
this.addState("disabled");
// Remove draggable
if (this.isDraggable()) {
this._applyDraggable(false, true);
}
// Remove droppable
if (this.isDroppable()) {
this._applyDroppable(false, true);
}
}
},
/*
---------------------------------------------------------------------------
EVENT HANDLER
---------------------------------------------------------------------------
*/
/**
* Event listener for <code>keypress</code> events.
*
* @param e {qx.event.type.KeySequence} KeyPress event
* @return {Boolean} Whether the event was processed
*/
_onKeyPress(e) {
// Execute action on press <ENTER>
if (e.getKeyIdentifier() == "Enter" && !e.isAltPressed()) {
var items = this.getSelection();
for (var i = 0; i < items.length; i++) {
items[i].fireEvent("action");
}
return true;
}
return false;
},
/*
---------------------------------------------------------------------------
FIND SUPPORT
---------------------------------------------------------------------------
*/
/**
* Handles the inline find - if enabled
*
* @param e {qx.event.type.KeyInput} key input event
*/
_onKeyInput(e) {
// do nothing if the find is disabled
if (!this.getEnableInlineFind()) {
return;
}
// Only useful in single or one selection mode
var mode = this.getSelectionMode();
if (!(mode === "single" || mode === "one")) {
return;
}
// Reset string after a second of non pressed key
if (new Date().valueOf() - this.__lastKeyPress > 1000) {
this.__pressedString = "";
}
// Combine keys the user pressed to a string
this.__pressedString += e.getChar();
// Find matching item
var matchedItem = this.findItemByLabelFuzzy(this.__pressedString);
// if an item was found, select it
if (matchedItem) {
this.setSelection([matchedItem]);
}
// Store timestamp
this.__lastKeyPress = new Date().valueOf();
},
/**
* Takes the given string and tries to find a ListItem
* which starts with this string. The search is not case sensitive and the
* first found ListItem will be returned. If there could not be found any
* qualifying list item, null will be returned.
*
* @param search {String} The text with which the label of the ListItem should start with
* @return {qx.ui.form.ListItem} The found ListItem or null
*/
findItemByLabelFuzzy(search) {
// lower case search text
search = search.toLowerCase();
// get all items of the list
var items = this.getChildren();
// go threw all items
for (var i = 0, l = items.length; i < l; i++) {
// get the label of the current item
var currentLabel = items[i].getLabel();
// if the label fits with the search text (ignore case, begins with)
if (currentLabel && currentLabel.toLowerCase().indexOf(search) == 0) {
// just return the first found element
return items[i];
}
}
// if no element was found, return null
return null;
},
/**
* Find an item by its {@link qx.ui.basic.Atom#getLabel}.
*
* @param search {String} A label or any item
* @param ignoreCase {Boolean?true} description
* @return {qx.ui.form.ListItem} The found ListItem or null
*/
findItem(search, ignoreCase) {
// lowercase search
if (ignoreCase !== false) {
search = search.toLowerCase();
}
// get all items of the list
var items = this.getChildren();
var item;
// go through all items
for (var i = 0, l = items.length; i < l; i++) {
item = items[i];
// get the content of the label; text content when rich
var label;
if (item.isRich()) {
var control = item.getChildControl("label", true);
if (control) {
var labelNode = control.getContentElement().getDomElement();
if (labelNode) {
label = qx.bom.element.Attribute.get(labelNode, "text");
}
}
} else {
label = item.getLabel();
}
if (label != null) {
if (label.translate) {
label = label.translate();
}
if (ignoreCase !== false) {
label = label.toLowerCase();
}
if (label.toString() == search.toString()) {
return item;
}
}
}
return null;
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct() {
this._disposeObjects("__content");
}
});