soccerloway / quill-better-table

Module for better table in Quill, more useful features are supported.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

I added the ability for 1- moving the table, 2- styling the table.

MuhammedAlkhudiry opened this issue · comments

I added the ability for:

  • moving the table.
  • styling the table.

Note: I tried to follow your way of making a tool as much as possible.

First: moving the table tool class:

            class table_movingTool {
                isDragging = false;
                parent = null;
                currentTarget = null;
                // google: remove event listener from bind, to get why use these variables.
                boundDrag = this.dragMoveToolHandler.bind(this);
                boundMoving = this.movingTableHandler.bind(this);
                boundDrop = this.dropMoveToolHandler.bind(this);

                constructor(table, quill, options) {
                    if (!table) return null;
                    this.table = table;
                    this.quill = quill;
                    this.options = options;
                    this.domNode = null;
                    this.parent = this.quill.root.parentNode;
                    this.initMovingTool();

                }

                initMovingTool() {
                    this.domNode = document.createElement('div');
                    this.domNode.classList.add('qlbt-move-tool');
                    this.domNode.innerHTML =
                        `<svg style="width:22px;height:22px"  viewBox="0 0 24 24"><path class="ql-custom-stroke-2" d="M20,2H4C2.89,2 2,2.89 2,4V20C2,21.11 2.89,22 4,22H20C21.11,22 22,21.11 22,20V4C22,2.89 21.11,2 20,2M20,20H4V4H20M13,8V10H11V8H9L12,5L15,8M16,15V13H14V11H16V9L19,12M10,13H8V15L5,12L8,9V11H10M15,16L12,19L9,16H11V14H13V16" /></svg>`;
                    this.domNode.addEventListener('mousedown', this.boundDrag);
                    this.parent.appendChild(this.domNode);
                    const tableRect = this.table.getBoundingClientRect();
                    const containerRect = this.quill.root.parentNode.getBoundingClientRect();
                    const tableViewRect = this.table.parentNode.getBoundingClientRect();
                    // css(this.domNode, {
                    //     width: '22px',
                    //     height: '22px',
                    //     left: ''.concat(tableRect.x - 25, 'px'),
                    //     top: ''.concat(tableViewRect.top - containerRect.top, 'px')
                    // });
                    new popper(this.table, this.domNode, {placement: 'bottom-start'}
                    );
                }

                locateMovingTool() {

                    this.quill.getModule('better-table').hideTableTools();
                }

                dragMoveToolHandler(e) {
                    this.isDragging = true;
                    this.table.style.opacity = '0.3';
                    document.addEventListener('mousemove', this.boundMoving);
                    document.addEventListener('mouseup', this.boundDrop);

                }

                dropMoveToolHandler(e) {
                    if (this.isDragging && e.target.nodeName === 'P') {
                        this.table.remove();
                        this.quill.root.insertBefore(this.table, e.target.nextSibling);
                        this.locateMovingTool();

                    }

                    this.isDragging = false;
                    this.table.style.opacity = '1';
                    if (this.currentTarget) this.currentTarget.classList.remove('current-position-table');
                    document.removeEventListener('mousemove', this.boundMoving);
                    document.removeEventListener('mouseup', this.boundDrop);

                }

                movingTableHandler(e) {
                    if (this.currentTarget) this.currentTarget.classList.remove('current-position-table');

                    e.preventDefault();
                    if (e.target.nodeName === 'P') {
                        this.currentTarget = e.target;
                        this.currentTarget.classList.add('current-position-table');
                    }
                }

                destroy() {
                    this.domNode.remove();
                    return null;
                }
            }

