From b2283106fc64b9e96ab42b7828ed8602c68d61b1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 19 Jan 2022 11:31:02 +0000 Subject: [PATCH] Added source code view/set button --- TODO | 1 - resources/js/editor/ProseMirrorView.js | 18 ++-- resources/js/editor/menu/DialogTextArea.js | 42 +++++++++ resources/js/editor/menu/icons.js | 4 + resources/js/editor/menu/index.js | 2 + .../js/editor/menu/item-anchor-button.js | 6 ++ .../js/editor/menu/item-html-source-button.js | 87 +++++++++++++++++++ resources/js/editor/node-views/ImageView.js | 1 - resources/js/editor/util.js | 11 +++ resources/sass/_editor.scss | 12 +++ 10 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 resources/js/editor/menu/DialogTextArea.js create mode 100644 resources/js/editor/menu/item-html-source-button.js diff --git a/TODO b/TODO index d8d562c66..ad0665afb 100644 --- a/TODO +++ b/TODO @@ -20,7 +20,6 @@ - Code blocks - Indents - Iframe/Media -- View Code - Attachment integration (Drag & drop) - Template system integration. diff --git a/resources/js/editor/ProseMirrorView.js b/resources/js/editor/ProseMirrorView.js index 63a47dc35..cc979ffb3 100644 --- a/resources/js/editor/ProseMirrorView.js +++ b/resources/js/editor/ProseMirrorView.js @@ -2,11 +2,12 @@ import {EditorState} from "prosemirror-state"; import {EditorView} from "prosemirror-view"; import {exampleSetup} from "prosemirror-example-setup"; -import {DOMParser, DOMSerializer} from "prosemirror-model"; +import {DOMParser} from "prosemirror-model"; import schema from "./schema"; import menu from "./menu"; import nodeViews from "./node-views"; +import {stateToHtml} from "./util"; class ProseMirrorView { constructor(target, content) { @@ -28,13 +29,16 @@ class ProseMirrorView { } get content() { - const fragment = DOMSerializer.fromSchema(schema).serializeFragment(this.view.state.doc.content); - const renderDoc = document.implementation.createHTMLDocument(); - renderDoc.body.appendChild(fragment); - return renderDoc.body.innerHTML; + return stateToHtml(this.view.state); + } + + focus() { + this.view.focus() + } + + destroy() { + this.view.destroy() } - focus() { this.view.focus() } - destroy() { this.view.destroy() } } export default ProseMirrorView; \ No newline at end of file diff --git a/resources/js/editor/menu/DialogTextArea.js b/resources/js/editor/menu/DialogTextArea.js new file mode 100644 index 000000000..85cfacd8e --- /dev/null +++ b/resources/js/editor/menu/DialogTextArea.js @@ -0,0 +1,42 @@ +// ::- Represents a submenu wrapping a group of elements that start +// hidden and expand to the right when hovered over or tapped. +import {prefix, randHtmlId} from "./menu-utils"; +import crel from "crelt"; + +class DialogTextArea { + // :: (?Object) + // The following options are recognized: + // + // **`label`**`: string` + // : The label to show for the input. + // **`id`**`: string` + // : The id to use for this input + // **`attrs`**`: Object` + // : The attributes to add to the input element. + // **`value`**`: function(state) -> string` + // : The getter for the input value. + constructor(options) { + this.options = options || {}; + } + + // :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool} + // Renders the submenu. + render(view) { + const id = randHtmlId(); + const inputAttrs = Object.assign({type: "text", name: this.options.id, id: this.options.id}, this.options.attrs || {}) + const input = crel("textarea", inputAttrs); + const label = this.options.label ? crel("label", {for: id}, this.options.label) : null; + + const rowRap = crel("div", {class: prefix + '-dialog-textarea-wrap'}, label, input); + + const update = (state) => { + input.value = this.options.value(state); + return true; + } + + return {dom: rowRap, update} + } + +} + +export default DialogTextArea; \ No newline at end of file diff --git a/resources/js/editor/menu/icons.js b/resources/js/editor/menu/icons.js index 030ac75bf..ba9b54d5d 100644 --- a/resources/js/editor/menu/icons.js +++ b/resources/js/editor/menu/icons.js @@ -95,6 +95,10 @@ export const icons = { close: { width: 24, height: 24, path: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z", + }, + 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", } }; diff --git a/resources/js/editor/menu/index.js b/resources/js/editor/menu/index.js index cefa678fe..11ef86425 100644 --- a/resources/js/editor/menu/index.js +++ b/resources/js/editor/menu/index.js @@ -13,6 +13,7 @@ import DialogForm from "./DialogForm"; import DialogInput from "./DialogInput"; import itemAnchorButtonItem from "./item-anchor-button"; +import itemHtmlSourceButton from "./item-html-source-button"; function cmdItem(cmd, options) { @@ -156,6 +157,7 @@ const inserts = [ title: "Horizontal Rule", icon: icons.horizontal_rule, }), + itemHtmlSourceButton(), ]; const utilities = [ diff --git a/resources/js/editor/menu/item-anchor-button.js b/resources/js/editor/menu/item-anchor-button.js index d95ac2e78..02dfba1ab 100644 --- a/resources/js/editor/menu/item-anchor-button.js +++ b/resources/js/editor/menu/item-anchor-button.js @@ -57,6 +57,12 @@ function getLinkDialog(submitter, closer) { }); } +/** + * @param {FormData} formData + * @param {PmEditorState} state + * @param {PmDispatchFunction} dispatch + * @return {boolean} + */ function applyLink(formData, state, dispatch) { const selection = state.selection; const attrs = Object.fromEntries(formData); diff --git a/resources/js/editor/menu/item-html-source-button.js b/resources/js/editor/menu/item-html-source-button.js new file mode 100644 index 000000000..65b2331e9 --- /dev/null +++ b/resources/js/editor/menu/item-html-source-button.js @@ -0,0 +1,87 @@ +import DialogBox from "./DialogBox"; +import DialogForm from "./DialogForm"; +import DialogTextArea from "./DialogTextArea"; + +import {MenuItem} from "./menu"; +import {icons} from "./icons"; +import {htmlToDoc, stateToHtml} from "../util"; + +/** + * @param {(function(FormData))} submitter + * @param {Function} closer + * @return {DialogBox} + */ +function getLinkDialog(submitter, closer) { + return new DialogBox([ + new DialogForm([ + new DialogTextArea({ + id: 'source', + value: stateToHtml, + attrs: { + rows: 10, + cols: 50, + } + }), + ], { + canceler: closer, + action: submitter, + }), + ], { + label: 'View/Edit HTML Source', + closer: closer, + }); +} + +/** + * @param {FormData} formData + * @param {PmEditorState} state + * @param {PmDispatchFunction} dispatch + * @return {boolean} + */ +function replaceEditorHtml(formData, state, dispatch) { + const html = formData.get('source'); + + if (dispatch) { + const tr = state.tr; + + const newDoc = htmlToDoc(html); + tr.replaceWith(0, state.doc.content.size, newDoc.content); + dispatch(tr); + } + + return true; +} + + +/** + * @param {PmEditorState} state + * @param {PmDispatchFunction} dispatch + * @param {PmView} view + * @param {Event} e + */ +function onPress(state, dispatch, view, e) { + const dialog = getLinkDialog((data) => { + replaceEditorHtml(data, state, dispatch); + dom.remove(); + }, () => { + dom.remove(); + }) + + const {dom, update} = dialog.render(view); + update(state); + document.body.appendChild(dom); +} + +/** + * @return {MenuItem} + */ +function htmlSourceButtonItem() { + return new MenuItem({ + title: "View HTML Source", + run: onPress, + enable: state => true, + icon: icons.source_code, + }); +} + +export default htmlSourceButtonItem; \ No newline at end of file diff --git a/resources/js/editor/node-views/ImageView.js b/resources/js/editor/node-views/ImageView.js index b283d8dd9..08738eff5 100644 --- a/resources/js/editor/node-views/ImageView.js +++ b/resources/js/editor/node-views/ImageView.js @@ -65,7 +65,6 @@ class ImageView { } removeHandlesListener(event) { - console.log(this.dom.contains(event.target), event.target); if (!this.dom.contains(event.target)) { this.removeHandles(); this.handles = []; diff --git a/resources/js/editor/util.js b/resources/js/editor/util.js index f94aa10fc..6d3fb7417 100644 --- a/resources/js/editor/util.js +++ b/resources/js/editor/util.js @@ -22,6 +22,17 @@ export function docToHtml(doc) { return renderDoc.body.innerHTML; } +/** + * @param {PmEditorState} state + * @return {String} + */ +export function stateToHtml(state) { + const fragment = DOMSerializer.fromSchema(schema).serializeFragment(state.doc.content); + const renderDoc = document.implementation.createHTMLDocument(); + renderDoc.body.appendChild(fragment); + return renderDoc.body.innerHTML; +} + /** * @class KeyedMultiStack * Holds many stacks, seperated via a key, with a simple diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 6a74068b8..c1cdf0de9 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -454,6 +454,18 @@ 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; + } +} + .ProseMirror-imagewrap { display: inline-block; line-height: 0;