UNPKG

editorjs-table-readonly

Version:

Table for Editor.js with configurable rows and columns and readonly mode

355 lines (324 loc) 10.4 kB
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;