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
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
|