working newlines, link shortcut

This commit is contained in:
Viktor Vaczi 2021-01-08 00:33:35 +01:00
parent 676b673c94
commit 9a55d38e4b
7 changed files with 186 additions and 163 deletions

View File

@ -4,54 +4,54 @@ import markdownit from "markdown-it";
import { writeFreelySchema } from "./schema"; import { writeFreelySchema } from "./schema";
export const writeAsMarkdownParser = new MarkdownParser( export const writeAsMarkdownParser = new MarkdownParser(
writeFreelySchema, writeFreelySchema,
markdownit("commonmark", { html: true }), markdownit("commonmark", { html: true }),
{ {
// blockquote: { block: "blockquote" }, // blockquote: { block: "blockquote" },
paragraph: { block: "paragraph" }, paragraph: { block: "paragraph" },
list_item: { block: "list_item" }, list_item: { block: "list_item" },
bullet_list: { block: "bullet_list" }, bullet_list: { block: "bullet_list" },
ordered_list: { ordered_list: {
block: "ordered_list", block: "ordered_list",
getAttrs: (tok) => ({ order: +tok.attrGet("start") || 1 }), getAttrs: (tok) => ({ order: +tok.attrGet("start") || 1 }),
},
heading: {
block: "heading",
getAttrs: (tok) => ({ level: +tok.tag.slice(1) }),
},
code_block: { block: "code_block", noCloseToken: true },
fence: {
block: "code_block",
getAttrs: (tok) => ({ params: tok.info || "" }),
noCloseToken: true,
},
// hr: { node: "horizontal_rule" },
image: {
node: "image",
getAttrs: (tok) => ({
src: tok.attrGet("src"),
title: tok.attrGet("title") || null,
alt: tok.children?.[0].content || null,
}),
},
hardbreak: { node: "hard_break" },
em: { mark: "em" },
strong: { mark: "strong" },
link: {
mark: "link",
getAttrs: (tok) => ({
href: tok.attrGet("href"),
title: tok.attrGet("title") || null,
}),
},
code_inline: { mark: "code", noCloseToken: true },
html_block: {
node: "readmore",
getAttrs(token) {
// TODO: Give different attributes depending on the token content
return {};
},
},
}, },
heading: {
block: "heading",
getAttrs: (tok) => ({ level: +tok.tag.slice(1) }),
},
code_block: { block: "code_block", noCloseToken: true },
fence: {
block: "code_block",
getAttrs: (tok) => ({ params: tok.info || "" }),
noCloseToken: true,
},
// hr: { node: "horizontal_rule" },
image: {
node: "image",
getAttrs: (tok) => ({
src: tok.attrGet("src"),
title: tok.attrGet("title") || null,
alt: tok.children?.[0].content || null,
}),
},
hardbreak: { node: "hard_break" },
em: { mark: "em" },
strong: { mark: "strong" },
link: {
mark: "link",
getAttrs: (tok) => ({
href: tok.attrGet("href"),
title: tok.attrGet("title") || null,
}),
},
code_inline: { mark: "code", noCloseToken: true },
html_block: {
node: "readmore",
getAttrs(token) {
// TODO: Give different attributes depending on the token content
return {};
},
},
}
); );

View File

@ -47,10 +47,6 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer(
state.renderInline(node); state.renderInline(node);
state.closeBlock(node); state.closeBlock(node);
}, },
// horizontal_rule(state, node) {
// state.write(node.attrs.markup || "---");
// state.closeBlock(node);
// },
bullet_list(state, node) { bullet_list(state, node) {
state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `); state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `);
}, },
@ -75,13 +71,13 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer(
state.write( state.write(
`![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${ `![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${
node.attrs.title ? ` ${state.quote(node.attrs.title)}` : "" node.attrs.title ? ` ${state.quote(node.attrs.title)}` : ""
})`, })`
); );
}, },
hard_break(state, node, parent, index) { hard_break(state, node, parent, index) {
for (let i = index + 1; i < parent.childCount; i += 1) for (let i = index + 1; i < parent.childCount; i += 1)
if (parent.child(i).type !== node.type) { if (parent.child(i).type !== node.type) {
state.write("\n"); state.write("\\\n");
return; return;
} }
}, },
@ -123,5 +119,5 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer(
}, },
escape: false, escape: false,
}, },
}, }
); );

View File

