]> BookStack Code Mirror - bookstack/commitdiff
Added source code view/set button
authorDan Brown <redacted>
Wed, 19 Jan 2022 11:31:02 +0000 (11:31 +0000)
committerDan Brown <redacted>
Wed, 19 Jan 2022 11:31:02 +0000 (11:31 +0000)
TODO
resources/js/editor/ProseMirrorView.js
resources/js/editor/menu/DialogTextArea.js [new file with mode: 0644]
resources/js/editor/menu/icons.js
resources/js/editor/menu/index.js
resources/js/editor/menu/item-anchor-button.js
resources/js/editor/menu/item-html-source-button.js [new file with mode: 0644]
resources/js/editor/node-views/ImageView.js
resources/js/editor/util.js
resources/sass/_editor.scss

diff --git a/TODO b/TODO
index d8d562c6642446f28b7cfda5653e3d2294f5d9f3..ad0665afbd13bb361b056507a58e3750b3205025 100644 (file)
--- a/TODO
+++ b/TODO
@@ -20,7 +20,6 @@
 - Code blocks
 - Indents
 - Iframe/Media
 - Code blocks
 - Indents
 - Iframe/Media
-- View Code
 - Attachment integration (Drag & drop)
 - Template system integration.
 
 - Attachment integration (Drag & drop)
 - Template system integration.
 
index 63a47dc35b09f07b3e78c0b3cb49df30a996f984..cc979ffb3f6e55c52d9d6cc508f6bee6a4daea68 100644 (file)
@@ -2,11 +2,12 @@ import {EditorState} from "prosemirror-state";
 import {EditorView} from "prosemirror-view";
 import {exampleSetup} from "prosemirror-example-setup";
 
 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 schema from "./schema";
 import menu from "./menu";
 import nodeViews from "./node-views";
+import {stateToHtml} from "./util";
 
 class ProseMirrorView {
     constructor(target, content) {
 
 class ProseMirrorView {
     constructor(target, content) {
@@ -28,13 +29,16 @@ class ProseMirrorView {
     }
 
     get content() {
     }
 
     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
 }
 
 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 (file)
index 0000000..85cfacd
--- /dev/null
@@ -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
index 030ac75bfe52386bdfbfb1ad67159c84ca077b2d..ba9b54d5d03f7ec16a32dc79300497c959a508fe 100644 (file)
@@ -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",
   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",
   }
 };
 
   }
 };
 
index cefa678fea120b08dd9bbeba3140610744345fda..11ef864252e3fe28052763fae0f9452d5e98a6dd 100644 (file)
@@ -13,6 +13,7 @@ import DialogForm from "./DialogForm";
 import DialogInput from "./DialogInput";
 
 import itemAnchorButtonItem from "./item-anchor-button";
 import DialogInput from "./DialogInput";
 
 import itemAnchorButtonItem from "./item-anchor-button";
+import itemHtmlSourceButton from "./item-html-source-button";
 
 
 function cmdItem(cmd, options) {
 
 
 function cmdItem(cmd, options) {
@@ -156,6 +157,7 @@ const inserts = [
         title: "Horizontal Rule",
         icon: icons.horizontal_rule,
     }),
         title: "Horizontal Rule",
         icon: icons.horizontal_rule,
     }),
+    itemHtmlSourceButton(),
 ];
 
 const utilities = [
 ];
 
 const utilities = [
index d95ac2e78f339a9c604e82f9e1ec938399d17ae9..02dfba1ab830a62e676ebed9e8b5f4916006034c 100644 (file)
@@ -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);
 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 (file)
index 0000000..65b2331
--- /dev/null
@@ -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
index b283d8dd9e1fa3fd6721b76bf39b627ab1fa6c27..08738eff51c49d3a7224845631417ad4663038b2 100644 (file)
@@ -65,7 +65,6 @@ class ImageView {
     }
 
     removeHandlesListener(event) {
     }
 
     removeHandlesListener(event) {
-        console.log(this.dom.contains(event.target), event.target);
         if (!this.dom.contains(event.target)) {
             this.removeHandles();
             this.handles = [];
         if (!this.dom.contains(event.target)) {
             this.removeHandles();
             this.handles = [];
index f94aa10fc93a908c69b04b13d014736e2a9bb546..6d3fb74176d493212c77a628cfdbab5ff176f2e2 100644 (file)
@@ -22,6 +22,17 @@ export function docToHtml(doc) {
     return renderDoc.body.innerHTML;
 }
 
     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
 /**
  * @class KeyedMultiStack
  * Holds many stacks, seperated via a key, with a simple
index 6a74068b81b9ce1886cf7e03175e5a37de8f3be5..c1cdf0de93cc74230dc49753fede820ba66f182e 100644 (file)
@@ -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;
 .ProseMirror-imagewrap {
   display: inline-block;
   line-height: 0;