Home Reference Source

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();
            }
        }
    }
};