editorjs-table-readonly
Version:
Table for Editor.js with configurable rows and columns and readonly mode
332 lines (284 loc) • 7.29 kB
JavaScript
import { create } from './documentUtils';
import './styles/table.pcss';
const CSS = {
table: 'tc-table',
inputField: 'tc-table__inp',
cell: 'tc-table__cell',
wrapper: 'tc-table__wrap',
area: 'tc-table__area',
highlight: 'tc-table__highlight'
};
/**
* Generates and manages _table contents.
*/
export class Table {
/**
* Creates
*/
constructor(readOnly) {
this.readOnly = readOnly;
this._numberOfColumns = 0;
this._numberOfRows = 0;
this._element = this._createTableWrapper();
this._table = this._element.querySelector('table');
this._selectedCell = null;
if (!this.readOnly) {
this._attachEvents();
}
}
/**
* returns selected/editable cell or null if row is not selected
* @return {HTMLElement|null}
*/
get selectedCell() {
return this._selectedCell;
}
/**
* sets a selected cell and highlights it
* @param cell - new current cell
*/
set selectedCell(cell) {
if (this._selectedCell) {
this._selectedCell.classList.remove(CSS.highlight);
}
this._selectedCell = cell;
if (this._selectedCell) {
this._selectedCell.classList.add(CSS.highlight);
}
}
/**
* returns current a row that contains current cell
* or null if no cell selected
* @returns {HTMLElement|null}
*/
get selectedRow() {
if (!this.selectedCell) return null;
return this.selectedCell.closest('tr');
}
/**
* Inserts column to the right from currently selected cell
*/
insertColumnAfter() {
this.insertColumn(1);
this.focusCellOnSelectedCell();
}
/**
* Inserts column to the left from currently selected cell
*/
insertColumnBefore() {
this.insertColumn();
this.focusCellOnSelectedCell();
}
/**
* Inserts new row below a current row
*/
insertRowBefore() {
this.insertRow();
this.focusCellOnSelectedCell();
}
/**
* Inserts row above a current row
*/
insertRowAfter() {
this.insertRow(1);
this.focusCellOnSelectedCell();
}
/**
* Insert a column into table relatively to a current cell
* @param {number} direction - direction of insertion. 0 is insertion before, 1 is insertion after
*/
insertColumn(direction = 0) {
direction = Math.min(Math.max(direction, 0), 1);
const insertionIndex = this.selectedCell
? this.selectedCell.cellIndex + direction
: 0;
this._numberOfColumns++;
/** Add cell in each row */
const rows = this._table.rows;
for (let i = 0; i < rows.length; i++) {
const cell = rows[i].insertCell(insertionIndex);
this._fillCell(cell);
}
}
/**
* Remove column that includes currently selected cell
* Do nothing if there's no current cell
*/
deleteColumn() {
if (!this.selectedCell) return;
const removalIndex = this.selectedCell.cellIndex;
this._numberOfColumns--;
/** Delete cell in each row */
const rows = this._table.rows;
for (let i = 0; i < rows.length; i++) {
rows[i].deleteCell(removalIndex);
}
}
/**
* Insert a row into table relatively to a current cell
* @param {number} direction - direction of insertion. 0 is insertion before, 1 is insertion after
* @return {HTMLElement} row
*/
insertRow(direction = 0) {
direction = Math.min(Math.max(direction, 0), 1);
const insertionIndex = this.selectedRow
? this.selectedRow.rowIndex + direction
: 0;
const row = this._table.insertRow(insertionIndex);
this._numberOfRows++;
this._fillRow(row);
return row;
}
/**
* Remove row in table on index place
* @param {number} index - number in the array of columns, where new column to insert,-1 if insert at the end
*/
deleteRow(index = -1) {
if (!this.selectedRow) return;
const removalIndex = this.selectedRow.rowIndex;
this._table.deleteRow(removalIndex);
this._numberOfRows--;
}
/**
* get html table wrapper
* @return {HTMLElement}
*/
get htmlElement() {
return this._element;
}
/**
* get real table tag
* @return {HTMLElement}
*/
get body() {
return this._table;
}
/**
* @private
*
* Creates table structure
* @return {HTMLElement} tbody - where rows will be
*/
_createTableWrapper() {
return create('div', [CSS.wrapper], null, [
create('table', [CSS.table])
// This function can be updated so that it will render the table with the give config instead of 3x3
]);
}
/**
* @private
*
* Create editable area of cell
* @return {HTMLElement} - the area
*/
_createContenteditableArea() {
return create('div', [CSS.inputField], { contenteditable: !this.readOnly });
}
/**
* @private
*
* Fills the empty cell of the editable area
* @param {HTMLElement} cell - empty cell
*/
_fillCell(cell) {
cell.classList.add(CSS.cell);
const content = this._createContenteditableArea();
cell.appendChild(create('div', [CSS.area], null, [content]));
}
/**
* @private
*
* Fills the empty row with cells in the size of numberOfColumns
* @param row = the empty row
*/
_fillRow(row) {
for (let i = 0; i < this._numberOfColumns; i++) {
const cell = row.insertCell();
this._fillCell(cell);
}
}
/**
* @private
*
* hang necessary events
*/
_attachEvents() {
this._table.addEventListener(
'focus',
(event) => {
this._focusEditField(event);
},
true
);
this._table.addEventListener('keydown', (event) => {
this._pressedEnterInEditField(event);
});
this._table.addEventListener('click', (event) => {
this._clickedOnCell(event);
});
this.htmlElement.addEventListener('keydown', (event) => {
this._containerKeydown(event);
});
}
/**
* @private
*
* When you focus on an editable field, remembers the cell
* @param {FocusEvent} event
*/
_focusEditField(event) {
this.selectedCell =
event.target.tagName === 'TD' ? event.target : event.target.closest('td');
}
focusCellOnSelectedCell() {
this.selectedCell.childNodes[0].childNodes[0].focus();
}
/**
* @private
*
* When enter is pressed when editing a field
* @param {KeyboardEvent} event
*/
_pressedEnterInEditField(event) {
if (!event.target.classList.contains(CSS.inputField)) {
return;
}
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
}
}
/**
* @private
*
* When clicking on a cell
* @param {MouseEvent} event
*/
_clickedOnCell(event) {
if (!event.target.classList.contains(CSS.cell)) {
return;
}
const content = event.target.querySelector('.' + CSS.inputField);
content.focus();
}
/**
* @private
*
* detects button presses when editing a table's content
* @param {KeyboardEvent} event
*/
_containerKeydown(event) {
if (event.key === 'Enter' && event.ctrlKey) {
this._containerEnterPressed(event);
}
}
/**
* @private
*
* if "Ctrl + Enter" is pressed then create new line under current and focus it
* @param {KeyboardEvent} event
*/
_containerEnterPressed(event) {
const newRow = this.insertRow(1);
newRow.cells[0].click();
}
}