]> BookStack Code Mirror - bookstack/commitdiff
Added table creation and insertion
authorDan Brown <redacted>
Wed, 19 Jan 2022 15:22:10 +0000 (15:22 +0000)
committerDan Brown <redacted>
Wed, 19 Jan 2022 15:22:10 +0000 (15:22 +0000)
TODO
package-lock.json
package.json
resources/js/editor/ProseMirrorView.js
resources/js/editor/commands.js
resources/js/editor/menu/TableCreatorGrid.js [new file with mode: 0644]
resources/js/editor/menu/icons.js
resources/js/editor/menu/index.js
resources/js/editor/schema-nodes.js
resources/sass/_editor.scss

diff --git a/TODO b/TODO
index ad0665afbd13bb361b056507a58e3750b3205025..018cd7af26be6d2edfcd7704d13826bc50ff240a 100644 (file)
--- a/TODO
+++ b/TODO
@@ -4,11 +4,10 @@
 
 ### In-Progress
 
-//
+- Tables
 
 ### Features
 
-- Tables
 - Images
 - Drawings
 - LTR/RTL control
index 1215895807ba574bc04e3defe59a5e446900efaf..5bc80061cecf9799e68bfe7bbee450ed79d04fa5 100644 (file)
@@ -17,6 +17,7 @@
         "prosemirror-model": "^1.15.0",
         "prosemirror-schema-list": "^1.1.6",
         "prosemirror-state": "^1.3.4",
+        "prosemirror-tables": "^1.1.1",
         "prosemirror-view": "^1.23.2",
         "sortablejs": "^1.14.0"
       },
         "prosemirror-transform": "^1.0.0"
       }
     },
+    "node_modules/prosemirror-tables": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz",
+      "integrity": "sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==",
+      "dependencies": {
+        "prosemirror-keymap": "^1.1.2",
+        "prosemirror-model": "^1.8.1",
+        "prosemirror-state": "^1.3.1",
+        "prosemirror-transform": "^1.2.1",
+        "prosemirror-view": "^1.13.3"
+      }
+    },
     "node_modules/prosemirror-transform": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.3.tgz",
         "prosemirror-transform": "^1.0.0"
       }
     },
+    "prosemirror-tables": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz",
+      "integrity": "sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==",
+      "requires": {
+        "prosemirror-keymap": "^1.1.2",
+        "prosemirror-model": "^1.8.1",
+        "prosemirror-state": "^1.3.1",
+        "prosemirror-transform": "^1.2.1",
+        "prosemirror-view": "^1.13.3"
+      }
+    },
     "prosemirror-transform": {
       "version": "1.3.3",
       "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.3.tgz",
index bd16a728d6d412eaa772f6d33bb24c0f42bdfd88..d986ef26de4405812afc9e07ef935940d30e4bd4 100644 (file)
@@ -37,6 +37,7 @@
     "prosemirror-model": "^1.15.0",
     "prosemirror-schema-list": "^1.1.6",
     "prosemirror-state": "^1.3.4",
+    "prosemirror-tables": "^1.1.1",
     "prosemirror-view": "^1.23.2",
     "sortablejs": "^1.14.0"
   }
index cc979ffb3f6e55c52d9d6cc508f6bee6a4daea68..bfd209db1fe4ffef32c1aa9c3e5334ba4679fd4f 100644 (file)
@@ -1,6 +1,7 @@
 import {EditorState} from "prosemirror-state";
 import {EditorView} from "prosemirror-view";
 import {exampleSetup} from "prosemirror-example-setup";
+import {tableEditing} from "prosemirror-tables";
 
 import {DOMParser} from "prosemirror-model";
 
@@ -22,6 +23,7 @@ class ProseMirrorView {
                 plugins: [
                     ...exampleSetup({schema, menuBar: false}),
                     menu,
+                    tableEditing(),
                 ]
             }),
             nodeViews,
