From 9b4ea368dc4cd520b9314f72dc94400cf8d1a593 Mon Sep 17 00:00:00 2001
From: Dan Brown
Date: Wed, 19 Jan 2022 16:46:45 +0000
Subject: [PATCH] Started on table editing/resizing
---
TODO | 4 +-
resources/js/editor/ProseMirrorView.js | 7 +-
resources/js/editor/commands.js | 8 +-
resources/js/editor/markdown-serializer.js | 4 +
resources/js/editor/menu/TableCreatorGrid.js | 30 +++---
resources/js/editor/schema-nodes.js | 68 +++++++++---
resources/sass/_editor.scss | 107 +++++++++++++++----
resources/views/editor-test.blade.php | 16 +++
8 files changed, 196 insertions(+), 48 deletions(-)
diff --git a/TODO b/TODO
index 018cd7af2..2fad1346e 100644
--- 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
diff --git a/resources/js/editor/ProseMirrorView.js b/resources/js/editor/ProseMirrorView.js
index bfd209db1..6b977dea4 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} 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() {
diff --git a/resources/js/editor/commands.js b/resources/js/editor/commands.js
index 904dbb9c8..bbb815b1d 100644
--- a/resources/js/editor/commands.js
+++ b/resources/js/editor/commands.js
@@ -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);
diff --git a/resources/js/editor/markdown-serializer.js b/resources/js/editor/markdown-serializer.js
index 8e7da7d91..ad7783243 100644
--- a/resources/js/editor/markdown-serializer.js
+++ b/resources/js/editor/markdown-serializer.js
@@ -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
diff --git a/resources/js/editor/menu/TableCreatorGrid.js b/resources/js/editor/menu/TableCreatorGrid.js
index e545b3d0f..293f0ae9f 100644
--- a/resources/js/editor/menu/TableCreatorGrid.js
+++ b/resources/js/editor/menu/TableCreatorGrid.js
@@ -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};
diff --git a/resources/js/editor/schema-nodes.js b/resources/js/editor/schema-nodes.js
index 1d910a4f6..69a253f20 100644
--- a/resources/js/editor/schema-nodes.js
+++ b/resources/js/editor/schema-nodes.js
@@ -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,
diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss
index 82481e397..9b6a5ea5e 100644
--- a/resources/sass/_editor.scss
+++ b/resources/sass/_editor.scss
@@ -1,5 +1,4 @@
-
#editor.bs-editor {
padding-top: 0;
}
@@ -46,9 +45,21 @@
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;
+}
diff --git a/resources/views/editor-test.blade.php b/resources/views/editor-test.blade.php
index aef1a689a..ef7e63cac 100644
--- a/resources/views/editor-test.blade.php
+++ b/resources/views/editor-test.blade.php
@@ -18,6 +18,22 @@
Some Red Content Lorem ipsum dolor sit amet.
Some Linked Content Lorem ipsum dolor sit amet.
+
+
+
+
+ Header A |
+ Header B |
+
+
+
+
+ Content 1 |
+ Content 2 |
+
+
+
+