diff --git a/less/prose-editor.less b/less/prose-editor.less index 6cd1cb5..bc47898 100644 --- a/less/prose-editor.less +++ b/less/prose-editor.less @@ -1,490 +1,502 @@ @classicHorizMargin: 2rem; body#pad.classic { header { display: flex; justify-content: space-between; align-items: center; } #editor { top: 4em; bottom: 1em; } #title { top: 4.25rem; bottom: unset; height: auto; font-weight: bold; font-size: 2em; padding: 0; border: 0; } #tools { #belt { float: none; } } #target { ul { a { padding: 0 0.5em !important; } } } } +.norm { + font-family: @serifFont; +} + +.sans { + font-family: @sansFont; +} + +.wrap { + font-family: @monoFont; +} + #title { margin-left: @classicHorizMargin; margin-right: @classicHorizMargin; } .ProseMirror { position: relative; height: calc(~"100% - 1.6em"); overflow-y: auto; box-sizing: border-box; -moz-box-sizing: border-box; font-size: 1.2em; word-wrap: break-word; white-space: pre-wrap; -webkit-font-variant-ligatures: none; font-variant-ligatures: none; padding: 0.5em @classicHorizMargin; line-height: 1.5; outline: none; } .ProseMirror pre { white-space: pre-wrap; } .ProseMirror li { position: relative; } .ProseMirror-hideselection *::selection { background: transparent; } .ProseMirror-hideselection *::-moz-selection { background: transparent; } .ProseMirror-hideselection { caret-color: transparent; } .ProseMirror-selectednode { outline: 2px solid #8cf; } /* Make sure li selections wrap around markers */ li.ProseMirror-selectednode { outline: none; } li.ProseMirror-selectednode:after { content: ""; position: absolute; left: -32px; right: -2px; top: -2px; bottom: -2px; border: 2px solid #8cf; pointer-events: none; } .ProseMirror-textblock-dropdown { min-width: 3em; } .ProseMirror-menu { margin: 0 -4px; line-height: 1; } .ProseMirror-tooltip .ProseMirror-menu { width: -webkit-fit-content; width: fit-content; white-space: pre; } .ProseMirror-menuitem { margin-right: 3px; display: inline-block; div { cursor: pointer; } } .ProseMirror-menuseparator { border-right: 1px solid #ddd; margin-right: 3px; } .ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu { font-size: 90%; white-space: nowrap; } .ProseMirror-menu-dropdown { vertical-align: 1px; cursor: pointer; position: relative; padding-right: 15px; } .ProseMirror-menu-dropdown-wrap { padding: 1px 0 1px 4px; display: inline-block; position: relative; } .ProseMirror-menu-dropdown:after { content: ""; border-left: 4px solid transparent; border-right: 4px solid transparent; border-top: 4px solid currentColor; opacity: .6; position: absolute; right: 4px; top: calc(50% - 2px); } .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { position: absolute; background: white; color: #666; border: 1px solid #aaa; padding: 2px; } .ProseMirror-menu-dropdown-menu { z-index: 15; min-width: 6em; } .ProseMirror-menu-dropdown-item { cursor: pointer; padding: 2px 8px 2px 4px; } .ProseMirror-menu-dropdown-item:hover { background: #f2f2f2; } .ProseMirror-menu-submenu-wrap { position: relative; margin-right: -4px; } .ProseMirror-menu-submenu-label:after { content: ""; border-top: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 4px solid currentColor; opacity: .6; position: absolute; right: 4px; top: calc(50% - 4px); } .ProseMirror-menu-submenu { display: none; min-width: 4em; left: 100%; top: -3px; } .ProseMirror-menu-active { background: #eee; border-radius: 4px; } .ProseMirror-menu-active { background: #eee; border-radius: 4px; } .ProseMirror-menu-disabled { opacity: .3; } .ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { display: block; } .ProseMirror-menubar { font-family: @sansFont; position: relative; min-height: 1em; color: #666; padding: 0.5em; top: 0; left: 0; right: 0; background: rgba(255, 255, 255, 0.8); z-index: 10; -moz-box-sizing: border-box; box-sizing: border-box; overflow: visible; margin-left: @classicHorizMargin; margin-right: @classicHorizMargin; } .ProseMirror-icon { display: inline-block; line-height: .8; vertical-align: -2px; /* Compensate for padding */ padding: 2px 8px; cursor: pointer; } .ProseMirror-menu-disabled.ProseMirror-icon { cursor: default; } .ProseMirror-icon svg { fill: currentColor; height: 1em; } .ProseMirror-icon span { vertical-align: text-top; } .ProseMirror-gapcursor { display: none; pointer-events: none; position: absolute; } .ProseMirror-gapcursor:after { content: ""; display: block; position: absolute; top: -2px; width: 20px; border-top: 1px solid black; animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; } @keyframes ProseMirror-cursor-blink { to { visibility: hidden; } } .ProseMirror-focused .ProseMirror-gapcursor { display: block; } /* Add space around the hr to make clicking it easier */ .ProseMirror-example-setup-style hr { padding: 4px 10px; border: none; margin: 1em 0; background: initial; } .ProseMirror-example-setup-style hr:after { content: ""; display: block; height: 1px; background-color: #ccc; line-height: 2px; } .ProseMirror ul, .ProseMirror ol { padding-left: 30px; } .ProseMirror blockquote { padding-left: 1em; border-left: 4px solid #ddd; color: #767676; margin-left: 0; margin-right: 0; } .ProseMirror-example-setup-style img { cursor: default; max-width: 100%; } .ProseMirror-prompt { background: white; padding: 1em; border: 1px solid silver; position: fixed; border-radius: 0.25em; z-index: 11; box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); } .ProseMirror-prompt h5 { margin: 0 0 0.75em; font-family: @sansFont; font-size: 100%; color: #444; } .ProseMirror-prompt input[type="text"], .ProseMirror-prompt textarea { background: #eee; border: none; outline: none; } .ProseMirror-prompt input[type="text"] { margin: 0.25em 0; } .ProseMirror-prompt-close { position: absolute; left: 2px; top: 1px; color: #666; border: none; background: transparent; padding: 0; } .ProseMirror-prompt-close:after { content: "✕"; font-size: 12px; } .ProseMirror-invalid { background: #ffc; border: 1px solid #cc7; border-radius: 4px; padding: 5px 10px; position: absolute; min-width: 10em; } .ProseMirror-prompt-buttons { margin-top: 5px; display: none; } #editor, .editor { position: fixed; top: 0; right: 0; bottom: 0; left: 0; color: black; background-clip: padding-box; padding: 5px 0; margin: 4em auto 23px auto; } .dark #editor { color: white; } .ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child { margin-top: 10px; } .ProseMirror p { margin-bottom: 1em; } textarea { width: 100%; height: 123px; border: 1px solid silver; box-sizing: border-box; -moz-box-sizing: border-box; padding: 3px 10px; border: none; outline: none; font-family: inherit; font-size: inherit; } .ProseMirror-menubar-wrapper { height: 100%; box-sizing: border-box; } .ProseMirror-menubar-wrapper, #markdown textarea { display: block; margin-bottom: 4px; } .editorreadmore { color: @textLinkColor; text-decoration: underline; text-align: center; width: 100%; } @media all and (min-width: 50em) { #photo-upload label { display: inline; } .ProseMirror-menubar, #title, #photo-upload { margin-left: 10%; margin-right: 10%; } .ProseMirror { padding-left: 10%; padding-right: 10%; } } @media all and (min-width: 60em) { .ProseMirror-menubar, #title, #photo-upload { margin-left: 15%; margin-right: 15%; } .ProseMirror { padding-left: 15%; padding-right: 15%; } } @media all and (min-width: 70em) { .ProseMirror-menubar, #title, #photo-upload { margin-left: 20%; margin-right: 20%; } .ProseMirror { padding-left: 20%; padding-right: 20%; } } @media all and (min-width: 85em) { .ProseMirror-menubar, #title, #photo-upload { margin-left: 25%; margin-right: 25%; } .ProseMirror { padding-left: 25%; padding-right: 25%; } } @media all and (min-width: 105em) { .ProseMirror-menubar, #title, #photo-upload { margin-left: 30%; margin-right: 30%; } .ProseMirror { padding-left: 30%; padding-right: 30%; } } diff --git a/prose/prose.js b/prose/prose.js index cb39c6d..964688f 100644 --- a/prose/prose.js +++ b/prose/prose.js @@ -1,120 +1,121 @@ // class MarkdownView { // constructor(target, content) { // this.textarea = target.appendChild(document.createElement("textarea")) // this.textarea.value = content // } // get content() { return this.textarea.value } // focus() { this.textarea.focus() } // destroy() { this.textarea.remove() } // } import { EditorView } from "prosemirror-view"; import { EditorState, TextSelection } from "prosemirror-state"; import { exampleSetup } from "prosemirror-example-setup"; import { keymap } from "prosemirror-keymap"; import { writeFreelyMarkdownParser } from "./markdownParser"; import { writeFreelyMarkdownSerializer } from "./markdownSerializer"; import { writeFreelySchema } from "./schema"; import { getMenu } from "./menu"; 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) { let typingTimer; 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; } const doc = writeFreelyMarkdownParser.parse(content) 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) { let newState = this.state.apply(transaction); const newContent = writeFreelyMarkdownSerializer .serialize(newState.doc) // Replace all \\\ns ( not followed by a \n ) with \n .replace(/(\\\n)(\n{0,1})/g, (match, p1, p2) => p2 !== "\n" ? "\n" + p2 : match ); $content.value = newContent; let draft = ""; if ($title.value != null && $title.value !== "") { draft = "# " + $title.value + "\n\n"; } draft += newContent; clearTimeout(typingTimer); typingTimer = setTimeout(doneTyping, doneTypingInterval); this.updateState(newState); }, handleDOMEvents: { drop: (view, event) => { // If a file is dropped externally into the editor, do not insert anything. This will not trigger if an image has been inserted after upload and is dragged and dropped internally to change its position. if (event.dataTransfer.files.length > 0) { event.preventDefault(); } } }, }); // 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 writeFreelyMarkdownSerializer.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); +window.editorView = view; diff --git a/templates/classic.tmpl b/templates/classic.tmpl index 58f82c7..0c411b4 100644 --- a/templates/classic.tmpl +++ b/templates/classic.tmpl @@ -1,402 +1,408 @@ {{define "pad"}} {{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}} {{if .CustomCSS}}{{end}}
{{if not .SingleUser}}

{{end}}
{{if .Editing}}{{end}}
{{end}}