jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
974 lines (856 loc) • 22.3 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Released under MIT see LICENSE.txt in the project root for license information.
* Copyright (c) 2013-2020 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
/**
* Module for working with tables . Delete, insert , merger, division of cells , rows and columns.
* When creating elements such as <table> for each of them
* creates a new instance Jodit.modules.TableProcessor and it can be accessed via $('table').data('table-processor')
*
* @module Table
* @param {Object} parent Jodit main object
* @param {HTMLTableElement} table Table for which to create a module
*/
import * as consts from '../core/constants';
import { Dom } from '../core/dom';
import { $$, attr, cssPath, each, trim } from '../core/helpers/';
import { ICreate, IJodit } from '../types';
import { ViewComponent } from '../core/component';
import { getContainer } from '../core/global';
import { debounce } from '../core/decorators';
export class Table extends ViewComponent<IJodit> {
private selected: Set<HTMLTableCellElement> = new Set();
private static selectedByTable: WeakMap<
HTMLTableElement,
Set<HTMLTableCellElement>
> = new WeakMap();
@debounce()
private recalculateStyles(): void {
const style = getContainer(this.j, Table, 'style', true);
const selectors: string[] = [];
this.selected.forEach(td => {
const selector = cssPath(td);
selector && selectors.push(selector);
});
style.innerHTML = selectors.length
? selectors.join(',') + `{${this.j.o.table.selectionCellStyle}}`
: '';
}
addSelection(td: HTMLTableCellElement): void {
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: HTMLTableCellElement): void {
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(): HTMLTableCellElement[] {
return Array.from(this.selected);
}
static getSelectedCellsByTable(
table: HTMLTableElement
): HTMLTableCellElement[] {
const cells = Table.selectedByTable.get(table);
return cells ? Array.from(cells) : [];
}
/** @override **/
destruct(): any {
this.selected.clear();
return super.destruct();
}
/**
* Returns rows count in the table
* @param table
*/
static getRowsCount(table: HTMLTableElement): number {
return table.rows.length;
}
/**
* Returns columns count in the table
* @param table
*/
static getColumnsCount(table: HTMLTableElement): number {
const matrix = Table.formalMatrix(table);
return matrix.reduce((max_count, cells) => {
return Math.max(max_count, cells.length);
}, 0);
}
/**
* Generate formal table martix columns*rows
*
* @param {HTMLTableElement} table
* @param {function(HTMLTableCellElement, int, int, int, int):boolean} [callback] if return false cycle break
* @return {Array}
*/
static formalMatrix(
table: HTMLTableElement,
callback?: (
cell: HTMLTableCellElement,
row: number,
col: number,
colSpan: number,
rowSpan: number
) => false | void
): HTMLTableCellElement[][] {
const matrix: HTMLTableCellElement[][] = [[]];
const rows = Array.from(table.rows);
const setCell = (
cell: HTMLTableCellElement,
i: number
): false | HTMLTableCellElement[][] | void => {
if (matrix[i] === undefined) {
matrix[i] = [];
}
const colSpan: number = cell.colSpan,
rowSpan = cell.rowSpan;
let column: number,
row: number,
currentColumn: number = 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 = Array.from<HTMLTableCellElement>(rows[i].cells);
for (let j = 0; j < cells.length; j += 1) {
if (setCell(cells[j], i) === false) {
return matrix;
}
}
}
return matrix;
}
/**
* Get cell coordinate in formal table (without colspan and rowspan)
*
* @param table
* @param cell
* @param max
*/
static formalCoordinate(
table: HTMLTableElement,
cell: HTMLTableCellElement,
max = false
): number[] {
let i: number = 0,
j: number = 0,
width: number = 1,
height: number = 1;
Table.formalMatrix(
table,
(
td: HTMLTableCellElement,
ii: number,
jj: number,
colSpan: number | void,
rowSpan: number | void
): false | void => {
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];
}
/**
* Inserts a new line after row what contains the selected cell
*
* @param {HTMLTableElement} table
* @param {Boolean|HTMLTableRowElement} [line=false] Insert a new line after/before this
* line contains the selected cell
* @param {Boolean} [after=true] Insert a new line after line contains the selected cell
*/
static appendRow(
table: HTMLTableElement,
line: false | HTMLTableRowElement,
after: boolean,
create: ICreate
): void {
let row: HTMLTableRowElement;
if (!line) {
const columnsCount: number = Table.getColumnsCount(table);
row = create.element('tr');
for (let j: number = 0; j < columnsCount; j += 1) {
row.appendChild(create.element('td'));
}
} else {
row = line.cloneNode(true) as HTMLTableRowElement;
$$('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 {
($$(':scope>tbody', table)[0] || table).appendChild(row);
}
}
/**
* Remove row
*
* @param {HTMLTableElement} table
* @param {int} rowIndex
*/
static removeRow(table: HTMLTableElement, rowIndex: number): void {
const box = Table.formalMatrix(table);
let dec: boolean;
const row = table.rows[rowIndex];
each<HTMLTableCellElement>(
box[rowIndex],
(j: number, cell: HTMLTableCellElement) => {
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: HTMLTableRowElement = Dom.next(
cell.parentNode,
(elm: Node | null) => Dom.isTag(elm, 'tr'),
table
) as HTMLTableRowElement;
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: number = cell.rowSpan;
attr(
cell,
'rowspan',
rowSpan - 1 > 1 ? (rowSpan - 1).toString() : null
);
}
}
);
Dom.safeRemove(row);
}
/**
* Insert column before / after all the columns containing the selected cells
*
* @param table
* @param j
* @param after
* @param create
*/
static appendColumn(
table: HTMLTableElement,
j: number,
after: boolean,
create: ICreate
): void {
const box: HTMLTableCellElement[][] = Table.formalMatrix(table);
let i: number;
if (j === undefined || j < 0) {
j = Table.getColumnsCount(table) - 1;
}
for (i = 0; i < box.length; i += 1) {
const cell = create.element('td');
const td: HTMLTableCellElement = box[i][j];
let added: boolean = false;
if (after) {
if (
(box[i] && td && j + 1 >= box[i].length) ||
td !== box[i][j + 1]
) {
if (td.nextSibling) {
td.parentNode &&
td.parentNode.insertBefore(cell, td.nextSibling);
} else {
td.parentNode && td.parentNode.appendChild(cell);
}
added = true;
}
} else {
if (
j - 1 < 0 ||
(box[i][j] !== box[i][j - 1] && box[i][j].parentNode)
) {
td.parentNode &&
td.parentNode.insertBefore(cell, box[i][j]);
added = true;
}
}
if (!added) {
box[i][j].setAttribute(
'colspan',
(
parseInt(attr(box[i][j], 'colspan') || '1', 10) + 1
).toString()
);
}
}
}
/**
* Remove column by index
*
* @param {HTMLTableElement} table
* @param {int} [j]
*/
static removeColumn(table: HTMLTableElement, j: number): void {
const box: HTMLTableCellElement[][] = Table.formalMatrix(table);
let dec: boolean;
each(box, (i: number, cells: HTMLTableCellElement[]) => {
const td: HTMLTableCellElement = 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: number = td.colSpan;
attr(
td,
'colspan',
colSpan - 1 > 1 ? (colSpan - 1).toString() : null
);
}
});
}
/**
* Define bound for selected cells
*
* @param {HTMLTableElement} table
* @param {Array.<HTMLTableCellElement>} selectedCells
* @return {number[][]}
*/
static getSelectedBound(
table: HTMLTableElement,
selectedCells: HTMLTableCellElement[]
): number[][] {
const bound = [
[Infinity, Infinity],
[0, 0]
];
const box = Table.formalMatrix(table);
let i: number, j: number, k: number;
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;
}
/**
* Try recalculate all coluns and rows after change
* @param {HTMLTableElement} table
*/
static normalizeTable(table: HTMLTableElement): void {
let i: number, j: number, min: number, not: boolean;
const __marked: HTMLTableCellElement[] = [],
box: HTMLTableCellElement[][] = Table.formalMatrix(table);
// remove extra colspans
for (j = 0; j < box[0].length; j += 1) {
min = 1000000;
not = false;
for (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 (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
);
}
}
}
// remove extra rowspans
for (i = 0; i < box.length; i += 1) {
min = 1000000;
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
);
}
}
}
// remove rowspans and colspans equal 1 and empty class
for (i = 0; i < box.length; i += 1) {
for (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
) {
box[i][j].removeAttribute('rowspan');
}
if (
box[i][j].hasAttribute('colspan') &&
box[i][j].colSpan === 1
) {
box[i][j].removeAttribute('colspan');
}
if (
box[i][j].hasAttribute('class') &&
!attr(box[i][j], 'class')
) {
box[i][j].removeAttribute('class');
}
}
}
Table.__unmark(__marked);
}
/**
* It combines all of the selected cells into one. The contents of the cells will also be combined
* @param table
*/
static mergeSelected(table: HTMLTableElement, jodit: IJodit): void {
const html: string[] = [],
bound: number[][] = Table.getSelectedBound(
table,
Table.getSelectedCellsByTable(table)
);
let w: number = 0,
first: HTMLTableCellElement | null = null,
first_j: number = 0,
td: HTMLTableCellElement,
cols: number = 0,
rows: number = 0;
const __marked: HTMLTableCellElement[] = [];
if (bound && (bound[0][0] - bound[1][0] || bound[0][1] - bound[1][1])) {
Table.formalMatrix(
table,
(
cell: HTMLTableCellElement,
i: number,
j: number,
cs: number,
rs: number
) => {
if (i >= bound[0][0] && i <= bound[1][0]) {
if (j >= bound[0][1] && j <= bound[1][1]) {
td = cell;
if ((td as any).__i_am_already_was) {
return;
}
(td as any).__i_am_already_was = true;
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 as HTMLTableCellElement).innerHTML = html.join('<br/>');
instance(jodit).addSelection(first);
delete (first as any).__i_am_already_was;
Table.__unmark(__marked);
Table.normalizeTable(table);
each(Array.from(table.rows), (index, tr) => {
if (!tr.cells.length) {
Dom.safeRemove(tr);
}
});
}
}
}
/**
* Divides all selected by `jodit_focused_cell` class table cell in 2 parts vertical. Those division into 2 columns
*/
static splitHorizontal(table: HTMLTableElement, jodit: IJodit): void {
let coord: number[],
td: HTMLTableCellElement,
tr: HTMLTableRowElement,
parent: HTMLTableRowElement,
after: HTMLTableCellElement;
const __marked: HTMLTableCellElement[] = [];
Table.getSelectedCellsByTable(table).forEach(
(cell: HTMLTableCellElement) => {
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) as HTMLTableRowElement,
tr
);
tr.appendChild(td);
} else {
Table.__mark(cell, 'rowspan', cell.rowSpan - 1, __marked);
Table.formalMatrix(
table,
(tdElm: HTMLTableCellElement, i: number, j: number) => {
if (
i > coord[0] &&
i < coord[0] + cell.rowSpan &&
coord[1] > j &&
(tdElm.parentNode as HTMLTableRowElement)
.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);
}
/**
* It splits all the selected cells into 2 parts horizontally. Those. are added new row
*/
static splitVertical(table: HTMLTableElement, jodit: IJodit): void {
let coord: number[], td: HTMLTableCellElement, percentage: number;
const __marked: HTMLTableCellElement[] = [];
Table.getSelectedCellsByTable(table).forEach(
(cell: HTMLTableCellElement) => {
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);
}
/**
* Set column width used delta value
*
* @param {HTMLTableElement} table
* @param {int} j column
* @param {int} delta
* @param {boolean} noUnmark
* @param {HTMLTableCellElement[]} marked
*/
static setColumnWidthByDelta(
table: HTMLTableElement,
j: number,
delta: number,
noUnmark: boolean,
marked: HTMLTableCellElement[]
): void {
const box = Table.formalMatrix(table);
let i: number, w: number, percent: number;
for (i = 0; i < box.length; i += 1) {
w = box[i][j].offsetWidth;
percent = ((w + delta) / table.offsetWidth) * 100;
Table.__mark(
box[i][j],
'width',
percent.toFixed(consts.ACCURACY) + '%',
marked
);
}
if (!noUnmark) {
Table.__unmark(marked);
}
}
/**
*
* @param {HTMLTableCellElement} cell
* @param {string} key
* @param {string} value
* @param {HTMLTableCellElement[]} marked
* @private
*/
private static __mark(
cell: HTMLTableCellElement,
key: string,
value: string | number,
marked: HTMLTableCellElement[]
) {
marked.push(cell);
if (!(cell as any).__marked_value) {
(cell as any).__marked_value = {};
}
(cell as any).__marked_value[key] = value === undefined ? 1 : value;
}
private static __unmark(marked: HTMLTableCellElement[]) {
marked.forEach(cell => {
if ((cell as any).__marked_value) {
each(
(cell as any).__marked_value,
(key: string, value: number) => {
switch (key) {
case 'remove':
Dom.safeRemove(cell);
break;
case 'rowspan':
if (value > 1) {
cell.setAttribute(
'rowspan',
value.toString()
);
} else {
cell.removeAttribute('rowspan');
}
break;
case 'colspan':
if (value > 1) {
cell.setAttribute(
'colspan',
value.toString()
);
} else {
cell.removeAttribute('colspan');
}
break;
case 'width':
cell.style.width = value.toString();
break;
}
delete (cell as any).__marked_value[key];
}
);
delete (cell as any).__marked_value;
}
});
}
}
const instance = (j: IJodit): Table => j.getInstance<Table>('Table', j.o);