editorjs-table-readonly
Version:
Table for Editor.js with configurable rows and columns and readonly mode
355 lines (324 loc) • 10.4 kB
JavaScript
const { TableConstructor } = require('./tableConstructor');
const toolboxIcon = require('./img/toolboxIcon.svg');
const insertColBefore = require('./img/insertColBeforeIcon.svg');
const insertColAfter = require('./img/indertColAfterIcon.svg');
const insertRowBefore = require('./img/insertRowBeforeIcon.svg');
const insertRowAfter = require('./img/insertRowAfter.svg');
const deleteRow = require('./img/deleteRowIcon.svg');
const deleteCol = require('./img/deleteColIcon.svg');
const Icons = {
Toolbox: toolboxIcon,
InsertColBefore: insertColBefore,
InsertColAfter: insertColAfter,
InsertRowBefore: insertRowBefore,
InsertRowAfter: insertRowAfter,
DeleteRow: deleteRow,
DeleteCol: deleteCol
};
const CSS = {
input: 'tc-table__inp'
};
let isEventListenerAttached = false;
/**
* Tool for table's creating
* @typedef {object} TableData - object with the data transferred to form a table
* @property {string[][]} content - two-dimensional array which contains table content
*/
class Table {
/**
* Notify core that the read-only mode is supported
*
* @returns {boolean}
* @public
*/
static get isReadOnlySupported() {
return true;
}
/**
* Allow to press Enter inside the CodeTool textarea
* @returns {boolean}
* @public
*/
static get enableLineBreaks() {
return true;
}
/**
* Get Tool toolbox settings
* icon - Tool icon's SVG
* title - title to show in toolbox
*
* @returns {{icon: string, title: string}}
*/
static get toolbox() {
return {
icon: Icons.Toolbox,
title: 'Table'
};
}
/**
* Render plugin`s main Element and fill it with saved data
* @param {TableData} data — previously saved data
* @param {object} config - user config for Tool
* @param {object} api - Editor.js API
* @param {boolean} readOnly - read-only mode flag
*/
constructor({ data, config, api, readOnly }) {
this.api = api;
this.wrapper = undefined;
this.config = config;
this.data = data;
this.readOnly = readOnly;
this._tableConstructor = new TableConstructor(data, config, api, readOnly);
if (!isEventListenerAttached) {
isEventListenerAttached = true;
const editor = document.querySelector('.codex-editor');
editor.addEventListener('paste', this._handlePaste.bind(this));
}
this.actions = [
{
actionName: 'InsertColBefore',
icon: Icons.InsertColBefore,
label: this.config.InsertColBefore || 'Insert column before'
},
{
actionName: 'InsertColAfter',
icon: Icons.InsertColAfter,
label: this.config.InsertColAfter || 'Insert column after'
},
{
actionName: 'InsertRowBefore',
icon: Icons.InsertRowBefore,
label: this.config.InsertRowBefore || 'Insert row before'
},
{
actionName: 'InsertRowAfter',
icon: Icons.InsertRowAfter,
label: this.config.InsertRowAfter || 'Insert row after'
},
{
actionName: 'DeleteRow',
icon: Icons.DeleteRow,
label: this.config.DeleteRow || 'Delete row'
},
{
actionName: 'DeleteCol',
icon: Icons.DeleteCol,
label: this.config.DeleteCol || 'Delete column'
}
];
}
/**
* perform selected action
* @param actionName {string} - action name
* @return {undefined}
*/
performAction(actionName) {
switch (actionName) {
case 'InsertColBefore':
this._tableConstructor.table.insertColumnBefore();
break;
case 'InsertColAfter':
this._tableConstructor.table.insertColumnAfter();
break;
case 'InsertRowBefore':
this._tableConstructor.table.insertRowBefore();
break;
case 'InsertRowAfter':
this._tableConstructor.table.insertRowAfter();
break;
case 'DeleteRow':
this._tableConstructor.table.deleteRow();
break;
case 'DeleteCol':
this._tableConstructor.table.deleteColumn();
break;
}
}
/**
* render actions toolbar
* @returns {HTMLDivElement}
*/
renderSettings() {
const wrapper = document.createElement('div');
this.actions.forEach(({ actionName, label, icon }) => {
const title = this.api.i18n.t(label);
const button = document.createElement('div');
button.classList.add('cdx-settings-button');
button.innerHTML = icon;
button.title = actionName;
this.api.tooltip.onHover(button, title, {
placement: 'top'
});
button.addEventListener(
'click',
this.performAction.bind(this, actionName)
);
wrapper.appendChild(button);
if (this._tableConstructor.table.selectedCell) {
this._tableConstructor.table.focusCellOnSelectedCell();
}
});
return wrapper;
}
/**
* Return Tool's view
* @returns {HTMLDivElement}
* @public
*/
render() {
this.wrapper = document.createElement('div');
if (this.data && this.data.content) {
//Creates table if Data is Present
this._createTableConfiguration();
} else {
// Create table preview if New table is initialised
this.wrapper.classList.add('table-selector');
this.wrapper.setAttribute('data-hoveredClass', 'm,n');
const rows = 6;
this.createCells(rows);
//Hover to select cells
if (this.wrapper.className === 'table-selector') {
this.wrapper.addEventListener('mouseover', (event) => {
const selectedCell = event.target.id;
if (selectedCell.length) {
const selectedRow = event.target.attributes.row.value;
const selectedColumn = event.target.attributes.column.value;
this.wrapper.setAttribute(
'data-hoveredClass',
`${selectedRow},${selectedColumn}`
);
}
});
}
//set the select cell to load table config
this.wrapper.addEventListener('click', (event) => {
const selectedCell = event.target.id;
if (selectedCell.length) {
const selectedRow = event.target.attributes.row.value;
const selectedColumn = event.target.attributes.column.value;
this.wrapper.removeEventListener('mouseover', () => {});
this.config.rows = selectedRow;
this.config.cols = selectedColumn;
this._createTableConfiguration();
}
});
}
return this.wrapper;
}
createCells(rows) {
if (rows !== 0) {
for (let i = 0; i < rows; i++) {
let rowDiv = document.createElement('div');
rowDiv.setAttribute('class', 'table-row');
for (let j = 0; j < rows; j++) {
let columnDivContainer = document.createElement('div');
let columnDiv = document.createElement('div');
columnDivContainer.setAttribute('class', 'table-cell-container');
columnDiv.setAttribute('class', 'table-cell');
columnDivContainer.setAttribute('id', `row_${i + 1}_cell_${j + 1}`);
columnDivContainer.setAttribute('column', j + 1);
columnDivContainer.setAttribute('row', i + 1);
columnDiv.setAttribute('id', `cell_${j + 1}`);
columnDiv.setAttribute('column', j + 1);
columnDiv.setAttribute('row', i + 1);
columnDivContainer.appendChild(columnDiv);
rowDiv.appendChild(columnDivContainer);
}
this.wrapper.appendChild(rowDiv);
}
}
const hiddenEl = document.createElement('input');
hiddenEl.classList.add('hidden-element');
hiddenEl.setAttribute('tabindex', 0);
this.wrapper.appendChild(hiddenEl);
}
_handlePaste(event) {
if (!this.readOnly) {
const pasteData = (event.clipboardData || window.clipboardData).getData(
'text/html'
);
if (pasteData.length) {
const parser = new DOMParser();
const markdown = parser.parseFromString(pasteData, 'text/html');
const tableElement = markdown.querySelector('table');
if (tableElement) {
event.stopPropagation();
event.preventDefault();
const rows = tableElement.querySelectorAll('tr');
const table = [];
for (const row of Array.from(rows)) {
const rowCells = [
...Array.from(row.querySelectorAll('td')),
...Array.from(row.querySelectorAll('th'))
];
const rowCellsText = rowCells.map((cell) => cell.textContent);
const formattedCellsText = rowCellsText.map((item) => {
return item
.replace(/</g, '< ')
.replace(/>/g, ' >')
.replace(/\n/g, '<br>')
.replace(/ /g, '\u00a0');
});
table.push(formattedCellsText);
}
const longestRow = Math.max(...table.map((row) => row.length));
const filledTable = table.map((item) => {
if (item.length < longestRow) {
while (item.length < longestRow) {
item.push('\u00a0');
}
}
return item;
});
this.api.blocks.insert('table', { content: [...filledTable] });
}
}
}
}
_createTableConfiguration() {
this.wrapper.innerHTML = '';
this._tableConstructor = new TableConstructor(
this.data,
this.config,
this.api,
this.readOnly
);
this.wrapper.appendChild(this._tableConstructor.htmlElement);
}
/**
* Extract Tool's data from the view
* @returns {TableData} - saved data
* @public
*/
save(toolsContent) {
const table = toolsContent.querySelector('table');
const data = [];
const rows = table ? table.rows : 0;
if (rows.length) {
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const cols = Array.from(row.cells);
const inputs = cols.map((cell) => cell.querySelector('.' + CSS.input));
const isWorthless = inputs.every(this._isEmpty);
if (isWorthless) {
continue;
}
data.push(inputs.map((input) => input.innerHTML));
}
return {
content: data
};
}
}
/**
* @private
*
* Check input field is empty
* @param {HTMLElement} input - input field
* @return {boolean}
*/
_isEmpty(input) {
return !input.textContent.trim();
}
}
module.exports = Table;