From 4b08eef12c3cd60545f37deb14bc2e61144ee927 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 19 Jan 2022 15:22:10 +0000 Subject: [PATCH] Added table creation and insertion --- TODO | 3 +- package-lock.json | 25 ++++++ package.json | 1 + resources/js/editor/ProseMirrorView.js | 2 + resources/js/editor/commands.js | 29 +++++++ resources/js/editor/menu/TableCreatorGrid.js | 80 ++++++++++++++++++++ resources/js/editor/menu/icons.js | 4 + resources/js/editor/menu/index.js | 7 +- resources/js/editor/schema-nodes.js | 17 ++++- resources/sass/_editor.scss | 23 ++++++ 10 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 resources/js/editor/menu/TableCreatorGrid.js diff --git a/TODO b/TODO index ad0665afb..018cd7af2 100644 --- a/TODO +++ b/TODO @@ -4,11 +4,10 @@ ### In-Progress -// +- Tables ### Features -- Tables - Images - Drawings - LTR/RTL control diff --git a/package-lock.json b/package-lock.json index 121589580..5bc80061c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, @@ -1520,6 +1521,18 @@ "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", @@ -3083,6 +3096,18 @@ "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", diff --git a/package.json b/package.json index bd16a728d..d986ef26d 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/resources/js/editor/ProseMirrorView.js b/resources/js/editor/ProseMirrorView.js index cc979ffb3..bfd209db1 100644 --- a/resources/js/editor/ProseMirrorView.js +++ b/resources/js/editor/ProseMirrorView.js @@ -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, diff --git a/resources/js/editor/commands.js b/resources/js/editor/commands.js index bd71ceba3..904dbb9c8 100644 --- a/resources/js/editor/commands.js +++ b/resources/js/editor/commands.js @@ -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 index 000000000..e545b3d0f --- /dev/null +++ b/resources/js/editor/menu/TableCreatorGrid.js @@ -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 diff --git a/resources/js/editor/menu/icons.js b/resources/js/editor/menu/icons.js index ba9b54d5d..3166f5dac 100644 --- a/resources/js/editor/menu/icons.js +++ b/resources/js/editor/menu/icons.js @@ -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", } }; diff --git a/resources/js/editor/menu/index.js b/resources/js/editor/menu/index.js index 11ef86425..665d5f9ef 100644 --- a/resources/js/editor/menu/index.js +++ b/resources/js/editor/menu/index.js @@ -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(), ]; diff --git a/resources/js/editor/schema-nodes.js b/resources/js/editor/schema-nodes.js index 5620ada5b..1d910a4f6 100644 --- a/resources/js/editor/schema-nodes.js +++ b/resources/js/editor/schema-nodes.js @@ -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 diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index c1cdf0de9..82481e397 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -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;