From 0fc5f174aef3edb1341a4db82ea7f28526c13b3a Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 3 Oct 2023 16:37:57 +0700 Subject: [PATCH 01/48] initial Spreadsheet module --- src/js/core/modules/optional.js | 3 +- src/js/modules/Spreadsheet/Spreadsheet.js | 162 ++++++++++++++++++++++ src/scss/tabulator.scss | 29 ++++ 3 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/js/modules/Spreadsheet/Spreadsheet.js diff --git a/src/js/core/modules/optional.js b/src/js/core/modules/optional.js index 04b309319..3fc6a2df8 100644 --- a/src/js/core/modules/optional.js +++ b/src/js/core/modules/optional.js @@ -31,5 +31,6 @@ export {default as ResizeTableModule} from '../../modules/ResizeTable/ResizeTabl export {default as ResponsiveLayoutModule} from '../../modules/ResponsiveLayout/ResponsiveLayout.js'; export {default as SelectRowModule} from '../../modules/SelectRow/SelectRow.js'; export {default as SortModule} from '../../modules/Sort/Sort.js'; +export {default as SpreadsheetModule} from '../../modules/Spreadsheet/Spreadsheet.js'; export {default as TooltipModule} from '../../modules/Tooltip/Tooltip.js'; -export {default as ValidateModule} from '../../modules/Validate/Validate.js'; \ No newline at end of file +export {default as ValidateModule} from '../../modules/Validate/Validate.js'; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js new file mode 100644 index 000000000..382fef5f1 --- /dev/null +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -0,0 +1,162 @@ +import Module from "../../core/Module.js"; + +class Spreadsheet extends Module { + constructor(table) { + super(table); + this.registerTableOption("spreadsheet", false); //enable Spreadsheet mode + this.registerTableOption("spreadsheetRowHeader", "row-id"); //table header field to identify row + this.rowCounter = 1; + this.selectStart = { row: 1, col: 1 }; + this.selectEnd = { row: 1, col: 1 }; + } + + initialize() { + if (!this.table.options.spreadsheet) return; + + // Avoid clashing with SelectRow module + if (this.table.options.spreadsheet && this.table.options.selectable) { + console.warn( + "Spreadsheet Warning - disable SelectRow module to use spreadsheet.", + "Tips: Add this to your table options `selectable: false`.", + ); + return; + } + + this.table.options.columns = [ + { + title: "", + field: this.options("spreadsheetRowHeader"), + headerSort: false, + resizable: false, + frozen: true, + editable: false, + cssClass: "tabulator-row-header", + }, + ...this.table.options.columns, + ]; + + this.subscribe("row-init", this.initializeRow.bind(this)); + this.subscribe("cell-format", this.formatCell.bind(this)); + this.subscribe("cell-click", this.clickCell.bind(this)); + this.subscribe("cell-layout", this.layoutCell.bind(this)); + + this.table.element.classList.add("tabulator-spreadsheet"); + } + + layoutCell(cell) { + var el = cell.getElement(); + el.classList.toggle("tabulator-cell-selected", this.isSelectedCell(cell)); + el.classList.toggle( + "tabulator-select-range-start", + this.isSelectedStartCell(cell), + ); + el.classList.toggle( + "tabulator-select-range-end", + this.isSelectedEndCell(cell), + ); + } + + layoutRow(row) { + row + .getElement() + .classList.toggle("tabulator-row-selected", this.selectingRow(row.id)); + row.cells.forEach((cell) => { + this.layoutCell(cell); + }); + } + + selectingRow(index) { + return ( + this.selectStart.col === 1 && + this.selectEnd.col === this.table.columnManager.columns.length - 1 && + index >= this.selectStart.row && + index <= this.selectEnd.row + ); + } + + isSelectedCell(cell) { + const columnIndex = this.table.columnManager.findColumnIndex(cell.column); + const rowIndex = cell.row.id; + return ( + rowIndex >= this.selectStart.row && + rowIndex <= this.selectEnd.row && + columnIndex >= this.selectStart.col && + columnIndex <= this.selectEnd.col + ); + } + + isSelectedStartCell(cell) { + const columnIndex = this.table.columnManager.findColumnIndex(cell.column); + const rowIndex = cell.row.id; + return ( + rowIndex === this.selectStart.row && columnIndex === this.selectStart.col + ); + } + + isSelectedEndCell(cell) { + const columnIndex = this.table.columnManager.findColumnIndex(cell.column); + const rowIndex = cell.row.id; + return ( + rowIndex === this.selectEnd.row && columnIndex === this.selectEnd.col + ); + } + + clickCell(event, cell) { + if (event.shiftKey) this.selectRange(cell); + else this.selectCell(cell); + } + + selectCell(cell) { + var col = this.table.columnManager.findColumnIndex(cell.column); + var row = cell.row.id; + if (cell.column.field === this.options("spreadsheetRowHeader")) { + this.selectStart = { col: col + 1, row }; + this.selectEnd = { + col: this.table.columnManager.getColumns().length - 1, + row, + }; + } else { + this.selectStart = { col, row }; + this.selectEnd = { col, row }; + } + this.layoutVisible(); + } + + selectRange(cell) { + var col = this.table.columnManager.findColumnIndex(cell.column); + var row = cell.row.id; + if (cell.column.field === this.options("spreadsheetRowHeader")) { + this.selectEnd = { + col: this.table.columnManager.getColumns().length - 1, + row, + }; + } else { + this.selectEnd = { col, row }; + } + this.layoutVisible(); + } + + layoutVisible() { + var visibleRows = this.table.rowManager.getVisibleRows(true); + visibleRows.forEach((row) => { + if (row.type === "row") { + this.layoutRow(row); + } + }); + } + + initializeRow(row) { + row.id = this.rowCounter++; + } + + formatCell(cell, value) { + if (cell.column.field === this.options("spreadsheetRowHeader")) { + return cell.row.id; + } + return value; + } +} + +Spreadsheet.moduleName = "spreadsheet"; + +export default Spreadsheet; diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index c8d46bf58..b54086cdd 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -25,6 +25,9 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered +$rowHeaderSelectedBackground: #4990F1 !default; //row header background color when selected (spreadsheet module) +$cellSelectedBackground: #C2D5EF !default; //cell background color when selected (spreadsheet module) +$cellSelectedBorderColor: #2975DD !default; //cell border color when selected (spreadsheet module) $editBoxColor:#1D68CD !default; //border color for edit boxes $errorColor:#dd0000 !default; //error indication @@ -1499,3 +1502,29 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ } } +// Spreadsheet module styling + +.tabulator-spreadsheet{ + & .tabulator-cell{ + user-select: none; + position: relative; + + &.tabulator-select-range-start.tabulator-select-range-end::after{ + content: ''; + position: absolute; + inset: 0; + border: 2px solid $cellSelectedBorderColor; + } + &.tabulator-cell-selected:not(.tabulator-select-range-start.tabulator-select-range-end):not(.tabulator-row-header){ + background-color: $cellSelectedBackground; + } + } + & .tabulator-row{ + &:has(.tabulator-cell-selected) .tabulator-row-header{ + background-color: $rowSelectedBackground; + } + &.tabulator-row-selected .tabulator-row-header{ + background-color: $rowHeaderSelectedBackground; + } + } +} From 7b7e99306b8b9666f9afa965a40d56a042a00677 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Wed, 4 Oct 2023 20:58:51 +0700 Subject: [PATCH 02/48] Avoid clashing SelectRow module, better selection, getSelectedData --- src/js/builds/esm.js | 1 + src/js/core/SpreadsheetTabulator.js | 13 ++ src/js/core/TabulatorFull.js | 6 +- src/js/modules/Spreadsheet/Spreadsheet.js | 223 +++++++++++++++------- src/scss/tabulator.scss | 14 +- 5 files changed, 179 insertions(+), 78 deletions(-) create mode 100644 src/js/core/SpreadsheetTabulator.js diff --git a/src/js/builds/esm.js b/src/js/builds/esm.js index 5d68fb6ff..3920ecbb0 100644 --- a/src/js/builds/esm.js +++ b/src/js/builds/esm.js @@ -1,6 +1,7 @@ export * from '../core/modules/optional.js'; export {default as Tabulator} from '../core/Tabulator.js'; export {default as TabulatorFull} from '../core/TabulatorFull.js'; +export {default as SpreadsheetTabulator} from '../core/SpreadsheetTabulator.js'; export {default as CellComponent} from '../core/cell/CellComponent.js'; export {default as ColumnComponent} from '../core/column/ColumnComponent.js'; diff --git a/src/js/core/SpreadsheetTabulator.js b/src/js/core/SpreadsheetTabulator.js new file mode 100644 index 000000000..45ddf27e4 --- /dev/null +++ b/src/js/core/SpreadsheetTabulator.js @@ -0,0 +1,13 @@ +//tabulator with all modules installed +import {default as Tabulator} from './Tabulator.js'; +import * as _modules from '../core/modules/optional.js'; +import ModuleBinder from './tools/ModuleBinder.js'; + +var { SelectRowModule, ...modules } = _modules; + +class SpreadsheetTabulator extends Tabulator {} + +//bind modules and static functionality +new ModuleBinder(SpreadsheetTabulator, modules); + +export default SpreadsheetTabulator; diff --git a/src/js/core/TabulatorFull.js b/src/js/core/TabulatorFull.js index f1a410101..be432b242 100644 --- a/src/js/core/TabulatorFull.js +++ b/src/js/core/TabulatorFull.js @@ -1,11 +1,13 @@ //tabulator with all modules installed import {default as Tabulator} from './Tabulator.js'; -import * as modules from '../core/modules/optional.js'; +import * as _modules from '../core/modules/optional.js'; import ModuleBinder from './tools/ModuleBinder.js'; class TabulatorFull extends Tabulator {} +var { SpreadsheetModule, ...modules } = _modules; + //bind modules and static functionality new ModuleBinder(TabulatorFull, modules); -export default TabulatorFull; \ No newline at end of file +export default TabulatorFull; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 382fef5f1..edd703ad5 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -1,23 +1,73 @@ import Module from "../../core/Module.js"; +class Selection { + constructor(row, col) { + this.row = row; + this.col = col; + } + + set(row, col) { + this.row = row; + this.col = col; + } +} + +class RangeSelection { + constructor() { + this.start = new Selection(0, 0); + this.end = new Selection(0, 0); + this.updateMinMax(); + } + + setStart(row, col) { + this.start.set(row, col); + this.updateMinMax(); + } + + setEnd(row, col) { + this.end.set(row, col); + this.updateMinMax(); + } + + updateMinMax() { + this.minRow = Math.min(this.start.row, this.end.row); + this.maxRow = Math.max(this.start.row, this.end.row); + this.minCol = Math.min(this.start.col, this.end.col); + this.maxCol = Math.max(this.start.col, this.end.col); + } + + withinSelection(row, col) { + return ( + row >= this.minRow && + row <= this.maxRow && + col >= this.minCol && + col <= this.maxCol + ); + } + + withinRow(row) { + return row >= this.minRow && row <= this.maxRow; + } +} + class Spreadsheet extends Module { constructor(table) { super(table); - this.registerTableOption("spreadsheet", false); //enable Spreadsheet mode - this.registerTableOption("spreadsheetRowHeader", "row-id"); //table header field to identify row - this.rowCounter = 1; - this.selectStart = { row: 1, col: 1 }; - this.selectEnd = { row: 1, col: 1 }; + + this.registerTableFunction( + "getSelectedData", + this.getSelectedData.bind(this), + ); + + this.range = new RangeSelection(this.selectStart, this.selectEnd); + this.dragging = false; } initialize() { - if (!this.table.options.spreadsheet) return; - // Avoid clashing with SelectRow module - if (this.table.options.spreadsheet && this.table.options.selectable) { - console.warn( - "Spreadsheet Warning - disable SelectRow module to use spreadsheet.", - "Tips: Add this to your table options `selectable: false`.", + if (this.table.modules.selectRow) { + console.error( + "Spreadsheet Error - Cannot use both SelectRow and Spreadsheet modules at the same time.", ); return; } @@ -25,9 +75,8 @@ class Spreadsheet extends Module { this.table.options.columns = [ { title: "", - field: this.options("spreadsheetRowHeader"), headerSort: false, - resizable: false, + resizable: true, frozen: true, editable: false, cssClass: "tabulator-row-header", @@ -35,104 +84,130 @@ class Spreadsheet extends Module { ...this.table.options.columns, ]; - this.subscribe("row-init", this.initializeRow.bind(this)); this.subscribe("cell-format", this.formatCell.bind(this)); - this.subscribe("cell-click", this.clickCell.bind(this)); + this.subscribe("cell-mousedown", this.mouseDownCell.bind(this)); + this.subscribe("cell-mousemove", this.moveCell.bind(this)); this.subscribe("cell-layout", this.layoutCell.bind(this)); + this.subscribe("data-refreshed", this.dataRefreshed.bind(this)); + + var mouseUpHandler = this.mouseUp.bind(this); + var mouseUpHandler = document.addEventListener("mouseup", mouseUpHandler); + this.subscribe("table-destroy", () => + document.removeEventListener(mouseUpHandler), + ); this.table.element.classList.add("tabulator-spreadsheet"); + + window.table = this.table; + } + + getSelectedData() { + var data = []; + var rows = this.table.getRows(); + var columns = this.table.columnManager.columnsByIndex.slice( + // skip row header + this.range.minCol + 1, + this.range.maxCol + 2, + ); + + for (var r = this.range.minRow; r <= this.range.maxRow; r++) { + var row = rows[r].getData(); + var result = {}; + columns.forEach((column) => { + result[column.field] = row[column.field]; + }); + data.push(result); + } + + return { + data, + rowCount: Math.abs(this.range.start.row - this.range.end.row) + 1, + columnCount: Math.abs(this.range.start.col - this.range.end.col) + 1, + }; } layoutCell(cell) { var el = cell.getElement(); el.classList.toggle("tabulator-cell-selected", this.isSelectedCell(cell)); - el.classList.toggle( - "tabulator-select-range-start", - this.isSelectedStartCell(cell), - ); - el.classList.toggle( - "tabulator-select-range-end", - this.isSelectedEndCell(cell), - ); + el.classList.toggle("tabulator-select-start", this.isSelectStartCell(cell)); + el.classList.toggle("tabulator-select-end", this.isSelectEndCell(cell)); } layoutRow(row) { row .getElement() - .classList.toggle("tabulator-row-selected", this.selectingRow(row.id)); - row.cells.forEach((cell) => { - this.layoutCell(cell); - }); + .classList.toggle( + "tabulator-row-selected", + this.selectingAllColumnsOfRow(row.position - 1), + ); + row.cells.forEach((cell) => this.layoutCell(cell)); } - selectingRow(index) { + selectingAllColumnsOfRow(row) { return ( - this.selectStart.col === 1 && - this.selectEnd.col === this.table.columnManager.columns.length - 1 && - index >= this.selectStart.row && - index <= this.selectEnd.row + this.range.start.col === 0 && + this.range.end.col === this.table.columnManager.columns.length - 2 && + this.range.withinRow(row) ); } isSelectedCell(cell) { - const columnIndex = this.table.columnManager.findColumnIndex(cell.column); - const rowIndex = cell.row.id; - return ( - rowIndex >= this.selectStart.row && - rowIndex <= this.selectEnd.row && - columnIndex >= this.selectStart.col && - columnIndex <= this.selectEnd.col - ); + const row = cell.row.position - 1; + const col = this.table.columnManager.findColumnIndex(cell.column) - 1; + return this.range.withinSelection(row, col); } - isSelectedStartCell(cell) { - const columnIndex = this.table.columnManager.findColumnIndex(cell.column); - const rowIndex = cell.row.id; - return ( - rowIndex === this.selectStart.row && columnIndex === this.selectStart.col - ); + isSelectStartCell(cell) { + const col = this.table.columnManager.findColumnIndex(cell.column) - 1; + const row = cell.row.position - 1; + return row === this.range.start.row && col === this.range.start.col; } - isSelectedEndCell(cell) { - const columnIndex = this.table.columnManager.findColumnIndex(cell.column); - const rowIndex = cell.row.id; - return ( - rowIndex === this.selectEnd.row && columnIndex === this.selectEnd.col - ); + isSelectEndCell(cell) { + const col = this.table.columnManager.findColumnIndex(cell.column) - 1; + const row = cell.row.position - 1; + return row === this.range.end.row && col === this.range.end.col; } - clickCell(event, cell) { + mouseDownCell(event, cell) { + this.dragging = true; if (event.shiftKey) this.selectRange(cell); else this.selectCell(cell); } + moveCell(event, cell) { + if (this.dragging) this.selectRange(cell); + } + + mouseUp() { + this.dragging = false; + } + selectCell(cell) { - var col = this.table.columnManager.findColumnIndex(cell.column); - var row = cell.row.id; + var row = cell.row.position - 1; + if (cell.column.field === this.options("spreadsheetRowHeader")) { - this.selectStart = { col: col + 1, row }; - this.selectEnd = { - col: this.table.columnManager.getColumns().length - 1, - row, - }; + this.range.setStart(row, 0); + this.range.setEnd(row, this.table.columnManager.getColumns().length - 2); } else { - this.selectStart = { col, row }; - this.selectEnd = { col, row }; + var col = this.table.columnManager.findColumnIndex(cell.column) - 1; + this.range.setStart(row, col); + this.range.setEnd(row, col); } + this.layoutVisible(); } selectRange(cell) { - var col = this.table.columnManager.findColumnIndex(cell.column); - var row = cell.row.id; + var col = this.table.columnManager.findColumnIndex(cell.column) - 1; + var row = cell.row.position - 1; + if (cell.column.field === this.options("spreadsheetRowHeader")) { - this.selectEnd = { - col: this.table.columnManager.getColumns().length - 1, - row, - }; + this.range.setEnd(row, this.table.columnManager.getColumns().length - 2); } else { - this.selectEnd = { col, row }; + this.range.setEnd(row, col); } + this.layoutVisible(); } @@ -145,16 +220,16 @@ class Spreadsheet extends Module { }); } - initializeRow(row) { - row.id = this.rowCounter++; - } - formatCell(cell, value) { if (cell.column.field === this.options("spreadsheetRowHeader")) { - return cell.row.id; + return cell.row.position; } return value; } + + dataRefreshed() { + this.layoutVisible(); + } } Spreadsheet.moduleName = "spreadsheet"; diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index b54086cdd..4573a52b7 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -1509,13 +1509,23 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ user-select: none; position: relative; - &.tabulator-select-range-start.tabulator-select-range-end::after{ + &.tabulator-select-start::before{ content: ''; position: absolute; inset: 0; border: 2px solid $cellSelectedBorderColor; } - &.tabulator-cell-selected:not(.tabulator-select-range-start.tabulator-select-range-end):not(.tabulator-row-header){ + &.tabulator-select-end::after{ + content: ''; + position: absolute; + right: -4px; + bottom: -4px; + width: 8px; + height: 8px; + border-radius: 999px; + background-color: $cellSelectedBorderColor; + } + &.tabulator-cell-selected:not(.tabulator-select-start.tabulator-select-end):not(.tabulator-row-header){ background-color: $cellSelectedBackground; } } From f3c4428ae3e7d5dcaadd683092f79bb8ed03c17a Mon Sep 17 00:00:00 2001 From: azmy60 Date: Thu, 5 Oct 2023 08:52:59 +0700 Subject: [PATCH 03/48] allow multiple range selection, handle multi page, select by column --- src/js/modules/Spreadsheet/Spreadsheet.js | 425 ++++++++++++++++------ src/scss/tabulator.scss | 61 ++-- 2 files changed, 357 insertions(+), 129 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index edd703ad5..131327c57 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -1,66 +1,73 @@ import Module from "../../core/Module.js"; -class Selection { +class Range { constructor(row, col) { - this.row = row; - this.col = col; - } - - set(row, col) { - this.row = row; - this.col = col; - } -} - -class RangeSelection { - constructor() { - this.start = new Selection(0, 0); - this.end = new Selection(0, 0); - this.updateMinMax(); + this.start = { row, col }; + this.end = { row, col }; + this._updateMinMax(); } setStart(row, col) { - this.start.set(row, col); - this.updateMinMax(); + this.start.row = row; + this.start.col = col; + this._updateMinMax(); } setEnd(row, col) { - this.end.set(row, col); - this.updateMinMax(); + this.end.row = row; + this.end.col = col; + this._updateMinMax(); } - updateMinMax() { + _updateMinMax() { this.minRow = Math.min(this.start.row, this.end.row); this.maxRow = Math.max(this.start.row, this.end.row); this.minCol = Math.min(this.start.col, this.end.col); this.maxCol = Math.max(this.start.col, this.end.col); } - withinSelection(row, col) { + atTopLeft(row, col) { + return row === this.minRow && col === this.minCol; + } + + atBottomRight(row, col) { + return row === this.maxRow && col === this.maxCol; + } + + isInside(row, col) { return ( - row >= this.minRow && + this.minRow <= row && row <= this.maxRow && - col >= this.minCol && + this.minCol <= col && col <= this.maxCol ); } + isStartingAt(row, col) { + return this.start.row === row && this.start.col === col; + } + withinRow(row) { return row >= this.minRow && row <= this.maxRow; } + + withinColumn(col) { + return col >= this.minCol && col <= this.maxCol; + } } class Spreadsheet extends Module { constructor(table) { super(table); + this.registerTableOption("rowHeaderField", "--row-position"); this.registerTableFunction( "getSelectedData", this.getSelectedData.bind(this), ); - this.range = new RangeSelection(this.selectStart, this.selectEnd); - this.dragging = false; + this.ranges = [new Range(0, 0)]; + this.selecting = false; } initialize() { @@ -72,9 +79,32 @@ class Spreadsheet extends Module { return; } + this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); + this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); + this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); + this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); + this.subscribe("cell-rendered", this.layoutCell.bind(this)); + this.subscribe("page-changed", this.handlePageChanged.bind(this)); + this.subscribe("table-layout", this.layoutSelection.bind(this)); + + var mouseUpHandler = this.handleMouseUp.bind(this); + var mouseUpHandler = document.addEventListener("mouseup", mouseUpHandler); + this.subscribe("table-destroy", () => + document.removeEventListener(mouseUpHandler), + ); + + this.initializeTable(); + } + + initializeTable() { + for (var column of this.table.options.columns) { + // Disable sorting by clicking header + column.headerSort = false; + } this.table.options.columns = [ { title: "", + field: this.options("rowHeaderField"), headerSort: false, resizable: true, frozen: true, @@ -84,33 +114,25 @@ class Spreadsheet extends Module { ...this.table.options.columns, ]; - this.subscribe("cell-format", this.formatCell.bind(this)); - this.subscribe("cell-mousedown", this.mouseDownCell.bind(this)); - this.subscribe("cell-mousemove", this.moveCell.bind(this)); - this.subscribe("cell-layout", this.layoutCell.bind(this)); - this.subscribe("data-refreshed", this.dataRefreshed.bind(this)); - - var mouseUpHandler = this.mouseUp.bind(this); - var mouseUpHandler = document.addEventListener("mouseup", mouseUpHandler); - this.subscribe("table-destroy", () => - document.removeEventListener(mouseUpHandler), - ); - this.table.element.classList.add("tabulator-spreadsheet"); - window.table = this.table; + this.overlay = document.createElement("div"); + this.overlay.classList.add("tabulator-selection-overlay"); + + this.table.rowManager.element.appendChild(this.overlay); } - getSelectedData() { + getSelectedData(range) { + if (!range) range = this.getActiveRange(); var data = []; var rows = this.table.getRows(); var columns = this.table.columnManager.columnsByIndex.slice( // skip row header - this.range.minCol + 1, - this.range.maxCol + 2, + range.minCol + 1, + range.maxCol + 2, ); - for (var r = this.range.minRow; r <= this.range.maxRow; r++) { + for (var r = range.minRow; r <= range.maxRow; r++) { var row = rows[r].getData(); var result = {}; columns.forEach((column) => { @@ -121,114 +143,301 @@ class Spreadsheet extends Module { return { data, - rowCount: Math.abs(this.range.start.row - this.range.end.row) + 1, - columnCount: Math.abs(this.range.start.col - this.range.end.col) + 1, + rowCount: data.length, + columnCount: columns.length, }; } layoutCell(cell) { var el = cell.getElement(); - el.classList.toggle("tabulator-cell-selected", this.isSelectedCell(cell)); - el.classList.toggle("tabulator-select-start", this.isSelectStartCell(cell)); - el.classList.toggle("tabulator-select-end", this.isSelectEndCell(cell)); - } + var row = cell.row.position - 1; + var col = this.table.columnManager.findColumnIndex(cell.column) - 1; - layoutRow(row) { - row - .getElement() - .classList.toggle( - "tabulator-row-selected", - this.selectingAllColumnsOfRow(row.position - 1), - ); - row.cells.forEach((cell) => this.layoutCell(cell)); + el.classList.toggle( + "tabulator-cell-selected", + this.ranges.some((range) => range.isInside(row, col)), + ); + + el.classList.toggle( + "tabulator-only-cell-selected", + this.ranges.length === 1 && + this.ranges[0].atTopLeft(row, col) && + this.ranges[0].atBottomRight(row, col), + ); + + if (cell.column.field === this.options("rowHeaderField")) { + var n = cell.row.position; + if (this.table.getPage) { + n += (this.table.getPage() - 1) * this.table.getPageSize(); + } + el.innerHTML = n; + } } selectingAllColumnsOfRow(row) { - return ( - this.range.start.col === 0 && - this.range.end.col === this.table.columnManager.columns.length - 2 && - this.range.withinRow(row) + if (this.selecting === "all") return true; + + return this.ranges.some( + (range) => + this.selecting === "row" && + range.start.col === 0 && + range.end.col === this.table.columnManager.columns.length - 2 && + range.withinRow(row), ); } - isSelectedCell(cell) { - const row = cell.row.position - 1; - const col = this.table.columnManager.findColumnIndex(cell.column) - 1; - return this.range.withinSelection(row, col); - } + selectingAllRowsOfColumn(col) { + if (this.selecting === "all") return true; - isSelectStartCell(cell) { - const col = this.table.columnManager.findColumnIndex(cell.column) - 1; - const row = cell.row.position - 1; - return row === this.range.start.row && col === this.range.start.col; + return this.ranges.some( + (range) => + this.selecting === "column" && + range.start.row === 0 && + range.end.row === this.rowsPerPage - 1 && + range.withinColumn(col), + ); } - isSelectEndCell(cell) { - const col = this.table.columnManager.findColumnIndex(cell.column) - 1; - const row = cell.row.position - 1; - return row === this.range.end.row && col === this.range.end.col; - } + handleColumnMouseDown(event, column) { + if (column.field === this.options("rowHeaderField")) { + this.ranges = [new Range(0, 0)]; + this.selecting = "all"; + + const topLeftCell = this.table.rowManager + .getRowFromPosition(1) + .getCells()[1]; + const bottomRightCell = this.table.columnManager.columns + .slice(-1)[0] + .getCells() + .slice(-1)[0]; + + this.beginSelection(topLeftCell); + this.endSelection(bottomRightCell); + } else { + this._select(event, column); + } - mouseDownCell(event, cell) { - this.dragging = true; - if (event.shiftKey) this.selectRange(cell); - else this.selectCell(cell); + this.layoutElement(); } - moveCell(event, cell) { - if (this.dragging) this.selectRange(cell); + handleColumnMouseMove(_, column) { + if (column.field === this.options("rowHeaderField")) return; + if (!this.selecting) return; + + if (column) this.endSelection(column); + this.layoutElement(); } - mouseUp() { - this.dragging = false; + handleCellMouseDown(event, cell) { + this._select(event, cell); + this.layoutElement(); } - selectCell(cell) { - var row = cell.row.position - 1; + _select(event, element) { + if (element.type === "column") { + this.selecting = "column"; + } else if (element.column.field === this.options("rowHeaderField")) { + this.selecting = "row"; + } else { + this.selecting = "cell"; + } - if (cell.column.field === this.options("spreadsheetRowHeader")) { - this.range.setStart(row, 0); - this.range.setEnd(row, this.table.columnManager.getColumns().length - 2); + if (event.shiftKey) { + if (this.ranges.length > 1) { + this.ranges = this.ranges.slice(-1); + } + + this.endSelection(element); + } else if (event.ctrlKey) { + this.ranges.push(new Range(0, 0)); + + this.beginSelection(element); + this.endSelection(element); } else { - var col = this.table.columnManager.findColumnIndex(cell.column) - 1; - this.range.setStart(row, col); - this.range.setEnd(row, col); + this.ranges = [new Range(0, 0)]; + + this.beginSelection(element); + this.endSelection(element); } + } + + handleCellMouseMove(_, cell) { + if (!this.selecting) return; - this.layoutVisible(); + this.endSelection(cell); + this.layoutElement(); } - selectRange(cell) { - var col = this.table.columnManager.findColumnIndex(cell.column) - 1; - var row = cell.row.position - 1; + handleMouseUp() { + this.selecting = false; + } + + beginSelection(element) { + var range = this.getActiveRange(); + + if (element.type === "column") { + range.setStart(0, this.table.columnManager.findColumnIndex(element) - 1); + return; + } + + var row = element.row.position - 1; + var col = this.table.columnManager.findColumnIndex(element.column) - 1; - if (cell.column.field === this.options("spreadsheetRowHeader")) { - this.range.setEnd(row, this.table.columnManager.getColumns().length - 2); + if (element.column.field === this.options("rowHeaderField")) { + range.setStart(row, 0); } else { - this.range.setEnd(row, col); + range.setStart(row, col); } + } - this.layoutVisible(); + endSelection(element) { + var range = this.getActiveRange(); + + if (element.type === "column") { + if (this.selecting === "column") { + range.setEnd( + this.rowsPerPage - 1, + this.table.columnManager.findColumnIndex(element) - 1, + ); + } else if (this.selecting === "cell") { + range.setEnd(0, this.table.columnManager.findColumnIndex(element) - 1); + } + return; + } + + var row = element.row.position - 1; + var col = this.table.columnManager.findColumnIndex(element.column) - 1; + var isRowHeader = element.column.field === this.options("rowHeaderField"); + + if (this.selecting === "row") { + range.setEnd(row, this.table.columnManager.getColumns().length - 2); + } else if (this.selecting !== "row" && isRowHeader) { + range.setEnd(row, 0); + } else if (this.selecting === "column") { + range.setEnd(this.rowsPerPage - 1, col); + } else { + range.setEnd(row, col); + } } - layoutVisible() { - var visibleRows = this.table.rowManager.getVisibleRows(true); - visibleRows.forEach((row) => { + layoutElement() { + this.table.rowManager.getVisibleRows(true).forEach((row) => { if (row.type === "row") { this.layoutRow(row); } }); + + this.table.columnManager.columns.forEach((column) => { + this.layoutColumn(column); + }); + + this.layoutSelection(); } - formatCell(cell, value) { - if (cell.column.field === this.options("spreadsheetRowHeader")) { - return cell.row.position; + layoutRow(row) { + row + .getElement() + .classList.toggle( + "tabulator-row-selected", + this.selectingAllColumnsOfRow(row.position - 1), + ); + row.cells.forEach((cell) => this.layoutCell(cell)); + } + + layoutColumn(column) { + column + .getElement() + .classList.toggle( + "tabulator-col-selected", + this.selectingAllRowsOfColumn( + this.table.columnManager.findColumnIndex(column) - 1, + ), + ); + } + + layoutSelection() { + var tableElement = this.table.rowManager.tableElement; + + this.overlay.style.left = tableElement.scrollLeft + "px"; + this.overlay.style.top = tableElement.scrollTop + "px"; + this.overlay.style.width = tableElement.clientWidth + "px"; + this.overlay.style.height = tableElement.clientHeight + "px"; + + while (this.overlay.firstChild) { + this.overlay.removeChild(this.overlay.firstChild); } - return value; + + var activeCell = this.getActiveCell(); + var activeEl = document.createElement("div"); + activeEl.classList.add("tabulator-selection-active-cell"); + activeEl.style.left = activeCell.getElement().offsetLeft + "px"; + activeEl.style.top = activeCell.row.getElement().offsetTop + "px"; + activeEl.style.width = + activeCell.getElement().offsetLeft + + activeCell.getElement().offsetWidth - + activeCell.getElement().offsetLeft + + "px"; + activeEl.style.height = + activeCell.row.getElement().offsetTop + + activeCell.row.getElement().offsetHeight - + activeCell.row.getElement().offsetTop + + "px"; + + this.overlay.appendChild(activeEl); + + var activeRange = this.getActiveRange(); + + this.ranges.forEach((range) => { + var topLeftCell = this.getCell(range.minRow, range.minCol); + var bottomRightCell = this.getCell(range.maxRow, range.maxCol); + + var rangeEl = document.createElement("div"); + rangeEl.classList.add("tabulator-selection-range"); + if (range === activeRange) { + rangeEl.classList.add("tabulator-selection-range-active"); + } + + rangeEl.style.left = topLeftCell.getElement().offsetLeft + "px"; + rangeEl.style.top = topLeftCell.row.getElement().offsetTop + "px"; + rangeEl.style.width = + bottomRightCell.getElement().offsetLeft + + bottomRightCell.getElement().offsetWidth - + topLeftCell.getElement().offsetLeft + + "px"; + rangeEl.style.height = + bottomRightCell.row.getElement().offsetTop + + bottomRightCell.row.getElement().offsetHeight - + topLeftCell.row.getElement().offsetTop + + "px"; + + this.overlay.appendChild(rangeEl); + }); + } + + handlePageChanged() { + this.ranges = [new Range(0, 0)]; + this.layoutElement(); + } + + getCell(row, col) { + return this.table.rowManager.getRowFromPosition(row + 1).getCells()[ + col + 1 + ]; + } + + getActiveRange() { + return this.ranges[this.ranges.length - 1]; + } + + getActiveCell() { + var activeRange = this.getActiveRange(); + return this.getCell(activeRange.start.row, activeRange.start.col); } - dataRefreshed() { - this.layoutVisible(); + get rowsPerPage() { + return this.table.getPageSize + ? this.table.getPageSize() + : this.table.rowManager.getRows().length; } } diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index 4573a52b7..76eed533c 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -25,9 +25,10 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered -$rowHeaderSelectedBackground: #4990F1 !default; //row header background color when selected (spreadsheet module) -$cellSelectedBackground: #C2D5EF !default; //cell background color when selected (spreadsheet module) -$cellSelectedBorderColor: #2975DD !default; //cell border color when selected (spreadsheet module) +$headerSelectedBackground: #4990F1 !default; //header background color when selected +$headerTextSelectedBackground: #FFFFFF !default; //header text color when selected +$cellSelectedBackground: #C2D5EF !default; //cell background color when selected +$cellSelectedBorderColor: #2975DD !default; //cell border color when selected $editBoxColor:#1D68CD !default; //border color for edit boxes $errorColor:#dd0000 !default; //error indication @@ -1509,23 +1510,7 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ user-select: none; position: relative; - &.tabulator-select-start::before{ - content: ''; - position: absolute; - inset: 0; - border: 2px solid $cellSelectedBorderColor; - } - &.tabulator-select-end::after{ - content: ''; - position: absolute; - right: -4px; - bottom: -4px; - width: 8px; - height: 8px; - border-radius: 999px; - background-color: $cellSelectedBorderColor; - } - &.tabulator-cell-selected:not(.tabulator-select-start.tabulator-select-end):not(.tabulator-row-header){ + &.tabulator-cell-selected:not(.tabulator-only-cell-selected):not(.tabulator-row-header){ background-color: $cellSelectedBackground; } } @@ -1534,7 +1519,41 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ background-color: $rowSelectedBackground; } &.tabulator-row-selected .tabulator-row-header{ - background-color: $rowHeaderSelectedBackground; + background-color: $headerSelectedBackground; + color: $headerTextSelectedBackground; + } + } + & .tabulator-header .tabulator-col.tabulator-col-selected{ + background-color: $headerSelectedBackground; + color: $headerTextSelectedBackground; + } + & .tabulator-selection-overlay { + position: absolute; + overflow: hidden; + z-index: 10; + pointer-events: none; + + & .tabulator-selection-range { + position: absolute; + box-sizing: border-box; + border: 1px solid $cellSelectedBorderColor; + + &.tabulator-selection-range-active::after { + content: ''; + position: absolute; + right: -3px; + bottom: -3px; + width: 6px; + height: 6px; + background-color: $cellSelectedBorderColor; + border-radius: 999px; + } + } + + & .tabulator-selection-active-cell { + position: absolute; + box-sizing: border-box; + border: 2px solid $cellSelectedBorderColor; } } } From dcaf452a822303d6a4a7b514bb55ff369e5bc46c Mon Sep 17 00:00:00 2001 From: azmy60 Date: Fri, 6 Oct 2023 15:10:55 +0700 Subject: [PATCH 04/48] use spreadsheet option to enable it --- src/js/builds/esm.js | 1 - src/js/core/SpreadsheetTabulator.js | 13 ------------- src/js/core/TabulatorFull.js | 4 +--- src/js/modules/SelectRow/SelectRow.js | 12 +++++++----- src/js/modules/Spreadsheet/Spreadsheet.js | 8 -------- 5 files changed, 8 insertions(+), 30 deletions(-) delete mode 100644 src/js/core/SpreadsheetTabulator.js diff --git a/src/js/builds/esm.js b/src/js/builds/esm.js index 3920ecbb0..5d68fb6ff 100644 --- a/src/js/builds/esm.js +++ b/src/js/builds/esm.js @@ -1,7 +1,6 @@ export * from '../core/modules/optional.js'; export {default as Tabulator} from '../core/Tabulator.js'; export {default as TabulatorFull} from '../core/TabulatorFull.js'; -export {default as SpreadsheetTabulator} from '../core/SpreadsheetTabulator.js'; export {default as CellComponent} from '../core/cell/CellComponent.js'; export {default as ColumnComponent} from '../core/column/ColumnComponent.js'; diff --git a/src/js/core/SpreadsheetTabulator.js b/src/js/core/SpreadsheetTabulator.js deleted file mode 100644 index 45ddf27e4..000000000 --- a/src/js/core/SpreadsheetTabulator.js +++ /dev/null @@ -1,13 +0,0 @@ -//tabulator with all modules installed -import {default as Tabulator} from './Tabulator.js'; -import * as _modules from '../core/modules/optional.js'; -import ModuleBinder from './tools/ModuleBinder.js'; - -var { SelectRowModule, ...modules } = _modules; - -class SpreadsheetTabulator extends Tabulator {} - -//bind modules and static functionality -new ModuleBinder(SpreadsheetTabulator, modules); - -export default SpreadsheetTabulator; diff --git a/src/js/core/TabulatorFull.js b/src/js/core/TabulatorFull.js index be432b242..334692be1 100644 --- a/src/js/core/TabulatorFull.js +++ b/src/js/core/TabulatorFull.js @@ -1,12 +1,10 @@ //tabulator with all modules installed import {default as Tabulator} from './Tabulator.js'; -import * as _modules from '../core/modules/optional.js'; +import * as modules from '../core/modules/optional.js'; import ModuleBinder from './tools/ModuleBinder.js'; class TabulatorFull extends Tabulator {} -var { SpreadsheetModule, ...modules } = _modules; - //bind modules and static functionality new ModuleBinder(TabulatorFull, modules); diff --git a/src/js/modules/SelectRow/SelectRow.js b/src/js/modules/SelectRow/SelectRow.js index fc9306320..4a512a86d 100644 --- a/src/js/modules/SelectRow/SelectRow.js +++ b/src/js/modules/SelectRow/SelectRow.js @@ -16,7 +16,11 @@ class SelectRow extends Module{ this.registerTableOption("selectableRollingSelection", true); //roll selection once maximum number of selectable rows is reached this.registerTableOption("selectablePersistence", true); // maintain selection when table view is updated this.registerTableOption("selectableCheck", function(data, row){return true;}); //check whether row is selectable - + } + + initialize(){ + if(this.table.options.spreadsheet) return; + this.registerTableFunction("selectRow", this.selectRows.bind(this)); this.registerTableFunction("deselectRow", this.deselectRows.bind(this)); this.registerTableFunction("toggleSelectRow", this.toggleRow.bind(this)); @@ -28,9 +32,7 @@ class SelectRow extends Module{ this.registerComponentFunction("row", "deselect", this.deselectRows.bind(this)); this.registerComponentFunction("row", "toggleSelect", this.toggleRow.bind(this)); this.registerComponentFunction("row", "isSelected", this.isRowSelected.bind(this)); - } - - initialize(){ + if(this.table.options.selectable !== false){ this.subscribe("row-init", this.initializeRow.bind(this)); this.subscribe("row-deleting", this.rowDeleted.bind(this)); @@ -472,4 +474,4 @@ class SelectRow extends Module{ SelectRow.moduleName = "selectRow"; -export default SelectRow; \ No newline at end of file +export default SelectRow; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 131327c57..b0b856f40 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -71,14 +71,6 @@ class Spreadsheet extends Module { } initialize() { - // Avoid clashing with SelectRow module - if (this.table.modules.selectRow) { - console.error( - "Spreadsheet Error - Cannot use both SelectRow and Spreadsheet modules at the same time.", - ); - return; - } - this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); From 5294ad2200107fc3f575e9eaf04ec43aa0e30e2b Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 9 Oct 2023 20:13:35 +0700 Subject: [PATCH 05/48] clipboard module integration --- src/js/core/ColumnManager.js | 1 + src/js/core/column/Column.js | 3 +- src/js/modules/Export/Export.js | 48 +++++--- src/js/modules/Spreadsheet/Spreadsheet.js | 127 ++++++++++++++-------- src/scss/tabulator.scss | 20 +++- 5 files changed, 129 insertions(+), 70 deletions(-) diff --git a/src/js/core/ColumnManager.js b/src/js/core/ColumnManager.js index bda0768cb..5cab3fe79 100644 --- a/src/js/core/ColumnManager.js +++ b/src/js/core/ColumnManager.js @@ -301,6 +301,7 @@ export default class ColumnManager extends CoreFeature { registerColumnPosition(col){ this.columnsByIndex.push(col); + return this.columnsByIndex.length; } _reIndexColumns(){ diff --git a/src/js/core/column/Column.js b/src/js/core/column/Column.js index a81c0f20f..447ad6886 100644 --- a/src/js/core/column/Column.js +++ b/src/js/core/column/Column.js @@ -22,6 +22,7 @@ class Column extends CoreFeature{ this.isGroup = false; this.hozAlign = ""; //horizontal text alignment this.vertAlign = ""; //vert text alignment + this.position = 0; //multi dimensional filed handling this.field =""; @@ -128,7 +129,7 @@ class Column extends CoreFeature{ //register column position with column manager registerColumnPosition(column){ - this.parent.registerColumnPosition(column); + this.position = this.parent.registerColumnPosition(column); } //register column position with column manager diff --git a/src/js/modules/Export/Export.js b/src/js/modules/Export/Export.js index 7005ea479..c090735da 100644 --- a/src/js/modules/Export/Export.js +++ b/src/js/modules/Export/Export.js @@ -35,10 +35,20 @@ class Export extends Module{ this.cloneTableStyle = style; this.config = config || {}; this.colVisProp = colVisProp; - - var headers = this.config.columnHeaders !== false ? this.headersToExportRows(this.generateColumnGroupHeaders()) : []; - var body = this.bodyToExportRows(this.rowLookup(range)); - + + var headers, body; + + if (range === 'spreadsheet') { + var columns = this.table.modules.spreadsheet.selectedColumns + headers = this.config.columnHeaders !== false + ? this.headersToExportRows(this.generateColumnGroupHeaders(columns.map((component) => component._column))) + : []; + body = this.bodyToExportRows(this.rowLookup(range), columns); + } else { + headers = this.config.columnHeaders !== false ? this.headersToExportRows(this.generateColumnGroupHeaders()) : []; + body = this.bodyToExportRows(this.rowLookup(range)); + } + return headers.concat(body); } @@ -73,7 +83,11 @@ class Export extends Module{ case "selected": rows = this.table.modules.selectRow.selectedRows; break; - + + case "spreadsheet": + rows = this.table.modules.spreadsheet.selectedRows; + break; + case "active": default: if(this.table.options.pagination){ @@ -87,10 +101,12 @@ class Export extends Module{ return Object.assign([], rows); } - generateColumnGroupHeaders(){ + generateColumnGroupHeaders(columns){ var output = []; - var columns = this.config.columnGroups !== false ? this.table.columnManager.columns : this.table.columnManager.columnsByIndex; + if (!columns) { + columns = this.config.columnGroups !== false ? this.table.columnManager.columns : this.table.columnManager.columnsByIndex; + } columns.forEach((column) => { var colData = this.processColumnGroup(column); @@ -99,7 +115,7 @@ class Export extends Module{ output.push(colData); } }); - + return output; } @@ -227,16 +243,16 @@ class Export extends Module{ return exportRows; } - bodyToExportRows(rows){ - - var columns = []; + bodyToExportRows(rows, columns = []){ var exportRows = []; - this.table.columnManager.columnsByIndex.forEach((column) => { - if (this.columnVisCheck(column)) { - columns.push(column.getComponent()); - } - }); + if (columns.length === 0) { + this.table.columnManager.columnsByIndex.forEach((column) => { + if (this.columnVisCheck(column)) { + columns.push(column.getComponent()); + } + }); + } if(this.config.columnCalcs !== false && this.table.modExists("columnCalcs")){ if(this.table.modules.columnCalcs.topInitialized){ diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index b0b856f40..713b3e800 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -34,7 +34,7 @@ class Range { return row === this.maxRow && col === this.maxCol; } - isInside(row, col) { + occupies(row, col) { return ( this.minRow <= row && row <= this.maxRow && @@ -60,31 +60,44 @@ class Spreadsheet extends Module { constructor(table) { super(table); + this.ranges = [new Range(0, 0)]; + this.selecting = false; + } + + initialize() { + if (!this.table.options.spreadsheet) return; + this.registerTableOption("rowHeaderField", "--row-position"); - this.registerTableFunction( - "getSelectedData", - this.getSelectedData.bind(this), - ); this.ranges = [new Range(0, 0)]; this.selecting = false; } initialize() { + if (!this.table.options.spreadsheet) return; + + this.registerTableFunction( + "getSelectedData", + this.getSelectedData.bind(this), + ); + this.registerTableFunction("getRanges", this.getSelectedData.bind(this)); + this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); - this.subscribe("cell-rendered", this.layoutCell.bind(this)); + this.subscribe("cell-rendered", this.renderCell.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); - this.subscribe("table-layout", this.layoutSelection.bind(this)); + this.subscribe("table-layout", this.layoutElement.bind(this)); var mouseUpHandler = this.handleMouseUp.bind(this); - var mouseUpHandler = document.addEventListener("mouseup", mouseUpHandler); + document.addEventListener("mouseup", mouseUpHandler); this.subscribe("table-destroy", () => document.removeEventListener(mouseUpHandler), ); + this.table.options.clipboardCopyRowRange = "spreadsheet"; + this.initializeTable(); } @@ -116,38 +129,31 @@ class Spreadsheet extends Module { getSelectedData(range) { if (!range) range = this.getActiveRange(); + var data = []; - var rows = this.table.getRows(); - var columns = this.table.columnManager.columnsByIndex.slice( - // skip row header - range.minCol + 1, - range.maxCol + 2, - ); + var rows = this.selectedRows; + var columns = this.selectedColumns.map((component) => component._column); - for (var r = range.minRow; r <= range.maxRow; r++) { - var row = rows[r].getData(); + rows.forEach((row) => { + var rowData = row.getData(); var result = {}; columns.forEach((column) => { - result[column.field] = row[column.field]; + result[column.field] = rowData[column.field]; }); data.push(result); - } + }); - return { - data, - rowCount: data.length, - columnCount: columns.length, - }; + return data; } - layoutCell(cell) { + renderCell(cell) { var el = cell.getElement(); var row = cell.row.position - 1; - var col = this.table.columnManager.findColumnIndex(cell.column) - 1; + var col = cell.column.position - 2; el.classList.toggle( "tabulator-cell-selected", - this.ranges.some((range) => range.isInside(row, col)), + this.ranges.some((range) => range.occupies(row, col)), ); el.classList.toggle( @@ -159,10 +165,10 @@ class Spreadsheet extends Module { if (cell.column.field === this.options("rowHeaderField")) { var n = cell.row.position; - if (this.table.getPage) { + if (this.table.initialized && this.table.getPage) { n += (this.table.getPage() - 1) * this.table.getPageSize(); } - el.innerHTML = n; + el.innerText = n; } } @@ -214,9 +220,9 @@ class Spreadsheet extends Module { handleColumnMouseMove(_, column) { if (column.field === this.options("rowHeaderField")) return; - if (!this.selecting) return; + if (!this.selecting || this.selecting === "all") return; - if (column) this.endSelection(column); + this.endSelection(column); this.layoutElement(); } @@ -254,7 +260,7 @@ class Spreadsheet extends Module { } handleCellMouseMove(_, cell) { - if (!this.selecting) return; + if (!this.selecting || this.selecting === "all") return; this.endSelection(cell); this.layoutElement(); @@ -268,12 +274,12 @@ class Spreadsheet extends Module { var range = this.getActiveRange(); if (element.type === "column") { - range.setStart(0, this.table.columnManager.findColumnIndex(element) - 1); + range.setStart(0, element.position - 2); return; } var row = element.row.position - 1; - var col = this.table.columnManager.findColumnIndex(element.column) - 1; + var col = element.column.position - 2; if (element.column.field === this.options("rowHeaderField")) { range.setStart(row, 0); @@ -287,18 +293,15 @@ class Spreadsheet extends Module { if (element.type === "column") { if (this.selecting === "column") { - range.setEnd( - this.rowsPerPage - 1, - this.table.columnManager.findColumnIndex(element) - 1, - ); + range.setEnd(this.rowsPerPage - 1, element.position - 2); } else if (this.selecting === "cell") { - range.setEnd(0, this.table.columnManager.findColumnIndex(element) - 1); + range.setEnd(0, element.position - 1); } return; } var row = element.row.position - 1; - var col = this.table.columnManager.findColumnIndex(element.column) - 1; + var col = element.column.position - 2; var isRowHeader = element.column.field === this.options("rowHeaderField"); if (this.selecting === "row") { @@ -323,7 +326,7 @@ class Spreadsheet extends Module { this.layoutColumn(column); }); - this.layoutSelection(); + this.renderSelection(); } layoutRow(row) { @@ -333,7 +336,13 @@ class Spreadsheet extends Module { "tabulator-row-selected", this.selectingAllColumnsOfRow(row.position - 1), ); - row.cells.forEach((cell) => this.layoutCell(cell)); + + row.getElement().classList.toggle( + "tabulator-row-highlight", + this.ranges.some((range) => range.withinRow(row.position - 1)), + ); + + row.cells.forEach((cell) => this.renderCell(cell)); } layoutColumn(column) { @@ -341,13 +350,16 @@ class Spreadsheet extends Module { .getElement() .classList.toggle( "tabulator-col-selected", - this.selectingAllRowsOfColumn( - this.table.columnManager.findColumnIndex(column) - 1, - ), + this.selectingAllRowsOfColumn(column.position - 2), ); + + column.getElement().classList.toggle( + "tabulator-col-highlight", + this.ranges.some((range) => range.withinColumn(column.position - 2)), + ); } - layoutSelection() { + renderSelection() { var tableElement = this.table.rowManager.tableElement; this.overlay.style.left = tableElement.scrollLeft + "px"; @@ -406,6 +418,10 @@ class Spreadsheet extends Module { }); } + tableBuilt() { + this.layoutElement(); + } + handlePageChanged() { this.ranges = [new Range(0, 0)]; this.layoutElement(); @@ -427,9 +443,24 @@ class Spreadsheet extends Module { } get rowsPerPage() { - return this.table.getPageSize - ? this.table.getPageSize() - : this.table.rowManager.getRows().length; + return this.table.rowManager.getVisibleRows().length; + } + + get selectedRows() { + var range = this.getActiveRange(); + return this.table.rowManager.activeRows.slice( + range.minRow, + range.maxRow + 1, + ); + } + + get selectedColumns() { + var range = this.getActiveRange(); + return this.table.getColumns().slice( + // skip row header + range.minCol + 1, + range.maxCol + 2, + ); } } diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index 76eed533c..7a24b35b1 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -25,6 +25,8 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered +$headerHighlightBackground: #9ABCEA !default; //row background color when selected +$headerTextHighlightBackground: #000000 !default; //row background color when selected $headerSelectedBackground: #4990F1 !default; //header background color when selected $headerTextSelectedBackground: #FFFFFF !default; //header text color when selected $cellSelectedBackground: #C2D5EF !default; //cell background color when selected @@ -1513,19 +1515,27 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ &.tabulator-cell-selected:not(.tabulator-only-cell-selected):not(.tabulator-row-header){ background-color: $cellSelectedBackground; } + } & .tabulator-row{ - &:has(.tabulator-cell-selected) .tabulator-row-header{ - background-color: $rowSelectedBackground; + &.tabulator-row-highlight .tabulator-row-header{ + background-color: $headerHighlightBackground; + color: $headerTextHighlightBackground; } &.tabulator-row-selected .tabulator-row-header{ background-color: $headerSelectedBackground; color: $headerTextSelectedBackground; } } - & .tabulator-header .tabulator-col.tabulator-col-selected{ - background-color: $headerSelectedBackground; - color: $headerTextSelectedBackground; + & .tabulator-col { + &.tabulator-col-highlight { + background-color: $headerHighlightBackground; + color: $headerTextHighlightBackground; + } + &.tabulator-col-selected{ + background-color: $headerSelectedBackground; + color: $headerTextSelectedBackground; + } } & .tabulator-selection-overlay { position: absolute; From 08991b92e02e6b6d78e3163c3b1441cfe2825323 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 9 Oct 2023 21:26:33 +0700 Subject: [PATCH 06/48] allow editing by double clicking --- src/js/modules/Spreadsheet/Spreadsheet.js | 67 +++++++++++++++-------- src/scss/tabulator.scss | 4 ++ 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 713b3e800..30dc04e0e 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -68,24 +68,18 @@ class Spreadsheet extends Module { if (!this.table.options.spreadsheet) return; this.registerTableOption("rowHeaderField", "--row-position"); - - this.ranges = [new Range(0, 0)]; - this.selecting = false; - } - - initialize() { - if (!this.table.options.spreadsheet) return; - this.registerTableFunction( "getSelectedData", this.getSelectedData.bind(this), ); - this.registerTableFunction("getRanges", this.getSelectedData.bind(this)); this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); + this.subscribe("column-width", this.handleColumnUpdating.bind(this)); + this.subscribe("column-resized", this.renderSelection.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); + this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); this.subscribe("cell-rendered", this.renderCell.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); @@ -96,8 +90,6 @@ class Spreadsheet extends Module { document.removeEventListener(mouseUpHandler), ); - this.table.options.clipboardCopyRowRange = "spreadsheet"; - this.initializeTable(); } @@ -105,6 +97,8 @@ class Spreadsheet extends Module { for (var column of this.table.options.columns) { // Disable sorting by clicking header column.headerSort = false; + // Edit on double click + if (column.editor) column.editable = false; } this.table.options.columns = [ { @@ -119,6 +113,8 @@ class Spreadsheet extends Module { ...this.table.options.columns, ]; + this.table.options.clipboardCopyRowRange = "spreadsheet"; + this.table.element.classList.add("tabulator-spreadsheet"); this.overlay = document.createElement("div"); @@ -127,12 +123,16 @@ class Spreadsheet extends Module { this.table.rowManager.element.appendChild(this.overlay); } - getSelectedData(range) { - if (!range) range = this.getActiveRange(); + getSelectedData() { + return this.getDataByRange(this.getActiveRange()); + } + getDataByRange(range) { var data = []; - var rows = this.selectedRows; - var columns = this.selectedColumns.map((component) => component._column); + var rows = this.getRowsByRange(range); + var columns = this.getColumnsByRange(range).map( + (component) => component._column, + ); rows.forEach((row) => { var rowData = row.getData(); @@ -270,6 +270,15 @@ class Spreadsheet extends Module { this.selecting = false; } + handleCellDblClick(_, cell) { + if ( + cell.column.field !== this.options("rowHeaderField") && + cell.column.definition.editor + ) { + cell.getComponent().edit(true); + } + } + beginSelection(element) { var range = this.getActiveRange(); @@ -359,7 +368,13 @@ class Spreadsheet extends Module { ); } + handleColumnUpdating() { + this.overlay.classList.add("tabulator-column-updating"); + } + renderSelection() { + this.overlay.classList.remove("tabulator-column-updating"); + var tableElement = this.table.rowManager.tableElement; this.overlay.style.left = tableElement.scrollLeft + "px"; @@ -442,26 +457,32 @@ class Spreadsheet extends Module { return this.getCell(activeRange.start.row, activeRange.start.col); } - get rowsPerPage() { - return this.table.rowManager.getVisibleRows().length; - } - - get selectedRows() { - var range = this.getActiveRange(); + getRowsByRange(range) { return this.table.rowManager.activeRows.slice( range.minRow, range.maxRow + 1, ); } - get selectedColumns() { - var range = this.getActiveRange(); + getColumnsByRange(range) { return this.table.getColumns().slice( // skip row header range.minCol + 1, range.maxCol + 2, ); } + + get rowsPerPage() { + return this.table.rowManager.getVisibleRows().length; + } + + get selectedRows() { + return this.getRowsByRange(this.getActiveRange()); + } + + get selectedColumns() { + return this.getColumnsByRange(this.getActiveRange()); + } } Spreadsheet.moduleName = "spreadsheet"; diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index 7a24b35b1..3aebbfbf0 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -1565,5 +1565,9 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ box-sizing: border-box; border: 2px solid $cellSelectedBorderColor; } + + &.tabulator-column-updating { + display: none; + } } } From 08a200081031fcfed8f3e94bfdd744c43e326bdb Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 10 Oct 2023 03:11:21 +0700 Subject: [PATCH 07/48] allow custom context menu and refactors --- src/js/modules/Spreadsheet/Spreadsheet.js | 159 ++++++++++------------ 1 file changed, 73 insertions(+), 86 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 30dc04e0e..0eb94904f 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -1,7 +1,8 @@ import Module from "../../core/Module.js"; class Range { - constructor(row, col) { + constructor(spreadsheet, row, col) { + this.spreadsheet = spreadsheet; this.start = { row, col }; this.end = { row, col }; this._updateMinMax(); @@ -26,33 +27,34 @@ class Range { this.maxCol = Math.max(this.start.col, this.end.col); } - atTopLeft(row, col) { - return row === this.minRow && col === this.minCol; - } - - atBottomRight(row, col) { - return row === this.maxRow && col === this.maxCol; + atTopLeft(cell) { + return ( + cell.row.position - 1 === this.minRow && + cell.column.position - 2 === this.minCol + ); } - occupies(row, col) { + atBottomRight(cell) { return ( - this.minRow <= row && - row <= this.maxRow && - this.minCol <= col && - col <= this.maxCol + cell.row.position - 1 === this.maxRow && + cell.column.position - 2 === this.maxCol ); } - isStartingAt(row, col) { - return this.start.row === row && this.start.col === col; + occupies(cell) { + return this.occupiesRow(cell.row) && this.occupiesColumn(cell.column); } - withinRow(row) { - return row >= this.minRow && row <= this.maxRow; + occupiesRow(row) { + return this.minRow <= row.position - 1 && row.position - 1 <= this.maxRow; } - withinColumn(col) { - return col >= this.minCol && col <= this.maxCol; + occupiesColumn(col) { + return this.minCol <= col.position - 2 && col.position - 2 <= this.maxCol; + } + + getData() { + return this.spreadsheet.getDataByRange(this); } } @@ -60,14 +62,16 @@ class Spreadsheet extends Module { constructor(table) { super(table); - this.ranges = [new Range(0, 0)]; this.selecting = false; + this.resetRanges(); + + this.registerTableOption("spreadsheet", false); //enable spreadsheet + this.registerTableOption("rowHeaderField", "--row-position"); //field name for row header } initialize() { if (!this.table.options.spreadsheet) return; - this.registerTableOption("rowHeaderField", "--row-position"); this.registerTableFunction( "getSelectedData", this.getSelectedData.bind(this), @@ -80,6 +84,7 @@ class Spreadsheet extends Module { this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); + this.subscribe("cell-contextmenu", this.handleCellContextMenu.bind(this)); this.subscribe("cell-rendered", this.renderCell.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); @@ -147,20 +152,18 @@ class Spreadsheet extends Module { } renderCell(cell) { - var el = cell.getElement(); - var row = cell.row.position - 1; - var col = cell.column.position - 2; + var element = cell.getElement(); - el.classList.toggle( + element.classList.toggle( "tabulator-cell-selected", - this.ranges.some((range) => range.occupies(row, col)), + this.ranges.some((range) => range.occupies(cell)), ); - el.classList.toggle( + element.classList.toggle( "tabulator-only-cell-selected", this.ranges.length === 1 && - this.ranges[0].atTopLeft(row, col) && - this.ranges[0].atBottomRight(row, col), + this.ranges[0].atTopLeft(cell) && + this.ranges[0].atBottomRight(cell), ); if (cell.column.field === this.options("rowHeaderField")) { @@ -168,37 +171,13 @@ class Spreadsheet extends Module { if (this.table.initialized && this.table.getPage) { n += (this.table.getPage() - 1) * this.table.getPageSize(); } - el.innerText = n; + element.innerText = n; } } - selectingAllColumnsOfRow(row) { - if (this.selecting === "all") return true; - - return this.ranges.some( - (range) => - this.selecting === "row" && - range.start.col === 0 && - range.end.col === this.table.columnManager.columns.length - 2 && - range.withinRow(row), - ); - } - - selectingAllRowsOfColumn(col) { - if (this.selecting === "all") return true; - - return this.ranges.some( - (range) => - this.selecting === "column" && - range.start.row === 0 && - range.end.row === this.rowsPerPage - 1 && - range.withinColumn(col), - ); - } - handleColumnMouseDown(event, column) { if (column.field === this.options("rowHeaderField")) { - this.ranges = [new Range(0, 0)]; + this.resetRanges(); this.selecting = "all"; const topLeftCell = this.table.rowManager @@ -227,10 +206,19 @@ class Spreadsheet extends Module { } handleCellMouseDown(event, cell) { + if (event.button === 2 && this.getActiveRange().occupies(cell)) return; + this._select(event, cell); this.layoutElement(); } + handleCellContextMenu(event, cell) { + var range = this.getActiveRange(); + if (event.button === 2 && range.occupies(cell)) { + this.dispatchExternal("rangeContextMenu", event, range, cell); + } + } + _select(event, element) { if (element.type === "column") { this.selecting = "column"; @@ -247,12 +235,12 @@ class Spreadsheet extends Module { this.endSelection(element); } else if (event.ctrlKey) { - this.ranges.push(new Range(0, 0)); + this.ranges.push(new Range(this, 0, 0)); this.beginSelection(element); this.endSelection(element); } else { - this.ranges = [new Range(0, 0)]; + this.resetRanges(); this.beginSelection(element); this.endSelection(element); @@ -328,6 +316,7 @@ class Spreadsheet extends Module { this.table.rowManager.getVisibleRows(true).forEach((row) => { if (row.type === "row") { this.layoutRow(row); + row.cells.forEach((cell) => this.renderCell(cell)); } }); @@ -339,33 +328,35 @@ class Spreadsheet extends Module { } layoutRow(row) { - row - .getElement() - .classList.toggle( - "tabulator-row-selected", - this.selectingAllColumnsOfRow(row.position - 1), + var selected = + this.selecting === "all" || + this.ranges.some( + (range) => + this.selecting === "row" && + range.start.col === 0 && + range.end.col === this.table.columnManager.columns.length - 2 && + range.occupiesRow(row), ); + var highlight = this.ranges.some((range) => range.occupiesRow(row)); - row.getElement().classList.toggle( - "tabulator-row-highlight", - this.ranges.some((range) => range.withinRow(row.position - 1)), - ); - - row.cells.forEach((cell) => this.renderCell(cell)); + row.getElement().classList.toggle("tabulator-row-selected", selected); + row.getElement().classList.toggle("tabulator-row-highlight", highlight); } layoutColumn(column) { - column - .getElement() - .classList.toggle( - "tabulator-col-selected", - this.selectingAllRowsOfColumn(column.position - 2), + var selected = + this.selecting === "all" || + this.ranges.some( + (range) => + this.selecting === "column" && + range.start.row === 0 && + range.end.row === this.rowsPerPage - 1 && + range.occupiesColumn(column), ); + var highlight = this.ranges.some((range) => range.occupiesColumn(column)); - column.getElement().classList.toggle( - "tabulator-col-highlight", - this.ranges.some((range) => range.withinColumn(column.position - 2)), - ); + column.getElement().classList.toggle("tabulator-col-selected", selected); + column.getElement().classList.toggle("tabulator-col-highlight", highlight); } handleColumnUpdating() { @@ -433,12 +424,8 @@ class Spreadsheet extends Module { }); } - tableBuilt() { - this.layoutElement(); - } - handlePageChanged() { - this.ranges = [new Range(0, 0)]; + this.resetRanges(); this.layoutElement(); } @@ -465,11 +452,11 @@ class Spreadsheet extends Module { } getColumnsByRange(range) { - return this.table.getColumns().slice( - // skip row header - range.minCol + 1, - range.maxCol + 2, - ); + return this.table.getColumns().slice(range.minCol + 1, range.maxCol + 2); + } + + resetRanges() { + this.ranges = [new Range(this, 0, 0)]; } get rowsPerPage() { From db54d9f02983c59c9476ff149d6e04660e7b5a97 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Wed, 11 Oct 2023 00:22:40 +0700 Subject: [PATCH 08/48] use rownum, add range to interaction, themes, refactors --- src/js/core/tools/InteractionMonitor.js | 12 +- src/js/modules/Export/Export.js | 2 +- .../Format/defaults/formatters/rownum.js | 6 +- src/js/modules/Menu/Menu.js | 11 + src/js/modules/Popup/Popup.js | 10 + src/js/modules/Spreadsheet/Range.js | 68 ++++++ src/js/modules/Spreadsheet/RangeComponent.js | 25 +++ src/js/modules/Spreadsheet/Spreadsheet.js | 212 ++++++++---------- src/scss/tabulator.scss | 104 +++++---- src/scss/themes/tabulator_midnight.scss | 4 + src/scss/themes/tabulator_simple.scss | 2 + src/scss/themes/tabulator_site.scss | 2 + 12 files changed, 286 insertions(+), 172 deletions(-) create mode 100644 src/js/modules/Spreadsheet/Range.js create mode 100644 src/js/modules/Spreadsheet/RangeComponent.js diff --git a/src/js/core/tools/InteractionMonitor.js b/src/js/core/tools/InteractionMonitor.js index d71238cd0..7bdafba90 100644 --- a/src/js/core/tools/InteractionMonitor.js +++ b/src/js/core/tools/InteractionMonitor.js @@ -32,6 +32,7 @@ export default class InteractionManager extends CoreFeature { "tabulator-row":"row", "tabulator-group":"group", "tabulator-col":"column", + "tabulator-range-cell":"range", }; this.pseudoTrackers = { @@ -51,6 +52,10 @@ export default class InteractionManager extends CoreFeature { subscriber:null, target:null, }, + "range":{ + subscriber:null, + target:null, + }, }; this.pseudoTracking = false; @@ -282,6 +287,11 @@ export default class InteractionManager extends CoreFeature { } } break; + case "range": + if(listener.components.includes("range")){ + component = this.table.modules.spreadsheet.findRange(target); + } + break; } } @@ -319,4 +329,4 @@ export default class InteractionManager extends CoreFeature { } } } -} \ No newline at end of file +} diff --git a/src/js/modules/Export/Export.js b/src/js/modules/Export/Export.js index c090735da..7b9ceaea3 100644 --- a/src/js/modules/Export/Export.js +++ b/src/js/modules/Export/Export.js @@ -39,7 +39,7 @@ class Export extends Module{ var headers, body; if (range === 'spreadsheet') { - var columns = this.table.modules.spreadsheet.selectedColumns + var columns = this.table.modules.spreadsheet.selectedColumns; headers = this.config.columnHeaders !== false ? this.headersToExportRows(this.generateColumnGroupHeaders(columns.map((component) => component._column))) : []; diff --git a/src/js/modules/Format/defaults/formatters/rownum.js b/src/js/modules/Format/defaults/formatters/rownum.js index 950fe297f..6dfbd79ec 100644 --- a/src/js/modules/Format/defaults/formatters/rownum.js +++ b/src/js/modules/Format/defaults/formatters/rownum.js @@ -1,10 +1,14 @@ export default function(cell, formatterParams, onRendered){ var content = document.createElement("span"); var row = cell.getRow(); + var table = cell.getTable(); row.watchPosition((position) => { + if (formatterParams.relativeToPage) { + position += table.modules.page.getPageSize() * (table.modules.page.getPage() - 1); + } content.innerText = position; }); return content; -} \ No newline at end of file +} diff --git a/src/js/modules/Menu/Menu.js b/src/js/modules/Menu/Menu.js index 3d35b9a72..7b49922a6 100644 --- a/src/js/modules/Menu/Menu.js +++ b/src/js/modules/Menu/Menu.js @@ -21,6 +21,7 @@ class Menu extends Module{ this.registerTableOption("groupContextMenu", false); this.registerTableOption("groupClickMenu", false); this.registerTableOption("groupDblClickMenu", false); + this.registerTableOption("rangeContextMenu", false); this.registerColumnOption("headerContextMenu"); this.registerColumnOption("headerClickMenu"); @@ -37,6 +38,7 @@ class Menu extends Module{ this.deprecatedOptionsCheck(); this.initializeRowWatchers(); this.initializeGroupWatchers(); + this.initializeRangeWatchers(); this.subscribe("column-init", this.initializeColumn.bind(this)); } @@ -76,6 +78,13 @@ class Menu extends Module{ this.subscribe("group-dblclick", this.loadMenuEvent.bind(this, this.table.options.groupDblClickMenu)); } } + + initializeRangeWatchers() { + if (this.table.options.rangeContextMenu) { + this.subscribe("range-contextmenu", this.loadMenuEvent.bind(this, this.table.options.rangeContextMenu)); + this.table.on("rangeTapHold", this.loadMenuEvent.bind(this, this.table.options.rangeContextMenu)); + } + } initializeColumn(column){ var def = column.definition; @@ -175,6 +184,8 @@ class Menu extends Module{ component = component._group; }else if(component._row){ component = component._row; + }else if(component._range) { + component = component._range; } menu = typeof menu == "function" ? menu.call(this.table, e, component.getComponent()) : menu; diff --git a/src/js/modules/Popup/Popup.js b/src/js/modules/Popup/Popup.js index aa229115a..0ffff333f 100644 --- a/src/js/modules/Popup/Popup.js +++ b/src/js/modules/Popup/Popup.js @@ -13,6 +13,7 @@ class Popup extends Module{ this.registerTableOption("groupContextPopup", false); this.registerTableOption("groupClickPopup", false); this.registerTableOption("groupDblClickPopup", false); + this.registerTableOption("rangeContextPopup", false); this.registerColumnOption("headerContextPopup"); this.registerColumnOption("headerClickPopup"); @@ -27,12 +28,14 @@ class Popup extends Module{ this.registerComponentFunction("column", "popup", this._componentPopupCall.bind(this)); this.registerComponentFunction("row", "popup", this._componentPopupCall.bind(this)); this.registerComponentFunction("group", "popup", this._componentPopupCall.bind(this)); + this.registerComponentFunction("range", "popup", this._componentPopupCall.bind(this)); } initialize(){ this.initializeRowWatchers(); this.initializeGroupWatchers(); + this.initializeRangeWatchers(); this.subscribe("column-init", this.initializeColumn.bind(this)); } @@ -70,6 +73,13 @@ class Popup extends Module{ this.subscribe("group-dblclick", this.loadPopupEvent.bind(this, this.table.options.groupDblClickPopup)); } } + + initializeRangeWatchers(){ + if(this.table.options.rangeContextPopup){ + this.subscribe("range-contextmenu", this.loadPopupEvent.bind(this, this.table.options.rangeContextPopup)); + this.table.on("rangeTapHold", this.loadPopupEvent.bind(this, this.table.options.rangeContextPopup)); + } + } initializeColumn(column){ var def = column.definition; diff --git a/src/js/modules/Spreadsheet/Range.js b/src/js/modules/Spreadsheet/Range.js new file mode 100644 index 000000000..22dd5c464 --- /dev/null +++ b/src/js/modules/Spreadsheet/Range.js @@ -0,0 +1,68 @@ +import RangeComponent from "./RangeComponent"; + +class Range { + constructor(table, row, col) { + this.table = table; + this.start = { row, col }; + this.end = { row, col }; + this._updateMinMax(); + } + + setStart(row, col) { + this.start.row = row; + this.start.col = col; + this._updateMinMax(); + } + + setEnd(row, col) { + this.end.row = row; + this.end.col = col; + this._updateMinMax(); + } + + _updateMinMax() { + this.minRow = Math.min(this.start.row, this.end.row); + this.maxRow = Math.max(this.start.row, this.end.row); + this.minCol = Math.min(this.start.col, this.end.col); + this.maxCol = Math.max(this.start.col, this.end.col); + } + + atTopLeft(cell) { + return ( + cell.row.position - 1 === this.minRow && + cell.column.position - 2 === this.minCol + ); + } + + atBottomRight(cell) { + return ( + cell.row.position - 1 === this.maxRow && + cell.column.position - 2 === this.maxCol + ); + } + + occupies(cell) { + return this.occupiesRow(cell.row) && this.occupiesColumn(cell.column); + } + + occupiesRow(row) { + return this.minRow <= row.position - 1 && row.position - 1 <= this.maxRow; + } + + occupiesColumn(col) { + return this.minCol <= col.position - 2 && col.position - 2 <= this.maxCol; + } + + getData() { + return this.table.modules.spreadsheet.getDataByRange(this); + } + + getComponent() { + if (!this.component) { + this.component = new RangeComponent(this); + } + return this.component; + } +} + +export default Range; diff --git a/src/js/modules/Spreadsheet/RangeComponent.js b/src/js/modules/Spreadsheet/RangeComponent.js new file mode 100644 index 000000000..1e37abc2d --- /dev/null +++ b/src/js/modules/Spreadsheet/RangeComponent.js @@ -0,0 +1,25 @@ +class RangeComponent { + constructor(range) { + this._range = range; + + return new Proxy(this, { + get: function (target, name, receiver) { + if (typeof target[name] !== "undefined") { + return target[name]; + } else { + return target._range.table.componentFunctionBinder.handle( + "range", + target._range, + name, + ); + } + }, + }); + } + + getData() { + return this._range.getData(); + } +} + +export default RangeComponent; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 0eb94904f..37531359b 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -1,62 +1,5 @@ import Module from "../../core/Module.js"; - -class Range { - constructor(spreadsheet, row, col) { - this.spreadsheet = spreadsheet; - this.start = { row, col }; - this.end = { row, col }; - this._updateMinMax(); - } - - setStart(row, col) { - this.start.row = row; - this.start.col = col; - this._updateMinMax(); - } - - setEnd(row, col) { - this.end.row = row; - this.end.col = col; - this._updateMinMax(); - } - - _updateMinMax() { - this.minRow = Math.min(this.start.row, this.end.row); - this.maxRow = Math.max(this.start.row, this.end.row); - this.minCol = Math.min(this.start.col, this.end.col); - this.maxCol = Math.max(this.start.col, this.end.col); - } - - atTopLeft(cell) { - return ( - cell.row.position - 1 === this.minRow && - cell.column.position - 2 === this.minCol - ); - } - - atBottomRight(cell) { - return ( - cell.row.position - 1 === this.maxRow && - cell.column.position - 2 === this.maxCol - ); - } - - occupies(cell) { - return this.occupiesRow(cell.row) && this.occupiesColumn(cell.column); - } - - occupiesRow(row) { - return this.minRow <= row.position - 1 && row.position - 1 <= this.maxRow; - } - - occupiesColumn(col) { - return this.minCol <= col.position - 2 && col.position - 2 <= this.maxCol; - } - - getData() { - return this.spreadsheet.getDataByRange(this); - } -} +import Range from "./Range.js"; class Spreadsheet extends Module { constructor(table) { @@ -66,7 +9,8 @@ class Spreadsheet extends Module { this.resetRanges(); this.registerTableOption("spreadsheet", false); //enable spreadsheet - this.registerTableOption("rowHeaderField", "--row-position"); //field name for row header + this.registerTableOption("spreadsheetRowHeader", {}); //row header definition + this.registerTableOption("rowHeaderField", "--row-header"); //field name for row header } initialize() { @@ -77,6 +21,11 @@ class Spreadsheet extends Module { this.getSelectedData.bind(this), ); + this.initializeWatchers(); + this.initializeTable(); + } + + initializeWatchers() { this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); this.subscribe("column-width", this.handleColumnUpdating.bind(this)); @@ -84,18 +33,9 @@ class Spreadsheet extends Module { this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); - this.subscribe("cell-contextmenu", this.handleCellContextMenu.bind(this)); this.subscribe("cell-rendered", this.renderCell.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); - - var mouseUpHandler = this.handleMouseUp.bind(this); - document.addEventListener("mouseup", mouseUpHandler); - this.subscribe("table-destroy", () => - document.removeEventListener(mouseUpHandler), - ); - - this.initializeTable(); } initializeTable() { @@ -105,25 +45,54 @@ class Spreadsheet extends Module { // Edit on double click if (column.editor) column.editable = false; } + this.table.options.columns = [ { title: "", field: this.options("rowHeaderField"), headerSort: false, - resizable: true, + resizable: false, frozen: true, editable: false, cssClass: "tabulator-row-header", + formatter: "rownum", + formatterParams: { relativeToPage: true }, + ...this.table.options.spreadsheetRowHeader, }, ...this.table.options.columns, ]; this.table.options.clipboardCopyRowRange = "spreadsheet"; - this.table.element.classList.add("tabulator-spreadsheet"); - this.overlay = document.createElement("div"); - this.overlay.classList.add("tabulator-selection-overlay"); + this.overlay.classList.add("tabulator-range-overlay"); + + this.rangeContainer = document.createElement("div"); + this.rangeContainer.classList.add("tabulator-range-container"); + + this.activeRangeCellElement = document.createElement("div"); + this.activeRangeCellElement.classList.add("tabulator-range-cell-active"); + + this.overlay.appendChild(this.rangeContainer); + this.overlay.appendChild(this.activeRangeCellElement); + + var resizeObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + this.overlay.style.width = entry.contentRect.width + "px"; + this.overlay.style.height = entry.contentRect.height + "px"; + } + }); + + resizeObserver.observe(this.table.rowManager.tableElement); + + var mouseUpHandler = this.handleMouseUp.bind(this); + + document.addEventListener("mouseup", mouseUpHandler); + + this.subscribe("table-destroy", () => { + document.removeEventListener(mouseUpHandler); + this.resizeObserver.disconnect(); + }); this.table.rowManager.element.appendChild(this.overlay); } @@ -135,15 +104,13 @@ class Spreadsheet extends Module { getDataByRange(range) { var data = []; var rows = this.getRowsByRange(range); - var columns = this.getColumnsByRange(range).map( - (component) => component._column, - ); + var columns = this.getColumnsByRange(range); rows.forEach((row) => { var rowData = row.getData(); var result = {}; columns.forEach((column) => { - result[column.field] = rowData[column.field]; + result[column._column.field] = rowData[column._column.field]; }); data.push(result); }); @@ -152,27 +119,22 @@ class Spreadsheet extends Module { } renderCell(cell) { - var element = cell.getElement(); + var el = cell.getElement(); - element.classList.toggle( - "tabulator-cell-selected", - this.ranges.some((range) => range.occupies(cell)), - ); + el.classList.add("tabulator-spreadsheet"); - element.classList.toggle( - "tabulator-only-cell-selected", + var rangeIdx = this.ranges.findIndex((range) => range.occupies(cell)); + + el.classList.toggle("tabulator-range-cell", rangeIdx !== -1); + + el.classList.toggle( + "tabulator-range-single-cell", this.ranges.length === 1 && this.ranges[0].atTopLeft(cell) && this.ranges[0].atBottomRight(cell), ); - if (cell.column.field === this.options("rowHeaderField")) { - var n = cell.row.position; - if (this.table.initialized && this.table.getPage) { - n += (this.table.getPage() - 1) * this.table.getPageSize(); - } - element.innerText = n; - } + el.dataset.range = rangeIdx; } handleColumnMouseDown(event, column) { @@ -235,7 +197,7 @@ class Spreadsheet extends Module { this.endSelection(element); } else if (event.ctrlKey) { - this.ranges.push(new Range(this, 0, 0)); + this.ranges.push(new Range(this.table, 0, 0)); this.beginSelection(element); this.endSelection(element); @@ -287,10 +249,11 @@ class Spreadsheet extends Module { endSelection(element) { var range = this.getActiveRange(); + var rowsCount = this.table.rowManager.getDisplayRows().length; if (element.type === "column") { if (this.selecting === "column") { - range.setEnd(this.rowsPerPage - 1, element.position - 2); + range.setEnd(rowsCount - 1, element.position - 2); } else if (this.selecting === "cell") { range.setEnd(0, element.position - 1); } @@ -306,7 +269,7 @@ class Spreadsheet extends Module { } else if (this.selecting !== "row" && isRowHeader) { range.setEnd(row, 0); } else if (this.selecting === "column") { - range.setEnd(this.rowsPerPage - 1, col); + range.setEnd(rowsCount - 1, col); } else { range.setEnd(row, col); } @@ -328,6 +291,10 @@ class Spreadsheet extends Module { } layoutRow(row) { + var el = row.getElement(); + + el.classList.add("tabulator-spreadsheet"); + var selected = this.selecting === "all" || this.ranges.some( @@ -339,24 +306,29 @@ class Spreadsheet extends Module { ); var highlight = this.ranges.some((range) => range.occupiesRow(row)); - row.getElement().classList.toggle("tabulator-row-selected", selected); - row.getElement().classList.toggle("tabulator-row-highlight", highlight); + el.classList.toggle("tabulator-row-selected", selected); + el.classList.toggle("tabulator-row-highlight", highlight); } layoutColumn(column) { + var el = column.getElement(); + + el.classList.add("tabulator-spreadsheet"); + + var rowsCount = this.table.rowManager.getDisplayRows().length; var selected = this.selecting === "all" || this.ranges.some( (range) => this.selecting === "column" && range.start.row === 0 && - range.end.row === this.rowsPerPage - 1 && + range.end.row === rowsCount - 1 && range.occupiesColumn(column), ); var highlight = this.ranges.some((range) => range.occupiesColumn(column)); - column.getElement().classList.toggle("tabulator-col-selected", selected); - column.getElement().classList.toggle("tabulator-col-highlight", highlight); + el.classList.toggle("tabulator-col-selected", selected); + el.classList.toggle("tabulator-col-highlight", highlight); } handleColumnUpdating() { @@ -366,35 +338,27 @@ class Spreadsheet extends Module { renderSelection() { this.overlay.classList.remove("tabulator-column-updating"); - var tableElement = this.table.rowManager.tableElement; - - this.overlay.style.left = tableElement.scrollLeft + "px"; - this.overlay.style.top = tableElement.scrollTop + "px"; - this.overlay.style.width = tableElement.clientWidth + "px"; - this.overlay.style.height = tableElement.clientHeight + "px"; - - while (this.overlay.firstChild) { - this.overlay.removeChild(this.overlay.firstChild); + while (this.rangeContainer.firstChild) { + this.rangeContainer.removeChild(this.rangeContainer.firstChild); } var activeCell = this.getActiveCell(); - var activeEl = document.createElement("div"); - activeEl.classList.add("tabulator-selection-active-cell"); - activeEl.style.left = activeCell.getElement().offsetLeft + "px"; - activeEl.style.top = activeCell.row.getElement().offsetTop + "px"; - activeEl.style.width = + + this.activeRangeCellElement.style.left = + activeCell.getElement().offsetLeft + "px"; + this.activeRangeCellElement.style.top = + activeCell.row.getElement().offsetTop + "px"; + this.activeRangeCellElement.style.width = activeCell.getElement().offsetLeft + activeCell.getElement().offsetWidth - activeCell.getElement().offsetLeft + "px"; - activeEl.style.height = + this.activeRangeCellElement.style.height = activeCell.row.getElement().offsetTop + activeCell.row.getElement().offsetHeight - activeCell.row.getElement().offsetTop + "px"; - this.overlay.appendChild(activeEl); - var activeRange = this.getActiveRange(); this.ranges.forEach((range) => { @@ -402,9 +366,9 @@ class Spreadsheet extends Module { var bottomRightCell = this.getCell(range.maxRow, range.maxCol); var rangeEl = document.createElement("div"); - rangeEl.classList.add("tabulator-selection-range"); + rangeEl.classList.add("tabulator-range"); if (range === activeRange) { - rangeEl.classList.add("tabulator-selection-range-active"); + rangeEl.classList.add("tabulator-range-active"); } rangeEl.style.left = topLeftCell.getElement().offsetLeft + "px"; @@ -420,7 +384,7 @@ class Spreadsheet extends Module { topLeftCell.row.getElement().offsetTop + "px"; - this.overlay.appendChild(rangeEl); + this.rangeContainer.appendChild(rangeEl); }); } @@ -429,6 +393,12 @@ class Spreadsheet extends Module { this.layoutElement(); } + findRange(cell) { + var rangeIdx = cell.dataset.range; + if (rangeIdx < 0) return; + return this.ranges[rangeIdx].getComponent(); + } + getCell(row, col) { return this.table.rowManager.getRowFromPosition(row + 1).getCells()[ col + 1 @@ -456,11 +426,7 @@ class Spreadsheet extends Module { } resetRanges() { - this.ranges = [new Range(this, 0, 0)]; - } - - get rowsPerPage() { - return this.table.rowManager.getVisibleRows().length; + this.ranges = [new Range(this.table, 0, 0)]; } get selectedRows() { diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index 3aebbfbf0..52731bd37 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -25,12 +25,16 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered -$headerHighlightBackground: #9ABCEA !default; //row background color when selected -$headerTextHighlightBackground: #000000 !default; //row background color when selected $headerSelectedBackground: #4990F1 !default; //header background color when selected $headerTextSelectedBackground: #FFFFFF !default; //header text color when selected $cellSelectedBackground: #C2D5EF !default; //cell background color when selected -$cellSelectedBorderColor: #2975DD !default; //cell border color when selected + +$headerHighlightBackground: #D6D6D6 !default; //header background color when highlighted +$headerTextHighlightBackground: #000000 !default; //header text color when highlighted +$headerBorderHighlightColor:#777 !default; //header border color when highlighted + +$rangeBorderColor: #2975DD !default; //range border color +$rangeHandleColor: #2975DD !default; //range handle color $editBoxColor:#1D68CD !default; //border color for edit boxes $errorColor:#dd0000 !default; //error indication @@ -1507,17 +1511,20 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ // Spreadsheet module styling -.tabulator-spreadsheet{ - & .tabulator-cell{ - user-select: none; - position: relative; - - &.tabulator-cell-selected:not(.tabulator-only-cell-selected):not(.tabulator-row-header){ - background-color: $cellSelectedBackground; - } - +.tabulator-headers .tabulator-spreadsheet.tabulator-col{ + &.tabulator-col-highlight { + background-color: $headerHighlightBackground; + color: $headerTextHighlightBackground; + border-color: $headerBorderHighlightColor; } - & .tabulator-row{ + &.tabulator-col-selected{ + background-color: $headerSelectedBackground; + color: $headerTextSelectedBackground; + } +} + +.tabulator-table .tabulator-spreadsheet{ + &.tabulator-row{ &.tabulator-row-highlight .tabulator-row-header{ background-color: $headerHighlightBackground; color: $headerTextHighlightBackground; @@ -1527,47 +1534,52 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ color: $headerTextSelectedBackground; } } - & .tabulator-col { - &.tabulator-col-highlight { - background-color: $headerHighlightBackground; - color: $headerTextHighlightBackground; + &.tabulator-cell{ + user-select: none; + position: relative; + + &.tabulator-range-cell:not(.tabulator-range-single-cell):not(.tabulator-row-header){ + background-color: $cellSelectedBackground; } - &.tabulator-col-selected{ - background-color: $headerSelectedBackground; - color: $headerTextSelectedBackground; + + &.tabulator-row-header { + text-align: center; } } - & .tabulator-selection-overlay { - position: absolute; - overflow: hidden; - z-index: 10; - pointer-events: none; +} - & .tabulator-selection-range { - position: absolute; - box-sizing: border-box; - border: 1px solid $cellSelectedBorderColor; +.tabulator-range-overlay { + position: absolute; + left: 0; + top: 0; + overflow: hidden; + z-index: 10; + pointer-events: none; - &.tabulator-selection-range-active::after { - content: ''; - position: absolute; - right: -3px; - bottom: -3px; - width: 6px; - height: 6px; - background-color: $cellSelectedBorderColor; - border-radius: 999px; - } - } + & .tabulator-range { + position: absolute; + box-sizing: border-box; + border: 1px solid $rangeBorderColor; - & .tabulator-selection-active-cell { + &.tabulator-range-active::after { + content: ''; position: absolute; - box-sizing: border-box; - border: 2px solid $cellSelectedBorderColor; + right: -3px; + bottom: -3px; + width: 6px; + height: 6px; + background-color: $rangeHandleColor; + border-radius: 999px; } + } - &.tabulator-column-updating { - display: none; - } + & .tabulator-range-cell-active { + position: absolute; + box-sizing: border-box; + border: 2px solid $rangeBorderColor; + } + + &.tabulator-column-updating { + display: none; } } diff --git a/src/scss/themes/tabulator_midnight.scss b/src/scss/themes/tabulator_midnight.scss index 92c5a6dfc..e6c832bac 100644 --- a/src/scss/themes/tabulator_midnight.scss +++ b/src/scss/themes/tabulator_midnight.scss @@ -24,6 +24,10 @@ $rowHoverBackground:#999 !default; //row background color on hover $rowSelectedBackground: #000 !default; //row background color when selected $rowSelectedBackgroundHover: #888 !default;//row background color when selected and hovered +$cellSelectedBackground: #495B75 !default; //cell background color when selected + +$headerHighlightBackground: #777 !default; //header background color when highlighted +$headerTextHighlightBackground: #fff !default; //header background color when highlighted $editBoxColor:#999 !default; //border color for edit boxes $errorColor:#dd0000 !default; //error indication diff --git a/src/scss/themes/tabulator_simple.scss b/src/scss/themes/tabulator_simple.scss index 7b0bebf0e..dec33780d 100644 --- a/src/scss/themes/tabulator_simple.scss +++ b/src/scss/themes/tabulator_simple.scss @@ -25,6 +25,8 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered +$rowBorderSelectedColor:#aaa !default; //table border color on selected +$headerBorderSelectedColor:#aaa !default; //header border color when selected $editBoxColor:#1D68CD !default; //border color for edit boxes $errorColor:#dd0000 !default; //error indication diff --git a/src/scss/themes/tabulator_site.scss b/src/scss/themes/tabulator_site.scss index 775841e77..a3b6f8e26 100644 --- a/src/scss/themes/tabulator_site.scss +++ b/src/scss/themes/tabulator_site.scss @@ -25,6 +25,8 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered +$headerHighlightBackground: #CCC !default; //header background color when highlighted + $editBoxColor:#1D68CD !default; //border color for edit boxes $errorColor:#dd0000 !default; //error indication From ad44510742d96c8c7bf81f8c85f1e91b6c26de0a Mon Sep 17 00:00:00 2001 From: azmy60 Date: Thu, 12 Oct 2023 00:11:57 +0700 Subject: [PATCH 09/48] optimize range rendering, ignore invisible columns, fix header context menu --- src/js/core/ColumnManager.js | 5 +- src/js/core/column/Column.js | 7 +- src/js/core/tools/InteractionMonitor.js | 2 +- src/js/modules/Spreadsheet/Range.js | 15 +- src/js/modules/Spreadsheet/RangeComponent.js | 12 + src/js/modules/Spreadsheet/Spreadsheet.js | 229 +++++++++++++------ src/scss/tabulator.scss | 6 +- 7 files changed, 193 insertions(+), 83 deletions(-) diff --git a/src/js/core/ColumnManager.js b/src/js/core/ColumnManager.js index 5cab3fe79..ab750cf6e 100644 --- a/src/js/core/ColumnManager.js +++ b/src/js/core/ColumnManager.js @@ -301,7 +301,6 @@ export default class ColumnManager extends CoreFeature { registerColumnPosition(col){ this.columnsByIndex.push(col); - return this.columnsByIndex.length; } _reIndexColumns(){ @@ -408,6 +407,10 @@ export default class ColumnManager extends CoreFeature { return index > -1 ? this.columnsByIndex[index] : false; } + + getVisibleColumnsByIndex() { + return this.columnsByIndex.filter((col) => col.visible); + } getColumns(){ return this.columns; diff --git a/src/js/core/column/Column.js b/src/js/core/column/Column.js index 447ad6886..d23808f80 100644 --- a/src/js/core/column/Column.js +++ b/src/js/core/column/Column.js @@ -22,7 +22,6 @@ class Column extends CoreFeature{ this.isGroup = false; this.hozAlign = ""; //horizontal text alignment this.vertAlign = ""; //vert text alignment - this.position = 0; //multi dimensional filed handling this.field =""; @@ -129,7 +128,7 @@ class Column extends CoreFeature{ //register column position with column manager registerColumnPosition(column){ - this.position = this.parent.registerColumnPosition(column); + this.parent.registerColumnPosition(column); } //register column position with column manager @@ -943,6 +942,10 @@ class Column extends CoreFeature{ return this.component; } + + get position() { + return this.table.columnManager.getVisibleColumnsByIndex().indexOf(this) + 1; + } } Column.defaultOptionList = defaultOptions; diff --git a/src/js/core/tools/InteractionMonitor.js b/src/js/core/tools/InteractionMonitor.js index 7bdafba90..b920857f1 100644 --- a/src/js/core/tools/InteractionMonitor.js +++ b/src/js/core/tools/InteractionMonitor.js @@ -32,7 +32,7 @@ export default class InteractionManager extends CoreFeature { "tabulator-row":"row", "tabulator-group":"group", "tabulator-col":"column", - "tabulator-range-cell":"range", + "tabulator-range":"range", }; this.pseudoTrackers = { diff --git a/src/js/modules/Spreadsheet/Range.js b/src/js/modules/Spreadsheet/Range.js index 22dd5c464..6998eb54a 100644 --- a/src/js/modules/Spreadsheet/Range.js +++ b/src/js/modules/Spreadsheet/Range.js @@ -1,11 +1,12 @@ import RangeComponent from "./RangeComponent"; class Range { - constructor(table, row, col) { + constructor(table, row, col, element) { this.table = table; this.start = { row, col }; this.end = { row, col }; this._updateMinMax(); + this.element = element; } setStart(row, col) { @@ -57,12 +58,24 @@ class Range { return this.table.modules.spreadsheet.getDataByRange(this); } + getCells() { + return this.table.modules.spreadsheet.getCellsByRange(this); + } + + getStructuredCells() { + return this.table.modules.spreadsheet.getCellsByRange(this, true); + } + getComponent() { if (!this.component) { this.component = new RangeComponent(this); } return this.component; } + + destroy() { + this.element.remove(); + } } export default Range; diff --git a/src/js/modules/Spreadsheet/RangeComponent.js b/src/js/modules/Spreadsheet/RangeComponent.js index 1e37abc2d..202e9b41a 100644 --- a/src/js/modules/Spreadsheet/RangeComponent.js +++ b/src/js/modules/Spreadsheet/RangeComponent.js @@ -17,9 +17,21 @@ class RangeComponent { }); } + getElement() { + return this._range.element; + } + getData() { return this._range.getData(); } + + getCells() { + return this._range.getCells(); + } + + getStructuredCells() { + return this._range.getStructuredCells(); + } } export default RangeComponent; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 37531359b..4ee706c82 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -5,8 +5,9 @@ class Spreadsheet extends Module { constructor(table) { super(table); + this.prevSelection = false; this.selecting = false; - this.resetRanges(); + this.ranges = []; this.registerTableOption("spreadsheet", false); //enable spreadsheet this.registerTableOption("spreadsheetRowHeader", {}); //row header definition @@ -28,8 +29,8 @@ class Spreadsheet extends Module { initializeWatchers() { this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); - this.subscribe("column-width", this.handleColumnUpdating.bind(this)); - this.subscribe("column-resized", this.renderSelection.bind(this)); + this.subscribe("column-width", this.handleColumnWidth.bind(this)); + this.subscribe("column-resized", this.layoutSelection.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); @@ -94,6 +95,8 @@ class Spreadsheet extends Module { this.resizeObserver.disconnect(); }); + this.resetRanges(); + this.table.rowManager.element.appendChild(this.overlay); } @@ -110,7 +113,7 @@ class Spreadsheet extends Module { var rowData = row.getData(); var result = {}; columns.forEach((column) => { - result[column._column.field] = rowData[column._column.field]; + result[column.field] = rowData[column.field]; }); data.push(result); }); @@ -118,6 +121,34 @@ class Spreadsheet extends Module { return data; } + getCellsByRange(range, structured) { + var cells = []; + var rows = this.getRowsByRange(range); + var columns = this.getColumnsByRange(range); + + if (structured) { + cells = rows.map((row) => { + var arr = []; + row.getCells().forEach((cell) => { + if (columns.includes(cell.column)) { + arr.push(cell.getComponent()); + } + }); + return arr; + }); + } else { + rows.forEach((row) => { + row.getCells().forEach((cell) => { + if (columns.includes(cell.column)) { + cells.push(cell.getComponent()); + } + }); + }); + } + + return cells; + } + renderCell(cell) { var el = cell.getElement(); @@ -125,7 +156,7 @@ class Spreadsheet extends Module { var rangeIdx = this.ranges.findIndex((range) => range.occupies(cell)); - el.classList.toggle("tabulator-range-cell", rangeIdx !== -1); + el.classList.toggle("tabulator-range", rangeIdx !== -1); el.classList.toggle( "tabulator-range-single-cell", @@ -138,17 +169,20 @@ class Spreadsheet extends Module { } handleColumnMouseDown(event, column) { + if ( + event.button === 2 && + (this.prevSelection === "column" || this.prevSelection === "all") && + this.getActiveRange().occupiesColumn(column) + ) { + return; + } + if (column.field === this.options("rowHeaderField")) { this.resetRanges(); this.selecting = "all"; - const topLeftCell = this.table.rowManager - .getRowFromPosition(1) - .getCells()[1]; - const bottomRightCell = this.table.columnManager.columns - .slice(-1)[0] - .getCells() - .slice(-1)[0]; + const topLeftCell = this.getCell(0, 0); + const bottomRightCell = this.getCell(-1, -1); this.beginSelection(topLeftCell); this.endSelection(bottomRightCell); @@ -168,7 +202,14 @@ class Spreadsheet extends Module { } handleCellMouseDown(event, cell) { - if (event.button === 2 && this.getActiveRange().occupies(cell)) return; + if ( + event.button === 2 && + (this.getActiveRange().occupies(cell) || + ((this.prevSelection === "row" || this.prevSelection === "all") && + this.getActiveRange().occupiesRow(cell.row))) + ) { + return; + } this._select(event, cell); this.layoutElement(); @@ -197,7 +238,7 @@ class Spreadsheet extends Module { this.endSelection(element); } else if (event.ctrlKey) { - this.ranges.push(new Range(this.table, 0, 0)); + this.addRange(); this.beginSelection(element); this.endSelection(element); @@ -217,6 +258,9 @@ class Spreadsheet extends Module { } handleMouseUp() { + if (this.selecting !== false) { + this.prevSelection = this.selecting; + } this.selecting = false; } @@ -255,7 +299,7 @@ class Spreadsheet extends Module { if (this.selecting === "column") { range.setEnd(rowsCount - 1, element.position - 2); } else if (this.selecting === "cell") { - range.setEnd(0, element.position - 1); + range.setEnd(0, element.position - 2); } return; } @@ -265,7 +309,10 @@ class Spreadsheet extends Module { var isRowHeader = element.column.field === this.options("rowHeaderField"); if (this.selecting === "row") { - range.setEnd(row, this.table.columnManager.getColumns().length - 2); + range.setEnd( + row, + this.table.columnManager.getVisibleColumnsByIndex().length - 2, + ); } else if (this.selecting !== "row" && isRowHeader) { range.setEnd(row, 0); } else if (this.selecting === "column") { @@ -283,11 +330,11 @@ class Spreadsheet extends Module { } }); - this.table.columnManager.columns.forEach((column) => { + this.table.columnManager.getVisibleColumnsByIndex().forEach((column) => { this.layoutColumn(column); }); - this.renderSelection(); + this.layoutSelection(); } layoutRow(row) { @@ -295,19 +342,29 @@ class Spreadsheet extends Module { el.classList.add("tabulator-spreadsheet"); - var selected = - this.selecting === "all" || - this.ranges.some( + var colCount = this.table.columnManager.getVisibleColumnsByIndex().length; + + var rangeIdx = -1; + + if (this.selecting === "row") { + rangeIdx = this.ranges.findIndex( (range) => - this.selecting === "row" && range.start.col === 0 && - range.end.col === this.table.columnManager.columns.length - 2 && + range.end.col === colCount - 2 && range.occupiesRow(row), ); + } else if (this.selecting === "all") { + rangeIdx = this.ranges.length - 1; + } + + var selected = rangeIdx !== -1; var highlight = this.ranges.some((range) => range.occupiesRow(row)); + el.classList.toggle("tabulator-range", selected); el.classList.toggle("tabulator-row-selected", selected); el.classList.toggle("tabulator-row-highlight", highlight); + + el.dataset.range = rangeIdx; } layoutColumn(column) { @@ -315,33 +372,38 @@ class Spreadsheet extends Module { el.classList.add("tabulator-spreadsheet"); - var rowsCount = this.table.rowManager.getDisplayRows().length; - var selected = - this.selecting === "all" || - this.ranges.some( + var rowCount = this.table.rowManager.getDisplayRows().length; + + var rangeIdx = -1; + + if (this.selecting === "column") { + rangeIdx = this.ranges.findIndex( (range) => - this.selecting === "column" && range.start.row === 0 && - range.end.row === rowsCount - 1 && + range.end.row === rowCount - 1 && range.occupiesColumn(column), ); + } else if (this.selecting === "all") { + rangeIdx = this.ranges.length - 1; + } + + var selected = rangeIdx !== -1; + var highlight = this.ranges.some((range) => range.occupiesColumn(column)); + el.classList.toggle("tabulator-range", selected); el.classList.toggle("tabulator-col-selected", selected); el.classList.toggle("tabulator-col-highlight", highlight); - } - handleColumnUpdating() { - this.overlay.classList.add("tabulator-column-updating"); + el.dataset.range = rangeIdx; } - renderSelection() { - this.overlay.classList.remove("tabulator-column-updating"); - - while (this.rangeContainer.firstChild) { - this.rangeContainer.removeChild(this.rangeContainer.firstChild); - } + handleColumnWidth() { + if (!this.table.initialized) return; + this.layoutSelection(); + } + layoutSelection() { var activeCell = this.getActiveCell(); this.activeRangeCellElement.style.left = @@ -359,33 +421,30 @@ class Spreadsheet extends Module { activeCell.row.getElement().offsetTop + "px"; - var activeRange = this.getActiveRange(); + this.ranges.forEach((range) => this.layoutRange(range)); + } - this.ranges.forEach((range) => { - var topLeftCell = this.getCell(range.minRow, range.minCol); - var bottomRightCell = this.getCell(range.maxRow, range.maxCol); + layoutRange(range) { + var topLeftCell = this.getCell(range.minRow, range.minCol); + var bottomRightCell = this.getCell(range.maxRow, range.maxCol); - var rangeEl = document.createElement("div"); - rangeEl.classList.add("tabulator-range"); - if (range === activeRange) { - rangeEl.classList.add("tabulator-range-active"); - } + range.element.classList.toggle( + "tabulator-range-active", + range === this.getActiveRange(), + ); - rangeEl.style.left = topLeftCell.getElement().offsetLeft + "px"; - rangeEl.style.top = topLeftCell.row.getElement().offsetTop + "px"; - rangeEl.style.width = - bottomRightCell.getElement().offsetLeft + - bottomRightCell.getElement().offsetWidth - - topLeftCell.getElement().offsetLeft + - "px"; - rangeEl.style.height = - bottomRightCell.row.getElement().offsetTop + - bottomRightCell.row.getElement().offsetHeight - - topLeftCell.row.getElement().offsetTop + - "px"; - - this.rangeContainer.appendChild(rangeEl); - }); + range.element.style.left = topLeftCell.getElement().offsetLeft + "px"; + range.element.style.top = topLeftCell.row.getElement().offsetTop + "px"; + range.element.style.width = + bottomRightCell.getElement().offsetLeft + + bottomRightCell.getElement().offsetWidth - + topLeftCell.getElement().offsetLeft + + "px"; + range.element.style.height = + bottomRightCell.row.getElement().offsetTop + + bottomRightCell.row.getElement().offsetHeight - + topLeftCell.row.getElement().offsetTop + + "px"; } handlePageChanged() { @@ -399,10 +458,19 @@ class Spreadsheet extends Module { return this.ranges[rangeIdx].getComponent(); } - getCell(row, col) { - return this.table.rowManager.getRowFromPosition(row + 1).getCells()[ - col + 1 - ]; + getCell(rowIdx, colIdx) { + if (rowIdx < 0) { + rowIdx = this.table.rowManager.getDisplayRows().length + rowIdx; + } + + var row = this.table.rowManager.getRowFromPosition(rowIdx + 1); + + if (colIdx < 0) { + colIdx = + this.table.columnManager.getVisibleColumnsByIndex().length + colIdx - 1; + } + + return row.getCells().filter((cell) => cell.column.visible)[colIdx + 1]; } getActiveRange() { @@ -415,18 +483,31 @@ class Spreadsheet extends Module { } getRowsByRange(range) { - return this.table.rowManager.activeRows.slice( - range.minRow, - range.maxRow + 1, - ); + return this.table.rowManager + .getDisplayRows() + .slice(range.minRow, range.maxRow + 1); } getColumnsByRange(range) { - return this.table.getColumns().slice(range.minCol + 1, range.maxCol + 2); + return this.table.columnManager + .getVisibleColumnsByIndex() + .slice(range.minCol + 1, range.maxCol + 2); + } + + addRange() { + var element = document.createElement("div"); + element.classList.add("tabulator-range"); + + var range = new Range(this.table, 0, 0, element); + + this.ranges.push(range); + this.rangeContainer.appendChild(element); } resetRanges() { - this.ranges = [new Range(this.table, 0, 0)]; + this.ranges.forEach((range) => range.destroy()); + this.ranges = []; + this.addRange(0, 0); } get selectedRows() { @@ -434,7 +515,9 @@ class Spreadsheet extends Module { } get selectedColumns() { - return this.getColumnsByRange(this.getActiveRange()); + return this.getColumnsByRange(this.getActiveRange()).map((col) => + col.getComponent(), + ); } } diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index 52731bd37..e6cf2d0a2 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -1538,7 +1538,7 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ user-select: none; position: relative; - &.tabulator-range-cell:not(.tabulator-range-single-cell):not(.tabulator-row-header){ + &.tabulator-range:not(.tabulator-range-single-cell):not(.tabulator-row-header){ background-color: $cellSelectedBackground; } @@ -1578,8 +1578,4 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ box-sizing: border-box; border: 2px solid $rangeBorderColor; } - - &.tabulator-column-updating { - display: none; - } } From abecafe4b6665bef151dcd324eaab37a5675944a Mon Sep 17 00:00:00 2001 From: azmy60 Date: Fri, 13 Oct 2023 17:36:31 +0700 Subject: [PATCH 10/48] Respect column editable, row/col range context menu, handle empty table --- src/js/core/tools/InteractionMonitor.js | 14 ++++++++- src/js/modules/Menu/Menu.js | 16 +++++++++-- src/js/modules/Spreadsheet/Spreadsheet.js | 35 ++++++++++++++++++----- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/js/core/tools/InteractionMonitor.js b/src/js/core/tools/InteractionMonitor.js index b920857f1..e9d918d37 100644 --- a/src/js/core/tools/InteractionMonitor.js +++ b/src/js/core/tools/InteractionMonitor.js @@ -32,7 +32,9 @@ export default class InteractionManager extends CoreFeature { "tabulator-row":"row", "tabulator-group":"group", "tabulator-col":"column", - "tabulator-range":"range", + "tabulator-range": "range", + "tabulator-range-row":"range-row", + "tabulator-range-col":"range-col", }; this.pseudoTrackers = { @@ -56,6 +58,14 @@ export default class InteractionManager extends CoreFeature { subscriber:null, target:null, }, + "range-row":{ + subscriber:null, + target:null, + }, + "range-col":{ + subscriber:null, + target:null, + }, }; this.pseudoTracking = false; @@ -288,6 +298,8 @@ export default class InteractionManager extends CoreFeature { } break; case "range": + case "range-row": + case "range-col": if(listener.components.includes("range")){ component = this.table.modules.spreadsheet.findRange(target); } diff --git a/src/js/modules/Menu/Menu.js b/src/js/modules/Menu/Menu.js index 7b49922a6..1883b0538 100644 --- a/src/js/modules/Menu/Menu.js +++ b/src/js/modules/Menu/Menu.js @@ -31,7 +31,8 @@ class Menu extends Module{ this.registerColumnOption("contextMenu"); this.registerColumnOption("clickMenu"); this.registerColumnOption("dblClickMenu"); - + this.registerColumnOption("rangeRowContextMenu"); + this.registerColumnOption("rangeColumnContextMenu"); } initialize(){ @@ -80,7 +81,7 @@ class Menu extends Module{ } initializeRangeWatchers() { - if (this.table.options.rangeContextMenu) { + if(this.table.options.rangeContextMenu) { this.subscribe("range-contextmenu", this.loadMenuEvent.bind(this, this.table.options.rangeContextMenu)); this.table.on("rangeTapHold", this.loadMenuEvent.bind(this, this.table.options.rangeContextMenu)); } @@ -126,6 +127,17 @@ class Menu extends Module{ this.columnSubscribers.dblClickMenu = this.loadMenuTableCellEvent.bind(this, "dblClickMenu"); this.subscribe("cell-dblclick", this.columnSubscribers.dblClickMenu); } + + //handle range events + if(def.rangeColumnContextMenu) { + this.subscribe("range-col-contextmenu", this.loadMenuEvent.bind(this, def.rangeColumnContextMenu)); + this.table.on("rangeColTapHold", this.loadMenuEvent.bind(this, def.rangeColumnContextMenu)); + } + + if(def.rangeRowContextMenu) { + this.subscribe("range-row-contextmenu", this.loadMenuEvent.bind(this, def.rangeRowContextMenu)); + this.table.on("rangeRowTapHold", this.loadMenuEvent.bind(this, def.rangeRowContextMenu)); + } } initializeColumnHeaderMenu(column){ diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 4ee706c82..b351e82f9 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -35,6 +35,8 @@ class Spreadsheet extends Module { this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); this.subscribe("cell-rendered", this.renderCell.bind(this)); + this.subscribe("edit-success", this.unbindEditable.bind(this)); + this.subscribe("edit-cancelled", this.unbindEditable.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); } @@ -43,8 +45,11 @@ class Spreadsheet extends Module { for (var column of this.table.options.columns) { // Disable sorting by clicking header column.headerSort = false; + // Edit on double click - if (column.editor) column.editable = false; + var editable = column.editable !== undefined ? column.editable : true; + column.__spreadsheet_editable = editable; + column.editable = false; } this.table.options.columns = [ @@ -158,6 +163,8 @@ class Spreadsheet extends Module { el.classList.toggle("tabulator-range", rangeIdx !== -1); + el.classList.toggle("tabulator-range-cell", rangeIdx !== -1); + el.classList.toggle( "tabulator-range-single-cell", this.ranges.length === 1 && @@ -265,12 +272,18 @@ class Spreadsheet extends Module { } handleCellDblClick(_, cell) { - if ( - cell.column.field !== this.options("rowHeaderField") && - cell.column.definition.editor - ) { - cell.getComponent().edit(true); - } + if (cell.column.field === this.options("rowHeaderField")) return; + + cell.column.definition.editable = + cell.column.definition.__spreadsheet_editable; + this.table.modules.edit.initializeColumnCheck(cell.column); + + cell.getComponent().edit(); + } + + unbindEditable(cell) { + cell.column.definition.editable = false; + this.table.modules.edit.initializeColumnCheck(cell.column); } beginSelection(element) { @@ -361,6 +374,7 @@ class Spreadsheet extends Module { var highlight = this.ranges.some((range) => range.occupiesRow(row)); el.classList.toggle("tabulator-range", selected); + el.classList.toggle("tabulator-range-row", selected); el.classList.toggle("tabulator-row-selected", selected); el.classList.toggle("tabulator-row-highlight", highlight); @@ -392,6 +406,7 @@ class Spreadsheet extends Module { var highlight = this.ranges.some((range) => range.occupiesColumn(column)); el.classList.toggle("tabulator-range", selected); + el.classList.toggle("tabulator-range-col", selected); el.classList.toggle("tabulator-col-selected", selected); el.classList.toggle("tabulator-col-highlight", highlight); @@ -406,6 +421,8 @@ class Spreadsheet extends Module { layoutSelection() { var activeCell = this.getActiveCell(); + if (!activeCell) return; + this.activeRangeCellElement.style.left = activeCell.getElement().offsetLeft + "px"; this.activeRangeCellElement.style.top = @@ -465,11 +482,15 @@ class Spreadsheet extends Module { var row = this.table.rowManager.getRowFromPosition(rowIdx + 1); + if (!row) return; + if (colIdx < 0) { colIdx = this.table.columnManager.getVisibleColumnsByIndex().length + colIdx - 1; } + if (colIdx < 0) return; + return row.getCells().filter((cell) => cell.column.visible)[colIdx + 1]; } From f5e250c11c018902a631e6451bb792c752c63aaa Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sun, 15 Oct 2023 02:14:57 +0700 Subject: [PATCH 11/48] respect row and column headers interaction, add some helpers --- src/js/core/tools/InteractionMonitor.js | 25 ++++++++++------------- src/js/modules/Menu/Menu.js | 16 ++------------- src/js/modules/Spreadsheet/Spreadsheet.js | 24 +++++++++++++++++----- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/js/core/tools/InteractionMonitor.js b/src/js/core/tools/InteractionMonitor.js index e9d918d37..38c4e4353 100644 --- a/src/js/core/tools/InteractionMonitor.js +++ b/src/js/core/tools/InteractionMonitor.js @@ -32,9 +32,7 @@ export default class InteractionManager extends CoreFeature { "tabulator-row":"row", "tabulator-group":"group", "tabulator-col":"column", - "tabulator-range": "range", - "tabulator-range-row":"range-row", - "tabulator-range-col":"range-col", + "tabulator-range":"range", }; this.pseudoTrackers = { @@ -58,14 +56,6 @@ export default class InteractionManager extends CoreFeature { subscriber:null, target:null, }, - "range-row":{ - subscriber:null, - target:null, - }, - "range-col":{ - subscriber:null, - target:null, - }, }; this.pseudoTracking = false; @@ -245,6 +235,15 @@ export default class InteractionManager extends CoreFeature { if(targets.group && targets.group === targets.row){ delete targets.row; } + + if(targets.range && targets.range === targets.column) { + delete targets.range; + } + + if(targets.range) { + if (targets.range === targets.row) delete targets.range; + else delete targets.row; + } return targets; } @@ -298,10 +297,8 @@ export default class InteractionManager extends CoreFeature { } break; case "range": - case "range-row": - case "range-col": if(listener.components.includes("range")){ - component = this.table.modules.spreadsheet.findRange(target); + component = this.table.modules.spreadsheet.findRangeByCellElement(target); } break; } diff --git a/src/js/modules/Menu/Menu.js b/src/js/modules/Menu/Menu.js index 1883b0538..7b49922a6 100644 --- a/src/js/modules/Menu/Menu.js +++ b/src/js/modules/Menu/Menu.js @@ -31,8 +31,7 @@ class Menu extends Module{ this.registerColumnOption("contextMenu"); this.registerColumnOption("clickMenu"); this.registerColumnOption("dblClickMenu"); - this.registerColumnOption("rangeRowContextMenu"); - this.registerColumnOption("rangeColumnContextMenu"); + } initialize(){ @@ -81,7 +80,7 @@ class Menu extends Module{ } initializeRangeWatchers() { - if(this.table.options.rangeContextMenu) { + if (this.table.options.rangeContextMenu) { this.subscribe("range-contextmenu", this.loadMenuEvent.bind(this, this.table.options.rangeContextMenu)); this.table.on("rangeTapHold", this.loadMenuEvent.bind(this, this.table.options.rangeContextMenu)); } @@ -127,17 +126,6 @@ class Menu extends Module{ this.columnSubscribers.dblClickMenu = this.loadMenuTableCellEvent.bind(this, "dblClickMenu"); this.subscribe("cell-dblclick", this.columnSubscribers.dblClickMenu); } - - //handle range events - if(def.rangeColumnContextMenu) { - this.subscribe("range-col-contextmenu", this.loadMenuEvent.bind(this, def.rangeColumnContextMenu)); - this.table.on("rangeColTapHold", this.loadMenuEvent.bind(this, def.rangeColumnContextMenu)); - } - - if(def.rangeRowContextMenu) { - this.subscribe("range-row-contextmenu", this.loadMenuEvent.bind(this, def.rangeRowContextMenu)); - this.table.on("rangeRowTapHold", this.loadMenuEvent.bind(this, def.rangeRowContextMenu)); - } } initializeColumnHeaderMenu(column){ diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index b351e82f9..52615259f 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -12,6 +12,12 @@ class Spreadsheet extends Module { this.registerTableOption("spreadsheet", false); //enable spreadsheet this.registerTableOption("spreadsheetRowHeader", {}); //row header definition this.registerTableOption("rowHeaderField", "--row-header"); //field name for row header + + this.registerColumnOption("__spreadsheet_editable"); + + this.registerTableFunction("findRangeByCell", this.findRangeByCell.bind(this)); + this.registerTableFunction("findRangeByRow", this.findRangeByRow.bind(this)); + this.registerTableFunction("findRangeByColumn", this.findRangeByColumn.bind(this)); } initialize() { @@ -163,8 +169,6 @@ class Spreadsheet extends Module { el.classList.toggle("tabulator-range", rangeIdx !== -1); - el.classList.toggle("tabulator-range-cell", rangeIdx !== -1); - el.classList.toggle( "tabulator-range-single-cell", this.ranges.length === 1 && @@ -374,7 +378,6 @@ class Spreadsheet extends Module { var highlight = this.ranges.some((range) => range.occupiesRow(row)); el.classList.toggle("tabulator-range", selected); - el.classList.toggle("tabulator-range-row", selected); el.classList.toggle("tabulator-row-selected", selected); el.classList.toggle("tabulator-row-highlight", highlight); @@ -406,7 +409,6 @@ class Spreadsheet extends Module { var highlight = this.ranges.some((range) => range.occupiesColumn(column)); el.classList.toggle("tabulator-range", selected); - el.classList.toggle("tabulator-range-col", selected); el.classList.toggle("tabulator-col-selected", selected); el.classList.toggle("tabulator-col-highlight", highlight); @@ -469,7 +471,19 @@ class Spreadsheet extends Module { this.layoutElement(); } - findRange(cell) { + findRangeByColumn(column) { + return this.ranges.find((range) => range.occupiesColumn(column._column)); + } + + findRangeByRow(row) { + return this.ranges.find((range) => range.occupiesRow(row._row)); + } + + findRangeByCell(cell) { + return this.ranges.find((range) => range.occupies(cell._cell)); + } + + findRangeByCellElement(cell) { var rangeIdx = cell.dataset.range; if (rangeIdx < 0) return; return this.ranges[rangeIdx].getComponent(); From ab2883d5bc4521415c6dcf03c357af89bf393f80 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 16 Oct 2023 09:11:34 +0700 Subject: [PATCH 12/48] refactors --- src/js/core/tools/InteractionMonitor.js | 21 +-------- src/js/modules/Menu/Menu.js | 8 ---- src/js/modules/Popup/Popup.js | 8 ---- src/js/modules/Spreadsheet/Spreadsheet.js | 55 +++++++++++------------ 4 files changed, 27 insertions(+), 65 deletions(-) diff --git a/src/js/core/tools/InteractionMonitor.js b/src/js/core/tools/InteractionMonitor.js index 38c4e4353..6ecbd05cd 100644 --- a/src/js/core/tools/InteractionMonitor.js +++ b/src/js/core/tools/InteractionMonitor.js @@ -32,7 +32,6 @@ export default class InteractionManager extends CoreFeature { "tabulator-row":"row", "tabulator-group":"group", "tabulator-col":"column", - "tabulator-range":"range", }; this.pseudoTrackers = { @@ -52,10 +51,6 @@ export default class InteractionManager extends CoreFeature { subscriber:null, target:null, }, - "range":{ - subscriber:null, - target:null, - }, }; this.pseudoTracking = false; @@ -231,20 +226,11 @@ export default class InteractionManager extends CoreFeature { } } } - + if(targets.group && targets.group === targets.row){ delete targets.row; } - if(targets.range && targets.range === targets.column) { - delete targets.range; - } - - if(targets.range) { - if (targets.range === targets.row) delete targets.range; - else delete targets.row; - } - return targets; } @@ -296,11 +282,6 @@ export default class InteractionManager extends CoreFeature { } } break; - case "range": - if(listener.components.includes("range")){ - component = this.table.modules.spreadsheet.findRangeByCellElement(target); - } - break; } } diff --git a/src/js/modules/Menu/Menu.js b/src/js/modules/Menu/Menu.js index 7b49922a6..ebdcd6cb9 100644 --- a/src/js/modules/Menu/Menu.js +++ b/src/js/modules/Menu/Menu.js @@ -38,7 +38,6 @@ class Menu extends Module{ this.deprecatedOptionsCheck(); this.initializeRowWatchers(); this.initializeGroupWatchers(); - this.initializeRangeWatchers(); this.subscribe("column-init", this.initializeColumn.bind(this)); } @@ -79,13 +78,6 @@ class Menu extends Module{ } } - initializeRangeWatchers() { - if (this.table.options.rangeContextMenu) { - this.subscribe("range-contextmenu", this.loadMenuEvent.bind(this, this.table.options.rangeContextMenu)); - this.table.on("rangeTapHold", this.loadMenuEvent.bind(this, this.table.options.rangeContextMenu)); - } - } - initializeColumn(column){ var def = column.definition; diff --git a/src/js/modules/Popup/Popup.js b/src/js/modules/Popup/Popup.js index 0ffff333f..ee3d5ff96 100644 --- a/src/js/modules/Popup/Popup.js +++ b/src/js/modules/Popup/Popup.js @@ -35,7 +35,6 @@ class Popup extends Module{ initialize(){ this.initializeRowWatchers(); this.initializeGroupWatchers(); - this.initializeRangeWatchers(); this.subscribe("column-init", this.initializeColumn.bind(this)); } @@ -74,13 +73,6 @@ class Popup extends Module{ } } - initializeRangeWatchers(){ - if(this.table.options.rangeContextPopup){ - this.subscribe("range-contextmenu", this.loadPopupEvent.bind(this, this.table.options.rangeContextPopup)); - this.table.on("rangeTapHold", this.loadPopupEvent.bind(this, this.table.options.rangeContextPopup)); - } - } - initializeColumn(column){ var def = column.definition; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 52615259f..1160fd430 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -8,16 +8,16 @@ class Spreadsheet extends Module { this.prevSelection = false; this.selecting = false; this.ranges = []; + this.rowHeaderField = "--row-header"; this.registerTableOption("spreadsheet", false); //enable spreadsheet - this.registerTableOption("spreadsheetRowHeader", {}); //row header definition - this.registerTableOption("rowHeaderField", "--row-header"); //field name for row header - - this.registerColumnOption("__spreadsheet_editable"); + this.registerTableOption("rowHeader", {}); //row header definition this.registerTableFunction("findRangeByCell", this.findRangeByCell.bind(this)); this.registerTableFunction("findRangeByRow", this.findRangeByRow.bind(this)); this.registerTableFunction("findRangeByColumn", this.findRangeByColumn.bind(this)); + + this.registerColumnOption("__spreadsheet_editable"); } initialize() { @@ -58,19 +58,23 @@ class Spreadsheet extends Module { column.editable = false; } + var rowHeaderDef = { + title: "", + field: this.rowHeaderField, + headerSort: false, + resizable: false, + frozen: true, + editable: false, + cssClass: "tabulator-row-header", + formatter: "rownum", + formatterParams: { relativeToPage: true }, + ...this.options("rowHeader"), + }; + + this.rowHeaderField = rowHeaderDef.field; + this.table.options.columns = [ - { - title: "", - field: this.options("rowHeaderField"), - headerSort: false, - resizable: false, - frozen: true, - editable: false, - cssClass: "tabulator-row-header", - formatter: "rownum", - formatterParams: { relativeToPage: true }, - ...this.table.options.spreadsheetRowHeader, - }, + rowHeaderDef, ...this.table.options.columns, ]; @@ -188,7 +192,7 @@ class Spreadsheet extends Module { return; } - if (column.field === this.options("rowHeaderField")) { + if (column.field === this.rowHeaderField) { this.resetRanges(); this.selecting = "all"; @@ -205,7 +209,7 @@ class Spreadsheet extends Module { } handleColumnMouseMove(_, column) { - if (column.field === this.options("rowHeaderField")) return; + if (column.field === this.rowHeaderField) return; if (!this.selecting || this.selecting === "all") return; this.endSelection(column); @@ -226,17 +230,10 @@ class Spreadsheet extends Module { this.layoutElement(); } - handleCellContextMenu(event, cell) { - var range = this.getActiveRange(); - if (event.button === 2 && range.occupies(cell)) { - this.dispatchExternal("rangeContextMenu", event, range, cell); - } - } - _select(event, element) { if (element.type === "column") { this.selecting = "column"; - } else if (element.column.field === this.options("rowHeaderField")) { + } else if (element.column.field === this.rowHeaderField) { this.selecting = "row"; } else { this.selecting = "cell"; @@ -276,7 +273,7 @@ class Spreadsheet extends Module { } handleCellDblClick(_, cell) { - if (cell.column.field === this.options("rowHeaderField")) return; + if (cell.column.field === this.rowHeaderField) return; cell.column.definition.editable = cell.column.definition.__spreadsheet_editable; @@ -301,7 +298,7 @@ class Spreadsheet extends Module { var row = element.row.position - 1; var col = element.column.position - 2; - if (element.column.field === this.options("rowHeaderField")) { + if (element.column.field === this.rowHeaderField) { range.setStart(row, 0); } else { range.setStart(row, col); @@ -323,7 +320,7 @@ class Spreadsheet extends Module { var row = element.row.position - 1; var col = element.column.position - 2; - var isRowHeader = element.column.field === this.options("rowHeaderField"); + var isRowHeader = element.column.field === this.rowHeaderField); if (this.selecting === "row") { range.setEnd( From 4c18d5e004e1921b31f1a8c511bb435322af1494 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 16 Oct 2023 09:26:25 +0700 Subject: [PATCH 13/48] expose getActiveRange --- src/js/modules/Spreadsheet/Spreadsheet.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 1160fd430..03e4e18fe 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -16,6 +16,7 @@ class Spreadsheet extends Module { this.registerTableFunction("findRangeByCell", this.findRangeByCell.bind(this)); this.registerTableFunction("findRangeByRow", this.findRangeByRow.bind(this)); this.registerTableFunction("findRangeByColumn", this.findRangeByColumn.bind(this)); + this.registerTableFunction("getActiveRange", this.getActiveRange.bind(this, true)); this.registerColumnOption("__spreadsheet_editable"); } @@ -320,7 +321,7 @@ class Spreadsheet extends Module { var row = element.row.position - 1; var col = element.column.position - 2; - var isRowHeader = element.column.field === this.rowHeaderField); + var isRowHeader = element.column.field === this.rowHeaderField; if (this.selecting === "row") { range.setEnd( @@ -505,8 +506,10 @@ class Spreadsheet extends Module { return row.getCells().filter((cell) => cell.column.visible)[colIdx + 1]; } - getActiveRange() { - return this.ranges[this.ranges.length - 1]; + getActiveRange(component) { + const range = this.ranges[this.ranges.length - 1]; + if (component) return range?.getComponent(); + return range } getActiveCell() { From 8d84b48e95b264dab7c4f1e471cb12b2002e82ae Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 16 Oct 2023 17:34:11 +0700 Subject: [PATCH 14/48] refactors, fix virtual dom vertical --- .../renderers/VirtualDomHorizontal.js | 4 + .../rendering/renderers/VirtualDomVertical.js | 16 ++ src/js/core/tools/Helpers.js | 16 +- src/js/modules/Spreadsheet/Range.js | 20 +-- src/js/modules/Spreadsheet/Spreadsheet.js | 147 +++++++++--------- src/scss/tabulator.scss | 2 +- 6 files changed, 119 insertions(+), 86 deletions(-) diff --git a/src/js/core/rendering/renderers/VirtualDomHorizontal.js b/src/js/core/rendering/renderers/VirtualDomHorizontal.js index 8bcdf254b..d23ff7d9c 100644 --- a/src/js/core/rendering/renderers/VirtualDomHorizontal.js +++ b/src/js/core/rendering/renderers/VirtualDomHorizontal.js @@ -370,6 +370,7 @@ export default class VirtualDomHorizontal extends Renderer{ if(changes){ this.tableElement.style.paddingRight = this.vDomPadRight + "px"; + this.dispatch("render-virtual-dom"); } } @@ -423,6 +424,7 @@ export default class VirtualDomHorizontal extends Renderer{ if(changes){ this.tableElement.style.paddingLeft = this.vDomPadLeft + "px"; + this.dispatch("render-virtual-dom"); } } @@ -467,6 +469,7 @@ export default class VirtualDomHorizontal extends Renderer{ if(changes){ this.tableElement.style.paddingRight = this.vDomPadRight + "px"; + this.dispatch("render-virtual-dom"); } } @@ -511,6 +514,7 @@ export default class VirtualDomHorizontal extends Renderer{ if(changes){ this.tableElement.style.paddingLeft = this.vDomPadLeft + "px"; + this.dispatch("render-virtual-dom"); } } diff --git a/src/js/core/rendering/renderers/VirtualDomVertical.js b/src/js/core/rendering/renderers/VirtualDomVertical.js index c4b775b2f..3cd4720df 100644 --- a/src/js/core/rendering/renderers/VirtualDomVertical.js +++ b/src/js/core/rendering/renderers/VirtualDomVertical.js @@ -463,6 +463,10 @@ export default class VirtualDomVertical extends Renderer{ table.style.paddingTop = this.vDomTopPad + "px"; this.vDomScrollPosTop -= paddingAdjust; } + + if (addedRows.length) { + this.dispatch("render-virtual-dom") + } } _removeTopRow(rows, fillableSpace){ @@ -507,6 +511,10 @@ export default class VirtualDomVertical extends Renderer{ this.tableElement.style.paddingTop = this.vDomTopPad + "px"; this.vDomScrollPosTop += this.vDomTop ? paddingAdjust : paddingAdjust + this.vDomWindowBuffer; } + + if (removableRows.length) { + this.dispatch("render-virtual-dom") + } } _addBottomRow(rows, fillableSpace){ @@ -574,6 +582,10 @@ export default class VirtualDomVertical extends Renderer{ table.style.paddingBottom = this.vDomBottomPad + "px"; this.vDomScrollPosBottom += paddingAdjust; } + + if (addedRows.length) { + this.dispatch("render-virtual-dom") + } } _removeBottomRow(rows, fillableSpace){ @@ -623,6 +635,10 @@ export default class VirtualDomVertical extends Renderer{ this.tableElement.style.paddingBottom = this.vDomBottomPad + "px"; this.vDomScrollPosBottom -= paddingAdjust; } + + if (removableRows.length) { + this.dispatch("render-virtual-dom") + } } _quickNormalizeRowHeight(rows){ diff --git a/src/js/core/tools/Helpers.js b/src/js/core/tools/Helpers.js index 1551602d1..ce368f1bf 100644 --- a/src/js/core/tools/Helpers.js +++ b/src/js/core/tools/Helpers.js @@ -44,4 +44,18 @@ export default class Helpers{ return clone; } -} \ No newline at end of file + + static debounce(func, delay = 0) { + var timeoutId; + + return function (...args) { + const context = this; + + clearTimeout(timeoutId); + + timeoutId = setTimeout(() => { + func.apply(context, args); + }, delay); + }; + } +} diff --git a/src/js/modules/Spreadsheet/Range.js b/src/js/modules/Spreadsheet/Range.js index 6998eb54a..9b346703b 100644 --- a/src/js/modules/Spreadsheet/Range.js +++ b/src/js/modules/Spreadsheet/Range.js @@ -22,23 +22,23 @@ class Range { } _updateMinMax() { - this.minRow = Math.min(this.start.row, this.end.row); - this.maxRow = Math.max(this.start.row, this.end.row); - this.minCol = Math.min(this.start.col, this.end.col); - this.maxCol = Math.max(this.start.col, this.end.col); + this.top = Math.min(this.start.row, this.end.row); + this.bottom = Math.max(this.start.row, this.end.row); + this.left = Math.min(this.start.col, this.end.col); + this.right = Math.max(this.start.col, this.end.col); } atTopLeft(cell) { return ( - cell.row.position - 1 === this.minRow && - cell.column.position - 2 === this.minCol + cell.row.position - 1 === this.top && + cell.column.position - 2 === this.left ); } atBottomRight(cell) { return ( - cell.row.position - 1 === this.maxRow && - cell.column.position - 2 === this.maxCol + cell.row.position - 1 === this.bottom && + cell.column.position - 2 === this.right ); } @@ -47,11 +47,11 @@ class Range { } occupiesRow(row) { - return this.minRow <= row.position - 1 && row.position - 1 <= this.maxRow; + return this.top <= row.position - 1 && row.position - 1 <= this.bottom; } occupiesColumn(col) { - return this.minCol <= col.position - 2 && col.position - 2 <= this.maxCol; + return this.left <= col.position - 2 && col.position - 2 <= this.right; } getData() { diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 03e4e18fe..4f68083c4 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -1,21 +1,23 @@ import Module from "../../core/Module.js"; +import Helpers from "../../core/tools/Helpers.js"; import Range from "./Range.js"; class Spreadsheet extends Module { constructor(table) { super(table); - this.prevSelection = false; - this.selecting = false; + this.selecting = "cell"; + this.mousedown = false; this.ranges = []; this.rowHeaderField = "--row-header"; + this.overlay = null; this.registerTableOption("spreadsheet", false); //enable spreadsheet this.registerTableOption("rowHeader", {}); //row header definition - this.registerTableFunction("findRangeByCell", this.findRangeByCell.bind(this)); - this.registerTableFunction("findRangeByRow", this.findRangeByRow.bind(this)); - this.registerTableFunction("findRangeByColumn", this.findRangeByColumn.bind(this)); + this.registerTableFunction("findRangeFromCell", this.findRangeFromCell.bind(this)); + this.registerTableFunction("findRangeFromRow", this.findRangeFromRow.bind(this)); + this.registerTableFunction("findRangeFromColumn", this.findRangeFromColumn.bind(this)); this.registerTableFunction("getActiveRange", this.getActiveRange.bind(this, true)); this.registerColumnOption("__spreadsheet_editable"); @@ -46,6 +48,13 @@ class Spreadsheet extends Module { this.subscribe("edit-cancelled", this.unbindEditable.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); + + var debouncedLayoutElement = Helpers.debounce(this.layoutElement.bind(this), 200); + + this.subscribe("render-virtual-dom", () => { + this.overlay.style.display = 'none'; + debouncedLayoutElement(); + }) } initializeTable() { @@ -97,6 +106,7 @@ class Spreadsheet extends Module { for (const entry of entries) { this.overlay.style.width = entry.contentRect.width + "px"; this.overlay.style.height = entry.contentRect.height + "px"; + this.overlay.style.padding = entry.target.style.padding; } }); @@ -172,10 +182,10 @@ class Spreadsheet extends Module { var rangeIdx = this.ranges.findIndex((range) => range.occupies(cell)); - el.classList.toggle("tabulator-range", rangeIdx !== -1); + el.classList.toggle("tabulator-cell-selected", rangeIdx !== -1); el.classList.toggle( - "tabulator-range-single-cell", + "tabulator-only-cell-selected", this.ranges.length === 1 && this.ranges[0].atTopLeft(cell) && this.ranges[0].atBottomRight(cell), @@ -187,31 +197,22 @@ class Spreadsheet extends Module { handleColumnMouseDown(event, column) { if ( event.button === 2 && - (this.prevSelection === "column" || this.prevSelection === "all") && + (this.selecting === "column" || this.selecting === "all") && this.getActiveRange().occupiesColumn(column) ) { return; } - if (column.field === this.rowHeaderField) { - this.resetRanges(); - this.selecting = "all"; - - const topLeftCell = this.getCell(0, 0); - const bottomRightCell = this.getCell(-1, -1); - - this.beginSelection(topLeftCell); - this.endSelection(bottomRightCell); - } else { - this._select(event, column); - } + this.mousedown = true; + this._select(event, column); this.layoutElement(); } handleColumnMouseMove(_, column) { if (column.field === this.rowHeaderField) return; - if (!this.selecting || this.selecting === "all") return; + if (!this.mousedown) return; + if (this.selecting === 'all') return; this.endSelection(column); this.layoutElement(); @@ -221,19 +222,34 @@ class Spreadsheet extends Module { if ( event.button === 2 && (this.getActiveRange().occupies(cell) || - ((this.prevSelection === "row" || this.prevSelection === "all") && + ((this.selecting === "row" || this.selecting === "all") && this.getActiveRange().occupiesRow(cell.row))) ) { return; } + this.mousedown = true; + this._select(event, cell); this.layoutElement(); } _select(event, element) { if (element.type === "column") { - this.selecting = "column"; + if (element.field === this.rowHeaderField) { + this.resetRanges(); + this.selecting = "all"; + + const topLeftCell = this.getCell(0, 0); + const bottomRightCell = this.getCell(-1, -1); + + this.beginSelection(topLeftCell); + this.endSelection(bottomRightCell); + + return; + } else { + this.selecting = "column"; + } } else if (element.column.field === this.rowHeaderField) { this.selecting = "row"; } else { @@ -260,17 +276,15 @@ class Spreadsheet extends Module { } handleCellMouseMove(_, cell) { - if (!this.selecting || this.selecting === "all") return; + if (!this.mousedown) return; + if (this.selecting === "all") return; this.endSelection(cell); this.layoutElement(); } handleMouseUp() { - if (this.selecting !== false) { - this.prevSelection = this.selecting; - } - this.selecting = false; + this.mousedown = false; } handleCellDblClick(_, cell) { @@ -355,62 +369,35 @@ class Spreadsheet extends Module { layoutRow(row) { var el = row.getElement(); - el.classList.add("tabulator-spreadsheet"); - - var colCount = this.table.columnManager.getVisibleColumnsByIndex().length; - - var rangeIdx = -1; + var selected = false; + var occupied = this.ranges.some((range) => range.occupiesRow(row)); if (this.selecting === "row") { - rangeIdx = this.ranges.findIndex( - (range) => - range.start.col === 0 && - range.end.col === colCount - 2 && - range.occupiesRow(row), - ); + selected = occupied; } else if (this.selecting === "all") { - rangeIdx = this.ranges.length - 1; + selected = true; } - var selected = rangeIdx !== -1; - var highlight = this.ranges.some((range) => range.occupiesRow(row)); - - el.classList.toggle("tabulator-range", selected); + el.classList.add("tabulator-spreadsheet"); el.classList.toggle("tabulator-row-selected", selected); - el.classList.toggle("tabulator-row-highlight", highlight); - - el.dataset.range = rangeIdx; + el.classList.toggle("tabulator-row-highlight", occupied); } layoutColumn(column) { var el = column.getElement(); - el.classList.add("tabulator-spreadsheet"); - - var rowCount = this.table.rowManager.getDisplayRows().length; - - var rangeIdx = -1; + var selected = false; + var occupied = this.ranges.some((range) => range.occupiesColumn(column)); if (this.selecting === "column") { - rangeIdx = this.ranges.findIndex( - (range) => - range.start.row === 0 && - range.end.row === rowCount - 1 && - range.occupiesColumn(column), - ); + selected = occupied; } else if (this.selecting === "all") { - rangeIdx = this.ranges.length - 1; + selected = true; } - var selected = rangeIdx !== -1; - - var highlight = this.ranges.some((range) => range.occupiesColumn(column)); - - el.classList.toggle("tabulator-range", selected); + el.classList.add("tabulator-spreadsheet"); el.classList.toggle("tabulator-col-selected", selected); - el.classList.toggle("tabulator-col-highlight", highlight); - - el.dataset.range = rangeIdx; + el.classList.toggle("tabulator-col-highlight", occupied); } handleColumnWidth() { @@ -419,6 +406,8 @@ class Spreadsheet extends Module { } layoutSelection() { + this.overlay.style.display = null; + var activeCell = this.getActiveCell(); if (!activeCell) return; @@ -442,8 +431,18 @@ class Spreadsheet extends Module { } layoutRange(range) { - var topLeftCell = this.getCell(range.minRow, range.minCol); - var bottomRightCell = this.getCell(range.maxRow, range.maxCol); + var _vDomTop = this.table.rowManager.renderer.vDomTop ?? 0; + var _vDomBottom = this.table.rowManager.renderer.vDomBottom ?? Infinity; + var _vDomLeft = this.table.columnManager.renderer.leftCol ?? 0; + var _vDomRight = this.table.columnManager.renderer.rightCol ?? Infinity; + + var top = range.top < _vDomTop ? _vDomTop : range.top; + var bottom = range.bottom > _vDomBottom ? _vDomBottom : range.bottom; + var left = range.left < _vDomLeft ? _vDomLeft : range.left; + var right = range.right > _vDomRight ? _vDomRight : range.right; + + var topLeftCell = this.getCell(top, left); + var bottomRightCell = this.getCell(bottom, right); range.element.classList.toggle( "tabulator-range-active", @@ -469,15 +468,15 @@ class Spreadsheet extends Module { this.layoutElement(); } - findRangeByColumn(column) { + findRangeFromColumn(column) { return this.ranges.find((range) => range.occupiesColumn(column._column)); } - findRangeByRow(row) { + findRangeFromRow(row) { return this.ranges.find((range) => range.occupiesRow(row._row)); } - findRangeByCell(cell) { + findRangeFromCell(cell) { return this.ranges.find((range) => range.occupies(cell._cell)); } @@ -520,13 +519,13 @@ class Spreadsheet extends Module { getRowsByRange(range) { return this.table.rowManager .getDisplayRows() - .slice(range.minRow, range.maxRow + 1); + .slice(range.top, range.bottom + 1); } getColumnsByRange(range) { return this.table.columnManager .getVisibleColumnsByIndex() - .slice(range.minCol + 1, range.maxCol + 2); + .slice(range.left + 1, range.right + 2); } addRange() { diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index e6cf2d0a2..c446e1ec5 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -1538,7 +1538,7 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ user-select: none; position: relative; - &.tabulator-range:not(.tabulator-range-single-cell):not(.tabulator-row-header){ + &.tabulator-cell-selected:not(.tabulator-only-cell-selected):not(.tabulator-row-header){ background-color: $cellSelectedBackground; } From 6ea2c966241f7ec49ef4623ad0793f918bc27e40 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 17 Oct 2023 11:33:17 +0700 Subject: [PATCH 15/48] Fix virtual dom horizontal and overlay sizing, optimize row rendering --- .../rendering/renderers/VirtualDomVertical.js | 8 +-- src/js/modules/Spreadsheet/Range.js | 6 ++ src/js/modules/Spreadsheet/Spreadsheet.js | 71 +++++++++++-------- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/js/core/rendering/renderers/VirtualDomVertical.js b/src/js/core/rendering/renderers/VirtualDomVertical.js index 3cd4720df..d5ea689e6 100644 --- a/src/js/core/rendering/renderers/VirtualDomVertical.js +++ b/src/js/core/rendering/renderers/VirtualDomVertical.js @@ -465,7 +465,7 @@ export default class VirtualDomVertical extends Renderer{ } if (addedRows.length) { - this.dispatch("render-virtual-dom") + this.dispatch("render-virtual-dom"); } } @@ -513,7 +513,7 @@ export default class VirtualDomVertical extends Renderer{ } if (removableRows.length) { - this.dispatch("render-virtual-dom") + this.dispatch("render-virtual-dom"); } } @@ -584,7 +584,7 @@ export default class VirtualDomVertical extends Renderer{ } if (addedRows.length) { - this.dispatch("render-virtual-dom") + this.dispatch("render-virtual-dom"); } } @@ -637,7 +637,7 @@ export default class VirtualDomVertical extends Renderer{ } if (removableRows.length) { - this.dispatch("render-virtual-dom") + this.dispatch("render-virtual-dom"); } } diff --git a/src/js/modules/Spreadsheet/Range.js b/src/js/modules/Spreadsheet/Range.js index 9b346703b..251ea90a6 100644 --- a/src/js/modules/Spreadsheet/Range.js +++ b/src/js/modules/Spreadsheet/Range.js @@ -54,6 +54,12 @@ class Range { return this.left <= col.position - 2 && col.position - 2 <= this.right; } + overlaps(left, top, right, bottom) { + if (this.left > right || left > this.right) return false; + if (this.top > bottom || top > this.bottom) return false; + return true; + } + getData() { return this.table.modules.spreadsheet.getDataByRange(this); } diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 4f68083c4..881e37519 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -49,12 +49,9 @@ class Spreadsheet extends Module { this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); - var debouncedLayoutElement = Helpers.debounce(this.layoutElement.bind(this), 200); - - this.subscribe("render-virtual-dom", () => { - this.overlay.style.display = 'none'; - debouncedLayoutElement(); - }) + // Wait after dom rendering to finish + var debouncedLayoutSelection = Helpers.debounce(this.layoutSelection.bind(this), 50); + this.subscribe("render-virtual-dom", debouncedLayoutSelection); } initializeTable() { @@ -101,12 +98,13 @@ class Spreadsheet extends Module { this.overlay.appendChild(this.rangeContainer); this.overlay.appendChild(this.activeRangeCellElement); - + var resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { - this.overlay.style.width = entry.contentRect.width + "px"; - this.overlay.style.height = entry.contentRect.height + "px"; - this.overlay.style.padding = entry.target.style.padding; + this.overlay.style.left = entry.target.offsetLeft + "px"; + this.overlay.style.top = entry.target.offsetTop + "px"; + this.overlay.style.width = entry.target.offsetWidth + "px"; + this.overlay.style.height = entry.target.offsetHeight + "px"; } }); @@ -215,7 +213,7 @@ class Spreadsheet extends Module { if (this.selecting === 'all') return; this.endSelection(column); - this.layoutElement(); + this.layoutElement(true); } handleCellMouseDown(event, cell) { @@ -280,7 +278,7 @@ class Spreadsheet extends Module { if (this.selecting === "all") return; this.endSelection(cell); - this.layoutElement(); + this.layoutElement(true); } handleMouseUp() { @@ -351,8 +349,12 @@ class Spreadsheet extends Module { } } - layoutElement() { - this.table.rowManager.getVisibleRows(true).forEach((row) => { + layoutElement(visibleRows) { + var rows = visibleRows + ? this.table.rowManager.getVisibleRows(true) + : this.table.rowManager.getRows() + + rows.forEach((row) => { if (row.type === "row") { this.layoutRow(row); row.cells.forEach((cell) => this.renderCell(cell)); @@ -406,14 +408,14 @@ class Spreadsheet extends Module { } layoutSelection() { - this.overlay.style.display = null; - var activeCell = this.getActiveCell(); if (!activeCell) return; this.activeRangeCellElement.style.left = - activeCell.getElement().offsetLeft + "px"; + activeCell.row.getElement().offsetLeft + + activeCell.getElement().offsetLeft + + "px"; this.activeRangeCellElement.style.top = activeCell.row.getElement().offsetTop + "px"; this.activeRangeCellElement.style.width = @@ -431,15 +433,22 @@ class Spreadsheet extends Module { } layoutRange(range) { - var _vDomTop = this.table.rowManager.renderer.vDomTop ?? 0; - var _vDomBottom = this.table.rowManager.renderer.vDomBottom ?? Infinity; - var _vDomLeft = this.table.columnManager.renderer.leftCol ?? 0; - var _vDomRight = this.table.columnManager.renderer.rightCol ?? Infinity; - - var top = range.top < _vDomTop ? _vDomTop : range.top; - var bottom = range.bottom > _vDomBottom ? _vDomBottom : range.bottom; - var left = range.left < _vDomLeft ? _vDomLeft : range.left; - var right = range.right > _vDomRight ? _vDomRight : range.right; + var _vDomTop = this.table.rowManager.renderer.vDomTop; + var _vDomBottom = this.table.rowManager.renderer.vDomBottom; + var _vDomLeft = this.table.columnManager.renderer.leftCol; + var _vDomRight = this.table.columnManager.renderer.rightCol; + + if (_vDomTop == null) _vDomTop = 0; + if (_vDomBottom == null) _vDomBottom = Infinity; + if (_vDomLeft == null) _vDomLeft = 0; + if (_vDomRight == null) _vDomRight = Infinity; + + if (!range.overlaps(_vDomLeft, _vDomTop, _vDomRight, _vDomBottom)) return; + + var top = Math.max(range.top, _vDomTop); + var bottom = Math.min(range.bottom, _vDomBottom); + var left = Math.max(range.left, _vDomLeft); + var right = Math.min(range.right, _vDomRight); var topLeftCell = this.getCell(top, left); var bottomRightCell = this.getCell(bottom, right); @@ -449,7 +458,10 @@ class Spreadsheet extends Module { range === this.getActiveRange(), ); - range.element.style.left = topLeftCell.getElement().offsetLeft + "px"; + range.element.style.left = + topLeftCell.row.getElement().offsetLeft + + topLeftCell.getElement().offsetLeft + + "px"; range.element.style.top = topLeftCell.row.getElement().offsetTop + "px"; range.element.style.width = bottomRightCell.getElement().offsetLeft + @@ -507,8 +519,9 @@ class Spreadsheet extends Module { getActiveRange(component) { const range = this.ranges[this.ranges.length - 1]; - if (component) return range?.getComponent(); - return range + if (!range) return null; + if (component) return range.getComponent(); + return range; } getActiveCell() { From b4498dc24f0062f8e6439ec60b9f81e0c50a0763 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 17 Oct 2023 12:26:57 +0700 Subject: [PATCH 16/48] cleanups --- src/js/modules/Menu/Menu.js | 3 --- src/js/modules/Popup/Popup.js | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/js/modules/Menu/Menu.js b/src/js/modules/Menu/Menu.js index ebdcd6cb9..2081ab327 100644 --- a/src/js/modules/Menu/Menu.js +++ b/src/js/modules/Menu/Menu.js @@ -21,7 +21,6 @@ class Menu extends Module{ this.registerTableOption("groupContextMenu", false); this.registerTableOption("groupClickMenu", false); this.registerTableOption("groupDblClickMenu", false); - this.registerTableOption("rangeContextMenu", false); this.registerColumnOption("headerContextMenu"); this.registerColumnOption("headerClickMenu"); @@ -176,8 +175,6 @@ class Menu extends Module{ component = component._group; }else if(component._row){ component = component._row; - }else if(component._range) { - component = component._range; } menu = typeof menu == "function" ? menu.call(this.table, e, component.getComponent()) : menu; diff --git a/src/js/modules/Popup/Popup.js b/src/js/modules/Popup/Popup.js index ee3d5ff96..8dbbebb24 100644 --- a/src/js/modules/Popup/Popup.js +++ b/src/js/modules/Popup/Popup.js @@ -13,7 +13,6 @@ class Popup extends Module{ this.registerTableOption("groupContextPopup", false); this.registerTableOption("groupClickPopup", false); this.registerTableOption("groupDblClickPopup", false); - this.registerTableOption("rangeContextPopup", false); this.registerColumnOption("headerContextPopup"); this.registerColumnOption("headerClickPopup"); @@ -28,7 +27,6 @@ class Popup extends Module{ this.registerComponentFunction("column", "popup", this._componentPopupCall.bind(this)); this.registerComponentFunction("row", "popup", this._componentPopupCall.bind(this)); this.registerComponentFunction("group", "popup", this._componentPopupCall.bind(this)); - this.registerComponentFunction("range", "popup", this._componentPopupCall.bind(this)); } From 2c3ee04e759a8b1e6fda0f5c238de54255c9cc07 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Wed, 18 Oct 2023 19:48:15 +0700 Subject: [PATCH 17/48] simplify css classes, fix unfocusable table --- src/js/modules/Spreadsheet/Spreadsheet.js | 46 ++++++++++++-------- src/scss/tabulator.scss | 51 +++++++++++------------ src/scss/themes/tabulator_midnight.scss | 1 - src/scss/themes/tabulator_simple.scss | 3 -- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 881e37519..598ff1971 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -21,6 +21,7 @@ class Spreadsheet extends Module { this.registerTableFunction("getActiveRange", this.getActiveRange.bind(this, true)); this.registerColumnOption("__spreadsheet_editable"); + this.registerColumnOption("__spreadsheet_editor"); } initialize() { @@ -44,8 +45,8 @@ class Spreadsheet extends Module { this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); this.subscribe("cell-rendered", this.renderCell.bind(this)); - this.subscribe("edit-success", this.unbindEditable.bind(this)); - this.subscribe("edit-cancelled", this.unbindEditable.bind(this)); + this.subscribe("edit-success", this.finishEditingCell.bind(this)); + this.subscribe("edit-cancelled", this.finishEditingCell.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); @@ -59,10 +60,16 @@ class Spreadsheet extends Module { // Disable sorting by clicking header column.headerSort = false; + // FIXME: tableholder is not focusable if we have a column that's + // `editable: false` and `editor: 'input' or something`. + // Edit on double click var editable = column.editable !== undefined ? column.editable : true; column.__spreadsheet_editable = editable; column.editable = false; + + column.__spreadsheet_editor = column.editor; + column.editor = false; } var rowHeaderDef = { @@ -176,11 +183,9 @@ class Spreadsheet extends Module { renderCell(cell) { var el = cell.getElement(); - el.classList.add("tabulator-spreadsheet"); - var rangeIdx = this.ranges.findIndex((range) => range.occupies(cell)); - el.classList.toggle("tabulator-cell-selected", rangeIdx !== -1); + el.classList.toggle("tabulator-selected", rangeIdx !== -1); el.classList.toggle( "tabulator-only-cell-selected", @@ -287,17 +292,24 @@ class Spreadsheet extends Module { handleCellDblClick(_, cell) { if (cell.column.field === this.rowHeaderField) return; + this.editCell(cell); + } + editCell(cell) { cell.column.definition.editable = cell.column.definition.__spreadsheet_editable; + cell.column.definition.editor = + cell.column.definition.__spreadsheet_editor; this.table.modules.edit.initializeColumnCheck(cell.column); - cell.getComponent().edit(); + if (this.table.modules.edit.allowEdit(cell)) { + cell.getComponent().edit(); + } } - unbindEditable(cell) { + finishEditingCell(cell) { cell.column.definition.editable = false; - this.table.modules.edit.initializeColumnCheck(cell.column); + cell.column.definition.editor = false; } beginSelection(element) { @@ -352,7 +364,7 @@ class Spreadsheet extends Module { layoutElement(visibleRows) { var rows = visibleRows ? this.table.rowManager.getVisibleRows(true) - : this.table.rowManager.getRows() + : this.table.rowManager.getRows(); rows.forEach((row) => { if (row.type === "row") { @@ -380,9 +392,8 @@ class Spreadsheet extends Module { selected = true; } - el.classList.add("tabulator-spreadsheet"); - el.classList.toggle("tabulator-row-selected", selected); - el.classList.toggle("tabulator-row-highlight", occupied); + el.classList.toggle("tabulator-selected", selected); + el.classList.toggle("tabulator-highlight", occupied); } layoutColumn(column) { @@ -397,9 +408,8 @@ class Spreadsheet extends Module { selected = true; } - el.classList.add("tabulator-spreadsheet"); - el.classList.toggle("tabulator-col-selected", selected); - el.classList.toggle("tabulator-col-highlight", occupied); + el.classList.toggle("tabulator-selected", selected); + el.classList.toggle("tabulator-highlight", occupied); } handleColumnWidth() { @@ -481,15 +491,15 @@ class Spreadsheet extends Module { } findRangeFromColumn(column) { - return this.ranges.find((range) => range.occupiesColumn(column._column)); + return this.ranges.find((range) => range.occupiesColumn(column._column)).getComponent(); } findRangeFromRow(row) { - return this.ranges.find((range) => range.occupiesRow(row._row)); + return this.ranges.find((range) => range.occupiesRow(row._row)).getComponent(); } findRangeFromCell(cell) { - return this.ranges.find((range) => range.occupies(cell._cell)); + return this.ranges.find((range) => range.occupies(cell._cell)).getComponent(); } findRangeByCellElement(cell) { diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index c446e1ec5..ec2f2f4a5 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -10,6 +10,8 @@ $headerTextColor:#555 !default; //header text color $headerBorderColor:#aaa !default; //header border color $headerSeparatorColor:#999 !default; //header bottom separator color $headerMargin:4px !default; //padding round header +$headerSelectedBackground: #4990F1 !default; //header background color when selected +$headerSelectedTextColor: #FFFFFF !default; //header text color when selected //column header arrows $sortArrowHover: #555 !default; @@ -25,13 +27,10 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered -$headerSelectedBackground: #4990F1 !default; //header background color when selected -$headerTextSelectedBackground: #FFFFFF !default; //header text color when selected $cellSelectedBackground: #C2D5EF !default; //cell background color when selected $headerHighlightBackground: #D6D6D6 !default; //header background color when highlighted $headerTextHighlightBackground: #000000 !default; //header text color when highlighted -$headerBorderHighlightColor:#777 !default; //header border color when highlighted $rangeBorderColor: #2975DD !default; //range border color $rangeHandleColor: #2975DD !default; //range handle color @@ -1511,40 +1510,38 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ // Spreadsheet module styling -.tabulator-headers .tabulator-spreadsheet.tabulator-col{ - &.tabulator-col-highlight { +.tabulator-header .tabulator-col{ + &.tabulator-highlight { background-color: $headerHighlightBackground; color: $headerTextHighlightBackground; - border-color: $headerBorderHighlightColor; } - &.tabulator-col-selected{ + &.tabulator-selected{ background-color: $headerSelectedBackground; - color: $headerTextSelectedBackground; + color: $headerSelectedTextColor; } } -.tabulator-table .tabulator-spreadsheet{ - &.tabulator-row{ - &.tabulator-row-highlight .tabulator-row-header{ - background-color: $headerHighlightBackground; - color: $headerTextHighlightBackground; - } - &.tabulator-row-selected .tabulator-row-header{ - background-color: $headerSelectedBackground; - color: $headerTextSelectedBackground; - } +.tabulator-row{ + &.tabulator-highlight .tabulator-row-header{ + background-color: $headerHighlightBackground; + color: $headerTextHighlightBackground; } - &.tabulator-cell{ - user-select: none; - position: relative; + &.tabulator-selected .tabulator-row-header{ + background-color: $headerSelectedBackground; + color: $headerSelectedTextColor; + } +} - &.tabulator-cell-selected:not(.tabulator-only-cell-selected):not(.tabulator-row-header){ - background-color: $cellSelectedBackground; - } +.tabulator-cell{ + user-select: none; + position: relative; - &.tabulator-row-header { - text-align: center; - } + &.tabulator-selected:not(.tabulator-only-cell-selected):not(.tabulator-row-header){ + background-color: $rowSelectedBackground; + } + + &.tabulator-row-header { + text-align: center; } } diff --git a/src/scss/themes/tabulator_midnight.scss b/src/scss/themes/tabulator_midnight.scss index e6c832bac..a013b3c78 100644 --- a/src/scss/themes/tabulator_midnight.scss +++ b/src/scss/themes/tabulator_midnight.scss @@ -24,7 +24,6 @@ $rowHoverBackground:#999 !default; //row background color on hover $rowSelectedBackground: #000 !default; //row background color when selected $rowSelectedBackgroundHover: #888 !default;//row background color when selected and hovered -$cellSelectedBackground: #495B75 !default; //cell background color when selected $headerHighlightBackground: #777 !default; //header background color when highlighted $headerTextHighlightBackground: #fff !default; //header background color when highlighted diff --git a/src/scss/themes/tabulator_simple.scss b/src/scss/themes/tabulator_simple.scss index dec33780d..814fdf63a 100644 --- a/src/scss/themes/tabulator_simple.scss +++ b/src/scss/themes/tabulator_simple.scss @@ -25,9 +25,6 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered -$rowBorderSelectedColor:#aaa !default; //table border color on selected -$headerBorderSelectedColor:#aaa !default; //header border color when selected - $editBoxColor:#1D68CD !default; //border color for edit boxes $errorColor:#dd0000 !default; //error indication From da2b5aec7ff5fb92811738086cecaf84fb65438f Mon Sep 17 00:00:00 2001 From: azmy60 Date: Fri, 20 Oct 2023 10:07:11 +0700 Subject: [PATCH 18/48] improve range getters --- src/js/modules/Spreadsheet/RangeComponent.js | 8 +++ src/js/modules/Spreadsheet/Spreadsheet.js | 52 +++++++++++--------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/js/modules/Spreadsheet/RangeComponent.js b/src/js/modules/Spreadsheet/RangeComponent.js index 202e9b41a..a965780f1 100644 --- a/src/js/modules/Spreadsheet/RangeComponent.js +++ b/src/js/modules/Spreadsheet/RangeComponent.js @@ -32,6 +32,14 @@ class RangeComponent { getStructuredCells() { return this._range.getStructuredCells(); } + + getTop() { + return this._range.top; + } + + getBottom() { + return this._range.bottom; + } } export default RangeComponent; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 598ff1971..8f80996e0 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -15,11 +15,6 @@ class Spreadsheet extends Module { this.registerTableOption("spreadsheet", false); //enable spreadsheet this.registerTableOption("rowHeader", {}); //row header definition - this.registerTableFunction("findRangeFromCell", this.findRangeFromCell.bind(this)); - this.registerTableFunction("findRangeFromRow", this.findRangeFromRow.bind(this)); - this.registerTableFunction("findRangeFromColumn", this.findRangeFromColumn.bind(this)); - this.registerTableFunction("getActiveRange", this.getActiveRange.bind(this, true)); - this.registerColumnOption("__spreadsheet_editable"); this.registerColumnOption("__spreadsheet_editor"); } @@ -27,13 +22,9 @@ class Spreadsheet extends Module { initialize() { if (!this.table.options.spreadsheet) return; - this.registerTableFunction( - "getSelectedData", - this.getSelectedData.bind(this), - ); - this.initializeWatchers(); this.initializeTable(); + this.initializeFunctions(); } initializeWatchers() { @@ -131,6 +122,35 @@ class Spreadsheet extends Module { this.table.rowManager.element.appendChild(this.overlay); } + initializeFunctions() { + this.registerTableFunction("getSelectedData", this.getSelectedData.bind(this)); + this.registerTableFunction("getActiveRange", this.getActiveRange.bind(this, true)); + + this.registerComponentFunction("cell", "getRange", (cell) => { + var range; + + if (cell.column.field === this.rowHeaderField) range = this.ranges.find((range) => range.occupiesRow(cell.row)); + else range = this.ranges.find((range) => range.occupies(cell)); + + if (!range) return null; + return range.getComponent(); + }); + + this.registerComponentFunction("row", "getRange", (row) => { + var range = this.ranges.find((range) => range.occupiesRow(row)); + + if (!range) return null; + return range.getComponent(); + }); + + this.registerComponentFunction("column", "getRange", (col) => { + var range = this.ranges.find((range) => range.occupiesColumn(col)); + + if (!range) return null; + return range.getComponent(); + }); + } + getSelectedData() { return this.getDataByRange(this.getActiveRange()); } @@ -490,18 +510,6 @@ class Spreadsheet extends Module { this.layoutElement(); } - findRangeFromColumn(column) { - return this.ranges.find((range) => range.occupiesColumn(column._column)).getComponent(); - } - - findRangeFromRow(row) { - return this.ranges.find((range) => range.occupiesRow(row._row)).getComponent(); - } - - findRangeFromCell(cell) { - return this.ranges.find((range) => range.occupies(cell._cell)).getComponent(); - } - findRangeByCellElement(cell) { var rangeIdx = cell.dataset.range; if (rangeIdx < 0) return; From 05d406aef58beb438b14330a311f86c2875b45b7 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 24 Oct 2023 13:44:24 +0700 Subject: [PATCH 19/48] fix cell height resize and add rows and columns getters --- src/js/modules/Spreadsheet/Range.js | 8 ++++++++ src/js/modules/Spreadsheet/RangeComponent.js | 8 ++++++++ src/js/modules/Spreadsheet/Spreadsheet.js | 6 ++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/js/modules/Spreadsheet/Range.js b/src/js/modules/Spreadsheet/Range.js index 251ea90a6..849a3ebf8 100644 --- a/src/js/modules/Spreadsheet/Range.js +++ b/src/js/modules/Spreadsheet/Range.js @@ -72,6 +72,14 @@ class Range { return this.table.modules.spreadsheet.getCellsByRange(this, true); } + getRows() { + return this.table.modules.spreadsheet.getRowsByRange(this).map((row) => row.getComponent()); + } + + getColumns() { + return this.table.modules.spreadsheet.getColumnsByRange(this).map((col) => col.getComponent()); + } + getComponent() { if (!this.component) { this.component = new RangeComponent(this); diff --git a/src/js/modules/Spreadsheet/RangeComponent.js b/src/js/modules/Spreadsheet/RangeComponent.js index a965780f1..b0eb7bd45 100644 --- a/src/js/modules/Spreadsheet/RangeComponent.js +++ b/src/js/modules/Spreadsheet/RangeComponent.js @@ -33,6 +33,14 @@ class RangeComponent { return this._range.getStructuredCells(); } + getRows() { + return this._range.getRows(); + } + + getColumns() { + return this._range.getColumns(); + } + getTop() { return this._range.top; } diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 8f80996e0..3a8df8594 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -30,8 +30,10 @@ class Spreadsheet extends Module { initializeWatchers() { this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); - this.subscribe("column-width", this.handleColumnWidth.bind(this)); + this.subscribe("column-width", this.handleColumnSize.bind(this)); + this.subscribe("column-height", this.handleColumnSize.bind(this)); this.subscribe("column-resized", this.layoutSelection.bind(this)); + this.subscribe("cell-height", this.layoutSelection.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); @@ -432,7 +434,7 @@ class Spreadsheet extends Module { el.classList.toggle("tabulator-highlight", occupied); } - handleColumnWidth() { + handleColumnSize() { if (!this.table.initialized) return; this.layoutSelection(); } From 02edb29c8a0d8261e408bb643edf312de317d7de Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 24 Oct 2023 13:57:49 +0700 Subject: [PATCH 20/48] fix destroy table --- src/js/modules/Spreadsheet/Spreadsheet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 3a8df8594..14fc2f0b3 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -115,8 +115,8 @@ class Spreadsheet extends Module { document.addEventListener("mouseup", mouseUpHandler); this.subscribe("table-destroy", () => { - document.removeEventListener(mouseUpHandler); - this.resizeObserver.disconnect(); + document.removeEventListener("mouseup", mouseUpHandler); + resizeObserver.disconnect(); }); this.resetRanges(); From 544771106c1df4eb4319c30d5b31bace40ba3cbf Mon Sep 17 00:00:00 2001 From: azmy60 Date: Fri, 27 Oct 2023 13:19:56 +0700 Subject: [PATCH 21/48] make header focusable so we can copy after clicking the headers --- src/js/core/ColumnManager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/core/ColumnManager.js b/src/js/core/ColumnManager.js index ab750cf6e..95d5b33c1 100644 --- a/src/js/core/ColumnManager.js +++ b/src/js/core/ColumnManager.js @@ -102,6 +102,7 @@ export default class ColumnManager extends CoreFeature { el.classList.add("tabulator-header"); el.setAttribute("role", "rowgroup"); + el.setAttribute("tabindex", 0); if(!this.table.options.headerVisible){ el.classList.add("tabulator-header-hidden"); From c85a8b1fa6170706a943a97e2286b307d144c79e Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 30 Oct 2023 11:46:57 +0700 Subject: [PATCH 22/48] debounce range layout --- src/js/modules/Spreadsheet/Spreadsheet.js | 29 +++++++++++++---------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 14fc2f0b3..b55c41adf 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -22,18 +22,14 @@ class Spreadsheet extends Module { initialize() { if (!this.table.options.spreadsheet) return; - this.initializeWatchers(); this.initializeTable(); + this.initializeWatchers(); this.initializeFunctions(); } initializeWatchers() { this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); - this.subscribe("column-width", this.handleColumnSize.bind(this)); - this.subscribe("column-height", this.handleColumnSize.bind(this)); - this.subscribe("column-resized", this.layoutSelection.bind(this)); - this.subscribe("cell-height", this.layoutSelection.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); @@ -43,9 +39,17 @@ class Spreadsheet extends Module { this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); - // Wait after dom rendering to finish - var debouncedLayoutSelection = Helpers.debounce(this.layoutSelection.bind(this), 50); - this.subscribe("render-virtual-dom", debouncedLayoutSelection); + var debouncedLayoutRanges = Helpers.debounce(this.layoutRanges.bind(this), 200); + function layoutRanges () { + this.overlay.style.visibility = "hidden"; + debouncedLayoutRanges(); + } + + this.subscribe("render-virtual-dom", layoutRanges.bind(this)); + this.subscribe("column-width", layoutRanges.bind(this)); + this.subscribe("column-height", layoutRanges.bind(this)); + this.subscribe("column-resized", layoutRanges.bind(this)); + this.subscribe("cell-height", layoutRanges.bind(this)); } initializeTable() { @@ -399,7 +403,7 @@ class Spreadsheet extends Module { this.layoutColumn(column); }); - this.layoutSelection(); + this.layoutRanges(); } layoutRow(row) { @@ -434,12 +438,9 @@ class Spreadsheet extends Module { el.classList.toggle("tabulator-highlight", occupied); } - handleColumnSize() { + layoutRanges() { if (!this.table.initialized) return; - this.layoutSelection(); - } - layoutSelection() { var activeCell = this.getActiveCell(); if (!activeCell) return; @@ -462,6 +463,8 @@ class Spreadsheet extends Module { "px"; this.ranges.forEach((range) => this.layoutRange(range)); + + this.overlay.style.visibility = "visible"; } layoutRange(range) { From 75fead6a5825948d408cc3aeb50aa38b18f27dc8 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 30 Oct 2023 15:37:15 +0700 Subject: [PATCH 23/48] add keybindings --- src/js/core/tools/Helpers.js | 4 + src/js/modules/Edit/Edit.js | 17 +- .../modules/Keybindings/defaults/actions.js | 48 ++++ .../modules/Keybindings/defaults/bindings.js | 14 + src/js/modules/Spreadsheet/Spreadsheet.js | 241 +++++++++++++++++- 5 files changed, 315 insertions(+), 9 deletions(-) diff --git a/src/js/core/tools/Helpers.js b/src/js/core/tools/Helpers.js index ce368f1bf..27558207e 100644 --- a/src/js/core/tools/Helpers.js +++ b/src/js/core/tools/Helpers.js @@ -58,4 +58,8 @@ export default class Helpers{ }, delay); }; } + + static clamp(number, min, max) { + return Math.max(min, Math.min(number, max)); + } } diff --git a/src/js/modules/Edit/Edit.js b/src/js/modules/Edit/Edit.js index 42d3e4a4f..95f55ef4a 100644 --- a/src/js/modules/Edit/Edit.js +++ b/src/js/modules/Edit/Edit.js @@ -55,13 +55,16 @@ class Edit extends Module{ this.subscribe("row-deleting", this.rowDeleteCheck.bind(this)); this.subscribe("row-layout", this.rowEditableCheck.bind(this)); this.subscribe("data-refreshing", this.cancelEdit.bind(this)); - - this.subscribe("keybinding-nav-prev", this.navigatePrev.bind(this, undefined)); - this.subscribe("keybinding-nav-next", this.keybindingNavigateNext.bind(this)); - this.subscribe("keybinding-nav-left", this.navigateLeft.bind(this, undefined)); - this.subscribe("keybinding-nav-right", this.navigateRight.bind(this, undefined)); - this.subscribe("keybinding-nav-up", this.navigateUp.bind(this, undefined)); - this.subscribe("keybinding-nav-down", this.navigateDown.bind(this, undefined)); + + if (!this.table.options.spreadsheet) { + this.subscribe("keybinding-nav-prev", this.navigatePrev.bind(this, undefined)); + this.subscribe("keybinding-nav-next", this.keybindingNavigateNext.bind(this)); + // FIXME this doesn't respect input navigations when editing + // this.subscribe("keybinding-nav-left", this.navigateLeft.bind(this, undefined)); + // this.subscribe("keybinding-nav-right", this.navigateRight.bind(this, undefined)); + this.subscribe("keybinding-nav-up", this.navigateUp.bind(this, undefined)); + this.subscribe("keybinding-nav-down", this.navigateDown.bind(this, undefined)); + } } diff --git a/src/js/modules/Keybindings/defaults/actions.js b/src/js/modules/Keybindings/defaults/actions.js index 224c15d88..1af8f0ed3 100644 --- a/src/js/modules/Keybindings/defaults/actions.js +++ b/src/js/modules/Keybindings/defaults/actions.js @@ -83,6 +83,54 @@ export default { this.dispatch("keybinding-nav-down", e); }, + navLeftAlt: function(e){ + this.dispatch("keybinding-nav-left-alt", e); + }, + + navRightAlt: function(e){ + this.dispatch("keybinding-nav-right-alt", e); + }, + + navUpAlt: function(e){ + this.dispatch("keybinding-nav-up-alt", e); + }, + + navDownAlt: function(e){ + this.dispatch("keybinding-nav-down-alt", e); + }, + + expandLeft:function(e){ + this.dispatch("keybinding-expand-left", e); + }, + + expandRight:function(e){ + this.dispatch("keybinding-expand-right", e); + }, + + expandUp:function(e){ + this.dispatch("keybinding-expand-up", e); + }, + + expandDown:function(e){ + this.dispatch("keybinding-expand-down", e); + }, + + expandLeftAlt: function(e){ + this.dispatch("keybinding-expand-left-alt", e); + }, + + expandRightAlt: function(e){ + this.dispatch("keybinding-expand-right-alt", e); + }, + + expandUpAlt: function(e){ + this.dispatch("keybinding-expand-up-alt", e); + }, + + expandDownAlt: function(e){ + this.dispatch("keybinding-expand-down-alt", e); + }, + undo:function(e){ var cell = false; if(this.table.options.history && this.table.modExists("history") && this.table.modExists("edit")){ diff --git a/src/js/modules/Keybindings/defaults/bindings.js b/src/js/modules/Keybindings/defaults/bindings.js index e1dc5d1c6..699039a72 100644 --- a/src/js/modules/Keybindings/defaults/bindings.js +++ b/src/js/modules/Keybindings/defaults/bindings.js @@ -3,6 +3,20 @@ export default { navNext:9, navUp:38, navDown:40, + navLeft:37, + navRight:39, + navUpAlt:["ctrl + 38", "meta + 38"], + navDownAlt:["ctrl + 40", "meta + 40"], + navLeftAlt:["ctrl + 37", "meta + 37"], + navRightAlt:["ctrl + 39", "meta + 39"], + expandUp:"shift + 38", + expandDown:"shift + 40", + expandLeft:"shift + 37", + expandRight:"shift + 39", + expandUpAlt:["ctrl + shift + 38", "meta + shift + 38"], + expandDownAlt:["ctrl + shift + 40", "meta + shift + 40"], + expandLeftAlt:["ctrl + shift + 37", "meta + shift + 37"], + expandRightAlt:["ctrl + shift + 39", "meta + shift + 39"], scrollPageUp:33, scrollPageDown:34, scrollToStart:36, diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index b55c41adf..020c4a2e8 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -50,6 +50,38 @@ class Spreadsheet extends Module { this.subscribe("column-height", layoutRanges.bind(this)); this.subscribe("column-resized", layoutRanges.bind(this)); this.subscribe("cell-height", layoutRanges.bind(this)); + + var self = this; + + function navigate(mode, dir) { + return function (e) { + e.preventDefault(); + self.navigate(mode, dir); + } + } + + this.subscribe("keybinding-nav-prev", navigate("normal", "left")); + this.subscribe("keybinding-nav-next", navigate("normal", "right")); + this.subscribe("keybinding-nav-left", navigate("normal", "left")); + this.subscribe("keybinding-nav-right", navigate("normal", "right")); + this.subscribe("keybinding-nav-up", navigate("normal", "up")); + this.subscribe("keybinding-nav-down", navigate("normal", "down")); + + + this.subscribe("keybinding-nav-left-alt", navigate("non-empty", "left")); + this.subscribe("keybinding-nav-right-alt", navigate("non-empty", "right")); + this.subscribe("keybinding-nav-up-alt", navigate("non-empty", "up")); + this.subscribe("keybinding-nav-down-alt", navigate("non-empty", "down")); + + this.subscribe("keybinding-expand-left", navigate("expand", "left")); + this.subscribe("keybinding-expand-right", navigate("expand", "right")); + this.subscribe("keybinding-expand-up", navigate("expand", "up")); + this.subscribe("keybinding-expand-down", navigate("expand", "down")); + + this.subscribe("keybinding-expand-left-alt", navigate("expand-non-empty", "left")); + this.subscribe("keybinding-expand-right-alt", navigate("expand-non-empty", "right")); + this.subscribe("keybinding-expand-up-alt", navigate("expand-non-empty", "up")); + this.subscribe("keybinding-expand-down-alt", navigate("expand-non-empty", "down")); } initializeTable() { @@ -337,6 +369,199 @@ class Spreadsheet extends Module { cell.column.definition.editable = false; cell.column.definition.editor = false; } + navigate(mode, dir) { + if (this.ranges.length > 1) { + this.ranges = this.ranges.filter((range) => { + if (range === this.getActiveRange()) { + range.setEnd(range.start.row, range.start.col); + return true; + } + range.destroy(); + return false; + }) + } + + var range = this.getActiveRange(); + + switch (mode) { + case "normal": { + var nextRow = range.start.row; + var nextCol = range.start.col; + + if (dir === "left") nextCol -= 1; + else if (dir === "right") nextCol += 1; + else if (dir === "up") nextRow -= 1; + else if (dir === "down") nextRow += 1; + + range.setStart(nextRow, nextCol); + range.setEnd(nextRow, nextCol); + + this.scrollIfNeeded(range, dir); + + break; + } + case "expand": { + var nextRow = range.end.row; + var nextCol = range.end.col; + + if (dir === "left") nextCol -= 1; + else if (dir === "right") nextCol += 1; + else if (dir === "up") nextRow -= 1; + else if (dir === "down") nextRow += 1; + + range.setEnd(nextRow, nextCol); + + this.scrollIfNeeded(range, dir); + + break; + } + case "non-empty": { + var nextRow = range.start.row; + var nextCol = range.start.col; + + if (dir === "left") nextCol = this.findPrevNonEmptyCellHoz(range); + else if (dir === "right") nextCol = this.findNextNonEmptyCellHoz(range); + else if (dir === "up") nextRow = this.findPrevNonEmptyCellVert(range); + else if (dir === "down") nextRow = this.findNextNonEmptyCellVert(range); + + range.setStart(nextRow, nextCol); + range.setEnd(nextRow, nextCol); + + this.scrollIfNeeded(range, dir); + + break; + } + case "expand-non-empty": { + var nextRow = range.end.row; + var nextCol = range.end.col; + + if (dir === "left") nextCol = this.findPrevNonEmptyCellHoz(range); + else if (dir === "right") nextCol = this.findNextNonEmptyCellHoz(range); + else if (dir === "up") nextRow = this.findPrevNonEmptyCellVert(range); + else if (dir === "down") nextRow = this.findNextNonEmptyCellVert(range); + + range.setEnd(nextRow, nextCol); + + this.scrollIfNeeded(range, dir); + + break; + } + } + + this.layoutElement(); + } + + scrollIfNeeded(range, dir) { + if (dir === "right") { + var column = this.getColumnByRangePos(range.end.col).getElement(); + var tableHolder = this.table.rowManager.element; + var columnOffsetRight = column.offsetLeft + column.offsetWidth; + var tableOffsetRight = tableHolder.scrollLeft + tableHolder.offsetWidth - this.table.rowManager.scrollbarWidth + + if (columnOffsetRight - tableOffsetRight > 0) { + this.table.rowManager.scrollHorizontal( + Math.min( + column.offsetLeft - this.rowHeaderColumn.getElement().offsetWidth, + this.table.rowManager.element.scrollLeft + column.offsetWidth + ) + ); + } + } + else if (dir === "left") { + var column = this.getColumnByRangePos(range.end.col).getElement(); + var tableHolder = this.table.rowManager.element + var columnOffsetLeft = column.offsetLeft; + var tableOffsetLeft = tableHolder.scrollLeft + this.rowHeaderColumn.getElement().offsetWidth; + + if (columnOffsetLeft < tableOffsetLeft) { + this.table.rowManager.scrollHorizontal(this.table.rowManager.element.scrollLeft - column.offsetWidth); + } + } + else if (dir === "down") { + var row = this.getRowByRangePos(range.end.row); + var tableHolder = this.table.rowManager.element; + var rowOffsetBottom = row.getElement().offsetTop + row.getElement().offsetHeight; + var tableOffsetBottom = tableHolder.scrollTop + tableHolder.offsetHeight - this.table.rowManager.scrollbarWidth; + + if (rowOffsetBottom - tableOffsetBottom > 0) { + this.table.scrollToRow(row, 'bottom'); + } + } + else if (dir === "up") { + var row = this.getRowByRangePos(range.end.row); + var tableHolder = this.table.rowManager.element; + var rowOffsetTop = row.getElement().offsetTop; + var tableOffsetTop = tableHolder.scrollTop; + + if (rowOffsetTop < tableOffsetTop) { + this.table.scrollToRow(row, 'top') + } + } + } + + findNextNonEmptyCellHoz(range){ + var row = this.getRowByRangePos(range.start.row); + var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); + var nextCol = 0; + + for (var i = range.end.col + 2; i < cells.length; i++){ + var cell = cells[i]; + if (Helpers.elVisible(cell.getElement())) { + nextCol = cell.column.position - 2; + if (cell.getValue()) break; + } + } + + return nextCol; + } + + findPrevNonEmptyCellHoz(range){ + var row = this.getRowByRangePos(range.start.row); + var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); + var prevCol = 0; + + for(var i = range.end.col; i >= 0; i--){ + var cell = cells[i]; + if (Helpers.elVisible(cell.getElement())) { + prevCol = cell.column.position - 2; + if (cell.getValue()) break + } + } + + return prevCol; + } + + findNextNonEmptyCellVert(range) { + var column = this.getColumnByRangePos(range.start.col); + var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())) + var nextRow = -1; + + for (var i = range.end.row + 1; i < cells.length; i++){ + var cell = cells[i]; + if (Helpers.elVisible(cell.getElement())) { + nextRow = cell.row.position - 1; + if (cell.getValue()) break; + } + } + + return nextRow; + } + + findPrevNonEmptyCellVert(range) { + var column = this.getColumnByRangePos(range.start.col); + var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); + var prevRow = 0; + + for(var i = range.end.row - 1; i >= 0; i--){ + var cell = cells[i]; + if (Helpers.elVisible(cell.getElement())) { + prevRow = cell.row.position - 1; + if (cell.getValue()) break; + } + } + + return prevRow; + } beginSelection(element) { var range = this.getActiveRange(); @@ -528,14 +753,14 @@ class Spreadsheet extends Module { var row = this.table.rowManager.getRowFromPosition(rowIdx + 1); - if (!row) return; + if (!row) return null; if (colIdx < 0) { colIdx = this.table.columnManager.getVisibleColumnsByIndex().length + colIdx - 1; } - if (colIdx < 0) return; + if (colIdx < 0) return null; return row.getCells().filter((cell) => cell.column.visible)[colIdx + 1]; } @@ -552,6 +777,14 @@ class Spreadsheet extends Module { return this.getCell(activeRange.start.row, activeRange.start.col); } + getRowByRangePos(pos) { + return this.table.rowManager.getDisplayRows()[pos]; + } + + getColumnByRangePos(pos) { + return this.table.columnManager.getVisibleColumnsByIndex()[pos + 1]; + } + getRowsByRange(range) { return this.table.rowManager .getDisplayRows() @@ -589,6 +822,10 @@ class Spreadsheet extends Module { col.getComponent(), ); } + + get rowHeaderColumn() { + return this.table.columnManager.columnsByField[this.rowHeaderField]; + } } Spreadsheet.moduleName = "spreadsheet"; From 2c519b76fe6e5d25bb08e3dd7cb568c193e84320 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Wed, 1 Nov 2023 21:28:49 +0700 Subject: [PATCH 24/48] remove render-virtual-dom, naming corrections, inline if, etc - remove mouse up listener after finish dragging - move table header tabindex initialization - fix variable definitions in switch case - fix overflowing key navigations - fix keyboard navigations --- src/js/core/ColumnManager.js | 1 - .../renderers/VirtualDomHorizontal.js | 6 +- .../rendering/renderers/VirtualDomVertical.js | 16 - .../modules/Keybindings/defaults/actions.js | 131 +++-- .../modules/Keybindings/defaults/bindings.js | 27 +- src/js/modules/Spreadsheet/Spreadsheet.js | 446 ++++++++++-------- src/scss/tabulator.scss | 16 +- 7 files changed, 370 insertions(+), 273 deletions(-) diff --git a/src/js/core/ColumnManager.js b/src/js/core/ColumnManager.js index 95d5b33c1..ab750cf6e 100644 --- a/src/js/core/ColumnManager.js +++ b/src/js/core/ColumnManager.js @@ -102,7 +102,6 @@ export default class ColumnManager extends CoreFeature { el.classList.add("tabulator-header"); el.setAttribute("role", "rowgroup"); - el.setAttribute("tabindex", 0); if(!this.table.options.headerVisible){ el.classList.add("tabulator-header-hidden"); diff --git a/src/js/core/rendering/renderers/VirtualDomHorizontal.js b/src/js/core/rendering/renderers/VirtualDomHorizontal.js index d23ff7d9c..9044f4e68 100644 --- a/src/js/core/rendering/renderers/VirtualDomHorizontal.js +++ b/src/js/core/rendering/renderers/VirtualDomHorizontal.js @@ -370,7 +370,6 @@ export default class VirtualDomHorizontal extends Renderer{ if(changes){ this.tableElement.style.paddingRight = this.vDomPadRight + "px"; - this.dispatch("render-virtual-dom"); } } @@ -424,7 +423,6 @@ export default class VirtualDomHorizontal extends Renderer{ if(changes){ this.tableElement.style.paddingLeft = this.vDomPadLeft + "px"; - this.dispatch("render-virtual-dom"); } } @@ -469,7 +467,6 @@ export default class VirtualDomHorizontal extends Renderer{ if(changes){ this.tableElement.style.paddingRight = this.vDomPadRight + "px"; - this.dispatch("render-virtual-dom"); } } @@ -514,7 +511,6 @@ export default class VirtualDomHorizontal extends Renderer{ if(changes){ this.tableElement.style.paddingLeft = this.vDomPadLeft + "px"; - this.dispatch("render-virtual-dom"); } } @@ -584,4 +580,4 @@ export default class VirtualDomHorizontal extends Renderer{ } } } -} \ No newline at end of file +} diff --git a/src/js/core/rendering/renderers/VirtualDomVertical.js b/src/js/core/rendering/renderers/VirtualDomVertical.js index d5ea689e6..c4b775b2f 100644 --- a/src/js/core/rendering/renderers/VirtualDomVertical.js +++ b/src/js/core/rendering/renderers/VirtualDomVertical.js @@ -463,10 +463,6 @@ export default class VirtualDomVertical extends Renderer{ table.style.paddingTop = this.vDomTopPad + "px"; this.vDomScrollPosTop -= paddingAdjust; } - - if (addedRows.length) { - this.dispatch("render-virtual-dom"); - } } _removeTopRow(rows, fillableSpace){ @@ -511,10 +507,6 @@ export default class VirtualDomVertical extends Renderer{ this.tableElement.style.paddingTop = this.vDomTopPad + "px"; this.vDomScrollPosTop += this.vDomTop ? paddingAdjust : paddingAdjust + this.vDomWindowBuffer; } - - if (removableRows.length) { - this.dispatch("render-virtual-dom"); - } } _addBottomRow(rows, fillableSpace){ @@ -582,10 +574,6 @@ export default class VirtualDomVertical extends Renderer{ table.style.paddingBottom = this.vDomBottomPad + "px"; this.vDomScrollPosBottom += paddingAdjust; } - - if (addedRows.length) { - this.dispatch("render-virtual-dom"); - } } _removeBottomRow(rows, fillableSpace){ @@ -635,10 +623,6 @@ export default class VirtualDomVertical extends Renderer{ this.tableElement.style.paddingBottom = this.vDomBottomPad + "px"; this.vDomScrollPosBottom -= paddingAdjust; } - - if (removableRows.length) { - this.dispatch("render-virtual-dom"); - } } _quickNormalizeRowHeight(rows){ diff --git a/src/js/modules/Keybindings/defaults/actions.js b/src/js/modules/Keybindings/defaults/actions.js index 1af8f0ed3..610a47e73 100644 --- a/src/js/modules/Keybindings/defaults/actions.js +++ b/src/js/modules/Keybindings/defaults/actions.js @@ -83,53 +83,110 @@ export default { this.dispatch("keybinding-nav-down", e); }, - navLeftAlt: function(e){ - this.dispatch("keybinding-nav-left-alt", e); - }, - - navRightAlt: function(e){ - this.dispatch("keybinding-nav-right-alt", e); + spreadsheetJumpLeft: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("jump", "left"); }, - - navUpAlt: function(e){ - this.dispatch("keybinding-nav-up-alt", e); + spreadsheetJumpRight: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("jump", "right"); }, - - navDownAlt: function(e){ - this.dispatch("keybinding-nav-down-alt", e); + spreadsheetJumpUp: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("jump", "up"); }, - - expandLeft:function(e){ - this.dispatch("keybinding-expand-left", e); + spreadsheetJumpDown: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("jump", "down"); }, - - expandRight:function(e){ - this.dispatch("keybinding-expand-right", e); + spreadsheetExpandLeft: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("expand", "left"); }, - - expandUp:function(e){ - this.dispatch("keybinding-expand-up", e); + spreadsheetExpandRight: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("expand", "right"); }, - - expandDown:function(e){ - this.dispatch("keybinding-expand-down", e); + spreadsheetExpandUp: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("expand", "up"); }, - - expandLeftAlt: function(e){ - this.dispatch("keybinding-expand-left-alt", e); + spreadsheetExpandDown: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("expand", "down"); }, - - expandRightAlt: function(e){ - this.dispatch("keybinding-expand-right-alt", e); + spreadsheetExpandJumpLeft: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("expand-jump", "left"); }, - - expandUpAlt: function(e){ - this.dispatch("keybinding-expand-up-alt", e); + spreadsheetExpandJumpRight: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("expand-jump", "right"); }, - - expandDownAlt: function(e){ - this.dispatch("keybinding-expand-down-alt", e); + spreadsheetExpandJumpUp: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("expand-jump", "up"); }, + spreadsheetExpandJumpDown: function(e){ + if (!this.table.options.spreadsheet) return; + e.preventDefault(); + this.table.modules.spreadsheet.navigate("expand-jump", "down"); + }, + + // navRightAlt: function(e){ + // this.dispatch("keybinding-nav-right-alt", e); + // }, + // + // navUpAlt: function(e){ + // this.dispatch("keybinding-nav-up-alt", e); + // }, + // + // navDownAlt: function(e){ + // this.dispatch("keybinding-nav-down-alt", e); + // }, + + // expandLeft:function(e){ + // this.dispatch("keybinding-expand-left", e); + // }, + // + // expandRight:function(e){ + // this.dispatch("keybinding-expand-right", e); + // }, + // + // expandUp:function(e){ + // this.dispatch("keybinding-expand-up", e); + // }, + // + // expandDown:function(e){ + // this.dispatch("keybinding-expand-down", e); + // }, + // + // expandLeftAlt: function(e){ + // this.dispatch("keybinding-expand-left-alt", e); + // }, + // + // expandRightAlt: function(e){ + // this.dispatch("keybinding-expand-right-alt", e); + // }, + // + // expandUpAlt: function(e){ + // this.dispatch("keybinding-expand-up-alt", e); + // }, + // + // expandDownAlt: function(e){ + // this.dispatch("keybinding-expand-down-alt", e); + // }, undo:function(e){ var cell = false; @@ -164,4 +221,4 @@ export default { } } }, -}; \ No newline at end of file +}; diff --git a/src/js/modules/Keybindings/defaults/bindings.js b/src/js/modules/Keybindings/defaults/bindings.js index 699039a72..d54c8f1aa 100644 --- a/src/js/modules/Keybindings/defaults/bindings.js +++ b/src/js/modules/Keybindings/defaults/bindings.js @@ -5,18 +5,6 @@ export default { navDown:40, navLeft:37, navRight:39, - navUpAlt:["ctrl + 38", "meta + 38"], - navDownAlt:["ctrl + 40", "meta + 40"], - navLeftAlt:["ctrl + 37", "meta + 37"], - navRightAlt:["ctrl + 39", "meta + 39"], - expandUp:"shift + 38", - expandDown:"shift + 40", - expandLeft:"shift + 37", - expandRight:"shift + 39", - expandUpAlt:["ctrl + shift + 38", "meta + shift + 38"], - expandDownAlt:["ctrl + shift + 40", "meta + shift + 40"], - expandLeftAlt:["ctrl + shift + 37", "meta + shift + 37"], - expandRightAlt:["ctrl + shift + 39", "meta + shift + 39"], scrollPageUp:33, scrollPageDown:34, scrollToStart:36, @@ -24,4 +12,17 @@ export default { undo:["ctrl + 90", "meta + 90"], redo:["ctrl + 89", "meta + 89"], copyToClipboard:["ctrl + 67", "meta + 67"], -}; \ No newline at end of file + + spreadsheetJumpUp:["ctrl + 38", "meta + 38"], + spreadsheetJumpDown:["ctrl + 40", "meta + 40"], + spreadsheetJumpLeft:["ctrl + 37", "meta + 37"], + spreadsheetJumpRight:["ctrl + 39", "meta + 39"], + spreadsheetExpandUp:"shift + 38", + spreadsheetExpandDown:"shift + 40", + spreadsheetExpandLeft:"shift + 37", + spreadsheetExpandRight:"shift + 39", + spreadsheetExpandJumpUp:["ctrl + shift + 38", "meta + shift + 38"], + spreadsheetExpandJumpDown:["ctrl + shift + 40", "meta + shift + 40"], + spreadsheetExpandJumpLeft:["ctrl + shift + 37", "meta + shift + 37"], + spreadsheetExpandJumpRight:["ctrl + shift + 39", "meta + shift + 39"], +}; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 020c4a2e8..ae238c678 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -13,14 +13,16 @@ class Spreadsheet extends Module { this.overlay = null; this.registerTableOption("spreadsheet", false); //enable spreadsheet - this.registerTableOption("rowHeader", {}); //row header definition + this.registerTableOption("spreadsheetRowHeader", {}); //row header definition this.registerColumnOption("__spreadsheet_editable"); this.registerColumnOption("__spreadsheet_editor"); } initialize() { - if (!this.table.options.spreadsheet) return; + if (!this.table.options.spreadsheet) { + return; + } this.initializeTable(); this.initializeWatchers(); @@ -45,19 +47,19 @@ class Spreadsheet extends Module { debouncedLayoutRanges(); } - this.subscribe("render-virtual-dom", layoutRanges.bind(this)); + this.subscribe("scroll-vertical", layoutRanges.bind(this)); + this.subscribe("scroll-horizontal", layoutRanges.bind(this)); this.subscribe("column-width", layoutRanges.bind(this)); this.subscribe("column-height", layoutRanges.bind(this)); this.subscribe("column-resized", layoutRanges.bind(this)); this.subscribe("cell-height", layoutRanges.bind(this)); - var self = this; - - function navigate(mode, dir) { + var navigate = (mode, dir) => { + var self = this; return function (e) { e.preventDefault(); self.navigate(mode, dir); - } + }; } this.subscribe("keybinding-nav-prev", navigate("normal", "left")); @@ -66,22 +68,6 @@ class Spreadsheet extends Module { this.subscribe("keybinding-nav-right", navigate("normal", "right")); this.subscribe("keybinding-nav-up", navigate("normal", "up")); this.subscribe("keybinding-nav-down", navigate("normal", "down")); - - - this.subscribe("keybinding-nav-left-alt", navigate("non-empty", "left")); - this.subscribe("keybinding-nav-right-alt", navigate("non-empty", "right")); - this.subscribe("keybinding-nav-up-alt", navigate("non-empty", "up")); - this.subscribe("keybinding-nav-down-alt", navigate("non-empty", "down")); - - this.subscribe("keybinding-expand-left", navigate("expand", "left")); - this.subscribe("keybinding-expand-right", navigate("expand", "right")); - this.subscribe("keybinding-expand-up", navigate("expand", "up")); - this.subscribe("keybinding-expand-down", navigate("expand", "down")); - - this.subscribe("keybinding-expand-left-alt", navigate("expand-non-empty", "left")); - this.subscribe("keybinding-expand-right-alt", navigate("expand-non-empty", "right")); - this.subscribe("keybinding-expand-up-alt", navigate("expand-non-empty", "up")); - this.subscribe("keybinding-expand-down-alt", navigate("expand-non-empty", "down")); } initializeTable() { @@ -101,6 +87,8 @@ class Spreadsheet extends Module { column.editor = false; } + var customRowHeader = this.options("spreadsheetRowHeader"); + var rowHeaderDef = { title: "", field: this.rowHeaderField, @@ -108,10 +96,14 @@ class Spreadsheet extends Module { resizable: false, frozen: true, editable: false, - cssClass: "tabulator-row-header", formatter: "rownum", formatterParams: { relativeToPage: true }, - ...this.options("rowHeader"), + + ...customRowHeader, + + cssClass: customRowHeader.cssClass + ? `tabulator-spreadsheet-row-header ${customRowHeader.cssClass}` + : "tabulator-spreadsheet-row-header", }; this.rowHeaderField = rowHeaderDef.field; @@ -134,30 +126,22 @@ class Spreadsheet extends Module { this.overlay.appendChild(this.rangeContainer); this.overlay.appendChild(this.activeRangeCellElement); - - var resizeObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - this.overlay.style.left = entry.target.offsetLeft + "px"; - this.overlay.style.top = entry.target.offsetTop + "px"; - this.overlay.style.width = entry.target.offsetWidth + "px"; - this.overlay.style.height = entry.target.offsetHeight + "px"; - } - }); - - resizeObserver.observe(this.table.rowManager.tableElement); - var mouseUpHandler = this.handleMouseUp.bind(this); + var self = this; - document.addEventListener("mouseup", mouseUpHandler); + this.mouseUpHandler = function () { + self.mousedown = false; + document.removeEventListener("mouseup", self.mouseUpHandler); + }; - this.subscribe("table-destroy", () => { - document.removeEventListener("mouseup", mouseUpHandler); - resizeObserver.disconnect(); + this.subscribe("table-destroy", function () { + document.removeEventListener("mouseup", self.mouseUpHandler); }); this.resetRanges(); this.table.rowManager.element.appendChild(this.overlay); + this.table.columnManager.element.setAttribute("tabindex", 0); } initializeFunctions() { @@ -167,24 +151,36 @@ class Spreadsheet extends Module { this.registerComponentFunction("cell", "getRange", (cell) => { var range; - if (cell.column.field === this.rowHeaderField) range = this.ranges.find((range) => range.occupiesRow(cell.row)); - else range = this.ranges.find((range) => range.occupies(cell)); + if (cell.column.field === this.rowHeaderField) { + range = this.ranges.find((range) => range.occupiesRow(cell.row)); + } else { + range = this.ranges.find((range) => range.occupies(cell)); + } + + if (!range) { + return null; + } - if (!range) return null; return range.getComponent(); }); this.registerComponentFunction("row", "getRange", (row) => { var range = this.ranges.find((range) => range.occupiesRow(row)); - if (!range) return null; + if (!range) { + return null; + } + return range.getComponent(); }); this.registerComponentFunction("column", "getRange", (col) => { var range = this.ranges.find((range) => range.occupiesColumn(col)); - if (!range) return null; + if (!range) { + return null; + } + return range.getComponent(); }); } @@ -243,10 +239,10 @@ class Spreadsheet extends Module { var rangeIdx = this.ranges.findIndex((range) => range.occupies(cell)); - el.classList.toggle("tabulator-selected", rangeIdx !== -1); + el.classList.toggle("tabulator-spreadsheet-selected", rangeIdx !== -1); el.classList.toggle( - "tabulator-only-cell-selected", + "tabulator-spreadsheet-only-cell-selected", this.ranges.length === 1 && this.ranges[0].atTopLeft(cell) && this.ranges[0].atBottomRight(cell), @@ -266,14 +262,16 @@ class Spreadsheet extends Module { this.mousedown = true; + document.addEventListener("mouseup", this.mouseUpHandler); + this._select(event, column); this.layoutElement(); } handleColumnMouseMove(_, column) { - if (column.field === this.rowHeaderField) return; - if (!this.mousedown) return; - if (this.selecting === 'all') return; + if (column.field === this.rowHeaderField || !this.mousedown || this.selecting === 'all') { + return; + } this.endSelection(column); this.layoutElement(true); @@ -291,6 +289,8 @@ class Spreadsheet extends Module { this.mousedown = true; + document.addEventListener("mouseup", this.mouseUpHandler); + this._select(event, cell); this.layoutElement(); } @@ -337,19 +337,19 @@ class Spreadsheet extends Module { } handleCellMouseMove(_, cell) { - if (!this.mousedown) return; - if (this.selecting === "all") return; + if (!this.mousedown || this.selecting === "all") { + return; + } this.endSelection(cell); this.layoutElement(true); } - handleMouseUp() { - this.mousedown = false; - } - handleCellDblClick(_, cell) { - if (cell.column.field === this.rowHeaderField) return; + if (cell.column.field === this.rowHeaderField) { + return; + } + this.editCell(cell); } @@ -369,6 +369,7 @@ class Spreadsheet extends Module { cell.column.definition.editable = false; cell.column.definition.editor = false; } + navigate(mode, dir) { if (this.ranges.length > 1) { this.ranges = this.ranges.filter((range) => { @@ -378,87 +379,128 @@ class Spreadsheet extends Module { } range.destroy(); return false; - }) + }); } var range = this.getActiveRange(); switch (mode) { case "normal": { - var nextRow = range.start.row; - var nextCol = range.start.col; - - if (dir === "left") nextCol -= 1; - else if (dir === "right") nextCol += 1; - else if (dir === "up") nextRow -= 1; - else if (dir === "down") nextRow += 1; + let nextRow = range.start.row; + let nextCol = range.start.col; + + if (dir === "left") { + nextCol = Math.max(nextCol - 1, 0); + } else if (dir === "right") { + nextCol = Math.min(nextCol + 1, this.getColumns().length - 2); + } else if (dir === "up") { + nextRow = Math.max(nextRow - 1, 0); + } else if (dir === "down") { + nextRow = Math.min(nextRow + 1, this.getRows().length - 1); + } range.setStart(nextRow, nextCol); range.setEnd(nextRow, nextCol); - this.scrollIfNeeded(range, dir); + this.selecting = "cell"; break; } case "expand": { - var nextRow = range.end.row; - var nextCol = range.end.col; + if ((dir === 'left' || dir === 'right') && this.selecting === 'row') { + break; + } + + if ((dir === 'up' || dir === 'down') && this.selecting === 'column') { + break; + } - if (dir === "left") nextCol -= 1; - else if (dir === "right") nextCol += 1; - else if (dir === "up") nextRow -= 1; - else if (dir === "down") nextRow += 1; + let nextRow = range.end.row; + let nextCol = range.end.col; + + if (dir === "left") { + nextCol = Math.max(nextCol - 1, 0); + } else if (dir === "right") { + nextCol = Math.min(nextCol + 1, this.getColumns().length - 2); + } else if (dir === "up") { + nextRow = Math.max(nextRow - 1, 0); + } else if (dir === "down") { + nextRow = Math.min(nextRow + 1, this.getRows().length - 1); + } range.setEnd(nextRow, nextCol); - this.scrollIfNeeded(range, dir); - break; } - case "non-empty": { - var nextRow = range.start.row; - var nextCol = range.start.col; - - if (dir === "left") nextCol = this.findPrevNonEmptyCellHoz(range); - else if (dir === "right") nextCol = this.findNextNonEmptyCellHoz(range); - else if (dir === "up") nextRow = this.findPrevNonEmptyCellVert(range); - else if (dir === "down") nextRow = this.findNextNonEmptyCellVert(range); + case "jump": { + let nextRow = range.start.row; + let nextCol = range.start.col; + + if (dir === "left") { + nextCol = this.findJumpCellLeft(range.start.row, range.start.col); + } else if (dir === "right") { + nextCol = this.findJumpCellRight(range.start.row, range.start.col); + } else if (dir === "up") { + nextRow = this.findJumpCellUp(range.start.row, range.start.col); + } else if (dir === "down") { + nextRow = this.findJumpCellDown(range.start.row, range.start.col); + } range.setStart(nextRow, nextCol); range.setEnd(nextRow, nextCol); - this.scrollIfNeeded(range, dir); + this.selecting = "cell"; break; } - case "expand-non-empty": { - var nextRow = range.end.row; - var nextCol = range.end.col; - - if (dir === "left") nextCol = this.findPrevNonEmptyCellHoz(range); - else if (dir === "right") nextCol = this.findNextNonEmptyCellHoz(range); - else if (dir === "up") nextRow = this.findPrevNonEmptyCellVert(range); - else if (dir === "down") nextRow = this.findNextNonEmptyCellVert(range); + case "expand-jump": { + let nextRow = range.end.row; + let nextCol = range.end.col; + + if (dir === "left") { + nextCol = this.findJumpCellLeft(range.start.row, range.end.col); + } else if (dir === "right") { + nextCol = this.findJumpCellRight(range.start.row, range.end.col); + } else if (dir === "up") { + nextRow = this.findJumpCellUp(range.end.row, range.start.col); + } else if (dir === "down") { + nextRow = this.findJumpCellDown(range.end.row, range.start.col); + } range.setEnd(nextRow, nextCol); - this.scrollIfNeeded(range, dir); - break; } } + this.autoScroll(range); + this.layoutElement(); } - scrollIfNeeded(range, dir) { - if (dir === "right") { - var column = this.getColumnByRangePos(range.end.col).getElement(); - var tableHolder = this.table.rowManager.element; - var columnOffsetRight = column.offsetLeft + column.offsetWidth; - var tableOffsetRight = tableHolder.scrollLeft + tableHolder.offsetWidth - this.table.rowManager.scrollbarWidth + autoScroll(range) { + var row = this.getRowByRangePos(range.end.row); + var column = this.getColumnByRangePos(range.end.col).getElement(); + var currentLeft = column.offsetLeft; + var currentRight = column.offsetLeft + column.offsetWidth; + var currentTop = row.getElement().offsetTop; + var currentBottom = currentTop + row.getElement().offsetHeight; + var viewBounds = this.getTableViewBounds(); + + var fullyVisibleHorizontal = viewBounds.left < currentLeft + && currentLeft < viewBounds.right + && viewBounds.left < currentRight + && currentRight < viewBounds.right; - if (columnOffsetRight - tableOffsetRight > 0) { + var fullyVisibileVertical = viewBounds.top < currentTop + && currentTop < viewBounds.bottom + && viewBounds.top < currentBottom + && currentBottom < viewBounds.bottom; + + if (!fullyVisibleHorizontal) { + if (currentLeft < viewBounds.left) { + this.table.rowManager.scrollHorizontal(this.table.rowManager.element.scrollLeft - column.offsetWidth); + } else if (currentRight > viewBounds.right) { this.table.rowManager.scrollHorizontal( Math.min( column.offsetLeft - this.rowHeaderColumn.getElement().offsetWidth, @@ -467,100 +509,78 @@ class Spreadsheet extends Module { ); } } - else if (dir === "left") { - var column = this.getColumnByRangePos(range.end.col).getElement(); - var tableHolder = this.table.rowManager.element - var columnOffsetLeft = column.offsetLeft; - var tableOffsetLeft = tableHolder.scrollLeft + this.rowHeaderColumn.getElement().offsetWidth; - if (columnOffsetLeft < tableOffsetLeft) { - this.table.rowManager.scrollHorizontal(this.table.rowManager.element.scrollLeft - column.offsetWidth); - } - } - else if (dir === "down") { - var row = this.getRowByRangePos(range.end.row); - var tableHolder = this.table.rowManager.element; - var rowOffsetBottom = row.getElement().offsetTop + row.getElement().offsetHeight; - var tableOffsetBottom = tableHolder.scrollTop + tableHolder.offsetHeight - this.table.rowManager.scrollbarWidth; - - if (rowOffsetBottom - tableOffsetBottom > 0) { + if (!fullyVisibileVertical) { + if (currentTop < viewBounds.top) { + this.table.scrollToRow(row, 'top'); + } else if (currentBottom > viewBounds.bottom) { this.table.scrollToRow(row, 'bottom'); } } - else if (dir === "up") { - var row = this.getRowByRangePos(range.end.row); - var tableHolder = this.table.rowManager.element; - var rowOffsetTop = row.getElement().offsetTop; - var tableOffsetTop = tableHolder.scrollTop; - - if (rowOffsetTop < tableOffsetTop) { - this.table.scrollToRow(row, 'top') - } - } } - findNextNonEmptyCellHoz(range){ - var row = this.getRowByRangePos(range.start.row); + findJumpCellLeft(rowPos, colPos){ + var row = this.getRowByRangePos(rowPos); var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); - var nextCol = 0; - - for (var i = range.end.col + 2; i < cells.length; i++){ + var jumpCol = 0; + + for (var i = colPos; i >= 0; i--){ var cell = cells[i]; - if (Helpers.elVisible(cell.getElement())) { - nextCol = cell.column.position - 2; + if (Helpers.elVisible(cell.getElement()) && cell.column.field !== this.rowHeaderField) { + jumpCol = cell.column.position - 2; if (cell.getValue()) break; } } - - return nextCol; + + return jumpCol; } - findPrevNonEmptyCellHoz(range){ - var row = this.getRowByRangePos(range.start.row); + findJumpCellRight(rowPos, colPos){ + var row = this.getRowByRangePos(rowPos); var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); - var prevCol = 0; + var jumpCol = 0; - for(var i = range.end.col; i >= 0; i--){ + for (var i = colPos + 2; i < cells.length; i++){ var cell = cells[i]; if (Helpers.elVisible(cell.getElement())) { - prevCol = cell.column.position - 2; - if (cell.getValue()) break + jumpCol = cell.column.position - 2; + if (cell.getValue()) break; } } - return prevCol; + return jumpCol; } - findNextNonEmptyCellVert(range) { - var column = this.getColumnByRangePos(range.start.col); - var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())) - var nextRow = -1; + findJumpCellUp(rowPos, colPos) { + var column = this.getColumnByRangePos(colPos); + var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); + var jumpRow = 0; - for (var i = range.end.row + 1; i < cells.length; i++){ + for (var i = rowPos - 1; i >= 0; i--){ var cell = cells[i]; if (Helpers.elVisible(cell.getElement())) { - nextRow = cell.row.position - 1; + jumpRow = cell.row.position - 1; if (cell.getValue()) break; } } - return nextRow; + return jumpRow; } - findPrevNonEmptyCellVert(range) { - var column = this.getColumnByRangePos(range.start.col); + findJumpCellDown(rowPos, colPos) { + var column = this.getColumnByRangePos(colPos); var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); - var prevRow = 0; + var jumpRow = -1; - for(var i = range.end.row - 1; i >= 0; i--){ + for (var i = rowPos + 1; i < cells.length; i++){ var cell = cells[i]; if (Helpers.elVisible(cell.getElement())) { - prevRow = cell.row.position - 1; + jumpRow = cell.row.position - 1; if (cell.getValue()) break; } } - return prevRow; + return jumpRow; } beginSelection(element) { @@ -583,7 +603,7 @@ class Spreadsheet extends Module { endSelection(element) { var range = this.getActiveRange(); - var rowsCount = this.table.rowManager.getDisplayRows().length; + var rowsCount = this.getRows().length; if (element.type === "column") { if (this.selecting === "column") { @@ -599,10 +619,7 @@ class Spreadsheet extends Module { var isRowHeader = element.column.field === this.rowHeaderField; if (this.selecting === "row") { - range.setEnd( - row, - this.table.columnManager.getVisibleColumnsByIndex().length - 2, - ); + range.setEnd(row, this.getColumns().length - 2); } else if (this.selecting !== "row" && isRowHeader) { range.setEnd(row, 0); } else if (this.selecting === "column") { @@ -613,9 +630,13 @@ class Spreadsheet extends Module { } layoutElement(visibleRows) { - var rows = visibleRows - ? this.table.rowManager.getVisibleRows(true) - : this.table.rowManager.getRows(); + var rows; + + if (visibleRows) { + rows = this.table.rowManager.getVisibleRows(true) + } else { + rows = this.table.rowManager.getRows(); + } rows.forEach((row) => { if (row.type === "row") { @@ -624,7 +645,7 @@ class Spreadsheet extends Module { } }); - this.table.columnManager.getVisibleColumnsByIndex().forEach((column) => { + this.getColumns().forEach((column) => { this.layoutColumn(column); }); @@ -643,8 +664,8 @@ class Spreadsheet extends Module { selected = true; } - el.classList.toggle("tabulator-selected", selected); - el.classList.toggle("tabulator-highlight", occupied); + el.classList.toggle("tabulator-spreadsheet-selected", selected); + el.classList.toggle("tabulator-spreadsheet-highlight", occupied); } layoutColumn(column) { @@ -659,16 +680,20 @@ class Spreadsheet extends Module { selected = true; } - el.classList.toggle("tabulator-selected", selected); - el.classList.toggle("tabulator-highlight", occupied); + el.classList.toggle("tabulator-spreadsheet-selected", selected); + el.classList.toggle("tabulator-spreadsheet-highlight", occupied); } layoutRanges() { - if (!this.table.initialized) return; + if (!this.table.initialized) { + return; + } var activeCell = this.getActiveCell(); - if (!activeCell) return; + if (!activeCell) { + return; + } this.activeRangeCellElement.style.left = activeCell.row.getElement().offsetLeft + @@ -698,12 +723,25 @@ class Spreadsheet extends Module { var _vDomLeft = this.table.columnManager.renderer.leftCol; var _vDomRight = this.table.columnManager.renderer.rightCol; - if (_vDomTop == null) _vDomTop = 0; - if (_vDomBottom == null) _vDomBottom = Infinity; - if (_vDomLeft == null) _vDomLeft = 0; - if (_vDomRight == null) _vDomRight = Infinity; + if (_vDomTop == null) { + _vDomTop = 0; + } + + if (_vDomBottom == null) { + _vDomBottom = Infinity; + } + + if (_vDomLeft == null) { + _vDomLeft = 0; + } + + if (_vDomRight == null) { + _vDomRight = Infinity; + } - if (!range.overlaps(_vDomLeft, _vDomTop, _vDomRight, _vDomBottom)) return; + if (!range.overlaps(_vDomLeft, _vDomTop, _vDomRight, _vDomBottom)) { + return; + } var top = Math.max(range.top, _vDomTop); var bottom = Math.min(range.bottom, _vDomBottom); @@ -742,33 +780,42 @@ class Spreadsheet extends Module { findRangeByCellElement(cell) { var rangeIdx = cell.dataset.range; - if (rangeIdx < 0) return; + if (rangeIdx < 0) { + return; + } return this.ranges[rangeIdx].getComponent(); } getCell(rowIdx, colIdx) { if (rowIdx < 0) { - rowIdx = this.table.rowManager.getDisplayRows().length + rowIdx; + rowIdx = this.getRows().length + rowIdx; } var row = this.table.rowManager.getRowFromPosition(rowIdx + 1); - if (!row) return null; + if (!row) { + return null; + } if (colIdx < 0) { - colIdx = - this.table.columnManager.getVisibleColumnsByIndex().length + colIdx - 1; + colIdx = this.getColumns().length + colIdx - 1; } - if (colIdx < 0) return null; + if (colIdx < 0) { + return null; + } return row.getCells().filter((cell) => cell.column.visible)[colIdx + 1]; } getActiveRange(component) { const range = this.ranges[this.ranges.length - 1]; - if (!range) return null; - if (component) return range.getComponent(); + if (!range) { + return null; + } + if (component) { + return range.getComponent(); + } return range; } @@ -778,23 +825,38 @@ class Spreadsheet extends Module { } getRowByRangePos(pos) { - return this.table.rowManager.getDisplayRows()[pos]; + return this.getRows()[pos]; } getColumnByRangePos(pos) { - return this.table.columnManager.getVisibleColumnsByIndex()[pos + 1]; + return this.getColumns()[pos + 1]; } getRowsByRange(range) { - return this.table.rowManager - .getDisplayRows() - .slice(range.top, range.bottom + 1); + return this.getRows() .slice(range.top, range.bottom + 1); } getColumnsByRange(range) { - return this.table.columnManager - .getVisibleColumnsByIndex() - .slice(range.left + 1, range.right + 2); + return this.getColumns() .slice(range.left + 1, range.right + 2); + } + + getRows() { + return this.table.rowManager.getDisplayRows(); + } + + getColumns() { + return this.table.columnManager.getVisibleColumnsByIndex(); + } + + getTableViewBounds() { + var tableHolder = this.table.rowManager.element; + var rowHeader = this.rowHeaderColumn.getElement(); + return { + left: tableHolder.scrollLeft + rowHeader.offsetWidth, + right: tableHolder.scrollLeft + tableHolder.offsetWidth - rowHeader.offsetWidth - this.table.rowManager.scrollbarWidth, + top: tableHolder.scrollTop, + bottom: tableHolder.scrollTop + tableHolder.offsetHeight - this.table.rowManager.scrollbarWidth, + }; } addRange() { diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index ec2f2f4a5..f80e2a47b 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -1511,22 +1511,22 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ // Spreadsheet module styling .tabulator-header .tabulator-col{ - &.tabulator-highlight { + &.tabulator-spreadsheet-highlight{ background-color: $headerHighlightBackground; color: $headerTextHighlightBackground; } - &.tabulator-selected{ + &.tabulator-spreadsheet-selected{ background-color: $headerSelectedBackground; color: $headerSelectedTextColor; } } .tabulator-row{ - &.tabulator-highlight .tabulator-row-header{ + &.tabulator-spreadsheet-highlight .tabulator-spreadsheet-row-header{ background-color: $headerHighlightBackground; color: $headerTextHighlightBackground; } - &.tabulator-selected .tabulator-row-header{ + &.tabulator-spreadsheet-selected .tabulator-spreadsheet-row-header{ background-color: $headerSelectedBackground; color: $headerSelectedTextColor; } @@ -1536,20 +1536,18 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ user-select: none; position: relative; - &.tabulator-selected:not(.tabulator-only-cell-selected):not(.tabulator-row-header){ + &.tabulator-spreadsheet-selected:not(.tabulator-spreadsheet-only-cell-selected):not(.tabulator-spreadsheet-row-header){ background-color: $rowSelectedBackground; } - &.tabulator-row-header { + &.tabulator-spreadsheet-row-header { text-align: center; } } .tabulator-range-overlay { position: absolute; - left: 0; - top: 0; - overflow: hidden; + inset: 0; z-index: 10; pointer-events: none; From 4b6987bef17843bb7d82eca5a15aaf7b4d91423e Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 13 Nov 2023 15:55:19 +0700 Subject: [PATCH 25/48] incorrect navigate jump indexing, use scrollend, cleanups --- .../modules/Keybindings/defaults/actions.js | 44 ------------------- src/js/modules/Spreadsheet/Spreadsheet.js | 28 +++++++++--- 2 files changed, 21 insertions(+), 51 deletions(-) diff --git a/src/js/modules/Keybindings/defaults/actions.js b/src/js/modules/Keybindings/defaults/actions.js index 610a47e73..411d023bf 100644 --- a/src/js/modules/Keybindings/defaults/actions.js +++ b/src/js/modules/Keybindings/defaults/actions.js @@ -144,50 +144,6 @@ export default { this.table.modules.spreadsheet.navigate("expand-jump", "down"); }, - // navRightAlt: function(e){ - // this.dispatch("keybinding-nav-right-alt", e); - // }, - // - // navUpAlt: function(e){ - // this.dispatch("keybinding-nav-up-alt", e); - // }, - // - // navDownAlt: function(e){ - // this.dispatch("keybinding-nav-down-alt", e); - // }, - - // expandLeft:function(e){ - // this.dispatch("keybinding-expand-left", e); - // }, - // - // expandRight:function(e){ - // this.dispatch("keybinding-expand-right", e); - // }, - // - // expandUp:function(e){ - // this.dispatch("keybinding-expand-up", e); - // }, - // - // expandDown:function(e){ - // this.dispatch("keybinding-expand-down", e); - // }, - // - // expandLeftAlt: function(e){ - // this.dispatch("keybinding-expand-left-alt", e); - // }, - // - // expandRightAlt: function(e){ - // this.dispatch("keybinding-expand-right-alt", e); - // }, - // - // expandUpAlt: function(e){ - // this.dispatch("keybinding-expand-up-alt", e); - // }, - // - // expandDownAlt: function(e){ - // this.dispatch("keybinding-expand-down-alt", e); - // }, - undo:function(e){ var cell = false; if(this.table.options.history && this.table.modExists("history") && this.table.modExists("edit")){ diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index ae238c678..4772f4174 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -42,13 +42,25 @@ class Spreadsheet extends Module { this.subscribe("table-layout", this.layoutElement.bind(this)); var debouncedLayoutRanges = Helpers.debounce(this.layoutRanges.bind(this), 200); - function layoutRanges () { + function layoutRanges (debounced = true) { this.overlay.style.visibility = "hidden"; - debouncedLayoutRanges(); + if (debounced) { + debouncedLayoutRanges(); + } + } + + if ("onscrollend" in window) { + this.subscribe("scroll-vertical", layoutRanges.bind(this, false)); + this.subscribe("scroll-horizontal", layoutRanges.bind(this, false)); + this.table.rowManager.element.addEventListener("scrollend", this.layoutRanges.bind(this)); + this.subscribe("table-destroy", () => { + this.table.rowManager.element.removeEventListener("scrollend", this.layoutRanges.bind(this)); + }) + } else { + this.subscribe("scroll-vertical", layoutRanges.bind(this)); + this.subscribe("scroll-horizontal", layoutRanges.bind(this)); } - this.subscribe("scroll-vertical", layoutRanges.bind(this)); - this.subscribe("scroll-horizontal", layoutRanges.bind(this)); this.subscribe("column-width", layoutRanges.bind(this)); this.subscribe("column-height", layoutRanges.bind(this)); this.subscribe("column-resized", layoutRanges.bind(this)); @@ -555,8 +567,9 @@ class Spreadsheet extends Module { var column = this.getColumnByRangePos(colPos); var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); var jumpRow = 0; + var jumpRowIdx = cells.findIndex((cell) => cell.row.position - 1 === rowPos) - 1; - for (var i = rowPos - 1; i >= 0; i--){ + for (var i = jumpRowIdx; i >= 0; i--){ var cell = cells[i]; if (Helpers.elVisible(cell.getElement())) { jumpRow = cell.row.position - 1; @@ -570,9 +583,10 @@ class Spreadsheet extends Module { findJumpCellDown(rowPos, colPos) { var column = this.getColumnByRangePos(colPos); var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); - var jumpRow = -1; + var jumpRow = 0; + var jumpRowIdx = cells.findIndex((cell) => cell.row.position - 1 === rowPos) + 1; - for (var i = rowPos + 1; i < cells.length; i++){ + for (var i = jumpRowIdx; i < cells.length; i++){ var cell = cells[i]; if (Helpers.elVisible(cell.getElement())) { jumpRow = cell.row.position - 1; From a46eb1f5019a7c1125a281d311235a0284d04bf6 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 14 Nov 2023 16:49:46 +0700 Subject: [PATCH 26/48] fix autoscroll issues - horizontal navigation does not scroll the tableHolder in some cases - vertical navigation is super slow when autoscrolls causing bottleneck. Using `.scrollTop` directly instead of `.scrollToRow` is much faster. --- src/js/modules/Spreadsheet/Spreadsheet.js | 47 +++++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 4772f4174..651cf0e5f 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -55,16 +55,16 @@ class Spreadsheet extends Module { this.table.rowManager.element.addEventListener("scrollend", this.layoutRanges.bind(this)); this.subscribe("table-destroy", () => { this.table.rowManager.element.removeEventListener("scrollend", this.layoutRanges.bind(this)); - }) + }); } else { - this.subscribe("scroll-vertical", layoutRanges.bind(this)); - this.subscribe("scroll-horizontal", layoutRanges.bind(this)); + this.subscribe("scroll-vertical", layoutRanges.bind(this, true)); + this.subscribe("scroll-horizontal", layoutRanges.bind(this, true)); } - this.subscribe("column-width", layoutRanges.bind(this)); - this.subscribe("column-height", layoutRanges.bind(this)); - this.subscribe("column-resized", layoutRanges.bind(this)); - this.subscribe("cell-height", layoutRanges.bind(this)); + this.subscribe("column-width", layoutRanges.bind(this, true)); + this.subscribe("column-height", layoutRanges.bind(this, true)); + this.subscribe("column-resized", layoutRanges.bind(this, true)); + this.subscribe("cell-height", layoutRanges.bind(this, true)); var navigate = (mode, dir) => { var self = this; @@ -72,7 +72,7 @@ class Spreadsheet extends Module { e.preventDefault(); self.navigate(mode, dir); }; - } + }; this.subscribe("keybinding-nav-prev", navigate("normal", "left")); this.subscribe("keybinding-nav-next", navigate("normal", "right")); @@ -396,6 +396,8 @@ class Spreadsheet extends Module { var range = this.getActiveRange(); + var moved = false; + switch (mode) { case "normal": { let nextRow = range.start.row; @@ -411,6 +413,10 @@ class Spreadsheet extends Module { nextRow = Math.min(nextRow + 1, this.getRows().length - 1); } + if (nextCol !== range.start.col || nextRow !== range.start.row) { + moved = true; + } + range.setStart(nextRow, nextCol); range.setEnd(nextRow, nextCol); @@ -440,6 +446,10 @@ class Spreadsheet extends Module { nextRow = Math.min(nextRow + 1, this.getRows().length - 1); } + if (nextCol !== range.end.col || nextRow !== range.end.row) { + moved = true; + } + range.setEnd(nextRow, nextCol); break; @@ -458,6 +468,10 @@ class Spreadsheet extends Module { nextRow = this.findJumpCellDown(range.start.row, range.start.col); } + if (nextCol !== range.start.col || nextRow !== range.start.row) { + moved = true; + } + range.setStart(nextRow, nextCol); range.setEnd(nextRow, nextCol); @@ -479,12 +493,20 @@ class Spreadsheet extends Module { nextRow = this.findJumpCellDown(range.end.row, range.start.col); } + if (nextCol !== range.end.col || nextRow !== range.end.row) { + moved = true; + } + range.setEnd(nextRow, nextCol); break; } } + if (!moved) { + return; + } + this.autoScroll(range); this.layoutElement(); @@ -523,10 +545,11 @@ class Spreadsheet extends Module { } if (!fullyVisibileVertical) { + var tableHolder = this.table.rowManager.element; if (currentTop < viewBounds.top) { - this.table.scrollToRow(row, 'top'); + tableHolder.scrollTop = row.getElement().offsetTop; } else if (currentBottom > viewBounds.bottom) { - this.table.scrollToRow(row, 'bottom'); + tableHolder.scrollTop = row.getElement().offsetTop + row.getElement().offsetHeight - tableHolder.clientHeight; } } } @@ -647,7 +670,7 @@ class Spreadsheet extends Module { var rows; if (visibleRows) { - rows = this.table.rowManager.getVisibleRows(true) + rows = this.table.rowManager.getVisibleRows(true); } else { rows = this.table.rowManager.getRows(); } @@ -867,7 +890,7 @@ class Spreadsheet extends Module { var rowHeader = this.rowHeaderColumn.getElement(); return { left: tableHolder.scrollLeft + rowHeader.offsetWidth, - right: tableHolder.scrollLeft + tableHolder.offsetWidth - rowHeader.offsetWidth - this.table.rowManager.scrollbarWidth, + right: Math.ceil(tableHolder.scrollLeft + tableHolder.clientWidth), top: tableHolder.scrollTop, bottom: tableHolder.scrollTop + tableHolder.offsetHeight - this.table.rowManager.scrollbarWidth, }; From a445d295cd40ac0a2404a479c3a10af0b522c1c8 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 14 Nov 2023 17:06:24 +0700 Subject: [PATCH 27/48] refactors --- src/js/modules/Spreadsheet/Spreadsheet.js | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 651cf0e5f..b42164ff7 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -42,29 +42,30 @@ class Spreadsheet extends Module { this.subscribe("table-layout", this.layoutElement.bind(this)); var debouncedLayoutRanges = Helpers.debounce(this.layoutRanges.bind(this), 200); - function layoutRanges (debounced = true) { + var hideRanges = () => { this.overlay.style.visibility = "hidden"; - if (debounced) { - debouncedLayoutRanges(); - } + } + var layoutRanges = () => { + hideRanges(); + debouncedLayoutRanges(); } if ("onscrollend" in window) { - this.subscribe("scroll-vertical", layoutRanges.bind(this, false)); - this.subscribe("scroll-horizontal", layoutRanges.bind(this, false)); + this.subscribe("scroll-vertical", hideRanges); + this.subscribe("scroll-horizontal", hideRanges); this.table.rowManager.element.addEventListener("scrollend", this.layoutRanges.bind(this)); this.subscribe("table-destroy", () => { this.table.rowManager.element.removeEventListener("scrollend", this.layoutRanges.bind(this)); }); } else { - this.subscribe("scroll-vertical", layoutRanges.bind(this, true)); - this.subscribe("scroll-horizontal", layoutRanges.bind(this, true)); + this.subscribe("scroll-vertical", layoutRanges); + this.subscribe("scroll-horizontal", layoutRanges); } - this.subscribe("column-width", layoutRanges.bind(this, true)); - this.subscribe("column-height", layoutRanges.bind(this, true)); - this.subscribe("column-resized", layoutRanges.bind(this, true)); - this.subscribe("cell-height", layoutRanges.bind(this, true)); + this.subscribe("column-width", layoutRanges); + this.subscribe("column-height", layoutRanges); + this.subscribe("column-resized", layoutRanges); + this.subscribe("cell-height", layoutRanges); var navigate = (mode, dir) => { var self = this; From 30ba414251046b54bbf460252f5fe560f57f7b66 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 14 Nov 2023 17:20:00 +0700 Subject: [PATCH 28/48] remove scrollend handler after used --- src/js/modules/Spreadsheet/Spreadsheet.js | 27 ++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index b42164ff7..59bc272ed 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -42,21 +42,28 @@ class Spreadsheet extends Module { this.subscribe("table-layout", this.layoutElement.bind(this)); var debouncedLayoutRanges = Helpers.debounce(this.layoutRanges.bind(this), 200); - var hideRanges = () => { - this.overlay.style.visibility = "hidden"; - } var layoutRanges = () => { - hideRanges(); + this.overlay.style.visibility = "hidden"; debouncedLayoutRanges(); } if ("onscrollend" in window) { - this.subscribe("scroll-vertical", hideRanges); - this.subscribe("scroll-horizontal", hideRanges); - this.table.rowManager.element.addEventListener("scrollend", this.layoutRanges.bind(this)); - this.subscribe("table-destroy", () => { - this.table.rowManager.element.removeEventListener("scrollend", this.layoutRanges.bind(this)); - }); + var scrolling = false; + var handleScrollEnd = () => { + this.layoutRanges(); + this.table.rowManager.element.removeEventListener("scrollend", handleScrollEnd); + scrolling = false; + } + var handleScroll = () => { + this.overlay.style.visibility = "hidden"; + if (scrolling) { + return; + } + scrolling = true; + this.table.rowManager.element.addEventListener("scrollend", handleScrollEnd); + } + this.subscribe("scroll-vertical", handleScroll); + this.subscribe("scroll-horizontal", handleScroll); } else { this.subscribe("scroll-vertical", layoutRanges); this.subscribe("scroll-horizontal", layoutRanges); From 2428229d2c92d9537672f63beaed1f84f550f6ba Mon Sep 17 00:00:00 2001 From: azmy60 Date: Wed, 15 Nov 2023 14:59:06 +0700 Subject: [PATCH 29/48] refactors using .scrollLeft directly instead of scrollHorizontal for faster scrolling and simplify stuff --- src/js/core/tools/Helpers.js | 4 -- src/js/modules/Spreadsheet/Spreadsheet.js | 87 +++++++++++------------ 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/src/js/core/tools/Helpers.js b/src/js/core/tools/Helpers.js index 27558207e..ce368f1bf 100644 --- a/src/js/core/tools/Helpers.js +++ b/src/js/core/tools/Helpers.js @@ -58,8 +58,4 @@ export default class Helpers{ }, delay); }; } - - static clamp(number, min, max) { - return Math.max(min, Math.min(number, max)); - } } diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 59bc272ed..64fbffe63 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -521,43 +521,53 @@ class Spreadsheet extends Module { } autoScroll(range) { - var row = this.getRowByRangePos(range.end.row); + var tableHolder = this.table.rowManager.element; + var rowHeader = this.rowHeaderColumn.getElement(); + var row = this.getRowByRangePos(range.end.row).getElement(); var column = this.getColumnByRangePos(range.end.col).getElement(); - var currentLeft = column.offsetLeft; - var currentRight = column.offsetLeft + column.offsetWidth; - var currentTop = row.getElement().offsetTop; - var currentBottom = currentTop + row.getElement().offsetHeight; - var viewBounds = this.getTableViewBounds(); - - var fullyVisibleHorizontal = viewBounds.left < currentLeft - && currentLeft < viewBounds.right - && viewBounds.left < currentRight - && currentRight < viewBounds.right; - - var fullyVisibileVertical = viewBounds.top < currentTop - && currentTop < viewBounds.bottom - && viewBounds.top < currentBottom - && currentBottom < viewBounds.bottom; - - if (!fullyVisibleHorizontal) { - if (currentLeft < viewBounds.left) { - this.table.rowManager.scrollHorizontal(this.table.rowManager.element.scrollLeft - column.offsetWidth); - } else if (currentRight > viewBounds.right) { - this.table.rowManager.scrollHorizontal( - Math.min( - column.offsetLeft - this.rowHeaderColumn.getElement().offsetWidth, - this.table.rowManager.element.scrollLeft + column.offsetWidth - ) - ); + + var rect = { + left: column.offsetLeft, + right: column.offsetLeft + column.offsetWidth, + top: row.offsetTop, + bottom: row.offsetTop + row.offsetHeight, + }; + + var view = { + left: tableHolder.scrollLeft + rowHeader.offsetWidth, + right: Math.ceil(tableHolder.scrollLeft + tableHolder.clientWidth), + top: tableHolder.scrollTop, + bottom: + tableHolder.scrollTop + + tableHolder.offsetHeight - + this.table.rowManager.scrollbarWidth, + }; + + var withinHorizontalView = + view.left < rect.left && + rect.left < view.right && + view.left < rect.right && + rect.right < view.right; + + var withinVerticalView = + view.top < rect.top && + rect.top < view.bottom && + view.top < rect.bottom && + rect.bottom < view.bottom; + + if (!withinHorizontalView) { + if (rect.left < view.left) { + tableHolder.scrollLeft = rect.left - rowHeader.offsetWidth; + } else if (rect.right > view.right) { + tableHolder.scrollLeft = rect.right - tableHolder.clientWidth; } } - if (!fullyVisibileVertical) { - var tableHolder = this.table.rowManager.element; - if (currentTop < viewBounds.top) { - tableHolder.scrollTop = row.getElement().offsetTop; - } else if (currentBottom > viewBounds.bottom) { - tableHolder.scrollTop = row.getElement().offsetTop + row.getElement().offsetHeight - tableHolder.clientHeight; + if (!withinVerticalView) { + if (rect.top < view.top) { + tableHolder.scrollTop = rect.top; + } else if (rect.bottom > view.bottom) { + tableHolder.scrollTop = rect.bottom - tableHolder.clientHeight; } } } @@ -893,17 +903,6 @@ class Spreadsheet extends Module { return this.table.columnManager.getVisibleColumnsByIndex(); } - getTableViewBounds() { - var tableHolder = this.table.rowManager.element; - var rowHeader = this.rowHeaderColumn.getElement(); - return { - left: tableHolder.scrollLeft + rowHeader.offsetWidth, - right: Math.ceil(tableHolder.scrollLeft + tableHolder.clientWidth), - top: tableHolder.scrollTop, - bottom: tableHolder.scrollTop + tableHolder.offsetHeight - this.table.rowManager.scrollbarWidth, - }; - } - addRange() { var element = document.createElement("div"); element.classList.add("tabulator-range"); From d417c18b6cc6192932d09910c508397bf0606c7b Mon Sep 17 00:00:00 2001 From: azmy60 Date: Wed, 15 Nov 2023 16:50:10 +0700 Subject: [PATCH 30/48] fix reset selection after changing page --- src/js/modules/Spreadsheet/Spreadsheet.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 64fbffe63..e63b841fd 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -829,6 +829,7 @@ class Spreadsheet extends Module { } handlePageChanged() { + this.selecting = 'cell'; this.resetRanges(); this.layoutElement(); } From 4db39adfbb4e17d86f207c1250a95b12a6934fa7 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Thu, 16 Nov 2023 19:27:55 +0700 Subject: [PATCH 31/48] prevent navigation when editing, refocus to table after editing, etc. - navigation keybindings should respect editing mode - refocus to table after editing the cell, so we can navigate to it - allow opening context menu via keyboard shortcut --- .../modules/Keybindings/defaults/actions.js | 63 ++++++++++++------- .../modules/Keybindings/defaults/bindings.js | 1 + src/js/modules/Menu/Menu.js | 6 +- src/js/modules/Spreadsheet/Spreadsheet.js | 47 ++++++++++++-- 4 files changed, 88 insertions(+), 29 deletions(-) diff --git a/src/js/modules/Keybindings/defaults/actions.js b/src/js/modules/Keybindings/defaults/actions.js index 411d023bf..a6cc7ed75 100644 --- a/src/js/modules/Keybindings/defaults/actions.js +++ b/src/js/modules/Keybindings/defaults/actions.js @@ -85,63 +85,80 @@ export default { spreadsheetJumpLeft: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("jump", "left"); + if (this.table.modules.spreadsheet.navigate("jump", "left")) { + e.preventDefault(); + } }, spreadsheetJumpRight: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("jump", "right"); + if (this.table.modules.spreadsheet.navigate("jump", "right")) { + e.preventDefault(); + } }, spreadsheetJumpUp: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("jump", "up"); + if (this.table.modules.spreadsheet.navigate("jump", "up")) { + e.preventDefault(); + } }, spreadsheetJumpDown: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("jump", "down"); + if (this.table.modules.spreadsheet.navigate("jump", "down")) { + e.preventDefault(); + } }, spreadsheetExpandLeft: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("expand", "left"); + if (this.table.modules.spreadsheet.navigate("expand", "left")) { + e.preventDefault(); + } }, spreadsheetExpandRight: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("expand", "right"); + if (this.table.modules.spreadsheet.navigate("expand", "right")) { + e.preventDefault(); + } }, spreadsheetExpandUp: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("expand", "up"); + if (this.table.modules.spreadsheet.navigate("expand", "up")) { + e.preventDefault(); + } }, spreadsheetExpandDown: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("expand", "down"); + if (this.table.modules.spreadsheet.navigate("expand", "down")) { + e.preventDefault(); + } }, spreadsheetExpandJumpLeft: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("expand-jump", "left"); + if (this.table.modules.spreadsheet.navigate("expand-jump", "left")) { + e.preventDefault(); + } }, spreadsheetExpandJumpRight: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("expand-jump", "right"); + if (this.table.modules.spreadsheet.navigate("expand-jump", "right")) { + e.preventDefault(); + } }, spreadsheetExpandJumpUp: function(e){ if (!this.table.options.spreadsheet) return; - e.preventDefault(); - this.table.modules.spreadsheet.navigate("expand-jump", "up"); + if (this.table.modules.spreadsheet.navigate("expand-jump", "up")) { + e.preventDefault(); + } }, spreadsheetExpandJumpDown: function(e){ if (!this.table.options.spreadsheet) return; + if (this.table.modules.spreadsheet.navigate("expand-jump", "down")) { + e.preventDefault(); + } + }, + spreadsheetEditCell: function(e){ + if (!this.table.options.spreadsheet) return; + this.table.modules.spreadsheet.editActiveCell(); e.preventDefault(); - this.table.modules.spreadsheet.navigate("expand-jump", "down"); }, undo:function(e){ diff --git a/src/js/modules/Keybindings/defaults/bindings.js b/src/js/modules/Keybindings/defaults/bindings.js index d54c8f1aa..a8f4da955 100644 --- a/src/js/modules/Keybindings/defaults/bindings.js +++ b/src/js/modules/Keybindings/defaults/bindings.js @@ -25,4 +25,5 @@ export default { spreadsheetExpandJumpDown:["ctrl + shift + 40", "meta + shift + 40"], spreadsheetExpandJumpLeft:["ctrl + shift + 37", "meta + shift + 37"], spreadsheetExpandJumpRight:["ctrl + shift + 39", "meta + shift + 39"], + spreadsheetEditCell:13, }; diff --git a/src/js/modules/Menu/Menu.js b/src/js/modules/Menu/Menu.js index 2081ab327..241ea519e 100644 --- a/src/js/modules/Menu/Menu.js +++ b/src/js/modules/Menu/Menu.js @@ -217,7 +217,11 @@ class Menu extends Module{ this.rootPopup = popup = this.popup(menuEl); }else{ - popup = parentPopup.child(menuEl); + if (parentPopup) { + popup = parentPopup.child(menuEl); + } else { + this.rootPopup = popup = this.popup(menuEl); + } } menu.forEach((item) => { diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index e63b841fd..b9d3873fb 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -45,7 +45,7 @@ class Spreadsheet extends Module { var layoutRanges = () => { this.overlay.style.visibility = "hidden"; debouncedLayoutRanges(); - } + }; if ("onscrollend" in window) { var scrolling = false; @@ -53,7 +53,7 @@ class Spreadsheet extends Module { this.layoutRanges(); this.table.rowManager.element.removeEventListener("scrollend", handleScrollEnd); scrolling = false; - } + }; var handleScroll = () => { this.overlay.style.visibility = "hidden"; if (scrolling) { @@ -61,7 +61,7 @@ class Spreadsheet extends Module { } scrolling = true; this.table.rowManager.element.addEventListener("scrollend", handleScrollEnd); - } + }; this.subscribe("scroll-vertical", handleScroll); this.subscribe("scroll-horizontal", handleScroll); } else { @@ -77,8 +77,9 @@ class Spreadsheet extends Module { var navigate = (mode, dir) => { var self = this; return function (e) { - e.preventDefault(); - self.navigate(mode, dir); + if(self.navigate(mode, dir)) { + e.preventDefault(); + } }; }; @@ -100,6 +101,7 @@ class Spreadsheet extends Module { // Edit on double click var editable = column.editable !== undefined ? column.editable : true; + // TODO: use column init event, and then assign column.modules column.__spreadsheet_editable = editable; column.editable = false; @@ -162,6 +164,29 @@ class Spreadsheet extends Module { this.table.rowManager.element.appendChild(this.overlay); this.table.columnManager.element.setAttribute("tabindex", 0); + + var pressingContextMenu = false; + + this.table.rowManager.element.addEventListener("keyup", (e) => { + if (e.key === 'ContextMenu') { + pressingContextMenu = true; + } + }); + + this.table.rowManager.element.addEventListener("contextmenu", (e) => { + if (!pressingContextMenu) return; + + var activeCell = this.getActiveCell(); + var menuDef = activeCell.column.definition.contextMenu; + var menu = typeof menuDef === "function" ? menuDef.call(this.table, e, activeCell.getComponent()) : menuDef; + + if (this.table.modules.menu && menu) { + this.table.modules.menu.loadMenu(e, activeCell, menu, activeCell.element); + } + + pressingContextMenu = false; + e.preventDefault(); + }); } initializeFunctions() { @@ -373,6 +398,10 @@ class Spreadsheet extends Module { this.editCell(cell); } + editActiveCell() { + this.editCell(this.getActiveCell()); + } + editCell(cell) { cell.column.definition.editable = cell.column.definition.__spreadsheet_editable; @@ -388,9 +417,15 @@ class Spreadsheet extends Module { finishEditingCell(cell) { cell.column.definition.editable = false; cell.column.definition.editor = false; + this.table.rowManager.element.focus(); } navigate(mode, dir) { + // Don't navigate while editing + if (this.table.modules.edit && this.table.modules.edit.currentCell) { + return false; + } + if (this.ranges.length > 1) { this.ranges = this.ranges.filter((range) => { if (range === this.getActiveRange()) { @@ -518,6 +553,8 @@ class Spreadsheet extends Module { this.autoScroll(range); this.layoutElement(); + + return true; } autoScroll(range) { From f71e71f5cbd15216009012e6ecbf08f835e1f78d Mon Sep 17 00:00:00 2001 From: azmy60 Date: Fri, 17 Nov 2023 17:09:33 +0700 Subject: [PATCH 32/48] trap focus contextmenu, navigate menu with arrow keys and enter we need this so when a menu is opened, we shouldn't be able to navigate the spreadsheet with the arrow keys. --- src/js/modules/Menu/Menu.js | 2 + src/js/modules/Spreadsheet/Spreadsheet.js | 218 +++++++++++++++++++--- src/scss/tabulator.scss | 4 + 3 files changed, 202 insertions(+), 22 deletions(-) diff --git a/src/js/modules/Menu/Menu.js b/src/js/modules/Menu/Menu.js index 241ea519e..e242aa7bf 100644 --- a/src/js/modules/Menu/Menu.js +++ b/src/js/modules/Menu/Menu.js @@ -289,6 +289,7 @@ class Menu extends Module{ this.rootPopup = null; if(this.currentComponent){ + this.dispatch("menu-closed"); this.dispatchExternal("menuClosed", this.currentComponent.getComponent()); this.currentComponent = null; } @@ -296,6 +297,7 @@ class Menu extends Module{ this.currentComponent = component; + this.dispatch("menu-opened", menu, popup); this.dispatchExternal("menuOpened", component.getComponent()); } } diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index b9d3873fb..9fa563895 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -27,6 +27,9 @@ class Spreadsheet extends Module { this.initializeTable(); this.initializeWatchers(); this.initializeFunctions(); + if (this.table.modules.menu) { + this.initializeMenuNavigation(); + } } initializeWatchers() { @@ -165,28 +168,6 @@ class Spreadsheet extends Module { this.table.rowManager.element.appendChild(this.overlay); this.table.columnManager.element.setAttribute("tabindex", 0); - var pressingContextMenu = false; - - this.table.rowManager.element.addEventListener("keyup", (e) => { - if (e.key === 'ContextMenu') { - pressingContextMenu = true; - } - }); - - this.table.rowManager.element.addEventListener("contextmenu", (e) => { - if (!pressingContextMenu) return; - - var activeCell = this.getActiveCell(); - var menuDef = activeCell.column.definition.contextMenu; - var menu = typeof menuDef === "function" ? menuDef.call(this.table, e, activeCell.getComponent()) : menuDef; - - if (this.table.modules.menu && menu) { - this.table.modules.menu.loadMenu(e, activeCell, menu, activeCell.element); - } - - pressingContextMenu = false; - e.preventDefault(); - }); } initializeFunctions() { @@ -229,6 +210,194 @@ class Spreadsheet extends Module { return range.getComponent(); }); } + + initializeMenuNavigation() { + var self = this; + var pressingContextMenu = false; + + function contextMenuKeyCheck(e) { + if (e.key === "ContextMenu") { + pressingContextMenu = true; + } + } + + function handleContextMenu(e) { + if (!pressingContextMenu) return; + + var activeCell = self.getActiveCell(); + var menuDef = activeCell.column.definition.contextMenu; + var menu = + typeof menuDef === "function" + ? menuDef.call(self.table, e, activeCell.getComponent()) + : menuDef; + + self.table.modules.menu.loadMenu(e, activeCell, menu, activeCell.element); + + pressingContextMenu = false; + e.preventDefault(); + } + + this.table.rowManager.element.addEventListener( + "keyup", + contextMenuKeyCheck, + ); + this.table.rowManager.element.addEventListener( + "contextmenu", + handleContextMenu, + ); + + this.subscribe("table-destroy", () => { + this.table.rowManager.element.removeEventListener( + "keyup", + contextMenuKeyCheck, + ); + this.table.rowManager.element.removeEventListener( + "contextmenu", + handleContextMenu, + ); + }); + + this.subscribe("menu-opened", (menu, popup) => { + var stack = [createState(menu, popup)]; + + function createState(menu, popup) { + var elements = popup.element.querySelectorAll(".tabulator-menu-item"); + var focusIdx = 0; + + function handleMouseOver(e) { + focusIdx = Array.prototype.indexOf.call(elements, e.target); + draw(); + } + + // We do this because the menu items use user-select: none; + function preventLosingFocus(e) { + e.preventDefault(); + } + + elements.forEach((element) => { + element.addEventListener("mouseover", handleMouseOver); + element.addEventListener("mousedown", preventLosingFocus); + }); + + return { + menu, + popup, + elements, + get focusIdx() { + return focusIdx; + }, + set focusIdx(value) { + if (value < 0) { + value = 0; + } else if (value >= elements.length) { + value = elements.length - 1; + } + focusIdx = value; + }, + get focusedMenu() { + return this.menu[focusIdx]; + }, + get focusedElement() { + return this.elements[focusIdx]; + }, + destroy() { + this.popup.hide(true); + this.elements.forEach((element) => { + element.removeEventListener("mouseover", handleMouseOver); + element.removeEventListener("mousedown", preventLosingFocus); + }); + }, + }; + } + + function navigate(nav) { + var state = stack[stack.length - 1]; + switch (nav) { + case "up": + state.focusIdx--; + break; + case "down": + state.focusIdx++; + break; + case "left": + if (stack.length > 1) { + stack.pop().destroy(); + } + break; + case "right": + if (state.focusedMenu.menu && state.focusedMenu.menu.length) { + state.focusedElement.click(); + var nextState = createState( + state.focusedMenu.menu, + state.popup.childPopup, + ); + stack.push(nextState); + } + break; + } + draw(); + } + + function draw() { + var state = stack[stack.length - 1]; + state.elements.forEach((element) => { + var selected = element === state.focusedElement; + element.classList.toggle( + "tabulator-spreadsheet-menu-item-focused", + selected, + ); + }); + } + + function handleUp(e) { + e.preventDefault(); + navigate("up"); + } + + function handleDown(e) { + e.preventDefault(); + navigate("down"); + } + + function handleLeft(e) { + e.preventDefault(); + navigate("left"); + } + + function handleRight(e) { + e.preventDefault(); + navigate("right"); + } + + function handleKeyUp(e) { + if (e.key === "Enter") { + var state = stack[stack.length - 1]; + state.focusedElement.click(); + } + } + + function subscribeListeners() { + self.subscribe("keybinding-nav-up", handleUp); + self.subscribe("keybinding-nav-down", handleDown); + self.subscribe("keybinding-nav-left", handleLeft); + self.subscribe("keybinding-nav-right", handleRight); + // TODO use keybinding module + self.table.element.addEventListener("keyup", handleKeyUp); + } + + function unsubscribeListeners() { + self.unsubscribe("keybinding-nav-up", handleUp); + self.unsubscribe("keybinding-nav-down", handleDown); + self.unsubscribe("keybinding-nav-left", handleLeft); + self.unsubscribe("keybinding-nav-right", handleRight); + self.table.element.removeEventListener("keyup", handleKeyUp); + } + + this.subscribe("menu-closed", unsubscribeListeners); + subscribeListeners(); + draw(); + }); + } getSelectedData() { return this.getDataByRange(this.getActiveRange()); @@ -426,6 +595,11 @@ class Spreadsheet extends Module { return false; } + // Don't navigate while a menu is open + if (this.table.modules.menu && this.table.modules.menu.currentComponent) { + return false; + } + if (this.ranges.length > 1) { this.ranges = this.ranges.filter((range) => { if (range === this.getActiveRange()) { diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index f80e2a47b..4c0e530e4 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -1574,3 +1574,7 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ border: 2px solid $rangeBorderColor; } } + +.tabulator-spreadsheet-menu-item-focused { + background-color: $rowAltBackgroundColor; +} From 43b2960a62718f4c419897a31233b9c0297a5702 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Fri, 17 Nov 2023 17:21:46 +0700 Subject: [PATCH 33/48] fix dangling listener & prevent editing when menu is opened --- src/js/modules/Keybindings/defaults/actions.js | 3 +++ src/js/modules/Spreadsheet/Spreadsheet.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/js/modules/Keybindings/defaults/actions.js b/src/js/modules/Keybindings/defaults/actions.js index a6cc7ed75..50c631af7 100644 --- a/src/js/modules/Keybindings/defaults/actions.js +++ b/src/js/modules/Keybindings/defaults/actions.js @@ -157,6 +157,9 @@ export default { }, spreadsheetEditCell: function(e){ if (!this.table.options.spreadsheet) return; + if (this.table.modules.menu && this.table.modules.menu.currentComponent) { + return; + } this.table.modules.spreadsheet.editActiveCell(); e.preventDefault(); }, diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 9fa563895..61c824d2c 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -386,6 +386,7 @@ class Spreadsheet extends Module { } function unsubscribeListeners() { + self.unsubscribe("menu-closed", unsubscribeListeners); self.unsubscribe("keybinding-nav-up", handleUp); self.unsubscribe("keybinding-nav-down", handleDown); self.unsubscribe("keybinding-nav-left", handleLeft); From 91f57b4c52554ddc2e423a3978cc57d186ca1cac Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 4 Dec 2023 17:55:22 +0700 Subject: [PATCH 34/48] fix(navigation): cell navigation improvements - if the range is at the edge of the table and the user presses `spreadsheetJump` key, it should stay there instead of moving to the other side of the table. - pressing `spreadsheetJump` should move to the next non-empty cell, just like in google sheet. --- src/js/modules/Spreadsheet/Spreadsheet.js | 165 ++++++++++++++++++---- 1 file changed, 138 insertions(+), 27 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 61c824d2c..e548d92c5 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -601,6 +601,7 @@ class Spreadsheet extends Module { return false; } + // If there are more than 1 range, use the active range and destroy the others if (this.ranges.length > 1) { this.ranges = this.ranges.filter((range) => { if (range === this.getActiveRange()) { @@ -787,13 +788,42 @@ class Spreadsheet extends Module { findJumpCellLeft(rowPos, colPos){ var row = this.getRowByRangePos(rowPos); var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); - var jumpCol = 0; + var isStartingCellEmpty = !cells[colPos + 1].getValue(); + var isLeftOfStartingCellEmpty = !cells[colPos]?.getValue(); + var jumpCol = colPos; - for (var i = colPos; i >= 0; i--){ - var cell = cells[i]; - if (Helpers.elVisible(cell.getElement()) && cell.column.field !== this.rowHeaderField) { - jumpCol = cell.column.position - 2; - if (cell.getValue()) break; + // Go until we find an empty / non-empty cell. + for (var i = colPos; i > 0; i--){ + var currentCell = cells[i]; + if (isStartingCellEmpty) { + if (!currentCell.getValue()) { + continue; + } + + if (currentCell.getValue()) { + jumpCol = currentCell.column.position - 2; + break; + } + } else { + if (!isLeftOfStartingCellEmpty && !currentCell.getValue()) { + break; + } + + jumpCol = currentCell.column.position - 2; + + if (isLeftOfStartingCellEmpty) { + if (!currentCell.getValue()) { + continue; + } + if (currentCell.getValue()) { + break; + } + } + + jumpCol = currentCell.column.position - 2; + if (currentCell.getValue()) { + continue; + } } } @@ -803,13 +833,40 @@ class Spreadsheet extends Module { findJumpCellRight(rowPos, colPos){ var row = this.getRowByRangePos(rowPos); var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); - var jumpCol = 0; + var isStartingCellEmpty = !cells[colPos + 1].getValue(); + var isRightOfStartingCellEmpty = !cells[colPos + 2]?.getValue(); + var jumpCol = colPos; for (var i = colPos + 2; i < cells.length; i++){ - var cell = cells[i]; - if (Helpers.elVisible(cell.getElement())) { - jumpCol = cell.column.position - 2; - if (cell.getValue()) break; + var currentCell = cells[i]; + if (isStartingCellEmpty) { + if (!currentCell.getValue()) { + continue; + } + + if (currentCell.getValue()) { + jumpCol = currentCell.column.position - 2; + break; + } + } else { + if (!isRightOfStartingCellEmpty && !currentCell.getValue()) { + break; + } + + jumpCol = currentCell.column.position - 2; + + if (isRightOfStartingCellEmpty) { + if (!currentCell.getValue()) { + continue; + } + if (currentCell.getValue()) { + break; + } + } + + if (currentCell.getValue()) { + continue; + } } } @@ -819,14 +876,41 @@ class Spreadsheet extends Module { findJumpCellUp(rowPos, colPos) { var column = this.getColumnByRangePos(colPos); var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); - var jumpRow = 0; - var jumpRowIdx = cells.findIndex((cell) => cell.row.position - 1 === rowPos) - 1; - - for (var i = jumpRowIdx; i >= 0; i--){ - var cell = cells[i]; - if (Helpers.elVisible(cell.getElement())) { - jumpRow = cell.row.position - 1; - if (cell.getValue()) break; + var isStartingCellEmpty = !cells[rowPos].getValue(); + var isTopOfStartingCellEmpty = !cells[rowPos - 1]?.getValue(); + var jumpRow = rowPos; + + for (var i = jumpRow - 1; i >= 0; i--){ + var currentCell = cells[i]; + if (isStartingCellEmpty) { + jumpRow = currentCell.row.position - 1; + + if (currentCell.getValue()) { + break; + } + + if (!currentCell.getValue()) { + continue; + } + } else { + if (!isTopOfStartingCellEmpty && !currentCell.getValue()) { + break; + } + + jumpRow = currentCell.row.position - 1; + + if (isTopOfStartingCellEmpty) { + if (!currentCell.getValue()) { + continue; + } + if (currentCell.getValue()) { + break; + } + } + + if (currentCell.getValue()) { + continue; + } } } @@ -836,14 +920,41 @@ class Spreadsheet extends Module { findJumpCellDown(rowPos, colPos) { var column = this.getColumnByRangePos(colPos); var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); - var jumpRow = 0; - var jumpRowIdx = cells.findIndex((cell) => cell.row.position - 1 === rowPos) + 1; - - for (var i = jumpRowIdx; i < cells.length; i++){ - var cell = cells[i]; - if (Helpers.elVisible(cell.getElement())) { - jumpRow = cell.row.position - 1; - if (cell.getValue()) break; + var isStartingCellEmpty = !cells[rowPos].getValue(); + var isBottomOfStartingCellEmpty = !cells[rowPos + 1]?.getValue(); + var jumpRow = rowPos; + + for (var i = jumpRow + 1; i < cells.length; i++){ + var currentCell = cells[i]; + if (isStartingCellEmpty) { + jumpRow = currentCell.row.position - 1; + + if (currentCell.getValue()) { + break; + } + + if (!currentCell.getValue()) { + continue; + } + } else { + if (!isBottomOfStartingCellEmpty && !currentCell.getValue()) { + break; + } + + jumpRow = currentCell.row.position - 1; + + if (isBottomOfStartingCellEmpty) { + if (!currentCell.getValue()) { + continue; + } + if (currentCell.getValue()) { + break; + } + } + + if (currentCell.getValue()) { + continue; + } } } From 8520052eaf1ef2c29b9630bb054f6ec548c54a47 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 4 Dec 2023 18:24:26 +0700 Subject: [PATCH 35/48] refactor(conditioning): dont use optional chanining --- src/js/modules/Spreadsheet/Spreadsheet.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index e548d92c5..f75969f6d 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -789,7 +789,7 @@ class Spreadsheet extends Module { var row = this.getRowByRangePos(rowPos); var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); var isStartingCellEmpty = !cells[colPos + 1].getValue(); - var isLeftOfStartingCellEmpty = !cells[colPos]?.getValue(); + var isLeftOfStartingCellEmpty = cells[colPos] ? !cells[colPos].getValue() : false; var jumpCol = colPos; // Go until we find an empty / non-empty cell. @@ -834,7 +834,7 @@ class Spreadsheet extends Module { var row = this.getRowByRangePos(rowPos); var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); var isStartingCellEmpty = !cells[colPos + 1].getValue(); - var isRightOfStartingCellEmpty = !cells[colPos + 2]?.getValue(); + var isRightOfStartingCellEmpty = cells[colPos + 2] ? !cells[colPos + 2].getValue() : false; var jumpCol = colPos; for (var i = colPos + 2; i < cells.length; i++){ @@ -877,7 +877,7 @@ class Spreadsheet extends Module { var column = this.getColumnByRangePos(colPos); var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); var isStartingCellEmpty = !cells[rowPos].getValue(); - var isTopOfStartingCellEmpty = !cells[rowPos - 1]?.getValue(); + var isTopOfStartingCellEmpty = cells[rowPos - 1] ? !cells[rowPos - 1].getValue() : false; var jumpRow = rowPos; for (var i = jumpRow - 1; i >= 0; i--){ @@ -921,7 +921,7 @@ class Spreadsheet extends Module { var column = this.getColumnByRangePos(colPos); var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); var isStartingCellEmpty = !cells[rowPos].getValue(); - var isBottomOfStartingCellEmpty = !cells[rowPos + 1]?.getValue(); + var isBottomOfStartingCellEmpty = cells[rowPos + 1] ? !cells[rowPos + 1].getValue() : false; var jumpRow = rowPos; for (var i = jumpRow + 1; i < cells.length; i++){ From 96582a6530ae2ecba1a70bf62638f58ce08035d5 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Mon, 4 Dec 2023 19:16:32 +0700 Subject: [PATCH 36/48] fix(autoScroll): fix auto scrolling issue fix cannot autoscroll to elements that are not in the DOM --- src/js/modules/Spreadsheet/Spreadsheet.js | 30 +++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index f75969f6d..3507a33a5 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -726,18 +726,32 @@ class Spreadsheet extends Module { return; } - this.autoScroll(range); + var row = this.getRowByRangePos(range.end.row); + var column = this.getColumnByRangePos(range.end.col); + + if ((dir === 'left' || dir === 'right') && column.getElement().parentNode === null) { + column.getComponent().scrollTo(undefined, false); + } else if ((dir === 'up' || dir === 'down') && row.getElement().parentNode === null) { + row.getComponent().scrollTo(undefined, false); + } else { + // Use faster autoScroll when the elements are on the DOM + this.autoScroll(range, row.getElement(), column.getElement()); + } this.layoutElement(); return true; } - autoScroll(range) { + autoScroll(range, row, column) { var tableHolder = this.table.rowManager.element; var rowHeader = this.rowHeaderColumn.getElement(); - var row = this.getRowByRangePos(range.end.row).getElement(); - var column = this.getColumnByRangePos(range.end.col).getElement(); + if (typeof row === 'undefined') { + row = this.getRowByRangePos(range.end.row).getElement(); + } + if (typeof column === 'undefined') { + column = this.getColumnByRangePos(range.end.col).getElement(); + } var rect = { left: column.offsetLeft, @@ -787,7 +801,7 @@ class Spreadsheet extends Module { findJumpCellLeft(rowPos, colPos){ var row = this.getRowByRangePos(rowPos); - var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); + var cells = row.cells.filter((cell) => cell.column.visible); var isStartingCellEmpty = !cells[colPos + 1].getValue(); var isLeftOfStartingCellEmpty = cells[colPos] ? !cells[colPos].getValue() : false; var jumpCol = colPos; @@ -832,7 +846,7 @@ class Spreadsheet extends Module { findJumpCellRight(rowPos, colPos){ var row = this.getRowByRangePos(rowPos); - var cells = row.cells.filter((cell) => Helpers.elVisible(cell.getElement())); + var cells = row.cells.filter((cell) => cell.column.visible); var isStartingCellEmpty = !cells[colPos + 1].getValue(); var isRightOfStartingCellEmpty = cells[colPos + 2] ? !cells[colPos + 2].getValue() : false; var jumpCol = colPos; @@ -875,7 +889,7 @@ class Spreadsheet extends Module { findJumpCellUp(rowPos, colPos) { var column = this.getColumnByRangePos(colPos); - var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); + var cells = column.cells.filter((cell) => this.table.rowManager.activeRows.includes(cell.row)); var isStartingCellEmpty = !cells[rowPos].getValue(); var isTopOfStartingCellEmpty = cells[rowPos - 1] ? !cells[rowPos - 1].getValue() : false; var jumpRow = rowPos; @@ -919,7 +933,7 @@ class Spreadsheet extends Module { findJumpCellDown(rowPos, colPos) { var column = this.getColumnByRangePos(colPos); - var cells = column.cells.filter((cell) => Helpers.elVisible(cell.getElement())); + var cells = column.cells.filter((cell) => this.table.rowManager.activeRows.includes(cell.row)); var isStartingCellEmpty = !cells[rowPos].getValue(); var isBottomOfStartingCellEmpty = cells[rowPos + 1] ? !cells[rowPos + 1].getValue() : false; var jumpRow = rowPos; From 5ebf3903a8e38ca203b3971411807e80ac75b527 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sun, 17 Dec 2023 06:36:18 +0700 Subject: [PATCH 37/48] spreadsheet keybinding does not respect editor Pressing enter in an input editor does not close it due to the enter key is registered in the keybindings module. --- src/js/modules/Edit/Edit.js | 17 +++- .../modules/Keybindings/defaults/actions.js | 8 -- .../modules/Keybindings/defaults/bindings.js | 1 - src/js/modules/Spreadsheet/Spreadsheet.js | 77 ++++++++++++------- 4 files changed, 61 insertions(+), 42 deletions(-) diff --git a/src/js/modules/Edit/Edit.js b/src/js/modules/Edit/Edit.js index 95f55ef4a..136ad883d 100644 --- a/src/js/modules/Edit/Edit.js +++ b/src/js/modules/Edit/Edit.js @@ -13,6 +13,7 @@ class Edit extends Module{ this.recursionBlock = false; //prevent focus recursion this.invalidEdit = false; this.editedCells = []; + this.elementToFocusOnBlur = false; this.editors = Edit.editors; @@ -680,26 +681,34 @@ class Edit extends Module{ } }else{ console.warn("Edit Error - Editor should return an instance of Node, the editor returned:", cellEditor); - element.blur(); + this.blur(cell); return false; } }else{ - element.blur(); + this.blur(cell); return false; } return true; }else{ this.mouseClick = false; - element.blur(); + this.blur(cell); return false; } }else{ this.mouseClick = false; - element.blur(); + this.blur(cell); return false; } } + + blur(cell) { + if (this.elementToFocusOnBlur) { + this.elementToFocusOnBlur.focus(); + } else { + cell.getElement().blur(); + } + } getEditedCells(){ var output = []; diff --git a/src/js/modules/Keybindings/defaults/actions.js b/src/js/modules/Keybindings/defaults/actions.js index 50c631af7..c6d4fc87a 100644 --- a/src/js/modules/Keybindings/defaults/actions.js +++ b/src/js/modules/Keybindings/defaults/actions.js @@ -155,14 +155,6 @@ export default { e.preventDefault(); } }, - spreadsheetEditCell: function(e){ - if (!this.table.options.spreadsheet) return; - if (this.table.modules.menu && this.table.modules.menu.currentComponent) { - return; - } - this.table.modules.spreadsheet.editActiveCell(); - e.preventDefault(); - }, undo:function(e){ var cell = false; diff --git a/src/js/modules/Keybindings/defaults/bindings.js b/src/js/modules/Keybindings/defaults/bindings.js index a8f4da955..d54c8f1aa 100644 --- a/src/js/modules/Keybindings/defaults/bindings.js +++ b/src/js/modules/Keybindings/defaults/bindings.js @@ -25,5 +25,4 @@ export default { spreadsheetExpandJumpDown:["ctrl + shift + 40", "meta + shift + 40"], spreadsheetExpandJumpLeft:["ctrl + shift + 37", "meta + shift + 37"], spreadsheetExpandJumpRight:["ctrl + shift + 39", "meta + shift + 39"], - spreadsheetEditCell:13, }; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 3507a33a5..9862c38f2 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -33,12 +33,14 @@ class Spreadsheet extends Module { } initializeWatchers() { + this.subscribe("column-init", this.initializeColumn.bind(this)); this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); this.subscribe("cell-rendered", this.renderCell.bind(this)); + this.subscribe("cell-editing", this.handleEditingCell.bind(this)); this.subscribe("edit-success", this.finishEditingCell.bind(this)); this.subscribe("edit-cancelled", this.finishEditingCell.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); @@ -98,18 +100,6 @@ class Spreadsheet extends Module { for (var column of this.table.options.columns) { // Disable sorting by clicking header column.headerSort = false; - - // FIXME: tableholder is not focusable if we have a column that's - // `editable: false` and `editor: 'input' or something`. - - // Edit on double click - var editable = column.editable !== undefined ? column.editable : true; - // TODO: use column init event, and then assign column.modules - column.__spreadsheet_editable = editable; - column.editable = false; - - column.__spreadsheet_editor = column.editor; - column.editor = false; } var customRowHeader = this.options("spreadsheetRowHeader"); @@ -154,6 +144,30 @@ class Spreadsheet extends Module { var self = this; + this.handleKeyDown = function (e) { + if (e.key === "Enter") { + // prevent action when pressing enter from editor + if (!e.target.classList.contains("tabulator-tableholder")) { + return; + } + + // is menu open? + if (self.table.modules.menu && self.table.modules.menu.currentComponent) { + return; + } + + // is editing a cell? + if (self.table.modules.edit && self.table.modules.edit.currentCell) { + return; + } + + self.editCell(self.getActiveCell()); + e.preventDefault(); + } + } + + this.table.rowManager.element.addEventListener("keydown", this.handleKeyDown); + this.mouseUpHandler = function () { self.mousedown = false; document.removeEventListener("mouseup", self.mouseUpHandler); @@ -161,6 +175,7 @@ class Spreadsheet extends Module { this.subscribe("table-destroy", function () { document.removeEventListener("mouseup", self.mouseUpHandler); + self.table.rowManager.element.removeEventListener("keydown", self.handleKeyDown); }); this.resetRanges(); @@ -168,6 +183,9 @@ class Spreadsheet extends Module { this.table.rowManager.element.appendChild(this.overlay); this.table.columnManager.element.setAttribute("tabindex", 0); + if (this.table.modules.edit) { + this.table.modules.edit.elementToFocusOnBlur = this.table.rowManager.element; + } } initializeFunctions() { @@ -210,6 +228,14 @@ class Spreadsheet extends Module { return range.getComponent(); }); } + + initializeColumn(column) { + if (column.modules.edit) { + // Block editor from taking action so we can trigger edit by + // double clicking. + column.modules.edit.blocked = true; + } + } initializeMenuNavigation() { var self = this; @@ -567,27 +593,20 @@ class Spreadsheet extends Module { this.editCell(cell); } - - editActiveCell() { - this.editCell(this.getActiveCell()); + + editCell(cell) { + cell.column.modules.edit.blocked = false; + cell.element.focus({ preventScroll: true }); + cell.column.modules.edit.blocked = true; } - editCell(cell) { - cell.column.definition.editable = - cell.column.definition.__spreadsheet_editable; - cell.column.definition.editor = - cell.column.definition.__spreadsheet_editor; - this.table.modules.edit.initializeColumnCheck(cell.column); - - if (this.table.modules.edit.allowEdit(cell)) { - cell.getComponent().edit(); - } + handleEditingCell() { + this.table.rowManager.element.removeEventListener("keydown", this.handleKeyDown); } - finishEditingCell(cell) { - cell.column.definition.editable = false; - cell.column.definition.editor = false; - this.table.rowManager.element.focus(); + finishEditingCell() { + self.table.rowManager.element.focus(); + this.table.rowManager.element.addEventListener("keydown", this.handleKeyDown); } navigate(mode, dir) { From 4358daedd3166fe2a1ee09b0008df98f0d1e24f3 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sun, 17 Dec 2023 18:05:21 +0700 Subject: [PATCH 38/48] undefined self --- src/js/modules/Spreadsheet/Spreadsheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 9862c38f2..c658c1d13 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -605,7 +605,7 @@ class Spreadsheet extends Module { } finishEditingCell() { - self.table.rowManager.element.focus(); + this.table.rowManager.element.focus(); this.table.rowManager.element.addEventListener("keydown", this.handleKeyDown); } From b1deac5ad00c4a0472dd14a223e430bbf982c1e6 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sat, 30 Dec 2023 03:51:37 +0700 Subject: [PATCH 39/48] fix disappearing row headers after setting columns this would add row header each time columns were set --- src/js/modules/Spreadsheet/Spreadsheet.js | 50 +++++++++++------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index c658c1d13..0c90a2a40 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -36,6 +36,7 @@ class Spreadsheet extends Module { this.subscribe("column-init", this.initializeColumn.bind(this)); this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); + this.subscribe("columns-loaded", this.handleColumnsLoaded.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); @@ -45,6 +46,7 @@ class Spreadsheet extends Module { this.subscribe("edit-cancelled", this.finishEditingCell.bind(this)); this.subscribe("page-changed", this.handlePageChanged.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); + this.subscribe("table-redraw", this.resetRanges.bind(this)); var debouncedLayoutRanges = Helpers.debounce(this.layoutRanges.bind(this), 200); var layoutRanges = () => { @@ -102,32 +104,6 @@ class Spreadsheet extends Module { column.headerSort = false; } - var customRowHeader = this.options("spreadsheetRowHeader"); - - var rowHeaderDef = { - title: "", - field: this.rowHeaderField, - headerSort: false, - resizable: false, - frozen: true, - editable: false, - formatter: "rownum", - formatterParams: { relativeToPage: true }, - - ...customRowHeader, - - cssClass: customRowHeader.cssClass - ? `tabulator-spreadsheet-row-header ${customRowHeader.cssClass}` - : "tabulator-spreadsheet-row-header", - }; - - this.rowHeaderField = rowHeaderDef.field; - - this.table.options.columns = [ - rowHeaderDef, - ...this.table.options.columns, - ]; - this.table.options.clipboardCopyRowRange = "spreadsheet"; this.overlay = document.createElement("div"); @@ -492,6 +468,28 @@ class Spreadsheet extends Module { el.dataset.range = rangeIdx; } + handleColumnsLoaded() { + var customRowHeader = this.options("spreadsheetRowHeader"); + var rowHeaderDef = { + title: "", + field: this.rowHeaderField, + headerSort: false, + resizable: false, + frozen: true, + editable: false, + formatter: "rownum", + formatterParams: { relativeToPage: true }, + + ...customRowHeader, + + cssClass: customRowHeader.cssClass + ? `tabulator-spreadsheet-row-header ${customRowHeader.cssClass}` + : "tabulator-spreadsheet-row-header", + }; + this.rowHeaderField = rowHeaderDef.field; + this.table.columnManager.addColumn(rowHeaderDef, true); + } + handleColumnMouseDown(event, column) { if ( event.button === 2 && From 90df6ce4f0960b558b0c04b05614eadb893f94c6 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sat, 30 Dec 2023 04:00:44 +0700 Subject: [PATCH 40/48] add left & right getters to range component --- src/js/modules/Spreadsheet/RangeComponent.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/js/modules/Spreadsheet/RangeComponent.js b/src/js/modules/Spreadsheet/RangeComponent.js index b0eb7bd45..082abff26 100644 --- a/src/js/modules/Spreadsheet/RangeComponent.js +++ b/src/js/modules/Spreadsheet/RangeComponent.js @@ -48,6 +48,14 @@ class RangeComponent { getBottom() { return this._range.bottom; } + + getLeft() { + return this._range.left; + } + + getRight() { + return this._range.right; + } } export default RangeComponent; From e58cd07d3cc50ddc96ab6c3e8a3734cefe27f6a3 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sat, 30 Dec 2023 04:07:23 +0700 Subject: [PATCH 41/48] cleanups --- src/js/modules/Spreadsheet/Spreadsheet.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 0c90a2a40..981412e76 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -14,9 +14,6 @@ class Spreadsheet extends Module { this.registerTableOption("spreadsheet", false); //enable spreadsheet this.registerTableOption("spreadsheetRowHeader", {}); //row header definition - - this.registerColumnOption("__spreadsheet_editable"); - this.registerColumnOption("__spreadsheet_editor"); } initialize() { From 1b929ab7a5ecbe7cc9b25b808996a0a6221e57f6 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sat, 30 Dec 2023 04:52:34 +0700 Subject: [PATCH 42/48] fix virtual dom error when adding row header --- src/js/modules/Spreadsheet/Spreadsheet.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 981412e76..6a517e0b3 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -33,7 +33,7 @@ class Spreadsheet extends Module { this.subscribe("column-init", this.initializeColumn.bind(this)); this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); - this.subscribe("columns-loaded", this.handleColumnsLoaded.bind(this)); + this.subscribe("columns-loading", this.handleColumnsLoading.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); this.subscribe("cell-dblclick", this.handleCellDblClick.bind(this)); @@ -465,7 +465,7 @@ class Spreadsheet extends Module { el.dataset.range = rangeIdx; } - handleColumnsLoaded() { + handleColumnsLoading() { var customRowHeader = this.options("spreadsheetRowHeader"); var rowHeaderDef = { title: "", @@ -484,7 +484,8 @@ class Spreadsheet extends Module { : "tabulator-spreadsheet-row-header", }; this.rowHeaderField = rowHeaderDef.field; - this.table.columnManager.addColumn(rowHeaderDef, true); + // Add this column before everything else + this.table.columnManager._addColumn(rowHeaderDef); } handleColumnMouseDown(event, column) { From e72011079649f2e2cef763ed2e4ad2b2c4cb4f69 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Sat, 30 Dec 2023 05:05:54 +0700 Subject: [PATCH 43/48] better redraw --- src/js/modules/Spreadsheet/Spreadsheet.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 6a517e0b3..9a8eb11d3 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -41,9 +41,9 @@ class Spreadsheet extends Module { this.subscribe("cell-editing", this.handleEditingCell.bind(this)); this.subscribe("edit-success", this.finishEditingCell.bind(this)); this.subscribe("edit-cancelled", this.finishEditingCell.bind(this)); - this.subscribe("page-changed", this.handlePageChanged.bind(this)); + this.subscribe("page-changed", this.redraw.bind(this)); this.subscribe("table-layout", this.layoutElement.bind(this)); - this.subscribe("table-redraw", this.resetRanges.bind(this)); + this.subscribe("table-redraw", this.redraw.bind(this)); var debouncedLayoutRanges = Helpers.debounce(this.layoutRanges.bind(this), 200); var layoutRanges = () => { @@ -399,6 +399,12 @@ class Spreadsheet extends Module { }); } + redraw() { + this.selecting = 'cell'; + this.resetRanges(); + this.layoutElement(); + } + getSelectedData() { return this.getDataByRange(this.getActiveRange()); } @@ -1180,12 +1186,6 @@ class Spreadsheet extends Module { "px"; } - handlePageChanged() { - this.selecting = 'cell'; - this.resetRanges(); - this.layoutElement(); - } - findRangeByCellElement(cell) { var rangeIdx = cell.dataset.range; if (rangeIdx < 0) { From 54433208860f510b093ecd05ea373077d82af2de Mon Sep 17 00:00:00 2001 From: azmy60 Date: Tue, 2 Jan 2024 00:59:55 +0700 Subject: [PATCH 44/48] disable headerSort if spreadsheet is enabled --- src/js/modules/Sort/Sort.js | 4 ++-- src/js/modules/Spreadsheet/Spreadsheet.js | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/js/modules/Sort/Sort.js b/src/js/modules/Sort/Sort.js index 8f5f1fe50..87dda6af0 100644 --- a/src/js/modules/Sort/Sort.js +++ b/src/js/modules/Sort/Sort.js @@ -108,7 +108,7 @@ class Sort extends Module{ tristate: column.definition.headerSortTristate, }; - if(column.definition.headerSort !== false){ + if(column.definition.headerSort !== false && !this.table.modules.spreadsheet){ colEl = column.getElement(); @@ -468,4 +468,4 @@ Sort.moduleName = "sort"; //load defaults Sort.sorters = defaultSorters; -export default Sort; \ No newline at end of file +export default Sort; diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 9a8eb11d3..dfc8d6c95 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -96,11 +96,6 @@ class Spreadsheet extends Module { } initializeTable() { - for (var column of this.table.options.columns) { - // Disable sorting by clicking header - column.headerSort = false; - } - this.table.options.clipboardCopyRowRange = "spreadsheet"; this.overlay = document.createElement("div"); From 3e005ed9ddcacc893ad72becd828dc6348381863 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Wed, 3 Jan 2024 00:01:46 +0700 Subject: [PATCH 45/48] add options to ResizeColumns and enable multi resize (spreadsheet) new table options in ResizeColumns: - resizeColumnsHandles - we can put handles on each cells (including headers) or only on the column headers. - resizeColumnsMode - resize the column in realtime or show a guide line when resizing. alongside, we also allow to resize multiple columns at once in when using spreadsheet. --- src/js/modules/ResizeColumns/ResizeColumns.js | 48 +++++++++++++++---- src/js/modules/Spreadsheet/Range.js | 4 +- src/js/modules/Spreadsheet/RangeComponent.js | 4 +- src/js/modules/Spreadsheet/Spreadsheet.js | 32 +++++++++++-- src/scss/tabulator.scss | 12 +++++ 5 files changed, 84 insertions(+), 16 deletions(-) diff --git a/src/js/modules/ResizeColumns/ResizeColumns.js b/src/js/modules/ResizeColumns/ResizeColumns.js index b105e60bb..2a80c6339 100644 --- a/src/js/modules/ResizeColumns/ResizeColumns.js +++ b/src/js/modules/ResizeColumns/ResizeColumns.js @@ -16,6 +16,8 @@ class ResizeColumns extends Module{ this.initialized = false; this.registerColumnOption("resizable", true); this.registerTableOption("resizableColumnFit", false); + this.registerTableOption("resizeColumnsHandles", "all"); //where are handles located (all, header-only) + this.registerTableOption("resizeColumnsMode", "live"); // (ive, guide } initialize(){ @@ -44,7 +46,7 @@ class ResizeColumns extends Module{ layoutCellHandles(cell){ - if(cell.row.type === "row"){ + if(cell.row.type === "row" && this.table.options.resizeColumnsHandles !== "header-only"){ this.deInitializeComponent(cell); this.initializeColumn("cell", cell, cell.column, cell.element); } @@ -213,12 +215,21 @@ class ResizeColumns extends Module{ } _mouseDown(e, column, handle){ - var self = this; + var self = this, + resizeGuideEl, + handleX = handle.getBoundingClientRect().x - self.table.element.getBoundingClientRect().x, + tableX = self.table.element.getBoundingClientRect().x; + + if (self.table.options.resizeColumnsMode === 'guide') { + resizeGuideEl = document.createElement("span"); + resizeGuideEl.classList.add('tabulator-col-resize-guide'); + self.table.element.appendChild(resizeGuideEl); + } self.table.element.classList.add("tabulator-block-select"); - - function mouseMove(e){ - var x = typeof e.screenX === "undefined" ? e.touches[0].screenX : e.screenX, + + function resize(e) { + var x = typeof e.clientX === "undefined" ? e.touches[0].clientX : e.clientX, startDiff = x - self.startX, moveDiff = x - self.latestX, blockedBefore, blockedAfter; @@ -261,7 +272,28 @@ class ResizeColumns extends Module{ } } + function mouseMove(e){ + if (self.table.options.resizeColumnsMode === 'live') { + resize(e); + } else { + var mouseX = typeof e.clientX === "undefined" ? e.touches[0].clientX : e.clientX, + diff = mouseX - self.startX, + columnX = column.element.getBoundingClientRect().left - tableX, + guideX = Math.max(handleX + diff, columnX + column.minWidth); + + if (column.maxWidth) { + guideX = Math.min(guideX, columnX + column.maxWidth); + } + + resizeGuideEl.style.left = guideX + "px"; + } + } + function mouseUp(e){ + if (self.table.options.resizeColumnsMode === 'guide') { + resize(e); + resizeGuideEl.remove(); + } //block editor from taking action while resizing is taking place if(self.startColumn.modules.edit){ @@ -294,8 +326,8 @@ class ResizeColumns extends Module{ if(self.startColumn.modules.edit){ self.startColumn.modules.edit.blocked = true; } - - self.startX = typeof e.screenX === "undefined" ? e.touches[0].screenX : e.screenX; + + self.startX = typeof e.clientX === "undefined" ? e.touches[0].clientX : e.clientX; self.latestX = self.startX; self.startWidth = column.getWidth(); @@ -308,4 +340,4 @@ class ResizeColumns extends Module{ ResizeColumns.moduleName = "resizeColumns"; -export default ResizeColumns; \ No newline at end of file +export default ResizeColumns; diff --git a/src/js/modules/Spreadsheet/Range.js b/src/js/modules/Spreadsheet/Range.js index 849a3ebf8..2015a9076 100644 --- a/src/js/modules/Spreadsheet/Range.js +++ b/src/js/modules/Spreadsheet/Range.js @@ -73,11 +73,11 @@ class Range { } getRows() { - return this.table.modules.spreadsheet.getRowsByRange(this).map((row) => row.getComponent()); + return this.table.modules.spreadsheet.getRowsByRange(this); } getColumns() { - return this.table.modules.spreadsheet.getColumnsByRange(this).map((col) => col.getComponent()); + return this.table.modules.spreadsheet.getColumnsByRange(this); } getComponent() { diff --git a/src/js/modules/Spreadsheet/RangeComponent.js b/src/js/modules/Spreadsheet/RangeComponent.js index 082abff26..45efd4bfe 100644 --- a/src/js/modules/Spreadsheet/RangeComponent.js +++ b/src/js/modules/Spreadsheet/RangeComponent.js @@ -34,11 +34,11 @@ class RangeComponent { } getRows() { - return this._range.getRows(); + return this._range.getRows().map((row) => row.getComponent()); } getColumns() { - return this._range.getColumns(); + return this._range.getColumns().map((column) => column.getComponent()); } getTop() { diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index dfc8d6c95..22dd525d9 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -33,6 +33,7 @@ class Spreadsheet extends Module { this.subscribe("column-init", this.initializeColumn.bind(this)); this.subscribe("column-mousedown", this.handleColumnMouseDown.bind(this)); this.subscribe("column-mousemove", this.handleColumnMouseMove.bind(this)); + this.subscribe("column-resized", this.handleColumnResized.bind(this)); this.subscribe("columns-loading", this.handleColumnsLoading.bind(this)); this.subscribe("cell-mousedown", this.handleCellMouseDown.bind(this)); this.subscribe("cell-mousemove", this.handleCellMouseMove.bind(this)); @@ -394,10 +395,12 @@ class Spreadsheet extends Module { }); } - redraw() { - this.selecting = 'cell'; - this.resetRanges(); - this.layoutElement(); + redraw(force) { + if (force) { + this.selecting = 'cell'; + this.resetRanges(); + this.layoutElement(); + } } getSelectedData() { @@ -489,6 +492,27 @@ class Spreadsheet extends Module { this.table.columnManager._addColumn(rowHeaderDef); } + handleColumnResized(column) { + if (this.selecting !== "column" && this.selecting !== "all") { + return; + } + + var selected = this.ranges.some((range) => range.occupiesColumn(column)); + + if (!selected) { + return; + } + + this.ranges.forEach((range) => { + var selectedColumns = range.getColumns(); + selectedColumns.forEach((selectedColumn) => { + if (selectedColumn !== column) { + selectedColumn.setWidth(column.width); + } + }) + }) + } + handleColumnMouseDown(event, column) { if ( event.button === 2 && diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index 4c0e530e4..8b815b6c5 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -100,6 +100,8 @@ $footerActiveColor:#d00 !default; //footer bottom active text color -khtml-user-select: none; -webkit-user-select: none; -o-user-select: none; + + outline: none; &.tabulator-header-hidden{ display:none; @@ -1578,3 +1580,13 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ .tabulator-spreadsheet-menu-item-focused { background-color: $rowAltBackgroundColor; } + +.tabulator-col-resize-guide { + position: absolute; + width: 4px; + margin-left: -2px; + height: 100%; + top: 0; + background-color: $rowTextColor; + opacity: .5; +} From 5bf68ae061c07ad5d8e288dfd60b4b30b184bb77 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Wed, 3 Jan 2024 01:35:22 +0700 Subject: [PATCH 46/48] fix resize guide position and style --- src/js/modules/ResizeColumns/ResizeColumns.js | 4 ++++ src/scss/tabulator.scss | 7 ++++--- src/scss/themes/tabulator_simple.scss | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/js/modules/ResizeColumns/ResizeColumns.js b/src/js/modules/ResizeColumns/ResizeColumns.js index 2a80c6339..8c568d626 100644 --- a/src/js/modules/ResizeColumns/ResizeColumns.js +++ b/src/js/modules/ResizeColumns/ResizeColumns.js @@ -330,6 +330,10 @@ class ResizeColumns extends Module{ self.startX = typeof e.clientX === "undefined" ? e.touches[0].clientX : e.clientX; self.latestX = self.startX; self.startWidth = column.getWidth(); + + if (self.table.options.resizeColumnsMode === "guide") { + resizeGuideEl.style.left = handleX + "px"; + } document.body.addEventListener("mousemove", mouseMove); document.body.addEventListener("mouseup", mouseUp); diff --git a/src/scss/tabulator.scss b/src/scss/tabulator.scss index 8b815b6c5..372410769 100644 --- a/src/scss/tabulator.scss +++ b/src/scss/tabulator.scss @@ -32,6 +32,8 @@ $cellSelectedBackground: #C2D5EF !default; //cell background color when selected $headerHighlightBackground: #D6D6D6 !default; //header background color when highlighted $headerTextHighlightBackground: #000000 !default; //header text color when highlighted +$columnResizeGuideColor: #999 !default; //column resize guide + $rangeBorderColor: #2975DD !default; //range border color $rangeHandleColor: #2975DD !default; //range handle color @@ -1584,9 +1586,8 @@ body.tabulator-print-fullscreen-hide>*:not(.tabulator-print-fullscreen){ .tabulator-col-resize-guide { position: absolute; width: 4px; - margin-left: -2px; + margin-left: -0.5px; height: 100%; top: 0; - background-color: $rowTextColor; - opacity: .5; + background-color: $columnResizeGuideColor; } diff --git a/src/scss/themes/tabulator_simple.scss b/src/scss/themes/tabulator_simple.scss index 814fdf63a..7b0bebf0e 100644 --- a/src/scss/themes/tabulator_simple.scss +++ b/src/scss/themes/tabulator_simple.scss @@ -25,6 +25,7 @@ $rowHoverBackground:#bbb !default; //row background color on hover $rowSelectedBackground: #9ABCEA !default; //row background color when selected $rowSelectedBackgroundHover: #769BCC !default;//row background color when selected and hovered + $editBoxColor:#1D68CD !default; //border color for edit boxes $errorColor:#dd0000 !default; //error indication From 94e0b847e770f7a0b1c8e5567710dd586e4c1694 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Fri, 5 Jan 2024 04:05:58 +0700 Subject: [PATCH 47/48] fix: bugs when entering editor - Clicking a cell would immediately open an editor for that cell. This happens after resizing the column of the cell. - Not able to enter the editor for the first time. --- src/js/modules/ResizeColumns/ResizeColumns.js | 2 +- src/js/modules/Spreadsheet/Spreadsheet.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/modules/ResizeColumns/ResizeColumns.js b/src/js/modules/ResizeColumns/ResizeColumns.js index 8c568d626..2cffc98d9 100644 --- a/src/js/modules/ResizeColumns/ResizeColumns.js +++ b/src/js/modules/ResizeColumns/ResizeColumns.js @@ -296,7 +296,7 @@ class ResizeColumns extends Module{ } //block editor from taking action while resizing is taking place - if(self.startColumn.modules.edit){ + if(self.startColumn.modules.edit && !self.table.modules.spreadsheet){ self.startColumn.modules.edit.blocked = false; } diff --git a/src/js/modules/Spreadsheet/Spreadsheet.js b/src/js/modules/Spreadsheet/Spreadsheet.js index 22dd525d9..5e9608d7b 100644 --- a/src/js/modules/Spreadsheet/Spreadsheet.js +++ b/src/js/modules/Spreadsheet/Spreadsheet.js @@ -616,6 +616,9 @@ class Spreadsheet extends Module { } editCell(cell) { + if (!cell.column.modules.edit) { + cell.column.modules.edit = {} + } cell.column.modules.edit.blocked = false; cell.element.focus({ preventScroll: true }); cell.column.modules.edit.blocked = true; From 52c0bb0dc457a0657e282e2606fd52fd5b2a3ec8 Mon Sep 17 00:00:00 2001 From: azmy60 Date: Thu, 18 Jan 2024 00:53:45 +0700 Subject: [PATCH 48/48] enable sort module --- src/js/modules/Sort/Sort.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/modules/Sort/Sort.js b/src/js/modules/Sort/Sort.js index 87dda6af0..0499945c7 100644 --- a/src/js/modules/Sort/Sort.js +++ b/src/js/modules/Sort/Sort.js @@ -108,7 +108,7 @@ class Sort extends Module{ tristate: column.definition.headerSortTristate, }; - if(column.definition.headerSort !== false && !this.table.modules.spreadsheet){ + if(column.definition.headerSort !== false){ colEl = column.getElement();