application/components/common/rich-editor2/plugins/table.js
import React from 'react';
import ToolbarButton from '../common/toolbar-button';
import Plain from 'slate-plain-serializer';
import {Block} from 'slate';
import {ensureEmptyGraf} from '../util/ensure-empty-graf';
export const SERIALIZER_RULES = [
{
deserialize(el, next) {
if (el.tagName.toLowerCase() == 'table') {
const firstChild = el.childNodes[0];
if (firstChild.tagName.toLowerCase() == 'tbody') {
return {
object: 'block',
type: 'table',
nodes: next(firstChild)
};
} else if (firstChild.tagName.toLowerCase() == 'colgroup') {
let rows = Array.from(el.childNodes);
rows.shift();
return {
object: 'block',
type: 'table',
nodes: next(rows)
};
} else {
return {
object: 'block',
type: 'table',
nodes: next(el.childNodes)
};
}
} else if (el.tagName.toLowerCase() == 'tr') {
return {
object: 'block',
type: 'table-row',
nodes: next(el.childNodes)
};
} else if (el.tagName.toLowerCase() == 'td') {
return {
object: 'block',
type: 'table-cell',
nodes: next(el.childNodes)
}
}
},
serialize(obj, children) {
if (obj.object == 'block') {
switch (obj.type) {
case 'table':
return (
<table className="__ceo-table">
<tbody>{children}</tbody>
</table>
);
case 'table-row':
return <tr>{children}</tr>;
case 'table-cell':
return <td>{children}</td>;
}
}
}
}
];
const onBackspace = function(e, editor, next) {
const {value} = editor;
const {selection} = value;
if (selection.start.offset != 0) {
return next();
}
// stop backspace at the head of a cell
event.preventDefault();
}
const onDelete = function(e, editor, next) {
const {value} = editor;
const {selection} = value;
if (selection.end.offset != value.startText.text.length) {
return next();
}
// stop delete at the end of a cell
event.preventDefault();
}
const onEnter = function(e, editor, next) {
// nothing atm
}
export function tableClickHandler(e) {
const {editor} = this;
const {editorContent} = this.state;
// are we currently in a table
const {start} = editor.value.selection;
const hasTable = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table';
})
if (!hasTable) {
const sibling = editor.value.document.getNextNode(start.key);
editor
.setBlocks('paragraph')
.wrapBlock('table')
.wrapBlock('table-row')
.wrapBlock('table-cell');
// insert a second cell
const parentRow = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table-row';
})
if (parentRow) {
// there's really no reason there shouldn't be but... javascript
const blankCell = Block.create({
type: 'table-cell',
nodes: [
Block.create({
type: 'paragraph'
})
]
});
editor.insertNodeByKey(parentRow.key, parentRow.nodes.size, blankCell);
}
if (!sibling) {
// make sure we have a blank line
ensureEmptyGraf(editor);
}
} else {
// editor.setBlocks('paragraph')
// .unwrapBlock('table-cell')
// .unwrapBlock('table-row')
// .unwrapBlock('table-table');
return
}
}
export function tableAddRow(e) {
const {editor} = this;
const {editorContent} = this.state;
// are we currently in a table
const {start} = editor.value.selection;
const table = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table';
})
if (!table) {
return;
}
// get all rows and count the cells
const rows = table.filterDescendants((node) => {
return node.type == 'table-row';
});
const cellCount = rows.first().nodes.size;
let newCells = [];
for (let i=0; i<cellCount; i++) {
newCells.push(Block.create({
type: 'table-cell',
nodes: [
Block.create({
type: 'paragraph'
})
]
}))
}
const newRow = Block.create({
type: 'table-row',
nodes: newCells
});
editor.insertNodeByKey(table.key, table.nodes.size, newRow);
// search up to find the parent table, then add row
// figure out the number of columns and add that number of cells
}
export function tableRemoveRow(e) {
const {editor} = this;
const {editorContent} = this.state;
const {start} = editor.value.selection;
const table = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table';
})
if (!table) {
return;
}
const selectedRow = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table-row';
})
editor.removeNodeByKey(selectedRow.key);
// search up to find the parent row
// then remove and move the cursor
}
export function tableAddColumn(e) {
const {editor} = this;
const {editorContent} = this.state;
// are we currently in a table
const {start} = editor.value.selection;
const table = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table';
})
if (!table) {
return;
}
// get all rows and count the cells
const rows = table.filterDescendants((node) => {
return node.type == 'table-row';
});
rows.map((row) => {
editor.insertNodeByKey(row.key, row.nodes.size, Block.create({
type: 'table-cell',
nodes: [
Block.create({
type: 'paragraph'
})
]
}));
});
}
export function tableRemoveColumn(e) {
const {editor} = this;
const {editorContent} = this.state;
const {start} = editor.value.selection;
const table = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table';
})
if (!table) {
return;
}
// first get all the rows
const rows = table.filterDescendants((node) => {
return node.type == 'table-row';
});
// then find my selected row
const selectedRow = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table-row';
});
// and my selected cell
const selectedCell = editor.value.document.getAncestors(start.key).find((block) => {
return block.type == 'table-cell';
});
// then from there determine the INDEX of the selected cell in the row
const selectedIndex = selectedRow.nodes.findIndex((block) => {
return block.key == selectedCell.key;
});
// then use that index to remove the correct column
rows.map((row) => {
const cell = row.nodes.get(selectedIndex);
editor.removeNodeByKey(cell.key);
});
}
export class TableButton extends ToolbarButton
{
handleOnClick(e) {
return tableClickHandler.call(this.props.editor, e);
}
getLabel() {
return 'T'
}
}
export class TableRowAddButton extends ToolbarButton
{
handleOnClick(e) {
return tableAddRow.call(this.props.editor, e);
}
getLabel() {
return 'TR+'
}
}
export class TableColumnAddButton extends ToolbarButton
{
handleOnClick(e) {
return tableAddColumn.call(this.props.editor, e);
}
getLabel() {
return 'TD+';
}
}
export class TableRowRemoveButton extends ToolbarButton
{
handleOnClick(e) {
return tableRemoveRow.call(this.props.editor, e);
}
getLabel() {
return 'TR-';
}
}
export class TableColumnRemoveButton extends ToolbarButton
{
handleOnClick(e) {
return tableRemoveColumn.call(this.props.editor, e);
}
getLabel() {
return 'TD-';
}
}
export function TablePlugin(options) {
return {
renderNode: (props, editor, next) => {
const {attributes, children, node} = props;
switch(node.type) {
case 'table':
return (
<table className='table-node'>
<tbody {...attributes}>{children}</tbody>
</table>
);
case 'table-row':
return <tr {...attributes}>{children}</tr>;
case 'table-cell':
return <td {...attributes}>{children}</td>;
default:
return next();
}
},
onKeyDown: (e, editor, next) => {
const {value} = editor;
const {document, selection} = value;
const {start, isCollapsed} = selection;
const startNode = document.getDescendant(start.key);
if (isCollapsed && start.isAtStartOfNode(startNode)) {
const previous = document.getPreviousText(startNode.key);
if (!previous) {
return next();
}
const previousBlock = document.getClosestBlock(previous.key);
if (previousBlock.type === 'table-cell') {
if (['Backspace', 'Delete', 'Enter'].includes(e.key)) {
// we're in a table cell
e.preventDefault();
} else {
// we're not in a table cell
return next();
}
}
}
if (value.startBlock.type != 'table-cell') {
return next();
}
switch(e.key) {
case 'Backspace':
return onBackspace(e, editor, next);
case 'Delete':
return onDelete(e, editor, next);
case 'Enter':
return onEnter(e, editor, next);
default:
return next();
}
}
}
};