]> BookStack Code Mirror - bookstack/commitdiff
Crawled forward slightly on table resizing
authorDan Brown <redacted>
Fri, 21 Jan 2022 12:16:05 +0000 (12:16 +0000)
committerDan Brown <redacted>
Fri, 21 Jan 2022 12:16:05 +0000 (12:16 +0000)
TODO
app/Uploads/UserAvatars.php
resources/js/editor/ProseMirrorView.js
resources/js/editor/node-views/TableView.js [new file with mode: 0644]
resources/js/editor/node-views/index.js
resources/js/editor/plugins/table-resizing.js [new file with mode: 0644]
resources/js/editor/schema-nodes.js
resources/views/editor-test.blade.php

diff --git a/TODO b/TODO
index 1e5cef12d3cbb37b993074faa832cd11028c4e5f..2301f0577b0158810217a2e6564b151ade56b70b 100644 (file)
--- 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
 
index f5b085a35d5cf802a45de6710034421f4c653716..aeb6cd49c88cf66d4d9f9cfdfb1ee5bfbffe411f 100644 (file)
@@ -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);
     }
 
     /**
index 6b977dea4aa9c8434fe9d2cbc80647d840f7fc1a..cad3fa7b27d9fd3b2c18b46a2862e10128cbd38a 100644 (file)
@@ -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 (file)
index 0000000..64a7602
--- /dev/null
@@ -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
index e675a1b2e6b5207cdb87cbbe28c0218c21aebb81..2db352e9e44290877e1dd50e3ad14bd2bdd870dd 100644 (file)
@@ -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 (file)
index 0000000..64223ea
--- /dev/null
@@ -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)
+}
index b26e17772a0a17a878e5ec74b06cf59908424544..5ebf76a7f0ef7645883159406c8bedc0fc96c4dc 100644 (file)
@@ -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,
index bf76a13f145c34a4baf47290459dcf3467efb95e..ee9537d252cdb32b0a4bc50fee332b194d09842c 100644 (file)
                 </thead>
                 <tbody>
                 <tr>
-                    <td>Content 1</td>
-                    <td>Content 2</td>
+                    <td style="width: 250px; height: 30px">Content 1</td>
+                    <td style="width: 320px; height: 30px">Content 2</td>
+                    <td style="width: 320px; height: 30px">Content 2</td>
+                </tr>
+                <tr>
+                    <td colspan="2">Row 2, Spanning 2</td>
+                    <td>Row 2 spanning 1</td>
+                </tr>
+                <tr>
+                    <td rowspan="2">Row 3/4, Column 1</td>
+                    <td>Row 3, Column 2</td>
+                    <td>Row 3, Column 3</td>
+                </tr>
+                <tr>
+                    <td>Row 4, Column 2</td>
+                    <td>Row 4, Column 3</td>
                 </tr>
                 </tbody>
             </table>
 
-            <iframe width="560" height="315" src="https://www.youtube.com/embed/n6hIa-fPx0M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
+{{--            <iframe width="560" height="315" src="https://www.youtube.com/embed/n6hIa-fPx0M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>--}}
 
             <p><img src="/user_avatar.png" alt="Logo"></p>
             <ul>