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 AHeader B
Content 1Content 2
+

Logo