index bd71ceba310349a42da8f42f0cedf3e98c964940..904dbb9c893ee634824f14b0af1d3cff5eabd56c 100644 (file)
@@ -58,6 +58,35 @@ export function insertBlockBefore(blockType) {
     }
 }
 
+/**
+ * @param {Number} rows
+ * @param {Number} columns
+ * @return {PmCommandHandler}
+ */
+export function insertTable(rows, columns) {
+    return function (state, dispatch) {
+        if (!dispatch) return true;
+
+        const tr = state.tr;
+        const nodes = state.schema.nodes;
+
+        const rowNodes = [];
+        for (let y = 0; y < rows; y++) {
+            const rowCells = [];
+            for (let x = 0; x < columns; x++) {
+                rowCells.push(nodes.table_cell.create(null));
+            }
+            rowNodes.push(nodes.table_row.create(null, rowCells));
+        }
+
+        const table = nodes.table.create(null, rowNodes);
+        tr.replaceSelectionWith(table);
+        dispatch(tr);
+
+        return true;
+    }
+}
+
 /**
  * @return {PmCommandHandler}
  */
diff --git a/resources/js/editor/menu/TableCreatorGrid.js b/resources/js/editor/menu/TableCreatorGrid.js
new file mode 100644 (file)
index 0000000..e545b3d
--- /dev/null
@@ -0,0 +1,80 @@
+import crel from "crelt"
+import {prefix} from "./menu-utils";
+import {insertTable} from "../commands";
+
+class TableCreatorGrid {
+
+    constructor() {
+        this.gridItems = [];
+        this.size = 10;
+        this.label = null;
+    }
+
+    // :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
+    // Renders the submenu.
+    render(view) {
+
+        for (let y = 0; y < this.size; y++) {
+            for (let x = 0; x < this.size; x++) {
+                const elem = crel("div", {class: prefix + "-table-creator-grid-item"});
+                this.gridItems.push(elem);
+                elem.addEventListener('mouseenter', event => this.updateGridItemActiveStatus(elem));
+            }
+        }
+
+        const gridWrap = crel("div", {
+            class: prefix + "-table-creator-grid",
+            style: `grid-template-columns: repeat(${this.size}, 14px);`,
+        }, this.gridItems);
+
+        gridWrap.addEventListener('mouseleave', event => {
+            this.updateGridItemActiveStatus(null);
+        });
+        gridWrap.addEventListener('click', event => {
+            if (event.target.classList.contains(prefix + "-table-creator-grid-item")) {
+                const {x, y} = this.getPositionOfGridItem(event.target);
+                insertTable(y + 1, x + 1)(view.state, view.dispatch);
+            }
+        });
+
+        const gridLabel = crel("div", {class: prefix + "-table-creator-grid-label"});
+        this.label = gridLabel;
+        const wrap = crel("div", {class: prefix + "-table-creator-grid-container"}, [gridWrap, gridLabel]);
+
+        function update(state) {
+            return true;
+        }
+
+        return {dom: wrap, update}
+    }
+
+    /**
+     * @param {Element|null} newTarget
+     */
+    updateGridItemActiveStatus(newTarget) {
+        const {x: xPos, y: yPos} = this.getPositionOfGridItem(newTarget);
+
+        for (let y = 0; y < this.size; y++) {
+            for (let x = 0; x < this.size; x++) {
+                const active = x <= xPos && y <= yPos;
+                const index = (y * this.size) + x;
+                this.gridItems[index].classList.toggle(prefix + "-table-creator-grid-item-active", active);
+            }
+        }
+
+        this.label.textContent = (xPos + yPos < 0) ? '' : `${xPos + 1} x ${yPos + 1}`;
+    }
+
+    /**
+     * @param {Element} gridItem
+     * @return {{x: number, y: number}}
+     */
+    getPositionOfGridItem(gridItem) {
+        const index = this.gridItems.indexOf(gridItem);
+        const y = Math.floor(index / this.size);
+        const x = index % this.size;
+        return {x, y};
+    }
+}
+
+export default TableCreatorGrid;
\ No newline at end of file
index ba9b54d5d03f7ec16a32dc79300497c959a508fe..3166f5dac7ffa2fdf72d851cd7c93835f60ab096 100644 (file)
@@ -99,6 +99,10 @@ export const icons = {
   source_code: {
     width: 24, height: 24,
     path: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z",
+  },
+  table: {
+    width: 24, height: 24,
+    path: "M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H4v-4h4v4zm0-6H4v-4h4v4zm0-6H4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4z",
   }
 };
 