Second: stying the table tool class:

            class table_styleTool {
                backgroundColorBound = this.backgroundColorHandler.bind(this);
                borderColorBound = this.borderColorHandler.bind(this);
                borderWidthBound = this.borderWidthHandler.bind(this);
                BorderStyleBound = this.borderStyleHandler.bind(this);
                backgroundColorBtn;
                borderColorBtn;
                borderWidthBtn;
                borderStyleBtn;

                constructor(table, quill, options) {
                    if (!table) return null;
                    this.table = table;
                    this.quill = quill;
                    this.options = options;
                    this.domNode = null;
                    this.parent = this.quill.root.parentNode;
                    this.initStylingTool();
                    this.createStyingItems();

                }

                initStylingTool() {
                    this.domNode = document.createElement('div');
                    this.domNode.classList.add('qlbt-style-tool');
                    this.domNode.addEventListener('click', this.boundClick);
                    this.parent.appendChild(this.domNode);
                    const tableRect = this.table.getBoundingClientRect();
                    const containerRect = this.quill.root.parentNode.getBoundingClientRect();
                    const tableViewRect = this.table.parentNode.getBoundingClientRect();
                    // css(this.domNode, {
                    //     width: '22px',
                    //     height: '22px',
                    //     left: ''.concat(tableRect.x - 25, 'px'),
                    //     top: ''.concat(tableViewRect.top - containerRect.top + 22, 'px')
                    // });
                    new popper(this.table, this.domNode, {
                            placement: 'left-start',
                            modifiers: {
                                offset: {
                                    enabled: true,
                                },
                                preventOverflow: {
                                    enabled: true,
                                    escapeWithReference: true,
                                }
                            }
                        }
                    );
                }

                createStyingItems() {
                    this.backgroundColorBtn = document.createElement('div');
                    this.backgroundColorBtn.id = 'backgroundColor';
                    this.backgroundColorBtn.innerHTML = `<svg style="width:22px;height:22px" viewBox="0 0 24 24">
    <path class="ql-custom-stroke-2" d="M19,11.5C19,11.5 17,13.67 17,15A2,2 0 0,0 19,17A2,2 0 0,0 21,15C21,13.67 19,11.5 19,11.5M5.21,10L10,5.21L14.79,10M16.56,8.94L7.62,0L6.21,1.41L8.59,3.79L3.44,8.94C2.85,9.5 2.85,10.47 3.44,11.06L8.94,16.56C9.23,16.85 9.62,17 10,17C10.38,17 10.77,16.85 11.06,16.56L16.56,11.06C17.15,10.47 17.15,9.5 16.56,8.94Z" />
</svg>`;

                    this.borderColorBtn = document.createElement('div');
                    this.borderColorBtn.id = 'borderColorBtn';
                    this.borderColorBtn.innerHTML = `<svg style="width:22px;height:22px" viewBox="0 0 24 24">
    <path class="ql-custom-stroke-2" d="M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z" />
</svg>`;

                    this.borderWidthBtn = document.createElement('div');
                    this.borderWidthBtn.id = 'borderWidthBtn';
                    this.borderWidthBtn.innerHTML = `<svg style="width:22px;height:22px" viewBox="0 0 24 24">
    <path class="ql-custom-stroke-2" d="M3,17H21V15H3V17M3,20H21V19H3V20M3,13H21V10H3V13M3,4V8H21V4H3Z" />
</svg>`;

                    this.borderStyleBtn = document.createElement('div');
                    this.borderStyleBtn.id = 'BorderStyleBtn';
                    this.borderStyleBtn.innerHTML = `<svg style="width:22px;height:22px" viewBox="0 0 24 24">
    <path class="ql-custom-stroke-2" d="M3,16H8V14H3V16M9.5,16H14.5V14H9.5V16M16,16H21V14H16V16M3,20H5V18H3V20M7,20H9V18H7V20M11,20H13V18H11V20M15,20H17V18H15V20M19,20H21V18H19V20M3,12H11V10H3V12M13,12H21V10H13V12M3,4V8H21V4H3Z" />
</svg>`;


                    this.backgroundColorBtn.addEventListener('click', this.backgroundColorBound);
                    this.borderColorBtn.addEventListener('click', this.borderColorBound);
                    this.borderWidthBtn.addEventListener('click', this.borderWidthBound);
                    this.borderStyleBtn.addEventListener('click', this.BorderStyleBound);

                    this.domNode.appendChild(this.backgroundColorBtn);
                    this.domNode.appendChild(this.borderColorBtn);
                    this.domNode.appendChild(this.borderWidthBtn);
                    this.domNode.appendChild(this.borderStyleBtn);

                }

                backgroundColorHandler(e) {

                    const clickedElement = e.target;
                    if (clickedElement === this.backgroundColorBtn) {
                        this.destroyCurrentPicker();
                        this.createColorPicker(this.backgroundColorBtn);
                    } else if (clickedElement.className === 'qlbt-style-colors-item') {
                        const selectedCells = this.quill.getModule('better-table').getSelectedCells();
                        if (!selectedCells) return;

                        selectedCells.forEach(cell => cell.domNode.style.backgroundColor =
                            clickedElement.dataset.value);

                    }
                }

                borderColorHandler(e) {
                    const clickedElement = e.target;
                    if (clickedElement === this.borderColorBtn) {
                        this.destroyCurrentPicker();
                        this.createColorPicker(this.borderColorBtn);
                    } else if (clickedElement.className === 'qlbt-style-colors-item') {
                        const selectedCells = this.quill.getModule('better-table').getSelectedCells();
                        if (!selectedCells) return;

                        selectedCells.forEach(cell => cell.domNode.style.borderColor =
                            clickedElement.dataset.value);
                    }
                }

                borderWidthHandler(e) {
                    const clickedElement = e.target;
                    if (clickedElement === this.borderWidthBtn) {
                        this.destroyCurrentPicker();
                        this.createPicker(this.borderWidthBtn, [0, 1, 1.5, 1.75, 2.25, 3, 4.5,
                            6]);
                    } else if (clickedElement.className === 'qlbt-style-item') {
                        const selectedCells = this.quill.getModule('better-table').getSelectedCells();
                        if (!selectedCells) return;

                        selectedCells.forEach(cell => cell.domNode.style.borderWidth =
                            `${clickedElement.dataset.value}px`);
                    }
                }

                borderStyleHandler(e) {
                    const clickedElement = e.target;
                    if (clickedElement === this.borderStyleBtn) {
                        this.destroyCurrentPicker();
                        this.createPicker(this.borderStyleBtn, ['solid', 'dotted',
                            'dashed']);
                    } else if (clickedElement.className === 'qlbt-style-item') {
                        const selectedCells = this.quill.getModule('better-table').getSelectedCells();
                        if (!selectedCells) return;

                        selectedCells.forEach(cell => cell.domNode.style.borderStyle =
                            clickedElement.dataset.value);
                    }
                }

                destroy() {
                    this.domNode.remove();
                    return null;
                }

                createPicker(pickerParent, items) {
                    let pickerContainer = document.createElement('span');
                    pickerContainer.classList.add('qlbt-style-picker-options');
                    pickerParent.appendChild(pickerContainer);
                    items.forEach(item => {
                        let itemSpan = document.createElement('span');
                        itemSpan.className = 'qlbt-style-item';
                        itemSpan.dataset.value = item;
                        if (pickerParent === this.borderWidthBtn) itemSpan.innerText = item;
                        else {
                            const line = document.createElement('div');
                            line.id = item;
                            itemSpan.appendChild(line);

                        }
                        pickerContainer.appendChild(itemSpan);
                    });
                }

                createColorPicker(colorPickerParent) {
                    let colors = ['#000000', '#e60000', '#ff9900',
                        '#ffff00',
                        '#008a00', '#0066cc', '#9933ff', '#ffffff',
                        '#facccc', '#ffebcc', '#ffffcc', '#cce8cc',
                        '#cce0f5', '#ebd6ff', '#bbbbbb', '#f06666',
                        '#ffc266', '#ffff66', '#66b966', '#66a3e0',
                        '#c285ff', '#888888', '#a10000', '#b26b00',
                        '#b2b200', '#006100', '#0047b2', '#6b24b2',
                        '#444444', '#5c0000', '#663d00', '#666600',
                        '#003700', '#002966', '#3d1466'];

                    let colorPickerContainer = document.createElement('span');
                    colorPickerContainer.classList.add('qlbt-style-colors-picker-options');
                    colorPickerParent.appendChild(colorPickerContainer);
                    colors.forEach(color => {
                        let colorSpan = document.createElement('span');
                        colorSpan.className = 'qlbt-style-colors-item';
                        colorSpan.dataset.value = color;
                        colorSpan.style.backgroundColor = color;
                        colorPickerContainer.appendChild(colorSpan);
                    });
                }

                destroyCurrentPicker() {
                    if (this.backgroundColorBtn.querySelector('span'))
                       this.backgroundColorBtn.removeChild(this.backgroundColorBtn.querySelector('span'));
                    else if (this.borderColorBtn.querySelector('span'))
                        this.borderColorBtn.removeChild(this.borderColorBtn.querySelector('span'));
                    else if (this.borderWidthBtn.querySelector('span'))
                        this.borderWidthBtn.removeChild(this.borderWidthBtn.querySelector('span'));
                    else if (this.borderStyleBtn.querySelector('span'))
                        this.borderStyleBtn.removeChild(this.borderStyleBtn.querySelector('span'));
                }
            }

