From 264966de029b6351dccaeba61e9c88fa52924db5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 21 Jan 2022 12:16:05 +0000 Subject: [PATCH] Crawled forward slightly on table resizing --- TODO | 3 +- app/Uploads/UserAvatars.php | 2 + resources/js/editor/ProseMirrorView.js | 3 +- resources/js/editor/node-views/TableView.js | 21 ++ resources/js/editor/node-views/index.js | 2 + resources/js/editor/plugins/table-resizing.js | 288 ++++++++++++++++++ resources/js/editor/schema-nodes.js | 66 +++- resources/views/editor-test.blade.php | 20 +- 8 files changed, 390 insertions(+), 15 deletions(-) create mode 100644 resources/js/editor/node-views/TableView.js create mode 100644 resources/js/editor/plugins/table-resizing.js diff --git a/TODO b/TODO index 1e5cef12d..2301f0577 100644 --- a/TODO +++ b/TODO @@ -2,7 +2,8 @@ - Table cell height resize & cell width resize via width style - Column resize source: https://github.com/ProseMirror/prosemirror-tables/blob/master/src/columnresizing.js - - Looks like all the required internals are exported so we can copy out & modify easily. + - Have updated column resizing to set cell widths + - Now need to handle table overall size on change, then heights. ### In-Progress diff --git a/app/Uploads/UserAvatars.php b/app/Uploads/UserAvatars.php index f5b085a35..aeb6cd49c 100644 --- a/app/Uploads/UserAvatars.php +++ b/app/Uploads/UserAvatars.php @@ -2,6 +2,7 @@ namespace BookStack\Uploads; +use BookStack\Auth\Access\LdapService; use BookStack\Auth\User; use BookStack\Exceptions\HttpFetchException; use Exception; @@ -16,6 +17,7 @@ class UserAvatars { $this->imageService = $imageService; $this->http = $http; + $ldapService = app()->make(LdapService::class); } /** diff --git a/resources/js/editor/ProseMirrorView.js b/resources/js/editor/ProseMirrorView.js index 6b977dea4..cad3fa7b2 100644 --- a/resources/js/editor/ProseMirrorView.js +++ b/resources/js/editor/ProseMirrorView.js @@ -1,7 +1,7 @@ import {EditorState} from "prosemirror-state"; import {EditorView} from "prosemirror-view"; import {exampleSetup} from "prosemirror-example-setup"; -import {tableEditing, columnResizing} from "prosemirror-tables"; +import {tableEditing} from "prosemirror-tables"; import {DOMParser} from "prosemirror-model"; @@ -9,6 +9,7 @@ import schema from "./schema"; import menu from "./menu"; import nodeViews from "./node-views"; import {stateToHtml} from "./util"; +import {columnResizing} from "./plugins/table-resizing"; class ProseMirrorView { constructor(target, content) { diff --git a/resources/js/editor/node-views/TableView.js b/resources/js/editor/node-views/TableView.js new file mode 100644 index 000000000..64a76025b --- /dev/null +++ b/resources/js/editor/node-views/TableView.js @@ -0,0 +1,21 @@ +class TableView { + /** + * @param {PmNode} node + * @param {PmView} view + * @param {(function(): number)} getPos + */ + constructor(node, view, getPos) { + this.dom = document.createElement("div") + this.dom.className = "ProseMirror-tableWrapper" + this.table = this.dom.appendChild(document.createElement("table")); + this.table.setAttribute('style', node.attrs.style); + this.colgroup = this.table.appendChild(document.createElement("colgroup")); + this.contentDOM = this.table.appendChild(document.createElement("tbody")); + } + + ignoreMutation(record) { + return record.type == "attributes" && (record.target == this.table || this.colgroup.contains(record.target)) + } +} + +export default TableView; \ No newline at end of file diff --git a/resources/js/editor/node-views/index.js b/resources/js/editor/node-views/index.js index e675a1b2e..2db352e9e 100644 --- a/resources/js/editor/node-views/index.js +++ b/resources/js/editor/node-views/index.js @@ -1,9 +1,11 @@ import ImageView from "./ImageView"; import IframeView from "./IframeView"; +import TableView from "./TableView"; const views = { image: (node, view, getPos) => new ImageView(node, view, getPos), iframe: (node, view, getPos) => new IframeView(node, view, getPos), + table: (node, view, getPos) => new TableView(node, view, getPos), }; export default views; \ No newline at end of file diff --git a/resources/js/editor/plugins/table-resizing.js b/resources/js/editor/plugins/table-resizing.js new file mode 100644 index 000000000..64223eaed --- /dev/null +++ b/resources/js/editor/plugins/table-resizing.js @@ -0,0 +1,288 @@ +/** + * 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) +} diff --git a/resources/js/editor/schema-nodes.js b/resources/js/editor/schema-nodes.js index b26e17772..5ebf76a7f 100644 --- a/resources/js/editor/schema-nodes.js +++ b/resources/js/editor/schema-nodes.js @@ -258,15 +258,6 @@ const ordered_list = Object.assign({}, orderedList, {content: "list_item+", grou const bullet_list = Object.assign({}, bulletList, {content: "list_item+", group: "block"}); const list_item = Object.assign({}, listItem, {content: 'paragraph block*'}); -const { - table_row, - table_cell, - table_header, -} = tableNodes({ - tableGroup: "block", - cellContent: "block+" -}); - const table = { content: "table_row+", attrs: { @@ -277,11 +268,66 @@ const table = { group: "block", parseDOM: [{tag: "table", getAttrs: domAttrsToAttrsParser(['style'])}], toDOM(node) { - console.log(extractAttrsForDom(node, ['style'])); return ["table", extractAttrsForDom(node, ['style']), ["tbody", 0]] } }; +const table_row = { + content: "(table_cell | table_header)*", + tableRole: "row", + parseDOM: [{tag: "tr"}], + toDOM() { return ["tr", 0] } +}; + +let cellAttrs = { + colspan: {default: 1}, + rowspan: {default: 1}, + width: {default: null}, + height: {default: null}, +}; + +function getCellAttrs(dom) { + return { + colspan: Number(dom.getAttribute("colspan") || 1), + rowspan: Number(dom.getAttribute("rowspan") || 1), + width: dom.style.width || null, + height: dom.style.height || null, + }; +} + +function setCellAttrs(node) { + let attrs = {}; + + const styles = []; + if (node.attrs.colspan != 1) attrs.colspan = node.attrs.colspan; + if (node.attrs.rowspan != 1) attrs.rowspan = node.attrs.rowspan; + if (node.attrs.width) styles.push(`width: ${node.attrs.width}`); + if (node.attrs.height) styles.push(`height: ${node.attrs.height}`); + if (styles) { + attrs.style = styles.join(';'); + } + + return attrs +} + +const table_cell = { + content: "block+", + attrs: cellAttrs, + tableRole: "cell", + isolating: true, + parseDOM: [{tag: "td", getAttrs: dom => getCellAttrs(dom)}], + toDOM(node) { return ["td", setCellAttrs(node), 0] } +}; + +const table_header = { + content: "block+", + attrs: cellAttrs, + tableRole: "header_cell", + isolating: true, + parseDOM: [{tag: "th", getAttrs: dom => getCellAttrs(dom)}], + toDOM(node) { return ["th", setCellAttrs(node), 0] } +}; + const nodes = { doc, paragraph, diff --git a/resources/views/editor-test.blade.php b/resources/views/editor-test.blade.php index bf76a13f1..ee9537d25 100644 --- a/resources/views/editor-test.blade.php +++ b/resources/views/editor-test.blade.php @@ -28,13 +28,27 @@ - Content 1 - Content 2 + Content 1 + Content 2 + Content 2 + + + Row 2, Spanning 2 + Row 2 spanning 1 + + + Row 3/4, Column 1 + Row 3, Column 2 + Row 3, Column 3 + + + Row 4, Column 2 + Row 4, Column 3 - +{{-- --}}

Logo