jodit
Version:
Jodit is an awesome and useful wysiwyg editor with filebrowser
685 lines (684 loc) • 26.2 kB
JavaScript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2026 Valerii Chupurnov. All rights reserved. https://xdsoft.net
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if (d = decorators[i])
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { ViewComponent } from "../../core/component/index.js";
import * as consts from "../../core/constants.js";
import { debounce } from "../../core/decorators/index.js";
import { Dom } from "../../core/dom/index.js";
import { getContainer } from "../../core/global.js";
import { $$, attr, cssPath, isNumber, toArray, trim } from "../../core/helpers/index.js";
const markedValue = new WeakMap();
export class Table extends ViewComponent {
constructor() {
super(...arguments);
this.selected = new Set();
}
/** @override */
className() {
return 'Table';
}
__recalculateStyles() {
const style = getContainer(this.j, Table, 'style', true);
const selectors = [];
this.selected.forEach(td => {
const selector = cssPath(td);
selector && selectors.push(selector);
});
style.innerHTML = selectors.length
? selectors.join(',') +
`{${this.jodit.options.table.selectionCellStyle}}`
: '';
}
addSelection(td) {
this.selected.add(td);
this.__recalculateStyles();
const table = Dom.closest(td, 'table', this.j.editor);
if (table) {
const cells = Table.__selectedByTable.get(table) || new Set();
cells.add(td);
Table.__selectedByTable.set(table, cells);
}
}
removeSelection(td) {
this.selected.delete(td);
this.__recalculateStyles();
const table = Dom.closest(td, 'table', this.j.editor);
if (table) {
const cells = Table.__selectedByTable.get(table);
if (cells) {
cells.delete(td);
if (!cells.size) {
Table.__selectedByTable.delete(table);
}
}
}
}
/**
* Returns array of selected cells
*/
getAllSelectedCells() {
return toArray(this.selected);
}
static __getSelectedCellsByTable(table) {
const cells = Table.__selectedByTable.get(table);
return cells ? toArray(cells) : [];
}
/** @override **/
destruct() {
this.selected.clear();
return super.destruct();
}
static __getRowsCount(table) {
return table.rows.length;
}
/**
* Returns rows count in the table
*/
getRowsCount(table) {
return Table.__getRowsCount(table);
}
static __getColumnsCount(table) {
const matrix = Table.__formalMatrix(table);
return matrix.reduce((max_count, cells) => Math.max(max_count, cells.length), 0);
}
/**
* Returns columns count in the table
*/
getColumnsCount(table) {
return Table.__getColumnsCount(table);
}
static __formalMatrix(table, callback) {
const matrix = [[]];
const rows = toArray(table.rows);
const setCell = (cell, i) => {
if (matrix[i] === undefined) {
matrix[i] = [];
}
const colSpan = cell.colSpan, rowSpan = cell.rowSpan;
let column, row, currentColumn = 0;
while (matrix[i][currentColumn]) {
currentColumn += 1;
}
for (row = 0; row < rowSpan; row += 1) {
for (column = 0; column < colSpan; column += 1) {
if (matrix[i + row] === undefined) {
matrix[i + row] = [];
}
if (callback &&
callback(cell, i + row, currentColumn + column, colSpan, rowSpan) === false) {
return false;
}
matrix[i + row][currentColumn + column] = cell;
}
}
};
for (let i = 0; i < rows.length; i += 1) {
const cells = toArray(rows[i].cells);
for (let j = 0; j < cells.length; j += 1) {
if (setCell(cells[j], i) === false) {
return matrix;
}
}
}
return matrix;
}
/**
* Generate formal table martix columns*rows
* @param table - Working table
* @param callback - if return false cycle break
*/
formalMatrix(table, callback) {
return Table.__formalMatrix(table, callback);
}
static __formalCoordinate(table, cell, max = false) {
let i = 0, j = 0, width = 1, height = 1;
Table.__formalMatrix(table, (td, ii, jj, colSpan, rowSpan) => {
if (cell === td) {
i = ii;
j = jj;
width = colSpan || 1;
height = rowSpan || 1;
if (max) {
j += (colSpan || 1) - 1;
i += (rowSpan || 1) - 1;
}
return false;
}
});
return [i, j, width, height];
}
/**
* Get cell coordinate in formal table (without colspan and rowspan)
*/
formalCoordinate(table, cell, max = false) {
return Table.__formalCoordinate(table, cell, max);
}
static __appendRow(table, line, after, create) {
var _a;
let row;
if (!line) {
const columnsCount = Table.__getColumnsCount(table);
row = create.element('tr');
for (let j = 0; j < columnsCount; j += 1) {
row.appendChild(create.element('td'));
}
}
else {
row = line.cloneNode(true);
$$('td,th', line).forEach(cell => {
const rowspan = attr(cell, 'rowspan');
if (rowspan && parseInt(rowspan, 10) > 1) {
const newRowSpan = parseInt(rowspan, 10) - 1;
attr(cell, 'rowspan', newRowSpan > 1 ? newRowSpan : null);
}
});
$$('td,th', row).forEach(cell => {
cell.innerHTML = '';
});
}
if (after && line && line.nextSibling) {
line.parentNode &&
line.parentNode.insertBefore(row, line.nextSibling);
}
else if (!after && line) {
line.parentNode && line.parentNode.insertBefore(row, line);
}
else {
(((_a = table.getElementsByTagName('tbody')) === null || _a === void 0 ? void 0 : _a[0]) || table).appendChild(row);
}
}
/**
* Inserts a new line after row what contains the selected cell
*
* @param table - Working table
* @param line - Insert a new line after/before this
* line contains the selected cell
* @param after - Insert a new line after line contains the selected cell
*/
appendRow(table, line, after) {
return Table.__appendRow(table, line, after, this.j.createInside);
}
static __removeRow(table, rowIndex) {
const box = Table.__formalMatrix(table);
let dec;
const row = table.rows[rowIndex];
box[rowIndex].forEach((cell, j) => {
dec = false;
if (rowIndex - 1 >= 0 && box[rowIndex - 1][j] === cell) {
dec = true;
}
else if (box[rowIndex + 1] && box[rowIndex + 1][j] === cell) {
if (cell.parentNode === row && cell.parentNode.nextSibling) {
dec = true;
let nextCell = j + 1;
while (box[rowIndex + 1][nextCell] === cell) {
nextCell += 1;
}
const nextRow = Dom.next(cell.parentNode, elm => Dom.isTag(elm, 'tr'), table);
if (nextRow) {
if (box[rowIndex + 1][nextCell]) {
nextRow.insertBefore(cell, box[rowIndex + 1][nextCell]);
}
else {
nextRow.appendChild(cell);
}
}
}
}
else {
Dom.safeRemove(cell);
}
if (dec &&
(cell.parentNode === row || cell !== box[rowIndex][j - 1])) {
const rowSpan = cell.rowSpan;
attr(cell, 'rowspan', rowSpan - 1 > 1 ? rowSpan - 1 : null);
}
});
Dom.safeRemove(row);
}
/**
* Remove row
*/
removeRow(table, rowIndex) {
return Table.__removeRow(table, rowIndex);
}
/**
* Insert column before / after all the columns containing the selected cells
*/
appendColumn(table, selectedCell, insertAfter = true) {
const box = Table.__formalMatrix(table);
if (!insertAfter && Dom.isCell(selectedCell.previousElementSibling)) {
return this.appendColumn(table, selectedCell.previousElementSibling, true);
}
const [, formalCol] = Table.__formalCoordinate(table, selectedCell, insertAfter);
const columnIndex = formalCol;
const newColumnIndex = insertAfter ? columnIndex + 1 : columnIndex;
for (let i = 0; i < box.length;) {
const cells = box[i];
if (cells[columnIndex] !== cells[newColumnIndex] ||
columnIndex === newColumnIndex) {
const cell = this.j.createInside.element('td');
if (insertAfter) {
Dom.after(cells[columnIndex], cell);
}
else {
Dom.before(cells[columnIndex], cell);
}
if (cells[columnIndex].rowSpan > 1) {
cell.rowSpan = cells[columnIndex].rowSpan;
}
}
else {
cells[columnIndex].colSpan += 1;
}
i += cells[columnIndex].rowSpan || 1;
}
}
static __removeColumn(table, j) {
const box = Table.__formalMatrix(table);
let dec;
box.forEach((cells, i) => {
const td = cells[j];
dec = false;
if (j - 1 >= 0 && box[i][j - 1] === td) {
dec = true;
}
else if (j + 1 < cells.length && box[i][j + 1] === td) {
dec = true;
}
else {
Dom.safeRemove(td);
}
if (dec && (i - 1 < 0 || td !== box[i - 1][j])) {
const colSpan = td.colSpan;
attr(td, 'colspan', colSpan - 1 > 1 ? (colSpan - 1).toString() : null);
}
});
}
/**
* Remove column by index
*/
removeColumn(table, j) {
return Table.__removeColumn(table, j);
}
static __getSelectedBound(table, selectedCells) {
const bound = [
[Infinity, Infinity],
[0, 0]
];
const box = Table.__formalMatrix(table);
let i, j, k;
for (i = 0; i < box.length; i += 1) {
for (j = 0; box[i] && j < box[i].length; j += 1) {
if (selectedCells.includes(box[i][j])) {
bound[0][0] = Math.min(i, bound[0][0]);
bound[0][1] = Math.min(j, bound[0][1]);
bound[1][0] = Math.max(i, bound[1][0]);
bound[1][1] = Math.max(j, bound[1][1]);
}
}
}
for (i = bound[0][0]; i <= bound[1][0]; i += 1) {
for (k = 1, j = bound[0][1]; j <= bound[1][1]; j += 1) {
while (box[i] && box[i][j - k] && box[i][j] === box[i][j - k]) {
bound[0][1] = Math.min(j - k, bound[0][1]);
bound[1][1] = Math.max(j - k, bound[1][1]);
k += 1;
}
k = 1;
while (box[i] && box[i][j + k] && box[i][j] === box[i][j + k]) {
bound[0][1] = Math.min(j + k, bound[0][1]);
bound[1][1] = Math.max(j + k, bound[1][1]);
k += 1;
}
k = 1;
while (box[i - k] && box[i][j] === box[i - k][j]) {
bound[0][0] = Math.min(i - k, bound[0][0]);
bound[1][0] = Math.max(i - k, bound[1][0]);
k += 1;
}
k = 1;
while (box[i + k] && box[i][j] === box[i + k][j]) {
bound[0][0] = Math.min(i + k, bound[0][0]);
bound[1][0] = Math.max(i + k, bound[1][0]);
k += 1;
}
}
}
return bound;
}
/**
* Define bound for selected cells
*/
getSelectedBound(table, selectedCells) {
return Table.__getSelectedBound(table, selectedCells);
}
static __normalizeTable(table) {
const __marked = [], box = Table.__formalMatrix(table);
Table.__removeExtraColspans(box, __marked);
Table.__removeExtraRowspans(box, __marked);
// remove rowspans and colspans equal 1 and empty class
for (let i = 0; i < box.length; i += 1) {
for (let j = 0; j < box[i].length; j += 1) {
if (box[i][j] === undefined) {
continue; // broken table
}
if (box[i][j].hasAttribute('rowspan') &&
box[i][j].rowSpan === 1) {
attr(box[i][j], 'rowspan', null);
}
if (box[i][j].hasAttribute('colspan') &&
box[i][j].colSpan === 1) {
attr(box[i][j], 'colspan', null);
}
if (box[i][j].hasAttribute('class') &&
!attr(box[i][j], 'class')) {
attr(box[i][j], 'class', null);
}
}
}
Table.__unmark(__marked);
}
static __removeExtraColspans(box, __marked) {
for (let j = 0; j < box[0].length; j += 1) {
let min = 1000000;
let not = false;
for (let i = 0; i < box.length; i += 1) {
if (box[i][j] === undefined) {
continue; // broken table
}
if (box[i][j].colSpan < 2) {
not = true;
break;
}
min = Math.min(min, box[i][j].colSpan);
}
if (!not) {
for (let i = 0; i < box.length; i += 1) {
if (box[i][j] === undefined) {
continue; // broken table
}
Table.__mark(box[i][j], 'colspan', box[i][j].colSpan - min + 1, __marked);
}
}
}
}
static __removeExtraRowspans(box, marked) {
let i = 0;
let j = 0;
for (i = 0; i < box.length; i += 1) {
let min = 1000000;
let not = false;
for (j = 0; j < box[i].length; j += 1) {
if (box[i][j] === undefined) {
continue; // broken table
}
if (box[i][j].rowSpan < 2) {
not = true;
break;
}
min = Math.min(min, box[i][j].rowSpan);
}
if (!not) {
for (j = 0; j < box[i].length; j += 1) {
if (box[i][j] === undefined) {
continue; // broken table
}
Table.__mark(box[i][j], 'rowspan', box[i][j].rowSpan - min + 1, marked);
}
}
}
}
/**
* Try recalculate all coluns and rows after change
*/
normalizeTable(table) {
return Table.__normalizeTable(table);
}
static __mergeSelected(table, jodit) {
const html = [], bound = Table.__getSelectedBound(table, Table.__getSelectedCellsByTable(table));
let w = 0, first = null, first_j = 0, td, cols = 0, rows = 0;
const alreadyMerged = new Set(), __marked = [];
if (bound && (bound[0][0] - bound[1][0] || bound[0][1] - bound[1][1])) {
Table.__formalMatrix(table, (cell, i, j, cs, rs) => {
if (i >= bound[0][0] && i <= bound[1][0]) {
if (j >= bound[0][1] && j <= bound[1][1]) {
td = cell;
if (alreadyMerged.has(td)) {
return;
}
alreadyMerged.add(td);
if (i === bound[0][0] && td.style.width) {
w += td.offsetWidth;
}
if (trim(cell.innerHTML.replace(/<br(\/)?>/g, '')) !== '') {
html.push(cell.innerHTML);
}
if (cs > 1) {
cols += cs - 1;
}
if (rs > 1) {
rows += rs - 1;
}
if (!first) {
first = cell;
first_j = j;
}
else {
Table.__mark(td, 'remove', 1, __marked);
instance(jodit).removeSelection(td);
}
}
}
});
cols = bound[1][1] - bound[0][1] + 1;
rows = bound[1][0] - bound[0][0] + 1;
if (first) {
if (cols > 1) {
Table.__mark(first, 'colspan', cols, __marked);
}
if (rows > 1) {
Table.__mark(first, 'rowspan', rows, __marked);
}
if (w) {
Table.__mark(first, 'width', ((w / table.offsetWidth) * 100).toFixed(consts.ACCURACY) + '%', __marked);
if (first_j) {
Table.__setColumnWidthByDelta(table, first_j, 0, true, __marked);
}
}
first.innerHTML = html.join('<br/>');
instance(jodit).addSelection(first);
alreadyMerged.delete(first);
Table.__unmark(__marked);
Table.__normalizeTable(table);
toArray(table.rows).forEach(tr => {
if (!tr.cells.length) {
Dom.safeRemove(tr);
}
});
}
}
}
/**
* It combines all the selected cells into one. The contents of the cells will also be combined
*/
mergeSelected(table) {
return Table.__mergeSelected(table, this.j);
}
static __splitHorizontal(table, jodit) {
let coord, td, tr, parent, after;
const __marked = [];
Table.__getSelectedCellsByTable(table).forEach((cell) => {
td = jodit.createInside.element('td');
td.appendChild(jodit.createInside.element('br'));
tr = jodit.createInside.element('tr');
coord = Table.__formalCoordinate(table, cell);
if (cell.rowSpan < 2) {
Table.__formalMatrix(table, (tdElm, i, j) => {
if (coord[0] === i &&
coord[1] !== j &&
tdElm !== cell) {
Table.__mark(tdElm, 'rowspan', tdElm.rowSpan + 1, __marked);
}
});
Dom.after(Dom.closest(cell, 'tr', table), tr);
tr.appendChild(td);
}
else {
Table.__mark(cell, 'rowspan', cell.rowSpan - 1, __marked);
Table.__formalMatrix(table, (tdElm, i, j) => {
if (i > coord[0] &&
i < coord[0] + cell.rowSpan &&
coord[1] > j &&
tdElm.parentNode
.rowIndex === i) {
after = tdElm;
}
if (coord[0] < i && tdElm === cell) {
parent = table.rows[i];
}
});
if (after) {
Dom.after(after, td);
}
else {
parent.insertBefore(td, parent.firstChild);
}
}
if (cell.colSpan > 1) {
Table.__mark(td, 'colspan', cell.colSpan, __marked);
}
Table.__unmark(__marked);
instance(jodit).removeSelection(cell);
});
this.__normalizeTable(table);
}
/**
* Divides all selected by `jodit_focused_cell` class table cell in 2 parts vertical. Those division into 2 columns
*/
splitHorizontal(table) {
return Table.__splitHorizontal(table, this.j);
}
static __splitVertical(table, jodit) {
let coord, td, percentage;
const __marked = [];
Table.__getSelectedCellsByTable(table).forEach(cell => {
coord = Table.__formalCoordinate(table, cell);
if (cell.colSpan < 2) {
Table.__formalMatrix(table, (tdElm, i, j) => {
if (coord[1] === j && coord[0] !== i && tdElm !== cell) {
Table.__mark(tdElm, 'colspan', tdElm.colSpan + 1, __marked);
}
});
}
else {
Table.__mark(cell, 'colspan', cell.colSpan - 1, __marked);
}
td = jodit.createInside.element('td');
td.appendChild(jodit.createInside.element('br'));
if (cell.rowSpan > 1) {
Table.__mark(td, 'rowspan', cell.rowSpan, __marked);
}
const oldWidth = cell.offsetWidth; // get old width
Dom.after(cell, td);
percentage = oldWidth / table.offsetWidth / 2;
Table.__mark(cell, 'width', (percentage * 100).toFixed(consts.ACCURACY) + '%', __marked);
Table.__mark(td, 'width', (percentage * 100).toFixed(consts.ACCURACY) + '%', __marked);
Table.__unmark(__marked);
instance(jodit).removeSelection(cell);
});
Table.__normalizeTable(table);
}
/**
* It splits all the selected cells into 2 parts horizontally. Those. are added new row
*/
splitVertical(table) {
return Table.__splitVertical(table, this.j);
}
static __setColumnWidthByDelta(table, column, delta, noUnmark, marked) {
const box = Table.__formalMatrix(table);
let clearWidthIndex = 0;
for (let i = 0; i < box.length; i += 1) {
const cell = box[i][column];
if (cell.colSpan > 1 && box.length > 1) {
continue;
}
const w = cell.offsetWidth;
const percent = ((w + delta) / table.offsetWidth) * 100;
Table.__mark(cell, 'width', percent.toFixed(consts.ACCURACY) + '%', marked);
clearWidthIndex = i;
break;
}
for (let i = clearWidthIndex + 1; i < box.length; i += 1) {
const cell = box[i][column];
Table.__mark(cell, 'width', null, marked);
}
if (!noUnmark) {
Table.__unmark(marked);
}
}
/**
* Set column width used delta value
*/
setColumnWidthByDelta(table, column, delta, noUnmark, marked) {
return Table.__setColumnWidthByDelta(table, column, delta, noUnmark, marked);
}
static __mark(cell, key, value, marked) {
var _a;
marked.push(cell);
const dict = (_a = markedValue.get(cell)) !== null && _a !== void 0 ? _a : {};
dict[key] = value === undefined ? 1 : value;
markedValue.set(cell, dict);
}
static __unmark(marked) {
marked.forEach(cell => {
const dict = markedValue.get(cell);
if (dict) {
Object.keys(dict).forEach((key) => {
const value = dict[key];
switch (key) {
case 'remove':
Dom.safeRemove(cell);
break;
case 'rowspan':
attr(cell, 'rowspan', isNumber(value) && value > 1 ? value : null);
break;
case 'colspan':
attr(cell, 'colspan', isNumber(value) && value > 1 ? value : null);
break;
case 'width':
if (value == null) {
cell.style.removeProperty('width');
if (!attr(cell, 'style')) {
attr(cell, 'style', null);
}
}
else {
cell.style.width = value.toString();
}
break;
}
delete dict[key];
});
markedValue.delete(cell);
}
});
}
}
Table.__selectedByTable = new WeakMap();
__decorate([
debounce()
], Table.prototype, "__recalculateStyles", null);
const instance = (j) => j.getInstance('Table', j.o);