@ -4,23 +4,29 @@ import { buildMenuItems } from "prosemirror-example-setup";
import { writeFreelySchema } from "./schema"; import { writeFreelySchema } from "./schema";
function canInsert(state, nodeType, attrs) { function canInsert(state, nodeType, attrs) {
let $from = state.selection.$from let $from = state.selection.$from;
for (let d = $from.depth; d >= 0; d--) { for (let d = $from.depth; d >= 0; d--) {
let index = $from.index(d) let index = $from.index(d);
if ($from.node(d).canReplaceWith(index, index, nodeType, attrs)) return true if ($from.node(d).canReplaceWith(index, index, nodeType, attrs))
} return true;
return false
} }
return false;
}
const ReadMoreItem = new MenuItem({ const ReadMoreItem = new MenuItem({
label: "Read more", label: "Read more",
select: (state) => canInsert(state, writeFreelySchema.nodes.readmore), select: (state) => canInsert(state, writeFreelySchema.nodes.readmore),
run(state, dispatch) { run(state, dispatch) {
dispatch(state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create())) dispatch(
}, state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create())
);
},
}); });
export const getMenu = ()=> { export const getMenu = () => {
const menuContent = [...buildMenuItems(writeFreelySchema).fullMenu, [ReadMoreItem]]; const menuContent = [
return menuContent ...buildMenuItems(writeFreelySchema).fullMenu,
} [ReadMoreItem],
];
return menuContent;
};

View File