Also, I added new function the module which is getSelectedCells();

                getSelectedCells() {
                    return this.tableSelection.selectedTds;
                }

I also, added style for these tool, and modify the style for existed tools:

  • for TableColumnTool, since changing the length of a column for upper tool is not convenient,
    I change it's style so changing length of a column is like the way Google Docs does it which is above the columns directly, so, take it or leave it.

.quill-better-table-wrapper {
    overflow-x: auto;
}

table {
    table-layout: fixed;
    border-collapse: collapse;
    text-align: right;
}

table td {
    border: 1px solid #000;
    padding: 2px 5px;
}

.qlbt-operation-menu {
    background-color: #fff;
    font-size: 14px;
    z-index: 100;
    overflow: hidden;
    display: grid;
    grid-template-columns: repeat(12, 30px);
    grid-template-rows: 35px;
    align-items: center;
    justify-items: center;
    transition: opacity 0.5s;
    box-shadow: 0 0 20px 4px rgba(154, 161, 177, .15), 0 4px 80px -8px rgba(36, 40, 47, .25), 0 4px 4px -2px rgba(91, 94, 105, .15);
    border-radius: 0.4em;
}

.qlbt-operation-menu .qlbt-operation-menu-item {
    background-color: #fff;
    cursor: pointer;
    color: #595959;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: .3s;
    border: none;
}


