@qooxdoo/framework
Version:
The JS Framework for Coders
1,716 lines (1,465 loc) • 65 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2006 STZ-IDA, Germany, http://www.stz-ida.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Til Schneider (til132)
* Fabian Jakobs (fjakobs)
* Jonathan Weiß (jonathan_rass)
************************************************************************ */
/**
* Table
*
* A detailed description can be found in the package description
* {@link qx.ui.table}.
*
* @childControl statusbar {qx.ui.basic.Label} label to show the status of the table
* @childControl column-button {qx.ui.table.columnmenu.Button} button to open the column menu
*/
qx.Class.define("qx.ui.table.Table", {
extend: qx.ui.core.Widget,
include: qx.ui.core.MDragDropScrolling,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param tableModel {qx.ui.table.ITableModel ? null}
* The table model to read the data from.
*
* @param custom {Map ? null}
* A map provided to override the various supplemental classes allocated
* within this constructor. Each property must be a function which
* returns an object instance, as indicated by shown the defaults listed
* here:
*
* <dl>
* <dt>initiallyHiddenColumns</dt>
* <dd>
* {Array?}
* A list of column numbers that should be initially invisible. Any
* column not mentioned will be initially visible, and if no array
* is provided, all columns will be initially visible.
* </dd>
* <dt>selectionManager</dt>
* <dd><pre class='javascript'>
* function(obj)
* {
* return new qx.ui.table.selection.Manager(obj);
* }
* </pre></dd>
* <dt>selectionModel</dt>
* <dd><pre class='javascript'>
* function(obj)
* {
* return new qx.ui.table.selection.Model(obj);
* }
* </pre></dd>
* <dt>tableColumnModel</dt>
* <dd><pre class='javascript'>
* function(obj)
* {
* return new qx.ui.table.columnmodel.Basic(obj);
* }
* </pre></dd>
* <dt>tablePaneModel</dt>
* <dd><pre class='javascript'>
* function(obj)
* {
* return new qx.ui.table.pane.Model(obj);
* }
* </pre></dd>
* <dt>tablePane</dt>
* <dd><pre class='javascript'>
* function(obj)
* {
* return new qx.ui.table.pane.Pane(obj);
* }
* </pre></dd>
* <dt>tablePaneHeader</dt>
* <dd><pre class='javascript'>
* function(obj)
* {
* return new qx.ui.table.pane.Header(obj);
* }
* </pre></dd>
* <dt>tablePaneScroller</dt>
* <dd><pre class='javascript'>
* function(obj)
* {
* return new qx.ui.table.pane.Scroller(obj);
* }
* </pre></dd>
* <dt>tablePaneModel</dt>
* <dd><pre class='javascript'>
* function(obj)
* {
* return new qx.ui.table.pane.Model(obj);
* }
* </pre></dd>
* <dt>columnMenu</dt>
* <dd><pre class='javascript'>
* function()
* {
* return new qx.ui.table.columnmenu.Button();
* }
* </pre></dd>
* </dl>
*/
construct(tableModel, custom) {
super();
//
// Use default objects if custom objects are not specified
//
if (!custom) {
custom = {};
}
if (custom.initiallyHiddenColumns) {
this.setInitiallyHiddenColumns(custom.initiallyHiddenColumns);
}
if (custom.selectionManager) {
this.setNewSelectionManager(custom.selectionManager);
}
if (custom.selectionModel) {
this.setNewSelectionModel(custom.selectionModel);
}
if (custom.tableColumnModel) {
this.setNewTableColumnModel(custom.tableColumnModel);
}
if (custom.tablePane) {
this.setNewTablePane(custom.tablePane);
}
if (custom.tablePaneHeader) {
this.setNewTablePaneHeader(custom.tablePaneHeader);
}
if (custom.tablePaneScroller) {
this.setNewTablePaneScroller(custom.tablePaneScroller);
}
if (custom.tablePaneModel) {
this.setNewTablePaneModel(custom.tablePaneModel);
}
if (custom.columnMenu) {
this.setNewColumnMenu(custom.columnMenu);
}
this._setLayout(new qx.ui.layout.VBox());
// Create the child widgets
this.__scrollerParent = new qx.ui.container.Composite(
new qx.ui.layout.HBox()
);
this._add(this.__scrollerParent, { flex: 1 });
// Allocate a default data row renderer
this.setDataRowRenderer(new qx.ui.table.rowrenderer.Default(this));
// Create the models
this.__selectionManager = this.getNewSelectionManager()(this);
this.setSelectionModel(this.getNewSelectionModel()(this));
this.setTableModel(tableModel || this.getEmptyTableModel());
// create the main meta column
this.setMetaColumnCounts([-1]);
// Make focusable
this.setTabIndex(1);
this.addListener("keydown", this._onKeyDown);
this.addListener("focus", this._onFocusChanged);
this.addListener("blur", this._onFocusChanged);
// attach the resize listener to the last child of the layout. This
// ensures that all other children are laid out before
var spacer = new qx.ui.core.Widget().set({
height: 0
});
this._add(spacer);
spacer.addListener("resize", this._onResize, this);
this.__focusedCol = null;
this.__focusedRow = null;
// add an event listener which updates the table content on locale change
if (qx.core.Environment.get("qx.dynlocale")) {
this.__changeLocaleTableListenerId = qx.locale.Manager.getInstance().addListener(
"changeLocale",
this._onChangeLocale,
this
);
}
this.initStatusBarVisible();
// If the table model has an init() method...
tableModel = this.getTableModel();
if (tableModel.init && typeof tableModel.init == "function") {
// ... then call it now to allow the table model to affect table
// properties.
tableModel.init(this);
}
// ARIA attrs
this.getContentElement().setAttribute("role", "grid");
},
/*
*****************************************************************************
EVENTS
*****************************************************************************
*/
events: {
/**
* Dispatched before adding the column list to the column visibility menu.
* The event data is a map with two properties: table and menu. Listeners
* may add additional items to the menu, which appear at the top of the
* menu.
*/
columnVisibilityMenuCreateStart: "qx.event.type.Data",
/**
* Dispatched after adding the column list to the column visibility menu.
* The event data is a map with two properties: table and menu. Listeners
* may add additional items to the menu, which appear at the bottom of the
* menu.
*/
columnVisibilityMenuCreateEnd: "qx.event.type.Data",
/**
* Dispatched when the width of the table has changed.
*/
tableWidthChanged: "qx.event.type.Event",
/**
* Dispatched when updating scrollbars discovers that a vertical scrollbar
* is needed when it previously was not, or vice versa. The data is a
* boolean indicating whether a vertical scrollbar is now being used.
*/
verticalScrollBarChanged: "qx.event.type.Data",
/**
* Dispatched when a data cell has been tapped.
*/
cellTap: "qx.ui.table.pane.CellEvent",
/**
* Dispatched when a data cell has been tapped.
*/
cellDbltap: "qx.ui.table.pane.CellEvent",
/**
* Dispatched when the context menu is needed in a data cell
*/
cellContextmenu: "qx.ui.table.pane.CellEvent",
/**
* Dispatched after a cell editor is flushed.
*
* The data is a map containing this properties:
* <ul>
* <li>row</li>
* <li>col</li>
* <li>value</li>
* <li>oldValue</li>
* </ul>
*/
dataEdited: "qx.event.type.Data"
},
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics: {
/** Events that must be redirected to the scrollers. */
__redirectEvents: { cellTap: 1, cellDbltap: 1, cellContextmenu: 1 }
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties: {
appearance: {
refine: true,
init: "table"
},
focusable: {
refine: true,
init: true
},
minWidth: {
refine: true,
init: 50
},
/**
* The list of columns that are initially hidden. This property is set by
* the constructor, from the value received in
* custom.initiallyHiddenColumns, and is only used when a column model is
* initialized. It can be of great benefit in tables with numerous columns
* where most are not initially visible. The process of creating the
* headers for all of the columns, only to have those columns discarded
* shortly thereafter when setColumnVisibility(false) is called, is a
* waste of (significant, in some browsers) time. Specifying the
* non-visible columns at constructor time can therefore avoid the initial
* creation of all of those superfluous widgets.
*/
initiallyHiddenColumns: {
init: null
},
/**
* Whether the widget contains content which may be selected by the user.
*
* If the value set to <code>true</code> the native browser selection can
* be used for text selection. But it is normally useful for
* forms fields, longer texts/documents, editors, etc.
*
* Note: This has no effect on Table!
*/
selectable: {
refine: true,
init: false
},
/** The selection model. */
selectionModel: {
check: "qx.ui.table.selection.Model",
apply: "_applySelectionModel",
event: "changeSelectionModel"
},
/** The table model. */
tableModel: {
check: "qx.ui.table.ITableModel",
apply: "_applyTableModel",
event: "changeTableModel"
},
/** The height of the table rows. */
rowHeight: {
check: "Number",
init: 20,
apply: "_applyRowHeight",
event: "changeRowHeight",
themeable: true
},
/**
* Force line height to match row height. May be disabled if cell
* renderers being used wish to render multiple lines of data within a
* cell. (With the default setting, all but the first of multiple lines
* of data will not be visible.)
*/
forceLineHeight: {
check: "Boolean",
init: true
},
/**
* Whether the header cells are visible. When setting this to false,
* you'll likely also want to set the {#columnVisibilityButtonVisible}
* property to false as well, to entirely remove the header row.
*/
headerCellsVisible: {
check: "Boolean",
init: true,
apply: "_applyHeaderCellsVisible",
themeable: true
},
/** The height of the header cells. */
headerCellHeight: {
check: "Integer",
init: 16,
apply: "_applyHeaderCellHeight",
event: "changeHeaderCellHeight",
nullable: true,
themeable: true
},
/** Whether to show the status bar */
statusBarVisible: {
check: "Boolean",
init: true,
apply: "_applyStatusBarVisible"
},
/** The Statusbartext, set it, if you want some more Information */
additionalStatusBarText: {
nullable: true,
init: null,
apply: "_applyAdditionalStatusBarText"
},
/** Whether to show the column visibility button */
columnVisibilityButtonVisible: {
check: "Boolean",
init: true,
apply: "_applyColumnVisibilityButtonVisible",
themeable: true
},
/**
* @type {Integer[]} The number of columns per meta column. If the last array entry is -1,
* this meta column will get the remaining columns.
*/
metaColumnCounts: {
check: "Object",
apply: "_applyMetaColumnCounts"
},
/**
* Whether the focus should moved when the pointer is moved over a cell. If false
* the focus is only moved on pointer taps.
*/
focusCellOnPointerMove: {
check: "Boolean",
init: false,
apply: "_applyFocusCellOnPointerMove"
},
/**
* Whether row focus change by keyboard also modifies selection
*/
rowFocusChangeModifiesSelection: {
check: "Boolean",
init: true
},
/**
* Whether the cell focus indicator should be shown
*/
showCellFocusIndicator: {
check: "Boolean",
init: true,
apply: "_applyShowCellFocusIndicator"
},
/**
* By default, the "cellContextmenu" event is fired only when a data cell
* is right-clicked. It is not fired when a right-click occurs in the
* empty area of the table below the last data row. By turning on this
* property, "cellContextMenu" events will also be generated when a
* right-click occurs in that empty area. In such a case, row identifier
* in the event data will be null, so event handlers can check (row ===
* null) to handle this case.
*/
contextMenuFromDataCellsOnly: {
check: "Boolean",
init: true,
apply: "_applyContextMenuFromDataCellsOnly"
},
/**
* Whether the table should keep the first visible row complete. If set to false,
* the first row may be rendered partial, depending on the vertical scroll value.
*/
keepFirstVisibleRowComplete: {
check: "Boolean",
init: true,
apply: "_applyKeepFirstVisibleRowComplete"
},
/**
* Whether the table cells should be updated when only the selection or the
* focus changed. This slows down the table update but allows to react on a
* changed selection or a changed focus in a cell renderer.
*/
alwaysUpdateCells: {
check: "Boolean",
init: false
},
/**
* Whether to reset the selection when a header cell is tapped. Since
* most data models do not have provisions to retain a selection after
* sorting, the default is to reset the selection in this case. Some data
* models, however, do have the capability to retain the selection, so
* when using those, this property should be set to false.
*/
resetSelectionOnHeaderTap: {
check: "Boolean",
init: true,
apply: "_applyResetSelectionOnHeaderTap"
},
/**
* Whether to reset the selection when the unpopulated table area is tapped.
* The default is false which keeps the behaviour as before
*/
resetSelectionOnTapBelowRows: {
check: "Boolean",
init: false,
apply: "_applyResetSelectionOnTapBelowRows"
},
/**
* If set then defines the minimum height of the focus indicator when editing
*/
minCellEditHeight: {
check: "Integer",
nullable: true,
init: null,
apply: "_applyMinCellEditHeight"
},
/** The renderer to use for styling the rows. */
dataRowRenderer: {
check: "qx.ui.table.IRowRenderer",
init: null,
nullable: true,
event: "changeDataRowRenderer"
},
/**
* The action to take when a cell is being edited and the focus moves elsewhere.
*/
cellEditorBlurAction: {
check: ["nothing", "save", "cancel"],
init: "nothing"
},
/**
* A function to call when before modal cell editor is opened.
*
* @signature function(cellEditor, cellInfo)
*
* @param cellEditor {qx.ui.window.Window}
* The modal window which has been created for this cell editor
*
* @param cellInfo {Map}
* Information about the cell for which this cell editor was created.
* It contains the following properties:
* col, row, xPos, value
*
*/
modalCellEditorPreOpenFunction: {
check: "Function",
init: null,
nullable: true
},
/**
* By default, all Scrollers' (meta-columns') horizontal scrollbars are
* shown if any one is required. Allow not showing any that are not
* required.
*/
excludeScrollerScrollbarsIfNotNeeded: {
check: "Boolean",
init: false,
nullable: false
},
/**
* A function to instantiate a new column menu button.
*/
newColumnMenu: {
check: "Function",
init() {
return new qx.ui.table.columnmenu.Button();
}
},
/**
* A function to instantiate a selection manager. this allows subclasses of
* Table to subclass this internal class. To take effect, this property must
* be set before calling the Table constructor.
*/
newSelectionManager: {
check: "Function",
init(obj) {
return new qx.ui.table.selection.Manager(obj);
}
},
/**
* A function to instantiate a selection model. this allows subclasses of
* Table to subclass this internal class. To take effect, this property must
* be set before calling the Table constructor.
*/
newSelectionModel: {
check: "Function",
init(obj) {
return new qx.ui.table.selection.Model(obj);
}
},
/**
* A function to instantiate a table column model. This allows subclasses
* of Table to subclass this internal class. To take effect, this
* property must be set before calling the Table constructor.
*/
newTableColumnModel: {
check: "Function",
init(table) {
return new qx.ui.table.columnmodel.Basic(table);
}
},
/**
* A function to instantiate a table pane. this allows subclasses of
* Table to subclass this internal class. To take effect, this property
* must be set before calling the Table constructor.
*/
newTablePane: {
check: "Function",
init(obj) {
return new qx.ui.table.pane.Pane(obj);
}
},
/**
* A function to instantiate a table pane. this allows subclasses of
* Table to subclass this internal class. To take effect, this property
* must be set before calling the Table constructor.
*/
newTablePaneHeader: {
check: "Function",
init(obj) {
return new qx.ui.table.pane.Header(obj);
}
},
/**
* A function to instantiate a table pane scroller. this allows
* subclasses of Table to subclass this internal class. To take effect,
* this property must be set before calling the Table constructor.
*/
newTablePaneScroller: {
check: "Function",
init(obj) {
return new qx.ui.table.pane.Scroller(obj);
}
},
/**
* A function to instantiate a table pane model. this allows subclasses
* of Table to subclass this internal class. To take effect, this
* property must be set before calling the Table constructor.
*/
newTablePaneModel: {
check: "Function",
init(columnModel) {
return new qx.ui.table.pane.Model(columnModel);
}
}
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members: {
__focusedCol: null,
__focusedRow: null,
__scrollerParent: null,
__selectionManager: null,
__additionalStatusBarText: null,
__lastRowCount: null,
__lastColCount: null,
__internalChange: null,
__columnMenuButtons: null,
__columnModel: null,
__emptyTableModel: null,
__hadVerticalScrollBar: null,
__timer: null,
// overridden
_createChildControlImpl(id, hash) {
var control;
switch (id) {
case "statusbar":
control = new qx.ui.basic.Label();
control.set({
allowGrowX: true
});
this._add(control);
break;
case "column-button":
control = this.getNewColumnMenu()();
control.set({
focusable: false
});
// Create the initial menu too
var menu = control.factory("menu", { table: this });
// Add a listener to initialize the column menu when it becomes visible
menu.addListener("appear", this._initColumnMenu, this);
break;
}
return control || super._createChildControlImpl(id);
},
// property modifier
_applySelectionModel(value, old) {
this.__selectionManager.setSelectionModel(value);
if (old != null) {
old.removeListener("changeSelection", this._onSelectionChanged, this);
}
value.addListener("changeSelection", this._onSelectionChanged, this);
},
// property modifier
_applyRowHeight(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].updateVerScrollBarMaximum();
}
},
// property modifier
_applyHeaderCellsVisible(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
if (value) {
scrollerArr[i]._showChildControl("header");
} else {
scrollerArr[i]._excludeChildControl("header");
}
}
// also hide the column visibility button
if (this.getColumnVisibilityButtonVisible()) {
this._applyColumnVisibilityButtonVisible(value);
}
},
// property modifier
_applyHeaderCellHeight(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].getHeader().setHeight(value);
}
},
// property modifier
_applyMinCellEditHeight(value) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].setMinCellEditHeight(value);
}
},
/**
* Get an empty table model instance to use for this table. Use this table
* to configure the table with no table model.
*
* @return {qx.ui.table.ITableModel} The empty table model
*/
getEmptyTableModel() {
if (!this.__emptyTableModel) {
this.__emptyTableModel = new qx.ui.table.model.Simple();
this.__emptyTableModel.setColumns([]);
this.__emptyTableModel.setData([]);
}
return this.__emptyTableModel;
},
// property modifier
_applyTableModel(value, old) {
this.getTableColumnModel().init(value.getColumnCount(), this);
if (old != null) {
old.removeListener(
"metaDataChanged",
this._onTableModelMetaDataChanged,
this
);
old.removeListener("dataChanged", this._onTableModelDataChanged, this);
}
value.addListener(
"metaDataChanged",
this._onTableModelMetaDataChanged,
this
);
value.addListener("dataChanged", this._onTableModelDataChanged, this);
// Update the status bar
this._updateStatusBar();
this._updateTableData(0, value.getRowCount(), 0, value.getColumnCount());
this._onTableModelMetaDataChanged();
// If the table model has an init() method, call it. We don't, however,
// call it if this is the initial setting of the table model, as the
// scrollers are not yet initialized. In that case, the init method is
// called explicitly by the Table constructor.
if (old && value.init && typeof value.init == "function") {
value.init(this);
}
},
/**
* Get the The table column model.
*
* @return {qx.ui.table.columnmodel.Basic} The table's column model
*/
getTableColumnModel() {
if (!this.__columnModel) {
var columnModel = (this.__columnModel =
this.getNewTableColumnModel()(this));
columnModel.addListener(
"visibilityChanged",
this._onColVisibilityChanged,
this
);
columnModel.addListener("widthChanged", this._onColWidthChanged, this);
columnModel.addListener("orderChanged", this._onColOrderChanged, this);
// Get the current table model
var tableModel = this.getTableModel();
columnModel.init(tableModel.getColumnCount(), this);
// Reset the table column model in each table pane model
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
var paneScroller = scrollerArr[i];
var paneModel = paneScroller.getTablePaneModel();
paneModel.setTableColumnModel(columnModel);
}
}
return this.__columnModel;
},
// property modifier
_applyStatusBarVisible(value, old) {
if (value) {
this._showChildControl("statusbar");
} else {
this._excludeChildControl("statusbar");
}
if (value) {
this._updateStatusBar();
}
},
// property modifier
_applyAdditionalStatusBarText(value, old) {
this.__additionalStatusBarText = value;
this._updateStatusBar();
},
// property modifier
_applyColumnVisibilityButtonVisible(value, old) {
if (value) {
this._showChildControl("column-button");
} else {
this._excludeChildControl("column-button");
}
},
// property modifier
_applyMetaColumnCounts(value, old) {
var metaColumnCounts = value;
var scrollerArr = this._getPaneScrollerArr();
var handlers = {};
if (value > old) {
// Save event listeners on the redirected events so we can re-apply
// them to new scrollers.
var manager = qx.event.Registration.getManager(scrollerArr[0]);
for (var evName in qx.ui.table.Table.__redirectEvents) {
handlers[evName] = {};
handlers[evName].capture = manager.getListeners(
scrollerArr[0],
evName,
true
);
handlers[evName].bubble = manager.getListeners(
scrollerArr[0],
evName,
false
);
}
}
// Remove the panes not needed any more
this._cleanUpMetaColumns(metaColumnCounts.length);
// Update the old panes
var leftX = 0;
for (var i = 0; i < scrollerArr.length; i++) {
var paneScroller = scrollerArr[i];
var paneModel = paneScroller.getTablePaneModel();
paneModel.setFirstColumnX(leftX);
paneModel.setMaxColumnCount(metaColumnCounts[i]);
leftX += metaColumnCounts[i];
}
// Add the new panes
if (metaColumnCounts.length > scrollerArr.length) {
var columnModel = this.getTableColumnModel();
for (var i = scrollerArr.length; i < metaColumnCounts.length; i++) {
var paneModel = this.getNewTablePaneModel()(columnModel);
paneModel.setFirstColumnX(leftX);
paneModel.setMaxColumnCount(metaColumnCounts[i]);
leftX += metaColumnCounts[i];
var paneScroller = this.getNewTablePaneScroller()(this);
paneScroller.setTablePaneModel(paneModel);
// Register event listener for vertical scrolling
paneScroller.addListener("changeScrollY", this._onScrollY, this);
// Apply redirected events to this new scroller
for (evName in qx.ui.table.Table.__redirectEvents) {
// On first setting of meta columns (constructing phase), there
// are no handlers to deal with yet.
if (!handlers[evName]) {
break;
}
if (
handlers[evName].capture &&
handlers[evName].capture.length > 0
) {
var capture = handlers[evName].capture;
for (var j = 0; j < capture.length; j++) {
// Determine what context to use. If the context does not
// exist, we assume that the context is this table. If it
// does exist and it equals the first pane scroller (from
// which we retrieved the listeners) then set the context
// to be this new pane scroller. Otherwise leave the context
// as it was set.
var context = capture[j].context;
if (!context) {
context = this;
} else if (context == scrollerArr[0]) {
context = paneScroller;
}
paneScroller.addListener(
evName,
capture[j].handler,
context,
true
);
}
}
if (handlers[evName].bubble && handlers[evName].bubble.length > 0) {
var bubble = handlers[evName].bubble;
for (var j = 0; j < bubble.length; j++) {
// Determine what context to use. If the context does not
// exist, we assume that the context is this table. If it
// does exist and it equals the first pane scroller (from
// which we retrieved the listeners) then set the context
// to be this new pane scroller. Otherwise leave the context
// as it was set.
var context = bubble[j].context;
if (!context) {
context = this;
} else if (context == scrollerArr[0]) {
context = paneScroller;
}
paneScroller.addListener(
evName,
bubble[j].handler,
context,
false
);
}
}
}
// last meta column is flexible
var flex = i == metaColumnCounts.length - 1 ? 1 : 0;
this.__scrollerParent.add(paneScroller, { flex: flex });
scrollerArr = this._getPaneScrollerArr();
}
}
// Update all meta columns
for (var i = 0; i < scrollerArr.length; i++) {
var paneScroller = scrollerArr[i];
var isLast = i == scrollerArr.length - 1;
// Set the right header height
paneScroller.getHeader().setHeight(this.getHeaderCellHeight());
// Put the column visibility button in the top right corner of the last meta column
paneScroller.setTopRightWidget(
isLast ? this.getChildControl("column-button") : null
);
}
if (!this.isColumnVisibilityButtonVisible()) {
this._excludeChildControl("column-button");
}
this._updateScrollerWidths();
this._updateScrollBarVisibility();
},
// property modifier
_applyFocusCellOnPointerMove(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].setFocusCellOnPointerMove(value);
}
},
// property modifier
_applyShowCellFocusIndicator(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].setShowCellFocusIndicator(value);
}
},
// property modifier
_applyContextMenuFromDataCellsOnly(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].setContextMenuFromDataCellsOnly(value);
}
},
// property modifier
_applyKeepFirstVisibleRowComplete(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].onKeepFirstVisibleRowCompleteChanged();
}
},
// property modifier
_applyResetSelectionOnHeaderTap(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].setResetSelectionOnHeaderTap(value);
}
},
// property modifier
_applyResetSelectionOnTapBelowRows(value, old) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].setResetSelectionOnTapBelowRows(value);
}
},
/**
* Returns the selection manager.
*
* @return {qx.ui.table.selection.Manager} the selection manager.
*/
getSelectionManager() {
return this.__selectionManager;
},
/**
* Returns an array containing all TablePaneScrollers in this table.
*
* @return {qx.ui.table.pane.Scroller[]} all TablePaneScrollers in this table.
*/
_getPaneScrollerArr() {
return this.__scrollerParent.getChildren();
},
/**
* Returns a TablePaneScroller of this table.
*
* @param metaColumn {Integer} the meta column to get the TablePaneScroller for.
* @return {qx.ui.table.pane.Scroller} the qx.ui.table.pane.Scroller.
*/
getPaneScroller(metaColumn) {
return this._getPaneScrollerArr()[metaColumn];
},
/**
* Cleans up the meta columns.
*
* @param fromMetaColumn {Integer} the first meta column to clean up. All following
* meta columns will be cleaned up, too. All previous meta columns will
* stay unchanged. If 0 all meta columns will be cleaned up.
*/
_cleanUpMetaColumns(fromMetaColumn) {
var scrollerArr = this._getPaneScrollerArr();
if (scrollerArr != null) {
for (var i = scrollerArr.length - 1; i >= fromMetaColumn; i--) {
scrollerArr[i].destroy();
}
}
},
/**
* Event handler. Called when the locale has changed.
*
* @param evt {Event} the event.
*/
_onChangeLocale(evt) {
this.updateContent();
this._updateStatusBar();
},
// overridden
_onChangeTheme() {
super._onChangeTheme();
this.getDataRowRenderer().initThemeValues();
this.updateContent();
this._updateStatusBar();
},
/**
* Event handler. Called when the selection has changed.
*
* @param evt {Map} the event.
*/
_onSelectionChanged(evt) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].onSelectionChanged();
}
this._updateStatusBar();
},
/**
* Event handler. Called when the table model meta data has changed.
*
* @param evt {Map} the event.
*/
_onTableModelMetaDataChanged(evt) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].onTableModelMetaDataChanged();
}
this._updateStatusBar();
},
/**
* Event handler. Called when the table model data has changed.
*
* @param evt {Map} the event.
*/
_onTableModelDataChanged(evt) {
var data = evt.getData();
this._updateTableData(
data.firstRow,
data.lastRow,
data.firstColumn,
data.lastColumn,
data.removeStart,
data.removeCount
);
},
// overridden
_onContextMenuOpen(e) {
// This is Widget's context menu handler which typically retrieves
// and displays the menu as soon as it receives a "contextmenu" event.
// We want to allow the cellContextmenu handler to create the menu,
// so we'll override this method with a null one, and do the menu
// placement and display handling in our _onContextMenu method.
},
/**
* To update the table if the table model has changed and remove selection.
*
* @param firstRow {Integer} The index of the first row that has changed.
* @param lastRow {Integer} The index of the last row that has changed.
* @param firstColumn {Integer} The model index of the first column that has changed.
* @param lastColumn {Integer} The model index of the last column that has changed.
* @param removeStart {Integer ? null} The first index of the interval (including), to remove selection.
* @param removeCount {Integer ? null} The count of the interval, to remove selection.
*/
_updateTableData(
firstRow,
lastRow,
firstColumn,
lastColumn,
removeStart,
removeCount
) {
var scrollerArr = this._getPaneScrollerArr();
// update selection if rows were removed
if (removeCount) {
this.getSelectionModel().removeSelectionInterval(
removeStart,
removeStart + removeCount - 1,
true
);
// remove focus if the focused row has been removed
if (
this.__focusedRow >= removeStart &&
this.__focusedRow < removeStart + removeCount
) {
this.setFocusedCell();
}
}
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].onTableModelDataChanged(
firstRow,
lastRow,
firstColumn,
lastColumn
);
}
var rowCount = this.getTableModel().getRowCount();
if (rowCount != this.__lastRowCount) {
this.__lastRowCount = rowCount;
this._updateScrollBarVisibility();
this._updateStatusBar();
// ARIA attrs
this.getContentElement().setAttribute("aria-rowcount", rowCount);
}
const colCount = this.getTableModel().getColumnCount();
if (colCount != this.__lastColCount) {
this.__lastColCount = colCount;
// ARIA attrs
this.getContentElement().setAttribute("aria-colcount", colCount);
}
},
/**
* Event handler. Called when a TablePaneScroller has been scrolled vertically.
*
* @param evt {Map} the event.
*/
_onScrollY(evt) {
if (!this.__internalChange) {
this.__internalChange = true;
// Set the same scroll position to all meta columns
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].setScrollY(evt.getData());
}
this.__internalChange = false;
}
},
/**
* Event handler. Called when a key was pressed.
*
* @param evt {qx.event.type.KeySequence} the event.
* @deprecated {6.0} please use _onKeyDown instead!
*/
_onKeyPress(evt) {
qx.log.Logger.deprecatedMethodWarning(
this._onKeyPress,
"The method '_onKeyPress()' is deprecated. Please use '_onKeyDown()' instead."
);
qx.log.Logger.deprecateMethodOverriding(
this,
qx.ui.table.Table,
"_onKeyPress",
"The method '_onKeyPress()' is deprecated. Please use '_onKeyDown()' instead."
);
this._onKeyDown(evt);
},
/**
* Event handler. Called when on key down event
*
* @param evt {qx.event.type.KeySequence} the event.
*/
_onKeyDown(evt) {
if (!this.getEnabled()) {
return;
}
// No editing mode
var oldFocusedRow = this.__focusedRow;
var consumed = false;
// Handle keys that are independent from the modifiers
var identifier = evt.getKeyIdentifier();
if (this.isEditing()) {
// Editing mode
if (evt.getModifiers() == 0) {
switch (identifier) {
case "Enter":
this.stopEditing();
var oldFocusedRow = this.__focusedRow;
this.moveFocusedCell(0, 1);
if (this.__focusedRow != oldFocusedRow) {
consumed = this.startEditing();
}
break;
case "Escape":
this.cancelEditing();
this.focus();
break;
default:
consumed = false;
break;
}
}
} else {
consumed = true;
// No editing mode
if (evt.isCtrlPressed()) {
// Handle keys that depend on modifiers
switch (identifier) {
case "A": // Ctrl + A
var rowCount = this.getTableModel().getRowCount();
if (rowCount > 0) {
this.getSelectionModel().setSelectionInterval(0, rowCount - 1);
}
break;
default:
consumed = false;
break;
}
} else {
// Handle keys that are independent from the modifiers
switch (identifier) {
case "Space":
this.__selectionManager.handleSelectKeyDown(
this.__focusedRow,
evt
);
break;
case "F2":
case "Enter":
this.startEditing();
consumed = true;
break;
case "Home":
this.setFocusedCell(this.__focusedCol, 0, true);
break;
case "End":
var rowCount = this.getTableModel().getRowCount();
this.setFocusedCell(this.__focusedCol, rowCount - 1, true);
break;
case "Left":
this.moveFocusedCell(-1, 0);
break;
case "Right":
this.moveFocusedCell(1, 0);
break;
case "Up":
this.moveFocusedCell(0, -1);
break;
case "Down":
this.moveFocusedCell(0, 1);
break;
case "PageUp":
case "PageDown":
var scroller = this.getPaneScroller(0);
var pane = scroller.getTablePane();
var rowHeight = this.getRowHeight();
var direction = identifier == "PageUp" ? -1 : 1;
rowCount = pane.getVisibleRowCount() - 1;
scroller.setScrollY(
scroller.getScrollY() + direction * rowCount * rowHeight
);
this.moveFocusedCell(0, direction * rowCount);
break;
default:
consumed = false;
}
}
}
if (
oldFocusedRow != this.__focusedRow &&
this.getRowFocusChangeModifiesSelection()
) {
// The focus moved -> Let the selection manager handle this event
this.__selectionManager.handleMoveKeyDown(this.__focusedRow, evt);
}
if (consumed) {
evt.preventDefault();
evt.stopPropagation();
}
},
/**
* Event handler. Called when the table gets the focus.
*
* @param evt {Map} the event.
*/
_onFocusChanged(evt) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].onFocusChanged();
}
},
/**
* Event handler. Called when the visibility of a column has changed.
*
* @param evt {Map} the event.
*/
_onColVisibilityChanged(evt) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].onColVisibilityChanged();
}
var data = evt.getData();
if (
this.__columnMenuButtons != null &&
data.col != null &&
data.visible != null
) {
this.__columnMenuButtons[data.col].setColumnVisible(data.visible);
}
this._updateScrollerWidths();
this._updateScrollBarVisibility();
},
/**
* Event handler. Called when the width of a column has changed.
*
* @param evt {Map} the event.
*/
_onColWidthChanged(evt) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
var data = evt.getData();
scrollerArr[i].setColumnWidth(data.col, data.newWidth);
}
this._updateScrollerWidths();
this._updateScrollBarVisibility();
},
/**
* Event handler. Called when the column order has changed.
*
* @param evt {Map} the event.
*/
_onColOrderChanged(evt) {
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].onColOrderChanged();
}
// A column may have been moved between meta columns
this._updateScrollerWidths();
this._updateScrollBarVisibility();
},
/**
* Gets the TablePaneScroller at a certain x position in the page. If there is
* no TablePaneScroller at this position, null is returned.
*
* @param pageX {Integer} the position in the page to check (in pixels).
* @return {qx.ui.table.pane.Scroller} the TablePaneScroller or null.
*/
getTablePaneScrollerAtPageX(pageX) {
var metaCol = this._getMetaColumnAtPageX(pageX);
return metaCol != -1 ? this.getPaneScroller(metaCol) : null;
},
/**
* Sets the currently focused cell. A value of <code>null</code> hides the
* focus cell.
*
* @param col {Integer?null} the model index of the focused cell's column.
* @param row {Integer?null} the model index of the focused cell's row.
* @param scrollVisible {Boolean ? false} whether to scroll the new focused cell
* visible.
*/
setFocusedCell(col, row, scrollVisible) {
if (
!this.isEditing() &&
(col != this.__focusedCol || row != this.__focusedRow)
) {
if (col === null) {
col = 0;
}
this.__focusedCol = col;
this.__focusedRow = row;
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
scrollerArr[i].setFocusedCell(col, row);
}
if (col != null && scrollVisible) {
this.scrollCellVisible(col, row);
}
// ARIA attrs
const cellId =
"qooxdoo-table-cell-" + this.toHashCode() + "-" + row + "-" + col;
this.getContentElement().setAttribute("aria-activedescendant", cellId);
}
},
/**
* Resets (clears) the current selection
*/
resetSelection() {
this.getSelectionModel().resetSelection();
},
/**
* Resets the focused cell.
*/
resetCellFocus() {
this.setFocusedCell(null, null, false);
},
/**
* Returns the column of the currently focused cell.
*
* @return {Integer} the model index of the focused cell's column.
*/
getFocusedColumn() {
return this.__focusedCol;
},
/**
* Returns the row of the currently focused cell.
*
* @return {Integer} the model index of the focused cell's column.
*/
getFocusedRow() {
return this.__focusedRow;
},
/**
* Select whether the focused row is highlighted
*
* @param bHighlight {Boolean}
* Flag indicating whether the focused row should be highlighted.
*
*/
highlightFocusedRow(bHighlight) {
this.getDataRowRenderer().setHighlightFocusRow(bHighlight);
},
/**
* Remove the highlighting of the current focus row.
*
* This is used to temporarily remove the highlighting of the currently
* focused row, and is expected to be used most typically by adding a
* listener on the "pointerout" event, so that the focus highlighting is
* suspended when the pointer leaves the table:
*
* table.addListener("pointerout", table.clearFocusedRowHighlight);
*
* @param evt {qx.event.type.Pointer} Incoming pointer event
*/
clearFocusedRowHighlight(evt) {
if (evt) {
var relatedTarget = evt.getRelatedTarget();
if (
relatedTarget instanceof qx.ui.table.pane.Pane ||
relatedTarget instanceof qx.ui.table.pane.FocusIndicator
) {
return;
}
}
// Remove focus from any cell that has it
this.resetCellFocus();
// Now, for each pane scroller...
var scrollerArr = this._getPaneScrollerArr();
for (var i = 0; i < scrollerArr.length; i++) {
// ... repaint without focus.
scrollerArr[i].onFocusChanged();
}
},
/**
* Moves the focus.
*
* @param deltaX {Integer} The delta by which the focus should be moved on the x axis.
* @param deltaY {Integer} The delta by which the focus should be moved on the y axis.
*/
moveFocusedCell(deltaX, deltaY) {
var col = this.__focusedCol;
var row = this.__focusedRow;
// could also be undefined [BUG #4676]. In that case default to first cell focus
if (
col === null ||
col === undefined ||
row === null ||
row === undefined
) {
this.setFocusedCell(0, 0, true);
return;
}
if (deltaX != 0) {
var columnModel = this.getTableColumnModel();
var x = columnModel.getVisibleX(col);
var colCount = columnModel.getVisibleColumnCount();
x = qx.lang.Number.limit(x + deltaX, 0, colCount - 1);
col = columnModel.getVisibleColumnAtX(x);
}
if (deltaY != 0) {
var tableModel = this.getTableModel();
row = qx.lang.Number.limit(
row + deltaY,
0,
tableModel.getRowCount() - 1
);
}
this.setFocused