Table::Plugins::Resizable

A plugin to make Table's columns and rows resizable

Example

Syntax

import { resizable } from 'komps/plugins'
Table.include(resizable)
new Table({
    resize: ['columns'],
    data: [...],
    columns: [...]
})

Options

types description default
resize Boolean, String

Enable ability to resize rows and columns. Pass "rows" or "columns" to reorder just one axis.

true
resizeMin Number

minimum number of pixels for a column

5

Events

description arguments
ColumnResize

After a resize finishes

changes:Array
RowResize

After a resize finishes

changes:Array
SOURCE CODE
import { createElement, listenerElement } from 'dolla';
import { uniq, isFunction } from '../../../support.js';
import cellsDimensions from './cellsDimensions.js';

function (proto) {
    this.include(cellsDimensions)
    
    this.events.push('columnResize', 'rowResize')
    this.assignableAttributes.resize = true
    this.assignableAttributes.resizeMin = 5
    
    
    const initializeWas = proto.initialize
    proto.initialize = function (...args) {
        if (this.resize === true) {
            this.resize = ['columns', 'rows']
        } else if (this.resize === false) {
            this.resize = []
        } else if (typeof this.resize === "string") {
            this.resize = [this.resize]
        }
        this.addEventListenerFor(this.cellSelector, 'mouseover', () => {
            this.clearResizeHandles()
        })
        this.addEventListener('mouseleave', () => {
            this.clearResizeHandles()
        })
        this.addEventListenerFor('.resizable', 'mouseover', e => {
            this.showResizeHandleFor(e.delegateTarget)
        });
        return initializeWas.call(this, ...args)
    }
    
    this.columnTypeRegistry.default.assignableAttributes.resize = true
    
    const renderColumnHeaderWas = proto.renderColumnHeader
    proto.renderColumnHeader = function (column, ...args) {
        const cell = renderColumnHeaderWas.call(this, column, ...args)
        if (this.resize.includes('columns') && column.resize != false) {
            cell.classList.add('resizable', 'resizable-column')
        }
        return cell
    }
    
    proto.clearResizeHandles = function () {
        if (this.resizing) { return }
        this.querySelectorAll(`${this.tagName}-resize-handle`).forEach(x => x.remove())
    }
    
    proto.showResizeHandleFor = function (cell) {
        if (this.resizing) { return }
        this.clearResizeHandles()

        const axises = ['column', 'row']
        axises.forEach(axis => {
            if (cell.classList.contains(`resizable-${axis}`)) {
                const handle = listenerElement(`${this.tagName}-resize-handle`, {
                    class: `resizable-${axis}`,
                    content: [{ tag: 'handle-start' }, { tag: 'handle-end' }],
                    style: {
                        'grid-column': cell.cellIndex,
                        'grid-row': cell.rowIndex
                    }
                }, 'pointerdown', this.activateAxisResize.bind(this))
                if (cell.classList.contains('frozen')) {
                    handle.classList.add(`frozen`, `frozen-${cell.cellIndex}`)
                }
                const axisTarget = axis == 'column' ? cell : cell.row;
                if (!axisTarget.previousElementSibling || !axisTarget.previousElementSibling.classList.contains(`resizable-${axis}`)) {
                    handle.querySelector('handle-start').remove()
                }
                handle.cell = cell
                this.append(handle)
            }
        })
    }
    
    proto.activateAxisResize = function (e) {
        this.setPointerCapture(e.pointerId)
        this.resizing = true
        const handle = e.target.parentElement
        const bb = this.getBoundingClientRect()
        
        let target = handle.cell
        
        if (e.target.localName == "handle-start") {
            if (handle.classList.contains('resizable-row')) {
                target = this.at(target.cellIndex, target.rowIndex - 1)
            } else {
                target = target.previousElementSibling
            }
        }
        let { direction, axis, axisMin, axisMax, offset, slice, start, blockDimension, inlineDimension, inlinePosition, index } = handle.classList.contains('resizable-row') ? {
            direction: 'Row',
            axis: 'y',
            axisMin: target.offsetTop + this.resizeMin,
            axisMax: this.scrollHeight,
            offset: this.scrollTop - bb.top,
            slice: target.row,
            start: e.y - target.offsetHeight,
            blockDimension: 'width',
            inlineDimension: 'height',
            inlinePosition: 'Top',
            index: 'rowIndex'
        } : {
            direction: 'Column',
            axis: 'x',
            axisMin: target.offsetLeft + this.resizeMin,
            axisMax: this.scrollWidth,
            offset: this.scrollLeft - bb.left,
            slice: target.column,
            start: e.x - target.offsetWidth,
            blockDimension: 'height',
            inlineDimension: 'Width',
            inlinePosition: 'Left',
            index: 'cellIndex'
        }
        this.classList.add('resizing-' + direction.toLowerCase())
        
        
        const selectedCells = Array.from(this.querySelectorAll(`${this.constructor.Header.tagName} > .selected`))
        let slices;
        if (selectedCells.includes(target)) {
            slices = uniq(selectedCells.map(cell => direction == 'Column' ? cell.column : cell.row))
        } else {
            if (this.clearSelectedCells) this.clearSelectedCells()
            if (this.selectCells) this.selectCells(target)
            slices = [slice]
        }
        if (this.clearOutlines) this.clearOutlines()
        
        const dragIndicator = createElement(`${this.constructor.tagName}-drag-indicator`, {
            class: direction.toLowerCase(),
            style: {
                'inset-inline-start': e[axis] + offset + "px",
                'block-size': this.cellsDimensions(blockDimension) + "px"
            }
        })
        this.append(dragIndicator)
        
        function mouseMove (e) {
            let target = e[axis] + offset
            target = Math.max(target, axisMin)
            target = Math.min(target, axisMax)
            dragIndicator.style.insetInlineStart = target + "px"
        }
        this.addEventListener('pointermove', mouseMove)
        this.addEventListener('pointerup', e => {
            let delta = e[axis] - start;
            delta = Math.max(delta, this.resizeMin)
            slices.forEach(slice => {
                slice[inlineDimension.toLowerCase()] = delta + "px"
            })
            
            this.trigger(`${direction.toLowerCase()}Resize`, {
                detail: {[direction.toLowerCase() + "s"]: slices}
            })
            
            dragIndicator.remove()
            this.removeEventListener('pointermove', mouseMove)
            this.resizing = false
            this.classList.remove('resizing-' + direction.toLowerCase())
            this.releasePointerCapture(e.pointerId)
        }, {once: true})
    }
    
    if (!Array.isArray(this.style)) {
        this.style = [this.style]
    }
    this.style.push(() => `
        ${this.tagName} {
            --select-color: #1a73e8;
            --handle-size: 10px;
        }
        ${this.tagName}.resizing-column,
        ${this.tagName}.resizing-row {
            user-select: none;
            -webkit-user-select: none;
        }
        ${this.tagName}.resizing-column {
            cursor: col-resize !important;
        }
        ${this.tagName}.resizing-row {
            cursor: row-resize !important;
        }
        ${this.tagName}.resizing-row komp-spreadsheet-header-cell,
        ${this.tagName}.resizing-row komp-spreadsheet-cell {
            cursor: row-resize !important;
        }
        
        ${this.tagName}-header,
        ${this.tagName}-cell {
            user-select: none;
            -webkit-user-select: none;
        }
        ${this.tagName}-resize-handle {
            pointer-events: none;
            position: sticky;
            inset-block-start: 0;
            pointer-events: none;
            display: grid;
            grid-template-columns: auto auto;
            justify-content: space-between;
            align-items: center;
            cursor: col-resize;
        }
        ${this.tagName}-resize-handle.resizable-row {
            writing-mode: vertical-lr;
            cursor: row-resize;
        }
        ${this.tagName}-resize-handle handle-start,
        ${this.tagName}-resize-handle handle-end {
            pointer-events: auto;
            display: block;
            pointer-events: auto;
            block-size: 50%;
            max-block-size: 2em;
            inline-size: 11px;
            border: 3px none currentColor;
            border-inline-style: solid;
            opacity: 0.2;
            position: relative;
        }
        ${this.tagName}-resize-handle handle-start {
            grid-column: 1;
            inset-inline-start: -6px;
        }
        ${this.tagName}-resize-handle handle-end {
            grid-column: 2;
            inset-inline-end: -6px;
        }
        ${this.tagName}-resize-handle handle-start:hover,
        ${this.tagName}-resize-handle handle-end:hover {
            opacity: 1;
            border-color: var(--select-color);
        }

        ${this.tagName}-drag-indicator {
            pointer-events: none;
            position: absolute;
            block-size: 100%;
            inline-size: 3px;
            inset-block-start: 0;
            background: var(--select-color);
        }
        ${this.tagName}-drag-indicator.row {
            writing-mode: vertical-lr;
        }
        ${this.tagName}-resize-handle      { z-index: 200; }
        ${this.tagName}-drag-indicator     { z-index: 200; }
    `)
}