.qlbt-col-tool, .qlbt-move-tool, .qlbt-style-tool {
    position: absolute;
    display: flex;
    z-index: 99;
    height: 16px;
}

.qlbt-col-tool {
    margin-top: -15px;
    margin-left: -3px;
    opacity: 0;
}

.qlbt-move-tool, .qlbt-style-tool {

    opacity: .35;
    transition: .2s;
    cursor: pointer;
}

.qlbt-style-tool .icon {
    position: relative;
}

.qlbt-move-tool:hover,
.qlbt-move-tool:focus-within,
.qlbt-style-tool:focus-within,
.qlbt-style-tool:hover {
    opacity: .8;
    transition: .2s;
}

.qlbt-style-tool {
    display: grid;

}

.qlbt-col-tool:hover .qlbt-col-tool-cell {
    /*background-color: var(--light-primary);*/
    transition: .2s;

}

.qlbt-col-tool .qlbt-col-tool-cell {
    position: relative;
    background-color: #fff;
    /*border-top: 1px solid #000;*/
    /*border-right: 2px solid #000;*/
    /*border-bottom: 1px solid #000;*/
}

.qlbt-col-tool .qlbt-col-tool-cell:first-child {
    /*border-left: 2px solid #000;*/
}

.qlbt-col-tool .qlbt-col-tool-cell-holder {
    position: absolute;
    right: 0;
    top: 0;
    bottom: 0;
    z-index: 3;
    width: 2px;
    background-color: var(--primary);
    cursor: ew-resize;
}

.qlbt-col-tool .qlbt-col-tool-cell-holder:hover {
    background-color: #35a7ed;
}

.qlbt-col-tool .qlbt-col-tool-cell-holder::before {
    content: "";
    position: absolute;
    top: 0;
    left: -6px;
    display: block;
    width: 8px;
    height: 100%;
}

.qlbt-col-tool .qlbt-col-tool-cell-holder::after {
    content: "";
    position: absolute;
    top: 0;
    right: -6px;
    display: block;
    width: 8px;
    height: 100%;
}


button.ql-copy-format.ql-active {
    cursor: url('data:image/svg+xml;utf8,<svg width="18"height="18" viewBox="0 0 22 22"><path class="ql-fill"d="M18,4V3A1,1 0 0,0 17,2H5A1,1 0 0,0 4,3V7A1,1 0 0,0 5,7H17A1,1 0 0,0 11,7V6H19V10H9V21A1,1 0 0,0 10,22H12A1,1 0 0,0 11,21V12H21V4H18Z"/></svg>'), auto;
}

#insertColumnLeft:hover path, #insertColumnRight:hover path, #insertRowUp:hover path,
#insertRowDown:hover {
    fill: var(--success);
    transition: .3s;

}

#deleteColumn:hover path, #deleteRow:hover path, #deleteTable:hover path {
    fill: var(--danger);
    transition: .3s;
}

#insertColumnLeft path, #insertColumnRight path, #insertRowUp path,
#insertRowDown, #deleteColumn path, #deleteRow path, #deleteTable path {
    transition: .3s;
}

.qlbt-operation-menu *:not(hr):hover {
    background-color: var(--light-primary);
}

.current-position-table {
    border-radius: .3em;
    border: 1px var(--dark-gray) dotted;
    background-color: var(--white);
    padding: 30px;
    transition: .3s;
}

