import Quill from 'quill';
import {
filterWordStyle,
getCellChildBlot,
getCellId
} from '../utils';
import TableHeader from './header';
import { ListContainer } from './list';
import { CELL_DEFAULT_WIDTH } from '../config';
const Block = Quill.import('blots/block');
const Break = Quill.import('blots/break');
const Container = Quill.import('blots/container');
const CELL_ATTRIBUTE = ['data-row', 'width', 'height', 'colspan', 'rowspan', 'style'];
const TABLE_ATTRIBUTE = ['border', 'cellspacing', 'style'];
const STYLE_RULES = ['color', 'border', 'width', 'height'];
const COL_ATTRIBUTE = ['width'];
class TableCellBlock extends Block {
static create(value: string) {
const node = super.create();
if (value) {
node.setAttribute('data-cell', value);
} else {
node.setAttribute('data-cell', cellId());
}
return node;
}
getAlign() {
for (const name of this.domNode.classList) {
if (/ql-align-/.test(name)) {
return name.split('ql-align-')[1];
}
}
return '';
}
format(name: string, value: string | Props) {
const cellId = this.formats()[this.statics.blotName];
if (name === TableCell.blotName && value) {
this.wrap(TableRow.blotName);
return this.wrap(name, value);
} else if (name === TableContainer.blotName) {
this.wrap(name, value);
} else if (name === 'header') {
return this.replaceWith('table-header', { cellId, value });
} else if (name === 'list') {
this.wrap(ListContainer.blotName, cellId);
return this.replaceWith('table-list', value);
} else {
super.format(name, value);
}
}
formats() {
return { [this.statics.blotName]: this.domNode.getAttribute('data-cell') };
}
}
TableCellBlock.blotName = 'table-cell-block';
TableCellBlock.className = 'ql-table-block';
TableCellBlock.tagName = 'P';
class TableCell extends Container {
checkMerge() {
if (
super.checkMerge() &&
this.next.children.head != null &&
this.next.children.head.formats
) {
const thisHead = this.children.head.formats()[this.children.head.statics.blotName];
const thisTail = this.children.tail.formats()[this.children.tail.statics.blotName];
const nextHead = this.next.children.head.formats()[this.next.children.head.statics.blotName];
const nextTail = this.next.children.tail.formats()[this.next.children.tail.statics.blotName];
const _thisHead = getCellId(thisHead);
const _thisTail = getCellId(thisTail);
const _nextHead = getCellId(nextHead);
const _nextTail = getCellId(nextTail);
return (
_thisHead === _thisTail &&
_thisHead === _nextHead &&
_thisHead === _nextTail
);
}
return false;
}
static create(value: Props) {
const node = super.create();
const keys = Object.keys(value);
for (const key of keys) {
value[key] && node.setAttribute(key, value[key]);
}
return node;
}
static formats(domNode: HTMLElement) {
const rowspan = this.getEmptyRowspan(domNode);
const formats = CELL_ATTRIBUTE.reduce((formats: Props, attr) => {
if (domNode.hasAttribute(attr)) {
if (attr === 'rowspan' && rowspan) {
formats[attr] = `${~~domNode.getAttribute(attr) - rowspan}`;
} else {
formats[attr] = filterWordStyle(domNode.getAttribute(attr));
}
}
return formats;
}, {});
if (this.hasColgroup(domNode)){
delete formats['width'];
if (formats['style']) {
(formats['style'] = formats['style'].replace(/width.*?;/g, ''));
}
}
return formats;
}
formats() {
const formats = this.statics.formats(this.domNode, this.scroll);
return { [this.statics.blotName]: formats };
}
static getEmptyRowspan(domNode: Element) {
let nextNode = domNode.parentElement.nextElementSibling;
let rowspan = 0;
while (nextNode && !nextNode.innerHTML.replace(/\s/g, '')) {
rowspan++;
nextNode = nextNode.nextElementSibling;
}
return rowspan;
}
static hasColgroup(domNode: Element) {
while (domNode && domNode.tagName !== 'TBODY') {
domNode = domNode.parentElement;
}
while (domNode) {
if (domNode.tagName === 'COLGROUP') {
return true;
}
domNode = domNode.previousElementSibling;
}
return false;
}
html() {
const reg = /<(ol)[^>]*>
]* data-list="bullet">(?:.*?)<\/li><\/(ol)>/gi;
return this.domNode.outerHTML.replace(reg, (
match: string, $1: string, $2: string
) => {
return match.replace($1, 'ul').replace($2, 'ul');
});
}
row() {
return this.parent;
}
rowOffset() {
if (this.row()) {
return this.row().rowOffset();
}
return -1;
}
table() {
let cur = this.parent;
while (cur != null && cur.statics.blotName !== 'table-container') {
cur = cur.parent;
}
return cur;
}
optimize(...args: unknown[]) {
super.optimize(...args);
this.children.forEach((child: TableCellBlock | TableHeader | ListContainer) => {
if (child.next == null) return;
const childFormats = getCellId(child.formats()[child.statics.blotName]);
const nextFormats = getCellId(child.next.formats()[child.next.statics.blotName]);
if (childFormats !== nextFormats) {
const next = this.splitAfter(child);
if (next) next.optimize();
// We might be able to merge with prev now
if (this.prev) this.prev.optimize();
}
});
}
}
TableCell.blotName = 'table-cell';
TableCell.tagName = 'TD';
class TableRow extends Container {
checkMerge() {
if (
super.checkMerge() &&
this.next.children.head != null &&
this.next.children.head.formats
) {
const thisHead = this.children.head.formats()[this.children.head.statics.blotName];
const thisTail = this.children.tail.formats()[this.children.tail.statics.blotName];
const nextHead = this.next.children.head.formats()[this.next.children.head.statics.blotName];
const nextTail = this.next.children.tail.formats()[this.next.children.tail.statics.blotName];
return (
thisHead['data-row'] === thisTail['data-row'] &&
thisHead['data-row'] === nextHead['data-row'] &&
thisHead['data-row'] === nextTail['data-row']
);
}
return false;
}
rowOffset() {
if (this.parent) {
return this.parent.children.indexOf(this);
}
return -1;
}
}
TableRow.blotName = 'table-row';
TableRow.tagName = 'TR';
class TableBody extends Container {}
TableBody.blotName = 'table-body';
TableBody.tagName = 'TBODY';
class TableTemporary extends Block {
static create(value: Props) {
const node = super.create();
const keys = Object.keys(value);
for (const key of keys) {
node.setAttribute(key, value[key]);
}
return node;
}
static formats(domNode: Element) {
return TABLE_ATTRIBUTE.reduce((formats: Props, attr) => {
if (domNode.hasAttribute(attr)) {
formats[attr] = domNode.getAttribute(attr);
}
return formats;
}, {});
}
formats() {
const formats = this.statics.formats(this.domNode, this.scroll);
return { [this.statics.blotName]: formats };
}
optimize(...args: unknown[]) {
if (
this.statics.requiredContainer &&
this.parent instanceof this.statics.requiredContainer
) {
const formats = this.formats()[this.statics.blotName];
for (const key of TABLE_ATTRIBUTE) {
if (formats[key]) {
this.parent.domNode.setAttribute(key, formats[key]);
} else {
this.parent.domNode.removeAttribute(key);
}
}
}
super.optimize(...args);
}
}
TableTemporary.blotName = 'table-temporary';
TableTemporary.className = 'ql-table-temporary';
TableTemporary.tagName = 'temporary';
class TableCol extends Block {
static create(value: Props) {
const node = super.create();
const keys = Object.keys(value);
for (const key of keys) {
node.setAttribute(key, value[key]);
}
return node;
}
static formats(domNode: Element) {
return COL_ATTRIBUTE.reduce((formats: Props, attr) => {
if (domNode.hasAttribute(attr)) {
formats[attr] = domNode.getAttribute(attr);
}
return formats;
}, {});
}
formats() {
const formats = this.statics.formats(this.domNode, this.scroll);
return { [this.statics.blotName]: formats };
}
html() {
return this.domNode.outerHTML;
}
}
TableCol.blotName = 'table-col';
TableCol.tagName = 'COL';
class TableColgroup extends Container {}
TableColgroup.blotName = 'table-colgroup';
TableColgroup.tagName = 'COLGROUP';
class TableContainer extends Container {
colgroup() {
const [colgroup] = this.descendant(TableColgroup);
return colgroup || this.findChild('table-colgroup');
}
deleteColumn(
changeTds: [Element, number][],
delTds: Element[],
hideMenus: () => void,
cols: Element[] = []
) {
const body = this.tbody();
const tableCells = this.descendants(TableCell);
if (body == null || body.children.head == null) return;
if (delTds.length === tableCells.length) {
this.deleteTable();
hideMenus();
} else {
for (const td of [...delTds, ...cols]) {
if (td.parentElement.children.length === 1) {
this.setCellRowspan(td.parentElement.previousElementSibling);
}
td.remove();
}
for (const [td, offset] of changeTds) {
this.setCellColspan(Quill.find(td), offset);
}
}
}
deleteRow(rows: TableRow[], hideMenus: () => void) {
const body = this.tbody();
if (body == null || body.children.head == null) return;
if (rows.length === body.children.length) {
this.deleteTable();
hideMenus();
} else {
const maxColumns = this.getMaxColumns(body.children.head.children);
for (const row of rows) {
const prev = this.getCorrectRow(row, maxColumns);
prev && prev.children.forEach((child: TableCell) => {
const rowspan = ~~child.domNode.getAttribute('rowspan');
if (rowspan > 1) {
child.domNode.setAttribute('rowspan', rowspan - 1);
}
});
row.remove();
}
}
}
deleteTable() {
this.remove();
}
findChild(blotName: string) {
let child = this.children.head;
while (child) {
if (child.statics.blotName === blotName) {
return child;
}
child = child.next;
}
return null;
}
getCorrectRow(prev: TableRow, maxColumns: number) {
let isCorrect = false;
while (prev && !isCorrect) {
const prevMaxColumns = this.getMaxColumns(prev.children);
if (maxColumns === prevMaxColumns) {
isCorrect = true;
return prev;
}
prev = prev.prev;
}
return prev;
}
getInsertRow(prev: TableRow, offset: number) {
const body = this.tbody();
if (body == null || body.children.head == null) return;
const id = tableId();
const row = this.scroll.create(TableRow.blotName);
const maxColumns = this.getMaxColumns(body.children.head.children);
const nextMaxColumns = this.getMaxColumns(prev.children);
if (nextMaxColumns === maxColumns) {
prev.children.forEach((child: TableCell) => {
const formats = { height: '24', 'data-row': id };
const colspan = ~~child.domNode.getAttribute('colspan');
this.insertTableCell(colspan, formats, row);
});
return row;
} else {
prev = this.getCorrectRow(prev.prev, maxColumns);
prev.children.forEach((child: TableCell) => {
const formats = { height: '24', 'data-row': id };
const colspan = ~~child.domNode.getAttribute('colspan');
const rowspan = ~~child.domNode.getAttribute('rowspan');
if (rowspan > 1) {
child.domNode.setAttribute('rowspan', rowspan + 1);
} else {
this.insertTableCell(colspan, formats, row);
}
});
return row;
}
}
getMaxColumns(children: TableCell[]) {
return children.reduce((num: number, child: TableCell) => {
const colspan = ~~child.domNode.getAttribute('colspan') || 1;
return num += colspan;
}, 0);
}
insertColumn(position: number, isLast: boolean, w: number) {
const colgroup = this.colgroup();
const body = this.tbody();
if (body == null || body.children.head == null) return;
const columnCells: [TableRow, string, TableCell | null][] = [];
const cols: [TableColgroup, TableCol | null][] = [];
let row = body.children.head;
while (row) {
if (isLast) {
const id = row.children.tail.domNode.getAttribute('data-row');
columnCells.push([row, id, null]);
} else {
let ref = row.children.head;
while (ref) {
const { left, right } = ref.domNode.getBoundingClientRect();
const id = ref.domNode.getAttribute('data-row');
if (Math.abs(left - position) <= 2) {
columnCells.push([row, id, ref]);
break;
} else if (Math.abs(right - position) <= 2 && !ref.next) {
columnCells.push([row, id, null]);
break;
// rowspan > 1 (insertLeft, position + w is left)
} else if (Math.abs(left - position - w) <= 2) {
columnCells.push([row, id, ref]);
break;
// rowspan > 1 (position between left and right, rowspan++)
} else if (position > left && position < right) {
columnCells.push([null, id, ref]);
break;
}
ref = ref.next;
}
}
row = row.next;
}
if (colgroup) {
if (isLast) {
cols.push([colgroup, null]);
} else {
let correctLeft = 0;
let correctRight = 0;
let col = colgroup.children.head;
while (col) {
const { left, width } = col.domNode.getBoundingClientRect();
correctLeft = correctLeft ? correctLeft : left;
correctRight = correctLeft + width;
if (Math.abs(correctLeft - position) <= 2) {
cols.push([colgroup, col]);
break;
} else if (Math.abs(correctRight - position) <= 2 && !col.next) {
cols.push([colgroup, null]);
break;
}
correctLeft += width;
col = col.next;
}
}
}
for (const [row, id, ref] of columnCells) {
if (!row) {
this.setCellColspan(ref, 1);
} else {
this.insertColumnCell(row, id, ref);
}
}
for (const [colgroup, ref] of cols) {
this.insertCol(colgroup, ref);
}
}
insertCol(colgroup: TableColgroup, ref: TableCol | null) {
const col = this.scroll.create(TableCol.blotName, { width: `${CELL_DEFAULT_WIDTH}` });
colgroup.insertBefore(col, ref);
}
insertColumnCell(row: TableRow, id: string, ref: TableCell | null) {
const colgroup = this.colgroup();
const formats = colgroup ? { 'data-row': id } : { 'data-row': id, width: `${CELL_DEFAULT_WIDTH}` };
const cell = this.scroll.create(TableCell.blotName, formats);
const cellBlock = this.scroll.create(TableCellBlock.blotName, cellId());
cell.appendChild(cellBlock);
row.insertBefore(cell, ref);
cellBlock.optimize();
}
insertRow(index: number, offset: number) {
const body = this.tbody();
if (body == null || body.children.head == null) return;
const ref = body.children.at(index);
const prev = ref ? ref : body.children.at(index - 1);
const correctRow = this.getInsertRow(prev, offset);
body.insertBefore(correctRow, ref);
}
insertTableCell(colspan: number, formats: Props, row: TableRow) {
if (colspan > 1) {
Object.assign(formats, { colspan });
} else {
delete formats['colspan'];
}
const cell = this.scroll.create(TableCell.blotName, formats);
const cellBlock = this.scroll.create(TableCellBlock.blotName, cellId());
cell.appendChild(cellBlock);
row.appendChild(cell);
cellBlock.optimize();
}
optimize(...args: unknown[]) {
super.optimize(...args);
const temporaries = this.descendants(TableTemporary);
if (temporaries.length > 1) {
temporaries.shift();
for (const temporary of temporaries) {
temporary.remove();
}
}
}
setCellColspan(cell: TableCell, offset: number) {
const blotName = cell.statics.blotName;
const formats = cell.formats()[blotName];
const colspan = (~~formats['colspan'] || 1) + offset;
const childBlot = getCellChildBlot(cell);
if (colspan > 1) {
Object.assign(formats, { colspan });
} else {
delete formats['colspan'];
}
childBlot.format(blotName, formats);
}
setCellRowspan(parentElement: Element) {
while (parentElement) {
const children = parentElement.querySelectorAll('td[rowspan]');
if (children.length) {
for (const child of children) {
const cell = Quill.find(child);
const blotName = cell.statics.blotName;
const formats = cell.formats()[blotName];
const rowspan = (~~formats['rowspan'] || 1) - 1;
const childBlot = getCellChildBlot(cell);
if (rowspan > 1) {
Object.assign(formats, { rowspan });
} else {
delete formats['rowspan'];
}
childBlot.format(blotName, formats);
}
break;
}
parentElement = parentElement.previousElementSibling;
}
}
tbody() {
const [body] = this.descendant(TableBody);
return body || this.findChild('table-body');
}
temporary() {
const [temporary] = this.descendant(TableTemporary);
return temporary;
}
}
TableContainer.blotName = 'table-container';
TableContainer.className = 'ql-table-better';
TableContainer.tagName = 'TABLE';
TableContainer.allowedChildren = [TableBody, TableTemporary, TableColgroup];
TableBody.requiredContainer = TableContainer;
TableTemporary.requiredContainer = TableContainer;
TableColgroup.requiredContainer = TableContainer;
TableBody.allowedChildren = [TableRow];
TableRow.requiredContainer = TableBody;
TableColgroup.allowedChildren = [TableCol];
TableCol.requiredContainer = TableColgroup;
TableRow.allowedChildren = [TableCell];
TableCell.requiredContainer = TableRow;
TableCell.allowedChildren = [TableCellBlock, TableHeader, ListContainer];
TableCellBlock.requiredContainer = TableCell;
TableHeader.requiredContainer = TableCell;
ListContainer.requiredContainer = TableCell;
function cellId() {
const id = Math.random()
.toString(36)
.slice(2, 6);
return `cell-${id}`;
}
function tableId() {
const id = Math.random()
.toString(36)
.slice(2, 6);
return `row-${id}`;
}
export {
cellId,
TableCellBlock,
TableCell,
TableRow,
TableBody,
TableTemporary,
TableContainer,
tableId,
TableCol,
TableColgroup
};