]> BookStack Code Mirror - bookstack/commitdiff
Started on table editing/resizing
authorDan Brown <redacted>
Wed, 19 Jan 2022 16:46:45 +0000 (16:46 +0000)
committerDan Brown <redacted>
Wed, 19 Jan 2022 16:46:45 +0000 (16:46 +0000)
TODO
resources/js/editor/ProseMirrorView.js
resources/js/editor/commands.js
resources/js/editor/markdown-serializer.js
resources/js/editor/menu/TableCreatorGrid.js
resources/js/editor/schema-nodes.js
resources/sass/_editor.scss
resources/views/editor-test.blade.php

diff --git a/TODO b/TODO
index 018cd7af26be6d2edfcd7704d13826bc50ff240a..2fad1346e920a014f32d296e00e9ba930d157d0f 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,6 +1,8 @@
 ### Next
 
-// 
+- 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.
 
 ### In-Progress
 
index bfd209db1fe4ffef32c1aa9c3e5334ba4679fd4f..6b977dea4aa9c8434fe9d2cbc80647d840f7fc1a 100644 (file)
@@ -1,7 +1,7 @@
 import {EditorState} from "prosemirror-state";
 import {EditorView} from "prosemirror-view";
 import {exampleSetup} from "prosemirror-example-setup";
-import {tableEditing} from "prosemirror-tables";
+import {tableEditing, columnResizing} from "prosemirror-tables";
 
 import {DOMParser} from "prosemirror-model";
 
@@ -23,11 +23,16 @@ class ProseMirrorView {
                 plugins: [
                     ...exampleSetup({schema, menuBar: false}),
                     menu,
+                    columnResizing(),
                     tableEditing(),
                 ]
             }),
             nodeViews,
         });
