jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
962 lines (847 loc) • 23.3 kB
text/typescript
/*!
* Jodit Editor (https://xdsoft.net/jodit/)
* Licensed under GNU General Public License version 2 or later or a commercial license or MIT;
* For GPL see LICENSE-GPL.txt in the project root for license information.
* For MIT see LICENSE-MIT.txt in the project root for license information.
* For commercial licenses see https://xdsoft.net/jodit/commercial/
* Copyright (c) 2013-2019 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
*/
import { Config } from '../Config';
import * as consts from '../constants';
import { Plugin } from '../modules/Plugin';
import { Dom } from '../modules/Dom';
import { Table } from '../modules/Table';
import {
$$,
getContentWidth,
offset,
scrollIntoView
} from '../modules/helpers/';
import { setTimeout } from '../modules/helpers/async';
import { IControlType } from '../types/toolbar';
import { IBound, IDictionary } from '../types/types';
import { IJodit } from '../types';
declare module '../Config' {
interface Config {
/**
* Use module {@link TableProcessor|TableProcessor}
*/
useTableProcessor: boolean;
useExtraClassesOptions: boolean;
}
}
Config.prototype.useTableProcessor = true;
Config.prototype.useExtraClassesOptions = true;
Config.prototype.controls.table = {
data: {
cols: 10,
rows: 10,
classList: {
'table table-bordered': 'Bootstrap Bordered',
'table table-striped': 'Bootstrap Striped',
'table table-dark': 'Bootstrap Dark'
}
},
popup: (editor: IJodit, current, control, close, button) => {
const default_rows_count: number =
control.data && control.data.rows ? control.data.rows : 10,
default_cols_count: number =
control.data && control.data.cols ? control.data.cols : 10;
const generateExtraClasses = (): string => {
if (!editor.options.useExtraClassesOptions) {
return '';
}
const out: string[] = [];
if (control.data) {
const classList: IDictionary<string> = control.data.classList;
Object.keys(classList).forEach((classes: string) => {
out.push(
`<label><input value="${classes}" type="checkbox"/>${
classList[classes]
}</label>`
);
});
}
return out.join('');
};
const form: HTMLFormElement = editor.create.fromHTML(
'<form class="jodit_form jodit_form_inserter">' +
'<label>' +
'<span>1</span> × <span>1</span>' +
'</label>' +
'<div class="jodit_form-table-creator-box">' +
'<div class="jodit_form-container"></div>' +
'<div class="jodit_form-options">' +
generateExtraClasses() +
'</div>' +
'</div>' +
'</form>'
) as HTMLFormElement,
rows: HTMLSpanElement = form.querySelectorAll('span')[0],
cols: HTMLSpanElement = form.querySelectorAll('span')[1],
blocksContainer: HTMLDivElement = form.querySelector(
'.jodit_form-container'
) as HTMLDivElement,
mainBox: HTMLDivElement = form.querySelector(
'.jodit_form-table-creator-box'
) as HTMLDivElement,
options: HTMLDivElement = form.querySelector(
'.jodit_form-options'
) as HTMLDivElement,
cells: HTMLDivElement[] = [];
const generateRows = (need_rows: number) => {
const cnt: number = need_rows * default_cols_count;
if (cells.length > cnt) {
for (let i = cnt; i < cells.length; i += 1) {
Dom.safeRemove(cells[i]);
delete cells[i];
}
cells.length = cnt;
}
for (let i = 0; i < cnt; i += 1) {
if (!cells[i]) {
const div = editor.create.div();
div.setAttribute('data-index', i.toString());
cells.push(div);
}
}
cells.forEach((cell: HTMLDivElement) => {
blocksContainer.appendChild(cell);
});
const width = (cells[0].offsetWidth || 18) * default_cols_count;
blocksContainer.style.width = width + 'px';
mainBox.style.width = width + options.offsetWidth + 1 + 'px';
};
const mouseenter = (e: MouseEvent, index?: number): void => {
const dv: HTMLDivElement = e.target as HTMLDivElement;
if (!dv || dv.tagName !== 'DIV') {
return;
}
let k =
index === undefined || isNaN(index)
? parseInt(dv.getAttribute('data-index') || '0', 10)
: index || 0;
const rows_count = Math.ceil((k + 1) / default_cols_count),
cols_count = (k % default_cols_count) + 1;
for (let i = 0; i < cells.length; i += 1) {
if (
cols_count >= (i % default_cols_count) + 1 &&
rows_count >= Math.ceil((i + 1) / default_cols_count)
) {
cells[i].className = 'hovered';
} else {
cells[i].className = '';
}
}
cols.innerText = cols_count.toString();
rows.innerText = rows_count.toString();
};
blocksContainer.addEventListener('mousemove', mouseenter);
editor.events.on(
blocksContainer,
'touchstart mousedown',
(e: MouseEvent) => {
const dv: HTMLDivElement = e.target as HTMLDivElement;
e.preventDefault();
e.stopImmediatePropagation();
if (dv.tagName !== 'DIV') {
return;
}
let k = parseInt(dv.getAttribute('data-index') || '0', 10);
const rows_count = Math.ceil((k + 1) / default_cols_count),
cols_count = (k % default_cols_count) + 1;
const crt = editor.create.inside,
tbody: HTMLTableSectionElement = crt.element('tbody'),
table: HTMLTableElement = crt.element('table');
table.appendChild(tbody);
table.style.width = '100%';
let first_td: HTMLTableCellElement | null = null,
tr: HTMLTableRowElement,
td: HTMLTableCellElement;
for (let i = 1; i <= rows_count; i += 1) {
tr = crt.element('tr');
for (let j = 1; j <= cols_count; j += 1) {
td = crt.element('td');
if (!first_td) {
first_td = td;
}
td.appendChild(crt.element('br'));
tr.appendChild(crt.text('\n'));
tr.appendChild(crt.text('\t'));
tr.appendChild(td);
}
tbody.appendChild(crt.text('\n'));
tbody.appendChild(tr);
}
const crnt = editor.selection.current();
if (crnt && editor.selection.isCollapsed()) {
const block: HTMLElement | false = Dom.closest(
crnt,
node => Dom.isBlock(node, editor.editorWindow),
editor.editor
) as HTMLElement | false;
if (
block &&
block !== editor.editor &&
!block.nodeName.match(
/^TD|TH|TBODY|TABLE|THEADER|TFOOTER$/
)
) {
editor.selection.setCursorAfter(block);
}
}
$$('input[type=checkbox]:checked', options).forEach(
(input: HTMLElement) => {
(input as HTMLInputElement).value
.split(/[\s]+/)
.forEach((className: string) => {
table.classList.add(className);
});
}
);
editor.selection.insertNode(crt.text('\n'));
editor.selection.insertNode(table, false);
if (first_td) {
editor.selection.setCursorIn(first_td);
scrollIntoView(
first_td,
editor.editor,
editor.editorDocument
);
}
close();
}
);
if (button && button.parentToolbar) {
editor.events
.off(
button.parentToolbar.container as object,
'afterOpenPopup.tableGenerator'
)
.on(
button.parentToolbar.container as object,
'afterOpenPopup.tableGenerator',
() => {
generateRows(default_rows_count);
if (cells[0]) {
cells[0].className = 'hovered';
}
},
'',
true
);
}
return form;
},
tooltip: 'Insert table'
} as IControlType;
/**
* Process tables in editor
*/
export class TableProcessor extends Plugin {
static isCell(tag: Node | null): boolean {
return !!tag && /^TD|TH$/i.test(tag.nodeName);
}
private __key: string = 'table_processor_observer';
private __selectMode: boolean = false;
private __resizerDelta: number = 0;
private __resizerHandler: HTMLElement;
showResizer() {
clearTimeout(this.hideTimeout);
this.__resizerHandler.style.display = 'block';
}
hideResizer() {
clearTimeout(this.hideTimeout);
this.hideTimeout = setTimeout(() => {
this.__resizerHandler.style.display = 'none';
}, this.jodit.defaultTimeout);
}
private hideTimeout: number;
private __drag: boolean = false;
private __wholeTable: boolean | null;
private __workCell: HTMLTableCellElement;
private __workTable: HTMLTableElement;
private __minX: number;
private __maxX: number;
/**
*
* @param {HTMLTableElement} [table]
* @param {HTMLTableCellElement} [currentCell]
* @private
*/
private __deSelectAll(
table?: HTMLTableElement,
currentCell?: HTMLTableCellElement | false
) {
const cells: HTMLTableCellElement[] = table
? Table.getAllSelectedCells(table)
: Table.getAllSelectedCells(this.jodit.editor);
if (cells.length) {
cells.forEach((cell: HTMLTableCellElement) => {
if (!currentCell || currentCell !== cell) {
Table.restoreSelection(cell);
}
});
}
}
/**
*
* @param {HTMLTableCellElement} cell
* @param {boolean|null} [wholeTable=null] true - resize whole table by left side,
* false - resize whole table by right side, null - resize column
* @private
*/
private __setWorkCell(
cell: HTMLTableCellElement,
wholeTable: boolean | null = null
) {
this.__wholeTable = wholeTable;
this.__workCell = cell;
this.__workTable = Dom.up(
cell,
(elm: Node | null) => elm && elm.nodeName === 'TABLE',
this.jodit.editor
) as HTMLTableElement;
}
private __addResizer = () => {
if (!this.__resizerHandler) {
this.__resizerHandler = this.jodit.container.querySelector(
'.jodit_table_resizer'
) as HTMLElement;
if (!this.__resizerHandler) {
this.__resizerHandler = this.jodit.create.div(
'jodit_table_resizer'
);
let startX: number = 0;
this.jodit.events
.on(
this.__resizerHandler,
'mousedown.table touchstart.table',
(event: MouseEvent) => {
this.__drag = true;
startX = event.clientX;
this.jodit.lock(this.__key);
this.__resizerHandler.classList.add(
'jodit_table_resizer-moved'
);
let box: ClientRect,
tableBox: ClientRect = this.__workTable.getBoundingClientRect();
this.__minX = 0;
this.__maxX = 1000000;
if (this.__wholeTable !== null) {
tableBox = (this.__workTable
.parentNode as HTMLElement).getBoundingClientRect();
this.__minX = tableBox.left;
this.__maxX = tableBox.left + tableBox.width;
} else {
// find maximum columns
const coordinate: number[] = Table.formalCoordinate(
this.__workTable,
this.__workCell,
true
);
Table.formalMatrix(
this.__workTable,
(td, i, j) => {
if (coordinate[1] === j) {
box = td.getBoundingClientRect();
this.__minX = Math.max(
box.left + consts.NEARBY / 2,
this.__minX
);
}
if (coordinate[1] + 1 === j) {
box = td.getBoundingClientRect();
this.__maxX = Math.min(
box.left +
box.width -
consts.NEARBY / 2,
this.__maxX
);
}
}
);
}
return false;
}
)
.on(this.__resizerHandler, 'mouseenter.table', () => {
clearTimeout(this.hideTimeout);
})
.on(
this.jodit.editorWindow,
'mousemove.table touchmove.table',
(event: MouseEvent) => {
if (this.__drag) {
let x = event.clientX;
const workplacePosition: IBound = offset(
(this.__resizerHandler.parentNode ||
this.jodit.ownerDocument
.documentElement) as HTMLElement,
this.jodit,
this.jodit.ownerDocument,
true
);
if (x < this.__minX) {
x = this.__minX;
}
if (x > this.__maxX) {
x = this.__maxX;
}
this.__resizerDelta =
x -
startX +
(!this.jodit.options.iframe
? 0
: workplacePosition.left);
this.__resizerHandler.style.left =
x -
(this.jodit.options.iframe
? 0
: workplacePosition.left) +
'px';
const sel = this.jodit.selection.sel;
sel && sel.removeAllRanges();
if (event.preventDefault) {
event.preventDefault();
}
}
}
);
this.jodit.workplace.appendChild(this.__resizerHandler);
}
}
};
/**
* Calc helper resizer position
*
* @param {HTMLTableElement} table
* @param {HTMLTableCellElement} cell
* @param {int} [offsetX=0]
* @param {int} [delta=0]
*
* @private
*/
private __calcResizerPosition(
table: HTMLTableElement,
cell: HTMLTableCellElement,
offsetX: number = 0,
delta: number = 0
) {
const box = offset(cell, this.jodit, this.jodit.editorDocument);
if (offsetX <= consts.NEARBY || box.width - offsetX <= consts.NEARBY) {
const workplacePosition: IBound = offset(
(this.__resizerHandler.parentNode ||
this.jodit.ownerDocument
.documentElement) as HTMLElement,
this.jodit,
this.jodit.ownerDocument,
true
),
parentBox: IBound = offset(
table,
this.jodit,
this.jodit.editorDocument
);
this.__resizerHandler.style.left =
(offsetX <= consts.NEARBY ? box.left : box.left + box.width) -
workplacePosition.left +
delta +
'px';
this.__resizerHandler.style.height = parentBox.height + 'px';
this.__resizerHandler.style.top =
parentBox.top - workplacePosition.top + 'px';
this.showResizer();
if (offsetX <= consts.NEARBY) {
const prevTD = Dom.prev(
cell,
TableProcessor.isCell,
cell.parentNode as HTMLElement
) as HTMLTableCellElement;
if (prevTD) {
this.__setWorkCell(prevTD);
} else {
this.__setWorkCell(cell, true);
}
} else {
const nextTD = Dom.next(
cell,
TableProcessor.isCell,
cell.parentNode as HTMLElement
);
this.__setWorkCell(cell, !nextTD ? false : null);
}
} else {
this.hideResizer();
}
}
/**
*
* @param {string} command
*/
private onExecCommand = (command: string): false | void => {
if (
/table(splitv|splitg|merge|empty|bin|binrow|bincolumn|addcolumn|addrow)/.test(
command
)
) {
command = command.replace('table', '');
const cells = Table.getAllSelectedCells(this.jodit.editor);
if (cells.length) {
const cell: HTMLTableCellElement | undefined = cells.shift();
if (!cell) {
return;
}
const table = Dom.closest(
cell,
'table',
this.jodit.editor
) as HTMLTableElement;
switch (command) {
case 'splitv':
Table.splitVertical(table);
break;
case 'splitg':
Table.splitHorizontal(table);
break;
case 'merge':
Table.mergeSelected(table);
break;
case 'empty':
Table.getAllSelectedCells(this.jodit.editor).forEach(
td => (td.innerHTML = '')
);
break;
case 'bin':
Dom.safeRemove(table);
break;
case 'binrow':
Table.removeRow(
table,
(cell.parentNode as HTMLTableRowElement).rowIndex
);
break;
case 'bincolumn':
Table.removeColumn(table, cell.cellIndex);
break;
case 'addcolumnafter':
case 'addcolumnbefore':
Table.appendColumn(
table,
cell.cellIndex,
command === 'addcolumnafter'
);
break;
case 'addrowafter':
case 'addrowbefore':
Table.appendRow(
table,
cell.parentNode as HTMLTableRowElement,
command === 'addrowafter'
);
break;
}
}
return false;
}
};
observe(table: HTMLTableElement) {
(table as any)[this.__key] = true;
let start: HTMLTableCellElement;
this.jodit.events
.on(
table,
'mousedown.table touchstart.table',
(event: MouseEvent) => {
if (this.jodit.options.readonly) {
return;
}
const cell: HTMLTableCellElement = Dom.up(
event.target as HTMLElement,
TableProcessor.isCell,
table
) as HTMLTableCellElement;
if (
cell &&
cell instanceof
(this.jodit.editorWindow as any).HTMLElement
) {
if (!cell.firstChild) {
cell.appendChild(
this.jodit.editorDocument.createElement('br')
);
}
start = cell;
Table.addSelected(cell);
this.__selectMode = true;
}
}
)
.on(table, 'mouseleave.table', (e: MouseEvent) => {
if (
this.__resizerHandler &&
this.__resizerHandler !== e.relatedTarget
) {
this.hideResizer();
}
})
.on(
table,
'mousemove.table touchmove.table',
(event: MouseEvent) => {
if (this.jodit.options.readonly) {
return;
}
if (this.__drag || this.jodit.isLockedNotBy(this.__key)) {
return;
}
const cell = Dom.up(
event.target as HTMLElement,
TableProcessor.isCell,
table
) as HTMLTableCellElement;
if (cell) {
if (this.__selectMode) {
if (cell !== start) {
this.jodit.lock(this.__key);
const sel = this.jodit.selection.sel;
sel && sel.removeAllRanges();
if (event.preventDefault) {
event.preventDefault();
}
}
this.__deSelectAll(table);
const bound = Table.getSelectedBound(table, [
cell,
start
]),
box = Table.formalMatrix(table);
for (
let i = bound[0][0];
i <= bound[1][0];
i += 1
) {
for (
let j = bound[0][1];
j <= bound[1][1];
j += 1
) {
Table.addSelected(box[i][j]);
}
}
const max = box[bound[1][0]][bound[1][1]],
min = box[bound[0][0]][bound[0][1]];
this.jodit.events.fire(
'showPopup',
table,
(): IBound => {
const minOffset: IBound = offset(
min,
this.jodit,
this.jodit.editorDocument
);
const maxOffset: IBound = offset(
max,
this.jodit,
this.jodit.editorDocument
);
return {
left: minOffset.left,
top: minOffset.top,
width:
maxOffset.left -
minOffset.left +
maxOffset.width,
height:
maxOffset.top -
minOffset.top +
maxOffset.height
};
}
);
event.stopPropagation();
} else {
this.__calcResizerPosition(
table,
cell,
event.offsetX
);
}
}
}
);
this.__addResizer();
}
/**
*
* @param {Jodit} editor
*/
afterInit(editor: IJodit): void {
if (!editor.options.useTableProcessor) {
return;
}
editor.events
.on(this.jodit.ownerWindow, 'mouseup.table touchend.table', () => {
if (this.__selectMode || this.__drag) {
this.__selectMode = false;
this.jodit.unlock();
}
if (this.__resizerHandler && this.__drag) {
this.__drag = false;
this.__resizerHandler.classList.remove(
'jodit_table_resizer-moved'
);
// resize column
if (this.__wholeTable === null) {
const __marked: HTMLTableCellElement[] = [];
Table.setColumnWidthByDelta(
this.__workTable,
Table.formalCoordinate(
this.__workTable,
this.__workCell,
true
)[1],
this.__resizerDelta,
true,
__marked
);
const nextTD = Dom.next(
this.__workCell,
TableProcessor.isCell,
this.__workCell.parentNode as HTMLElement
) as HTMLTableCellElement;
Table.setColumnWidthByDelta(
this.__workTable,
Table.formalCoordinate(this.__workTable, nextTD)[1],
-this.__resizerDelta,
false,
__marked
);
} else {
const width = this.__workTable.offsetWidth,
parentWidth = getContentWidth(
this.__workTable.parentNode as HTMLElement,
this.jodit.editorWindow
);
// right side
if (!this.__wholeTable) {
this.__workTable.style.width =
((width + this.__resizerDelta) / parentWidth) *
100 +
'%';
} else {
const margin = parseInt(
this.jodit.editorWindow.getComputedStyle(
this.__workTable
).marginLeft || '0',
10
);
this.__workTable.style.width =
((width - this.__resizerDelta) / parentWidth) *
100 +
'%';
this.__workTable.style.marginLeft =
((margin + this.__resizerDelta) / parentWidth) *
100 +
'%';
}
}
editor.setEditorValue();
editor.selection.focus();
}
})
.on(this.jodit.ownerWindow, 'scroll.table', () => {
if (this.__drag) {
const parent = Dom.up(
this.__workCell,
(elm: Node | null) => elm && elm.nodeName === 'TABLE',
editor.editor
) as HTMLElement;
if (parent) {
const parentBox = parent.getBoundingClientRect();
this.__resizerHandler.style.top = parentBox.top + 'px';
}
}
})
.on(
this.jodit.ownerWindow,
'mousedown.table touchend.table',
(event: MouseEvent) => {
// need use event['originalEvent'] because of IE can not set target from
// another window to current window
const current_cell: HTMLTableCellElement = Dom.closest(
(event as any).originalEvent.target as HTMLElement,
'TD|TH',
this.jodit.editor
) as HTMLTableCellElement;
let table: HTMLTableElement | null = null;
if (
current_cell instanceof
(this.jodit.editorWindow as any).HTMLTableCellElement
) {
table = Dom.closest(
current_cell,
'table',
this.jodit.editor
) as HTMLTableElement;
}
if (table) {
this.__deSelectAll(
table,
current_cell instanceof
(this.jodit.editorWindow as any)
.HTMLTableCellElement
? current_cell
: false
);
} else {
this.__deSelectAll();
}
}
)
.on('afterGetValueFromEditor.table', (data: { value: string }) => {
const rxp = new RegExp(
`([\s]*)${consts.JODIT_SELECTED_CELL_MARKER}="1"`,
'g'
);
if (rxp.test(data.value)) {
data.value = data.value.replace(rxp, '');
}
})
.on('change.table afterCommand.table afterSetMode.table', () => {
($$('table', editor.editor) as HTMLTableElement[]).forEach(
(table: HTMLTableElement) => {
if (!(table as any)[this.__key]) {
this.observe(table);
}
}
);
})
.on('beforeSetMode.table', () => {
Table.getAllSelectedCells(editor.editor).forEach(td => {
Table.restoreSelection(td);
Table.normalizeTable(Dom.closest(
td,
'table',
editor.editor
) as HTMLTableElement);
});
})
.on('keydown.table', (event: KeyboardEvent) => {
if (event.which === consts.KEY_TAB) {
($$('table', editor.editor) as HTMLTableElement[]).forEach(
(table: HTMLTableElement) => {
this.__deSelectAll(table);
}
);
}
})
.on('beforeCommand.table', this.onExecCommand.bind(this));
}
beforeDestruct(jodit: IJodit): void {
if (jodit.events) {
jodit.events.off(this.jodit.ownerWindow, '.table');
jodit.events.off('.table');
}
}
}