@ -6,5 +6,9 @@
<!-- <label>&nbsp;<input type=radio name=inputformat value=prosemirror checked> WYSIWYM</label> --> <!-- <label>&nbsp;<input type=radio name=inputformat value=prosemirror checked> WYSIWYM</label> -->
<!-- </div> --> <!-- </div> -->
<div style="display: none"><textarea id="content">This is a comment written in [Markdown](http://commonmark.org). *You* may know the syntax for inserting a link, but does your whole audience?&#13;&#13;So you can give people the **choice** to use a more familiar, discoverable interface.</textarea></div> <div style="display: none">
<textarea id="content">
This is a comment written in [Markdown](http://commonmark.org). *You* may know the syntax for inserting a link, but does your whole audience?&#13;&#13;So you can give people the **choice** to use a more familiar, discoverable interface.</textarea
>
</div>
<script src="dist/prose.bundle.js"></script> <script src="dist/prose.bundle.js"></script>

View File

@ -9,89 +9,101 @@
// destroy() { this.textarea.remove() } // destroy() { this.textarea.remove() }
// } // }
import { EditorView } from "prosemirror-view" import { EditorView } from "prosemirror-view";
import { EditorState } from "prosemirror-state" import { EditorState, TextSelection } from "prosemirror-state";
import { exampleSetup } from "prosemirror-example-setup" import { exampleSetup } from "prosemirror-example-setup";
import { keymap } from "prosemirror-keymap"; import { keymap } from "prosemirror-keymap";
import { writeAsMarkdownParser } from "./markdownParser" import { writeAsMarkdownParser } from "./markdownParser";
import { writeAsMarkdownSerializer } from "./markdownSerializer" import { writeAsMarkdownSerializer } from "./markdownSerializer";
import { writeFreelySchema } from "./schema" import { writeFreelySchema } from "./schema";
import { getMenu } from "./menu" import { getMenu } from "./menu";
let $title = document.querySelector('#title') let $title = document.querySelector("#title");
let $content = document.querySelector('#content') let $content = document.querySelector("#content");
// Bugs:
// 1. When there's just an empty line and a hard break is inserted with shift-enter then two enters are inserted
// which do not show up in the markdown ( maybe bc. they are training enters )
class ProseMirrorView { class ProseMirrorView {
constructor(target, content) { constructor(target, content) {
this.view = new EditorView(target, { let localDraft = localStorage.getItem(window.draftKey);
state: EditorState.create({ if (localDraft != null) {
doc: function (content) { content = localDraft;
// console.log('loading '+window.draftKey) }
let localDraft = localStorage.getItem(window.draftKey); if (content.indexOf("# ") === 0) {
if (localDraft != null) { let eol = content.indexOf("\n");
content = localDraft let title = content.substring("# ".length, eol);
} content = content.substring(eol + "\n\n".length);
if (content.indexOf("# ") === 0) { $title.value = title;
let eol = content.indexOf("\n");
let title = content.substring("# ".length, eol);
content = content.substring(eol + "\n\n".length);
$title.value = title;
}
return writeAsMarkdownParser.parse(content)
}(content),
plugins: [
keymap({
"Mod-Enter": () => {
document.getElementById("publish").click();
return true;
},
"Mod-k": ()=> {
console.log("TODO-link");
return true;
}
}),
...exampleSetup({ schema: writeFreelySchema, menuContent: getMenu() }),
]
}),
dispatchTransaction(transaction) {
// console.log('saving to '+window.draftKey)
const newContent = writeAsMarkdownSerializer.serialize(transaction.doc)
console.log({newContent})
$content.value = newContent
localStorage.setItem(window.draftKey, function () {
let draft = "";
if ($title.value != null && $title.value !== "") {
draft = "# " + $title.value + "\n\n"
}
draft += $content.value
return draft
}());
let newState = this.state.apply(transaction)
this.updateState(newState)
}
})
} }
get content() { const doc = writeAsMarkdownParser.parse(
return defaultMarkdownSerializer.serialize(this.view.state.doc) // Replace all "solo" \n's with \\\n for correct markdown parsing
} content.replaceAll(/(?<!\n)\n(?!\n)/g, "\\\n")
focus() { this.view.focus() } );
destroy() { this.view.destroy() }
this.view = new EditorView(target, {
state: EditorState.create({
doc,
plugins: [
keymap({
"Mod-Enter": () => {
document.getElementById("publish").click();
return true;
},
"Mod-k": () => {
const linkButton = document.querySelector(".ProseMirror-icon[title='Add or remove link']")
linkButton.dispatchEvent(new Event('mousedown'));
return true;
},
}),
...exampleSetup({
schema: writeFreelySchema,
menuContent: getMenu(),
}),
],
}),
dispatchTransaction(transaction) {
const newContent = writeAsMarkdownSerializer
.serialize(transaction.doc)
// Replace all \\\ns ( not followed by a \n ) with \n
.replaceAll(/\\\n(?!\n)/g, "\n");
$content.value = newContent;
let draft = "";
if ($title.value != null && $title.value !== "") {
draft = "# " + $title.value + "\n\n";
}
draft += newContent;
localStorage.setItem(window.draftKey, draft);
let newState = this.state.apply(transaction);
this.updateState(newState);
},
});
// Editor is focused to the last position. This is a workaround for a bug:
// 1. 1 type something in an existing entry
// 2. reload - works fine, the draft is reloaded
// 3. reload again - the draft is somehow removed from localStorage and the original content is loaded
// When the editor is focused the content is re-saved to localStorage
// This is also useful for editing, so it's not a bad thing even
const lastPosition = this.view.state.doc.content.size;
const selection = TextSelection.create(this.view.state.doc, lastPosition);
this.view.dispatch(this.view.state.tr.setSelection(selection));
this.view.focus();
}
get content() {
return defaultMarkdownSerializer.serialize(this.view.state.doc);
}
focus() {
this.view.focus();
}
destroy() {
this.view.destroy();
}
} }
let place = document.querySelector("#editor") let place = document.querySelector("#editor");
let view = new ProseMirrorView(place, $content.value) let view = new ProseMirrorView(place, $content.value);
// document.querySelectorAll("input[type=radio]").forEach(button => {
// button.addEventListener("change", () => {
// if (!button.checked) return
// let View = button.value == "markdown" ? MarkdownView : ProseMirrorView
// if (view instanceof View) return
// let content = view.content
// view.destroy()
// view = new View(place, content)
// view.focus()
// })
// })

View File

@ -1,16 +1,21 @@
import { schema } from "prosemirror-markdown" import { schema } from "prosemirror-markdown";
import { Schema } from "prosemirror-model"; import { Schema } from "prosemirror-model";
export const writeFreelySchema = new Schema({ export const writeFreelySchema = new Schema({
nodes: schema.spec.nodes.remove("blockquote") nodes: schema.spec.nodes
.remove("horizontal_rule") .remove("blockquote")
.addToEnd("readmore", { .remove("horizontal_rule")
inline: false, .addToEnd("readmore", {
content: "", inline: false,
group: "block", content: "",
draggable: true, group: "block",
toDOM: (node) => ["div", { class: "editorreadmore", style: "width: 100%;text-align:center" }, "Read more..."], draggable: true,
parseDOM: [{ tag: "div.editorreadmore" }], toDOM: (node) => [
}), "div",
marks: schema.spec.marks, { class: "editorreadmore", style: "width: 100%;text-align:center" },
"Read more...",
],
parseDOM: [{ tag: "div.editorreadmore" }],
}),
marks: schema.spec.marks,
}); });

File diff suppressed because one or more lines are too long