working newlines, link shortcut
This commit is contained in:
parent
676b673c94
commit
9a55d38e4b
|
@ -4,54 +4,54 @@ import markdownit from "markdown-it";
|
|||
import { writeFreelySchema } from "./schema";
|
||||
|
||||
export const writeAsMarkdownParser = new MarkdownParser(
|
||||
writeFreelySchema,
|
||||
markdownit("commonmark", { html: true }),
|
||||
{
|
||||
// blockquote: { block: "blockquote" },
|
||||
paragraph: { block: "paragraph" },
|
||||
list_item: { block: "list_item" },
|
||||
bullet_list: { block: "bullet_list" },
|
||||
ordered_list: {
|
||||
block: "ordered_list",
|
||||
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 {};
|
||||
},
|
||||
},
|
||||
writeFreelySchema,
|
||||
markdownit("commonmark", { html: true }),
|
||||
{
|
||||
// blockquote: { block: "blockquote" },
|
||||
paragraph: { block: "paragraph" },
|
||||
list_item: { block: "list_item" },
|
||||
bullet_list: { block: "bullet_list" },
|
||||
ordered_list: {
|
||||
block: "ordered_list",
|
||||
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 {};
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -47,10 +47,6 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer(
|
|||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
// horizontal_rule(state, node) {
|
||||
// state.write(node.attrs.markup || "---");
|
||||
// state.closeBlock(node);
|
||||
// },
|
||||
bullet_list(state, node) {
|
||||
state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `);
|
||||
},
|
||||
|
@ -75,13 +71,13 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer(
|
|||
state.write(
|
||||
`![${state.esc(node.attrs.alt || "")}](${state.esc(node.attrs.src)}${
|
||||
node.attrs.title ? ` ${state.quote(node.attrs.title)}` : ""
|
||||
})`,
|
||||
})`
|
||||
);
|
||||
},
|
||||
hard_break(state, node, parent, index) {
|
||||
for (let i = index + 1; i < parent.childCount; i += 1)
|
||||
if (parent.child(i).type !== node.type) {
|
||||
state.write("\n");
|
||||
state.write("\\\n");
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
@ -123,5 +119,5 @@ export const writeAsMarkdownSerializer = new MarkdownSerializer(
|
|||
},
|
||||
escape: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -4,23 +4,29 @@ import { buildMenuItems } from "prosemirror-example-setup";
|
|||
import { writeFreelySchema } from "./schema";
|
||||
|
||||
function canInsert(state, nodeType, attrs) {
|
||||
let $from = state.selection.$from
|
||||
for (let d = $from.depth; d >= 0; d--) {
|
||||
let index = $from.index(d)
|
||||
if ($from.node(d).canReplaceWith(index, index, nodeType, attrs)) return true
|
||||
}
|
||||
return false
|
||||
let $from = state.selection.$from;
|
||||
for (let d = $from.depth; d >= 0; d--) {
|
||||
let index = $from.index(d);
|
||||
if ($from.node(d).canReplaceWith(index, index, nodeType, attrs))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ReadMoreItem = new MenuItem({
|
||||
label: "Read more",
|
||||
select: (state) => canInsert(state, writeFreelySchema.nodes.readmore),
|
||||
run(state, dispatch) {
|
||||
dispatch(state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create()))
|
||||
},
|
||||
label: "Read more",
|
||||
select: (state) => canInsert(state, writeFreelySchema.nodes.readmore),
|
||||
run(state, dispatch) {
|
||||
dispatch(
|
||||
state.tr.replaceSelectionWith(writeFreelySchema.nodes.readmore.create())
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const getMenu = ()=> {
|
||||
const menuContent = [...buildMenuItems(writeFreelySchema).fullMenu, [ReadMoreItem]];
|
||||
return menuContent
|
||||
}
|
||||
export const getMenu = () => {
|
||||
const menuContent = [
|
||||
...buildMenuItems(writeFreelySchema).fullMenu,
|
||||
[ReadMoreItem],
|
||||
];
|
||||
return menuContent;
|
||||
};
|
||||
|
|
|
@ -6,5 +6,9 @@
|
|||
<!-- <label> <input type=radio name=inputformat value=prosemirror checked> WYSIWYM</label> -->
|
||||
<!-- </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? 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? So you can give people the **choice** to use a more familiar, discoverable interface.</textarea
|
||||
>
|
||||
</div>
|
||||
<script src="dist/prose.bundle.js"></script>
|
||||
|
|
166
prose/prose.js
166
prose/prose.js
|
@ -9,89 +9,101 @@
|
|||
// destroy() { this.textarea.remove() }
|
||||
// }
|
||||
|
||||
import { EditorView } from "prosemirror-view"
|
||||
import { EditorState } from "prosemirror-state"
|
||||
import { exampleSetup } from "prosemirror-example-setup"
|
||||
import { EditorView } from "prosemirror-view";
|
||||
import { EditorState, TextSelection } from "prosemirror-state";
|
||||
import { exampleSetup } from "prosemirror-example-setup";
|
||||
import { keymap } from "prosemirror-keymap";
|
||||
|
||||
import { writeAsMarkdownParser } from "./markdownParser"
|
||||
import { writeAsMarkdownSerializer } from "./markdownSerializer"
|
||||
import { writeFreelySchema } from "./schema"
|
||||
import { getMenu } from "./menu"
|
||||
import { writeAsMarkdownParser } from "./markdownParser";
|
||||
import { writeAsMarkdownSerializer } from "./markdownSerializer";
|
||||
import { writeFreelySchema } from "./schema";
|
||||
import { getMenu } from "./menu";
|
||||
|
||||
let $title = document.querySelector('#title')
|
||||
let $content = document.querySelector('#content')
|
||||
let $title = document.querySelector("#title");
|
||||
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 {
|
||||
constructor(target, content) {
|
||||
this.view = new EditorView(target, {
|
||||
state: EditorState.create({
|
||||
doc: function (content) {
|
||||
// console.log('loading '+window.draftKey)
|
||||
let localDraft = localStorage.getItem(window.draftKey);
|
||||
if (localDraft != null) {
|
||||
content = localDraft
|
||||
}
|
||||
if (content.indexOf("# ") === 0) {
|
||||
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)
|
||||
}
|
||||
})
|
||||
constructor(target, content) {
|
||||
let localDraft = localStorage.getItem(window.draftKey);
|
||||
if (localDraft != null) {
|
||||
content = localDraft;
|
||||
}
|
||||
if (content.indexOf("# ") === 0) {
|
||||
let eol = content.indexOf("\n");
|
||||
let title = content.substring("# ".length, eol);
|
||||
content = content.substring(eol + "\n\n".length);
|
||||
$title.value = title;
|
||||
}
|
||||
|
||||
get content() {
|
||||
return defaultMarkdownSerializer.serialize(this.view.state.doc)
|
||||
}
|
||||
focus() { this.view.focus() }
|
||||
destroy() { this.view.destroy() }
|
||||
const doc = writeAsMarkdownParser.parse(
|
||||
// Replace all "solo" \n's with \\\n for correct markdown parsing
|
||||
content.replaceAll(/(?<!\n)\n(?!\n)/g, "\\\n")
|
||||
);
|
||||
|
||||
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 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()
|
||||
// })
|
||||
// })
|
||||
let place = document.querySelector("#editor");
|
||||
let view = new ProseMirrorView(place, $content.value);
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import { schema } from "prosemirror-markdown"
|
||||
import { schema } from "prosemirror-markdown";
|
||||
import { Schema } from "prosemirror-model";
|
||||
|
||||
export const writeFreelySchema = new Schema({
|
||||
nodes: schema.spec.nodes.remove("blockquote")
|
||||
.remove("horizontal_rule")
|
||||
.addToEnd("readmore", {
|
||||
inline: false,
|
||||
content: "",
|
||||
group: "block",
|
||||
draggable: true,
|
||||
toDOM: (node) => ["div", { class: "editorreadmore", style: "width: 100%;text-align:center" }, "Read more..."],
|
||||
parseDOM: [{ tag: "div.editorreadmore" }],
|
||||
}),
|
||||
marks: schema.spec.marks,
|
||||
nodes: schema.spec.nodes
|
||||
.remove("blockquote")
|
||||
.remove("horizontal_rule")
|
||||
.addToEnd("readmore", {
|
||||
inline: false,
|
||||
content: "",
|
||||
group: "block",
|
||||
draggable: true,
|
||||
toDOM: (node) => [
|
||||
"div",
|
||||
{ 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
Loading…
Reference in New Issue