Added source code view/set button

This commit is contained in:
Dan Brown 2022-01-19 11:31:02 +00:00
parent 7125530e55
commit b2283106fc
No known key found for this signature in database
GPG key ID: 46D9F943C24A2EF9
10 changed files with 175 additions and 9 deletions

1
TODO
View file

@ -20,7 +20,6 @@
- Code blocks
- Indents
- Iframe/Media
- View Code
- Attachment integration (Drag & drop)
- Template system integration.

View file

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

View file

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

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

View file

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

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

View file

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

View file

@ -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 = [];

View file

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

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