You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11587 lines
400 KiB
JavaScript

"use strict";
(self["webpackChunk_JUPYTERLAB_CORE_OUTPUT"] = self["webpackChunk_JUPYTERLAB_CORE_OUTPUT"] || []).push([[8929],{
/***/ 98929:
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ AsyncCellRenderer: () => (/* binding */ AsyncCellRenderer),
/* harmony export */ BasicKeyHandler: () => (/* binding */ BasicKeyHandler),
/* harmony export */ BasicMouseHandler: () => (/* binding */ BasicMouseHandler),
/* harmony export */ BasicSelectionModel: () => (/* binding */ BasicSelectionModel),
/* harmony export */ BooleanCellEditor: () => (/* binding */ BooleanCellEditor),
/* harmony export */ CellEditor: () => (/* binding */ CellEditor),
/* harmony export */ CellEditorController: () => (/* binding */ CellEditorController),
/* harmony export */ CellGroup: () => (/* binding */ CellGroup),
/* harmony export */ CellRenderer: () => (/* binding */ CellRenderer),
/* harmony export */ DataGrid: () => (/* binding */ DataGrid),
/* harmony export */ DataModel: () => (/* binding */ DataModel),
/* harmony export */ DateCellEditor: () => (/* binding */ DateCellEditor),
/* harmony export */ DynamicOptionCellEditor: () => (/* binding */ DynamicOptionCellEditor),
/* harmony export */ GraphicsContext: () => (/* binding */ GraphicsContext),
/* harmony export */ HyperlinkRenderer: () => (/* binding */ HyperlinkRenderer),
/* harmony export */ ImageRenderer: () => (/* binding */ ImageRenderer),
/* harmony export */ InputCellEditor: () => (/* binding */ InputCellEditor),
/* harmony export */ IntegerCellEditor: () => (/* binding */ IntegerCellEditor),
/* harmony export */ IntegerInputValidator: () => (/* binding */ IntegerInputValidator),
/* harmony export */ JSONModel: () => (/* binding */ JSONModel),
/* harmony export */ MutableDataModel: () => (/* binding */ MutableDataModel),
/* harmony export */ NumberCellEditor: () => (/* binding */ NumberCellEditor),
/* harmony export */ NumberInputValidator: () => (/* binding */ NumberInputValidator),
/* harmony export */ OptionCellEditor: () => (/* binding */ OptionCellEditor),
/* harmony export */ PassInputValidator: () => (/* binding */ PassInputValidator),
/* harmony export */ RendererMap: () => (/* binding */ RendererMap),
/* harmony export */ SectionList: () => (/* binding */ SectionList),
/* harmony export */ SelectionModel: () => (/* binding */ SelectionModel),
/* harmony export */ TextCellEditor: () => (/* binding */ TextCellEditor),
/* harmony export */ TextInputValidator: () => (/* binding */ TextInputValidator),
/* harmony export */ TextRenderer: () => (/* binding */ TextRenderer),
/* harmony export */ resolveOption: () => (/* binding */ resolveOption)
/* harmony export */ });
/* harmony import */ var _lumino_domutils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(23738);
/* harmony import */ var _lumino_domutils__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_lumino_domutils__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(71864);
/* harmony import */ var _lumino_keyboard__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _lumino_dragdrop__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(67344);
/* harmony import */ var _lumino_dragdrop__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_lumino_dragdrop__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(56114);
/* harmony import */ var _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__);
/* harmony import */ var _lumino_signaling__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(2536);
/* harmony import */ var _lumino_signaling__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_lumino_signaling__WEBPACK_IMPORTED_MODULE_4__);
/* harmony import */ var _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(60920);
/* harmony import */ var _lumino_widgets__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_lumino_widgets__WEBPACK_IMPORTED_MODULE_5__);
/* harmony import */ var _lumino_messaging__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(34993);
/* harmony import */ var _lumino_messaging__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__);
/* harmony import */ var _lumino_coreutils__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(5406);
/* harmony import */ var _lumino_coreutils__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(_lumino_coreutils__WEBPACK_IMPORTED_MODULE_7__);
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A basic implementation of a data grid key handler.
*
* #### Notes
* This class may be subclassed and customized as needed.
*/
class BasicKeyHandler {
constructor() {
this._disposed = false;
}
/**
* Whether the key handler is disposed.
*/
get isDisposed() {
return this._disposed;
}
/**
* Dispose of the resources held by the key handler.
*/
dispose() {
this._disposed = true;
}
/**
* Handle the key down event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keydown event of interest.
*
* #### Notes
* This will not be called if the mouse button is pressed.
*/
onKeyDown(grid, event) {
// if grid is editable and cell selection available, start cell editing
// on key press (letters, numbers and space only)
if (grid.editable &&
grid.selectionModel.cursorRow !== -1 &&
grid.selectionModel.cursorColumn !== -1) {
const input = String.fromCharCode(event.keyCode);
if (/[a-zA-Z0-9-_ ]/.test(input)) {
const row = grid.selectionModel.cursorRow;
const column = grid.selectionModel.cursorColumn;
const cell = {
grid: grid,
row: row,
column: column
};
grid.editorController.edit(cell);
if ((0,_lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__.getKeyboardLayout)().keyForKeydownEvent(event) === 'Space') {
event.stopPropagation();
event.preventDefault();
}
return;
}
}
switch ((0,_lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__.getKeyboardLayout)().keyForKeydownEvent(event)) {
case 'ArrowLeft':
this.onArrowLeft(grid, event);
break;
case 'ArrowRight':
this.onArrowRight(grid, event);
break;
case 'ArrowUp':
this.onArrowUp(grid, event);
break;
case 'ArrowDown':
this.onArrowDown(grid, event);
break;
case 'PageUp':
this.onPageUp(grid, event);
break;
case 'PageDown':
this.onPageDown(grid, event);
break;
case 'Escape':
this.onEscape(grid, event);
break;
case 'Delete':
this.onDelete(grid, event);
break;
case 'C':
this.onKeyC(grid, event);
break;
case 'Enter':
if (grid.selectionModel) {
grid.moveCursor(event.shiftKey ? 'up' : 'down');
grid.scrollToCursor();
}
break;
case 'Tab':
if (grid.selectionModel) {
grid.moveCursor(event.shiftKey ? 'left' : 'right');
grid.scrollToCursor();
event.stopPropagation();
event.preventDefault();
}
break;
}
}
/**
* Handle the `'ArrowLeft'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onArrowLeft(grid, event) {
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Fetch the selection model.
let model = grid.selectionModel;
// Fetch the modifier flags.
let shift = event.shiftKey;
let accel = _lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event);
// Handle no model with the accel modifier.
if (!model && accel) {
grid.scrollTo(0, grid.scrollY);
return;
}
// Handle no model and no modifier. (ignore shift)
if (!model) {
grid.scrollByStep('left');
return;
}
// Fetch the selection mode.
let mode = model.selectionMode;
// Handle the row selection mode with accel key.
if (mode === 'row' && accel) {
grid.scrollTo(0, grid.scrollY);
return;
}
// Handle the row selection mode with no modifier. (ignore shift)
if (mode === 'row') {
grid.scrollByStep('left');
return;
}
// Fetch the cursor and selection.
let r = model.cursorRow;
let c = model.cursorColumn;
let cs = model.currentSelection();
// Set up the selection variables.
let r1;
let r2;
let c1;
let c2;
let cr;
let cc;
let clear;
// Dispatch based on the modifier keys.
if (accel && shift) {
r1 = cs ? cs.r1 : 0;
r2 = cs ? cs.r2 : 0;
c1 = cs ? cs.c1 : 0;
c2 = 0;
cr = r;
cc = c;
clear = 'current';
}
else if (shift) {
r1 = cs ? cs.r1 : 0;
r2 = cs ? cs.r2 : 0;
c1 = cs ? cs.c1 : 0;
c2 = cs ? cs.c2 - 1 : 0;
cr = r;
cc = c;
clear = 'current';
}
else if (accel) {
r1 = r;
r2 = r;
c1 = 0;
c2 = 0;
cr = r1;
cc = c1;
clear = 'all';
}
else {
r1 = r;
r2 = r;
c1 = c - 1;
c2 = c - 1;
cr = r1;
cc = c1;
clear = 'all';
}
// Create the new selection.
model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear });
// Re-fetch the current selection.
cs = model.currentSelection();
// Bail if there is no selection.
if (!cs) {
return;
}
// Scroll the grid appropriately.
if (shift || mode === 'column') {
grid.scrollToColumn(cs.c2);
}
else {
grid.scrollToCursor();
}
}
/**
* Handle the `'ArrowRight'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onArrowRight(grid, event) {
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Fetch the selection model.
let model = grid.selectionModel;
// Fetch the modifier flags.
let shift = event.shiftKey;
let accel = _lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event);
// Handle no model with the accel modifier.
if (!model && accel) {
grid.scrollTo(grid.maxScrollX, grid.scrollY);
return;
}
// Handle no model and no modifier. (ignore shift)
if (!model) {
grid.scrollByStep('right');
return;
}
// Fetch the selection mode.
let mode = model.selectionMode;
// Handle the row selection model with accel key.
if (mode === 'row' && accel) {
grid.scrollTo(grid.maxScrollX, grid.scrollY);
return;
}
// Handle the row selection mode with no modifier. (ignore shift)
if (mode === 'row') {
grid.scrollByStep('right');
return;
}
// Fetch the cursor and selection.
let r = model.cursorRow;
let c = model.cursorColumn;
let cs = model.currentSelection();
// Set up the selection variables.
let r1;
let r2;
let c1;
let c2;
let cr;
let cc;
let clear;
// Dispatch based on the modifier keys.
if (accel && shift) {
r1 = cs ? cs.r1 : 0;
r2 = cs ? cs.r2 : 0;
c1 = cs ? cs.c1 : 0;
c2 = Infinity;
cr = r;
cc = c;
clear = 'current';
}
else if (shift) {
r1 = cs ? cs.r1 : 0;
r2 = cs ? cs.r2 : 0;
c1 = cs ? cs.c1 : 0;
c2 = cs ? cs.c2 + 1 : 0;
cr = r;
cc = c;
clear = 'current';
}
else if (accel) {
r1 = r;
r2 = r;
c1 = Infinity;
c2 = Infinity;
cr = r1;
cc = c1;
clear = 'all';
}
else {
r1 = r;
r2 = r;
c1 = c + 1;
c2 = c + 1;
cr = r1;
cc = c1;
clear = 'all';
}
// Create the new selection.
model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear });
// Re-fetch the current selection.
cs = model.currentSelection();
// Bail if there is no selection.
if (!cs) {
return;
}
// Scroll the grid appropriately.
if (shift || mode === 'column') {
grid.scrollToColumn(cs.c2);
}
else {
grid.scrollToCursor();
}
}
/**
* Handle the `'ArrowUp'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onArrowUp(grid, event) {
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Fetch the selection model.
let model = grid.selectionModel;
// Fetch the modifier flags.
let shift = event.shiftKey;
let accel = _lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event);
// Handle no model with the accel modifier.
if (!model && accel) {
grid.scrollTo(grid.scrollX, 0);
return;
}
// Handle no model and no modifier. (ignore shift)
if (!model) {
grid.scrollByStep('up');
return;
}
// Fetch the selection mode.
let mode = model.selectionMode;
// Handle the column selection mode with accel key.
if (mode === 'column' && accel) {
grid.scrollTo(grid.scrollX, 0);
return;
}
// Handle the column selection mode with no modifier. (ignore shift)
if (mode === 'column') {
grid.scrollByStep('up');
return;
}
// Fetch the cursor and selection.
let r = model.cursorRow;
let c = model.cursorColumn;
let cs = model.currentSelection();
// Set up the selection variables.
let r1;
let r2;
let c1;
let c2;
let cr;
let cc;
let clear;
// Dispatch based on the modifier keys.
if (accel && shift) {
r1 = cs ? cs.r1 : 0;
r2 = 0;
c1 = cs ? cs.c1 : 0;
c2 = cs ? cs.c2 : 0;
cr = r;
cc = c;
clear = 'current';
}
else if (shift) {
r1 = cs ? cs.r1 : 0;
r2 = cs ? cs.r2 - 1 : 0;
c1 = cs ? cs.c1 : 0;
c2 = cs ? cs.c2 : 0;
cr = r;
cc = c;
clear = 'current';
}
else if (accel) {
r1 = 0;
r2 = 0;
c1 = c;
c2 = c;
cr = r1;
cc = c1;
clear = 'all';
}
else {
r1 = r - 1;
r2 = r - 1;
c1 = c;
c2 = c;
cr = r1;
cc = c1;
clear = 'all';
}
// Create the new selection.
model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear });
// Re-fetch the current selection.
cs = model.currentSelection();
// Bail if there is no selection.
if (!cs) {
return;
}
// Scroll the grid appropriately.
if (shift || mode === 'row') {
grid.scrollToRow(cs.r2);
}
else {
grid.scrollToCursor();
}
}
/**
* Handle the `'ArrowDown'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onArrowDown(grid, event) {
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Fetch the selection model.
let model = grid.selectionModel;
// Fetch the modifier flags.
let shift = event.shiftKey;
let accel = _lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event);
// Handle no model with the accel modifier.
if (!model && accel) {
grid.scrollTo(grid.scrollX, grid.maxScrollY);
return;
}
// Handle no model and no modifier. (ignore shift)
if (!model) {
grid.scrollByStep('down');
return;
}
// Fetch the selection mode.
let mode = model.selectionMode;
// Handle the column selection mode with accel key.
if (mode === 'column' && accel) {
grid.scrollTo(grid.scrollX, grid.maxScrollY);
return;
}
// Handle the column selection mode with no modifier. (ignore shift)
if (mode === 'column') {
grid.scrollByStep('down');
return;
}
// Fetch the cursor and selection.
let r = model.cursorRow;
let c = model.cursorColumn;
let cs = model.currentSelection();
// Set up the selection variables.
let r1;
let r2;
let c1;
let c2;
let cr;
let cc;
let clear;
// Dispatch based on the modifier keys.
if (accel && shift) {
r1 = cs ? cs.r1 : 0;
r2 = Infinity;
c1 = cs ? cs.c1 : 0;
c2 = cs ? cs.c2 : 0;
cr = r;
cc = c;
clear = 'current';
}
else if (shift) {
r1 = cs ? cs.r1 : 0;
r2 = cs ? cs.r2 + 1 : 0;
c1 = cs ? cs.c1 : 0;
c2 = cs ? cs.c2 : 0;
cr = r;
cc = c;
clear = 'current';
}
else if (accel) {
r1 = Infinity;
r2 = Infinity;
c1 = c;
c2 = c;
cr = r1;
cc = c1;
clear = 'all';
}
else {
r1 = r + 1;
r2 = r + 1;
c1 = c;
c2 = c;
cr = r1;
cc = c1;
clear = 'all';
}
// Create the new selection.
model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear });
// Re-fetch the current selection.
cs = model.currentSelection();
// Bail if there is no selection.
if (!cs) {
return;
}
// Scroll the grid appropriately.
if (shift || mode === 'row') {
grid.scrollToRow(cs.r2);
}
else {
grid.scrollToCursor();
}
}
/**
* Handle the `'PageUp'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onPageUp(grid, event) {
// Ignore the event if the accel key is pressed.
if (_lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event)) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Fetch the selection model.
let model = grid.selectionModel;
// Scroll by page if there is no selection model.
if (!model || model.selectionMode === 'column') {
grid.scrollByPage('up');
return;
}
// Get the normal number of cells in the page height.
let n = Math.floor(grid.pageHeight / grid.defaultSizes.rowHeight);
// Fetch the cursor and selection.
let r = model.cursorRow;
let c = model.cursorColumn;
let cs = model.currentSelection();
// Set up the selection variables.
let r1;
let r2;
let c1;
let c2;
let cr;
let cc;
let clear;
// Select or resize as needed.
if (event.shiftKey) {
r1 = cs ? cs.r1 : 0;
r2 = cs ? cs.r2 - n : 0;
c1 = cs ? cs.c1 : 0;
c2 = cs ? cs.c2 : 0;
cr = r;
cc = c;
clear = 'current';
}
else {
r1 = cs ? cs.r1 - n : 0;
r2 = r1;
c1 = c;
c2 = c;
cr = r1;
cc = c;
clear = 'all';
}
// Create the new selection.
model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear });
// Re-fetch the current selection.
cs = model.currentSelection();
// Bail if there is no selection.
if (!cs) {
return;
}
// Scroll the grid appropriately.
grid.scrollToRow(cs.r2);
}
/**
* Handle the `'PageDown'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onPageDown(grid, event) {
// Ignore the event if the accel key is pressed.
if (_lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event)) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Fetch the selection model.
let model = grid.selectionModel;
// Scroll by page if there is no selection model.
if (!model || model.selectionMode === 'column') {
grid.scrollByPage('down');
return;
}
// Get the normal number of cells in the page height.
let n = Math.floor(grid.pageHeight / grid.defaultSizes.rowHeight);
// Fetch the cursor and selection.
let r = model.cursorRow;
let c = model.cursorColumn;
let cs = model.currentSelection();
// Set up the selection variables.
let r1;
let r2;
let c1;
let c2;
let cr;
let cc;
let clear;
// Select or resize as needed.
if (event.shiftKey) {
r1 = cs ? cs.r1 : 0;
r2 = cs ? cs.r2 + n : 0;
c1 = cs ? cs.c1 : 0;
c2 = cs ? cs.c2 : 0;
cr = r;
cc = c;
clear = 'current';
}
else {
r1 = cs ? cs.r1 + n : 0;
r2 = r1;
c1 = c;
c2 = c;
cr = r1;
cc = c;
clear = 'all';
}
// Create the new selection.
model.select({ r1, c1, r2, c2, cursorRow: cr, cursorColumn: cc, clear });
// Re-fetch the current selection.
cs = model.currentSelection();
// Bail if there is no selection.
if (!cs) {
return;
}
// Scroll the grid appropriately.
grid.scrollToRow(cs.r2);
}
/**
* Handle the `'Escape'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onEscape(grid, event) {
if (grid.selectionModel) {
grid.selectionModel.clear();
}
}
/**
* Handle the `'Delete'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onDelete(grid, event) {
if (grid.editable && !grid.selectionModel.isEmpty) {
const dataModel = grid.dataModel;
// Fetch the max row and column.
let maxRow = dataModel.rowCount('body') - 1;
let maxColumn = dataModel.columnCount('body') - 1;
for (let s of grid.selectionModel.selections()) {
// Clamp the cell to the model bounds.
let sr1 = Math.max(0, Math.min(s.r1, maxRow));
let sc1 = Math.max(0, Math.min(s.c1, maxColumn));
let sr2 = Math.max(0, Math.min(s.r2, maxRow));
let sc2 = Math.max(0, Math.min(s.c2, maxColumn));
for (let r = sr1; r <= sr2; ++r) {
for (let c = sc1; c <= sc2; ++c) {
dataModel.setData('body', r, c, null);
}
}
}
}
}
/**
* Handle the `'C'` key press for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The keyboard event of interest.
*/
onKeyC(grid, event) {
// Bail early if the modifiers aren't correct for copy.
if (event.shiftKey || !_lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event)) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Copy the current selection to the clipboard.
grid.copyToClipboard();
}
}
/**
* An object which renders the cells of a data grid.
*
* #### Notes
* If the predefined cell renderers are insufficient for a particular
* use case, a custom cell renderer can be defined which derives from
* this class.
*
* The data grid renders cells in column-major order, by region. The
* region order is: body, row header, column header, corner header.
*/
class CellRenderer {
}
/**
* The namespace for the `CellRenderer` class statics.
*/
(function (CellRenderer) {
/**
* Resolve a config option for a cell renderer.
*
* @param option - The config option to resolve.
*
* @param config - The cell config object.
*
* @returns The resolved value for the option.
*/
function resolveOption(option, config) {
return typeof option === 'function'
? option(config)
: option;
}
CellRenderer.resolveOption = resolveOption;
})(CellRenderer || (CellRenderer = {}));
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A cell renderer which renders data values as text.
*/
class TextRenderer extends CellRenderer {
/**
* Construct a new text renderer.
*
* @param options - The options for initializing the renderer.
*/
constructor(options = {}) {
super();
this.font = options.font || '12px sans-serif';
this.textColor = options.textColor || '#000000';
this.backgroundColor = options.backgroundColor || '';
this.verticalAlignment = options.verticalAlignment || 'center';
this.horizontalAlignment = options.horizontalAlignment || 'left';
this.horizontalPadding = options.horizontalPadding || 8;
this.format = options.format || TextRenderer.formatGeneric();
this.elideDirection = options.elideDirection || 'none';
this.wrapText = options.wrapText || false;
}
/**
* Paint the content for a cell.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
paint(gc, config) {
this.drawBackground(gc, config);
this.drawText(gc, config);
}
/**
* Draw the background for the cell.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
drawBackground(gc, config) {
// Resolve the background color for the cell.
let color = CellRenderer.resolveOption(this.backgroundColor, config);
// Bail if there is no background color to draw.
if (!color) {
return;
}
// Fill the cell with the background color.
gc.fillStyle = color;
gc.fillRect(config.x, config.y, config.width, config.height);
}
/**
* Get the full text to be rendered by the cell.
*/
getText(config) {
return this.format(config);
}
/**
* Draw the text for the cell.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
drawText(gc, config) {
// Resolve the font for the cell.
let font = CellRenderer.resolveOption(this.font, config);
// Bail if there is no font to draw.
if (!font) {
return;
}
// Resolve the text color for the cell.
let color = CellRenderer.resolveOption(this.textColor, config);
// Bail if there is no text color to draw.
if (!color) {
return;
}
// Format the cell value to text.
let text = this.getText(config);
// Bail if there is no text to draw.
if (!text) {
return;
}
// Resolve the vertical and horizontal alignment.
let vAlign = CellRenderer.resolveOption(this.verticalAlignment, config);
let hAlign = CellRenderer.resolveOption(this.horizontalAlignment, config);
// Resolve the elision direction
let elideDirection = CellRenderer.resolveOption(this.elideDirection, config);
// Resolve the text wrapping flag
let wrapText = CellRenderer.resolveOption(this.wrapText, config);
// Compute the padded text box height for the specified alignment.
let boxHeight = config.height - (vAlign === 'center' ? 1 : 2);
// Bail if the text box has no effective size.
if (boxHeight <= 0) {
return;
}
// Compute the text height for the gc font.
let textHeight = TextRenderer.measureFontHeight(font);
// Set up the text position variables.
let textX;
let textY;
let boxWidth;
// Compute the Y position for the text.
switch (vAlign) {
case 'top':
textY = config.y + 2 + textHeight;
break;
case 'center':
textY = config.y + config.height / 2 + textHeight / 2;
break;
case 'bottom':
textY = config.y + config.height - 2;
break;
default:
throw 'unreachable';
}
// Compute the X position for the text.
switch (hAlign) {
case 'left':
textX = config.x + this.horizontalPadding;
boxWidth = config.width - 14;
break;
case 'center':
textX = config.x + config.width / 2;
boxWidth = config.width;
break;
case 'right':
textX = config.x + config.width - this.horizontalPadding;
boxWidth = config.width - 14;
break;
default:
throw 'unreachable';
}
// Clip the cell if the text is taller than the text box height.
if (textHeight > boxHeight) {
gc.beginPath();
gc.rect(config.x, config.y, config.width, config.height - 1);
gc.clip();
}
// Set the gc state.
gc.font = font;
gc.fillStyle = color;
gc.textAlign = hAlign;
gc.textBaseline = 'bottom';
// Terminate call here if we're not eliding or wrapping text
if (elideDirection === 'none' && !wrapText) {
gc.fillText(text, textX, textY);
return;
}
// The current text width in pixels.
let textWidth = gc.measureText(text).width;
// Apply text wrapping if enabled.
if (wrapText && textWidth > boxWidth) {
// Make sure box clipping happens.
gc.beginPath();
gc.rect(config.x, config.y, config.width, config.height - 1);
gc.clip();
// Split column name to words based on
// whitespace preceding a word boundary.
// "Hello world" --> ["Hello ", "world"]
const wordsInColumn = text.split(/\s(?=\b)/);
// Y-coordinate offset for any additional lines
let curY = textY;
let textInCurrentLine = wordsInColumn.shift();
// Single word. Applying text wrap on word by splitting
// it into characters and fitting the maximum number of
// characters possible per line (box width).
if (wordsInColumn.length === 0) {
let curLineTextWidth = gc.measureText(textInCurrentLine).width;
while (curLineTextWidth > boxWidth && textInCurrentLine !== '') {
// Iterating from the end of the string until we find a
// substring (0,i) which has a width less than the box width.
for (let i = textInCurrentLine.length; i > 0; i--) {
const curSubString = textInCurrentLine.substring(0, i);
const curSubStringWidth = gc.measureText(curSubString).width;
if (curSubStringWidth < boxWidth || curSubString.length === 1) {
// Found a substring which has a width less than the current
// box width. Rendering that substring on the current line
// and setting the remainder of the parent string as the next
// string to iterate on for the next line.
const nextLineText = textInCurrentLine.substring(i, textInCurrentLine.length);
textInCurrentLine = nextLineText;
curLineTextWidth = gc.measureText(textInCurrentLine).width;
gc.fillText(curSubString, textX, curY);
curY += textHeight;
// No need to continue iterating after we identified
// an index to break the string on.
break;
}
}
}
}
// Multiple words in column header. Fitting maximum
// number of words possible per line (box width).
else {
while (wordsInColumn.length !== 0) {
// Processing the next word in the queue.
const curWord = wordsInColumn.shift();
// Joining that word with the existing text for
// the current line.
const incrementedText = [textInCurrentLine, curWord].join(' ');
const incrementedTextWidth = gc.measureText(incrementedText).width;
if (incrementedTextWidth > boxWidth) {
// If the newly combined text has a width larger than
// the box width, we render the line before the current
// word was added. We set the current word as the next
// line.
gc.fillText(textInCurrentLine, textX, curY);
curY += textHeight;
textInCurrentLine = curWord;
}
else {
// The combined text hasd a width less than the box width. We
// set the the current line text to be the new combined text.
textInCurrentLine = incrementedText;
}
}
}
gc.fillText(textInCurrentLine, textX, curY);
// Terminating the call here as we don't want
// to apply text eliding when wrapping is active.
return;
}
// Elide text that is too long
const elide = '\u2026';
// Loop until text width fits box or only one character remains
while (textWidth > boxWidth && text.length > 1) {
// Convert text string to array for dealing with astral symbols
const textArr = [...text];
if (elideDirection === 'right') {
// If text width is substantially bigger, take half the string
if (textArr.length > 4 && textWidth >= 2 * boxWidth) {
text =
textArr.slice(0, Math.floor(textArr.length / 2 + 1)).join('') +
elide;
}
else {
// Otherwise incrementally remove the last character
text = textArr.slice(0, textArr.length - 2).join('') + elide;
}
}
else {
// If text width is substantially bigger, take half the string
if (textArr.length > 4 && textWidth >= 2 * boxWidth) {
text = elide + textArr.slice(Math.floor(textArr.length / 2)).join('');
}
else {
// Otherwise incrementally remove the last character
text = elide + textArr.slice(2).join('');
}
}
// Measure new text width
textWidth = gc.measureText(text).width;
}
// Draw the text for the cell.
gc.fillText(text, textX, textY);
}
}
/**
* The namespace for the `TextRenderer` class statics.
*/
(function (TextRenderer) {
/**
* Create a generic text format function.
*
* @param options - The options for creating the format function.
*
* @returns A new generic text format function.
*
* #### Notes
* This formatter uses the builtin `String()` to coerce any value
* to a string.
*/
function formatGeneric(options = {}) {
let missing = options.missing || '';
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
return String(value);
};
}
TextRenderer.formatGeneric = formatGeneric;
/**
* Create a fixed decimal format function.
*
* @param options - The options for creating the format function.
*
* @returns A new fixed decimal format function.
*
* #### Notes
* This formatter uses the builtin `Number()` and `toFixed()` to
* coerce values.
*
* The `formatIntlNumber()` formatter is more flexible, but slower.
*/
function formatFixed(options = {}) {
let digits = options.digits;
let missing = options.missing || '';
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
return Number(value).toFixed(digits);
};
}
TextRenderer.formatFixed = formatFixed;
/**
* Create a significant figure format function.
*
* @param options - The options for creating the format function.
*
* @returns A new significant figure format function.
*
* #### Notes
* This formatter uses the builtin `Number()` and `toPrecision()`
* to coerce values.
*
* The `formatIntlNumber()` formatter is more flexible, but slower.
*/
function formatPrecision(options = {}) {
let digits = options.digits;
let missing = options.missing || '';
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
return Number(value).toPrecision(digits);
};
}
TextRenderer.formatPrecision = formatPrecision;
/**
* Create a scientific notation format function.
*
* @param options - The options for creating the format function.
*
* @returns A new scientific notation format function.
*
* #### Notes
* This formatter uses the builtin `Number()` and `toExponential()`
* to coerce values.
*
* The `formatIntlNumber()` formatter is more flexible, but slower.
*/
function formatExponential(options = {}) {
let digits = options.digits;
let missing = options.missing || '';
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
return Number(value).toExponential(digits);
};
}
TextRenderer.formatExponential = formatExponential;
/**
* Create an international number format function.
*
* @param options - The options for creating the format function.
*
* @returns A new international number format function.
*
* #### Notes
* This formatter uses the builtin `Intl.NumberFormat` object to
* coerce values.
*
* This is the most flexible (but slowest) number formatter.
*/
function formatIntlNumber(options = {}) {
let missing = options.missing || '';
let nft = new Intl.NumberFormat(options.locales, options.options);
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
return nft.format(value);
};
}
TextRenderer.formatIntlNumber = formatIntlNumber;
/**
* Create a date format function.
*
* @param options - The options for creating the format function.
*
* @returns A new date format function.
*
* #### Notes
* This formatter uses `Date.toDateString()` to format the values.
*
* If a value is not a `Date` object, `new Date(value)` is used to
* coerce the value to a date.
*
* The `formatIntlDateTime()` formatter is more flexible, but slower.
*/
function formatDate(options = {}) {
let missing = options.missing || '';
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
if (value instanceof Date) {
return value.toDateString();
}
return new Date(value).toDateString();
};
}
TextRenderer.formatDate = formatDate;
/**
* Create a time format function.
*
* @param options - The options for creating the format function.
*
* @returns A new time format function.
*
* #### Notes
* This formatter uses `Date.toTimeString()` to format the values.
*
* If a value is not a `Date` object, `new Date(value)` is used to
* coerce the value to a date.
*
* The `formatIntlDateTime()` formatter is more flexible, but slower.
*/
function formatTime(options = {}) {
let missing = options.missing || '';
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
if (value instanceof Date) {
return value.toTimeString();
}
return new Date(value).toTimeString();
};
}
TextRenderer.formatTime = formatTime;
/**
* Create an ISO datetime format function.
*
* @param options - The options for creating the format function.
*
* @returns A new ISO datetime format function.
*
* #### Notes
* This formatter uses `Date.toISOString()` to format the values.
*
* If a value is not a `Date` object, `new Date(value)` is used to
* coerce the value to a date.
*
* The `formatIntlDateTime()` formatter is more flexible, but slower.
*/
function formatISODateTime(options = {}) {
let missing = options.missing || '';
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
if (value instanceof Date) {
return value.toISOString();
}
return new Date(value).toISOString();
};
}
TextRenderer.formatISODateTime = formatISODateTime;
/**
* Create a UTC datetime format function.
*
* @param options - The options for creating the format function.
*
* @returns A new UTC datetime format function.
*
* #### Notes
* This formatter uses `Date.toUTCString()` to format the values.
*
* If a value is not a `Date` object, `new Date(value)` is used to
* coerce the value to a date.
*
* The `formatIntlDateTime()` formatter is more flexible, but slower.
*/
function formatUTCDateTime(options = {}) {
let missing = options.missing || '';
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
if (value instanceof Date) {
return value.toUTCString();
}
return new Date(value).toUTCString();
};
}
TextRenderer.formatUTCDateTime = formatUTCDateTime;
/**
* Create an international datetime format function.
*
* @param options - The options for creating the format function.
*
* @returns A new international datetime format function.
*
* #### Notes
* This formatter uses the builtin `Intl.DateTimeFormat` object to
* coerce values.
*
* This is the most flexible (but slowest) datetime formatter.
*/
function formatIntlDateTime(options = {}) {
let missing = options.missing || '';
let dtf = new Intl.DateTimeFormat(options.locales, options.options);
return ({ value }) => {
if (value === null || value === undefined) {
return missing;
}
return dtf.format(value);
};
}
TextRenderer.formatIntlDateTime = formatIntlDateTime;
/**
* Measure the height of a font.
*
* @param font - The CSS font string of interest.
*
* @returns The height of the font bounding box.
*
* #### Notes
* This function uses a temporary DOM node to measure the text box
* height for the specified font. The first call for a given font
* will incur a DOM reflow, but the return value is cached, so any
* subsequent call for the same font will return the cached value.
*/
function measureFontHeight(font) {
// Look up the cached font height.
let height = Private$6.fontHeightCache[font];
// Return the cached font height if it exists.
if (height !== undefined) {
return height;
}
// Normalize the font.
Private$6.fontMeasurementGC.font = font;
let normFont = Private$6.fontMeasurementGC.font;
// Set the font on the measurement node.
Private$6.fontMeasurementNode.style.font = normFont;
// Add the measurement node to the document.
document.body.appendChild(Private$6.fontMeasurementNode);
// Measure the node height.
height = Private$6.fontMeasurementNode.offsetHeight;
// Remove the measurement node from the document.
document.body.removeChild(Private$6.fontMeasurementNode);
// Cache the measured height for the font and norm font.
Private$6.fontHeightCache[font] = height;
Private$6.fontHeightCache[normFont] = height;
// Return the measured height.
return height;
}
TextRenderer.measureFontHeight = measureFontHeight;
})(TextRenderer || (TextRenderer = {}));
/**
* The namespace for the module implementation details.
*/
var Private$6;
(function (Private) {
/**
* A cache of measured font heights.
*/
Private.fontHeightCache = Object.create(null);
/**
* The DOM node used for font height measurement.
*/
Private.fontMeasurementNode = (() => {
let node = document.createElement('div');
node.style.position = 'absolute';
node.style.top = '-99999px';
node.style.left = '-99999px';
node.style.visibility = 'hidden';
node.textContent = 'M';
return node;
})();
/**
* The GC used for font measurement.
*/
Private.fontMeasurementGC = (() => {
let canvas = document.createElement('canvas');
canvas.width = 0;
canvas.height = 0;
return canvas.getContext('2d');
})();
})(Private$6 || (Private$6 = {}));
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A cell renderer which renders data values as text.
*/
class HyperlinkRenderer extends TextRenderer {
/**
* Construct a new text renderer.
*
* @param options - The options for initializing the renderer.
*/
constructor(options = {}) {
// Set default parameters before passing over the super.
options.textColor = options.textColor || 'navy';
options.font = options.font || 'bold 12px sans-serif';
super(options);
this.url = options.url;
this.urlName = options.urlName;
}
/**
* Get the full text to be rendered by the cell.
*/
getText(config) {
let urlName = CellRenderer.resolveOption(this.urlName, config);
// If we have a friendly URL name, use that.
if (urlName) {
return this.format({
...config,
value: urlName
});
}
// Otherwise use the raw value attribute.
return this.format(config);
}
/**
* Draw the text for the cell.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
drawText(gc, config) {
// Resolve the font for the cell.
let font = CellRenderer.resolveOption(this.font, config);
// Bail if there is no font to draw.
if (!font) {
return;
}
// Resolve the text color for the cell.
let color = CellRenderer.resolveOption(this.textColor, config);
// Bail if there is no text color to draw.
if (!color) {
return;
}
let text = this.getText(config);
// Bail if there is no text to draw.
if (!text) {
return;
}
// Resolve the vertical and horizontal alignment.
let vAlign = CellRenderer.resolveOption(this.verticalAlignment, config);
let hAlign = CellRenderer.resolveOption(this.horizontalAlignment, config);
// Resolve the elision direction
let elideDirection = CellRenderer.resolveOption(this.elideDirection, config);
// Resolve the text wrapping flag
let wrapText = CellRenderer.resolveOption(this.wrapText, config);
// Compute the padded text box height for the specified alignment.
let boxHeight = config.height - (vAlign === 'center' ? 1 : 2);
// Bail if the text box has no effective size.
if (boxHeight <= 0) {
return;
}
// Compute the text height for the gc font.
let textHeight = HyperlinkRenderer.measureFontHeight(font);
// Set up the text position variables.
let textX;
let textY;
let boxWidth;
// Compute the Y position for the text.
switch (vAlign) {
case 'top':
textY = config.y + 2 + textHeight;
break;
case 'center':
textY = config.y + config.height / 2 + textHeight / 2;
break;
case 'bottom':
textY = config.y + config.height - 2;
break;
default:
throw 'unreachable';
}
// Compute the X position for the text.
switch (hAlign) {
case 'left':
textX = config.x + 8;
boxWidth = config.width - 14;
break;
case 'center':
textX = config.x + config.width / 2;
boxWidth = config.width;
break;
case 'right':
textX = config.x + config.width - 8;
boxWidth = config.width - 14;
break;
default:
throw 'unreachable';
}
// Clip the cell if the text is taller than the text box height.
if (textHeight > boxHeight) {
gc.beginPath();
gc.rect(config.x, config.y, config.width, config.height - 1);
gc.clip();
}
// Set the gc state.
gc.font = font;
gc.fillStyle = color;
gc.textAlign = hAlign;
gc.textBaseline = 'bottom';
// Terminate call here if we're not eliding or wrapping text
if (elideDirection === 'none' && !wrapText) {
gc.fillText(text, textX, textY);
return;
}
// The current text width in pixels.
let textWidth = gc.measureText(text).width;
// Apply text wrapping if enabled.
if (wrapText && textWidth > boxWidth) {
// Make sure box clipping happens.
gc.beginPath();
gc.rect(config.x, config.y, config.width, config.height - 1);
gc.clip();
// Split column name to words based on
// whitespace preceding a word boundary.
// "Hello world" --> ["Hello ", "world"]
const wordsInColumn = text.split(/\s(?=\b)/);
// Y-coordinate offset for any additional lines
let curY = textY;
let textInCurrentLine = wordsInColumn.shift();
// Single word. Applying text wrap on word by splitting
// it into characters and fitting the maximum number of
// characters possible per line (box width).
if (wordsInColumn.length === 0) {
let curLineTextWidth = gc.measureText(textInCurrentLine).width;
while (curLineTextWidth > boxWidth && textInCurrentLine !== '') {
// Iterating from the end of the string until we find a
// substring (0,i) which has a width less than the box width.
for (let i = textInCurrentLine.length; i > 0; i--) {
const curSubString = textInCurrentLine.substring(0, i);
const curSubStringWidth = gc.measureText(curSubString).width;
if (curSubStringWidth < boxWidth || curSubString.length === 1) {
// Found a substring which has a width less than the current
// box width. Rendering that substring on the current line
// and setting the remainder of the parent string as the next
// string to iterate on for the next line.
const nextLineText = textInCurrentLine.substring(i, textInCurrentLine.length);
textInCurrentLine = nextLineText;
curLineTextWidth = gc.measureText(textInCurrentLine).width;
gc.fillText(curSubString, textX, curY);
curY += textHeight;
// No need to continue iterating after we identified
// an index to break the string on.
break;
}
}
}
}
// Multiple words in column header. Fitting maximum
// number of words possible per line (box width).
else {
while (wordsInColumn.length !== 0) {
// Processing the next word in the queue.
const curWord = wordsInColumn.shift();
// Joining that word with the existing text for
// the current line.
const incrementedText = [textInCurrentLine, curWord].join(' ');
const incrementedTextWidth = gc.measureText(incrementedText).width;
if (incrementedTextWidth > boxWidth) {
// If the newly combined text has a width larger than
// the box width, we render the line before the current
// word was added. We set the current word as the next
// line.
gc.fillText(textInCurrentLine, textX, curY);
curY += textHeight;
textInCurrentLine = curWord;
}
else {
// The combined text hasd a width less than the box width. We
// set the the current line text to be the new combined text.
textInCurrentLine = incrementedText;
}
}
}
gc.fillText(textInCurrentLine, textX, curY);
// Terminating the call here as we don't want
// to apply text eliding when wrapping is active.
return;
}
// Elide text that is too long
let elide = '\u2026';
// Compute elided text
if (elideDirection === 'right') {
while (textWidth > boxWidth && text.length > 1) {
if (text.length > 4 && textWidth >= 2 * boxWidth) {
// If text width is substantially bigger, take half the string
text = text.substring(0, text.length / 2 + 1) + elide;
}
else {
// Otherwise incrementally remove the last character
text = text.substring(0, text.length - 2) + elide;
}
textWidth = gc.measureText(text).width;
}
}
else {
while (textWidth > boxWidth && text.length > 1) {
if (text.length > 4 && textWidth >= 2 * boxWidth) {
// If text width is substantially bigger, take half the string
text = elide + text.substring(text.length / 2);
}
else {
// Otherwise incrementally remove the last character
text = elide + text.substring(2);
}
textWidth = gc.measureText(text).width;
}
}
// Draw the text for the cell.
gc.fillText(text, textX, textY);
}
}
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
/**
* A collection of helper functions relating to merged cell groups
*/
var CellGroup;
(function (CellGroup) {
/**
* Checks if two cell-groups are intersecting
* in the given axis.
* @param group1
* @param group2
* @param axis
*/
function areCellGroupsIntersectingAtAxis(group1, group2, axis) {
if (axis === 'row') {
return ((group1.r1 >= group2.r1 && group1.r1 <= group2.r2) ||
(group1.r2 >= group2.r1 && group1.r2 <= group2.r2) ||
(group2.r1 >= group1.r1 && group2.r1 <= group1.r2) ||
(group2.r2 >= group1.r1 && group2.r2 <= group1.r2));
}
return ((group1.c1 >= group2.c1 && group1.c1 <= group2.c2) ||
(group1.c2 >= group2.c1 && group1.c2 <= group2.c2) ||
(group2.c1 >= group1.c1 && group2.c1 <= group1.c2) ||
(group2.c2 >= group1.c1 && group2.c2 <= group1.c2));
}
CellGroup.areCellGroupsIntersectingAtAxis = areCellGroupsIntersectingAtAxis;
/**
* Checks if cell-groups are intersecting.
* @param group1
* @param group2
*/
function areCellGroupsIntersecting(group1, group2) {
return (((group1.r1 >= group2.r1 && group1.r1 <= group2.r2) ||
(group1.r2 >= group2.r1 && group1.r2 <= group2.r2) ||
(group2.r1 >= group1.r1 && group2.r1 <= group1.r2) ||
(group2.r2 >= group1.r1 && group2.r2 <= group1.r2)) &&
((group1.c1 >= group2.c1 && group1.c1 <= group2.c2) ||
(group1.c2 >= group2.c1 && group1.c2 <= group2.c2) ||
(group2.c1 >= group1.c1 && group2.c1 <= group1.c2) ||
(group2.c2 >= group1.c1 && group2.c2 <= group1.c2)));
}
CellGroup.areCellGroupsIntersecting = areCellGroupsIntersecting;
/**
* Retrieves the index of the cell-group to which
* the cell at the given row, column belongs.
* @param dataModel
* @param rgn
* @param row
* @param column
*/
function getGroupIndex(dataModel, rgn, row, column) {
const numGroups = dataModel.groupCount(rgn);
for (let i = 0; i < numGroups; i++) {
const group = dataModel.group(rgn, i);
if (row >= group.r1 &&
row <= group.r2 &&
column >= group.c1 &&
column <= group.c2) {
return i;
}
}
return -1;
}
CellGroup.getGroupIndex = getGroupIndex;
/**
* Returns a cell-group for the given row/index coordinates.
* @param dataModel
* @param rgn
* @param row
* @param column
*/
function getGroup(dataModel, rgn, row, column) {
const groupIndex = getGroupIndex(dataModel, rgn, row, column);
if (groupIndex === -1) {
return null;
}
return dataModel.group(rgn, groupIndex);
}
CellGroup.getGroup = getGroup;
/**
* Returns all cell groups which belong to
* a given cell cell region.
* @param dataModel
* @param rgn
*/
function getCellGroupsAtRegion(dataModel, rgn) {
let groupsAtRegion = [];
const numGroups = dataModel.groupCount(rgn);
for (let i = 0; i < numGroups; i++) {
const group = dataModel.group(rgn, i);
groupsAtRegion.push(group);
}
return groupsAtRegion;
}
CellGroup.getCellGroupsAtRegion = getCellGroupsAtRegion;
/**
* Calculates and returns a merged cell-group from
* two cell-group objects.
* @param groups
*/
function joinCellGroups(groups) {
let startRow = Number.MAX_VALUE;
let endRow = Number.MIN_VALUE;
let startColumn = Number.MAX_VALUE;
let endColumn = Number.MIN_VALUE;
for (const group of groups) {
startRow = Math.min(startRow, group.r1);
endRow = Math.max(endRow, group.r2);
startColumn = Math.min(startColumn, group.c1);
endColumn = Math.max(endColumn, group.c2);
}
return { r1: startRow, r2: endRow, c1: startColumn, c2: endColumn };
}
CellGroup.joinCellGroups = joinCellGroups;
/**
* Merges a cell group with other cells groups in the
* same region if they intersect.
* @param dataModel the data model of the grid.
* @param group the target cell group.
* @param region the region of the cell group.
* @returns a new cell group after merging has happened.
*/
function joinCellGroupWithMergedCellGroups(dataModel, group, region) {
let joinedGroup = { ...group };
const mergedCellGroups = getCellGroupsAtRegion(dataModel, region);
for (let g = 0; g < mergedCellGroups.length; g++) {
const mergedGroup = mergedCellGroups[g];
if (areCellGroupsIntersecting(joinedGroup, mergedGroup)) {
joinedGroup = joinCellGroups([joinedGroup, mergedGroup]);
}
}
return joinedGroup;
}
CellGroup.joinCellGroupWithMergedCellGroups = joinCellGroupWithMergedCellGroups;
/**
* Retrieves a list of cell groups intersecting at
* a given row.
* @param dataModel data model of the grid.
* @param rgn the cell region.
* @param row the target row to look for intersections at.
* @returns all cell groups intersecting with the row.
*/
function getCellGroupsAtRow(dataModel, rgn, row) {
let groupsAtRow = [];
const numGroups = dataModel.groupCount(rgn);
for (let i = 0; i < numGroups; i++) {
const group = dataModel.group(rgn, i);
if (row >= group.r1 && row <= group.r2) {
groupsAtRow.push(group);
}
}
return groupsAtRow;
}
CellGroup.getCellGroupsAtRow = getCellGroupsAtRow;
/**
* Retrieves a list of cell groups intersecting at
* a given column.
* @param dataModel data model of the grid.
* @param rgn the cell region.
* @param column the target column to look for intersections at.
* @returns all cell groups intersecting with the column.
*/
function getCellGroupsAtColumn(dataModel, rgn, column) {
let groupsAtColumn = [];
const numGroups = dataModel.groupCount(rgn);
for (let i = 0; i < numGroups; i++) {
const group = dataModel.group(rgn, i);
if (column >= group.c1 && column <= group.c2) {
groupsAtColumn.push(group);
}
}
return groupsAtColumn;
}
CellGroup.getCellGroupsAtColumn = getCellGroupsAtColumn;
/**
* Merges a target cell group with any cell groups
* it intersects with at a given row or column.
* @param dataModel data model of the grid.
* @param regions list of cell regions.
* @param axis row or column.
* @param group the target cell group.
* @returns a new merged cell group.
*/
function joinCellGroupsIntersectingAtAxis(dataModel, regions, axis, group) {
let groupsAtAxis = [];
if (axis === 'row') {
for (const region of regions) {
for (let r = group.r1; r <= group.r2; r++) {
groupsAtAxis = groupsAtAxis.concat(CellGroup.getCellGroupsAtRow(dataModel, region, r));
}
}
}
else {
for (const region of regions) {
for (let c = group.c1; c <= group.c2; c++) {
groupsAtAxis = groupsAtAxis.concat(CellGroup.getCellGroupsAtColumn(dataModel, region, c));
}
}
}
let mergedGroupAtAxis = CellGroup.joinCellGroups(groupsAtAxis);
if (groupsAtAxis.length > 0) {
let mergedCellGroups = [];
for (const region of regions) {
mergedCellGroups = mergedCellGroups.concat(CellGroup.getCellGroupsAtRegion(dataModel, region));
}
for (let g = 0; g < mergedCellGroups.length; g++) {
const group = mergedCellGroups[g];
if (CellGroup.areCellGroupsIntersectingAtAxis(mergedGroupAtAxis, group, axis)) {
mergedGroupAtAxis = CellGroup.joinCellGroups([
group,
mergedGroupAtAxis
]);
mergedCellGroups.splice(g, 1);
g = 0;
}
}
}
return mergedGroupAtAxis;
}
CellGroup.joinCellGroupsIntersectingAtAxis = joinCellGroupsIntersectingAtAxis;
})(CellGroup || (CellGroup = {}));
/**
* A basic implementation of a data grid mouse handler.
*
* #### Notes
* This class may be subclassed and customized as needed.
*/
class BasicMouseHandler {
constructor() {
this._disposed = false;
this._pressData = null;
}
/**
* Dispose of the resources held by the mouse handler.
*/
dispose() {
// Bail early if the handler is already disposed.
if (this._disposed) {
return;
}
// Release any held resources.
this.release();
// Mark the handler as disposed.
this._disposed = true;
}
/**
* Whether the mouse handler is disposed.
*/
get isDisposed() {
return this._disposed;
}
/**
* Release the resources held by the handler.
*/
release() {
// Bail early if the is no press data.
if (!this._pressData) {
return;
}
// Clear the autoselect timeout.
if (this._pressData.type === 'select') {
this._pressData.timeout = -1;
}
// Clear the press data.
this._pressData.override.dispose();
this._pressData = null;
}
/**
* Handle the mouse hover event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The mouse hover event of interest.
*/
onMouseHover(grid, event) {
// Hit test the grid.
let hit = grid.hitTest(event.clientX, event.clientY);
// Get the resize handle for the hit test.
let handle = Private$5.resizeHandleForHitTest(hit);
// Fetch the cursor for the handle.
let cursor = this.cursorForHandle(handle);
// Hyperlink logic.
const config = Private$5.createCellConfigObject(grid, hit);
if (config) {
// Retrieve renderer for hovered cell.
const renderer = grid.cellRenderers.get(config);
if (renderer instanceof HyperlinkRenderer) {
cursor = this.cursorForHandle('hyperlink');
}
}
// Update the viewport cursor based on the part.
grid.viewport.node.style.cursor = cursor;
// TODO support user-defined hover items
}
/**
* Handle the mouse leave event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The mouse hover event of interest.
*/
onMouseLeave(grid, event) {
// TODO support user-defined hover popups.
// Clear the viewport cursor.
grid.viewport.node.style.cursor = '';
}
/**
* Handle the mouse down event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The mouse down event of interest.
*/
onMouseDown(grid, event) {
// Unpack the event.
let { clientX, clientY } = event;
// Hit test the grid.
let hit = grid.hitTest(clientX, clientY);
// Unpack the hit test.
const { region, row, column } = hit;
// Bail if the hit test is on an uninteresting region.
if (region === 'void') {
return;
}
// Fetch the modifier flags.
let shift = event.shiftKey;
let accel = _lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event);
// Hyperlink logic.
if (grid) {
// Create cell config object.
const config = Private$5.createCellConfigObject(grid, hit);
// Retrieve cell renderer.
let renderer = grid.cellRenderers.get(config);
// Only process hyperlink renderers.
if (renderer instanceof HyperlinkRenderer) {
// Use the url param if it exists.
let url = CellRenderer.resolveOption(renderer.url, config);
// Otherwise assume cell value is the URL.
if (!url) {
const format = TextRenderer.formatGeneric();
url = format(config);
}
// Open the hyperlink only if user hit Ctrl+Click.
if (accel) {
window.open(url);
// Reset cursor default after clicking
const cursor = this.cursorForHandle('none');
grid.viewport.node.style.cursor = cursor;
// Not applying selections if navigating away.
return;
}
}
}
// If the hit test is the body region, the only option is select.
if (region === 'body') {
// Fetch the selection model.
let model = grid.selectionModel;
// Bail early if there is no selection model.
if (!model) {
return;
}
// Override the document cursor.
let override = _lumino_dragdrop__WEBPACK_IMPORTED_MODULE_2__.Drag.overrideCursor('default');
// Set up the press data.
this._pressData = {
type: 'select',
region,
row,
column,
override,
localX: -1,
localY: -1,
timeout: -1
};
// Set up the selection variables.
let r1;
let c1;
let r2;
let c2;
let cursorRow;
let cursorColumn;
let clear;
// Accel == new selection, keep old selections.
if (accel) {
r1 = row;
r2 = row;
c1 = column;
c2 = column;
cursorRow = row;
cursorColumn = column;
clear = 'none';
}
else if (shift) {
r1 = model.cursorRow;
r2 = row;
c1 = model.cursorColumn;
c2 = column;
cursorRow = model.cursorRow;
cursorColumn = model.cursorColumn;
clear = 'current';
}
else {
r1 = row;
r2 = row;
c1 = column;
c2 = column;
cursorRow = row;
cursorColumn = column;
clear = 'all';
}
// Make the selection.
model.select({ r1, c1, r2, c2, cursorRow, cursorColumn, clear });
// Done.
return;
}
// Otherwise, the hit test is on a header region.
// Convert the hit test into a part.
let handle = Private$5.resizeHandleForHitTest(hit);
// Fetch the cursor for the handle.
let cursor = this.cursorForHandle(handle);
// Handle horizontal resize.
if (handle === 'left' || handle === 'right') {
// Set up the resize data type.
const type = 'column-resize';
// Determine the column region.
let rgn = region === 'column-header' ? 'body' : 'row-header';
// Determine the section index.
let index = handle === 'left' ? column - 1 : column;
// Fetch the section size.
let size = grid.columnSize(rgn, index);
// Override the document cursor.
let override = _lumino_dragdrop__WEBPACK_IMPORTED_MODULE_2__.Drag.overrideCursor(cursor);
// Create the temporary press data.
this._pressData = { type, region: rgn, index, size, clientX, override };
// Done.
return;
}
// Handle vertical resize
if (handle === 'top' || handle === 'bottom') {
// Set up the resize data type.
const type = 'row-resize';
// Determine the row region.
let rgn = region === 'row-header' ? 'body' : 'column-header';
// Determine the section index.
let index = handle === 'top' ? row - 1 : row;
// Fetch the section size.
let size = grid.rowSize(rgn, index);
// Override the document cursor.
let override = _lumino_dragdrop__WEBPACK_IMPORTED_MODULE_2__.Drag.overrideCursor(cursor);
// Create the temporary press data.
this._pressData = { type, region: rgn, index, size, clientY, override };
// Done.
return;
}
// Otherwise, the only option is select.
// Fetch the selection model.
let model = grid.selectionModel;
// Bail if there is no selection model.
if (!model) {
return;
}
// Override the document cursor.
let override = _lumino_dragdrop__WEBPACK_IMPORTED_MODULE_2__.Drag.overrideCursor('default');
// Set up the press data.
this._pressData = {
type: 'select',
region,
row,
column,
override,
localX: -1,
localY: -1,
timeout: -1
};
// Set up the selection variables.
let r1;
let c1;
let r2;
let c2;
let cursorRow;
let cursorColumn;
let clear;
// Compute the selection based on the pressed region.
if (region === 'corner-header') {
r1 = 0;
r2 = Infinity;
c1 = 0;
c2 = Infinity;
cursorRow = accel ? 0 : shift ? model.cursorRow : 0;
cursorColumn = accel ? 0 : shift ? model.cursorColumn : 0;
clear = accel ? 'none' : shift ? 'current' : 'all';
}
else if (region === 'row-header') {
r1 = accel ? row : shift ? model.cursorRow : row;
r2 = row;
const selectionGroup = { r1: r1, c1: 0, r2: r2, c2: 0 };
const joinedGroup = CellGroup.joinCellGroupsIntersectingAtAxis(grid.dataModel, ['row-header', 'body'], 'row', selectionGroup);
// Check if there are any merges
if (joinedGroup.r1 != Number.MAX_VALUE) {
r1 = joinedGroup.r1;
r2 = joinedGroup.r2;
}
c1 = 0;
c2 = Infinity;
cursorRow = accel ? row : shift ? model.cursorRow : row;
cursorColumn = accel ? 0 : shift ? model.cursorColumn : 0;
clear = accel ? 'none' : shift ? 'current' : 'all';
}
else if (region === 'column-header') {
r1 = 0;
r2 = Infinity;
c1 = accel ? column : shift ? model.cursorColumn : column;
c2 = column;
const selectionGroup = { r1: 0, c1: c1, r2: 0, c2: c2 };
const joinedGroup = CellGroup.joinCellGroupsIntersectingAtAxis(grid.dataModel, ['column-header', 'body'], 'column', selectionGroup);
// Check if there are any merges
if (joinedGroup.c1 != Number.MAX_VALUE) {
c1 = joinedGroup.c1;
c2 = joinedGroup.c2;
}
cursorRow = accel ? 0 : shift ? model.cursorRow : 0;
cursorColumn = accel ? column : shift ? model.cursorColumn : column;
clear = accel ? 'none' : shift ? 'current' : 'all';
}
else {
r1 = accel ? row : shift ? model.cursorRow : row;
r2 = row;
c1 = accel ? column : shift ? model.cursorColumn : column;
c2 = column;
cursorRow = accel ? row : shift ? model.cursorRow : row;
cursorColumn = accel ? column : shift ? model.cursorColumn : column;
clear = accel ? 'none' : shift ? 'current' : 'all';
}
// Make the selection.
model.select({ r1, c1, r2, c2, cursorRow, cursorColumn, clear });
}
/**
* Handle the mouse move event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The mouse move event of interest.
*/
onMouseMove(grid, event) {
// Fetch the press data.
const data = this._pressData;
// Bail early if there is no press data.
if (!data) {
return;
}
// Handle a row resize.
if (data.type === 'row-resize') {
let dy = event.clientY - data.clientY;
grid.resizeRow(data.region, data.index, data.size + dy);
return;
}
// Handle a column resize.
if (data.type === 'column-resize') {
let dx = event.clientX - data.clientX;
grid.resizeColumn(data.region, data.index, data.size + dx);
return;
}
// Otherwise, it's a select.
// Mouse moves during a corner header press are a no-op.
if (data.region === 'corner-header') {
return;
}
// Fetch the selection model.
let model = grid.selectionModel;
// Bail early if the selection model was removed.
if (!model) {
return;
}
// Map to local coordinates.
let { lx, ly } = grid.mapToLocal(event.clientX, event.clientY);
// Update the local mouse coordinates in the press data.
data.localX = lx;
data.localY = ly;
// Fetch the grid geometry.
let hw = grid.headerWidth;
let hh = grid.headerHeight;
let vpw = grid.viewportWidth;
let vph = grid.viewportHeight;
let sx = grid.scrollX;
let sy = grid.scrollY;
let msx = grid.maxScrollY;
let msy = grid.maxScrollY;
// Fetch the selection mode.
let mode = model.selectionMode;
// Set up the timeout variable.
let timeout = -1;
// Compute the timemout based on hit region and mouse position.
if (data.region === 'row-header' || mode === 'row') {
if (ly < hh && sy > 0) {
timeout = Private$5.computeTimeout(hh - ly);
}
else if (ly >= vph && sy < msy) {
timeout = Private$5.computeTimeout(ly - vph);
}
}
else if (data.region === 'column-header' || mode === 'column') {
if (lx < hw && sx > 0) {
timeout = Private$5.computeTimeout(hw - lx);
}
else if (lx >= vpw && sx < msx) {
timeout = Private$5.computeTimeout(lx - vpw);
}
}
else {
if (lx < hw && sx > 0) {
timeout = Private$5.computeTimeout(hw - lx);
}
else if (lx >= vpw && sx < msx) {
timeout = Private$5.computeTimeout(lx - vpw);
}
else if (ly < hh && sy > 0) {
timeout = Private$5.computeTimeout(hh - ly);
}
else if (ly >= vph && sy < msy) {
timeout = Private$5.computeTimeout(ly - vph);
}
}
// Update or initiate the autoselect if needed.
if (timeout >= 0) {
if (data.timeout < 0) {
data.timeout = timeout;
setTimeout(() => {
Private$5.autoselect(grid, data);
}, timeout);
}
else {
data.timeout = timeout;
}
return;
}
// Otherwise, clear the autoselect timeout.
data.timeout = -1;
// Map the position to virtual coordinates.
let { vx, vy } = grid.mapToVirtual(event.clientX, event.clientY);
// Clamp the coordinates to the limits.
vx = Math.max(0, Math.min(vx, grid.bodyWidth - 1));
vy = Math.max(0, Math.min(vy, grid.bodyHeight - 1));
// Set up the selection variables.
let r1;
let c1;
let r2;
let c2;
let cursorRow = model.cursorRow;
let cursorColumn = model.cursorColumn;
let clear = 'current';
// Compute the selection based pressed region.
if (data.region === 'row-header' || mode === 'row') {
r1 = data.row;
r2 = grid.rowAt('body', vy);
const selectionGroup = { r1: r1, c1: 0, r2: r2, c2: 0 };
const joinedGroup = CellGroup.joinCellGroupsIntersectingAtAxis(grid.dataModel, ['row-header', 'body'], 'row', selectionGroup);
// Check if there are any merges
if (joinedGroup.r1 != Number.MAX_VALUE) {
r1 = Math.min(r1, joinedGroup.r1);
r2 = Math.max(r2, joinedGroup.r2);
}
c1 = 0;
c2 = Infinity;
}
else if (data.region === 'column-header' || mode === 'column') {
r1 = 0;
r2 = Infinity;
c1 = data.column;
c2 = grid.columnAt('body', vx);
const selectionGroup = { r1: 0, c1: c1, r2: 0, c2: c2 };
const joinedGroup = CellGroup.joinCellGroupsIntersectingAtAxis(grid.dataModel, ['column-header', 'body'], 'column', selectionGroup);
// Check if there are any merges
if (joinedGroup.c1 != Number.MAX_VALUE) {
c1 = joinedGroup.c1;
c2 = joinedGroup.c2;
}
}
else {
r1 = cursorRow;
r2 = grid.rowAt('body', vy);
c1 = cursorColumn;
c2 = grid.columnAt('body', vx);
}
// Make the selection.
model.select({ r1, c1, r2, c2, cursorRow, cursorColumn, clear });
}
/**
* Handle the mouse up event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The mouse up event of interest.
*/
onMouseUp(grid, event) {
this.release();
}
/**
* Handle the mouse double click event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The mouse up event of interest.
*/
onMouseDoubleClick(grid, event) {
var _a, _b, _c;
if (!grid.dataModel) {
this.release();
return;
}
// Unpack the event.
let { clientX, clientY } = event;
// Hit test the grid.
let hit = grid.hitTest(clientX, clientY);
// Unpack the hit test.
let { region, row, column } = hit;
if (region === 'void') {
this.release();
return;
}
if (region === 'column-header' || region === 'corner-header') {
// Convert the hit test into a part.
const handle = Private$5.resizeHandleForHitTest(hit);
if (handle === 'left' || handle === 'right') {
let colIndex = handle === 'left' ? column - 1 : column;
let colRegion = region === 'column-header' ? 'body' : 'row-header';
if (colIndex < 0) {
if (region === 'column-header') {
// If the column is -1, it means we are in the corner header
colIndex = grid.dataModel.columnCount('row-header') - 1;
colRegion = 'row-header';
}
else {
// If we are on the left edge of the row header, do nothing
return;
}
}
const cs = (_a = grid.selectionModel) === null || _a === void 0 ? void 0 : _a.currentSelection();
const cv = grid.currentViewport;
const rowCount = (_c = (_b = grid.selectionModel) === null || _b === void 0 ? void 0 : _b.dataModel.rowCount('body')) !== null && _c !== void 0 ? _c : 0;
if (colRegion == 'body' &&
cs != null &&
cv != null &&
cs.r1 == 0 &&
cs.r2 == rowCount - 1) {
// One or more columns are selected
let c1 = Math.max(Math.min(cs.c1, cs.c2), cv.firstColumn);
let c2 = Math.min(Math.max(cs.c1, cs.c2), cv.lastColumn);
if (c1 <= colIndex && colIndex <= c2) {
// When we double-click one of the selected column headers, resize all visible selected columns.
for (let ci = c1; ci <= c2; ci++) {
grid.resizeColumn(colRegion, ci, null);
}
}
else {
// When we double-click the column header outside the selection, resize only the clicked column.
grid.resizeColumn(colRegion, colIndex, null);
}
}
else {
// When no columns are selected, resize only the clicked column.
grid.resizeColumn(colRegion, colIndex, null);
}
}
}
if (region === 'body') {
if (grid.editable) {
const cell = {
grid: grid,
row: row,
column: column
};
grid.editorController.edit(cell);
}
}
this.release();
}
/**
* Handle the context menu event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The context menu event of interest.
*/
onContextMenu(grid, event) {
// TODO support user-defined context menus
}
/**
* Handle the wheel event for the data grid.
*
* @param grid - The data grid of interest.
*
* @param event - The wheel event of interest.
*/
onWheel(grid, event) {
// Bail if a mouse press is in progress.
if (this._pressData) {
return;
}
// Extract the delta X and Y movement.
let dx = event.deltaX;
let dy = event.deltaY;
// Convert the delta values to pixel values.
switch (event.deltaMode) {
case 0: // DOM_DELTA_PIXEL
break;
case 1: {
// DOM_DELTA_LINE
let ds = grid.defaultSizes;
dx *= ds.columnWidth;
dy *= ds.rowHeight;
break;
}
case 2: // DOM_DELTA_PAGE
dx *= grid.pageWidth;
dy *= grid.pageHeight;
break;
default:
throw 'unreachable';
}
// Only scroll and stop the event propagation if needed.
if (
// Scrolling left and not reached min already
(dx < 0 && grid.scrollX !== 0) ||
// Scrolling right and not reached max already
(dx > 0 && grid.scrollX !== grid.maxScrollX) ||
// Scrolling top and not reached min already
(dy < 0 && grid.scrollY !== 0) ||
// Scrolling down and not reached max already
(dy > 0 && grid.scrollY !== grid.maxScrollY)) {
event.preventDefault();
event.stopPropagation();
// Scroll by the desired amount.
grid.scrollBy(dx, dy);
}
}
/**
* Convert a resize handle into a cursor.
*/
cursorForHandle(handle) {
return Private$5.cursorMap[handle];
}
/**
* Get the current pressData
*/
get pressData() {
return this._pressData;
}
}
/**
* The namespace for the module implementation details.
*/
var Private$5;
(function (Private) {
/**
* Creates a CellConfig object from a hit region.
*/
function createCellConfigObject(grid, hit) {
const { region, row, column } = hit;
// Terminate call if region is void.
if (region === 'void') {
return undefined;
}
// Augment hit region params with value and metadata.
const value = grid.dataModel.data(region, row, column);
const metadata = grid.dataModel.metadata(region, row, column);
// Create cell config object to retrieve cell renderer.
const config = {
...hit,
value: value,
metadata: metadata
};
return config;
}
Private.createCellConfigObject = createCellConfigObject;
/**
* Get the resize handle for a grid hit test.
*/
function resizeHandleForHitTest(hit) {
// Fetch the row and column.
let r = hit.row;
let c = hit.column;
// Fetch the leading and trailing sizes.
let lw = hit.x;
let lh = hit.y;
let tw = hit.width - hit.x;
let th = hit.height - hit.y;
// Set up the result variable.
let result;
// Dispatch based on hit test region.
switch (hit.region) {
case 'corner-header':
if (c > 0 && lw <= 5) {
result = 'left';
}
else if (tw <= 6) {
result = 'right';
}
else if (r > 0 && lh <= 5) {
result = 'top';
}
else if (th <= 6) {
result = 'bottom';
}
else {
result = 'none';
}
break;
case 'column-header':
if (c > 0 && lw <= 5) {
result = 'left';
}
else if (tw <= 6) {
result = 'right';
}
else if (r > 0 && lh <= 5) {
result = 'top';
}
else if (th <= 6) {
result = 'bottom';
}
else {
result = 'none';
}
break;
case 'row-header':
if (c > 0 && lw <= 5) {
result = 'left';
}
else if (tw <= 6) {
result = 'right';
}
else if (r > 0 && lh <= 5) {
result = 'top';
}
else if (th <= 6) {
result = 'bottom';
}
else {
result = 'none';
}
break;
case 'body':
result = 'none';
break;
case 'void':
result = 'none';
break;
default:
throw 'unreachable';
}
// Return the result.
return result;
}
Private.resizeHandleForHitTest = resizeHandleForHitTest;
/**
* A timer callback for the autoselect loop.
*
* @param grid - The datagrid of interest.
*
* @param data - The select data of interest.
*/
function autoselect(grid, data) {
// Bail early if the timeout has been reset.
if (data.timeout < 0) {
return;
}
// Fetch the selection model.
let model = grid.selectionModel;
// Bail early if the selection model has been removed.
if (!model) {
return;
}
// Fetch the current selection.
let cs = model.currentSelection();
// Bail early if there is no current selection.
if (!cs) {
return;
}
// Fetch local X and Y coordinates of the mouse.
let lx = data.localX;
let ly = data.localY;
// Set up the selection variables.
let r1 = cs.r1;
let c1 = cs.c1;
let r2 = cs.r2;
let c2 = cs.c2;
let cursorRow = model.cursorRow;
let cursorColumn = model.cursorColumn;
let clear = 'current';
// Fetch the grid geometry.
let hw = grid.headerWidth;
let hh = grid.headerHeight;
let vpw = grid.viewportWidth;
let vph = grid.viewportHeight;
// Fetch the selection mode.
let mode = model.selectionMode;
// Update the selection based on the hit region.
if (data.region === 'row-header' || mode === 'row') {
r2 += ly <= hh ? -1 : ly >= vph ? 1 : 0;
}
else if (data.region === 'column-header' || mode === 'column') {
c2 += lx <= hw ? -1 : lx >= vpw ? 1 : 0;
}
else {
r2 += ly <= hh ? -1 : ly >= vph ? 1 : 0;
c2 += lx <= hw ? -1 : lx >= vpw ? 1 : 0;
}
// Update the current selection.
model.select({ r1, c1, r2, c2, cursorRow, cursorColumn, clear });
// Re-fetch the current selection.
cs = model.currentSelection();
// Bail if there is no selection.
if (!cs) {
return;
}
// Scroll the grid based on the hit region.
if (data.region === 'row-header' || mode === 'row') {
grid.scrollToRow(cs.r2);
}
else if (data.region === 'column-header' || mode == 'column') {
grid.scrollToColumn(cs.c2);
}
else if (mode === 'cell') {
grid.scrollToCell(cs.r2, cs.c2);
}
// Schedule the next call with the current timeout.
setTimeout(() => {
autoselect(grid, data);
}, data.timeout);
}
Private.autoselect = autoselect;
/**
* Compute the scroll timeout for the given delta distance.
*
* @param delta - The delta pixels from the origin.
*
* @returns The scaled timeout in milliseconds.
*/
function computeTimeout(delta) {
return 5 + 120 * (1 - Math.min(128, Math.abs(delta)) / 128);
}
Private.computeTimeout = computeTimeout;
/**
* A mapping of resize handle to cursor.
*/
Private.cursorMap = {
top: 'ns-resize',
left: 'ew-resize',
right: 'ew-resize',
bottom: 'ns-resize',
hyperlink: 'pointer',
none: 'default'
};
})(Private$5 || (Private$5 = {}));
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A base class for creating data grid selection models.
*
* #### Notes
* If the predefined selection models are insufficient for a particular
* use case, a custom model can be defined which derives from this class.
*/
class SelectionModel {
/**
* Construct a new selection model.
*
* @param options - The options for initializing the model.
*/
constructor(options) {
this._changed = new _lumino_signaling__WEBPACK_IMPORTED_MODULE_4__.Signal(this);
this._selectionMode = 'cell';
this.dataModel = options.dataModel;
this._selectionMode = options.selectionMode || 'cell';
this.dataModel.changed.connect(this.onDataModelChanged, this);
}
/**
* A signal emitted when the selection model has changed.
*/
get changed() {
return this._changed;
}
/**
* Get the selection mode for the model.
*/
get selectionMode() {
return this._selectionMode;
}
/**
* Set the selection mode for the model.
*
* #### Notes
* This will clear the selection model.
*/
set selectionMode(value) {
// Bail early if the mode does not change.
if (this._selectionMode === value) {
return;
}
// Update the internal mode.
this._selectionMode = value;
// Clear the current selections.
this.clear();
}
/**
* Test whether any selection intersects a row.
*
* @param index - The row index of interest.
*
* @returns Whether any selection intersects the row.
*
* #### Notes
* This method may be reimplemented in a subclass.
*/
isRowSelected(index) {
return (0,_lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.some)(this.selections(), s => Private$4.containsRow(s, index));
}
/**
* Test whether any selection intersects a column.
*
* @param index - The column index of interest.
*
* @returns Whether any selection intersects the column.
*
* #### Notes
* This method may be reimplemented in a subclass.
*/
isColumnSelected(index) {
return (0,_lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.some)(this.selections(), s => Private$4.containsColumn(s, index));
}
/**
* Test whether any selection intersects a cell.
*
* @param row - The row index of interest.
*
* @param column - The column index of interest.
*
* @returns Whether any selection intersects the cell.
*
* #### Notes
* This method may be reimplemented in a subclass.
*/
isCellSelected(row, column) {
return (0,_lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.some)(this.selections(), s => Private$4.containsCell(s, row, column));
}
/**
* A signal handler for the data model `changed` signal.
*
* @param args - The arguments for the signal.
*
* #### Notes
* Selection model implementations should update their selections
* in a manner that is relevant for the changes to the data model.
*
* The default implementation of this method is a no-op.
*/
onDataModelChanged(sender, args) {
// pass
}
/**
* Emit the `changed` signal for the selection model.
*
* #### Notes
* Subclasses should call this method whenever the selection model
* has changed so that attached data grids can update themselves.
*/
emitChanged() {
this._changed.emit(undefined);
}
}
/**
* The namespace for the module implementation details.
*/
var Private$4;
(function (Private) {
/**
* Test whether a selection contains a given row.
*/
function containsRow(selection, row) {
let { r1, r2 } = selection;
return (row >= r1 && row <= r2) || (row >= r2 && row <= r1);
}
Private.containsRow = containsRow;
/**
* Test whether a selection contains a given column.
*/
function containsColumn(selection, column) {
let { c1, c2 } = selection;
return (column >= c1 && column <= c2) || (column >= c2 && column <= c1);
}
Private.containsColumn = containsColumn;
/**
* Test whether a selection contains a given cell.
*/
function containsCell(selection, row, column) {
return containsRow(selection, row) && containsColumn(selection, column);
}
Private.containsCell = containsCell;
})(Private$4 || (Private$4 = {}));
/**
* A basic selection model implementation.
*
* #### Notes
* This selection model is sufficient for most use cases where
* structural knowledge of the data source is *not* required.
*/
class BasicSelectionModel extends SelectionModel {
constructor() {
super(...arguments);
this._cursorRow = -1;
this._cursorColumn = -1;
this._cursorRectIndex = -1;
this._selections = [];
}
/**
* Whether the selection model is empty.
*/
get isEmpty() {
return this._selections.length === 0;
}
/**
* The row index of the cursor.
*/
get cursorRow() {
return this._cursorRow;
}
/**
* The column index of the cursor.
*/
get cursorColumn() {
return this._cursorColumn;
}
/**
* Move cursor down/up/left/right while making sure it remains
* within the bounds of selected rectangles
*
* @param direction - The direction of the movement.
*/
moveCursorWithinSelections(direction) {
// Bail early if there are no selections or no existing cursor
if (this.isEmpty || this.cursorRow === -1 || this._cursorColumn === -1) {
return;
}
// Bail early if only single cell is selected
const firstSelection = this._selections[0];
if (this._selections.length === 1 &&
firstSelection.r1 === firstSelection.r2 &&
firstSelection.c1 === firstSelection.c2) {
return;
}
// start from last selection rectangle
if (this._cursorRectIndex === -1) {
this._cursorRectIndex = this._selections.length - 1;
}
let cursorRect = this._selections[this._cursorRectIndex];
const dr = direction === 'down' ? 1 : direction === 'up' ? -1 : 0;
const dc = direction === 'right' ? 1 : direction === 'left' ? -1 : 0;
let newRow = this._cursorRow + dr;
let newColumn = this._cursorColumn + dc;
const r1 = Math.min(cursorRect.r1, cursorRect.r2);
const r2 = Math.max(cursorRect.r1, cursorRect.r2);
const c1 = Math.min(cursorRect.c1, cursorRect.c2);
const c2 = Math.max(cursorRect.c1, cursorRect.c2);
const moveToNextRect = () => {
this._cursorRectIndex =
(this._cursorRectIndex + 1) % this._selections.length;
cursorRect = this._selections[this._cursorRectIndex];
newRow = Math.min(cursorRect.r1, cursorRect.r2);
newColumn = Math.min(cursorRect.c1, cursorRect.c2);
};
const moveToPreviousRect = () => {
this._cursorRectIndex =
this._cursorRectIndex === 0
? this._selections.length - 1
: this._cursorRectIndex - 1;
cursorRect = this._selections[this._cursorRectIndex];
newRow = Math.max(cursorRect.r1, cursorRect.r2);
newColumn = Math.max(cursorRect.c1, cursorRect.c2);
};
if (newRow > r2) {
newRow = r1;
newColumn += 1;
if (newColumn > c2) {
moveToNextRect();
}
}
else if (newRow < r1) {
newRow = r2;
newColumn -= 1;
if (newColumn < c1) {
moveToPreviousRect();
}
}
else if (newColumn > c2) {
newColumn = c1;
newRow += 1;
if (newRow > r2) {
moveToNextRect();
}
}
else if (newColumn < c1) {
newColumn = c2;
newRow -= 1;
if (newRow < r1) {
moveToPreviousRect();
}
}
this._cursorRow = newRow;
this._cursorColumn = newColumn;
// Emit the changed signal.
this.emitChanged();
}
/**
* Get the current selection in the selection model.
*
* @returns The current selection or `null`.
*
* #### Notes
* This is the selection which holds the cursor.
*/
currentSelection() {
return this._selections[this._selections.length - 1] || null;
}
/**
* Get an iterator of the selections in the model.
*
* @returns A new iterator of the current selections.
*
* #### Notes
* The data grid will render the selections in order.
*/
*selections() {
yield* this._selections;
}
/**
* Select the specified cells.
*
* @param args - The arguments for the selection.
*/
select(args) {
// Fetch the current row and column counts;
let rowCount = this.dataModel.rowCount('body');
let columnCount = this.dataModel.columnCount('body');
// Bail early if there is no content.
if (rowCount <= 0 || columnCount <= 0) {
return;
}
// Unpack the arguments.
let { r1, c1, r2, c2, cursorRow, cursorColumn, clear } = args;
// Clear the necessary selections.
if (clear === 'all') {
this._selections.length = 0;
}
else if (clear === 'current') {
this._selections.pop();
}
// Clamp to the data model bounds.
r1 = Math.max(0, Math.min(r1, rowCount - 1));
r2 = Math.max(0, Math.min(r2, rowCount - 1));
c1 = Math.max(0, Math.min(c1, columnCount - 1));
c2 = Math.max(0, Math.min(c2, columnCount - 1));
// Indicate if a row/column has already been selected.
let alreadySelected = false;
// Handle the selection mode.
if (this.selectionMode === 'row') {
c1 = 0;
c2 = columnCount - 1;
alreadySelected =
this._selections.filter(selection => selection.r1 === r1).length !== 0;
// Remove from selections if already selected.
this._selections = alreadySelected
? this._selections.filter(selection => selection.r1 !== r1)
: this._selections;
}
else if (this.selectionMode === 'column') {
r1 = 0;
r2 = rowCount - 1;
alreadySelected =
this._selections.filter(selection => selection.c1 === c1).length !== 0;
// Remove from selections if already selected.
this._selections = alreadySelected
? this._selections.filter(selection => selection.c1 !== c1)
: this._selections;
}
// Alias the cursor row and column.
let cr = cursorRow;
let cc = cursorColumn;
// Compute the new cursor location.
if (cr < 0 || (cr < r1 && cr < r2) || (cr > r1 && cr > r2)) {
cr = r1;
}
if (cc < 0 || (cc < c1 && cc < c2) || (cc > c1 && cc > c2)) {
cc = c1;
}
// Update the cursor.
this._cursorRow = cr;
this._cursorColumn = cc;
this._cursorRectIndex = this._selections.length;
// Add the new selection if it wasn't already selected.
if (!alreadySelected) {
this._selections.push({ r1, c1, r2, c2 });
}
// Emit the changed signal.
this.emitChanged();
}
/**
* Clear all selections in the selection model.
*/
clear() {
// Bail early if there are no selections.
if (this._selections.length === 0) {
return;
}
// Reset the internal state.
this._cursorRow = -1;
this._cursorColumn = -1;
this._cursorRectIndex = -1;
this._selections.length = 0;
// Emit the changed signal.
this.emitChanged();
}
/**
* A signal handler for the data model `changed` signal.
*
* @param args - The arguments for the signal.
*/
onDataModelChanged(sender, args) {
// Bail early if the model has no current selections.
if (this._selections.length === 0) {
return;
}
// Bail early if the cells have changed in place.
if (args.type === 'cells-changed') {
return;
}
// Bail early if there is no change to the row or column count.
if (args.type === 'rows-moved' || args.type === 'columns-moved') {
return;
}
// Fetch the last row and column index.
let lr = sender.rowCount('body') - 1;
let lc = sender.columnCount('body') - 1;
// Bail early if the data model is empty.
if (lr < 0 || lc < 0) {
this._selections.length = 0;
this.emitChanged();
return;
}
// Fetch the selection mode.
let mode = this.selectionMode;
// Set up the assignment index variable.
let j = 0;
// Iterate over the current selections.
for (let i = 0, n = this._selections.length; i < n; ++i) {
// Unpack the selection.
let { r1, c1, r2, c2 } = this._selections[i];
// Skip the selection if it will disappear.
if ((lr < r1 && lr < r2) || (lc < c1 && lc < c2)) {
continue;
}
// Modify the bounds based on the selection mode.
if (mode === 'row') {
r1 = Math.max(0, Math.min(r1, lr));
r2 = Math.max(0, Math.min(r2, lr));
c1 = 0;
c2 = lc;
}
else if (mode === 'column') {
r1 = 0;
r2 = lr;
c1 = Math.max(0, Math.min(c1, lc));
c2 = Math.max(0, Math.min(c2, lc));
}
else {
r1 = Math.max(0, Math.min(r1, lr));
r2 = Math.max(0, Math.min(r2, lr));
c1 = Math.max(0, Math.min(c1, lc));
c2 = Math.max(0, Math.min(c2, lc));
}
// Assign the modified selection to the array.
this._selections[j++] = { r1, c1, r2, c2 };
}
// Remove the stale selections.
this._selections.length = j;
// Emit the changed signal.
this.emitChanged();
}
}
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2023, Lumino Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* An object which renders the cells of a data grid asynchronously.
*
* #### Notes
* For performance reason, the datagrid only paints cells synchronously,
* though if your cell renderer inherits from AsyncCellRenderer, you will
* be able to do some asynchronous work prior to painting the cell.
* See `ImageRenderer` for an example of an asynchronous renderer.
*/
class AsyncCellRenderer extends CellRenderer {
}
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
// default validation error message
const DEFAULT_INVALID_INPUT_MESSAGE = 'Invalid input!';
/**
* A cell input validator object which always returns valid.
*/
class PassInputValidator {
/**
* Validate cell input.
*
* @param cell - The object holding cell configuration data.
*
* @param value - The cell value input.
*
* @returns An object with validation result.
*/
validate(cell, value) {
return { valid: true };
}
}
/**
* Text cell input validator.
*/
class TextInputValidator {
constructor() {
/**
* Minimum text length
*
* The default is Number.NaN, meaning no minimum constraint
*/
this.minLength = Number.NaN;
/**
* Maximum text length
*
* The default is Number.NaN, meaning no maximum constraint
*/
this.maxLength = Number.NaN;
/**
* Required text pattern as regular expression
*
* The default is null, meaning no pattern constraint
*/
this.pattern = null;
}
/**
* Validate cell input.
*
* @param cell - The object holding cell configuration data.
*
* @param value - The cell value input.
*
* @returns An object with validation result.
*/
validate(cell, value) {
if (value === null) {
return { valid: true };
}
if (typeof value !== 'string') {
return {
valid: false,
message: 'Input must be valid text'
};
}
if (!isNaN(this.minLength) && value.length < this.minLength) {
return {
valid: false,
message: `Text length must be greater than ${this.minLength}`
};
}
if (!isNaN(this.maxLength) && value.length > this.maxLength) {
return {
valid: false,
message: `Text length must be less than ${this.maxLength}`
};
}
if (this.pattern && !this.pattern.test(value)) {
return {
valid: false,
message: `Text doesn't match the required pattern`
};
}
return { valid: true };
}
}
/**
* Integer cell input validator.
*/
class IntegerInputValidator {
constructor() {
/**
* Minimum value
*
* The default is Number.NaN, meaning no minimum constraint
*/
this.min = Number.NaN;
/**
* Maximum value
*
* The default is Number.NaN, meaning no maximum constraint
*/
this.max = Number.NaN;
}
/**
* Validate cell input.
*
* @param cell - The object holding cell configuration data.
*
* @param value - The cell value input.
*
* @returns An object with validation result.
*/
validate(cell, value) {
if (value === null) {
return { valid: true };
}
if (isNaN(value) || value % 1 !== 0) {
return {
valid: false,
message: 'Input must be valid integer'
};
}
if (!isNaN(this.min) && value < this.min) {
return {
valid: false,
message: `Input must be greater than ${this.min}`
};
}
if (!isNaN(this.max) && value > this.max) {
return {
valid: false,
message: `Input must be less than ${this.max}`
};
}
return { valid: true };
}
}
/**
* Real number cell input validator.
*/
class NumberInputValidator {
constructor() {
/**
* Minimum value
*
* The default is Number.NaN, meaning no minimum constraint
*/
this.min = Number.NaN;
/**
* Maximum value
*
* The default is Number.NaN, meaning no maximum constraint
*/
this.max = Number.NaN;
}
/**
* Validate cell input.
*
* @param cell - The object holding cell configuration data.
*
* @param value - The cell value input.
*
* @returns An object with validation result.
*/
validate(cell, value) {
if (value === null) {
return { valid: true };
}
if (isNaN(value)) {
return {
valid: false,
message: 'Input must be valid number'
};
}
if (!isNaN(this.min) && value < this.min) {
return {
valid: false,
message: `Input must be greater than ${this.min}`
};
}
if (!isNaN(this.max) && value > this.max) {
return {
valid: false,
message: `Input must be less than ${this.max}`
};
}
return { valid: true };
}
}
/**
* An abstract base class that provides the most of the functionality
* needed by a cell editor. All of the built-in cell editors
* for various cell types are derived from this base class. Custom cell editors
* can be easily implemented by extending this class.
*/
class CellEditor {
/**
* Construct a new cell editor.
*/
constructor() {
/**
* A signal emitted when input changes.
*/
this.inputChanged = new _lumino_signaling__WEBPACK_IMPORTED_MODULE_4__.Signal(this);
/**
* Notification popup used to show validation error messages.
*/
this.validityNotification = null;
/**
* Whether the cell editor is disposed.
*/
this._disposed = false;
/**
* Whether the value input is valid.
*/
this._validInput = true;
/**
* Grid wheel event handler.
*/
this._gridWheelEventHandler = null;
this.inputChanged.connect(() => {
this.validate();
});
}
/**
* Whether the cell editor is disposed.
*/
get isDisposed() {
return this._disposed;
}
/**
* Dispose of the resources held by cell editor.
*/
dispose() {
if (this._disposed) {
return;
}
if (this._gridWheelEventHandler) {
this.cell.grid.node.removeEventListener('wheel', this._gridWheelEventHandler);
this._gridWheelEventHandler = null;
}
this._closeValidityNotification();
this._disposed = true;
this.cell.grid.node.removeChild(this.viewportOccluder);
}
/**
* Start editing the cell.
*
* @param cell - The object holding cell configuration data.
*
* @param options - The cell editing options.
*/
edit(cell, options) {
this.cell = cell;
this.onCommit = options && options.onCommit;
this.onCancel = options && options.onCancel;
this.validator =
options && options.validator
? options.validator
: this.createValidatorBasedOnType();
this._gridWheelEventHandler = () => {
this._closeValidityNotification();
this.updatePosition();
};
cell.grid.node.addEventListener('wheel', this._gridWheelEventHandler);
this._addContainer();
this.updatePosition();
this.startEditing();
}
/**
* Cancel editing the cell.
*/
cancel() {
if (this._disposed) {
return;
}
this.dispose();
if (this.onCancel) {
this.onCancel();
}
}
/**
* Whether the value input is valid.
*/
get validInput() {
return this._validInput;
}
/**
* Validate the cell input. Shows validation error notification when input is invalid.
*/
validate() {
let value;
try {
value = this.getInput();
}
catch (error) {
console.log(`Input error: ${error.message}`);
this.setValidity(false, error.message || DEFAULT_INVALID_INPUT_MESSAGE);
return;
}
if (this.validator) {
const result = this.validator.validate(this.cell, value);
if (result.valid) {
this.setValidity(true);
}
else {
this.setValidity(false, result.message || DEFAULT_INVALID_INPUT_MESSAGE);
}
}
else {
this.setValidity(true);
}
}
/**
* Set validity flag.
*
* @param valid - Whether the input is valid.
*
* @param message - Notification message to show.
*
* If message is set to empty string (which is the default)
* existing notification popup is removed if any.
*/
setValidity(valid, message = '') {
this._validInput = valid;
this._closeValidityNotification();
if (valid) {
this.editorContainer.classList.remove('lm-mod-invalid');
}
else {
this.editorContainer.classList.add('lm-mod-invalid');
// show a notification popup
if (message !== '') {
this.validityNotification = new CellEditor.Notification({
target: this.editorContainer,
message: message,
placement: 'bottom',
timeout: 5000
});
this.validityNotification.show();
}
}
}
/**
* Create and return a cell input validator based on configuration of the
* cell being edited. If no suitable validator can be found, it returns undefined.
*/
createValidatorBasedOnType() {
const cell = this.cell;
const metadata = cell.grid.dataModel.metadata('body', cell.row, cell.column);
switch (metadata && metadata.type) {
case 'string':
{
const validator = new TextInputValidator();
if (typeof metadata.format === 'string') {
const format = metadata.format;
switch (format) {
case 'email':
validator.pattern = new RegExp('^([a-z0-9_.-]+)@([da-z.-]+).([a-z.]{2,6})$');
break;
case 'uuid':
validator.pattern = new RegExp('[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}');
break;
}
}
if (metadata.constraint) {
if (metadata.constraint.minLength !== undefined) {
validator.minLength = metadata.constraint.minLength;
}
if (metadata.constraint.maxLength !== undefined) {
validator.maxLength = metadata.constraint.maxLength;
}
if (typeof metadata.constraint.pattern === 'string') {
validator.pattern = new RegExp(metadata.constraint.pattern);
}
}
return validator;
}
case 'number':
{
const validator = new NumberInputValidator();
if (metadata.constraint) {
if (metadata.constraint.minimum !== undefined) {
validator.min = metadata.constraint.minimum;
}
if (metadata.constraint.maximum !== undefined) {
validator.max = metadata.constraint.maximum;
}
}
return validator;
}
case 'integer':
{
const validator = new IntegerInputValidator();
if (metadata.constraint) {
if (metadata.constraint.minimum !== undefined) {
validator.min = metadata.constraint.minimum;
}
if (metadata.constraint.maximum !== undefined) {
validator.max = metadata.constraint.maximum;
}
}
return validator;
}
}
return undefined;
}
/**
* Compute cell rectangle and return with other cell properties.
*/
getCellInfo(cell) {
const { grid, row, column } = cell;
let data, columnX, rowY, width, height;
const cellGroup = CellGroup.getGroup(grid.dataModel, 'body', row, column);
if (cellGroup) {
columnX =
grid.headerWidth -
grid.scrollX +
grid.columnOffset('body', cellGroup.c1);
rowY =
grid.headerHeight - grid.scrollY + grid.rowOffset('body', cellGroup.r1);
width = 0;
height = 0;
for (let r = cellGroup.r1; r <= cellGroup.r2; r++) {
height += grid.rowSize('body', r);
}
for (let c = cellGroup.c1; c <= cellGroup.c2; c++) {
width += grid.columnSize('body', c);
}
data = grid.dataModel.data('body', cellGroup.r1, cellGroup.c1);
}
else {
columnX =
grid.headerWidth - grid.scrollX + grid.columnOffset('body', column);
rowY = grid.headerHeight - grid.scrollY + grid.rowOffset('body', row);
width = grid.columnSize('body', column);
height = grid.rowSize('body', row);
data = grid.dataModel.data('body', row, column);
}
return {
grid: grid,
row: row,
column: column,
data: data,
x: columnX,
y: rowY,
width: width,
height: height
};
}
/**
* Reposition cell editor by moving viewport occluder and cell editor container.
*/
updatePosition() {
const grid = this.cell.grid;
const cellInfo = this.getCellInfo(this.cell);
const headerHeight = grid.headerHeight;
const headerWidth = grid.headerWidth;
this.viewportOccluder.style.top = headerHeight + 'px';
this.viewportOccluder.style.left = headerWidth + 'px';
this.viewportOccluder.style.width = grid.viewportWidth - headerWidth + 'px';
this.viewportOccluder.style.height =
grid.viewportHeight - headerHeight + 'px';
this.viewportOccluder.style.position = 'absolute';
this.editorContainer.style.left = cellInfo.x - 1 - headerWidth + 'px';
this.editorContainer.style.top = cellInfo.y - 1 - headerHeight + 'px';
this.editorContainer.style.width = cellInfo.width + 1 + 'px';
this.editorContainer.style.height = cellInfo.height + 1 + 'px';
this.editorContainer.style.visibility = 'visible';
this.editorContainer.style.position = 'absolute';
}
/**
* Commit the edited value.
*
* @param cursorMovement - Cursor move direction based on keys pressed to end the edit.
*
* @returns true on valid input, false otherwise.
*/
commit(cursorMovement = 'none') {
this.validate();
if (!this._validInput) {
return false;
}
let value;
try {
value = this.getInput();
}
catch (error) {
console.log(`Input error: ${error.message}`);
return false;
}
this.dispose();
if (this.onCommit) {
this.onCommit({
cell: this.cell,
value: value,
cursorMovement: cursorMovement
});
}
return true;
}
/**
* Create container elements needed to prevent editor widget overflow
* beyond viewport and to position cell editor widget.
*/
_addContainer() {
this.viewportOccluder = document.createElement('div');
this.viewportOccluder.className = 'lm-DataGrid-cellEditorOccluder';
this.cell.grid.node.appendChild(this.viewportOccluder);
this.editorContainer = document.createElement('div');
this.editorContainer.className = 'lm-DataGrid-cellEditorContainer';
this.viewportOccluder.appendChild(this.editorContainer);
// update mouse event pass-through state based on input validity
this.editorContainer.addEventListener('mouseleave', (event) => {
this.viewportOccluder.style.pointerEvents = this._validInput
? 'none'
: 'auto';
});
this.editorContainer.addEventListener('mouseenter', (event) => {
this.viewportOccluder.style.pointerEvents = 'none';
});
}
/**
* Remove validity notification popup.
*/
_closeValidityNotification() {
if (this.validityNotification) {
this.validityNotification.close();
this.validityNotification = null;
}
}
}
/**
* Abstract base class with shared functionality
* for cell editors which use HTML Input widget as editor.
*/
class InputCellEditor extends CellEditor {
/**
* Handle the DOM events for the editor.
*
* @param event - The DOM event sent to the editor.
*/
handleEvent(event) {
switch (event.type) {
case 'keydown':
this._onKeyDown(event);
break;
case 'blur':
this._onBlur(event);
break;
case 'input':
this._onInput(event);
break;
}
}
/**
* Dispose of the resources held by cell editor.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._unbindEvents();
super.dispose();
}
/**
* Start editing the cell.
*/
startEditing() {
this.createWidget();
const cell = this.cell;
const cellInfo = this.getCellInfo(cell);
this.input.value = this.deserialize(cellInfo.data);
this.editorContainer.appendChild(this.input);
this.input.focus();
this.input.select();
this.bindEvents();
}
deserialize(value) {
if (value === null || value === undefined) {
return '';
}
return value.toString();
}
createWidget() {
const input = document.createElement('input');
input.classList.add('lm-DataGrid-cellEditorWidget');
input.classList.add('lm-DataGrid-cellEditorInput');
input.spellcheck = false;
input.type = this.inputType;
this.input = input;
}
bindEvents() {
this.input.addEventListener('keydown', this);
this.input.addEventListener('blur', this);
this.input.addEventListener('input', this);
}
_unbindEvents() {
this.input.removeEventListener('keydown', this);
this.input.removeEventListener('blur', this);
this.input.removeEventListener('input', this);
}
_onKeyDown(event) {
switch ((0,_lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__.getKeyboardLayout)().keyForKeydownEvent(event)) {
case 'Enter':
this.commit(event.shiftKey ? 'up' : 'down');
break;
case 'Tab':
this.commit(event.shiftKey ? 'left' : 'right');
event.stopPropagation();
event.preventDefault();
break;
case 'Escape':
this.cancel();
break;
}
}
_onBlur(event) {
if (this.isDisposed) {
return;
}
if (!this.commit()) {
event.preventDefault();
event.stopPropagation();
this.input.focus();
}
}
_onInput(event) {
this.inputChanged.emit(void 0);
}
}
/**
* Cell editor for text cells.
*/
class TextCellEditor extends InputCellEditor {
constructor() {
super(...arguments);
this.inputType = 'text';
}
/**
* Return the current text input entered.
*/
getInput() {
return this.input.value;
}
}
/**
* Cell editor for real number cells.
*/
class NumberCellEditor extends InputCellEditor {
constructor() {
super(...arguments);
this.inputType = 'number';
}
/**
* Start editing the cell.
*/
startEditing() {
super.startEditing();
this.input.step = 'any';
const cell = this.cell;
const metadata = cell.grid.dataModel.metadata('body', cell.row, cell.column);
const constraint = metadata.constraint;
if (constraint) {
if (constraint.minimum) {
this.input.min = constraint.minimum;
}
if (constraint.maximum) {
this.input.max = constraint.maximum;
}
}
}
/**
* Return the current number input entered. This method throws exception
* if input is invalid.
*/
getInput() {
let value = this.input.value;
if (value.trim() === '') {
return null;
}
const floatValue = parseFloat(value);
if (isNaN(floatValue)) {
throw new Error('Invalid input');
}
return floatValue;
}
}
/**
* Cell editor for integer cells.
*/
class IntegerCellEditor extends InputCellEditor {
constructor() {
super(...arguments);
this.inputType = 'number';
}
/**
* Start editing the cell.
*/
startEditing() {
super.startEditing();
this.input.step = '1';
const cell = this.cell;
const metadata = cell.grid.dataModel.metadata('body', cell.row, cell.column);
const constraint = metadata.constraint;
if (constraint) {
if (constraint.minimum) {
this.input.min = constraint.minimum;
}
if (constraint.maximum) {
this.input.max = constraint.maximum;
}
}
}
/**
* Return the current integer input entered. This method throws exception
* if input is invalid.
*/
getInput() {
let value = this.input.value;
if (value.trim() === '') {
return null;
}
let intValue = parseInt(value);
if (isNaN(intValue)) {
throw new Error('Invalid input');
}
return intValue;
}
}
/**
* Cell editor for date cells.
*/
class DateCellEditor extends CellEditor {
/**
* Handle the DOM events for the editor.
*
* @param event - The DOM event sent to the editor.
*/
handleEvent(event) {
switch (event.type) {
case 'keydown':
this._onKeyDown(event);
break;
case 'blur':
this._onBlur(event);
break;
}
}
/**
* Dispose of the resources held by cell editor.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._unbindEvents();
super.dispose();
}
/**
* Start editing the cell.
*/
startEditing() {
this._createWidget();
const cell = this.cell;
const cellInfo = this.getCellInfo(cell);
this._input.value = this._deserialize(cellInfo.data);
this.editorContainer.appendChild(this._input);
this._input.focus();
this._bindEvents();
}
/**
* Return the current date input entered.
*/
getInput() {
return this._input.value;
}
_deserialize(value) {
if (value === null || value === undefined) {
return '';
}
return value.toString();
}
_createWidget() {
const input = document.createElement('input');
input.type = 'date';
input.pattern = 'd{4}-d{2}-d{2}';
input.classList.add('lm-DataGrid-cellEditorWidget');
input.classList.add('lm-DataGrid-cellEditorInput');
this._input = input;
}
_bindEvents() {
this._input.addEventListener('keydown', this);
this._input.addEventListener('blur', this);
}
_unbindEvents() {
this._input.removeEventListener('keydown', this);
this._input.removeEventListener('blur', this);
}
_onKeyDown(event) {
switch ((0,_lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__.getKeyboardLayout)().keyForKeydownEvent(event)) {
case 'Enter':
this.commit(event.shiftKey ? 'up' : 'down');
break;
case 'Tab':
this.commit(event.shiftKey ? 'left' : 'right');
event.stopPropagation();
event.preventDefault();
break;
case 'Escape':
this.cancel();
break;
}
}
_onBlur(event) {
if (this.isDisposed) {
return;
}
if (!this.commit()) {
event.preventDefault();
event.stopPropagation();
this._input.focus();
}
}
}
/**
* Cell editor for boolean cells.
*/
class BooleanCellEditor extends CellEditor {
/**
* Handle the DOM events for the editor.
*
* @param event - The DOM event sent to the editor.
*/
handleEvent(event) {
switch (event.type) {
case 'keydown':
this._onKeyDown(event);
break;
case 'mousedown':
// fix focus loss problem in Safari and Firefox
this._input.focus();
event.stopPropagation();
event.preventDefault();
break;
case 'blur':
this._onBlur(event);
break;
}
}
/**
* Dispose of the resources held by cell editor.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._unbindEvents();
super.dispose();
}
/**
* Start editing the cell.
*/
startEditing() {
this._createWidget();
const cell = this.cell;
const cellInfo = this.getCellInfo(cell);
this._input.checked = this._deserialize(cellInfo.data);
this.editorContainer.appendChild(this._input);
this._input.focus();
this._bindEvents();
}
/**
* Return the current boolean input entered.
*/
getInput() {
return this._input.checked;
}
_deserialize(value) {
if (value === null || value === undefined) {
return false;
}
return value == true;
}
_createWidget() {
const input = document.createElement('input');
input.classList.add('lm-DataGrid-cellEditorWidget');
input.classList.add('lm-DataGrid-cellEditorCheckbox');
input.type = 'checkbox';
input.spellcheck = false;
this._input = input;
}
_bindEvents() {
this._input.addEventListener('keydown', this);
this._input.addEventListener('mousedown', this);
this._input.addEventListener('blur', this);
}
_unbindEvents() {
this._input.removeEventListener('keydown', this);
this._input.removeEventListener('mousedown', this);
this._input.removeEventListener('blur', this);
}
_onKeyDown(event) {
switch ((0,_lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__.getKeyboardLayout)().keyForKeydownEvent(event)) {
case 'Enter':
this.commit(event.shiftKey ? 'up' : 'down');
break;
case 'Tab':
this.commit(event.shiftKey ? 'left' : 'right');
event.stopPropagation();
event.preventDefault();
break;
case 'Escape':
this.cancel();
break;
}
}
_onBlur(event) {
if (this.isDisposed) {
return;
}
if (!this.commit()) {
event.preventDefault();
event.stopPropagation();
this._input.focus();
}
}
}
/**
* Cell editor for option cells.
*
* It supports multiple option selection. If cell metadata contains
* type attribute 'array', then it behaves as a multi select.
* In that case cell data is expected to be list of string values.
*/
class OptionCellEditor extends CellEditor {
constructor() {
super(...arguments);
this._isMultiSelect = false;
}
/**
* Dispose of the resources held by cell editor.
*/
dispose() {
if (this.isDisposed) {
return;
}
super.dispose();
if (this._isMultiSelect) {
document.body.removeChild(this._select);
}
}
/**
* Start editing the cell.
*/
startEditing() {
const cell = this.cell;
const cellInfo = this.getCellInfo(cell);
const metadata = cell.grid.dataModel.metadata('body', cell.row, cell.column);
this._isMultiSelect = metadata.type === 'array';
this._createWidget();
if (this._isMultiSelect) {
this._select.multiple = true;
const values = this._deserialize(cellInfo.data);
for (let i = 0; i < this._select.options.length; ++i) {
const option = this._select.options.item(i);
option.selected = values.indexOf(option.value) !== -1;
}
document.body.appendChild(this._select);
}
else {
this._select.value = this._deserialize(cellInfo.data);
this.editorContainer.appendChild(this._select);
}
this._select.focus();
this._bindEvents();
this.updatePosition();
}
/**
* Return the current option input.
*/
getInput() {
if (this._isMultiSelect) {
const input = [];
for (let i = 0; i < this._select.selectedOptions.length; ++i) {
input.push(this._select.selectedOptions.item(i).value);
}
return input;
}
else {
return this._select.value;
}
}
/**
* Reposition cell editor.
*/
updatePosition() {
super.updatePosition();
if (!this._isMultiSelect) {
return;
}
const cellInfo = this.getCellInfo(this.cell);
this._select.style.position = 'absolute';
const editorContainerRect = this.editorContainer.getBoundingClientRect();
this._select.style.left = editorContainerRect.left + 'px';
this._select.style.top = editorContainerRect.top + cellInfo.height + 'px';
this._select.style.width = editorContainerRect.width + 'px';
this._select.style.maxHeight = '60px';
this.editorContainer.style.visibility = 'hidden';
}
_deserialize(value) {
if (value === null || value === undefined) {
return '';
}
if (this._isMultiSelect) {
const values = [];
if (Array.isArray(value)) {
for (let item of value) {
values.push(item.toString());
}
}
return values;
}
else {
return value.toString();
}
}
_createWidget() {
const cell = this.cell;
const metadata = cell.grid.dataModel.metadata('body', cell.row, cell.column);
const items = metadata.constraint.enum;
const select = document.createElement('select');
select.classList.add('lm-DataGrid-cellEditorWidget');
for (let item of items) {
const option = document.createElement('option');
option.value = item;
option.text = item;
select.appendChild(option);
}
this._select = select;
}
_bindEvents() {
this._select.addEventListener('keydown', this._onKeyDown.bind(this));
this._select.addEventListener('blur', this._onBlur.bind(this));
}
_onKeyDown(event) {
switch ((0,_lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__.getKeyboardLayout)().keyForKeydownEvent(event)) {
case 'Enter':
this.commit(event.shiftKey ? 'up' : 'down');
break;
case 'Tab':
this.commit(event.shiftKey ? 'left' : 'right');
event.stopPropagation();
event.preventDefault();
break;
case 'Escape':
this.cancel();
break;
}
}
_onBlur(event) {
if (this.isDisposed) {
return;
}
if (!this.commit()) {
event.preventDefault();
event.stopPropagation();
this._select.focus();
}
}
}
/**
* Cell editor for option cells whose value can be any value
* from set of pre-defined options or values that can be input by user.
*/
class DynamicOptionCellEditor extends CellEditor {
/**
* Handle the DOM events for the editor.
*
* @param event - The DOM event sent to the editor.
*/
handleEvent(event) {
switch (event.type) {
case 'keydown':
this._onKeyDown(event);
break;
case 'blur':
this._onBlur(event);
break;
}
}
/**
* Dispose of the resources held by cell editor.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._unbindEvents();
super.dispose();
}
/**
* Start editing the cell.
*/
startEditing() {
this._createWidget();
const cell = this.cell;
const cellInfo = this.getCellInfo(cell);
this._input.value = this._deserialize(cellInfo.data);
this.editorContainer.appendChild(this._input);
this._input.focus();
this._input.select();
this._bindEvents();
}
/**
* Return the current option input.
*/
getInput() {
return this._input.value;
}
_deserialize(value) {
if (value === null || value === undefined) {
return '';
}
return value.toString();
}
_createWidget() {
const cell = this.cell;
const grid = cell.grid;
const dataModel = grid.dataModel;
const rowCount = dataModel.rowCount('body');
const listId = 'cell-editor-list';
const list = document.createElement('datalist');
list.id = listId;
const input = document.createElement('input');
input.classList.add('lm-DataGrid-cellEditorWidget');
input.classList.add('lm-DataGrid-cellEditorInput');
const valueSet = new Set();
for (let r = 0; r < rowCount; ++r) {
const data = dataModel.data('body', r, cell.column);
if (data) {
valueSet.add(data);
}
}
valueSet.forEach((value) => {
const option = document.createElement('option');
option.value = value;
option.text = value;
list.appendChild(option);
});
this.editorContainer.appendChild(list);
input.setAttribute('list', listId);
this._input = input;
}
_bindEvents() {
this._input.addEventListener('keydown', this);
this._input.addEventListener('blur', this);
}
_unbindEvents() {
this._input.removeEventListener('keydown', this);
this._input.removeEventListener('blur', this);
}
_onKeyDown(event) {
switch ((0,_lumino_keyboard__WEBPACK_IMPORTED_MODULE_1__.getKeyboardLayout)().keyForKeydownEvent(event)) {
case 'Enter':
this.commit(event.shiftKey ? 'up' : 'down');
break;
case 'Tab':
this.commit(event.shiftKey ? 'left' : 'right');
event.stopPropagation();
event.preventDefault();
break;
case 'Escape':
this.cancel();
break;
}
}
_onBlur(event) {
if (this.isDisposed) {
return;
}
if (!this.commit()) {
event.preventDefault();
event.stopPropagation();
this._input.focus();
}
}
}
/**
* The namespace for the `CellEditor` class statics.
*/
(function (CellEditor) {
/**
* A widget which implements a notification popup.
*/
class Notification extends _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.Widget {
/**
* Construct a new notification.
*
* @param options - The options for initializing the notification.
*/
constructor(options) {
super({ node: Notification.createNode() });
this._message = '';
this.addClass('lm-DataGrid-notification');
this.setFlag(_lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.Widget.Flag.DisallowLayout);
this._target = options.target;
this._message = options.message || '';
this._placement = options.placement || 'bottom';
_lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.Widget.attach(this, document.body);
if (options.timeout && options.timeout > 0) {
setTimeout(() => {
this.close();
}, options.timeout);
}
}
/**
* Handle the DOM events for the notification.
*
* @param event - The DOM event sent to the notification.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the notification's DOM node.
*
* This should not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'mousedown':
this._evtMouseDown(event);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* Get the placement of the notification.
*/
get placement() {
return this._placement;
}
/**
* Set the placement of the notification.
*/
set placement(value) {
// Do nothing if the placement does not change.
if (this._placement === value) {
return;
}
// Update the internal placement.
this._placement = value;
// Schedule an update for notification.
this.update();
}
/**
* Get the current value of the message.
*/
get message() {
return this._message;
}
/**
* Set the current value of the message.
*
*/
set message(value) {
// Do nothing if the value does not change.
if (this._message === value) {
return;
}
// Update the internal value.
this._message = value;
// Schedule an update for notification.
this.update();
}
/**
* Get the node presenting the message.
*/
get messageNode() {
return this.node.getElementsByClassName('lm-DataGrid-notificationMessage')[0];
}
/**
* A method invoked on a 'before-attach' message.
*/
onBeforeAttach(msg) {
this.node.addEventListener('mousedown', this);
this.update();
}
/**
* A method invoked on an 'after-detach' message.
*/
onAfterDetach(msg) {
this.node.removeEventListener('mousedown', this);
}
/**
* A method invoked on an 'update-request' message.
*/
onUpdateRequest(msg) {
const targetRect = this._target.getBoundingClientRect();
const style = this.node.style;
switch (this._placement) {
case 'bottom':
style.left = targetRect.left + 'px';
style.top = targetRect.bottom + 'px';
break;
case 'top':
style.left = targetRect.left + 'px';
style.height = targetRect.top + 'px';
style.top = '0';
style.alignItems = 'flex-end';
style.justifyContent = 'flex-end';
break;
case 'left':
style.left = '0';
style.width = targetRect.left + 'px';
style.top = targetRect.top + 'px';
style.alignItems = 'flex-end';
style.justifyContent = 'flex-end';
break;
case 'right':
style.left = targetRect.right + 'px';
style.top = targetRect.top + 'px';
break;
}
this.messageNode.innerHTML = this._message;
}
/**
* Handle the `'mousedown'` event for the notification.
*/
_evtMouseDown(event) {
// Do nothing if it's not a left mouse press.
if (event.button !== 0) {
return;
}
event.preventDefault();
event.stopPropagation();
this.close();
}
}
CellEditor.Notification = Notification;
/**
* The namespace for the `Notification` class statics.
*/
(function (Notification) {
/**
* Create the DOM node for notification.
*/
function createNode() {
const node = document.createElement('div');
const container = document.createElement('div');
container.className = 'lm-DataGrid-notificationContainer';
const message = document.createElement('span');
message.className = 'lm-DataGrid-notificationMessage';
container.appendChild(message);
node.appendChild(container);
return node;
}
Notification.createNode = createNode;
})(Notification = CellEditor.Notification || (CellEditor.Notification = {}));
})(CellEditor || (CellEditor = {}));
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Resolve a config option for a cell editor.
*
* @param option - The config option to resolve.
*
* @param config - The cell config object.
*
* @returns The resolved value for the option.
*/
function resolveOption(option, config) {
return typeof option === 'function'
? option(config)
: option;
}
/**
* An object which manages cell editing. It stores editor overrides,
* decides which editor to use for a cell, makes sure there is only one editor active.
*/
class CellEditorController {
constructor() {
// active cell editor
this._editor = null;
// active cell being edited
this._cell = null;
// cell editor overrides based on cell data type identifier
this._typeBasedOverrides = new Map();
// cell editor overrides based on partial metadata match
this._metadataBasedOverrides = new Map();
}
/**
* Override cell editor for the cells matching the identifier.
*
* @param identifier - Cell identifier to use when matching cells.
* if identifier is a CellDataType, then cell matching is done using data type of the cell,
* if identifier is a Metadata, then partial match of the cell metadata with identifier is used for match,
* if identifier is 'default' then override is used as default editor when no other editor is found suitable
*
* @param editor - The cell editor to use or resolver to use to get an editor for matching cells.
*/
setEditor(identifier, editor) {
if (typeof identifier === 'string') {
this._typeBasedOverrides.set(identifier, editor);
}
else {
const key = this._metadataIdentifierToKey(identifier);
this._metadataBasedOverrides.set(key, [identifier, editor]);
}
}
/**
* Start editing a cell.
*
* @param cell - The object holding cell configuration data.
*
* @param options - The cell editing options.
*/
edit(cell, options) {
const grid = cell.grid;
if (!grid.editable) {
console.error('Grid cannot be edited!');
return false;
}
this.cancel();
this._cell = cell;
options = options || {};
options.onCommit = options.onCommit || this._onCommit.bind(this);
options.onCancel = options.onCancel || this._onCancel.bind(this);
// if an editor is passed in with options, then use it for editing
if (options.editor) {
this._editor = options.editor;
options.editor.edit(cell, options);
return true;
}
// choose an editor based on overrides / cell data type
const editor = this._getEditor(cell);
if (editor) {
this._editor = editor;
editor.edit(cell, options);
return true;
}
return false;
}
/**
* Cancel editing.
*/
cancel() {
if (this._editor) {
this._editor.cancel();
this._editor = null;
}
this._cell = null;
}
_onCommit(response) {
const cell = this._cell;
if (!cell) {
return;
}
const grid = cell.grid;
const dataModel = grid.dataModel;
let row = cell.row;
let column = cell.column;
const cellGroup = CellGroup.getGroup(grid.dataModel, 'body', row, column);
if (cellGroup) {
row = cellGroup.r1;
column = cellGroup.c1;
}
dataModel.setData('body', row, column, response.value);
grid.viewport.node.focus();
if (response.cursorMovement !== 'none') {
grid.moveCursor(response.cursorMovement);
grid.scrollToCursor();
}
}
_onCancel() {
if (!this._cell) {
return;
}
this._cell.grid.viewport.node.focus();
}
_getDataTypeKey(cell) {
const metadata = cell.grid.dataModel
? cell.grid.dataModel.metadata('body', cell.row, cell.column)
: null;
if (!metadata) {
return 'default';
}
let key = '';
if (metadata) {
key = metadata.type;
}
if (metadata.constraint && metadata.constraint.enum) {
if (metadata.constraint.enum === 'dynamic') {
key += ':dynamic-option';
}
else {
key += ':option';
}
}
return key;
}
_objectToKey(object) {
let str = '';
for (let key in object) {
const value = object[key];
if (typeof value === 'object') {
str += `${key}:${this._objectToKey(value)}`;
}
else {
str += `[${key}:${value}]`;
}
}
return str;
}
_metadataIdentifierToKey(metadata) {
return this._objectToKey(metadata);
}
_metadataMatchesIdentifier(metadata, identifier) {
for (let key in identifier) {
if (!metadata.hasOwnProperty(key)) {
return false;
}
const identifierValue = identifier[key];
const metadataValue = metadata[key];
if (typeof identifierValue === 'object') {
if (!this._metadataMatchesIdentifier(metadataValue, identifierValue)) {
return false;
}
}
else if (metadataValue !== identifierValue) {
return false;
}
}
return true;
}
_getMetadataBasedEditor(cell) {
let editorMatched;
const metadata = cell.grid.dataModel.metadata('body', cell.row, cell.column);
if (metadata) {
this._metadataBasedOverrides.forEach(value => {
if (!editorMatched) {
let [identifier, editor] = value;
if (this._metadataMatchesIdentifier(metadata, identifier)) {
editorMatched = resolveOption(editor, cell);
}
}
});
}
return editorMatched;
}
/**
* Choose the most appropriate cell editor to use based on overrides / cell data type.
*
* If no match is found in overrides or based on cell data type, and if cell has a primitive
* data type then TextCellEditor is used as default cell editor. If 'default' cell editor
* is overridden, then it is used instead of TextCellEditor for default.
*/
_getEditor(cell) {
const dtKey = this._getDataTypeKey(cell);
// find an editor based on data type based override
if (this._typeBasedOverrides.has(dtKey)) {
const editor = this._typeBasedOverrides.get(dtKey);
return resolveOption(editor, cell);
} // find an editor based on metadata match based override
else if (this._metadataBasedOverrides.size > 0) {
const editor = this._getMetadataBasedEditor(cell);
if (editor) {
return editor;
}
}
// choose an editor based on data type
switch (dtKey) {
case 'string':
return new TextCellEditor();
case 'number':
return new NumberCellEditor();
case 'integer':
return new IntegerCellEditor();
case 'boolean':
return new BooleanCellEditor();
case 'date':
return new DateCellEditor();
case 'string:option':
case 'number:option':
case 'integer:option':
case 'date:option':
case 'array:option':
return new OptionCellEditor();
case 'string:dynamic-option':
case 'number:dynamic-option':
case 'integer:dynamic-option':
case 'date:dynamic-option':
return new DynamicOptionCellEditor();
}
// if an override exists for 'default', then use it
if (this._typeBasedOverrides.has('default')) {
const editor = this._typeBasedOverrides.get('default');
return resolveOption(editor, cell);
}
// if cell has a primitive data type then use TextCellEditor
const data = cell.grid.dataModel.data('body', cell.row, cell.column);
if (!data || typeof data !== 'object') {
return new TextCellEditor();
}
// no suitable editor found for the cell
return undefined;
}
}
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* An object which provides the data for a data grid.
*
* #### Notes
* If the predefined data models are insufficient for a particular use
* case, a custom model can be defined which derives from this class.
*/
class DataModel {
constructor() {
this._changed = new _lumino_signaling__WEBPACK_IMPORTED_MODULE_4__.Signal(this);
}
/**
* A signal emitted when the data model has changed.
*/
get changed() {
return this._changed;
}
/**
* Get the count of merged cell groups pertaining to a given
* cell region.
* @param region the target cell region.
*/
groupCount(region) {
return 0;
}
/**
* Get the metadata for a cell in the data model.
*
* @param region - The cell region of interest.
*
* @param row - The row index of the cell of interest.
*
* @param column - The column index of the cell of interest.
*
* @returns The metadata for the specified cell.
*
* #### Notes
* The returned metadata should be treated as immutable.
*
* This method is called often, and so should be efficient.
*
* The default implementation returns `{}`.
*/
metadata(region, row, column) {
return DataModel.emptyMetadata;
}
/**
* Get the merged cell group corresponding to a region and index number.
* @param region the cell region of cell group.
* @param groupIndex the group index of the cell group.
* @returns a cell group.
*/
group(region, groupIndex) {
return null;
}
/**
* Emit the `changed` signal for the data model.
*
* #### Notes
* Subclasses should call this method whenever the data model has
* changed so that attached data grids can update themselves.
*/
emitChanged(args) {
this._changed.emit(args);
}
}
/**
* An object which provides the mutable data for a data grid.
*
* #### Notes
* This object is an extension to `DataModel` and it only adds ability to
* change data for cells.
*/
class MutableDataModel extends DataModel {
}
/**
* The namespace for the `DataModel` class statics.
*/
(function (DataModel) {
/**
* A singleton empty metadata object.
*/
DataModel.emptyMetadata = Object.freeze({});
})(DataModel || (DataModel = {}));
/**
* A thin caching wrapper around a 2D canvas rendering context.
*
* #### Notes
* This class is mostly a transparent wrapper around a canvas rendering
* context which improves performance when writing context state.
*
* For best performance, avoid reading state from the `gc`. Writes are
* cached based on the previously written value.
*
* Unless otherwise specified, the API and semantics of this class are
* identical to the builtin 2D canvas rendering context:
* https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
*
* The wrapped canvas context should not be manipulated externally
* until the wrapping `GraphicsContext` object is disposed.
*/
class GraphicsContext {
/**
* Create a new graphics context object.
*
* @param context - The 2D canvas rendering context to wrap.
*/
constructor(context) {
this._disposed = false;
this._context = context;
this._state = Private$3.State.create(context);
}
dispose() {
// Bail if the gc is already disposed.
if (this._disposed) {
return;
}
// Mark the gc as disposed.
this._disposed = true;
// Pop any unrestored saves.
while (this._state.next) {
this._state = this._state.next;
this._context.restore();
}
}
get isDisposed() {
return this._disposed;
}
get fillStyle() {
return this._context.fillStyle;
}
set fillStyle(value) {
if (this._state.fillStyle !== value) {
this._state.fillStyle = value;
this._context.fillStyle = value;
}
}
get strokeStyle() {
return this._context.strokeStyle;
}
set strokeStyle(value) {
if (this._state.strokeStyle !== value) {
this._state.strokeStyle = value;
this._context.strokeStyle = value;
}
}
get font() {
return this._context.font;
}
set font(value) {
if (this._state.font !== value) {
this._state.font = value;
this._context.font = value;
}
}
get textAlign() {
return this._context.textAlign;
}
set textAlign(value) {
if (this._state.textAlign !== value) {
this._state.textAlign = value;
this._context.textAlign = value;
}
}
get textBaseline() {
return this._context.textBaseline;
}
set textBaseline(value) {
if (this._state.textBaseline !== value) {
this._state.textBaseline = value;
this._context.textBaseline = value;
}
}
get lineCap() {
return this._context.lineCap;
}
set lineCap(value) {
if (this._state.lineCap !== value) {
this._state.lineCap = value;
this._context.lineCap = value;
}
}
get lineDashOffset() {
return this._context.lineDashOffset;
}
set lineDashOffset(value) {
if (this._state.lineDashOffset !== value) {
this._state.lineDashOffset = value;
this._context.lineDashOffset = value;
}
}
get lineJoin() {
return this._context.lineJoin;
}
set lineJoin(value) {
if (this._state.lineJoin !== value) {
this._state.lineJoin = value;
this._context.lineJoin = value;
}
}
get lineWidth() {
return this._context.lineWidth;
}
set lineWidth(value) {
if (this._state.lineWidth !== value) {
this._state.lineWidth = value;
this._context.lineWidth = value;
}
}
get miterLimit() {
return this._context.miterLimit;
}
set miterLimit(value) {
if (this._state.miterLimit !== value) {
this._state.miterLimit = value;
this._context.miterLimit = value;
}
}
get shadowBlur() {
return this._context.shadowBlur;
}
set shadowBlur(value) {
if (this._state.shadowBlur !== value) {
this._state.shadowBlur = value;
this._context.shadowBlur = value;
}
}
get shadowColor() {
return this._context.shadowColor;
}
set shadowColor(value) {
if (this._state.shadowColor !== value) {
this._state.shadowColor = value;
this._context.shadowColor = value;
}
}
get shadowOffsetX() {
return this._context.shadowOffsetX;
}
set shadowOffsetX(value) {
if (this._state.shadowOffsetX !== value) {
this._state.shadowOffsetX = value;
this._context.shadowOffsetX = value;
}
}
get shadowOffsetY() {
return this._context.shadowOffsetY;
}
set shadowOffsetY(value) {
if (this._state.shadowOffsetY !== value) {
this._state.shadowOffsetY = value;
this._context.shadowOffsetY = value;
}
}
get imageSmoothingEnabled() {
return this._context.imageSmoothingEnabled;
}
set imageSmoothingEnabled(value) {
if (this._state.imageSmoothingEnabled !== value) {
this._state.imageSmoothingEnabled = value;
this._context.imageSmoothingEnabled = value;
}
}
get globalAlpha() {
return this._context.globalAlpha;
}
set globalAlpha(value) {
if (this._state.globalAlpha !== value) {
this._state.globalAlpha = value;
this._context.globalAlpha = value;
}
}
get globalCompositeOperation() {
return this._context.globalCompositeOperation;
}
set globalCompositeOperation(value) {
if (this._state.globalCompositeOperation !== value) {
this._state.globalCompositeOperation = value;
this._context.globalCompositeOperation = value;
}
}
getLineDash() {
return this._context.getLineDash();
}
setLineDash(segments) {
this._context.setLineDash(segments);
}
rotate(angle) {
this._context.rotate(angle);
}
scale(x, y) {
this._context.scale(x, y);
}
transform(m11, m12, m21, m22, dx, dy) {
this._context.transform(m11, m12, m21, m22, dx, dy);
}
translate(x, y) {
this._context.translate(x, y);
}
setTransform(m11, m12, m21, m22, dx, dy) {
this._context.setTransform(m11, m12, m21, m22, dx, dy);
}
save() {
// Clone an push the current state to the stack.
this._state = Private$3.State.push(this._state);
// Save the wrapped context state.
this._context.save();
}
restore() {
// Bail if there is no state to restore.
if (!this._state.next) {
return;
}
// Pop the saved state from the stack.
this._state = Private$3.State.pop(this._state);
// Restore the wrapped context state.
this._context.restore();
}
beginPath() {
return this._context.beginPath();
}
closePath() {
this._context.closePath();
}
isPointInPath(x, y, fillRule) {
let result;
if (arguments.length === 2) {
result = this._context.isPointInPath(x, y);
}
else {
result = this._context.isPointInPath(x, y, fillRule);
}
return result;
}
arc(x, y, radius, startAngle, endAngle, anticlockwise) {
if (arguments.length === 5) {
this._context.arc(x, y, radius, startAngle, endAngle);
}
else {
this._context.arc(x, y, radius, startAngle, endAngle, anticlockwise);
}
}
arcTo(x1, y1, x2, y2, radius) {
this._context.arcTo(x1, y1, x2, y2, radius);
}
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
this._context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
}
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise) {
if (arguments.length === 7) {
this._context.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle);
}
else {
this._context.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise);
}
}
lineTo(x, y) {
this._context.lineTo(x, y);
}
moveTo(x, y) {
this._context.moveTo(x, y);
}
quadraticCurveTo(cpx, cpy, x, y) {
this._context.quadraticCurveTo(cpx, cpy, x, y);
}
rect(x, y, w, h) {
this._context.rect(x, y, w, h);
}
clip(fillRule) {
if (arguments.length === 0) {
this._context.clip();
}
else {
this._context.clip(fillRule);
}
}
fill(fillRule) {
if (arguments.length === 0) {
this._context.fill();
}
else {
this._context.fill(fillRule);
}
}
stroke() {
this._context.stroke();
}
clearRect(x, y, w, h) {
return this._context.clearRect(x, y, w, h);
}
fillRect(x, y, w, h) {
this._context.fillRect(x, y, w, h);
}
fillText(text, x, y, maxWidth) {
if (arguments.length === 3) {
this._context.fillText(text, x, y);
}
else {
this._context.fillText(text, x, y, maxWidth);
}
}
strokeRect(x, y, w, h) {
this._context.strokeRect(x, y, w, h);
}
strokeText(text, x, y, maxWidth) {
if (arguments.length === 3) {
this._context.strokeText(text, x, y);
}
else {
this._context.strokeText(text, x, y, maxWidth);
}
}
measureText(text) {
return this._context.measureText(text);
}
createLinearGradient(x0, y0, x1, y1) {
return this._context.createLinearGradient(x0, y0, x1, y1);
}
createRadialGradient(x0, y0, r0, x1, y1, r1) {
return this._context.createRadialGradient(x0, y0, r0, x1, y1, r1);
}
createPattern(image, repetition) {
return this._context.createPattern(image, repetition);
}
createImageData() {
// eslint-disable-next-line prefer-spread, prefer-rest-params
return this._context.createImageData.apply(this._context, arguments);
}
getImageData(sx, sy, sw, sh) {
return this._context.getImageData(sx, sy, sw, sh);
}
putImageData() {
// eslint-disable-next-line prefer-spread, prefer-rest-params
this._context.putImageData.apply(this._context, arguments);
}
drawImage() {
// eslint-disable-next-line prefer-spread, prefer-rest-params
this._context.drawImage.apply(this._context, arguments);
}
drawFocusIfNeeded(element) {
this._context.drawFocusIfNeeded(element);
}
}
/**
* The namespace for the module implementation details.
*/
var Private$3;
(function (Private) {
/**
* The index of next valid pool object.
*/
let pi = -1;
/**
* A state object allocation pool.
*/
const pool = [];
/**
* An object which holds the state for a gc.
*/
class State {
/**
* Create a gc state object from a 2D canvas context.
*/
static create(context) {
let state = pi < 0 ? new State() : pool[pi--];
state.next = null;
state.fillStyle = context.fillStyle;
state.font = context.font;
state.globalAlpha = context.globalAlpha;
state.globalCompositeOperation = context.globalCompositeOperation;
state.imageSmoothingEnabled = context.imageSmoothingEnabled;
state.lineCap = context.lineCap;
state.lineDashOffset = context.lineDashOffset;
state.lineJoin = context.lineJoin;
state.lineWidth = context.lineWidth;
state.miterLimit = context.miterLimit;
state.shadowBlur = context.shadowBlur;
state.shadowColor = context.shadowColor;
state.shadowOffsetX = context.shadowOffsetX;
state.shadowOffsetY = context.shadowOffsetY;
state.strokeStyle = context.strokeStyle;
state.textAlign = context.textAlign;
state.textBaseline = context.textBaseline;
return state;
}
/**
* Clone an existing gc state object and add it to the state stack.
*/
static push(other) {
let state = pi < 0 ? new State() : pool[pi--];
state.next = other;
state.fillStyle = other.fillStyle;
state.font = other.font;
state.globalAlpha = other.globalAlpha;
state.globalCompositeOperation = other.globalCompositeOperation;
state.imageSmoothingEnabled = other.imageSmoothingEnabled;
state.lineCap = other.lineCap;
state.lineDashOffset = other.lineDashOffset;
state.lineJoin = other.lineJoin;
state.lineWidth = other.lineWidth;
state.miterLimit = other.miterLimit;
state.shadowBlur = other.shadowBlur;
state.shadowColor = other.shadowColor;
state.shadowOffsetX = other.shadowOffsetX;
state.shadowOffsetY = other.shadowOffsetY;
state.strokeStyle = other.strokeStyle;
state.textAlign = other.textAlign;
state.textBaseline = other.textBaseline;
return state;
}
/**
* Pop the next state object and return the current to the pool
*/
static pop(state) {
state.fillStyle = '';
state.strokeStyle = '';
pool[++pi] = state;
return state.next;
}
}
Private.State = State;
})(Private$3 || (Private$3 = {}));
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A class which manages the mapping of cell renderers.
*/
class RendererMap {
/**
* Construct a new renderer map.
*
* @param values - The initial values for the map.
*
* @param fallback - The renderer of last resort.
*/
constructor(values = {}, fallback) {
this._changed = new _lumino_signaling__WEBPACK_IMPORTED_MODULE_4__.Signal(this);
this._values = { ...values };
this._fallback = fallback || new TextRenderer();
}
/**
* A signal emitted when the renderer map has changed.
*/
get changed() {
return this._changed;
}
/**
* Get the cell renderer to use for the given cell config.
*
* @param config - The cell config of interest.
*
* @returns The renderer to use for the cell.
*/
get(config) {
// Fetch the renderer from the values map.
let renderer = this._values[config.region];
// Execute a resolver function if necessary.
if (typeof renderer === 'function') {
try {
renderer = renderer(config);
}
catch (err) {
renderer = undefined;
console.error(err);
}
}
// Return the renderer or the fallback.
return renderer || this._fallback;
}
/**
* Update the renderer map with new values
*
* @param values - The updated values for the map.
*
* @param fallback - The renderer of last resort.
*
* #### Notes
* This method always emits the `changed` signal.
*/
update(values = {}, fallback) {
this._values = { ...this._values, ...values };
this._fallback = fallback || this._fallback;
this._changed.emit(undefined);
}
}
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* An object which manages a collection of variable sized sections.
*
* #### Notes
* This class is an implementation detail. It is designed to manage
* the variable row and column sizes for a data grid. User code will
* not interact with this class directly.
*/
class SectionList {
/**
* Construct a new section list.
*
* @param options - The options for initializing the list.
*/
constructor(options) {
this._count = 0;
this._length = 0;
this._sections = [];
this._minimumSize = options.minimumSize || 2;
this._defaultSize = Math.max(this._minimumSize, Math.floor(options.defaultSize));
}
/**
* The total size of all sections in the list.
*
* #### Complexity
* Constant.
*/
get length() {
return this._length;
}
/**
* The total number of sections in the list.
*
* #### Complexity
* Constant.
*/
get count() {
return this._count;
}
/**
* Get the minimum size of sections in the list.
*
* #### Complexity
* Constant.
*/
get minimumSize() {
return this._minimumSize;
}
/**
* Set the minimum size of sections in the list.
*
* #### Complexity
* Linear on the number of resized sections.
*/
set minimumSize(value) {
// Normalize the value.
value = Math.max(2, Math.floor(value));
// Bail early if the value does not change.
if (this._minimumSize === value) {
return;
}
// Update the internal minimum size.
this._minimumSize = value;
// Update default size if larger than minimum size
if (value > this._defaultSize) {
this.defaultSize = value;
}
}
/**
* Get the default size of sections in the list.
*
* #### Complexity
* Constant.
*/
get defaultSize() {
return this._defaultSize;
}
/**
* Set the default size of sections in the list.
*
* #### Complexity
* Linear on the number of resized sections.
*/
set defaultSize(value) {
// Normalize the value.
value = Math.max(this._minimumSize, Math.floor(value));
// Bail early if the value does not change.
if (this._defaultSize === value) {
return;
}
// Compute the delta default size.
let delta = value - this._defaultSize;
// Update the internal default size.
this._defaultSize = value;
// Update the length.
this._length += delta * (this._count - this._sections.length);
// Bail early if there are no modified sections.
if (this._sections.length === 0) {
return;
}
// Recompute the offsets of the modified sections.
for (let i = 0, n = this._sections.length; i < n; ++i) {
// Look up the previous and current modified sections.
let prev = this._sections[i - 1];
let curr = this._sections[i];
// Adjust the offset for the current section.
if (prev) {
let count = curr.index - prev.index - 1;
curr.offset = prev.offset + prev.size + count * value;
}
else {
curr.offset = curr.index * value;
}
}
}
/**
* Clamp a size to the minimum section size
*
* @param size - The size to clamp.
*
* @returns The size or the section minimum size, whichever is larger
*/
clampSize(size) {
return Math.max(this._minimumSize, Math.floor(size));
}
/**
* Find the index of the section which covers the given offset.
*
* @param offset - The offset of the section of interest.
*
* @returns The index of the section which covers the given offset,
* or `-1` if the offset is out of range.
*
* #### Complexity
* Logarithmic on the number of resized sections.
*/
indexOf(offset) {
// Bail early if the offset is out of range.
if (offset < 0 || offset >= this._length || this._count === 0) {
return -1;
}
// Handle the simple case of no modified sections.
if (this._sections.length === 0) {
return Math.floor(offset / this._defaultSize);
}
// Find the modified section for the given offset.
let i = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, offset, Private$2.offsetCmp);
// Return the index of an exact match.
if (i < this._sections.length && this._sections[i].offset <= offset) {
return this._sections[i].index;
}
// Handle the case of no modified sections before the offset.
if (i === 0) {
return Math.floor(offset / this._defaultSize);
}
// Compute the index from the previous modified section.
let section = this._sections[i - 1];
let span = offset - (section.offset + section.size);
return section.index + Math.floor(span / this._defaultSize) + 1;
}
/**
* Find the offset of the section at the given index.
*
* @param index - The index of the section of interest.
*
* @returns The offset of the section at the given index, or `-1`
* if the index is out of range.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*
* #### Complexity
* Logarithmic on the number of resized sections.
*/
offsetOf(index) {
// Bail early if the index is out of range.
if (index < 0 || index >= this._count) {
return -1;
}
// Handle the simple case of no modified sections.
if (this._sections.length === 0) {
return index * this._defaultSize;
}
// Find the modified section for the given index.
let i = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, index, Private$2.indexCmp);
// Return the offset of an exact match.
if (i < this._sections.length && this._sections[i].index === index) {
return this._sections[i].offset;
}
// Handle the case of no modified sections before the index.
if (i === 0) {
return index * this._defaultSize;
}
// Compute the offset from the previous modified section.
let section = this._sections[i - 1];
let span = index - section.index - 1;
return section.offset + section.size + span * this._defaultSize;
}
/**
* Find the extent of the section at the given index.
*
* @param index - The index of the section of interest.
*
* @returns The extent of the section at the given index, or `-1`
* if the index is out of range.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*
* #### Complexity
* Logarithmic on the number of resized sections.
*/
extentOf(index) {
// Bail early if the index is out of range.
if (index < 0 || index >= this._count) {
return -1;
}
// Handle the simple case of no modified sections.
if (this._sections.length === 0) {
return (index + 1) * this._defaultSize - 1;
}
// Find the modified section for the given index.
let i = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, index, Private$2.indexCmp);
// Return the offset of an exact match.
if (i < this._sections.length && this._sections[i].index === index) {
return this._sections[i].offset + this._sections[i].size - 1;
}
// Handle the case of no modified sections before the index.
if (i === 0) {
return (index + 1) * this._defaultSize - 1;
}
// Compute the offset from the previous modified section.
let section = this._sections[i - 1];
let span = index - section.index;
return section.offset + section.size + span * this._defaultSize - 1;
}
/**
* Find the size of the section at the given index.
*
* @param index - The index of the section of interest.
*
* @returns The size of the section at the given index, or `-1`
* if the index is out of range.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*
* #### Complexity
* Logarithmic on the number of resized sections.
*/
sizeOf(index) {
// Bail early if the index is out of range.
if (index < 0 || index >= this._count) {
return -1;
}
// Handle the simple case of no modified sections.
if (this._sections.length === 0) {
return this._defaultSize;
}
// Find the modified section for the given index.
let i = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, index, Private$2.indexCmp);
// Return the size of an exact match.
if (i < this._sections.length && this._sections[i].index === index) {
return this._sections[i].size;
}
// Return the default size for all other cases.
return this._defaultSize;
}
/**
* Resize a section in the list.
*
* @param index - The index of the section to resize. This method
* is a no-op if this value is out of range.
*
* @param size - The new size of the section. This value will be
* clamped to an integer `>= 0`.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*
* #### Complexity
* Linear on the number of resized sections.
*/
resize(index, size) {
// Bail early if the index is out of range.
if (index < 0 || index >= this._count) {
return;
}
// Clamp the size to an integer >= minimum size.
size = Math.max(this._minimumSize, Math.floor(size));
// Find the modified section for the given index.
let i = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, index, Private$2.indexCmp);
// Update or create the modified section as needed.
let delta;
if (i < this._sections.length && this._sections[i].index === index) {
let section = this._sections[i];
delta = size - section.size;
section.size = size;
}
else if (i === 0) {
let offset = index * this._defaultSize;
_lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.insert(this._sections, i, { index, offset, size });
delta = size - this._defaultSize;
}
else {
let section = this._sections[i - 1];
let span = index - section.index - 1;
let offset = section.offset + section.size + span * this._defaultSize;
_lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.insert(this._sections, i, { index, offset, size });
delta = size - this._defaultSize;
}
// Adjust the length.
this._length += delta;
// Update all modified sections after the resized section.
for (let j = i + 1, n = this._sections.length; j < n; ++j) {
this._sections[j].offset += delta;
}
}
/**
* Insert sections into the list.
*
* @param index - The index at which to insert the sections. This
* value will be clamped to the bounds of the list.
*
* @param count - The number of sections to insert. This method
* is a no-op if this value is `<= 0`.
*
* #### Undefined Behavior
* An `index` or `count` which is non-integral.
*
* #### Complexity
* Linear on the number of resized sections.
*/
insert(index, count) {
// Bail early if there are no sections to insert.
if (count <= 0) {
return;
}
// Clamp the index to the bounds of the list.
index = Math.max(0, Math.min(index, this._count));
// Add the new sections to the totals.
let span = count * this._defaultSize;
this._count += count;
this._length += span;
// Bail early if there are no modified sections to update.
if (this._sections.length === 0) {
return;
}
// Find the modified section for the given index.
let i = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, index, Private$2.indexCmp);
// Update all modified sections after the insert location.
for (let n = this._sections.length; i < n; ++i) {
let section = this._sections[i];
section.index += count;
section.offset += span;
}
}
/**
* Remove sections from the list.
*
* @param index - The index of the first section to remove. This
* method is a no-op if this value is out of range.
*
* @param count - The number of sections to remove. This method
* is a no-op if this value is `<= 0`.
*
* #### Undefined Behavior
* An `index` or `count` which is non-integral.
*
* #### Complexity
* Linear on the number of resized sections.
*/
remove(index, count) {
// Bail early if there is nothing to remove.
if (index < 0 || index >= this._count || count <= 0) {
return;
}
// Clamp the count to the bounds of the list.
count = Math.min(this._count - index, count);
// Handle the simple case of no modified sections to update.
if (this._sections.length === 0) {
this._count -= count;
this._length -= count * this._defaultSize;
return;
}
// Handle the simple case of removing all sections.
if (count === this._count) {
this._length = 0;
this._count = 0;
this._sections.length = 0;
return;
}
// Find the modified section for the start index.
let i = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, index, Private$2.indexCmp);
// Find the modified section for the end index.
let j = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, index + count, Private$2.indexCmp);
// Remove the relevant modified sections.
let removed = this._sections.splice(i, j - i);
// Compute the total removed span.
let span = (count - removed.length) * this._defaultSize;
for (let k = 0, n = removed.length; k < n; ++k) {
span += removed[k].size;
}
// Adjust the totals.
this._count -= count;
this._length -= span;
// Update all modified sections after the removed span.
for (let k = i, n = this._sections.length; k < n; ++k) {
let section = this._sections[k];
section.index -= count;
section.offset -= span;
}
}
/**
* Move sections within the list.
*
* @param index - The index of the first section to move. This method
* is a no-op if this value is out of range.
*
* @param count - The number of sections to move. This method is a
* no-op if this value is `<= 0`.
*
* @param destination - The destination index for the first section.
* This value will be clamped to the allowable range.
*
* #### Undefined Behavior
* An `index`, `count`, or `destination` which is non-integral.
*
* #### Complexity
* Linear on the number of moved resized sections.
*/
move(index, count, destination) {
// Bail early if there is nothing to move.
if (index < 0 || index >= this._count || count <= 0) {
return;
}
// Handle the simple case of no modified sections.
if (this._sections.length === 0) {
return;
}
// Clamp the move count to the limit.
count = Math.min(count, this._count - index);
// Clamp the destination index to the limit.
destination = Math.min(Math.max(0, destination), this._count - count);
// Bail early if there is no effective move.
if (index === destination) {
return;
}
// Compute the first affected index.
let i1 = Math.min(index, destination);
// Look up the first affected modified section.
let k1 = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, i1, Private$2.indexCmp);
// Bail early if there are no affected modified sections.
if (k1 === this._sections.length) {
return;
}
// Compute the last affected index.
let i2 = Math.max(index + count - 1, destination + count - 1);
// Look up the last affected modified section.
let k2 = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.upperBound(this._sections, i2, Private$2.indexCmp) - 1;
// Bail early if there are no affected modified sections.
if (k2 < k1) {
return;
}
// Compute the pivot index.
let pivot = destination < index ? index : index + count;
// Compute the count for each side of the pivot.
let count1 = pivot - i1;
let count2 = i2 - pivot + 1;
// Compute the span for each side of the pivot.
let span1 = count1 * this._defaultSize;
let span2 = count2 * this._defaultSize;
// Adjust the spans for the modified sections.
for (let j = k1; j <= k2; ++j) {
let section = this._sections[j];
if (section.index < pivot) {
span1 += section.size - this._defaultSize;
}
else {
span2 += section.size - this._defaultSize;
}
}
// Look up the pivot section.
let k3 = _lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.lowerBound(this._sections, pivot, Private$2.indexCmp);
// Rotate the modified sections if needed.
if (k1 <= k3 && k3 <= k2) {
_lumino_algorithm__WEBPACK_IMPORTED_MODULE_3__.ArrayExt.rotate(this._sections, k3 - k1, k1, k2);
}
// Adjust the modified section indices and offsets.
for (let j = k1; j <= k2; ++j) {
let section = this._sections[j];
if (section.index < pivot) {
section.index += count2;
section.offset += span2;
}
else {
section.index -= count1;
section.offset -= span1;
}
}
}
/**
* Reset all modified sections to the default size.
*
* #### Complexity
* Constant.
*/
reset() {
this._sections.length = 0;
this._length = this._count * this._defaultSize;
}
/**
* Remove all sections from the list.
*
* #### Complexity
* Constant.
*/
clear() {
this._count = 0;
this._length = 0;
this._sections.length = 0;
}
}
/**
* The namespace for the module implementation details.
*/
var Private$2;
(function (Private) {
/**
* A comparison function for searching by offset.
*/
function offsetCmp(section, offset) {
if (offset < section.offset) {
return 1;
}
if (section.offset + section.size <= offset) {
return -1;
}
return 0;
}
Private.offsetCmp = offsetCmp;
/**
* A comparison function for searching by index.
*/
function indexCmp(section, index) {
return section.index - index;
}
Private.indexCmp = indexCmp;
})(Private$2 || (Private$2 = {}));
/**
* A widget which implements a high-performance tabular data grid.
*
* #### Notes
* A data grid is implemented as a composition of child widgets. These
* child widgets are considered an implementation detail. Manipulating
* the child widgets of a data grid directly is undefined behavior.
*
* This class is not designed to be subclassed.
*
* See also the related [example](../../examples/datagrid/index.html) and
* its [source](https://github.com/jupyterlab/lumino/tree/main/examples/example-datagrid).
*/
class DataGrid extends _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.Widget {
/**
* Construct a new data grid.
*
* @param options - The options for initializing the data grid.
*/
constructor(options = {}) {
super();
this._scrollX = 0;
this._scrollY = 0;
this._viewportWidth = 0;
this._viewportHeight = 0;
this._mousedown = false;
this._keyHandler = null;
this._mouseHandler = null;
this._vScrollBarMinWidth = 0;
this._hScrollBarMinHeight = 0;
this._dpiRatio = Math.ceil(window.devicePixelRatio);
this._dataModel = null;
this._selectionModel = null;
this._editingEnabled = false;
this.addClass('lm-DataGrid');
// Parse the simple options.
this._style = options.style || DataGrid.defaultStyle;
this._stretchLastRow = options.stretchLastRow || false;
this._stretchLastColumn = options.stretchLastColumn || false;
this._headerVisibility = options.headerVisibility || 'all';
this._cellRenderers = options.cellRenderers || new RendererMap();
this._copyConfig = options.copyConfig || DataGrid.defaultCopyConfig;
// Connect to the renderer map changed signal.
this._cellRenderers.changed.connect(this._onRenderersChanged, this);
// Parse the default sizes.
let defaultSizes = options.defaultSizes || DataGrid.defaultSizes;
let minimumSizes = options.minimumSizes || DataGrid.minimumSizes;
// Set up the sections lists.
this._rowSections = new SectionList({
defaultSize: defaultSizes.rowHeight,
minimumSize: minimumSizes.rowHeight
});
this._columnSections = new SectionList({
defaultSize: defaultSizes.columnWidth,
minimumSize: minimumSizes.columnWidth
});
this._rowHeaderSections = new SectionList({
defaultSize: defaultSizes.rowHeaderWidth,
minimumSize: minimumSizes.rowHeaderWidth
});
this._columnHeaderSections = new SectionList({
defaultSize: defaultSizes.columnHeaderHeight,
minimumSize: minimumSizes.columnHeaderHeight
});
// Create the canvas, buffer, and overlay objects.
this._canvas = Private$1.createCanvas();
this._buffer = Private$1.createCanvas();
this._overlay = Private$1.createCanvas();
// Get the graphics contexts for the canvases.
this._canvasGC = this._canvas.getContext('2d');
this._bufferGC = this._buffer.getContext('2d');
this._overlayGC = this._overlay.getContext('2d');
// Set up the on-screen canvas.
this._canvas.style.position = 'absolute';
this._canvas.style.top = '0px';
this._canvas.style.left = '0px';
this._canvas.style.width = '0px';
this._canvas.style.height = '0px';
// Set up the on-screen overlay.
this._overlay.style.position = 'absolute';
this._overlay.style.top = '0px';
this._overlay.style.left = '0px';
this._overlay.style.width = '0px';
this._overlay.style.height = '0px';
// Create the internal widgets for the data grid.
this._viewport = new _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.Widget();
this._viewport.node.tabIndex = -1;
this._viewport.node.style.outline = 'none';
this._vScrollBar = new _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.ScrollBar({ orientation: 'vertical' });
this._hScrollBar = new _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.ScrollBar({ orientation: 'horizontal' });
this._scrollCorner = new _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.Widget();
this._editorController = new CellEditorController();
// Add the extra class names to the child widgets.
this._viewport.addClass('lm-DataGrid-viewport');
this._vScrollBar.addClass('lm-DataGrid-scrollBar');
this._hScrollBar.addClass('lm-DataGrid-scrollBar');
this._scrollCorner.addClass('lm-DataGrid-scrollCorner');
// Add the on-screen canvas to the viewport node.
this._viewport.node.appendChild(this._canvas);
// Add the on-screen overlay to the viewport node.
this._viewport.node.appendChild(this._overlay);
// Install the message hooks.
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.installMessageHook(this._viewport, this);
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.installMessageHook(this._hScrollBar, this);
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.installMessageHook(this._vScrollBar, this);
// Hide the scroll bars and corner from the outset.
this._vScrollBar.hide();
this._hScrollBar.hide();
this._scrollCorner.hide();
// Connect to the scroll bar signals.
this._vScrollBar.thumbMoved.connect(this._onThumbMoved, this);
this._hScrollBar.thumbMoved.connect(this._onThumbMoved, this);
this._vScrollBar.pageRequested.connect(this._onPageRequested, this);
this._hScrollBar.pageRequested.connect(this._onPageRequested, this);
this._vScrollBar.stepRequested.connect(this._onStepRequested, this);
this._hScrollBar.stepRequested.connect(this._onStepRequested, this);
// Set the layout cell config for the child widgets.
_lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.GridLayout.setCellConfig(this._viewport, { row: 0, column: 0 });
_lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.GridLayout.setCellConfig(this._vScrollBar, { row: 0, column: 1 });
_lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.GridLayout.setCellConfig(this._hScrollBar, { row: 1, column: 0 });
_lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.GridLayout.setCellConfig(this._scrollCorner, { row: 1, column: 1 });
// Create the layout for the data grid.
let layout = new _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.GridLayout({
rowCount: 2,
columnCount: 2,
rowSpacing: 0,
columnSpacing: 0,
fitPolicy: 'set-no-constraint'
});
// Set the stretch factors for the grid.
layout.setRowStretch(0, 1);
layout.setRowStretch(1, 0);
layout.setColumnStretch(0, 1);
layout.setColumnStretch(1, 0);
// Add the child widgets to the layout.
layout.addWidget(this._viewport);
layout.addWidget(this._vScrollBar);
layout.addWidget(this._hScrollBar);
layout.addWidget(this._scrollCorner);
// Install the layout on the data grid.
this.layout = layout;
}
/**
* Dispose of the resources held by the widgets.
*/
dispose() {
// Release the mouse.
this._releaseMouse();
// Dispose of the handlers.
if (this._keyHandler) {
this._keyHandler.dispose();
}
if (this._mouseHandler) {
this._mouseHandler.dispose();
}
this._keyHandler = null;
this._mouseHandler = null;
// Clear the models.
this._dataModel = null;
this._selectionModel = null;
// Clear the section lists.
this._rowSections.clear();
this._columnSections.clear();
this._rowHeaderSections.clear();
this._columnHeaderSections.clear();
// Dispose of the base class.
super.dispose();
}
/**
* Get the data model for the data grid.
*/
get dataModel() {
return this._dataModel;
}
/**
* Set the data model for the data grid.
*
* #### Notes
* This will automatically remove the current selection model.
*/
set dataModel(value) {
// Do nothing if the model does not change.
if (this._dataModel === value) {
return;
}
// Release the mouse.
this._releaseMouse();
// Clear the selection model.
this.selectionModel = null;
// Disconnect the change handler from the old model.
if (this._dataModel) {
this._dataModel.changed.disconnect(this._onDataModelChanged, this);
}
// Connect the change handler for the new model.
if (value) {
value.changed.connect(this._onDataModelChanged, this);
}
// Update the internal model reference.
this._dataModel = value;
// Clear the section lists.
this._rowSections.clear();
this._columnSections.clear();
this._rowHeaderSections.clear();
this._columnHeaderSections.clear();
// Populate the section lists.
if (value) {
this._rowSections.insert(0, value.rowCount('body'));
this._columnSections.insert(0, value.columnCount('body'));
this._rowHeaderSections.insert(0, value.columnCount('row-header'));
this._columnHeaderSections.insert(0, value.rowCount('column-header'));
}
// Reset the scroll position.
this._scrollX = 0;
this._scrollY = 0;
// Sync the viewport.
this._syncViewport();
}
/**
* Get the selection model for the data grid.
*/
get selectionModel() {
return this._selectionModel;
}
/**
* Set the selection model for the data grid.
*/
set selectionModel(value) {
// Do nothing if the selection model does not change.
if (this._selectionModel === value) {
return;
}
// Release the mouse.
this._releaseMouse();
// Ensure the data models are a match.
if (value && value.dataModel !== this._dataModel) {
throw new Error('SelectionModel.dataModel !== DataGrid.dataModel');
}
// Disconnect the change handler from the old model.
if (this._selectionModel) {
this._selectionModel.changed.disconnect(this._onSelectionsChanged, this);
}
// Connect the change handler for the new model.
if (value) {
value.changed.connect(this._onSelectionsChanged, this);
}
// Update the internal selection model reference.
this._selectionModel = value;
// Schedule a repaint of the overlay.
this.repaintOverlay();
}
/**
* Get the key handler for the data grid.
*/
get keyHandler() {
return this._keyHandler;
}
/**
* Set the key handler for the data grid.
*/
set keyHandler(value) {
this._keyHandler = value;
}
/**
* Get the mouse handler for the data grid.
*/
get mouseHandler() {
return this._mouseHandler;
}
/**
* Set the mouse handler for the data grid.
*/
set mouseHandler(value) {
// Bail early if the mouse handler does not change.
if (this._mouseHandler === value) {
return;
}
// Release the mouse.
this._releaseMouse();
// Update the internal mouse handler.
this._mouseHandler = value;
}
/**
* Get the style for the data grid.
*/
get style() {
return this._style;
}
/**
* Set the style for the data grid.
*/
set style(value) {
// Bail if the style does not change.
if (this._style === value) {
return;
}
// Update the internal style.
this._style = { ...value };
// Schedule a repaint of the content.
this.repaintContent();
// Schedule a repaint of the overlay.
this.repaintOverlay();
}
/**
* Get the cell renderer map for the data grid.
*/
get cellRenderers() {
return this._cellRenderers;
}
/**
* Set the cell renderer map for the data grid.
*/
set cellRenderers(value) {
// Bail if the renderer map does not change.
if (this._cellRenderers === value) {
return;
}
// Disconnect the old map.
this._cellRenderers.changed.disconnect(this._onRenderersChanged, this);
// Connect the new map.
value.changed.connect(this._onRenderersChanged, this);
// Update the internal renderer map.
this._cellRenderers = value;
// Schedule a repaint of the grid content.
this.repaintContent();
}
/**
* Get the header visibility for the data grid.
*/
get headerVisibility() {
return this._headerVisibility;
}
/**
* Set the header visibility for the data grid.
*/
set headerVisibility(value) {
// Bail if the visibility does not change.
if (this._headerVisibility === value) {
return;
}
// Update the internal visibility.
this._headerVisibility = value;
// Sync the viewport.
this._syncViewport();
}
/**
* Get the default sizes for the various sections of the data grid.
*/
get defaultSizes() {
let rowHeight = this._rowSections.defaultSize;
let columnWidth = this._columnSections.defaultSize;
let rowHeaderWidth = this._rowHeaderSections.defaultSize;
let columnHeaderHeight = this._columnHeaderSections.defaultSize;
return { rowHeight, columnWidth, rowHeaderWidth, columnHeaderHeight };
}
/**
* Set the default sizes for the various sections of the data grid.
*/
set defaultSizes(value) {
// Update the section default sizes.
this._rowSections.defaultSize = value.rowHeight;
this._columnSections.defaultSize = value.columnWidth;
this._rowHeaderSections.defaultSize = value.rowHeaderWidth;
this._columnHeaderSections.defaultSize = value.columnHeaderHeight;
// Sync the viewport.
this._syncViewport();
}
/**
* Get the minimum sizes for the various sections of the data grid.
*/
get minimumSizes() {
let rowHeight = this._rowSections.minimumSize;
let columnWidth = this._columnSections.minimumSize;
let rowHeaderWidth = this._rowHeaderSections.minimumSize;
let columnHeaderHeight = this._columnHeaderSections.minimumSize;
return { rowHeight, columnWidth, rowHeaderWidth, columnHeaderHeight };
}
/**
* Set the minimum sizes for the various sections of the data grid.
*/
set minimumSizes(value) {
// Update the section default sizes.
this._rowSections.minimumSize = value.rowHeight;
this._columnSections.minimumSize = value.columnWidth;
this._rowHeaderSections.minimumSize = value.rowHeaderWidth;
this._columnHeaderSections.minimumSize = value.columnHeaderHeight;
// Sync the viewport.
this._syncViewport();
}
/**
* Get the copy configuration for the data grid.
*/
get copyConfig() {
return this._copyConfig;
}
/**
* Set the copy configuration for the data grid.
*/
set copyConfig(value) {
this._copyConfig = value;
}
/**
* Get whether the last row is stretched.
*/
get stretchLastRow() {
return this._stretchLastRow;
}
/**
* Set whether the last row is stretched.
*/
set stretchLastRow(value) {
// Bail early if the value does not change.
if (value === this._stretchLastRow) {
return;
}
// Update the internal value.
this._stretchLastRow = value;
// Sync the viewport
this._syncViewport();
}
/**
* Get whether the last column is stretched.
*/
get stretchLastColumn() {
return this._stretchLastColumn;
}
/**
* Set whether the last column is stretched.
*/
set stretchLastColumn(value) {
// Bail early if the value does not change.
if (value === this._stretchLastColumn) {
return;
}
// Update the internal value.
this._stretchLastColumn = value;
// Sync the viewport
this._syncViewport();
}
/**
* The virtual width of the row headers.
*/
get headerWidth() {
if (this._headerVisibility === 'none') {
return 0;
}
if (this._headerVisibility === 'column') {
return 0;
}
return this._rowHeaderSections.length;
}
/**
* The virtual height of the column headers.
*/
get headerHeight() {
if (this._headerVisibility === 'none') {
return 0;
}
if (this._headerVisibility === 'row') {
return 0;
}
return this._columnHeaderSections.length;
}
/**
* The virtual width of the grid body.
*
* #### Notes
* This does *not* account for a stretched last column.
*/
get bodyWidth() {
return this._columnSections.length;
}
/**
* The virtual height of the grid body.
*
* #### Notes
* This does *not* account for a stretched last row.
*/
get bodyHeight() {
return this._rowSections.length;
}
/**
* The virtual width of the entire grid.
*
* #### Notes
* This does *not* account for a stretched last column.
*/
get totalWidth() {
return this.headerWidth + this.bodyWidth;
}
/**
* The virtual height of the entire grid.
*
* #### Notes
* This does *not* account for a stretched last row.
*/
get totalHeight() {
return this.headerHeight + this.bodyHeight;
}
/**
* The actual width of the viewport.
*/
get viewportWidth() {
return this._viewportWidth;
}
/**
* The actual height of the viewport.
*/
get viewportHeight() {
return this._viewportHeight;
}
/**
* The width of the visible portion of the grid body.
*/
get pageWidth() {
return Math.max(0, this.viewportWidth - this.headerWidth);
}
/**
* The height of the visible portion of the grid body.
*/
get pageHeight() {
return Math.max(0, this.viewportHeight - this.headerHeight);
}
/**
* The current scroll X position of the viewport.
*/
get scrollX() {
return this._hScrollBar.value;
}
/**
* The current scroll Y position of the viewport.
*/
get scrollY() {
return this._vScrollBar.value;
}
/**
* The maximum scroll X position for the grid.
*/
get maxScrollX() {
return Math.max(0, this.bodyWidth - this.pageWidth - 1);
}
/**
* The maximum scroll Y position for the grid.
*/
get maxScrollY() {
return Math.max(0, this.bodyHeight - this.pageHeight - 1);
}
/**
* The viewport widget for the data grid.
*/
get viewport() {
return this._viewport;
}
/**
* The cell editor controller object for the data grid.
*/
get editorController() {
return this._editorController;
}
set editorController(controller) {
this._editorController = controller;
}
/**
* Whether the cell editing is enabled for the data grid.
*/
get editingEnabled() {
return this._editingEnabled;
}
set editingEnabled(enabled) {
this._editingEnabled = enabled;
}
/**
* Whether the grid cells are editable.
*
* `editingEnabled` flag must be on and grid must have required
* selection model, editor controller and data model properties.
*/
get editable() {
return (this._editingEnabled &&
this._selectionModel !== null &&
this._editorController !== null &&
this.dataModel instanceof MutableDataModel);
}
/**
* The rendering context for painting the data grid.
*/
get canvasGC() {
return this._canvasGC;
}
/**
* The row sections of the data grid.
*/
get rowSections() {
return this._rowSections;
}
/**
* The column sections of the data grid.
*/
get columnSections() {
return this._columnSections;
}
/**
* The row header sections of the data grid.
*/
get rowHeaderSections() {
return this._rowHeaderSections;
}
/**
* The column header sections of the data grid.
*/
get columnHeaderSections() {
return this._columnHeaderSections;
}
/**
* Scroll the grid to the specified row.
*
* @param row - The row index of the cell.
*
* #### Notes
* This is a no-op if the row is already visible.
*/
scrollToRow(row) {
// Fetch the row count.
let nr = this._rowSections.count;
// Bail early if there is no content.
if (nr === 0) {
return;
}
// Floor the row index.
row = Math.floor(row);
// Clamp the row index.
row = Math.max(0, Math.min(row, nr - 1));
// Get the virtual bounds of the row.
let y1 = this._rowSections.offsetOf(row);
let y2 = this._rowSections.extentOf(row);
// Get the virtual bounds of the viewport.
let vy1 = this._scrollY;
let vy2 = this._scrollY + this.pageHeight - 1;
// Set up the delta variables.
let dy = 0;
// Compute the delta Y scroll.
if (y1 < vy1) {
dy = y1 - vy1 - 10;
}
else if (y2 > vy2) {
dy = y2 - vy2 + 10;
}
// Bail early if no scroll is needed.
if (dy === 0) {
return;
}
// Scroll by the computed delta.
this.scrollBy(0, dy);
}
/**
* Scroll the grid to the specified column.
*
* @param column - The column index of the cell.
*
* #### Notes
* This is a no-op if the column is already visible.
*/
scrollToColumn(column) {
// Fetch the column count.
let nc = this._columnSections.count;
// Bail early if there is no content.
if (nc === 0) {
return;
}
// Floor the column index.
column = Math.floor(column);
// Clamp the column index.
column = Math.max(0, Math.min(column, nc - 1));
// Get the virtual bounds of the column.
let x1 = this._columnSections.offsetOf(column);
let x2 = this._columnSections.extentOf(column);
// Get the virtual bounds of the viewport.
let vx1 = this._scrollX;
let vx2 = this._scrollX + this.pageWidth - 1;
// Set up the delta variables.
let dx = 0;
// Compute the delta X scroll.
if (x1 < vx1) {
dx = x1 - vx1 - 10;
}
else if (x2 > vx2) {
dx = x2 - vx2 + 10;
}
// Bail early if no scroll is needed.
if (dx === 0) {
return;
}
// Scroll by the computed delta.
this.scrollBy(dx, 0);
}
/**
* Scroll the grid to the specified cell.
*
* @param row - The row index of the cell.
*
* @param column - The column index of the cell.
*
* #### Notes
* This is a no-op if the cell is already visible.
*/
scrollToCell(row, column) {
// Fetch the row and column count.
let nr = this._rowSections.count;
let nc = this._columnSections.count;
// Bail early if there is no content.
if (nr === 0 || nc === 0) {
return;
}
// Floor the cell index.
row = Math.floor(row);
column = Math.floor(column);
// Clamp the cell index.
row = Math.max(0, Math.min(row, nr - 1));
column = Math.max(0, Math.min(column, nc - 1));
// Get the virtual bounds of the cell.
let x1 = this._columnSections.offsetOf(column);
let x2 = this._columnSections.extentOf(column);
let y1 = this._rowSections.offsetOf(row);
let y2 = this._rowSections.extentOf(row);
// Get the virtual bounds of the viewport.
let vx1 = this._scrollX;
let vx2 = this._scrollX + this.pageWidth - 1;
let vy1 = this._scrollY;
let vy2 = this._scrollY + this.pageHeight - 1;
// Set up the delta variables.
let dx = 0;
let dy = 0;
// Compute the delta X scroll.
if (x1 < vx1) {
dx = x1 - vx1 - 10;
}
else if (x2 > vx2) {
dx = x2 - vx2 + 10;
}
// Compute the delta Y scroll.
if (y1 < vy1) {
dy = y1 - vy1 - 10;
}
else if (y2 > vy2) {
dy = y2 - vy2 + 10;
}
// Bail early if no scroll is needed.
if (dx === 0 && dy === 0) {
return;
}
// Scroll by the computed delta.
this.scrollBy(dx, dy);
}
/**
* Move cursor down/up/left/right while making sure it remains
* within the bounds of selected rectangles
*
* @param direction - The direction of the movement.
*/
moveCursor(direction) {
// Bail early if there is no selection
if (!this.dataModel ||
!this._selectionModel ||
this._selectionModel.isEmpty) {
return;
}
const iter = this._selectionModel.selections();
const onlyOne = iter.next() && !iter.next();
// if there is a single selection that is a single cell selection
// then move the selection and cursor within grid bounds
if (onlyOne) {
const currentSel = this._selectionModel.currentSelection();
if (currentSel.r1 === currentSel.r2 && currentSel.c1 === currentSel.c2) {
const dr = direction === 'down' ? 1 : direction === 'up' ? -1 : 0;
const dc = direction === 'right' ? 1 : direction === 'left' ? -1 : 0;
let newRow = currentSel.r1 + dr;
let newColumn = currentSel.c1 + dc;
const rowCount = this.dataModel.rowCount('body');
const columnCount = this.dataModel.columnCount('body');
if (newRow >= rowCount) {
newRow = 0;
newColumn += 1;
}
else if (newRow === -1) {
newRow = rowCount - 1;
newColumn -= 1;
}
if (newColumn >= columnCount) {
newColumn = 0;
newRow += 1;
if (newRow >= rowCount) {
newRow = 0;
}
}
else if (newColumn === -1) {
newColumn = columnCount - 1;
newRow -= 1;
if (newRow === -1) {
newRow = rowCount - 1;
}
}
this._selectionModel.select({
r1: newRow,
c1: newColumn,
r2: newRow,
c2: newColumn,
cursorRow: newRow,
cursorColumn: newColumn,
clear: 'all'
});
return;
}
}
// if there are multiple selections, move cursor
// within selection rectangles
this._selectionModel.moveCursorWithinSelections(direction);
}
/**
* Scroll the grid to the current cursor position.
*
* #### Notes
* This is a no-op if the cursor is already visible or
* if there is no selection model installed on the grid.
*/
scrollToCursor() {
// Bail early if there is no selection model.
if (!this._selectionModel) {
return;
}
// Fetch the cursor row and column.
let row = this._selectionModel.cursorRow;
let column = this._selectionModel.cursorColumn;
// Scroll to the cursor cell.
this.scrollToCell(row, column);
}
/**
* Scroll the viewport by the specified amount.
*
* @param dx - The X scroll amount.
*
* @param dy - The Y scroll amount.
*/
scrollBy(dx, dy) {
this.scrollTo(this.scrollX + dx, this.scrollY + dy);
}
/**
* Scroll the viewport by one page.
*
* @param dir - The desired direction of the scroll.
*/
scrollByPage(dir) {
let dx = 0;
let dy = 0;
switch (dir) {
case 'up':
dy = -this.pageHeight;
break;
case 'down':
dy = this.pageHeight;
break;
case 'left':
dx = -this.pageWidth;
break;
case 'right':
dx = this.pageWidth;
break;
default:
throw 'unreachable';
}
this.scrollTo(this.scrollX + dx, this.scrollY + dy);
}
/**
* Scroll the viewport by one cell-aligned step.
*
* @param dir - The desired direction of the scroll.
*/
scrollByStep(dir) {
let r;
let c;
let x = this.scrollX;
let y = this.scrollY;
let rows = this._rowSections;
let columns = this._columnSections;
switch (dir) {
case 'up':
r = rows.indexOf(y - 1);
y = r < 0 ? y : rows.offsetOf(r);
break;
case 'down':
r = rows.indexOf(y);
y = r < 0 ? y : rows.offsetOf(r) + rows.sizeOf(r);
break;
case 'left':
c = columns.indexOf(x - 1);
x = c < 0 ? x : columns.offsetOf(c);
break;
case 'right':
c = columns.indexOf(x);
x = c < 0 ? x : columns.offsetOf(c) + columns.sizeOf(c);
break;
default:
throw 'unreachable';
}
this.scrollTo(x, y);
}
/**
* Scroll to the specified offset position.
*
* @param x - The desired X position.
*
* @param y - The desired Y position.
*/
scrollTo(x, y) {
// Floor and clamp the position to the allowable range.
x = Math.max(0, Math.min(Math.floor(x), this.maxScrollX));
y = Math.max(0, Math.min(Math.floor(y), this.maxScrollY));
// Update the scroll bar values with the desired position.
this._hScrollBar.value = x;
this._vScrollBar.value = y;
// Post a scroll request message to the viewport.
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.postMessage(this._viewport, Private$1.ScrollRequest);
}
/**
* Get the row count for a particular region in the data grid.
*
* @param region - The row region of interest.
*
* @returns The row count for the specified region.
*/
rowCount(region) {
let count;
if (region === 'body') {
count = this._rowSections.count;
}
else {
count = this._columnHeaderSections.count;
}
return count;
}
/**
* Get the column count for a particular region in the data grid.
*
* @param region - The column region of interest.
*
* @returns The column count for the specified region.
*/
columnCount(region) {
let count;
if (region === 'body') {
count = this._columnSections.count;
}
else {
count = this._rowHeaderSections.count;
}
return count;
}
/**
* Get the row at a virtual offset in the data grid.
*
* @param region - The region which holds the row of interest.
*
* @param offset - The virtual offset of the row of interest.
*
* @returns The index of the row, or `-1` if the offset is out of range.
*
* #### Notes
* This method accounts for a stretched last row.
*/
rowAt(region, offset) {
// Bail early if the offset is negative.
if (offset < 0) {
return -1;
}
// Return early for the column header region.
if (region === 'column-header') {
return this._columnHeaderSections.indexOf(offset);
}
// Fetch the index.
let index = this._rowSections.indexOf(offset);
// Return early if the section is found.
if (index >= 0) {
return index;
}
// Bail early if the last row is not stretched.
if (!this._stretchLastRow) {
return -1;
}
// Fetch the geometry.
let bh = this.bodyHeight;
let ph = this.pageHeight;
// Bail early if no row stretching is required.
if (ph <= bh) {
return -1;
}
// Bail early if the offset is out of bounds.
if (offset >= ph) {
return -1;
}
// Otherwise, return the last row.
return this._rowSections.count - 1;
}
/**
* Get the column at a virtual offset in the data grid.
*
* @param region - The region which holds the column of interest.
*
* @param offset - The virtual offset of the column of interest.
*
* @returns The index of the column, or `-1` if the offset is out of range.
*
* #### Notes
* This method accounts for a stretched last column.
*/
columnAt(region, offset) {
if (offset < 0) {
return -1;
}
// Return early for the row header region.
if (region === 'row-header') {
return this._rowHeaderSections.indexOf(offset);
}
// Fetch the index.
let index = this._columnSections.indexOf(offset);
// Return early if the section is found.
if (index >= 0) {
return index;
}
// Bail early if the last column is not stretched.
if (!this._stretchLastColumn) {
return -1;
}
// Fetch the geometry.
let bw = this.bodyWidth;
let pw = this.pageWidth;
// Bail early if no column stretching is required.
if (pw <= bw) {
return -1;
}
// Bail early if the offset is out of bounds.
if (offset >= pw) {
return -1;
}
// Otherwise, return the last column.
return this._columnSections.count - 1;
}
/**
* Get the offset of a row in the data grid.
*
* @param region - The region which holds the row of interest.
*
* @param index - The index of the row of interest.
*
* @returns The offset of the row, or `-1` if the index is out of range.
*
* #### Notes
* A stretched last row has no effect on the return value.
*/
rowOffset(region, index) {
let offset;
if (region === 'body') {
offset = this._rowSections.offsetOf(index);
}
else {
offset = this._columnHeaderSections.offsetOf(index);
}
return offset;
}
/**
* Get the offset of a column in the data grid.
*
* @param region - The region which holds the column of interest.
*
* @param index - The index of the column of interest.
*
* @returns The offset of the column, or `-1` if the index is out of range.
*
* #### Notes
* A stretched last column has no effect on the return value.
*/
columnOffset(region, index) {
let offset;
if (region === 'body') {
offset = this._columnSections.offsetOf(index);
}
else {
offset = this._rowHeaderSections.offsetOf(index);
}
return offset;
}
/**
* Get the size of a row in the data grid.
*
* @param region - The region which holds the row of interest.
*
* @param index - The index of the row of interest.
*
* @returns The size of the row, or `-1` if the index is out of range.
*
* #### Notes
* This method accounts for a stretched last row.
*/
rowSize(region, index) {
// Return early for the column header region.
if (region === 'column-header') {
return this._columnHeaderSections.sizeOf(index);
}
// Fetch the row size.
let size = this._rowSections.sizeOf(index);
// Bail early if the index is out of bounds.
if (size < 0) {
return size;
}
// Return early if the last row is not stretched.
if (!this._stretchLastRow) {
return size;
}
// Return early if its not the last row.
if (index < this._rowSections.count - 1) {
return size;
}
// Fetch the geometry.
let bh = this.bodyHeight;
let ph = this.pageHeight;
// Return early if no stretching is needed.
if (ph <= bh) {
return size;
}
// Return the adjusted size.
return size + (ph - bh);
}
/**
* Get the size of a column in the data grid.
*
* @param region - The region which holds the column of interest.
*
* @param index - The index of the column of interest.
*
* @returns The size of the column, or `-1` if the index is out of range.
*
* #### Notes
* This method accounts for a stretched last column.
*/
columnSize(region, index) {
// Return early for the row header region.
if (region === 'row-header') {
return this._rowHeaderSections.sizeOf(index);
}
// Fetch the column size.
let size = this._columnSections.sizeOf(index);
// Bail early if the index is out of bounds.
if (size < 0) {
return size;
}
// Return early if the last column is not stretched.
if (!this._stretchLastColumn) {
return size;
}
// Return early if its not the last column.
if (index < this._columnSections.count - 1) {
return size;
}
// Fetch the geometry.
let bw = this.bodyWidth;
let pw = this.pageWidth;
// Return early if no stretching is needed.
if (pw <= bw) {
return size;
}
// Return the adjusted size.
return size + (pw - bw);
}
/**
* Resize a row in the data grid.
*
* @param region - The region which holds the row of interest.
*
* @param index - The index of the row of interest.
*
* @param size - The desired size of the row.
*/
resizeRow(region, index, size) {
let msg = new Private$1.RowResizeRequest(region, index, size);
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.postMessage(this._viewport, msg);
}
/**
* Resize a column in the data grid.
*
* @param region - The region which holds the column of interest.
*
* @param index - The index of the column of interest.
*
* @param size - The desired size of the column.
*/
resizeColumn(region, index, size) {
let msg = new Private$1.ColumnResizeRequest(region, index, size);
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.postMessage(this._viewport, msg);
}
/**
* Reset modified rows to their default size.
*
* @param region - The row region of interest.
*/
resetRows(region) {
switch (region) {
case 'all':
this._rowSections.reset();
this._columnHeaderSections.reset();
break;
case 'body':
this._rowSections.reset();
break;
case 'column-header':
this._columnHeaderSections.reset();
break;
default:
throw 'unreachable';
}
this.repaintContent();
this.repaintOverlay();
}
/**
* Reset modified columns to their default size.
*
* @param region - The column region of interest.
*/
resetColumns(region) {
switch (region) {
case 'all':
this._columnSections.reset();
this._rowHeaderSections.reset();
break;
case 'body':
this._columnSections.reset();
break;
case 'row-header':
this._rowHeaderSections.reset();
break;
default:
throw 'unreachable';
}
this.repaintContent();
this.repaintOverlay();
}
/**
* Auto sizes column-header widths based on their text content.
* @param area which area to resize: 'body', 'row-header' or 'all'.
* @param padding padding added to resized columns (pixels).
* @param numCols specify cap on the number of column resizes (optional).
*/
fitColumnNames(area = 'all', padding = 15, numCols) {
// Attempt resizing only if a data model is present.
if (this.dataModel) {
// Tracking remaining columns to be resized if numCols arg passed.
let colsRemaining = numCols === undefined || numCols < 0 ? undefined : numCols;
if (area === 'row-header' || area === 'all') {
// Respecting any column resize cap, if one has been passed.
if (colsRemaining !== undefined) {
const rowColumnCount = this.dataModel.columnCount('row-header');
/*
If we have more row-header columns than columns available
for resize, resize only remaining columns as per allowance
and set remaining resize allowance number to 0.
*/
if (colsRemaining - rowColumnCount < 0) {
this._fitRowColumnHeaders(this.dataModel, padding, colsRemaining);
colsRemaining = 0;
}
else {
/*
Otherwise the entire row-header column count can be resized.
Resize all row-header columns and subtract from remaining
column resize allowance.
*/
this._fitRowColumnHeaders(this.dataModel, padding, rowColumnCount);
colsRemaining = colsRemaining - rowColumnCount;
}
}
else {
// No column resize cap passed - resizing all columns.
this._fitRowColumnHeaders(this.dataModel, padding);
}
}
if (area === 'body' || area === 'all') {
// Respecting any column resize cap, if one has been passed.
if (colsRemaining !== undefined) {
const bodyColumnCount = this.dataModel.columnCount('body');
/*
If we have more body columns than columns available
for resize, resize only remaining columns as per allowance
and set remaining resize allowance number to 0.
*/
if (colsRemaining - bodyColumnCount < 0) {
this._fitBodyColumnHeaders(this.dataModel, padding, colsRemaining);
}
else {
/*
Otherwise the entire body column count can be resized.
Resize based on the smallest number between remaining
resize allowance and body column count.
*/
this._fitBodyColumnHeaders(this.dataModel, padding, Math.min(colsRemaining, bodyColumnCount));
}
}
else {
// No column resize cap passed - resizing all columns.
this._fitBodyColumnHeaders(this.dataModel, padding);
}
}
}
}
/**
* Map a client position to local viewport coordinates.
*
* @param clientX - The client X position of the mouse.
*
* @param clientY - The client Y position of the mouse.
*
* @returns The local viewport coordinates for the position.
*/
mapToLocal(clientX, clientY) {
// Fetch the viewport rect.
let rect = this._viewport.node.getBoundingClientRect();
// Extract the rect coordinates.
let { left, top } = rect;
// Round the rect coordinates for sub-pixel positioning.
left = Math.floor(left);
top = Math.floor(top);
// Convert to local coordinates.
let lx = clientX - left;
let ly = clientY - top;
// Return the local coordinates.
return { lx, ly };
}
/**
* Map a client position to virtual grid coordinates.
*
* @param clientX - The client X position of the mouse.
*
* @param clientY - The client Y position of the mouse.
*
* @returns The virtual grid coordinates for the position.
*/
mapToVirtual(clientX, clientY) {
// Convert to local coordiates.
let { lx, ly } = this.mapToLocal(clientX, clientY);
// Convert to virtual coordinates.
let vx = lx + this.scrollX - this.headerWidth;
let vy = ly + this.scrollY - this.headerHeight;
// Return the local coordinates.
return { vx, vy };
}
/**
* Hit test the viewport for the given client position.
*
* @param clientX - The client X position of the mouse.
*
* @param clientY - The client Y position of the mouse.
*
* @returns The hit test result, or `null` if the client
* position is out of bounds.
*
* #### Notes
* This method accounts for a stretched last row and/or column.
*/
hitTest(clientX, clientY) {
// Convert the mouse position into local coordinates.
let { lx, ly } = this.mapToLocal(clientX, clientY);
// Fetch the header and body dimensions.
let hw = this.headerWidth;
let hh = this.headerHeight;
let bw = this.bodyWidth;
let bh = this.bodyHeight;
let ph = this.pageHeight;
let pw = this.pageWidth;
// Adjust the body width for a stretched last column.
if (this._stretchLastColumn && pw > bw) {
bw = pw;
}
// Adjust the body height for a stretched last row.
if (this._stretchLastRow && ph > bh) {
bh = ph;
}
// Check for a corner header hit.
if (lx >= 0 && lx < hw && ly >= 0 && ly < hh) {
// Convert to unscrolled virtual coordinates.
let vx = lx;
let vy = ly;
// Fetch the row and column index.
let row = this.rowAt('column-header', vy);
let column = this.columnAt('row-header', vx);
// Fetch the cell offset position.
let ox = this.columnOffset('row-header', column);
let oy = this.rowOffset('column-header', row);
// Fetch cell width and height.
let width = this.columnSize('row-header', column);
let height = this.rowSize('column-header', row);
// Compute the leading and trailing positions.
let x = vx - ox;
let y = vy - oy;
// Return the hit test result.
return { region: 'corner-header', row, column, x, y, width, height };
}
// Check for a column header hit.
if (ly >= 0 && ly < hh && lx >= 0 && lx < hw + bw) {
// Convert to unscrolled virtual coordinates.
let vx = lx + this._scrollX - hw;
let vy = ly;
// Fetch the row and column index.
let row = this.rowAt('column-header', vy);
let column = this.columnAt('body', vx);
// Fetch the cell offset position.
let ox = this.columnOffset('body', column);
let oy = this.rowOffset('column-header', row);
// Fetch the cell width and height.
let width = this.columnSize('body', column);
let height = this.rowSize('column-header', row);
// Compute the leading and trailing positions.
let x = vx - ox;
let y = vy - oy;
// Return the hit test result.
return { region: 'column-header', row, column, x, y, width, height };
}
// Check for a row header hit.
if (lx >= 0 && lx < hw && ly >= 0 && ly < hh + bh) {
// Convert to unscrolled virtual coordinates.
let vx = lx;
let vy = ly + this._scrollY - hh;
// Fetch the row and column index.
let row = this.rowAt('body', vy);
let column = this.columnAt('row-header', vx);
// Fetch the cell offset position.
let ox = this.columnOffset('row-header', column);
let oy = this.rowOffset('body', row);
// Fetch the cell width and height.
let width = this.columnSize('row-header', column);
let height = this.rowSize('body', row);
// Compute the leading and trailing positions.
let x = vx - ox;
let y = vy - oy;
// Return the hit test result.
return { region: 'row-header', row, column, x, y, width, height };
}
// Check for a body hit.
if (lx >= hw && lx < hw + bw && ly >= hh && ly < hh + bh) {
// Convert to unscrolled virtual coordinates.
let vx = lx + this._scrollX - hw;
let vy = ly + this._scrollY - hh;
// Fetch the row and column index.
let row = this.rowAt('body', vy);
let column = this.columnAt('body', vx);
// Fetch the cell offset position.
let ox = this.columnOffset('body', column);
let oy = this.rowOffset('body', row);
// Fetch the cell width and height.
let width = this.columnSize('body', column);
let height = this.rowSize('body', row);
// Compute the part coordinates.
let x = vx - ox;
let y = vy - oy;
// Return the result.
return { region: 'body', row, column, x, y, width, height };
}
// Otherwise, it's a void space hit.
let row = -1;
let column = -1;
let x = -1;
let y = -1;
let width = -1;
let height = -1;
// Return the hit test result.
return { region: 'void', row, column, x, y, width, height };
}
/**
* Copy the current selection to the system clipboard.
*
* #### Notes
* The grid must have a data model and a selection model.
*
* The behavior can be configured via `DataGrid.copyConfig`.
*/
copyToClipboard() {
// Fetch the data model.
let dataModel = this._dataModel;
// Bail early if there is no data model.
if (!dataModel) {
return;
}
// Fetch the selection model.
let selectionModel = this._selectionModel;
// Bail early if there is no selection model.
if (!selectionModel) {
return;
}
// Coerce the selections to an array.
let selections = Array.from(selectionModel.selections());
// Bail early if there are no selections.
if (selections.length === 0) {
return;
}
// Alert that multiple selections cannot be copied.
if (selections.length > 1) {
alert('Cannot copy multiple grid selections.');
return;
}
// Fetch the model counts.
let br = dataModel.rowCount('body');
let bc = dataModel.columnCount('body');
// Bail early if there is nothing to copy.
if (br === 0 || bc === 0) {
return;
}
// Unpack the selection.
let { r1, c1, r2, c2 } = selections[0];
// Clamp the selection to the model bounds.
r1 = Math.max(0, Math.min(r1, br - 1));
c1 = Math.max(0, Math.min(c1, bc - 1));
r2 = Math.max(0, Math.min(r2, br - 1));
c2 = Math.max(0, Math.min(c2, bc - 1));
// Ensure the limits are well-orderd.
if (r2 < r1)
[r1, r2] = [r2, r1];
if (c2 < c1)
[c1, c2] = [c2, c1];
// Fetch the header counts.
let rhc = dataModel.columnCount('row-header');
let chr = dataModel.rowCount('column-header');
// Unpack the copy config.
let separator = this._copyConfig.separator;
let format = this._copyConfig.format;
let headers = this._copyConfig.headers;
let warningThreshold = this._copyConfig.warningThreshold;
// Compute the number of cells to be copied.
let rowCount = r2 - r1 + 1;
let colCount = c2 - c1 + 1;
switch (headers) {
case 'none':
rhc = 0;
chr = 0;
break;
case 'row':
chr = 0;
colCount += rhc;
break;
case 'column':
rhc = 0;
rowCount += chr;
break;
case 'all':
rowCount += chr;
colCount += rhc;
break;
default:
throw 'unreachable';
}
// Compute the total cell count.
let cellCount = rowCount * colCount;
// Allow the user to cancel a large copy request.
if (cellCount > warningThreshold) {
let msg = `Copying ${cellCount} cells may take a while. Continue?`;
if (!window.confirm(msg)) {
return;
}
}
// Set up the format args.
let args = {
region: 'body',
row: 0,
column: 0,
value: null,
metadata: {}
};
// Allocate the array of rows.
let rows = new Array(rowCount);
// Iterate over the rows.
for (let j = 0; j < rowCount; ++j) {
// Allocate the array of cells.
let cells = new Array(colCount);
// Iterate over the columns.
for (let i = 0; i < colCount; ++i) {
// Set up the format variables.
let region;
let row;
let column;
// Populate the format variables.
if (j < chr && i < rhc) {
region = 'corner-header';
row = j;
column = i;
}
else if (j < chr) {
region = 'column-header';
row = j;
column = i - rhc + c1;
}
else if (i < rhc) {
region = 'row-header';
row = j - chr + r1;
column = i;
}
else {
region = 'body';
row = j - chr + r1;
column = i - rhc + c1;
}
// Populate the format args.
args.region = region;
args.row = row;
args.column = column;
args.value = dataModel.data(region, row, column);
args.metadata = dataModel.metadata(region, row, column);
// Format the cell.
cells[i] = format(args);
}
// Save the row of cells.
rows[j] = cells;
}
// Convert the cells into lines.
let lines = rows.map(cells => cells.join(separator));
// Convert the lines into text.
let text = lines.join('\n');
// Copy the text to the clipboard.
_lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.ClipboardExt.copyText(text);
}
/**
* Process a message sent to the widget.
*
* @param msg - The message sent to the widget.
*/
processMessage(msg) {
// Ignore child show/hide messages. The data grid controls the
// visibility of its children, and will manually dispatch the
// fit-request messages as a result of visibility change.
if (msg.type === 'child-shown' || msg.type === 'child-hidden') {
return;
}
// Recompute the scroll bar minimums before the layout refits.
if (msg.type === 'fit-request') {
let vsbLimits = _lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.ElementExt.sizeLimits(this._vScrollBar.node);
let hsbLimits = _lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.ElementExt.sizeLimits(this._hScrollBar.node);
this._vScrollBarMinWidth = vsbLimits.minWidth;
this._hScrollBarMinHeight = hsbLimits.minHeight;
}
// Process all other messages as normal.
super.processMessage(msg);
}
/**
* Intercept a message sent to a message handler.
*
* @param handler - The target handler of the message.
*
* @param msg - The message to be sent to the handler.
*
* @returns `true` if the message should continue to be processed
* as normal, or `false` if processing should cease immediately.
*/
messageHook(handler, msg) {
// Process viewport messages.
if (handler === this._viewport) {
this._processViewportMessage(msg);
return true;
}
// Process horizontal scroll bar messages.
if (handler === this._hScrollBar && msg.type === 'activate-request') {
this.activate();
return false;
}
// Process vertical scroll bar messages.
if (handler === this._vScrollBar && msg.type === 'activate-request') {
this.activate();
return false;
}
// Ignore all other messages.
return true;
}
/**
* Handle the DOM events for the data grid.
*
* @param event - The DOM event sent to the data grid.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the data grid's DOM node. It
* should not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'keydown':
this._evtKeyDown(event);
break;
case 'mousedown':
this._evtMouseDown(event);
break;
case 'mousemove':
this._evtMouseMove(event);
break;
case 'mouseup':
this._evtMouseUp(event);
break;
case 'dblclick':
this._evtMouseDoubleClick(event);
break;
case 'mouseleave':
this._evtMouseLeave(event);
break;
case 'contextmenu':
this._evtContextMenu(event);
break;
case 'wheel':
this._evtWheel(event);
break;
case 'resize':
this._refreshDPI();
break;
}
}
/**
* Get the current viewport.
*
* @returns The current viewport as row/column coordinates.
* Returns undefined if the grid is not visible.
*/
get currentViewport() {
let width = this.viewport.node.offsetWidth;
let height = this.viewport.node.offsetHeight;
width = Math.round(width);
height = Math.round(height);
if (width <= 0 || height <= 0) {
return;
}
const contentW = this._columnSections.length - this.scrollX;
const contentH = this._rowSections.length - this.scrollY;
const contentX = this.headerWidth;
const contentY = this.headerHeight;
const x1 = contentX;
const y1 = contentY;
const x2 = Math.min(width - 1, contentX + contentW - 1);
const y2 = Math.min(height - 1, contentY + contentH - 1);
const firstRow = this._rowSections.indexOf(y1 - contentY + this.scrollY);
const firstColumn = this._columnSections.indexOf(x1 - contentX + this.scrollX);
const lastRow = this._rowSections.indexOf(y2 - contentY + this.scrollY);
const lastColumn = this._columnSections.indexOf(x2 - contentX + this.scrollX);
return {
firstRow,
firstColumn,
lastRow,
lastColumn
};
}
/**
* A message handler invoked on an `'activate-request'` message.
*/
onActivateRequest(msg) {
this.viewport.node.focus({ preventScroll: true });
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
window.addEventListener('resize', this);
this.node.addEventListener('wheel', this);
this._viewport.node.addEventListener('keydown', this);
this._viewport.node.addEventListener('mousedown', this);
this._viewport.node.addEventListener('mousemove', this);
this._viewport.node.addEventListener('dblclick', this);
this._viewport.node.addEventListener('mouseleave', this);
this._viewport.node.addEventListener('contextmenu', this);
this.repaintContent();
this.repaintOverlay();
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
onAfterDetach(msg) {
window.removeEventListener('resize', this);
this.node.removeEventListener('wheel', this);
this._viewport.node.removeEventListener('keydown', this);
this._viewport.node.removeEventListener('mousedown', this);
this._viewport.node.removeEventListener('mousemove', this);
this._viewport.node.removeEventListener('mouseleave', this);
this._viewport.node.removeEventListener('dblclick', this);
this._viewport.node.removeEventListener('contextmenu', this);
this._releaseMouse();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
onBeforeShow(msg) {
this.repaintContent();
this.repaintOverlay();
}
/**
* A message handler invoked on a `'resize'` message.
*/
onResize(msg) {
if (this._editorController) {
this._editorController.cancel();
}
this._syncScrollState();
}
/**
* Schedule a repaint of all of the grid content.
*/
repaintContent() {
let msg = new Private$1.PaintRequest('all', 0, 0, 0, 0);
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.postMessage(this._viewport, msg);
}
/**
* Schedule a repaint of specific grid content.
*/
repaintRegion(region, r1, c1, r2, c2) {
let msg = new Private$1.PaintRequest(region, r1, c1, r2, c2);
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.postMessage(this._viewport, msg);
}
/**
* Schedule a repaint of the overlay.
*/
repaintOverlay() {
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.postMessage(this._viewport, Private$1.OverlayPaintRequest);
}
_getMaxWidthInColumn(index, columnRegion) {
const dataModel = this.dataModel;
if (!dataModel) {
return null;
}
const columnHeaderRegion = columnRegion == 'row-header' ? 'corner-header' : 'column-header';
return Math.max(this._getMaxWidthInArea(dataModel, index, columnHeaderRegion, 'column-header'), this._getMaxWidthInArea(dataModel, index, columnRegion, 'body'));
}
_getMaxWidthInArea(dataModel, index, region, rowRegion) {
const numRows = dataModel.rowCount(rowRegion);
// Will only allocate up to 1_000_000 elements otherwise performance can tank.
const configs = Array.from({ length: Math.min(numRows, 1000000) }, (_val, idx) => DataGrid._getConfig(dataModel, idx, index, region));
// Heuristic: Sort by the length of the text to render and only fully calculate the text width
// for the top 100_000 rows by text length
if (numRows > 100000) {
// Sort by descending length
configs.sort(x => -this._getTextToRender(x).length);
}
let maxWidth = 0;
for (let i = 0; i < numRows && i < 100000; ++i) {
const textWidth = this._getCellTextWidth(configs[i]);
maxWidth = Math.max(maxWidth, textWidth);
}
return maxWidth;
}
static _getConfig(dataModel, row, col, location) {
return {
x: 0,
y: 0,
width: 0,
height: 0,
region: location,
row: row,
column: col,
value: DataGrid._getCellValue(dataModel, location, row, col),
metadata: DataGrid._getCellMetadata(dataModel, location, row, col)
};
}
_getTextToRender(config) {
const renderer = this.cellRenderers.get(config);
return renderer.getText(config);
}
_getCellTextWidth(config) {
// Get the renderer for the given cell.
const renderer = this.cellRenderers.get(config);
// Use the canvas context to measure the cell's text width
const gc = this.canvasGC;
gc.font = CellRenderer.resolveOption(renderer.font, config);
gc.fillStyle = CellRenderer.resolveOption(renderer.textColor, config);
gc.textAlign = CellRenderer.resolveOption(renderer.horizontalAlignment, config);
gc.textBaseline = 'bottom';
const text = this._getTextToRender(config);
return gc.measureText(text).width + 2 * renderer.horizontalPadding;
}
/**
* Ensure the canvas is at least the specified size.
*
* This method will retain the valid canvas content.
*/
_resizeCanvasIfNeeded(width, height) {
// Scale the size by the dpi ratio.
width = width * this._dpiRatio;
height = height * this._dpiRatio;
// Compute the maximum canvas size for the given width and height.
let maxW = (Math.ceil((width + 1) / 512) + 1) * 512;
let maxH = (Math.ceil((height + 1) / 512) + 1) * 512;
// Get the current size of the canvas.
let curW = this._canvas.width;
let curH = this._canvas.height;
// Bail early if the canvas size is within bounds.
if (curW >= width && curH >= height && curW <= maxW && curH <= maxH) {
return;
}
// Compute the expanded canvas size.
let expW = maxW - 512;
let expH = maxH - 512;
// Set the transforms to the identity matrix.
this._canvasGC.setTransform(1, 0, 0, 1, 0, 0);
this._bufferGC.setTransform(1, 0, 0, 1, 0, 0);
this._overlayGC.setTransform(1, 0, 0, 1, 0, 0);
// Resize the buffer if needed.
if (curW < width) {
this._buffer.width = expW;
}
else if (curW > maxW) {
this._buffer.width = maxW;
}
// Resize the buffer height if needed.
if (curH < height) {
this._buffer.height = expH;
}
else if (curH > maxH) {
this._buffer.height = maxH;
}
// Test whether there is content to blit.
let needBlit = curW > 0 && curH > 0 && width > 0 && height > 0;
// Copy the valid canvas content into the buffer if needed.
if (needBlit) {
this._bufferGC.drawImage(this._canvas, 0, 0);
}
// Resize the canvas width if needed.
if (curW < width) {
this._canvas.width = expW;
this._canvas.style.width = `${expW / this._dpiRatio}px`;
}
else if (curW > maxW) {
this._canvas.width = maxW;
this._canvas.style.width = `${maxW / this._dpiRatio}px`;
}
// Resize the canvas height if needed.
if (curH < height) {
this._canvas.height = expH;
this._canvas.style.height = `${expH / this._dpiRatio}px`;
}
else if (curH > maxH) {
this._canvas.height = maxH;
this._canvas.style.height = `${maxH / this._dpiRatio}px`;
}
// Copy the valid canvas content from the buffer if needed.
if (needBlit) {
this._canvasGC.drawImage(this._buffer, 0, 0);
}
// Copy the valid overlay content into the buffer if needed.
if (needBlit) {
this._bufferGC.drawImage(this._overlay, 0, 0);
}
// Resize the overlay width if needed.
if (curW < width) {
this._overlay.width = expW;
this._overlay.style.width = `${expW / this._dpiRatio}px`;
}
else if (curW > maxW) {
this._overlay.width = maxW;
this._overlay.style.width = `${maxW / this._dpiRatio}px`;
}
// Resize the overlay height if needed.
if (curH < height) {
this._overlay.height = expH;
this._overlay.style.height = `${expH / this._dpiRatio}px`;
}
else if (curH > maxH) {
this._overlay.height = maxH;
this._overlay.style.height = `${maxH / this._dpiRatio}px`;
}
// Copy the valid overlay content from the buffer if needed.
if (needBlit) {
this._overlayGC.drawImage(this._buffer, 0, 0);
}
}
/**
* Sync the scroll bars and scroll state with the viewport.
*
* #### Notes
* If the visibility of either scroll bar changes, a synchronous
* fit-request will be dispatched to the data grid to immediately
* resize the viewport.
*/
_syncScrollState() {
// Fetch the viewport dimensions.
let bw = this.bodyWidth;
let bh = this.bodyHeight;
let pw = this.pageWidth;
let ph = this.pageHeight;
// Get the current scroll bar visibility.
let hasVScroll = !this._vScrollBar.isHidden;
let hasHScroll = !this._hScrollBar.isHidden;
// Get the minimum sizes of the scroll bars.
let vsw = this._vScrollBarMinWidth;
let hsh = this._hScrollBarMinHeight;
// Get the page size as if no scroll bars are visible.
let apw = pw + (hasVScroll ? vsw : 0);
let aph = ph + (hasHScroll ? hsh : 0);
// Test whether scroll bars are needed for the adjusted size.
let needVScroll = aph < bh - 1;
let needHScroll = apw < bw - 1;
// Re-test the horizontal scroll if a vertical scroll is needed.
if (needVScroll && !needHScroll) {
needHScroll = apw - vsw < bw - 1;
}
// Re-test the vertical scroll if a horizontal scroll is needed.
if (needHScroll && !needVScroll) {
needVScroll = aph - hsh < bh - 1;
}
// If the visibility changes, immediately refit the grid.
if (needVScroll !== hasVScroll || needHScroll !== hasHScroll) {
this._vScrollBar.setHidden(!needVScroll);
this._hScrollBar.setHidden(!needHScroll);
this._scrollCorner.setHidden(!needVScroll || !needHScroll);
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.sendMessage(this, _lumino_widgets__WEBPACK_IMPORTED_MODULE_5__.Widget.Msg.FitRequest);
}
// Update the scroll bar limits.
this._vScrollBar.maximum = this.maxScrollY;
this._vScrollBar.page = this.pageHeight;
this._hScrollBar.maximum = this.maxScrollX;
this._hScrollBar.page = this.pageWidth;
// Re-clamp the scroll position.
this._scrollTo(this._scrollX, this._scrollY);
}
/**
* Sync the viewport to the given scroll position.
*
* #### Notes
* This schedules a full repaint and syncs the scroll state.
*/
_syncViewport() {
this.repaintContent();
this.repaintOverlay();
this._syncScrollState();
}
/**
* Process a message sent to the viewport
*/
_processViewportMessage(msg) {
switch (msg.type) {
case 'resize':
this._onViewportResize(msg);
break;
case 'scroll-request':
this._onViewportScrollRequest(msg);
break;
case 'paint-request':
this._onViewportPaintRequest(msg);
break;
case 'overlay-paint-request':
this._onViewportOverlayPaintRequest(msg);
break;
case 'row-resize-request':
this._onViewportRowResizeRequest(msg);
break;
case 'column-resize-request':
this._onViewportColumnResizeRequest(msg);
break;
}
}
/**
* A message hook invoked on a viewport `'resize'` message.
*/
_onViewportResize(msg) {
// Bail early if the viewport is not visible.
if (!this._viewport.isVisible) {
return;
}
// Unpack the message data.
let { width, height } = msg;
// Measure the viewport node if the dimensions are unknown.
if (width === -1) {
width = this._viewport.node.offsetWidth;
}
if (height === -1) {
height = this._viewport.node.offsetHeight;
}
// Round the dimensions to the nearest pixel.
width = Math.round(width);
height = Math.round(height);
// Get the current size of the viewport.
let oldWidth = this._viewportWidth;
let oldHeight = this._viewportHeight;
// Updated internal viewport size.
this._viewportWidth = width;
this._viewportHeight = height;
// Resize the canvas if needed.
this._resizeCanvasIfNeeded(width, height);
// Bail early if there is nothing to paint.
if (width === 0 || height === 0) {
return;
}
// Paint the whole grid if the old size was zero.
if (oldWidth === 0 || oldHeight === 0) {
this.paintContent(0, 0, width, height);
this._paintOverlay();
return;
}
// Paint the right edge as needed.
if (this._stretchLastColumn && this.pageWidth > this.bodyWidth) {
let bx = this._columnSections.offsetOf(this._columnSections.count - 1);
let x = Math.min(this.headerWidth + bx, oldWidth);
this.paintContent(x, 0, width - x, height);
}
else if (width > oldWidth) {
this.paintContent(oldWidth, 0, width - oldWidth + 1, height);
}
// Paint the bottom edge as needed.
if (this._stretchLastRow && this.pageHeight > this.bodyHeight) {
let by = this._rowSections.offsetOf(this._rowSections.count - 1);
let y = Math.min(this.headerHeight + by, oldHeight);
this.paintContent(0, y, width, height - y);
}
else if (height > oldHeight) {
this.paintContent(0, oldHeight, width, height - oldHeight + 1);
}
// Paint the overlay.
this._paintOverlay();
}
/**
* A message hook invoked on a viewport `'scroll-request'` message.
*/
_onViewportScrollRequest(msg) {
this._scrollTo(this._hScrollBar.value, this._vScrollBar.value);
}
/**
* A message hook invoked on a viewport `'paint-request'` message.
*/
_onViewportPaintRequest(msg) {
// Bail early if the viewport is not visible.
if (!this._viewport.isVisible) {
return;
}
// Bail early if the viewport has zero area.
if (this._viewportWidth === 0 || this._viewportHeight === 0) {
return;
}
// Set up the paint limits.
let xMin = 0;
let yMin = 0;
let xMax = this._viewportWidth - 1;
let yMax = this._viewportHeight - 1;
// Fetch the scroll position.
let sx = this._scrollX;
let sy = this._scrollY;
// Fetch the header dimensions.
let hw = this.headerWidth;
let hh = this.headerHeight;
// Fetch the section lists.
let rs = this._rowSections;
let cs = this._columnSections;
let rhs = this._rowHeaderSections;
let chs = this._columnHeaderSections;
// Unpack the message data.
let { region, r1, c1, r2, c2 } = msg;
// Set up the paint variables.
let x1;
let y1;
let x2;
let y2;
// Fill the paint variables based on the paint region.
switch (region) {
case 'all':
x1 = xMin;
y1 = yMin;
x2 = xMax;
y2 = yMax;
break;
case 'body':
r1 = Math.max(0, Math.min(r1, rs.count));
c1 = Math.max(0, Math.min(c1, cs.count));
r2 = Math.max(0, Math.min(r2, rs.count));
c2 = Math.max(0, Math.min(c2, cs.count));
x1 = cs.offsetOf(c1) - sx + hw;
y1 = rs.offsetOf(r1) - sy + hh;
x2 = cs.extentOf(c2) - sx + hw;
y2 = rs.extentOf(r2) - sy + hh;
break;
case 'row-header':
r1 = Math.max(0, Math.min(r1, rs.count));
c1 = Math.max(0, Math.min(c1, rhs.count));
r2 = Math.max(0, Math.min(r2, rs.count));
c2 = Math.max(0, Math.min(c2, rhs.count));
x1 = rhs.offsetOf(c1);
y1 = rs.offsetOf(r1) - sy + hh;
x2 = rhs.extentOf(c2);
y2 = rs.extentOf(r2) - sy + hh;
break;
case 'column-header':
r1 = Math.max(0, Math.min(r1, chs.count));
c1 = Math.max(0, Math.min(c1, cs.count));
r2 = Math.max(0, Math.min(r2, chs.count));
c2 = Math.max(0, Math.min(c2, cs.count));
x1 = cs.offsetOf(c1) - sx + hw;
y1 = chs.offsetOf(r1);
x2 = cs.extentOf(c2) - sx + hw;
y2 = chs.extentOf(r2);
break;
case 'corner-header':
r1 = Math.max(0, Math.min(r1, chs.count));
c1 = Math.max(0, Math.min(c1, rhs.count));
r2 = Math.max(0, Math.min(r2, chs.count));
c2 = Math.max(0, Math.min(c2, rhs.count));
x1 = rhs.offsetOf(c1);
y1 = chs.offsetOf(r1);
x2 = rhs.extentOf(c2);
y2 = chs.extentOf(r2);
break;
default:
throw 'unreachable';
}
// Bail early if the dirty rect is outside the bounds.
if (x2 < xMin || y2 < yMin || x1 > xMax || y1 > yMax) {
return;
}
// Clamp the dirty rect to the paint bounds.
x1 = Math.max(xMin, Math.min(x1, xMax));
y1 = Math.max(yMin, Math.min(y1, yMax));
x2 = Math.max(xMin, Math.min(x2, xMax));
y2 = Math.max(yMin, Math.min(y2, yMax));
// Paint the content of the dirty rect.
this.paintContent(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
}
/**
* A message hook invoked on a viewport `'overlay-paint-request'` message.
*/
_onViewportOverlayPaintRequest(msg) {
// Bail early if the viewport is not visible.
if (!this._viewport.isVisible) {
return;
}
// Bail early if the viewport has zero area.
if (this._viewportWidth === 0 || this._viewportHeight === 0) {
return;
}
// Paint the content of the overlay.
this._paintOverlay();
}
/**
* A message hook invoked on a viewport `'row-resize-request'` message.
*/
_onViewportRowResizeRequest(msg) {
if (msg.region === 'body') {
this._resizeRow(msg.index, msg.size);
}
else {
this._resizeColumnHeader(msg.index, msg.size);
}
}
/**
* A message hook invoked on a viewport `'column-resize-request'` message.
*/
_onViewportColumnResizeRequest(msg) {
if (msg.region === 'body') {
this._resizeColumn(msg.index, msg.size);
}
else {
this._resizeRowHeader(msg.index, msg.size);
}
}
/**
* Handle the `thumbMoved` signal from a scroll bar.
*/
_onThumbMoved(sender) {
_lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.MessageLoop.postMessage(this._viewport, Private$1.ScrollRequest);
}
/**
* Handle the `pageRequested` signal from a scroll bar.
*/
_onPageRequested(sender, dir) {
if (sender === this._vScrollBar) {
this.scrollByPage(dir === 'decrement' ? 'up' : 'down');
}
else {
this.scrollByPage(dir === 'decrement' ? 'left' : 'right');
}
}
/**
* Handle the `stepRequested` signal from a scroll bar.
*/
_onStepRequested(sender, dir) {
if (sender === this._vScrollBar) {
this.scrollByStep(dir === 'decrement' ? 'up' : 'down');
}
else {
this.scrollByStep(dir === 'decrement' ? 'left' : 'right');
}
}
/**
* A signal handler for the data model `changed` signal.
*/
_onDataModelChanged(sender, args) {
switch (args.type) {
case 'rows-inserted':
this._onRowsInserted(args);
break;
case 'columns-inserted':
this._onColumnsInserted(args);
break;
case 'rows-removed':
this._onRowsRemoved(args);
break;
case 'columns-removed':
this._onColumnsRemoved(args);
break;
case 'rows-moved':
this._onRowsMoved(args);
break;
case 'columns-moved':
this._onColumnsMoved(args);
break;
case 'cells-changed':
this._onCellsChanged(args);
break;
case 'model-reset':
this._onModelReset(args);
break;
default:
throw 'unreachable';
}
}
/**
* A signal handler for the selection model `changed` signal.
*/
_onSelectionsChanged(sender) {
this.repaintOverlay();
}
/**
* Handle rows being inserted in the data model.
*/
_onRowsInserted(args) {
// Unpack the arg data.
let { region, index, span } = args;
// Bail early if there are no sections to insert.
if (span <= 0) {
return;
}
// Look up the relevant section list.
let list;
if (region === 'body') {
list = this._rowSections;
}
else {
list = this._columnHeaderSections;
}
// Insert the span, maintaining the scroll position as needed.
if (this._scrollY === this.maxScrollY && this.maxScrollY > 0) {
list.insert(index, span);
this._scrollY = this.maxScrollY;
}
else {
list.insert(index, span);
}
// Sync the viewport.
this._syncViewport();
}
/**
* Handle columns being inserted into the data model.
*/
_onColumnsInserted(args) {
// Unpack the arg data.
let { region, index, span } = args;
// Bail early if there are no sections to insert.
if (span <= 0) {
return;
}
// Look up the relevant section list.
let list;
if (region === 'body') {
list = this._columnSections;
}
else {
list = this._rowHeaderSections;
}
// Insert the span, maintaining the scroll position as needed.
if (this._scrollX === this.maxScrollX && this.maxScrollX > 0) {
list.insert(index, span);
this._scrollX = this.maxScrollX;
}
else {
list.insert(index, span);
}
// Sync the viewport.
this._syncViewport();
}
/**
* Handle rows being removed from the data model.
*/
_onRowsRemoved(args) {
// Unpack the arg data.
let { region, index, span } = args;
// Bail early if there are no sections to remove.
if (span <= 0) {
return;
}
// Look up the relevant section list.
let list;
if (region === 'body') {
list = this._rowSections;
}
else {
list = this._columnHeaderSections;
}
// Bail if the index or is invalid
if (index < 0 || index >= list.count) {
return;
}
// Remove the span, maintaining the scroll position as needed.
if (this._scrollY === this.maxScrollY && this.maxScrollY > 0) {
list.remove(index, span);
this._scrollY = this.maxScrollY;
}
else {
list.remove(index, span);
}
// Sync the viewport.
this._syncViewport();
}
/**
* Handle columns being removed from the data model.
*/
_onColumnsRemoved(args) {
// Unpack the arg data.
let { region, index, span } = args;
// Bail early if there are no sections to remove.
if (span <= 0) {
return;
}
// Look up the relevant section list.
let list;
if (region === 'body') {
list = this._columnSections;
}
else {
list = this._rowHeaderSections;
}
// Bail if the index or is invalid
if (index < 0 || index >= list.count) {
return;
}
// Remove the span, maintaining the scroll position as needed.
if (this._scrollX === this.maxScrollX && this.maxScrollX > 0) {
list.remove(index, span);
this._scrollX = this.maxScrollX;
}
else {
list.remove(index, span);
}
// Sync the viewport.
this._syncViewport();
}
/**
* Handle rows moving in the data model.
*/
_onRowsMoved(args) {
// Unpack the arg data.
let { region, index, span, destination } = args;
// Bail early if there are no sections to move.
if (span <= 0) {
return;
}
// Look up the relevant section list.
let list;
if (region === 'body') {
list = this._rowSections;
}
else {
list = this._columnHeaderSections;
}
// Bail early if the index is out of range.
if (index < 0 || index >= list.count) {
return;
}
// Clamp the move span to the limit.
span = Math.min(span, list.count - index);
// Clamp the destination index to the limit.
destination = Math.min(Math.max(0, destination), list.count - span);
// Bail early if there is no effective move.
if (index === destination) {
return;
}
// Compute the first affected index.
let r1 = Math.min(index, destination);
// Compute the last affected index.
let r2 = Math.max(index + span - 1, destination + span - 1);
// Move the sections in the list.
list.move(index, span, destination);
// Schedule a repaint of the dirty cells.
if (region === 'body') {
this.repaintRegion('body', r1, 0, r2, Infinity);
this.repaintRegion('row-header', r1, 0, r2, Infinity);
}
else {
this.repaintRegion('column-header', r1, 0, r2, Infinity);
this.repaintRegion('corner-header', r1, 0, r2, Infinity);
}
// Sync the viewport.
this._syncViewport();
}
/**
* Handle columns moving in the data model.
*/
_onColumnsMoved(args) {
// Unpack the arg data.
let { region, index, span, destination } = args;
// Bail early if there are no sections to move.
if (span <= 0) {
return;
}
// Look up the relevant section list.
let list;
if (region === 'body') {
list = this._columnSections;
}
else {
list = this._rowHeaderSections;
}
// Bail early if the index is out of range.
if (index < 0 || index >= list.count) {
return;
}
// Clamp the move span to the limit.
span = Math.min(span, list.count - index);
// Clamp the destination index to the limit.
destination = Math.min(Math.max(0, destination), list.count - span);
// Bail early if there is no effective move.
if (index === destination) {
return;
}
// Move the sections in the list.
list.move(index, span, destination);
// Compute the first affected index.
let c1 = Math.min(index, destination);
// Compute the last affected index.
let c2 = Math.max(index + span - 1, destination + span - 1);
// Schedule a repaint of the dirty cells.
if (region === 'body') {
this.repaintRegion('body', 0, c1, Infinity, c2);
this.repaintRegion('column-header', 0, c1, Infinity, c2);
}
else {
this.repaintRegion('row-header', 0, c1, Infinity, c2);
this.repaintRegion('corner-header', 0, c1, Infinity, c2);
}
// Sync the viewport.
this._syncViewport();
}
/**
* Handle cells changing in the data model.
*/
_onCellsChanged(args) {
// Unpack the arg data.
let { region, row, column, rowSpan, columnSpan } = args;
// Bail early if there are no cells to modify.
if (rowSpan <= 0 && columnSpan <= 0) {
return;
}
// Compute the changed cell bounds.
let r1 = row;
let c1 = column;
let r2 = r1 + rowSpan - 1;
let c2 = c1 + columnSpan - 1;
// Schedule a repaint of the cell content.
this.repaintRegion(region, r1, c1, r2, c2);
}
/**
* Handle a full data model reset.
*/
_onModelReset(args) {
// Look up the various current section counts.
let nr = this._rowSections.count;
let nc = this._columnSections.count;
let nrh = this._rowHeaderSections.count;
let nch = this._columnHeaderSections.count;
// Compute the delta count for each region.
let dr = this._dataModel.rowCount('body') - nr;
let dc = this._dataModel.columnCount('body') - nc;
let drh = this._dataModel.columnCount('row-header') - nrh;
let dch = this._dataModel.rowCount('column-header') - nch;
// Update the row sections, if needed.
if (dr > 0) {
this._rowSections.insert(nr, dr);
}
else if (dr < 0) {
this._rowSections.remove(nr + dr, -dr);
}
// Update the column sections, if needed.
if (dc > 0) {
this._columnSections.insert(nc, dc);
}
else if (dc < 0) {
this._columnSections.remove(nc + dc, -dc);
}
// Update the row header sections, if needed.
if (drh > 0) {
this._rowHeaderSections.insert(nrh, drh);
}
else if (drh < 0) {
this._rowHeaderSections.remove(nrh + drh, -drh);
}
// Update the column header sections, if needed.
if (dch > 0) {
this._columnHeaderSections.insert(nch, dch);
}
else if (dch < 0) {
this._columnHeaderSections.remove(nch + dch, -dch);
}
// Sync the viewport.
this._syncViewport();
}
/**
* A signal handler for the renderer map `changed` signal.
*/
_onRenderersChanged() {
this.repaintContent();
}
/**
* Handle the `'keydown'` event for the data grid.
*/
_evtKeyDown(event) {
if (this._mousedown) {
event.preventDefault();
event.stopPropagation();
}
else if (this._keyHandler) {
this._keyHandler.onKeyDown(this, event);
}
}
/**
* Handle the `'mousedown'` event for the data grid.
*/
_evtMouseDown(event) {
// Ignore everything except the left mouse button.
if (event.button !== 0) {
return;
}
// Activate the grid.
this.activate();
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Add the extra document listeners.
document.addEventListener('keydown', this, true);
document.addEventListener('mouseup', this, true);
document.addEventListener('mousedown', this, true);
document.addEventListener('mousemove', this, true);
document.addEventListener('contextmenu', this, true);
// Flip the mousedown flag.
this._mousedown = true;
// Dispatch to the mouse handler.
if (this._mouseHandler) {
this._mouseHandler.onMouseDown(this, event);
}
}
/**
* Handle the `'mousemove'` event for the data grid.
*/
_evtMouseMove(event) {
// Stop the event propagation if the mouse is down.
if (this._mousedown) {
event.preventDefault();
event.stopPropagation();
}
// Bail if there is no mouse handler.
if (!this._mouseHandler) {
return;
}
// Dispatch to the mouse handler.
if (this._mousedown) {
this._mouseHandler.onMouseMove(this, event);
}
else {
this._mouseHandler.onMouseHover(this, event);
}
}
/**
* Handle the `'mouseup'` event for the data grid.
*/
_evtMouseUp(event) {
// Ignore everything except the left mouse button.
if (event.button !== 0) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Dispatch to the mouse handler.
if (this._mouseHandler) {
this._mouseHandler.onMouseUp(this, event);
}
// Release the mouse.
this._releaseMouse();
}
/**
* Handle the `'dblclick'` event for the data grid.
*/
_evtMouseDoubleClick(event) {
// Ignore everything except the left mouse button.
if (event.button !== 0) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Dispatch to the mouse handler.
if (this._mouseHandler) {
this._mouseHandler.onMouseDoubleClick(this, event);
}
// Release the mouse.
this._releaseMouse();
}
/**
* Handle the `'mouseleave'` event for the data grid.
*/
_evtMouseLeave(event) {
if (this._mousedown) {
event.preventDefault();
event.stopPropagation();
}
else if (this._mouseHandler) {
this._mouseHandler.onMouseLeave(this, event);
}
}
/**
* Handle the `'contextmenu'` event for the data grid.
*/
_evtContextMenu(event) {
if (this._mousedown) {
event.preventDefault();
event.stopPropagation();
}
else if (this._mouseHandler) {
this._mouseHandler.onContextMenu(this, event);
}
}
/**
* Handle the `'wheel'` event for the data grid.
*/
_evtWheel(event) {
// Ignore the event if `accel` is held.
if (_lumino_domutils__WEBPACK_IMPORTED_MODULE_0__.Platform.accelKey(event)) {
return;
}
// Bail early if there is no mouse handler.
if (!this._mouseHandler) {
return;
}
// Dispatch to the mouse handler.
this._mouseHandler.onWheel(this, event);
}
/**
* Release the mouse grab.
*/
_releaseMouse() {
// Clear the mousedown flag.
this._mousedown = false;
// Relase the mouse handler.
if (this._mouseHandler) {
this._mouseHandler.release();
}
// Remove the document listeners.
document.removeEventListener('keydown', this, true);
document.removeEventListener('mouseup', this, true);
document.removeEventListener('mousedown', this, true);
document.removeEventListener('mousemove', this, true);
document.removeEventListener('contextmenu', this, true);
}
/**
* Refresh the dpi ratio.
*/
_refreshDPI() {
// Get the best integral value for the dpi ratio.
let dpiRatio = Math.ceil(window.devicePixelRatio);
// Bail early if the computed dpi ratio has not changed.
if (this._dpiRatio === dpiRatio) {
return;
}
// Update the internal dpi ratio.
this._dpiRatio = dpiRatio;
// Schedule a repaint of the content.
this.repaintContent();
// Schedule a repaint of the overlay.
this.repaintOverlay();
// Update the canvas size for the new dpi ratio.
this._resizeCanvasIfNeeded(this._viewportWidth, this._viewportHeight);
// Ensure the canvas style is scaled for the new ratio.
this._canvas.style.width = `${this._canvas.width / this._dpiRatio}px`;
this._canvas.style.height = `${this._canvas.height / this._dpiRatio}px`;
// Ensure the overlay style is scaled for the new ratio.
this._overlay.style.width = `${this._overlay.width / this._dpiRatio}px`;
this._overlay.style.height = `${this._overlay.height / this._dpiRatio}px`;
}
/**
* Resize a row section immediately.
*/
_resizeRow(index, size) {
// Look up the target section list.
let list = this._rowSections;
// Bail early if the index is out of range.
if (index < 0 || index >= list.count) {
return;
}
// Look up the old size of the section.
let oldSize = list.sizeOf(index);
// Normalize the new size of the section.
let newSize = list.clampSize(size);
// Bail early if the size does not change.
if (oldSize === newSize) {
return;
}
// Resize the section in the list.
list.resize(index, newSize);
// Get the current size of the viewport.
let vw = this._viewportWidth;
let vh = this._viewportHeight;
// If there is nothing to paint, sync the scroll state.
if (!this._viewport.isVisible || vw === 0 || vh === 0) {
this._syncScrollState();
return;
}
// Compute the size delta.
let delta = newSize - oldSize;
// Look up the column header height.
let hh = this.headerHeight;
// Compute the viewport offset of the section.
let offset = list.offsetOf(index) + hh - this._scrollY;
// Bail early if there is nothing to paint.
if (hh >= vh || offset >= vh) {
this._syncScrollState();
return;
}
// Update the scroll position if the section is not visible.
if (offset + oldSize <= hh) {
this._scrollY += delta;
this._syncScrollState();
return;
}
// Compute the paint origin of the section.
let pos = Math.max(hh, offset);
// Paint from the section onward if it spans the viewport.
if (offset + oldSize >= vh || offset + newSize >= vh) {
this.paintContent(0, pos, vw, vh - pos);
this._paintOverlay();
this._syncScrollState();
return;
}
// Compute the X blit dimensions.
let sx = 0;
let sw = vw;
let dx = 0;
// Compute the Y blit dimensions.
let sy;
let sh;
let dy;
if (offset + newSize <= hh) {
sy = hh - delta;
sh = vh - sy;
dy = hh;
}
else {
sy = offset + oldSize;
sh = vh - sy;
dy = sy + delta;
}
// Blit the valid content to the destination.
this._blitContent(this._canvas, sx, sy, sw, sh, dx, dy);
// Repaint the section if needed.
if (newSize > 0 && offset + newSize > hh) {
this.paintContent(0, pos, vw, offset + newSize - pos);
}
// Paint the trailing space as needed.
if (this._stretchLastRow && this.pageHeight > this.bodyHeight) {
let r = this._rowSections.count - 1;
let y = hh + this._rowSections.offsetOf(r);
this.paintContent(0, y, vw, vh - y);
}
else if (delta < 0) {
this.paintContent(0, vh + delta, vw, -delta);
}
// Repaint merged cells that are intersected by the resized row
// Otherwise it will be cut in two by the valid content, and drawn incorrectly
for (const rgn of ['body', 'row-header']) {
const cellGroups = CellGroup.getCellGroupsAtRow(this.dataModel, rgn, index);
let paintRgn = {
region: rgn,
xMin: 0,
xMax: 0,
yMin: 0,
yMax: 0
};
let backgroundColor = undefined;
switch (rgn) {
case 'body':
paintRgn.xMin = this.headerWidth;
paintRgn.xMax = this.headerWidth + this.bodyWidth;
paintRgn.yMin = this.headerHeight;
paintRgn.yMax = this.headerHeight + this.bodyHeight;
backgroundColor = this._style.backgroundColor;
break;
case 'row-header':
paintRgn.xMin = 0;
paintRgn.xMax = this.headerWidth;
paintRgn.yMin = this.headerHeight;
paintRgn.yMax = this.headerHeight + this.bodyHeight;
backgroundColor = this._style.headerBackgroundColor;
break;
}
this._paintMergedCells(cellGroups, paintRgn, backgroundColor);
}
// Paint the overlay.
this._paintOverlay();
// Sync the scroll state.
this._syncScrollState();
}
/**
* Resize a column section immediately.
*/
_resizeColumn(index, size) {
// Look up the target section list.
let list = this._columnSections;
// Bail early if the index is out of range.
if (index < 0 || index >= list.count) {
return;
}
const adjustedSize = size !== null && size !== void 0 ? size : this._getMaxWidthInColumn(index, 'body');
if (!adjustedSize || adjustedSize == 0) {
return;
}
// Look up the old size of the section.
let oldSize = list.sizeOf(index);
// Normalize the new size of the section.
let newSize = list.clampSize(adjustedSize);
// Bail early if the size does not change.
if (oldSize === newSize) {
return;
}
// Resize the section in the list.
list.resize(index, newSize);
// Get the current size of the viewport.
let vw = this._viewportWidth;
let vh = this._viewportHeight;
// If there is nothing to paint, sync the scroll state.
if (!this._viewport.isVisible || vw === 0 || vh === 0) {
this._syncScrollState();
return;
}
// Compute the size delta.
let delta = newSize - oldSize;
// Look up the row header width.
let hw = this.headerWidth;
// Compute the viewport offset of the section.
let offset = list.offsetOf(index) + hw - this._scrollX;
// Bail early if there is nothing to paint.
if (hw >= vw || offset >= vw) {
this._syncScrollState();
return;
}
// Update the scroll position if the section is not visible.
if (offset + oldSize <= hw) {
this._scrollX += delta;
this._syncScrollState();
return;
}
// Compute the paint origin of the section.
let pos = Math.max(hw, offset);
// Paint from the section onward if it spans the viewport.
if (offset + oldSize >= vw || offset + newSize >= vw) {
this.paintContent(pos, 0, vw - pos, vh);
this._paintOverlay();
this._syncScrollState();
return;
}
// Compute the Y blit dimensions.
let sy = 0;
let sh = vh;
let dy = 0;
// Compute the X blit dimensions.
let sx;
let sw;
let dx;
if (offset + newSize <= hw) {
sx = hw - delta;
sw = vw - sx;
dx = hw;
}
else {
sx = offset + oldSize;
sw = vw - sx;
dx = sx + delta;
}
// Blit the valid content to the destination.
this._blitContent(this._canvas, sx, sy, sw, sh, dx, dy);
// Repaint the section if needed.
if (newSize > 0 && offset + newSize > hw) {
this.paintContent(pos, 0, offset + newSize - pos, vh);
}
// Paint the trailing space as needed.
if (this._stretchLastColumn && this.pageWidth > this.bodyWidth) {
let c = this._columnSections.count - 1;
let x = hw + this._columnSections.offsetOf(c);
this.paintContent(x, 0, vw - x, vh);
}
else if (delta < 0) {
this.paintContent(vw + delta, 0, -delta, vh);
}
// Repaint merged cells that are intersected by the resized column
// Otherwise it will be cut in two by the valid content, and drawn incorrectly
for (const rgn of ['body', 'column-header']) {
const cellGroups = CellGroup.getCellGroupsAtColumn(this.dataModel, rgn, index);
let paintRgn = {
region: rgn,
xMin: 0,
xMax: 0,
yMin: 0,
yMax: 0
};
let backgroundColor = undefined;
switch (rgn) {
case 'body':
paintRgn.xMin = this.headerWidth;
paintRgn.xMax = this.headerWidth + this.bodyWidth;
paintRgn.yMin = this.headerHeight;
paintRgn.yMax = this.headerHeight + this.bodyHeight;
backgroundColor = this._style.backgroundColor;
break;
case 'column-header':
paintRgn.xMin = this.headerWidth;
paintRgn.xMax = this.headerWidth + this.bodyWidth;
paintRgn.yMin = 0;
paintRgn.yMax = this.headerHeight;
backgroundColor = this._style.headerBackgroundColor;
break;
}
this._paintMergedCells(cellGroups, paintRgn, backgroundColor);
}
// Paint the overlay.
this._paintOverlay();
// Sync the scroll state after painting.
this._syncScrollState();
}
/**
* Resize a row header section immediately.
*/
_resizeRowHeader(index, size) {
// Look up the target section list.
let list = this._rowHeaderSections;
// Bail early if the index is out of range.
if (index < 0 || index >= list.count) {
return;
}
const adjustedSize = size !== null && size !== void 0 ? size : this._getMaxWidthInColumn(index, 'row-header');
if (!adjustedSize || adjustedSize == 0) {
return;
}
// Look up the old size of the section.
let oldSize = list.sizeOf(index);
// Normalize the new size of the section.
let newSize = list.clampSize(adjustedSize);
// Bail early if the size does not change.
if (oldSize === newSize) {
return;
}
// Resize the section in the list.
list.resize(index, newSize);
// Get the current size of the viewport.
let vw = this._viewportWidth;
let vh = this._viewportHeight;
// If there is nothing to paint, sync the scroll state.
if (!this._viewport.isVisible || vw === 0 || vh === 0) {
this._syncScrollState();
return;
}
// Compute the size delta.
let delta = newSize - oldSize;
// Look up the offset of the section.
let offset = list.offsetOf(index);
// Bail early if the section is fully outside the viewport.
if (offset >= vw) {
this._syncScrollState();
return;
}
// Paint the entire tail if the section spans the viewport.
if (offset + oldSize >= vw || offset + newSize >= vw) {
this.paintContent(offset, 0, vw - offset, vh);
this._paintOverlay();
this._syncScrollState();
return;
}
// Compute the blit content dimensions.
let sx = offset + oldSize;
let sy = 0;
let sw = vw - sx;
let sh = vh;
let dx = sx + delta;
let dy = 0;
// Blit the valid content to the destination.
this._blitContent(this._canvas, sx, sy, sw, sh, dx, dy);
// Repaint the header section if needed.
if (newSize > 0) {
this.paintContent(offset, 0, newSize, vh);
}
// Paint the trailing space as needed.
if (this._stretchLastColumn && this.pageWidth > this.bodyWidth) {
let c = this._columnSections.count - 1;
let x = this.headerWidth + this._columnSections.offsetOf(c);
this.paintContent(x, 0, vw - x, vh);
}
else if (delta < 0) {
this.paintContent(vw + delta, 0, -delta, vh);
}
// Repaint merged cells that are intersected by the resized row
// Otherwise it will be cut in two by the valid content, and drawn incorrectly
for (const rgn of [
'corner-header',
'row-header'
]) {
const cellGroups = CellGroup.getCellGroupsAtColumn(this.dataModel, rgn, index);
let paintRgn = {
region: rgn,
xMin: 0,
xMax: 0,
yMin: 0,
yMax: 0
};
switch (rgn) {
case 'corner-header':
paintRgn.xMin = 0;
paintRgn.xMax = this.headerWidth;
paintRgn.yMin = 0;
paintRgn.yMax = this.headerHeight;
break;
case 'row-header':
paintRgn.xMin = 0;
paintRgn.xMax = this.headerWidth;
paintRgn.yMin = this.headerHeight;
paintRgn.yMax = this.headerHeight + this.bodyHeight;
break;
}
this._paintMergedCells(cellGroups, paintRgn, this._style.headerBackgroundColor);
}
// Paint the overlay.
this._paintOverlay();
// Sync the scroll state after painting.
this._syncScrollState();
}
/**
* Resize a column header section immediately.
*/
_resizeColumnHeader(index, size) {
// Look up the target section list.
let list = this._columnHeaderSections;
// Bail early if the index is out of range.
if (index < 0 || index >= list.count) {
return;
}
// Look up the old size of the section.
let oldSize = list.sizeOf(index);
// Normalize the new size of the section.
let newSize = list.clampSize(size);
// Bail early if the size does not change.
if (oldSize === newSize) {
return;
}
// Resize the section in the list.
list.resize(index, newSize);
// Get the current size of the viewport.
let vw = this._viewportWidth;
let vh = this._viewportHeight;
// If there is nothing to paint, sync the scroll state.
if (!this._viewport.isVisible || vw === 0 || vh === 0) {
this._syncScrollState();
return;
}
// Paint the overlay.
this._paintOverlay();
// Compute the size delta.
let delta = newSize - oldSize;
// Look up the offset of the section.
let offset = list.offsetOf(index);
// Bail early if the section is fully outside the viewport.
if (offset >= vh) {
this._syncScrollState();
return;
}
// Paint the entire tail if the section spans the viewport.
if (offset + oldSize >= vh || offset + newSize >= vh) {
this.paintContent(0, offset, vw, vh - offset);
this._paintOverlay();
this._syncScrollState();
return;
}
// Compute the blit content dimensions.
let sx = 0;
let sy = offset + oldSize;
let sw = vw;
let sh = vh - sy;
let dx = 0;
let dy = sy + delta;
// Blit the valid contents to the destination.
this._blitContent(this._canvas, sx, sy, sw, sh, dx, dy);
// Repaint the header section if needed.
if (newSize > 0) {
this.paintContent(0, offset, vw, newSize);
}
// Paint the trailing space as needed.
if (this._stretchLastRow && this.pageHeight > this.bodyHeight) {
let r = this._rowSections.count - 1;
let y = this.headerHeight + this._rowSections.offsetOf(r);
this.paintContent(0, y, vw, vh - y);
}
else if (delta < 0) {
this.paintContent(0, vh + delta, vw, -delta);
}
// Repaint merged cells that are intersected by the resized row
// Otherwise it will be cut in two by the valid content, and drawn incorrectly
for (const rgn of [
'corner-header',
'column-header'
]) {
const cellGroups = CellGroup.getCellGroupsAtRow(this.dataModel, rgn, index);
let paintRgn = {
region: rgn,
xMin: 0,
xMax: 0,
yMin: 0,
yMax: 0
};
switch (rgn) {
case 'corner-header':
paintRgn.xMin = 0;
paintRgn.xMax = this.headerWidth;
paintRgn.yMin = 0;
paintRgn.yMax = this.headerHeight;
break;
case 'column-header':
paintRgn.xMin = this.headerWidth;
paintRgn.xMax = this.headerWidth + this.bodyWidth;
paintRgn.yMin = 0;
paintRgn.yMax = this.headerHeight;
break;
}
this._paintMergedCells(cellGroups, paintRgn, this._style.headerBackgroundColor);
}
// Paint the overlay.
this._paintOverlay();
// Sync the scroll state after painting.
this._syncScrollState();
}
/**
* Scroll immediately to the specified offset position.
*/
_scrollTo(x, y) {
// Bail if no data model found.
if (!this.dataModel) {
return;
}
// Floor and clamp the position to the allowable range.
x = Math.max(0, Math.min(Math.floor(x), this.maxScrollX));
y = Math.max(0, Math.min(Math.floor(y), this.maxScrollY));
// Synchronize the scroll bar values.
this._hScrollBar.value = x;
this._vScrollBar.value = y;
// Compute the delta scroll amount.
let dx = x - this._scrollX;
let dy = y - this._scrollY;
// Bail early if there is no effective scroll.
if (dx === 0 && dy === 0) {
return;
}
// Bail early if the viewport is not visible.
if (!this._viewport.isVisible) {
this._scrollX = x;
this._scrollY = y;
return;
}
// Get the current size of the viewport.
let width = this._viewportWidth;
let height = this._viewportHeight;
// Bail early if the viewport is empty.
if (width === 0 || height === 0) {
this._scrollX = x;
this._scrollY = y;
return;
}
// Get the visible content origin.
let contentX = this.headerWidth;
let contentY = this.headerHeight;
// Get the visible content dimensions.
let contentWidth = width - contentX;
let contentHeight = height - contentY;
// Bail early if there is no content to draw.
if (contentWidth <= 0 && contentHeight <= 0) {
this._scrollX = x;
this._scrollY = y;
return;
}
// Compute the area which needs painting for the `dx` scroll.
let dxArea = 0;
if (dx !== 0 && contentWidth > 0) {
if (Math.abs(dx) >= contentWidth) {
dxArea = contentWidth * height;
}
else {
dxArea = Math.abs(dx) * height;
}
}
// Compute the area which needs painting for the `dy` scroll.
let dyArea = 0;
if (dy !== 0 && contentHeight > 0) {
if (Math.abs(dy) >= contentHeight) {
dyArea = width * contentHeight;
}
else {
dyArea = width * Math.abs(dy);
}
}
// If the area sum is larger than the total, paint everything.
if (dxArea + dyArea >= width * height) {
this._scrollX = x;
this._scrollY = y;
this.paintContent(0, 0, width, height);
this._paintOverlay();
return;
}
// Update the internal Y scroll position.
this._scrollY = y;
// Scroll the Y axis if needed. If the scroll distance exceeds
// the visible height, paint everything. Otherwise, blit the
// valid content and paint the dirty region.
if (dy !== 0 && contentHeight > 0) {
if (Math.abs(dy) >= contentHeight) {
this.paintContent(0, contentY, width, contentHeight);
}
else {
const x = 0;
const y = dy < 0 ? contentY : contentY + dy;
const w = width;
const h = contentHeight - Math.abs(dy);
this._blitContent(this._canvas, x, y, w, h, x, y - dy);
this.paintContent(0, dy < 0 ? contentY : height - dy, width, Math.abs(dy));
// Repaint merged cells that are intersected by the scroll level
// Otherwise it will be cut in two by the valid content, and drawn incorrectly
for (const rgn of ['body', 'row-header']) {
const cellgroups = CellGroup.getCellGroupsAtRegion(this.dataModel, rgn);
let paintRgn = {
region: rgn,
xMin: 0,
xMax: 0,
yMin: 0,
yMax: 0
};
let backgroundColor = undefined;
switch (rgn) {
case 'body':
paintRgn.xMin = this.headerWidth;
paintRgn.xMax = this.headerWidth + this.bodyWidth;
paintRgn.yMin = this.headerHeight;
paintRgn.yMax = this.headerHeight + this.bodyHeight;
backgroundColor = this._style.backgroundColor;
break;
case 'row-header':
paintRgn.xMin = 0;
paintRgn.xMax = this.headerWidth;
paintRgn.yMin = this.headerHeight;
paintRgn.yMax = this.headerHeight + this.bodyHeight;
backgroundColor = this._style.headerBackgroundColor;
break;
}
this._paintMergedCells(cellgroups, paintRgn, backgroundColor);
}
}
}
// Update the internal X scroll position.
this._scrollX = x;
// Scroll the X axis if needed. If the scroll distance exceeds
// the visible width, paint everything. Otherwise, blit the
// valid content and paint the dirty region.
if (dx !== 0 && contentWidth > 0) {
if (Math.abs(dx) >= contentWidth) {
this.paintContent(contentX, 0, contentWidth, height);
}
else {
const x = dx < 0 ? contentX : contentX + dx;
const y = 0;
const w = contentWidth - Math.abs(dx);
const h = height;
this._blitContent(this._canvas, x, y, w, h, x - dx, y);
this.paintContent(dx < 0 ? contentX : width - dx, 0, Math.abs(dx), height);
// Repaint merged cells that are intersected by the scroll level
// Otherwise it will be cut in two by the valid content, and drawn incorrectly
for (const rgn of ['body', 'column-header']) {
const cellGroups = CellGroup.getCellGroupsAtRegion(this.dataModel, rgn);
let paintRgn = {
region: rgn,
xMin: 0,
xMax: 0,
yMin: 0,
yMax: 0
};
let backgroundColor = undefined;
switch (rgn) {
case 'body':
paintRgn.xMin = this.headerWidth;
paintRgn.xMax = this.headerWidth + this.bodyWidth;
paintRgn.yMin = this.headerHeight;
paintRgn.yMax = this.headerHeight + this.bodyHeight;
backgroundColor = this._style.backgroundColor;
break;
case 'column-header':
paintRgn.xMin = this.headerWidth;
paintRgn.xMax = this.headerWidth + this.bodyWidth;
paintRgn.yMin = 0;
paintRgn.yMax = this.headerHeight;
backgroundColor = this._style.headerBackgroundColor;
break;
}
this._paintMergedCells(cellGroups, paintRgn, backgroundColor);
}
}
}
// Paint the overlay.
this._paintOverlay();
}
/**
* Blit content into the on-screen grid canvas.
*
* The rect should be expressed in viewport coordinates.
*
* This automatically accounts for the dpi ratio.
*/
_blitContent(source, x, y, w, h, dx, dy) {
// Scale the blit coordinates by the dpi ratio.
x *= this._dpiRatio;
y *= this._dpiRatio;
w *= this._dpiRatio;
h *= this._dpiRatio;
dx *= this._dpiRatio;
dy *= this._dpiRatio;
// Save the current gc state.
this._canvasGC.save();
// Set the transform to the identity matrix.
this._canvasGC.setTransform(1, 0, 0, 1, 0, 0);
// Draw the specified content.
this._canvasGC.drawImage(source, x, y, w, h, dx, dy, w, h);
// Restore the gc state.
this._canvasGC.restore();
}
/**
* Paint the grid content for the given dirty rect.
*
* The rect should be expressed in valid viewport coordinates.
*
* This is the primary paint entry point. The individual `_draw*`
* methods should not be invoked directly. This method dispatches
* to the drawing methods in the correct order.
*/
paintContent(rx, ry, rw, rh) {
// Scale the canvas and buffer GC for the dpi ratio.
this._canvasGC.setTransform(this._dpiRatio, 0, 0, this._dpiRatio, 0, 0);
this._bufferGC.setTransform(this._dpiRatio, 0, 0, this._dpiRatio, 0, 0);
// Clear the dirty rect of all content.
this._canvasGC.clearRect(rx, ry, rw, rh);
// Draw the void region.
this._drawVoidRegion(rx, ry, rw, rh);
// Draw the body region.
this._drawBodyRegion(rx, ry, rw, rh);
// Draw the row header region.
this._drawRowHeaderRegion(rx, ry, rw, rh);
// Draw the column header region.
this._drawColumnHeaderRegion(rx, ry, rw, rh);
// Draw the corner header region.
this.drawCornerHeaderRegion(rx, ry, rw, rh);
}
/**
* Resizes body column headers so their text fits
* without clipping or wrapping.
* @param dataModel
*/
_fitBodyColumnHeaders(dataModel, padding, numCols) {
// Get the body column count
const bodyColumnCount = numCols === undefined ? dataModel.columnCount('body') : numCols;
for (let i = 0; i < bodyColumnCount; i++) {
/*
if we're working with nested column headers,
retrieve the nested levels and iterate on them.
*/
const numRows = dataModel.rowCount('column-header');
/*
Calculate the maximum text width, across
all nested rows under a given column number.
*/
let maxWidth = 0;
for (let j = 0; j < numRows; j++) {
const config = DataGrid._getConfig(dataModel, j, i, 'column-header');
const textWidth = this._getCellTextWidth(config);
// Update the maximum width for that column.
maxWidth = Math.max(maxWidth, textWidth);
}
/*
Send a resize message with new width for the given column.
Using a padding of 15 pixels to leave some room.
*/
this.resizeColumn('body', i, maxWidth + padding);
}
}
/**
* Resizes row header columns so their text fits
* without clipping or wrapping.
* @param dataModel
*/
_fitRowColumnHeaders(dataModel, padding, numCols) {
/*
if we're working with nested row headers,
retrieve the nested levels and iterate on them.
*/
const rowColumnCount = numCols === undefined ? dataModel.columnCount('row-header') : numCols;
for (let i = 0; i < rowColumnCount; i++) {
const numCols = dataModel.rowCount('column-header');
/*
Calculate the maximum text width, across
all nested columns under a given row index.
*/
let maxWidth = 0;
for (let j = 0; j < numCols; j++) {
const config = DataGrid._getConfig(dataModel, j, i, 'corner-header');
const textWidth = this._getCellTextWidth(config);
maxWidth = Math.max(maxWidth, textWidth);
}
/*
Send a resize message with new width for the given column.
Using a padding of 15 pixels to leave some room.
*/
this.resizeColumn('row-header', i, maxWidth + padding);
}
}
/**
* Paint the overlay content for the entire grid.
*
* This is the primary overlay paint entry point. The individual
* `_draw*` methods should not be invoked directly. This method
* dispatches to the drawing methods in the correct order.
*/
_paintOverlay() {
// Scale the overlay GC for the dpi ratio.
this._overlayGC.setTransform(this._dpiRatio, 0, 0, this._dpiRatio, 0, 0);
// Clear the overlay of all content.
this._overlayGC.clearRect(0, 0, this._overlay.width, this._overlay.height);
// Draw the body selections.
this._drawBodySelections();
// Draw the row header selections.
this._drawRowHeaderSelections();
// Draw the column header selections.
this._drawColumnHeaderSelections();
// Draw the cursor.
this._drawCursor();
// Draw the shadows.
this._drawShadows();
}
/**
* Draw the void region for the dirty rect.
*/
_drawVoidRegion(rx, ry, rw, rh) {
// Look up the void color.
let color = this._style.voidColor;
// Bail if there is no void color.
if (!color) {
return;
}
// Fill the dirty rect with the void color.
this._canvasGC.fillStyle = color;
this._canvasGC.fillRect(rx, ry, rw, rh);
}
/**
* Draw the body region which intersects the dirty rect.
*/
_drawBodyRegion(rx, ry, rw, rh) {
// Get the visible content dimensions.
let contentW = this._columnSections.length - this._scrollX;
let contentH = this._rowSections.length - this._scrollY;
// Bail if there is no content to draw.
if (contentW <= 0 || contentH <= 0) {
return;
}
// Get the visible content origin.
let contentX = this.headerWidth;
let contentY = this.headerHeight;
// Bail if the dirty rect does not intersect the content area.
if (rx + rw <= contentX) {
return;
}
if (ry + rh <= contentY) {
return;
}
if (rx >= contentX + contentW) {
return;
}
if (ry >= contentY + contentH) {
return;
}
// Fetch the geometry.
let bh = this.bodyHeight;
let bw = this.bodyWidth;
let ph = this.pageHeight;
let pw = this.pageWidth;
// Get the upper and lower bounds of the dirty content area.
let x1 = Math.max(rx, contentX);
let y1 = Math.max(ry, contentY);
let x2 = Math.min(rx + rw - 1, contentX + contentW - 1);
let y2 = Math.min(ry + rh - 1, contentY + contentH - 1);
// Convert the dirty content bounds into cell bounds.
let r1 = this._rowSections.indexOf(y1 - contentY + this._scrollY);
let c1 = this._columnSections.indexOf(x1 - contentX + this._scrollX);
let r2 = this._rowSections.indexOf(y2 - contentY + this._scrollY);
let c2 = this._columnSections.indexOf(x2 - contentX + this._scrollX);
// Fetch the max row and column.
let maxRow = this._rowSections.count - 1;
let maxColumn = this._columnSections.count - 1;
// Handle a dirty content area larger than the cell count.
if (r2 < 0) {
r2 = maxRow;
}
if (c2 < 0) {
c2 = maxColumn;
}
// Convert the cell bounds back to visible coordinates.
let x = this._columnSections.offsetOf(c1) + contentX - this._scrollX;
let y = this._rowSections.offsetOf(r1) + contentY - this._scrollY;
// Set up the paint region size variables.
let width = 0;
let height = 0;
// Allocate the section sizes arrays.
let rowSizes = new Array(r2 - r1 + 1);
let columnSizes = new Array(c2 - c1 + 1);
// Get the row sizes for the region.
for (let j = r1; j <= r2; ++j) {
let size = this._rowSections.sizeOf(j);
rowSizes[j - r1] = size;
height += size;
}
// Get the column sizes for the region.
for (let i = c1; i <= c2; ++i) {
let size = this._columnSections.sizeOf(i);
columnSizes[i - c1] = size;
width += size;
}
// Adjust the geometry if the last row is streched.
if (this._stretchLastRow && ph > bh && r2 === maxRow) {
let dh = this.pageHeight - this.bodyHeight;
rowSizes[rowSizes.length - 1] += dh;
height += dh;
y2 += dh;
}
// Adjust the geometry if the last column is streched.
if (this._stretchLastColumn && pw > bw && c2 === maxColumn) {
let dw = this.pageWidth - this.bodyWidth;
columnSizes[columnSizes.length - 1] += dw;
width += dw;
x2 += dw;
}
// Create the paint region object.
let rgn = {
region: 'body',
xMin: x1,
yMin: y1,
xMax: x2,
yMax: y2,
x,
y,
width,
height,
row: r1,
column: c1,
rowSizes,
columnSizes
};
// Draw the background.
this._drawBackground(rgn, this._style.backgroundColor);
// Draw the row background.
this._drawRowBackground(rgn, this._style.rowBackgroundColor);
// Draw the column background.
this._drawColumnBackground(rgn, this._style.columnBackgroundColor);
// Draw the cell content for the paint region.
this._drawCells(rgn);
// Draw the horizontal grid lines.
this._drawHorizontalGridLines(rgn, this._style.horizontalGridLineColor || this._style.gridLineColor);
// Draw the vertical grid lines.
this._drawVerticalGridLines(rgn, this._style.verticalGridLineColor || this._style.gridLineColor);
// Get the cellgroups from the cell-region that intersects with the paint region
const cellGroups = CellGroup.getCellGroupsAtRegion(this.dataModel, rgn.region).filter(group => {
return this.cellGroupInteresectsRegion(group, rgn);
});
// Draw merged cells
this._paintMergedCells(cellGroups, rgn, this._style.backgroundColor);
}
/**
* Draw the row header region which intersects the dirty rect.
*/
_drawRowHeaderRegion(rx, ry, rw, rh) {
// Get the visible content dimensions.
let contentW = this.headerWidth;
let contentH = this.bodyHeight - this._scrollY;
// Bail if there is no content to draw.
if (contentW <= 0 || contentH <= 0) {
return;
}
// Get the visible content origin.
let contentX = 0;
let contentY = this.headerHeight;
// Bail if the dirty rect does not intersect the content area.
if (rx + rw <= contentX) {
return;
}
if (ry + rh <= contentY) {
return;
}
if (rx >= contentX + contentW) {
return;
}
if (ry >= contentY + contentH) {
return;
}
// Fetch the geometry.
let bh = this.bodyHeight;
let ph = this.pageHeight;
// Get the upper and lower bounds of the dirty content area.
let x1 = rx;
let y1 = Math.max(ry, contentY);
let x2 = Math.min(rx + rw - 1, contentX + contentW - 1);
let y2 = Math.min(ry + rh - 1, contentY + contentH - 1);
// Convert the dirty content bounds into cell bounds.
let r1 = this._rowSections.indexOf(y1 - contentY + this._scrollY);
let c1 = this._rowHeaderSections.indexOf(x1);
let r2 = this._rowSections.indexOf(y2 - contentY + this._scrollY);
let c2 = this._rowHeaderSections.indexOf(x2);
// Fetch max row and column.
let maxRow = this._rowSections.count - 1;
let maxColumn = this._rowHeaderSections.count - 1;
// Handle a dirty content area larger than the cell count.
if (r2 < 0) {
r2 = maxRow;
}
if (c2 < 0) {
c2 = maxColumn;
}
// Convert the cell bounds back to visible coordinates.
let x = this._rowHeaderSections.offsetOf(c1);
let y = this._rowSections.offsetOf(r1) + contentY - this._scrollY;
// Set up the paint region size variables.
let width = 0;
let height = 0;
// Allocate the section sizes arrays.
let rowSizes = new Array(r2 - r1 + 1);
let columnSizes = new Array(c2 - c1 + 1);
// Get the row sizes for the region.
for (let j = r1; j <= r2; ++j) {
let size = this._rowSections.sizeOf(j);
rowSizes[j - r1] = size;
height += size;
}
// Get the column sizes for the region.
for (let i = c1; i <= c2; ++i) {
let size = this._rowHeaderSections.sizeOf(i);
columnSizes[i - c1] = size;
width += size;
}
// Adjust the geometry if the last row is stretched.
if (this._stretchLastRow && ph > bh && r2 === maxRow) {
let dh = this.pageHeight - this.bodyHeight;
rowSizes[rowSizes.length - 1] += dh;
height += dh;
y2 += dh;
}
// Create the paint region object.
let rgn = {
region: 'row-header',
xMin: x1,
yMin: y1,
xMax: x2,
yMax: y2,
x,
y,
width,
height,
row: r1,
column: c1,
rowSizes,
columnSizes
};
// Draw the background.
this._drawBackground(rgn, this._style.headerBackgroundColor);
// Draw the cell content for the paint region.
this._drawCells(rgn);
// Draw the horizontal grid lines.
this._drawHorizontalGridLines(rgn, this._style.headerHorizontalGridLineColor ||
this._style.headerGridLineColor);
// Draw the vertical grid lines.
this._drawVerticalGridLines(rgn, this._style.headerVerticalGridLineColor || this._style.headerGridLineColor);
// Get the cellgroups from the cell-region that intersects with the paint region
const cellGroups = CellGroup.getCellGroupsAtRegion(this.dataModel, rgn.region).filter(group => {
return this.cellGroupInteresectsRegion(group, rgn);
});
// Draw merged cells
this._paintMergedCells(cellGroups, rgn, this._style.headerBackgroundColor);
}
/**
* Draw the column header region which intersects the dirty rect.
*/
_drawColumnHeaderRegion(rx, ry, rw, rh) {
// Get the visible content dimensions.
let contentW = this.bodyWidth - this._scrollX;
let contentH = this.headerHeight;
// Bail if there is no content to draw.
if (contentW <= 0 || contentH <= 0) {
return;
}
// Get the visible content origin.
let contentX = this.headerWidth;
let contentY = 0;
// Bail if the dirty rect does not intersect the content area.
if (rx + rw <= contentX) {
return;
}
if (ry + rh <= contentY) {
return;
}
if (rx >= contentX + contentW) {
return;
}
if (ry >= contentY + contentH) {
return;
}
// Fetch the geometry.
let bw = this.bodyWidth;
let pw = this.pageWidth;
// Get the upper and lower bounds of the dirty content area.
let x1 = Math.max(rx, contentX);
let y1 = ry;
let x2 = Math.min(rx + rw - 1, contentX + contentW - 1);
let y2 = Math.min(ry + rh - 1, contentY + contentH - 1);
// Convert the dirty content bounds into cell bounds.
let r1 = this._columnHeaderSections.indexOf(y1);
let c1 = this._columnSections.indexOf(x1 - contentX + this._scrollX);
let r2 = this._columnHeaderSections.indexOf(y2);
let c2 = this._columnSections.indexOf(x2 - contentX + this._scrollX);
// Fetch the max row and column.
let maxRow = this._columnHeaderSections.count - 1;
let maxColumn = this._columnSections.count - 1;
// Handle a dirty content area larger than the cell count.
if (r2 < 0) {
r2 = maxRow;
}
if (c2 < 0) {
c2 = maxColumn;
}
// Convert the cell bounds back to visible coordinates.
let x = this._columnSections.offsetOf(c1) + contentX - this._scrollX;
let y = this._columnHeaderSections.offsetOf(r1);
// Set up the paint region size variables.
let width = 0;
let height = 0;
// Allocate the section sizes arrays.
let rowSizes = new Array(r2 - r1 + 1);
let columnSizes = new Array(c2 - c1 + 1);
// Get the row sizes for the region.
for (let j = r1; j <= r2; ++j) {
let size = this._columnHeaderSections.sizeOf(j);
rowSizes[j - r1] = size;
height += size;
}
// Get the column sizes for the region.
for (let i = c1; i <= c2; ++i) {
let size = this._columnSections.sizeOf(i);
columnSizes[i - c1] = size;
width += size;
}
// Adjust the geometry if the last column is stretched.
if (this._stretchLastColumn && pw > bw && c2 === maxColumn) {
let dw = this.pageWidth - this.bodyWidth;
columnSizes[columnSizes.length - 1] += dw;
width += dw;
x2 += dw;
}
// Create the paint region object.
let rgn = {
region: 'column-header',
xMin: x1,
yMin: y1,
xMax: x2,
yMax: y2,
x,
y,
width,
height,
row: r1,
column: c1,
rowSizes,
columnSizes
};
// Draw the background.
this._drawBackground(rgn, this._style.headerBackgroundColor);
// Draw the cell content for the paint region.
this._drawCells(rgn);
// Draw the horizontal grid lines.
this._drawHorizontalGridLines(rgn, this._style.headerHorizontalGridLineColor ||
this._style.headerGridLineColor);
// Draw the vertical grid lines.
this._drawVerticalGridLines(rgn, this._style.headerVerticalGridLineColor || this._style.headerGridLineColor);
// Get the cellgroups from the cell-region that intersects with the paint region
const cellGroups = CellGroup.getCellGroupsAtRegion(this.dataModel, rgn.region).filter(group => {
return this.cellGroupInteresectsRegion(group, rgn);
});
// Draw merged cells
this._paintMergedCells(cellGroups, rgn, this._style.headerBackgroundColor);
}
/**
* Draw the corner header region which intersects the dirty rect.
*/
drawCornerHeaderRegion(rx, ry, rw, rh) {
// Get the visible content dimensions.
let contentW = this.headerWidth;
let contentH = this.headerHeight;
// Bail if there is no content to draw.
if (contentW <= 0 || contentH <= 0) {
return;
}
// Get the visible content origin.
let contentX = 0;
let contentY = 0;
// Bail if the dirty rect does not intersect the content area.
if (rx + rw <= contentX) {
return;
}
if (ry + rh <= contentY) {
return;
}
if (rx >= contentX + contentW) {
return;
}
if (ry >= contentY + contentH) {
return;
}
// Get the upper and lower bounds of the dirty content area.
let x1 = rx;
let y1 = ry;
let x2 = Math.min(rx + rw - 1, contentX + contentW - 1);
let y2 = Math.min(ry + rh - 1, contentY + contentH - 1);
// Convert the dirty content bounds into cell bounds.
let r1 = this._columnHeaderSections.indexOf(y1);
let c1 = this._rowHeaderSections.indexOf(x1);
let r2 = this._columnHeaderSections.indexOf(y2);
let c2 = this._rowHeaderSections.indexOf(x2);
// Handle a dirty content area larger than the cell count.
if (r2 < 0) {
r2 = this._columnHeaderSections.count - 1;
}
if (c2 < 0) {
c2 = this._rowHeaderSections.count - 1;
}
// Convert the cell bounds back to visible coordinates.
let x = this._rowHeaderSections.offsetOf(c1);
let y = this._columnHeaderSections.offsetOf(r1);
// Set up the paint region size variables.
let width = 0;
let height = 0;
// Allocate the section sizes arrays.
let rowSizes = new Array(r2 - r1 + 1);
let columnSizes = new Array(c2 - c1 + 1);
// Get the row sizes for the region.
for (let j = r1; j <= r2; ++j) {
let size = this._columnHeaderSections.sizeOf(j);
rowSizes[j - r1] = size;
height += size;
}
// Get the column sizes for the region.
for (let i = c1; i <= c2; ++i) {
let size = this._rowHeaderSections.sizeOf(i);
columnSizes[i - c1] = size;
width += size;
}
// Create the paint region object.
let rgn = {
region: 'corner-header',
xMin: x1,
yMin: y1,
xMax: x2,
yMax: y2,
x,
y,
width,
height,
row: r1,
column: c1,
rowSizes,
columnSizes
};
// Draw the background.
this._drawBackground(rgn, this._style.headerBackgroundColor);
// Draw the cell content for the paint region.
this._drawCells(rgn);
// Draw the horizontal grid lines.
this._drawHorizontalGridLines(rgn, this._style.headerHorizontalGridLineColor ||
this._style.headerGridLineColor);
// Draw the vertical grid lines.
this._drawVerticalGridLines(rgn, this._style.headerVerticalGridLineColor || this._style.headerGridLineColor);
// Get the cellgroups from the cell-region that intersects with the paint region
const cellGroups = CellGroup.getCellGroupsAtRegion(this.dataModel, rgn.region).filter(group => {
return this.cellGroupInteresectsRegion(group, rgn);
});
// Draw merged cells
this._paintMergedCells(cellGroups, rgn, this._style.headerBackgroundColor);
}
/**
* Draw the background for the given paint region.
*/
_drawBackground(rgn, color) {
// Bail if there is no color to draw.
if (!color) {
return;
}
// Unpack the region.
let { xMin, yMin, xMax, yMax } = rgn;
// Fill the region with the specified color.
this._canvasGC.fillStyle = color;
this._canvasGC.fillRect(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1);
}
/**
* Draw the row background for the given paint region.
*/
_drawRowBackground(rgn, colorFn) {
// Bail if there is no color function.
if (!colorFn) {
return;
}
// Compute the X bounds for the row.
let x1 = Math.max(rgn.xMin, rgn.x);
let x2 = Math.min(rgn.x + rgn.width - 1, rgn.xMax);
// Draw the background for the rows in the region.
for (let y = rgn.y, j = 0, n = rgn.rowSizes.length; j < n; ++j) {
// Fetch the size of the row.
let size = rgn.rowSizes[j];
// Skip zero sized rows.
if (size === 0) {
continue;
}
// Get the background color for the row.
let color = colorFn(rgn.row + j);
// Fill the row with the background color if needed.
if (color) {
let y1 = Math.max(rgn.yMin, y);
let y2 = Math.min(y + size - 1, rgn.yMax);
this._canvasGC.fillStyle = color;
this._canvasGC.fillRect(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
}
// Increment the running Y coordinate.
y += size;
}
}
/**
* Draw the column background for the given paint region.
*/
_drawColumnBackground(rgn, colorFn) {
// Bail if there is no color function.
if (!colorFn) {
return;
}
// Compute the Y bounds for the column.
let y1 = Math.max(rgn.yMin, rgn.y);
let y2 = Math.min(rgn.y + rgn.height - 1, rgn.yMax);
// Draw the background for the columns in the region.
for (let x = rgn.x, i = 0, n = rgn.columnSizes.length; i < n; ++i) {
// Fetch the size of the column.
let size = rgn.columnSizes[i];
// Skip zero sized columns.
if (size === 0) {
continue;
}
// Get the background color for the column.
let color = colorFn(rgn.column + i);
// Fill the column with the background color if needed.
if (color) {
let x1 = Math.max(rgn.xMin, x);
let x2 = Math.min(x + size - 1, rgn.xMax);
this._canvasGC.fillStyle = color;
this._canvasGC.fillRect(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
}
// Increment the running X coordinate.
x += size;
}
}
/**
* Returns column size
* @param region
* @param index
*/
_getColumnSize(region, index) {
if (region === 'corner-header') {
return this._rowHeaderSections.sizeOf(index);
}
return this.columnSize(region, index);
}
/**
* Returns row size
* @param region
* @param index
*/
_getRowSize(region, index) {
if (region === 'corner-header') {
return this._columnHeaderSections.sizeOf(index);
}
return this.rowSize(region, index);
}
/**
* Draw the cells for the given paint region.
*/
_drawCells(rgn) {
// Bail if there is no data model.
if (!this._dataModel) {
return;
}
// Set up the cell config object for rendering.
let config = {
x: 0,
y: 0,
width: 0,
height: 0,
region: rgn.region,
row: 0,
column: 0,
value: null,
metadata: DataModel.emptyMetadata
};
let groupIndex = -1;
// Save the buffer gc before wrapping.
this._bufferGC.save();
// Wrap the buffer gc for painting the cells.
let gc = new GraphicsContext(this._bufferGC);
let height = 0;
// Loop over the columns in the region.
for (let x = rgn.x, i = 0, n = rgn.columnSizes.length; i < n; ++i) {
// Fetch the size of the column.
let width = rgn.columnSizes[i];
// Skip zero sized columns.
if (width === 0) {
continue;
}
// Compute the column index.
let column = rgn.column + i;
// Update the config for the current column.
config.x = x;
config.width = width;
config.column = column;
// Loop over the rows in the column.
for (let y = rgn.y, j = 0, n = rgn.rowSizes.length; j < n; ++j) {
// Fetch the size of the row.
height = rgn.rowSizes[j];
// Skip zero sized rows.
if (height === 0) {
continue;
}
// Compute the row index.
let row = rgn.row + j;
groupIndex = CellGroup.getGroupIndex(this.dataModel, config.region, row, column);
// For merged cell regions, don't do anything, we draw merged regions later.
if (groupIndex !== -1) {
y += height;
continue;
}
// Clear the buffer rect for the cell.
gc.clearRect(x, y, width, height);
let value = DataGrid._getCellValue(this.dataModel, rgn.region, row, column);
let metadata = DataGrid._getCellMetadata(this.dataModel, rgn.region, row, column);
// Update the config for the current cell.
config.y = y;
config.height = height;
config.width = width;
config.row = row;
config.value = value;
config.metadata = metadata;
// Get the renderer for the cell.
let renderer = this._cellRenderers.get(config);
// Save the GC state.
gc.save();
// Paint the cell into the off-screen buffer.
try {
if (renderer instanceof AsyncCellRenderer) {
if (renderer.isReady(config)) {
renderer.paint(gc, config);
}
else {
renderer.paintPlaceholder(gc, config);
renderer.load(config).then(() => {
const r1 = row;
const r2 = row + 1;
const c1 = column;
const c2 = column + 1;
this.repaintRegion(rgn.region, r1, c1, r2, c2);
});
}
}
else {
renderer.paint(gc, config);
}
}
catch (err) {
console.error(err);
}
// Restore the GC state.
gc.restore();
// Compute the actual X bounds for the cell.
let x1 = Math.max(rgn.xMin, config.x);
let x2 = Math.min(config.x + config.width - 1, rgn.xMax);
// Compute the actual Y bounds for the cell.
let y1 = Math.max(rgn.yMin, config.y);
let y2 = Math.min(config.y + config.height - 1, rgn.yMax);
this._blitContent(this._buffer, x1, y1, x2 - x1 + 1, y2 - y1 + 1, x1, y1);
// Increment the running Y coordinate.
y += height;
}
// Restore the GC state.
gc.restore();
// Increment the running X coordinate.
x += width;
}
// Dispose of the wrapped gc.
gc.dispose();
// Restore the final buffer gc state.
this._bufferGC.restore();
}
// TODO Move this in the utils file (but we need the PaintRegion typing)
cellGroupInteresectsRegion(group, rgn) {
const rgnR1 = rgn.row;
const rgnR2 = rgn.row + rgn.rowSizes.length;
const rgnC1 = rgn.column;
const rgnC2 = rgn.column + rgn.columnSizes.length;
const dx = Math.min(group.r2, rgnR2) - Math.max(group.r1, rgnR1);
const dy = Math.min(group.c2, rgnC2) - Math.max(group.c1, rgnC1);
return dx >= 0 && dy >= 0;
}
static _getCellValue(dm, region, row, col) {
// Get the value for the cell.
try {
return dm.data(region, row, col);
}
catch (err) {
console.error(err);
return null;
}
}
static _getCellMetadata(dm, region, row, col) {
// Get the metadata for the cell.
try {
return dm.metadata(region, row, col);
}
catch (err) {
console.error(err);
return DataModel.emptyMetadata;
}
}
/**
* Paint group cells.
*/
_paintMergedCells(cellGroups, rgn, backgroundColor) {
// Bail if there is no data model.
if (!this._dataModel) {
return;
}
// Set up the cell config object for rendering.
let config = {
x: 0,
y: 0,
width: 0,
height: 0,
region: rgn.region,
row: 0,
column: 0,
value: null,
metadata: DataModel.emptyMetadata
};
if (backgroundColor) {
this._canvasGC.fillStyle = backgroundColor;
}
// Set the line width for the grid lines.
this._canvasGC.lineWidth = 1;
// Save the buffer gc before wrapping.
this._bufferGC.save();
// Wrap the buffer gc for painting the cells.
let gc = new GraphicsContext(this._bufferGC);
for (const group of cellGroups) {
let width = 0;
for (let c = group.c1; c <= group.c2; c++) {
width += this._getColumnSize(rgn.region, c);
}
let height = 0;
for (let r = group.r1; r <= group.r2; r++) {
height += this._getRowSize(rgn.region, r);
}
let value = DataGrid._getCellValue(this.dataModel, rgn.region, group.r1, group.c1);
let metadata = DataGrid._getCellMetadata(this.dataModel, rgn.region, group.r1, group.c2);
let x = 0;
let y = 0;
switch (rgn.region) {
case 'body':
x =
this._columnSections.offsetOf(group.c1) +
this.headerWidth -
this._scrollX;
y =
this._rowSections.offsetOf(group.r1) +
this.headerHeight -
this._scrollY;
break;
case 'column-header':
x =
this._columnSections.offsetOf(group.c1) +
this.headerWidth -
this._scrollX;
y = this._rowSections.offsetOf(group.r1);
break;
case 'row-header':
x = this._columnSections.offsetOf(group.c1);
y =
this._rowSections.offsetOf(group.r1) +
this.headerHeight -
this._scrollY;
break;
case 'corner-header':
x = this._columnSections.offsetOf(group.c1);
y = this._rowSections.offsetOf(group.r1);
break;
}
config.x = x;
config.y = y;
config.width = width;
config.height = height;
config.region = rgn.region;
config.row = group.r1;
config.column = group.c1;
config.value = value;
config.metadata = metadata;
// Compute the actual X bounds for the cell.
const x1 = Math.max(rgn.xMin, x);
const x2 = Math.min(x + width - 2, rgn.xMax);
// Compute the actual Y bounds for the cell.
const y1 = Math.max(rgn.yMin, y);
const y2 = Math.min(y + height - 2, rgn.yMax);
if (x2 <= x1 || y2 <= y1) {
continue;
}
// Draw the background.
if (backgroundColor) {
this._canvasGC.fillRect(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
}
// Get the renderer for the cell.
let renderer = this._cellRenderers.get(config);
// Clear the buffer rect for the cell.
gc.clearRect(config.x, config.y, width, height);
// Save the GC state.
gc.save();
// Paint the cell into the off-screen buffer.
try {
if (renderer instanceof AsyncCellRenderer) {
if (renderer.isReady(config)) {
renderer.paint(gc, config);
}
else {
renderer.paintPlaceholder(gc, config);
const r1 = group.r1;
const r2 = group.r2;
const c1 = group.c1;
const c2 = group.c2;
renderer.load(config).then(() => {
this.repaintRegion(rgn.region, r1, c1, r2, c2);
});
}
}
else {
renderer.paint(gc, config);
}
}
catch (err) {
console.error(err);
}
// Restore the GC state.
gc.restore();
this._blitContent(this._buffer, x1, y1, x2 - x1 + 1, y2 - y1 + 1, x1, y1);
}
// Dispose of the wrapped gc.
gc.dispose();
// Restore the final buffer gc state.
this._bufferGC.restore();
}
/**
* Draw the horizontal grid lines for the given paint region.
*/
_drawHorizontalGridLines(rgn, color) {
// Bail if there is no color to draw.
if (!color) {
return;
}
// Compute the X bounds for the horizontal lines.
const x1 = Math.max(rgn.xMin, rgn.x);
const x2 = Math.min(rgn.x + rgn.width, rgn.xMax + 1);
// Begin the path for the grid lines.
this._canvasGC.beginPath();
// Set the line width for the grid lines.
this._canvasGC.lineWidth = 1;
// Fetch the geometry.
const bh = this.bodyHeight;
const ph = this.pageHeight;
// Fetch the number of grid lines to be drawn.
let n = rgn.rowSizes.length;
// Adjust the count down if the last line shouldn't be drawn.
if (this._stretchLastRow && ph > bh) {
if (rgn.row + n === this._rowSections.count) {
n -= 1;
}
}
// Draw the horizontal grid lines.
for (let y = rgn.y, j = 0; j < n; ++j) {
// Fetch the size of the row.
let size = rgn.rowSizes[j];
// Skip zero sized rows.
if (size === 0) {
continue;
}
// Compute the Y position of the line.
let pos = y + size - 1;
// Draw the line if it's in range of the dirty rect.
if (pos >= rgn.yMin && pos <= rgn.yMax) {
this._canvasGC.moveTo(x1, pos + 0.5);
this._canvasGC.lineTo(x2, pos + 0.5);
}
// Increment the running Y coordinate.
y += size;
}
// Stroke the lines with the specified color.
this._canvasGC.strokeStyle = color;
this._canvasGC.stroke();
}
/**
* Draw the vertical grid lines for the given paint region.
*/
_drawVerticalGridLines(rgn, color) {
// Bail if there is no color to draw.
if (!color) {
return;
}
// Compute the Y bounds for the vertical lines.
const y1 = Math.max(rgn.yMin, rgn.y);
const y2 = Math.min(rgn.y + rgn.height, rgn.yMax + 1);
// Begin the path for the grid lines
this._canvasGC.beginPath();
// Set the line width for the grid lines.
this._canvasGC.lineWidth = 1;
// Fetch the geometry.
const bw = this.bodyWidth;
const pw = this.pageWidth;
// Fetch the number of grid lines to be drawn.
let n = rgn.columnSizes.length;
// Adjust the count down if the last line shouldn't be drawn.
if (this._stretchLastColumn && pw > bw) {
if (rgn.column + n === this._columnSections.count) {
n -= 1;
}
}
// Draw the vertical grid lines.
for (let x = rgn.x, i = 0; i < n; ++i) {
// Fetch the size of the column.
let size = rgn.columnSizes[i];
// Skip zero sized columns.
if (size === 0) {
continue;
}
// Compute the X position of the line.
let pos = x + size - 1;
// Draw the line if it's in range of the dirty rect.
if (pos >= rgn.xMin && pos <= rgn.xMax) {
this._canvasGC.moveTo(pos + 0.5, y1);
this._canvasGC.lineTo(pos + 0.5, y2);
}
// Increment the running X coordinate.
x += size;
}
// Stroke the lines with the specified color.
this._canvasGC.strokeStyle = color;
this._canvasGC.stroke();
}
/**
* Draw the body selections for the data grid.
*/
_drawBodySelections() {
// Fetch the selection model.
let model = this._selectionModel;
// Bail early if there are no selections.
if (!model || model.isEmpty) {
return;
}
// Fetch the selection colors.
let fill = this._style.selectionFillColor;
let stroke = this._style.selectionBorderColor;
// Bail early if there is nothing to draw.
if (!fill && !stroke) {
return;
}
// Fetch the scroll geometry.
let sx = this._scrollX;
let sy = this._scrollY;
// Get the first visible cell of the grid.
let r1 = this._rowSections.indexOf(sy);
let c1 = this._columnSections.indexOf(sx);
// Bail early if there are no visible cells.
if (r1 < 0 || c1 < 0) {
return;
}
// Fetch the extra geometry.
let bw = this.bodyWidth;
let bh = this.bodyHeight;
let pw = this.pageWidth;
let ph = this.pageHeight;
let hw = this.headerWidth;
let hh = this.headerHeight;
// Get the last visible cell of the grid.
let r2 = this._rowSections.indexOf(sy + ph);
let c2 = this._columnSections.indexOf(sx + pw);
// Fetch the max row and column.
let maxRow = this._rowSections.count - 1;
let maxColumn = this._columnSections.count - 1;
// Clamp the last cell if the void space is visible.
r2 = r2 < 0 ? maxRow : r2;
c2 = c2 < 0 ? maxColumn : c2;
// Fetch the overlay gc.
let gc = this._overlayGC;
// Save the gc state.
gc.save();
// Set up the body clipping rect.
gc.beginPath();
gc.rect(hw, hh, pw, ph);
gc.clip();
// Set up the gc style.
if (fill) {
gc.fillStyle = fill;
}
if (stroke) {
gc.strokeStyle = stroke;
gc.lineWidth = 1;
}
// Iterate over the selections.
for (let s of model.selections()) {
// Skip the section if it's not visible.
if (s.r1 < r1 && s.r2 < r1) {
continue;
}
if (s.r1 > r2 && s.r2 > r2) {
continue;
}
if (s.c1 < c1 && s.c2 < c1) {
continue;
}
if (s.c1 > c2 && s.c2 > c2) {
continue;
}
// Clamp the cell to the model bounds.
let sr1 = Math.max(0, Math.min(s.r1, maxRow));
let sc1 = Math.max(0, Math.min(s.c1, maxColumn));
let sr2 = Math.max(0, Math.min(s.r2, maxRow));
let sc2 = Math.max(0, Math.min(s.c2, maxColumn));
// Swap index order if needed.
let tmp;
if (sr1 > sr2) {
tmp = sr1;
sr1 = sr2;
sr2 = tmp;
}
if (sc1 > sc2) {
tmp = sc1;
sc1 = sc2;
sc2 = tmp;
}
const joinedGroup = CellGroup.joinCellGroupWithMergedCellGroups(this.dataModel, { r1: sr1, r2: sr2, c1: sc1, c2: sc2 }, 'body');
sr1 = joinedGroup.r1;
sr2 = joinedGroup.r2;
sc1 = joinedGroup.c1;
sc2 = joinedGroup.c2;
// Convert to pixel coordinates.
let x1 = this._columnSections.offsetOf(sc1) - sx + hw;
let y1 = this._rowSections.offsetOf(sr1) - sy + hh;
let x2 = this._columnSections.extentOf(sc2) - sx + hw;
let y2 = this._rowSections.extentOf(sr2) - sy + hh;
// Adjust the trailing X coordinate for column stretch.
if (this._stretchLastColumn && pw > bw && sc2 === maxColumn) {
x2 = hw + pw - 1;
}
// Adjust the trailing Y coordinate for row stretch.
if (this._stretchLastRow && ph > bh && sr2 === maxRow) {
y2 = hh + ph - 1;
}
// Clamp the bounds to just outside of the clipping rect.
x1 = Math.max(hw - 1, x1);
y1 = Math.max(hh - 1, y1);
x2 = Math.min(hw + pw + 1, x2);
y2 = Math.min(hh + ph + 1, y2);
// Skip zero sized ranges.
if (x2 < x1 || y2 < y1) {
continue;
}
// Fill the rect if needed.
if (fill) {
gc.fillRect(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
}
// Stroke the rect if needed.
if (stroke) {
gc.strokeRect(x1 - 0.5, y1 - 0.5, x2 - x1 + 1, y2 - y1 + 1);
}
}
// Restore the gc state.
gc.restore();
}
/**
* Draw the row header selections for the data grid.
*/
_drawRowHeaderSelections() {
// Fetch the selection model.
let model = this._selectionModel;
// Bail early if there are no selections or if the selectionMode is the entire column.
if (!model || model.isEmpty || model.selectionMode == 'column') {
return;
}
// Bail early if the row headers are not visible.
if (this.headerWidth === 0 || this.pageHeight === 0) {
return;
}
// Fetch the selection colors.
let fill = this._style.headerSelectionFillColor;
let stroke = this._style.headerSelectionBorderColor;
// Bail early if there is nothing to draw.
if (!fill && !stroke) {
return;
}
// Fetch common geometry.
let sy = this._scrollY;
let bh = this.bodyHeight;
let ph = this.pageHeight;
let hw = this.headerWidth;
let hh = this.headerHeight;
let rs = this._rowSections;
// Fetch the overlay gc.
let gc = this._overlayGC;
// Save the gc state.
gc.save();
// Set up the header clipping rect.
gc.beginPath();
gc.rect(0, hh, hw, ph);
gc.clip();
// Set up the gc style.
if (fill) {
gc.fillStyle = fill;
}
if (stroke) {
gc.strokeStyle = stroke;
gc.lineWidth = 1;
}
// Fetch the max row.
let maxRow = rs.count - 1;
// Fetch the visible rows.
let r1 = rs.indexOf(sy);
let r2 = rs.indexOf(sy + ph - 1);
r2 = r2 < 0 ? maxRow : r2;
// Iterate over the visible rows.
for (let j = r1; j <= r2; ++j) {
// Skip rows which aren't selected.
if (!model.isRowSelected(j)) {
continue;
}
// Get the dimensions of the row.
let y = rs.offsetOf(j) - sy + hh;
let h = rs.sizeOf(j);
// Adjust the height for row stretch.
if (this._stretchLastRow && ph > bh && j === maxRow) {
h = hh + ph - y;
}
// Skip zero sized rows.
if (h === 0) {
continue;
}
// Fill the rect if needed.
if (fill) {
gc.fillRect(0, y, hw, h);
}
// Draw the border if needed.
if (stroke) {
gc.beginPath();
gc.moveTo(hw - 0.5, y - 1);
gc.lineTo(hw - 0.5, y + h);
gc.stroke();
}
}
// Restore the gc state.
gc.restore();
}
/**
* Draw the column header selections for the data grid.
*/
_drawColumnHeaderSelections() {
// Fetch the selection model.
let model = this._selectionModel;
// Bail early if there are no selections or if the selectionMode is the entire row
if (!model || model.isEmpty || model.selectionMode == 'row') {
return;
}
// Bail early if the column headers are not visible.
if (this.headerHeight === 0 || this.pageWidth === 0) {
return;
}
// Fetch the selection colors.
let fill = this._style.headerSelectionFillColor;
let stroke = this._style.headerSelectionBorderColor;
// Bail early if there is nothing to draw.
if (!fill && !stroke) {
return;
}
// Fetch common geometry.
let sx = this._scrollX;
let bw = this.bodyWidth;
let pw = this.pageWidth;
let hw = this.headerWidth;
let hh = this.headerHeight;
let cs = this._columnSections;
// Fetch the overlay gc.
let gc = this._overlayGC;
// Save the gc state.
gc.save();
// Set up the header clipping rect.
gc.beginPath();
gc.rect(hw, 0, pw, hh);
gc.clip();
// Set up the gc style.
if (fill) {
gc.fillStyle = fill;
}
if (stroke) {
gc.strokeStyle = stroke;
gc.lineWidth = 1;
}
// Fetch the max column.
let maxCol = cs.count - 1;
// Fetch the visible columns.
let c1 = cs.indexOf(sx);
let c2 = cs.indexOf(sx + pw - 1);
c2 = c2 < 0 ? maxCol : c2;
// Iterate over the visible columns.
for (let i = c1; i <= c2; ++i) {
// Skip columns which aren't selected.
if (!model.isColumnSelected(i)) {
continue;
}
// Get the dimensions of the column.
let x = cs.offsetOf(i) - sx + hw;
let w = cs.sizeOf(i);
// Adjust the width for column stretch.
if (this._stretchLastColumn && pw > bw && i === maxCol) {
w = hw + pw - x;
}
// Skip zero sized columns.
if (w === 0) {
continue;
}
// Fill the rect if needed.
if (fill) {
gc.fillRect(x, 0, w, hh);
}
// Draw the border if needed.
if (stroke) {
gc.beginPath();
gc.moveTo(x - 1, hh - 0.5);
gc.lineTo(x + w, hh - 0.5);
gc.stroke();
}
}
// Restore the gc state.
gc.restore();
}
/**
* Draw the overlay cursor for the data grid.
*/
_drawCursor() {
// Fetch the selection model.
let model = this._selectionModel;
// Bail early if there is no cursor.
if (!model || model.isEmpty || model.selectionMode !== 'cell') {
return;
}
// Extract the style information.
let fill = this._style.cursorFillColor;
let stroke = this._style.cursorBorderColor;
// Bail early if there is nothing to draw.
if (!fill && !stroke) {
return;
}
// Fetch the cursor location.
let startRow = model.cursorRow;
let startColumn = model.cursorColumn;
// Fetch the max row and column.
let maxRow = this._rowSections.count - 1;
let maxColumn = this._columnSections.count - 1;
// Bail early if the cursor is out of bounds.
if (startRow < 0 || startRow > maxRow) {
return;
}
if (startColumn < 0 || startColumn > maxColumn) {
return;
}
let endRow = startRow;
let endColumn = startColumn;
const joinedGroup = CellGroup.joinCellGroupWithMergedCellGroups(this.dataModel, { r1: startRow, r2: endRow, c1: startColumn, c2: endColumn }, 'body');
startRow = joinedGroup.r1;
endRow = joinedGroup.r2;
startColumn = joinedGroup.c1;
endColumn = joinedGroup.c2;
// Fetch geometry.
let sx = this._scrollX;
let sy = this._scrollY;
let bw = this.bodyWidth;
let bh = this.bodyHeight;
let pw = this.pageWidth;
let ph = this.pageHeight;
let hw = this.headerWidth;
let hh = this.headerHeight;
let vw = this._viewportWidth;
let vh = this._viewportHeight;
// Get the cursor bounds in viewport coordinates.
let x1 = this._columnSections.offsetOf(startColumn) - sx + hw;
let x2 = this._columnSections.extentOf(endColumn) - sx + hw;
let y1 = this._rowSections.offsetOf(startRow) - sy + hh;
let y2 = this._rowSections.extentOf(endRow) - sy + hh;
// Adjust the trailing X coordinate for column stretch.
if (this._stretchLastColumn && pw > bw && startColumn === maxColumn) {
x2 = vw - 1;
}
// Adjust the trailing Y coordinate for row stretch.
if (this._stretchLastRow && ph > bh && startRow === maxRow) {
y2 = vh - 1;
}
// Skip zero sized cursors.
if (x2 < x1 || y2 < y1) {
return;
}
// Bail early if the cursor is off the screen.
if (x1 - 1 >= vw || y1 - 1 >= vh || x2 + 1 < hw || y2 + 1 < hh) {
return;
}
// Fetch the overlay gc.
let gc = this._overlayGC;
// Save the gc state.
gc.save();
// Set up the body clipping rect.
gc.beginPath();
gc.rect(hw, hh, pw, ph);
gc.clip();
// Clear any existing overlay content.
gc.clearRect(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
// Fill the cursor rect if needed.
if (fill) {
// Set up the fill style.
gc.fillStyle = fill;
// Fill the cursor rect.
gc.fillRect(x1, y1, x2 - x1 + 1, y2 - y1 + 1);
}
// Stroke the cursor border if needed.
if (stroke) {
// Set up the stroke style.
gc.strokeStyle = stroke;
gc.lineWidth = 2;
// Stroke the cursor rect.
gc.strokeRect(x1, y1, x2 - x1, y2 - y1);
}
// Restore the gc state.
gc.restore();
}
/**
* Draw the overlay shadows for the data grid.
*/
_drawShadows() {
// Fetch the scroll shadow from the style.
let shadow = this._style.scrollShadow;
// Bail early if there is no shadow to draw.
if (!shadow) {
return;
}
// Fetch the scroll position.
let sx = this._scrollX;
let sy = this._scrollY;
// Fetch maximum scroll position.
let sxMax = this.maxScrollX;
let syMax = this.maxScrollY;
// Fetch the header width and height.
let hw = this.headerWidth;
let hh = this.headerHeight;
// Fetch the page width and height.
let pw = this.pageWidth;
let ph = this.pageHeight;
// Fetch the viewport width and height.
let vw = this._viewportWidth;
let vh = this._viewportHeight;
// Fetch the body width and height.
let bw = this.bodyWidth;
let bh = this.bodyHeight;
// Adjust the body size for row and column stretch.
if (this._stretchLastRow && ph > bh) {
bh = ph;
}
if (this._stretchLastColumn && pw > bw) {
bw = pw;
}
// Fetch the gc object.
let gc = this._overlayGC;
// Save the gc state.
gc.save();
// Draw the column header shadow if needed.
if (sy > 0) {
// Set up the gradient coordinates.
let x0 = 0;
let y0 = hh;
let x1 = 0;
let y1 = y0 + shadow.size;
// Create the gradient object.
let grad = gc.createLinearGradient(x0, y0, x1, y1);
// Set the gradient stops.
grad.addColorStop(0, shadow.color1);
grad.addColorStop(0.5, shadow.color2);
grad.addColorStop(1, shadow.color3);
// Set up the rect coordinates.
let x = 0;
let y = hh;
let w = hw + Math.min(pw, bw - sx);
let h = shadow.size;
// Fill the shadow rect with the fill style.
gc.fillStyle = grad;
gc.fillRect(x, y, w, h);
}
// Draw the row header shadow if needed.
if (sx > 0) {
// Set up the gradient coordinates.
let x0 = hw;
let y0 = 0;
let x1 = x0 + shadow.size;
let y1 = 0;
// Create the gradient object.
let grad = gc.createLinearGradient(x0, y0, x1, y1);
// Set the gradient stops.
grad.addColorStop(0, shadow.color1);
grad.addColorStop(0.5, shadow.color2);
grad.addColorStop(1, shadow.color3);
// Set up the rect coordinates.
let x = hw;
let y = 0;
let w = shadow.size;
let h = hh + Math.min(ph, bh - sy);
// Fill the shadow rect with the fill style.
gc.fillStyle = grad;
gc.fillRect(x, y, w, h);
}
// Draw the column footer shadow if needed.
if (sy < syMax) {
// Set up the gradient coordinates.
let x0 = 0;
let y0 = vh;
let x1 = 0;
let y1 = vh - shadow.size;
// Create the gradient object.
let grad = gc.createLinearGradient(x0, y0, x1, y1);
// Set the gradient stops.
grad.addColorStop(0, shadow.color1);
grad.addColorStop(0.5, shadow.color2);
grad.addColorStop(1, shadow.color3);
// Set up the rect coordinates.
let x = 0;
let y = vh - shadow.size;
let w = hw + Math.min(pw, bw - sx);
let h = shadow.size;
// Fill the shadow rect with the fill style.
gc.fillStyle = grad;
gc.fillRect(x, y, w, h);
}
// Draw the row footer shadow if needed.
if (sx < sxMax) {
// Set up the gradient coordinates.
let x0 = vw;
let y0 = 0;
let x1 = vw - shadow.size;
let y1 = 0;
// Create the gradient object.
let grad = gc.createLinearGradient(x0, y0, x1, y1);
// Set the gradient stops.
grad.addColorStop(0, shadow.color1);
grad.addColorStop(0.5, shadow.color2);
grad.addColorStop(1, shadow.color3);
// Set up the rect coordinates.
let x = vw - shadow.size;
let y = 0;
let w = shadow.size;
let h = hh + Math.min(ph, bh - sy);
// Fill the shadow rect with the fill style.
gc.fillStyle = grad;
gc.fillRect(x, y, w, h);
}
// Restore the gc state.
gc.restore();
}
}
/**
* The namespace for the `DataGrid` class statics.
*/
(function (DataGrid) {
/**
* A generic format function for the copy handler.
*
* @param args - The format args for the function.
*
* @returns The string representation of the value.
*
* #### Notes
* This function uses `String()` to coerce a value to a string.
*/
function copyFormatGeneric(args) {
if (args.value === null || args.value === undefined) {
return '';
}
return String(args.value);
}
DataGrid.copyFormatGeneric = copyFormatGeneric;
/**
* The default theme for a data grid.
*/
DataGrid.defaultStyle = {
voidColor: '#F3F3F3',
backgroundColor: '#FFFFFF',
gridLineColor: 'rgba(20, 20, 20, 0.15)',
headerBackgroundColor: '#F3F3F3',
headerGridLineColor: 'rgba(20, 20, 20, 0.25)',
selectionFillColor: 'rgba(49, 119, 229, 0.2)',
selectionBorderColor: 'rgba(0, 107, 247, 1.0)',
cursorBorderColor: 'rgba(0, 107, 247, 1.0)',
headerSelectionFillColor: 'rgba(20, 20, 20, 0.1)',
headerSelectionBorderColor: 'rgba(0, 107, 247, 1.0)',
scrollShadow: {
size: 10,
color1: 'rgba(0, 0, 0, 0.20)',
color2: 'rgba(0, 0, 0, 0.05)',
color3: 'rgba(0, 0, 0, 0.00)'
}
};
/**
* The default sizes for a data grid.
*/
DataGrid.defaultSizes = {
rowHeight: 20,
columnWidth: 64,
rowHeaderWidth: 64,
columnHeaderHeight: 20
};
/**
* The default minimum sizes for a data grid.
*/
DataGrid.minimumSizes = {
rowHeight: 20,
columnWidth: 10,
rowHeaderWidth: 10,
columnHeaderHeight: 20
};
/**
* The default copy config for a data grid.
*/
DataGrid.defaultCopyConfig = {
separator: '\t',
format: copyFormatGeneric,
headers: 'none',
warningThreshold: 1e6
};
})(DataGrid || (DataGrid = {}));
/**
* The namespace for the module implementation details.
*/
var Private$1;
(function (Private) {
/**
* A singleton `scroll-request` conflatable message.
*/
Private.ScrollRequest = new _lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.ConflatableMessage('scroll-request');
/**
* A singleton `overlay-paint-request` conflatable message.
*/
Private.OverlayPaintRequest = new _lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.ConflatableMessage('overlay-paint-request');
/**
* Create a new zero-sized canvas element.
*/
function createCanvas() {
let canvas = document.createElement('canvas');
canvas.width = 0;
canvas.height = 0;
return canvas;
}
Private.createCanvas = createCanvas;
/**
* Checks whether a given regions has merged cells in it.
* @param dataModel grid's data model.
* @param region the paint region to be checked.
* @returns boolean.
*/
function regionHasMergedCells(dataModel, region) {
const regionGroups = CellGroup.getCellGroupsAtRegion(dataModel, region);
return regionGroups.length > 0;
}
Private.regionHasMergedCells = regionHasMergedCells;
/**
* A conflatable message which merges dirty paint regions.
*/
class PaintRequest extends _lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.ConflatableMessage {
/**
* Construct a new paint request messages.
*
* @param region - The cell region for the paint.
*
* @param r1 - The top-left row of the dirty region.
*
* @param c1 - The top-left column of the dirty region.
*
* @param r2 - The bottom-right row of the dirty region.
*
* @param c2 - The bottom-right column of the dirty region.
*/
constructor(region, r1, c1, r2, c2) {
super('paint-request');
this._region = region;
this._r1 = r1;
this._c1 = c1;
this._r2 = r2;
this._c2 = c2;
}
/**
* The cell region for the paint.
*/
get region() {
return this._region;
}
/**
* The top-left row of the dirty region.
*/
get r1() {
return this._r1;
}
/**
* The top-left column of the dirty region.
*/
get c1() {
return this._c1;
}
/**
* The bottom-right row of the dirty region.
*/
get r2() {
return this._r2;
}
/**
* The bottom-right column of the dirty region.
*/
get c2() {
return this._c2;
}
/**
* Conflate this message with another paint request.
*/
conflate(other) {
// Bail early if the request is already painting everything.
if (this._region === 'all') {
return true;
}
// Any region can conflate with the `'all'` region.
if (other._region === 'all') {
this._region = 'all';
return true;
}
// Otherwise, do not conflate with a different region.
if (this._region !== other._region) {
return false;
}
// Conflate the region to the total boundary.
this._r1 = Math.min(this._r1, other._r1);
this._c1 = Math.min(this._c1, other._c1);
this._r2 = Math.max(this._r2, other._r2);
this._c2 = Math.max(this._c2, other._c2);
return true;
}
}
Private.PaintRequest = PaintRequest;
/**
* A conflatable message for resizing rows.
*/
class RowResizeRequest extends _lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.ConflatableMessage {
/**
* Construct a new row resize request.
*
* @param region - The row region which holds the section.
*
* @param index - The index of row in the region.
*
* @param size - The target size of the section.
*/
constructor(region, index, size) {
super('row-resize-request');
this._region = region;
this._index = index;
this._size = size;
}
/**
* The row region which holds the section.
*/
get region() {
return this._region;
}
/**
* The index of the row in the region.
*/
get index() {
return this._index;
}
/**
* The target size of the section.
*/
get size() {
return this._size;
}
/**
* Conflate this message with another row resize request.
*/
conflate(other) {
if (this._region !== other._region || this._index !== other._index) {
return false;
}
this._size = other._size;
return true;
}
}
Private.RowResizeRequest = RowResizeRequest;
/**
* A conflatable message for resizing columns.
*/
class ColumnResizeRequest extends _lumino_messaging__WEBPACK_IMPORTED_MODULE_6__.ConflatableMessage {
/**
* Construct a new column resize request.
*
* @param region - The column region which holds the section.
*
* @param index - The index of column in the region.
*
* @param size - The target size of the section.
* If null, then infer the size to fit.
*/
constructor(region, index, size) {
super('column-resize-request');
this._region = region;
this._index = index;
this._size = size;
}
/**
* The column region which holds the section.
*/
get region() {
return this._region;
}
/**
* The index of the column in the region.
*/
get index() {
return this._index;
}
/**
* The target size of the section.
*/
get size() {
return this._size;
}
/**
* Conflate this message with another column resize request.
*/
conflate(other) {
if (this._region !== other._region || this._index !== other._index) {
return false;
}
this._size = other._size;
return true;
}
}
Private.ColumnResizeRequest = ColumnResizeRequest;
})(Private$1 || (Private$1 = {}));
/**
* A data model implementation for in-memory JSON data.
*/
class JSONModel extends DataModel {
/**
* Create a data model with static JSON data.
*
* @param options - The options for initializing the data model.
*/
constructor(options) {
super();
let split = Private.splitFields(options.schema);
this._data = options.data;
this._bodyFields = split.bodyFields;
this._headerFields = split.headerFields;
this._missingValues = Private.createMissingMap(options.schema);
}
/**
* Get the row count for a region in the data model.
*
* @param region - The row region of interest.
*
* @returns - The row count for the region.
*/
rowCount(region) {
if (region === 'body') {
return this._data.length;
}
return 1; // TODO multiple column-header rows?
}
/**
* Get the column count for a region in the data model.
*
* @param region - The column region of interest.
*
* @returns - The column count for the region.
*/
columnCount(region) {
if (region === 'body') {
return this._bodyFields.length;
}
return this._headerFields.length;
}
/**
* Get the data value for a cell in the data model.
*
* @param region - The cell region of interest.
*
* @param row - The row index of the cell of interest.
*
* @param column - The column index of the cell of interest.
*
* @returns - The data value for the specified cell.
*
* #### Notes
* A `missingValue` as defined by the schema is converted to `null`.
*/
data(region, row, column) {
// Set up the field and value variables.
let field;
let value;
// Look up the field and value for the region.
switch (region) {
case 'body':
field = this._bodyFields[column];
value = this._data[row][field.name];
break;
case 'column-header':
field = this._bodyFields[column];
value = field.title || field.name;
break;
case 'row-header':
field = this._headerFields[column];
value = this._data[row][field.name];
break;
case 'corner-header':
field = this._headerFields[column];
value = field.title || field.name;
break;
default:
throw 'unreachable';
}
// Test whether the value is a missing value.
let missing = this._missingValues !== null &&
typeof value === 'string' &&
this._missingValues[value] === true;
// Return the final value.
return missing ? null : value;
}
/**
* Get the metadata for a cell in the data model.
*
* @param region - The cell region of interest.
*
* @param row - The row index of the cell of of interest.
*
* @param column - The column index of the cell of interest.
*
* @returns The metadata for the cell.
*/
metadata(region, row, column) {
if (region === 'body' || region === 'column-header') {
return this._bodyFields[column];
}
return this._headerFields[column];
}
}
/**
* The namespace for the module implementation details.
*/
var Private;
(function (Private) {
/**
* Split the schema fields into header and body fields.
*/
function splitFields(schema) {
// Normalize the primary keys.
let primaryKeys;
if (schema.primaryKey === undefined) {
primaryKeys = [];
}
else if (typeof schema.primaryKey === 'string') {
primaryKeys = [schema.primaryKey];
}
else {
primaryKeys = schema.primaryKey;
}
// Separate the fields for the body and header.
let bodyFields = [];
let headerFields = [];
for (let field of schema.fields) {
if (primaryKeys.indexOf(field.name) === -1) {
bodyFields.push(field);
}
else {
headerFields.push(field);
}
}
// Return the separated fields.
return { bodyFields, headerFields };
}
Private.splitFields = splitFields;
/**
* Create a missing values map for a schema.
*
* This returns `null` if there are no missing values.
*/
function createMissingMap(schema) {
// Bail early if there are no missing values.
if (!schema.missingValues || schema.missingValues.length === 0) {
return null;
}
// Collect the missing values into a map.
let result = Object.create(null);
for (let value of schema.missingValues) {
result[value] = true;
}
// Return the populated map.
return result;
}
Private.createMissingMap = createMissingMap;
})(Private || (Private = {}));
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2023, Lumino Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
const PERCENTAGE_REGEX = /^(\d+(\.\d+)?)%$/;
const PIXEL_REGEX = /^(\d+(\.\d+)?)px$/;
/**
* A cell renderer which renders data values as images.
*/
class ImageRenderer extends AsyncCellRenderer {
/**
* Construct a new text renderer.
*
* @param options - The options for initializing the renderer.
*/
constructor(options = {}) {
super();
this.backgroundColor = options.backgroundColor || '';
this.textColor = options.textColor || '#000000';
this.placeholder = options.placeholder || '...';
this.width = options.width || '';
// Not using the || operator, because the empty string '' is a valid value
this.height = options.height === undefined ? '100%' : options.height;
}
/**
* Whether the renderer is ready or not for that specific config.
* If it's not ready, the datagrid will paint the placeholder.
* If it's ready, the datagrid will paint the image synchronously.
*
* @param config - The configuration data for the cell.
*
* @returns Whether the renderer is ready for this config or not.
*/
isReady(config) {
return (!config.value || ImageRenderer.dataCache.get(config.value) !== undefined);
}
/**
* Load the image asynchronously for a specific config.
*
* @param config - The configuration data for the cell.
*/
async load(config) {
// Bail early if there is nothing to do
if (!config.value) {
return;
}
const value = config.value;
const loadedPromise = new _lumino_coreutils__WEBPACK_IMPORTED_MODULE_7__.PromiseDelegate();
ImageRenderer.dataCache.set(value, undefined);
const img = new Image();
img.onload = () => {
ImageRenderer.dataCache.set(value, img);
loadedPromise.resolve();
};
img.src = value;
return loadedPromise.promise;
}
/**
* Paint the placeholder for a cell, waiting for the renderer to be ready.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
paintPlaceholder(gc, config) {
this.drawBackground(gc, config);
this.drawPlaceholder(gc, config);
}
/**
* Paint the content for a cell.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
paint(gc, config) {
this.drawBackground(gc, config);
this.drawImage(gc, config);
}
/**
* Draw the background for the cell.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
drawBackground(gc, config) {
// Resolve the background color for the cell.
const color = CellRenderer.resolveOption(this.backgroundColor, config);
// Bail if there is no background color to draw.
if (!color) {
return;
}
// Fill the cell with the background color.
gc.fillStyle = color;
gc.fillRect(config.x, config.y, config.width, config.height);
}
/**
* Draw the placeholder for the cell.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
drawPlaceholder(gc, config) {
const placeholder = CellRenderer.resolveOption(this.placeholder, config);
const color = CellRenderer.resolveOption(this.textColor, config);
const textX = config.x + config.width / 2;
const textY = config.y + config.height / 2;
// Draw the placeholder.
gc.fillStyle = color;
gc.fillText(placeholder, textX, textY);
}
/**
* Draw the image for the cell.
*
* @param gc - The graphics context to use for drawing.
*
* @param config - The configuration data for the cell.
*/
drawImage(gc, config) {
// Bail early if there is nothing to draw
if (!config.value) {
return;
}
const img = ImageRenderer.dataCache.get(config.value);
// If it's not loaded yet, show the placeholder
if (!img) {
return this.drawPlaceholder(gc, config);
}
const width = CellRenderer.resolveOption(this.width, config);
const height = CellRenderer.resolveOption(this.height, config);
// width and height are unset, we display the image with its original size
if (!width && !height) {
gc.drawImage(img, config.x, config.y);
return;
}
let requestedWidth = img.width;
let requestedHeight = img.height;
let widthPercentageMatch;
let widthPixelMatch;
let heightPercentageMatch;
let heightPixelMatch;
if ((widthPercentageMatch = width.match(PERCENTAGE_REGEX))) {
requestedWidth =
(parseFloat(widthPercentageMatch[1]) / 100) * config.width;
}
else if ((widthPixelMatch = width.match(PIXEL_REGEX))) {
requestedWidth = parseFloat(widthPixelMatch[1]);
}
if ((heightPercentageMatch = height.match(PERCENTAGE_REGEX))) {
requestedHeight =
(parseFloat(heightPercentageMatch[1]) / 100) * config.height;
}
else if ((heightPixelMatch = height.match(PIXEL_REGEX))) {
requestedHeight = parseFloat(heightPixelMatch[1]);
}
// If width is not set, we compute it respecting the image size ratio
if (!width) {
requestedWidth = (img.width / img.height) * requestedHeight;
}
// If height is not set, we compute it respecting the image size ratio
if (!height) {
requestedHeight = (img.height / img.width) * requestedWidth;
}
gc.drawImage(img, config.x, config.y, requestedWidth, requestedHeight);
}
}
ImageRenderer.dataCache = new Map();
//# sourceMappingURL=index.es6.js.map
/***/ })
}]);
//# sourceMappingURL=8929.f522b600b8907f9241c6.js.map?v=f522b600b8907f9241c6