/** * This file originates from https://github.com/ProseMirror/prosemirror-tables * and is hence subject to the MIT license found here: * https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE * @copyright Marijn Haverbeke and others */ import {Plugin, PluginKey} from "prosemirror-state" import {Decoration, DecorationSet} from "prosemirror-view" import { cellAround, pointsAtCell, setAttr, TableMap, } from "prosemirror-tables"; export const key = new PluginKey("tableColumnResizing") export function columnResizing(options = {}) { const { handleWidth, cellMinWidth, lastColumnResizable } = Object.assign({ handleWidth: 5, cellMinWidth: 25, lastColumnResizable: true }, options); let plugin = new Plugin({ key, state: { init(_, state) { return new ResizeState(-1, false) }, apply(tr, prev) { return prev.apply(tr) } }, props: { attributes(state) { let pluginState = key.getState(state) return pluginState.activeHandle > -1 ? {class: "resize-cursor"} : null }, handleDOMEvents: { mousemove(view, event) { handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable) }, mouseleave(view) { handleMouseLeave(view) }, mousedown(view, event) { handleMouseDown(view, event, cellMinWidth) } }, decorations(state) { let pluginState = key.getState(state) if (pluginState.activeHandle > -1) return handleDecorations(state, pluginState.activeHandle) }, nodeViews: {} } }) return plugin } class ResizeState { constructor(activeHandle, dragging) { this.activeHandle = activeHandle this.dragging = dragging } apply(tr) { let state = this, action = tr.getMeta(key) if (action && action.setHandle != null) return new ResizeState(action.setHandle, null) if (action && action.setDragging !== undefined) return new ResizeState(state.activeHandle, action.setDragging) if (state.activeHandle > -1 && tr.docChanged) { let handle = tr.mapping.map(state.activeHandle, -1) if (!pointsAtCell(tr.doc.resolve(handle))) handle = null state = new ResizeState(handle, state.dragging) } return state } } function handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable) { let pluginState = key.getState(view.state) if (!pluginState.dragging) { let target = domCellAround(event.target), cell = -1 if (target) { let {left, right} = target.getBoundingClientRect() if (event.clientX - left <= handleWidth) cell = edgeCell(view, event, "left") else if (right - event.clientX <= handleWidth) cell = edgeCell(view, event, "right") } if (cell != pluginState.activeHandle) { if (!lastColumnResizable && cell !== -1) { let $cell = view.state.doc.resolve(cell) let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1) let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1 if (col == map.width - 1) { return } } updateHandle(view, cell) } } } function handleMouseLeave(view) { let pluginState = key.getState(view.state) if (pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1) } function handleMouseDown(view, event, cellMinWidth) { let pluginState = key.getState(view.state) if (pluginState.activeHandle == -1 || pluginState.dragging) return false let cell = view.state.doc.nodeAt(pluginState.activeHandle) let width = currentColWidth(view, pluginState.activeHandle, cell.attrs) view.dispatch(view.state.tr.setMeta(key, {setDragging: {startX: event.clientX, startWidth: width}})) function finish(event) { window.removeEventListener("mouseup", finish) window.removeEventListener("mousemove", move) let pluginState = key.getState(view.state) if (pluginState.dragging) { updateColumnWidth(view, pluginState.activeHandle, draggedWidth(pluginState.dragging, event, cellMinWidth)) view.dispatch(view.state.tr.setMeta(key, {setDragging: null})) } } function move(event) { if (!event.which) return finish(event) let pluginState = key.getState(view.state) let dragged = draggedWidth(pluginState.dragging, event, cellMinWidth) displayColumnWidth(view, pluginState.activeHandle, dragged, cellMinWidth) } window.addEventListener("mouseup", finish) window.addEventListener("mousemove", move) event.preventDefault() return true } function currentColWidth(view, cellPos, {colspan, colwidth}) { let width = colwidth && colwidth[colwidth.length - 1] if (width) return width let dom = view.domAtPos(cellPos) let node = dom.node.childNodes[dom.offset] let domWidth = node.offsetWidth, parts = colspan if (colwidth) for (let i = 0; i < colspan; i++) if (colwidth[i]) { domWidth -= colwidth[i] parts-- } return domWidth / parts } function domCellAround(target) { while (target && target.nodeName != "TD" && target.nodeName != "TH") target = target.classList.contains("ProseMirror") ? null : target.parentNode return target } function edgeCell(view, event, side) { let found = view.posAtCoords({left: event.clientX, top: event.clientY}) if (!found) return -1 let {pos} = found let $cell = cellAround(view.state.doc.resolve(pos)) if (!$cell) return -1 if (side == "right") return $cell.pos let map = TableMap.get($cell.node(-1)), start = $cell.start(-1) let index = map.map.indexOf($cell.pos - start) return index % map.width == 0 ? -1 : start + map.map[index - 1] } function draggedWidth(dragging, event, cellMinWidth) { let offset = event.clientX - dragging.startX return Math.max(cellMinWidth, dragging.startWidth + offset) } function updateHandle(view, value) { view.dispatch(view.state.tr.setMeta(key, {setHandle: value})) } function updateColumnWidth(view, cell, width) { let $cell = view.state.doc.resolve(cell); let table = $cell.node(-1); let map = TableMap.get(table); let start = $cell.start(-1); let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; let tr = view.state.tr; for (let row = 0; row < map.height; row++) { let mapIndex = row * map.width + col; // Rowspanning cell that has already been handled if (row && map.map[mapIndex] == map.map[mapIndex - map.width]) continue let pos = map.map[mapIndex] let {attrs} = table.nodeAt(pos); const newWidth = (attrs.colspan * width) + 'px'; tr.setNodeMarkup(start + pos, null, setAttr(attrs, "width", newWidth)); } if (tr.docChanged) view.dispatch(tr) } function displayColumnWidth(view, cell, width, cellMinWidth) { const $cell = view.state.doc.resolve(cell) const table = $cell.node(-1); const start = $cell.start(-1); const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1 let dom = view.domAtPos($cell.start(-1)).node while (dom.nodeName !== "TABLE") { dom = dom.parentNode } updateColumnsOnResize(view, table, dom, cellMinWidth, col, width) } function updateColumnsOnResize(view, tableNode, tableDom, cellMinWidth, overrideCol, overrideValue) { console.log({tableNode, tableDom, cellMinWidth, overrideCol, overrideValue}); let totalWidth = 0; let fixedWidth = true; const rows = tableDom.querySelectorAll('tr'); for (let y = 0; y < rows.length; y++) { const row = rows[y]; const cell = row.children[overrideCol]; cell.style.width = `${overrideValue}px`; if (y === 0) { for (let x = 0; x < row.children.length; x++) { const cell = row.children[x]; if (cell.style.width) { const width = Number(cell.style.width.replace('px', '')); totalWidth += width || cellMinWidth; } else { fixedWidth = false; totalWidth += cellMinWidth; } } } } console.log(totalWidth); if (fixedWidth) { tableDom.style.width = totalWidth + "px" tableDom.style.minWidth = "" } else { tableDom.style.width = "" tableDom.style.minWidth = totalWidth + "px" } } function zeroes(n) { let result = [] for (let i = 0; i < n; i++) result.push(0) return result } function handleDecorations(state, cell) { let decorations = [] let $cell = state.doc.resolve(cell) let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1) let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan for (let row = 0; row < map.height; row++) { let index = col + row * map.width - 1 // For positions that are have either a different cell or the end // of the table to their right, and either the top of the table or // a different cell above them, add a decoration if ((col == map.width || map.map[index] != map.map[index + 1]) && (row == 0 || map.map[index - 1] != map.map[index - 1 - map.width])) { let cellPos = map.map[index] let pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1 let dom = document.createElement("div") dom.className = "column-resize-handle" decorations.push(Decoration.widget(pos, dom)) } } return DecorationSet.create(state.doc, decorations) }