.qlbt-style-colors-picker-options {
    background-color: #fff;
    position: absolute;
    top: 1px;
    right: 40px;
    padding: 3px 5px;
    width: 152px;
}

.qlbt-style-picker-options {
    background-color: #fff;
    display: grid;
    grid-template-rows: repeat(auto-fit, 1fr);
    grid-gap: 5px;
    position: absolute;
    top: 1px;
    right: 40px;
    font-size: 16px;
    font-weight: bold;
    text-align: center;
    color: var(--black);
    box-shadow: 0 0 6px #00000035;
    border-radius: .3em;
}

.qlbt-style-colors-item {
    border: 1px solid transparent;
    float: left;
    height: 16px;
    margin: 2px;
    width: 16px;
    z-index: 50;
    transition: .2s;
}

.qlbt-style-item {
    z-index: 50;
    padding: 5px;
    transition: .2s;

}

.qlbt-style-item:hover, .qlbt-style-colors-item:hover, .selected-item {
    opacity: 1;
    transition: .2s;
}

.qlbt-style-colors-item:hover, .selected-item {
    border-color: var(--dark-gray);
    transition: .2s;
}

.qlbt-style-item:hover, .selected-item {
    background-color: var(--light-primary);
    transition: .2s;

}

#solid {
    width: 50px;
    margin: 4px;
    display: inline-block;
    border-top: 2px solid rgb(0, 0, 0);
}

#dotted {
    width: 50px;
    margin-bottom: 4px;
    display: inline-block;
    border-top: 2px dotted rgb(0, 0, 0);
}

#dashed {
    width: 50px;
    margin-bottom: 4px;
    display: inline-block;
    border-top: 2px dashed rgb(0, 0, 0);
}

Note: I use popper.js for positioning the tools, you can remove it or consider using it.

feel free to take and modfiy my code as needed.

I'm sorry if issues page is not the place for these kind of posts.

commented

Thanks for your awesome work~
I think moving the table is useful for most users. I am glad to add this feature in quill-better-table, can you provide me a sandbox or a demo in codepen? Or PR welcome.
About styling the table, I can not imagine the final effect. A demo will help me have a clearer understanding.
About popper.js, I think keep less dependences is also important.
Keep in touch~ PR is a better place for these.

Thanks for your awesome work~
I think moving the table is useful for most users. I am glad to add this feature in quill-better-table, can you provide me a sandbox or a demo in codepen? Or PR welcome.
About styling the table, I can not imagine the final effect. A demo will help me have a clearer understanding.
About popper.js, I think keep less dependences is also important.
Keep in touch~ PR is a better place for these.

Thanks for your awesome module!

I'm sorry, but I was working in production file which is this file https://github.com/soccerloway/quill-better-table/blob/master/dist/quill-better-table.js, since I did not expect to change alot, so showing it in CodePen is a pain.
However, I'm planning to use the source file for the future (I will work for row-height changing tool, and an option to equalize the size of each cell, and others, don't wait for me if you want to implement those :) )

About styling the table, these images my help:

For background color
123

  • Note: the color-picker is nearly identical to the native one in the editor

For border style
345

No image needed for the rest

Since you are here, please try to solve these two problems in column-width changing tool:

  1. Changing a column width in should not change the width of the table (try changing a column width in quill then in Google Docs to understand)
  2. The first column width in the left can't be change.
commented

Pretty UI~ I can not wait to receive your contribution now. You can send me PRs in your free time.
I will try to add the features you mentioned for quill-better-table gradually in my free time.

work for row-height changing tool

I have implemented row-height changing tool before, row-height was affected by contents, it will call blots' optimize function many times, have a low performance. So I canceled this feature. Maybe automatic row-height is enough to use. It's my personal opinion~:)

Changing a column width in should not change the width of the table (try changing a column width in quill then in Google Docs to understand)

In quill-better-table, the width of table will change by column width. Since the table may be displayed in a small area, I decide to wrap table in a scrollable div. It is good for displaying table in anywhere. So I think the width of table is necessary.

@MuhammedAlkhudiry I am waiting for your contributions~~~Thanks a lot:)

@KMLX28 could you please give further info on how to integrate your above changes with current quill-better-table master branch? Am interested in trying, but not sure where to include your changes

commented

These feature changes for moving table are not yet merged with master right?

I've added in the basic plumbing for this in this commit in my fork, if anyone wants to play with it or use it as the basis for a pull request.

arimus@48bdead

It mostly works with some quirks here and there. Thanks @KMLX28 for the basics. I had to adjust a decent amount, but for the most part it's working pretty well.