index 11ef864252e3fe28052763fae0f9452d5e98a6dd..665d5f9ef654406d6a1e8f35a6c7c95d1d50ca3c 100644 (file)
@@ -4,13 +4,11 @@ import {
 } from "./menu"
 import {icons} from "./icons";
 import ColorPickerGrid from "./ColorPickerGrid";
-import DialogBox from "./DialogBox";
+import TableCreatorGrid from "./TableCreatorGrid";
 import {toggleMark} from "prosemirror-commands";
 import {menuBar} from "./menubar"
 import schema from "../schema";
 import {removeMarks} from "../commands";
-import DialogForm from "./DialogForm";
-import DialogInput from "./DialogInput";
 
 import itemAnchorButtonItem from "./item-anchor-button";
 import itemHtmlSourceButton from "./item-html-source-button";
@@ -157,6 +155,9 @@ const inserts = [
         title: "Horizontal Rule",
         icon: icons.horizontal_rule,
     }),
+    new DropdownSubmenu([
+        new TableCreatorGrid()
+    ], {icon: icons.table}),
     itemHtmlSourceButton(),
 ];
 
index 5620ada5bc15cd3caa1770f7d024f2d82525c45c..1d910a4f6384e3761b4319c7867c8f3a50ce9e9b 100644 (file)
@@ -1,4 +1,5 @@
 import {orderedList, bulletList, listItem} from "prosemirror-schema-list";
+import {tableNodes} from "prosemirror-tables";
 
 /**
  * @param {HTMLElement} node
@@ -200,7 +201,7 @@ const callout = {
     ],
     toDOM(node) {
         const type = node.attrs.type || 'info';
-        return ['p', addAlignmentAttr(node, {class: 'callout ' + type}) , 0];
+        return ['p', addAlignmentAttr(node, {class: 'callout ' + type}), 0];
     }
 };
 
@@ -208,6 +209,16 @@ 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,
+    table_row,
+    table_cell,
+    table_header,
+} = tableNodes({
+    tableGroup: "block",
+    cellContent: "block*"
+});
+
 const nodes = {
     doc,
     paragraph,
@@ -222,6 +233,10 @@ const nodes = {
     ordered_list,
     bullet_list,
     list_item,
+    table,
+    table_row,
+    table_cell,
+    table_header,
 };
 
 export default nodes;
\ No newline at end of file
index c1cdf0de93cc74230dc49753fede820ba66f182e..82481e39784c0d893d815ccb97e017088c4154f0 100644 (file)
@@ -371,6 +371,29 @@ img.ProseMirror-separator {
   display: block;
 }
 
+.ProseMirror-menu-table-creator-grid {
+  display: grid;
+  gap: 2px;
+}
+
+.ProseMirror-menu-table-creator-grid-item {
+  width: 14px;
+  height: 14px;
+  border: 2px solid #BBB;
+  display: block;
+  cursor: pointer;
+}
+
+.ProseMirror-menu-table-creator-grid-item-active {
+  border: 2px solid #555;
+  background-color: #DDD;
+}
+
+.ProseMirror-menu-table-creator-grid-label {
+  padding: $-xs;
+  text-align: center;
+}
+
 .ProseMirror-menu-dialog-wrap {
   position: fixed;
   top: 0;