+
+        // Fix for native handles (Such as table size handling) in some browsers
+        document.execCommand("enableObjectResizing", false, "false")
+        document.execCommand("enableInlineTableEditing", false, "false")
     }
 
     get content() {
index 904dbb9c893ee634824f14b0af1d3cff5eabd56c..bbb815b1d6f290beef6356db040f2626bd249a5b 100644 (file)
@@ -61,9 +61,10 @@ export function insertBlockBefore(blockType) {
 /**
  * @param {Number} rows
  * @param {Number} columns
+ * @param {Object} tableAttrs
  * @return {PmCommandHandler}
  */
-export function insertTable(rows, columns) {
+export function insertTable(rows, columns, tableAttrs) {
     return function (state, dispatch) {
         if (!dispatch) return true;
 
@@ -74,12 +75,13 @@ export function insertTable(rows, columns) {
         for (let y = 0; y < rows; y++) {
             const rowCells = [];
             for (let x = 0; x < columns; x++) {
-                rowCells.push(nodes.table_cell.create(null));
+                const cellText = nodes.paragraph.create(null);
+                rowCells.push(nodes.table_cell.create(null, cellText));
             }
             rowNodes.push(nodes.table_row.create(null, rowCells));
         }
 
-        const table = nodes.table.create(null, rowNodes);
+        const table = nodes.table.create(tableAttrs, rowNodes);
         tr.replaceSelectionWith(table);
         dispatch(tr);
 
index 8e7da7d91855abefe6db3307fa62e5b58d9f65e7..ad7783243018484fc15f2cec59ed9add1864ecaa 100644 (file)
@@ -9,6 +9,10 @@ nodes.callout = function (state, node) {
     writeNodeAsHtml(state, node);
 };
 
+nodes.table = function (state, node) {
+    writeNodeAsHtml(state, node);
+};
+
 function isPlainURL(link, parent, index, side) {
     if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) {
         return false
index e545b3d0f5086229dd339198224a53a5f4bd8f20..293f0ae9f3bd8ea51c91dc4845113a357e2e3efa 100644 (file)
@@ -5,7 +5,6 @@ import {insertTable} from "../commands";
 class TableCreatorGrid {
 
     constructor() {
-        this.gridItems = [];
         this.size = 10;
         this.label = null;
     }
@@ -14,26 +13,31 @@ class TableCreatorGrid {
     // Renders the submenu.
     render(view) {
 
+        const gridItems = [];
         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));
+                gridItems.push(elem);
+                elem.addEventListener('mouseenter', event => {
+                    this.updateGridItemActiveStatus(elem, gridItems);
+                });
             }
         }
 
         const gridWrap = crel("div", {
             class: prefix + "-table-creator-grid",
             style: `grid-template-columns: repeat(${this.size}, 14px);`,
-        }, this.gridItems);
+        }, gridItems);
 
         gridWrap.addEventListener('mouseleave', event => {
-            this.updateGridItemActiveStatus(null);
+            this.updateGridItemActiveStatus(null, gridItems);
         });
         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 {x, y} = this.getPositionOfGridItem(event.target, gridItems);
+                insertTable(y + 1, x + 1, {
+                    style: 'width: 100%;',
+                })(view.state, view.dispatch);
             }
         });
 
@@ -50,15 +54,16 @@ class TableCreatorGrid {
 
     /**
      * @param {Element|null} newTarget
+     * @param {Element[]} gridItems
      */
-    updateGridItemActiveStatus(newTarget) {
-        const {x: xPos, y: yPos} = this.getPositionOfGridItem(newTarget);
+    updateGridItemActiveStatus(newTarget, gridItems) {
+        const {x: xPos, y: yPos} = this.getPositionOfGridItem(newTarget, gridItems);
 
         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);
+                gridItems[index].classList.toggle(prefix + "-table-creator-grid-item-active", active);
             }
         }
 
@@ -67,10 +72,11 @@ class TableCreatorGrid {
 
     /**
      * @param {Element} gridItem
+     * @param {Element[]} gridItems
      * @return {{x: number, y: number}}
      */
-    getPositionOfGridItem(gridItem) {
-        const index = this.gridItems.indexOf(gridItem);
+    getPositionOfGridItem(gridItem, gridItems) {
+        const index = gridItems.indexOf(gridItem);
         const y = Math.floor(index / this.size);
         const x = index % this.size;
         return {x, y};
index 1d910a4f6384e3761b4319c7867c8f3a50ce9e9b..69a253f20c1583dc46151683bba3ea4d1898a066 100644 (file)
@@ -17,17 +17,6 @@ function getAlignAttrFromDomNode(node) {
     return null;
 }
 
-/**
- * @param {String} className
- * @param {Object} attrs
- * @return {Object}
- */
-function addClassToAttrs(className, attrs) {
-    return Object.assign({}, attrs, {
-        class: attrs.class ? attrs.class + ' ' + className : className,
-    });
-}
-
 /**
  * @param node
  * @param {Object} attrs
@@ -49,6 +38,45 @@ function getAttrsParserForAlignment(node) {
     };
 }
 
+/**
+ * @param {String} className
+ * @param {Object} attrs
+ * @return {Object}
+ */
+function addClassToAttrs(className, attrs) {
+    return Object.assign({}, attrs, {
+        class: attrs.class ? attrs.class + ' ' + className : className,
+    });
+}
+
+/**
+ * @param {String[]} attrNames
+ * @return {function(Element): {}}
+ */
+function domAttrsToAttrsParser(attrNames) {
+    return function (node) {
+        const attrs = {};
+        for (const attr of attrNames) {
+            attrs[attr] = node.hasAttribute(attr) ? node.getAttribute(attr) : null;
+        }
+        return attrs;
+    };
+}
+
+/**
+ * @param {PmNode} node
+ * @param {String[]} attrNames
+ */
+function extractAttrsForDom(node, attrNames) {
+    const domAttrs = {};
+    for (const attr of attrNames) {
+        if (node.attrs[attr]) {
+            domAttrs[attr] = node.attrs[attr];
+        }
+    }
+    return domAttrs;
+}
+
 const doc = {
     content: "block+",
 };
@@ -210,15 +238,29 @@ const bullet_list = Object.assign({}, bulletList, {content: "list_item+", group:
 const list_item = Object.assign({}, listItem, {content: 'paragraph block*'});
 
 const {
-    table,
     table_row,
     table_cell,
     table_header,
 } = tableNodes({
     tableGroup: "block",
-    cellContent: "block*"
+    cellContent: "block+"
 });
 
+const table = {
+    content: "table_row+",
+    attrs: {
+        style: {default: null},
+    },
+    tableRole: "table",
+    isolating: true,
+    group: "block",
+    parseDOM: [{tag: "table", getAttrs: domAttrsToAttrsParser(['style'])}],
+    toDOM(node) {
+        console.log(extractAttrsForDom(node, ['style']));
+        return ["table", extractAttrsForDom(node, ['style']), ["tbody", 0]]
+    }
+};
+
 const nodes = {
     doc,
     paragraph,
index 82481e39784c0d893d815ccb97e017088c4154f0..9b6a5ea5ec9f333498fc257fe32bda0550793df3 100644 (file)
@@ -1,5 +1,4 @@
 
-
 #editor.bs-editor {
   padding-top: 0;
 }
   position: relative;
 }
 
-.ProseMirror-hideselection *::selection { background: transparent; }
-.ProseMirror-hideselection *::-moz-selection { background: transparent; }
-.ProseMirror-hideselection { caret-color: transparent; }
+.ProseMirror table td, .ProseMirror table th {
+  min-height: 1rem;
+}
+
+.ProseMirror-hideselection *::selection {
+  background: transparent;
+}
+
+.ProseMirror-hideselection *::-moz-selection {
+  background: transparent;
+}
+
+.ProseMirror-hideselection {
+  caret-color: transparent;
+}
 
 .ProseMirror-selectednode {
   outline: 2px solid #8cf;
@@ -64,7 +75,9 @@ li.ProseMirror-selectednode:after {
   content: "";
   position: absolute;
   left: -32px;
-  right: -2px; top: -2px; bottom: -2px;
+  right: -2px;
+  top: -2px;
+  bottom: -2px;
   border: 2px solid #8cf;
   pointer-events: none;
 }
@@ -201,7 +214,9 @@ img.ProseMirror-separator {
   min-height: 1em;
   color: #666;
   padding: 1px 6px;
-  top: 0; left: 0; right: 0;
+  top: 0;
+  left: 0;
+  right: 0;
   border-bottom: 1px solid silver;
   background: white;
   z-index: 10;
@@ -256,6 +271,7 @@ img.ProseMirror-separator {
 .ProseMirror-focused .ProseMirror-gapcursor {
   display: block;
 }
+
 /* Add space around the hr to make clicking it easier */
 
 .ProseMirror-example-setup-style hr {
@@ -271,7 +287,8 @@ img.ProseMirror-separator {
 .ProseMirror blockquote {
   padding-left: 1em;
   border-left: 3px solid #eee;
-  margin-left: 0; margin-right: 0;
+  margin-left: 0;
+  margin-right: 0;
 }
 
 .ProseMirror-example-setup-style img {
@@ -308,9 +325,12 @@ img.ProseMirror-separator {
 
 .ProseMirror-prompt-close {
   position: absolute;
-  left: 2px; top: 1px;
+  left: 2px;
+  top: 1px;
   color: #666;
-  border: none; background: transparent; padding: 0;
+  border: none;
+  background: transparent;
+  padding: 0;
 }
 
 .ProseMirror-prompt-close:after {
@@ -331,6 +351,7 @@ img.ProseMirror-separator {
   margin-top: 5px;
   display: none;
 }
+
 #editor, .editor {
   background: white;
   color: black;
@@ -341,13 +362,13 @@ img.ProseMirror-separator {
   margin-bottom: 23px;
 }
 
-.ProseMirror p:first-child,
-.ProseMirror h1:first-child,
-.ProseMirror h2:first-child,
-.ProseMirror h3:first-child,
-.ProseMirror h4:first-child,
-.ProseMirror h5:first-child,
-.ProseMirror h6:first-child {
+.ProseMirror p:first-child,
+.ProseMirror h1:first-child,
+.ProseMirror h2:first-child,
+.ProseMirror h3:first-child,
+.ProseMirror h4:first-child,
+.ProseMirror h5:first-child,
+.ProseMirror h6:first-child {
   margin-top: 10px;
 }
 
@@ -357,7 +378,9 @@ img.ProseMirror-separator {
   outline: none;
 }
 
-.ProseMirror p { margin-bottom: 1em }
+.ProseMirror > p {
+  margin-bottom: 1em
+}
 
 .ProseMirror-menu-color-grid-container {
   display: grid;
@@ -454,6 +477,7 @@ img.ProseMirror-separator {
   color: #666;
   min-width: 80px;
   cursor: pointer;
+
   &:hover {
     background-color: #EEE;
   }
@@ -468,10 +492,12 @@ img.ProseMirror-separator {
   grid-template-columns: 1fr 2fr;
   align-items: center;
   padding: $-xs 0;
+
   label {
     padding: 0 $-s;
     font-size: .9rem;
   }
+
   input {
     margin: 0 $-s;
   }
@@ -479,10 +505,12 @@ img.ProseMirror-separator {
 
 .ProseMirror-menu-dialog-textarea-wrap {
   padding: $-xs $-s;
+
   label {
     padding: 0 $-s;
     font-size: .9rem;
   }
+
   textarea {
     width: 100%;
     font-size: 0.8rem;
@@ -495,6 +523,7 @@ img.ProseMirror-separator {
   font-size: 0;
   position: relative;
 }
+
 .ProseMirror-imagewrap.ProseMirror-selectednode {
   outline: 0;
 }
@@ -502,6 +531,7 @@ img.ProseMirror-separator {
 .ProseMirror img[data-show-handles] {
   outline: 4px solid #000;
 }
+
 .ProseMirror-dragdummy {
   position: absolute;
   z-index: 2;
@@ -510,6 +540,7 @@ img.ProseMirror-separator {
   max-width: none !important;
   max-height: none !important;
 }
+
 .ProseMirror-grabhandle {
   width: 12px;
   height: 12px;
@@ -518,15 +549,55 @@ img.ProseMirror-separator {
   position: absolute;
   background-color: #FFF;
 }
+
 .ProseMirror-grabhandle-left-top {
   cursor: nw-resize;
 }
+
 .ProseMirror-grabhandle-right-top {
   cursor: ne-resize;
 }
+
 .ProseMirror-grabhandle-right-bottom {
   cursor: se-resize;
 }
+
 .ProseMirror-grabhandle-left-bottom {
   cursor: sw-resize;
-}
\ No newline at end of file
+}
+
+.ProseMirror .tableWrapper {
+  overflow-x: auto;
+}
+.ProseMirror table {
+  border-collapse: collapse;
+  table-layout: fixed;
+  width: 100%;
+  overflow: hidden;
+}
+.ProseMirror td, .ProseMirror th {
+  vertical-align: top;
+  box-sizing: border-box;
+  position: relative;
+}
+.ProseMirror .column-resize-handle {
+  position: absolute;
+  right: -2px; top: 0; bottom: 0;
+  width: 4px;
+  z-index: 20;
+  background-color: #adf;
+  pointer-events: none;
+}
+.ProseMirror.resize-cursor {
+  cursor: ew-resize;
+  cursor: col-resize;
+}
+/* Give selected cells a blue overlay */
+.ProseMirror .selectedCell:after {
+  z-index: 2;
+  position: absolute;
+  content: "";
+  left: 0; right: 0; top: 0; bottom: 0;
+  background: rgba(200, 200, 255, 0.4);
+  pointer-events: none;
+}
index aef1a689a0979227ac8ed9b001f5360767114e23..ef7e63cac9da8351d5f67df4d30e1d44d54b6cb8 100644 (file)
                 Some <span style="color: red;">Red Content</span> Lorem ipsum dolor sit amet. <br>
                 Some <a href="https://cats.com" target="_blank" title="link A">Linked Content</a> Lorem ipsum dolor sit amet. <br>
             </p>
+
+            <table style="width: 100%;">
+                <thead>
+                <tr>
+                    <th>Header A</th>
+                    <th>Header B</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr>
+                    <td>Content 1</td>
+                    <td>Content 2</td>
+                </tr>
+                </tbody>
+            </table>
+
             <p><img src="/user_avatar.png" alt="Logo"></p>
             <ul>
                 <li>Item A</li>