Started on table editing/resizing

This commit is contained in:
Dan Brown 2022-01-19 16:46:45 +00:00
parent 4b08eef12c
commit 9b4ea368dc
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
8 changed files with 196 additions and 48 deletions

4
TODO
View file

@ -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

View 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() {

View 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);

View 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

View 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};

View 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,

View file

@ -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;
}
}
.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;
}

View file

@ -18,6 +18,22 @@
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>