WIP: implement WYSIWYG editor w/ prosemirror
This commit is contained in:
parent
cb1553d67e
commit
ee712bbfaa
File diff suppressed because one or more lines are too long
151
prose/prose.js
151
prose/prose.js
|
@ -9,161 +9,12 @@
|
||||||
// destroy() { this.textarea.remove() }
|
// destroy() { this.textarea.remove() }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
import {Schema} from "prosemirror-model"
|
|
||||||
import {EditorView} from "prosemirror-view"
|
import {EditorView} from "prosemirror-view"
|
||||||
import {EditorState, Plugin} from "prosemirror-state"
|
import {EditorState, Plugin} from "prosemirror-state"
|
||||||
import {defaultMarkdownParser,
|
import {schema, defaultMarkdownParser,
|
||||||
defaultMarkdownSerializer} from "prosemirror-markdown"
|
defaultMarkdownSerializer} from "prosemirror-markdown"
|
||||||
import {exampleSetup} from "prosemirror-example-setup"
|
import {exampleSetup} from "prosemirror-example-setup"
|
||||||
|
|
||||||
// TODO: maybe don't need to use our own schema but waiting to figure out
|
|
||||||
// line break issues
|
|
||||||
const schema = new Schema({
|
|
||||||
nodes: {
|
|
||||||
doc: {
|
|
||||||
content: "block+"
|
|
||||||
},
|
|
||||||
|
|
||||||
paragraph: {
|
|
||||||
content: "inline*",
|
|
||||||
group: "block",
|
|
||||||
parseDOM: [{tag: "p"}],
|
|
||||||
toDOM() { return ["p", 0] }
|
|
||||||
},
|
|
||||||
|
|
||||||
blockquote: {
|
|
||||||
content: "block+",
|
|
||||||
group: "block",
|
|
||||||
parseDOM: [{tag: "blockquote"}],
|
|
||||||
toDOM() { return ["blockquote", 0] }
|
|
||||||
},
|
|
||||||
|
|
||||||
horizontal_rule: {
|
|
||||||
group: "block",
|
|
||||||
parseDOM: [{tag: "hr"}],
|
|
||||||
toDOM() { return ["div", ["hr"]] }
|
|
||||||
},
|
|
||||||
|
|
||||||
heading: {
|
|
||||||
attrs: {level: {default: 1}},
|
|
||||||
content: "inline*",
|
|
||||||
group: "block",
|
|
||||||
defining: true,
|
|
||||||
parseDOM: [{tag: "h1", attrs: {level: 1}},
|
|
||||||
{tag: "h2", attrs: {level: 2}},
|
|
||||||
{tag: "h3", attrs: {level: 3}},
|
|
||||||
{tag: "h4", attrs: {level: 4}},
|
|
||||||
{tag: "h5", attrs: {level: 5}},
|
|
||||||
{tag: "h6", attrs: {level: 6}}],
|
|
||||||
toDOM(node) { return ["h" + node.attrs.level, 0] }
|
|
||||||
},
|
|
||||||
|
|
||||||
code_block: {
|
|
||||||
content: "text*",
|
|
||||||
group: "block",
|
|
||||||
code: true,
|
|
||||||
defining: true,
|
|
||||||
marks: "",
|
|
||||||
attrs: {params: {default: ""}},
|
|
||||||
parseDOM: [{tag: "pre", preserveWhitespace: "full", getAttrs: node => (
|
|
||||||
{params: node.getAttribute("data-params") || ""}
|
|
||||||
)}],
|
|
||||||
toDOM(node) { return ["pre", node.attrs.params ? {"data-params": node.attrs.params} : {}, ["code", 0]] }
|
|
||||||
},
|
|
||||||
|
|
||||||
ordered_list: {
|
|
||||||
content: "list_item+",
|
|
||||||
group: "block",
|
|
||||||
attrs: {order: {default: 1}, tight: {default: false}},
|
|
||||||
parseDOM: [{tag: "ol", getAttrs(dom) {
|
|
||||||
return {order: dom.hasAttribute("start") ? +dom.getAttribute("start") : 1,
|
|
||||||
tight: dom.hasAttribute("data-tight")}
|
|
||||||
}}],
|
|
||||||
toDOM(node) {
|
|
||||||
return ["ol", {start: node.attrs.order == 1 ? null : node.attrs.order,
|
|
||||||
"data-tight": node.attrs.tight ? "true" : null}, 0]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
bullet_list: {
|
|
||||||
content: "list_item+",
|
|
||||||
group: "block",
|
|
||||||
attrs: {tight: {default: false}},
|
|
||||||
parseDOM: [{tag: "ul", getAttrs: dom => ({tight: dom.hasAttribute("data-tight")})}],
|
|
||||||
toDOM(node) { return ["ul", {"data-tight": node.attrs.tight ? "true" : null}, 0] }
|
|
||||||
},
|
|
||||||
|
|
||||||
list_item: {
|
|
||||||
content: "paragraph block*",
|
|
||||||
defining: true,
|
|
||||||
parseDOM: [{tag: "li"}],
|
|
||||||
toDOM() { return ["li", 0] }
|
|
||||||
},
|
|
||||||
|
|
||||||
text: {
|
|
||||||
group: "inline"
|
|
||||||
},
|
|
||||||
|
|
||||||
image: {
|
|
||||||
inline: true,
|
|
||||||
attrs: {
|
|
||||||
src: {},
|
|
||||||
alt: {default: null},
|
|
||||||
title: {default: null}
|
|
||||||
},
|
|
||||||
group: "inline",
|
|
||||||
draggable: true,
|
|
||||||
parseDOM: [{tag: "img[src]", getAttrs(dom) {
|
|
||||||
return {
|
|
||||||
src: dom.getAttribute("src"),
|
|
||||||
title: dom.getAttribute("title"),
|
|
||||||
alt: dom.getAttribute("alt")
|
|
||||||
}
|
|
||||||
}}],
|
|
||||||
toDOM(node) { return ["img", node.attrs] }
|
|
||||||
},
|
|
||||||
|
|
||||||
hard_break: {
|
|
||||||
inline: true,
|
|
||||||
group: "inline",
|
|
||||||
selectable: false,
|
|
||||||
parseDOM: [{tag: "br"}],
|
|
||||||
toDOM() { return ["br"] }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
marks: {
|
|
||||||
em: {
|
|
||||||
parseDOM: [{tag: "i"}, {tag: "em"},
|
|
||||||
{style: "font-style", getAttrs: value => value == "italic" && null}],
|
|
||||||
toDOM() { return ["em"] }
|
|
||||||
},
|
|
||||||
|
|
||||||
strong: {
|
|
||||||
parseDOM: [{tag: "b"}, {tag: "strong"},
|
|
||||||
{style: "font-weight", getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null}],
|
|
||||||
toDOM() { return ["strong"] }
|
|
||||||
},
|
|
||||||
|
|
||||||
link: {
|
|
||||||
attrs: {
|
|
||||||
href: {},
|
|
||||||
title: {default: null}
|
|
||||||
},
|
|
||||||
inclusive: false,
|
|
||||||
parseDOM: [{tag: "a[href]", getAttrs(dom) {
|
|
||||||
return {href: dom.getAttribute("href"), title: dom.getAttribute("title")}
|
|
||||||
}}],
|
|
||||||
toDOM(node) { return ["a", node.attrs] }
|
|
||||||
},
|
|
||||||
|
|
||||||
code: {
|
|
||||||
parseDOM: [{tag: "code"}],
|
|
||||||
toDOM() { return ["code"] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
class ProseMirrorView {
|
class ProseMirrorView {
|
||||||
constructor(target, content) {
|
constructor(target, content) {
|
||||||
this.view = new EditorView(target, {
|
this.view = new EditorView(target, {
|
||||||
|
|
|
@ -0,0 +1,365 @@
|
||||||
|
.ProseMirror {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
-webkit-font-variant-ligatures: none;
|
||||||
|
font-variant-ligatures: 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
border-top-left-radius: inherit;
|
||||||
|
border-top-right-radius: inherit;
|
||||||
|
position: relative;
|
||||||
|
min-height: 1em;
|
||||||
|
color: #666;
|
||||||
|
padding: 1px 6px;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
border-bottom: 1px solid silver;
|
||||||
|
background: white;
|
||||||
|
z-index: 10;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 2px 10px;
|
||||||
|
border: none;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-example-setup-style hr:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 1px;
|
||||||
|
background-color: silver;
|
||||||
|
line-height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror ul, .ProseMirror ol {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror blockquote {
|
||||||
|
padding-left: 1em;
|
||||||
|
border-left: 3px solid #eee;
|
||||||
|
margin-left: 0; margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-example-setup-style img {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt {
|
||||||
|
background: white;
|
||||||
|
padding: 5px 10px 5px 15px;
|
||||||
|
border: 1px solid silver;
|
||||||
|
position: fixed;
|
||||||
|
border-radius: 3px;
|
||||||
|
z-index: 11;
|
||||||
|
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 100%;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt input[type="text"],
|
||||||
|
.ProseMirror-prompt textarea {
|
||||||
|
background: #eee;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-prompt input[type="text"] {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 5px 0;
|
||||||
|
margin: 4em auto 23px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
padding: 4px 8px 4px 14px;
|
||||||
|
line-height: 1.2;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror p { margin-bottom: 1em }
|
||||||
|
|
||||||
|
.ProseMirror { height: 120px; overflow-y: auto; box-sizing: border-box; -moz-box-sizing: border-box }
|
||||||
|
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, #markdown textarea { display: block; margin-bottom: 4px }
|
||||||
|
|
||||||
|
@media all and (min-width: 50em) {
|
||||||
|
#editor {
|
||||||
|
margin-left: 10%;
|
||||||
|
margin-right: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (min-width: 60em) {
|
||||||
|
#editor {
|
||||||
|
margin-left: 15%;
|
||||||
|
margin-right: 15%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (min-width: 70em) {
|
||||||
|
#editor {
|
||||||
|
margin-left: 20%;
|
||||||
|
margin-right: 20%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (min-width: 85em) {
|
||||||
|
#editor {
|
||||||
|
margin-left: 25%;
|
||||||
|
margin-right: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (min-width: 105em) {
|
||||||
|
#editor {
|
||||||
|
margin-left: 30%;
|
||||||
|
margin-right: 30%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,6 +110,9 @@ Element.prototype.show = function() {
|
||||||
|
|
||||||
|
|
||||||
var H = {
|
var H = {
|
||||||
|
getQEl: function(elementQuery) {
|
||||||
|
return new Element(document.querySelector(elementQuery));
|
||||||
|
},
|
||||||
getEl: function(elementId) {
|
getEl: function(elementId) {
|
||||||
return new Element(document.getElementById(elementId));
|
return new Element(document.getElementById(elementId));
|
||||||
},
|
},
|
||||||
|
@ -124,6 +127,17 @@ var H = {
|
||||||
}
|
}
|
||||||
$el.el.value = val;
|
$el.el.value = val;
|
||||||
},
|
},
|
||||||
|
saveText: function($el, key) {
|
||||||
|
localStorage.setItem(key, $el.el.innerText);
|
||||||
|
},
|
||||||
|
loadText: function($el, key, onlyLoadPopulated) {
|
||||||
|
var val = localStorage.getItem(key);
|
||||||
|
if (onlyLoadPopulated && val == null) {
|
||||||
|
// Do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$el.el.innerText = val;
|
||||||
|
},
|
||||||
set: function(key, value) {
|
set: function(key, value) {
|
||||||
localStorage.setItem(key, value);
|
localStorage.setItem(key, value);
|
||||||
},
|
},
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,376 @@
|
||||||
|
{{define "pad"}}<!DOCTYPE HTML>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<title>{{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}}</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/write.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/prose.css" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<meta name="google" value="notranslate">
|
||||||
|
</head>
|
||||||
|
<body id="pad" class="light">
|
||||||
|
|
||||||
|
<div id="overlay"></div>
|
||||||
|
|
||||||
|
<!-- <div style="text-align: center"> -->
|
||||||
|
<!-- <label style="border-right: 1px solid silver"> -->
|
||||||
|
<!-- Markdown <input type=radio name=inputformat value=markdown checked> </label> -->
|
||||||
|
<!-- <label> <input type=radio name=inputformat value=prosemirror> WYSIWYM</label> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<div id="editor" style="margin-bottom: 0"></div>
|
||||||
|
|
||||||
|
<div style="display: none"><textarea id="content">{{if .Post.Content }}{{.Post.Content}}{{end}}</textarea></div>
|
||||||
|
|
||||||
|
<header id="tools">
|
||||||
|
<div id="clip">
|
||||||
|
{{if not .SingleUser}}<h1><a href="/me/c/" title="View blogs"><img class="ic-24dp" src="/img/ic_blogs_dark@2x.png" /></a></h1>{{end}}
|
||||||
|
<nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul>
|
||||||
|
{{if .Editing}}<li>{{if .EditCollection}}<a href="{{.EditCollection.CanonicalURL}}">{{.EditCollection.Title}}</a>{{else}}<a>Draft</a>{{end}}</li>
|
||||||
|
{{else}}<li><a id="publish-to"><span id="target-name">Draft</span> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
||||||
|
<ul>
|
||||||
|
<li class="menu-heading">Publish to...</li>
|
||||||
|
{{if .Blogs}}{{range $idx, $el := .Blogs}}
|
||||||
|
<li class="target{{if eq $idx 0}} selected{{end}}" id="blog-{{$el.Alias}}"><a href="#{{$el.Alias}}"><i class="material-icons md-18">public</i> {{if $el.Title}}{{$el.Title}}{{else}}{{$el.Alias}}{{end}}</a></li>
|
||||||
|
{{end}}{{end}}
|
||||||
|
<li class="target" id="blog-anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
|
||||||
|
<li id="user-separator" class="separator"><hr /></li>
|
||||||
|
{{ if .SingleUser }}
|
||||||
|
<li><a href="/"><i class="material-icons md-18">launch</i> View Blog</a></li>
|
||||||
|
<li><a href="/me/c/{{.Username}}"><i class="material-icons md-18">palette</i> Customize</a></li>
|
||||||
|
<li><a href="/me/c/{{.Username}}/stats"><i class="material-icons md-18">trending_up</i> Stats</a></li>
|
||||||
|
{{ else }}
|
||||||
|
<li><a href="/me/c/"><i class="material-icons md-18">library_books</i> View Blogs</a></li>
|
||||||
|
{{ end }}
|
||||||
|
<li><a href="/me/posts/"><i class="material-icons md-18">view_list</i> View Drafts</a></li>
|
||||||
|
<li><a href="/me/logout"><i class="material-icons md-18">power_settings_new</i> Log out</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>{{end}}
|
||||||
|
</ul></nav>
|
||||||
|
<nav id="font-picker" class="if-room room-3 hidden" style="margin-left:-1em"><ul>
|
||||||
|
<li><a href="#" id="" onclick="return false"><img class="ic-24dp" src="/img/ic_font_dark@2x.png" /> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /></a>
|
||||||
|
<ul style="text-align: center">
|
||||||
|
<li class="menu-heading">Font</li>
|
||||||
|
<li class="selected"><a class="font norm" href="#norm">Serif</a></li>
|
||||||
|
<li><a class="font sans" href="#sans">Sans-serif</a></li>
|
||||||
|
<li><a class="font wrap" href="#wrap">Monospace</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul></nav>
|
||||||
|
<span id="wc" class="hidden if-room room-4">0 words</span>
|
||||||
|
</div>
|
||||||
|
<noscript style="margin-left: 2em;"><strong>NOTE</strong>: for now, you'll need Javascript enabled to post.</noscript>
|
||||||
|
<div id="belt">
|
||||||
|
{{if .Editing}}<div class="tool hidden if-room"><a href="{{if .EditCollection}}{{.EditCollection.CanonicalURL}}{{.Post.Slug}}/edit/meta{{else}}/{{if .SingleUser}}d/{{end}}{{.Post.Id}}/meta{{end}}" title="Edit post metadata" id="edit-meta"><img class="ic-24dp" src="/img/ic_info_dark@2x.png" /></a></div>{{end}}
|
||||||
|
<div class="tool hidden if-room room-2"><a href="#theme" title="Toggle theme" id="toggle-theme"><img class="ic-24dp" src="/img/ic_brightness_dark@2x.png" /></a></div>
|
||||||
|
<div class="tool if-room room-1"><a href="{{if not .User}}/pad/posts{{else}}/me/posts/{{end}}" title="View posts" id="view-posts"><img class="ic-24dp" src="/img/ic_list_dark@2x.png" /></a></div>
|
||||||
|
<div class="tool"><a href="#publish" title="Publish" id="publish"><img class="ic-24dp" src="/img/ic_send_dark@2x.png" /></a></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<script src="/js/prose.bundle.js"></script>
|
||||||
|
<script src="/js/h.js"></script>
|
||||||
|
<script>
|
||||||
|
function toggleTheme() {
|
||||||
|
var btns = Array.prototype.slice.call(document.getElementById('tools').querySelectorAll('a img'));
|
||||||
|
var newTheme = '';
|
||||||
|
if (document.body.classList.contains('light')) {
|
||||||
|
newTheme = 'dark';
|
||||||
|
document.body.className = document.body.className.replace(/(?:^|\s)light(?!\S)/g, newTheme);
|
||||||
|
for (var i=0; i<btns.length; i++) {
|
||||||
|
btns[i].src = btns[i].src.replace('_dark@2x.png', '@2x.png');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextnewTheme = 'light';
|
||||||
|
document.body.className = document.body.className.replace(/(?:^|\s)dark(?!\S)/g, newTheme);
|
||||||
|
for (var i=0; i<btns.length; i++) {
|
||||||
|
btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
H.set('padTheme', newTheme);
|
||||||
|
}
|
||||||
|
if (H.get('padTheme', 'light') != 'light') {
|
||||||
|
toggleTheme();
|
||||||
|
}
|
||||||
|
var $writer = H.getQEl('div.ProseMirror');
|
||||||
|
var $content = H.getEl('content');
|
||||||
|
var $btnPublish = H.getEl('publish');
|
||||||
|
var $wc = H.getEl("wc");
|
||||||
|
var updateWordCount = function() {
|
||||||
|
var words = 0;
|
||||||
|
var val = $content.el.innerText.trim();
|
||||||
|
if (val != '') {
|
||||||
|
words = $content.el.innerText.trim().replace(/\s+/gi, ' ').split(' ').length;
|
||||||
|
}
|
||||||
|
$wc.el.innerText = words + " word" + (words != 1 ? "s" : "");
|
||||||
|
};
|
||||||
|
var setButtonStates = function() {
|
||||||
|
if (!canPublish) {
|
||||||
|
$btnPublish.el.className = 'disabled';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($content.el.innerText.length === 0 || (draftDoc != 'lastDoc' && $content.el.innerText == origDoc)) {
|
||||||
|
$btnPublish.el.className = 'disabled';
|
||||||
|
} else {
|
||||||
|
$btnPublish.el.className = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
{{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}';
|
||||||
|
var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}}
|
||||||
|
H.loadText($content, draftDoc, true);
|
||||||
|
updateWordCount();
|
||||||
|
|
||||||
|
var typingTimer;
|
||||||
|
var doneTypingInterval = 200;
|
||||||
|
|
||||||
|
var posts;
|
||||||
|
{{if and .Post.Id (not .Post.Slug)}}
|
||||||
|
var token = null;
|
||||||
|
var curPostIdx;
|
||||||
|
posts = JSON.parse(H.get('posts', '[]'));
|
||||||
|
for (var i=0; i<posts.length; i++) {
|
||||||
|
if (posts[i].id == "{{.Post.Id}}") {
|
||||||
|
token = posts[i].token;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var canPublish = token != null;
|
||||||
|
{{else}}var canPublish = true;{{end}}
|
||||||
|
var publishing = false;
|
||||||
|
var justPublished = false;
|
||||||
|
var silenced = {{.Silenced}};
|
||||||
|
var publish = function(content, font) {
|
||||||
|
if (silenced === true) {
|
||||||
|
alert("Your account is silenced, so you can't publish or update posts.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
|
||||||
|
if (!token) {
|
||||||
|
alert("You don't have permission to update this post.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($btnPublish.el.className == 'disabled') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
$btnPublish.el.children[0].textContent = 'more_horiz';
|
||||||
|
publishing = true;
|
||||||
|
var xpostTarg = H.get('crosspostTarget', '[]');
|
||||||
|
|
||||||
|
var http = new XMLHttpRequest();
|
||||||
|
var lang = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
|
||||||
|
lang = lang.substring(0, 2);
|
||||||
|
var post = H.getTitleStrict(content);
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
body: post.content,
|
||||||
|
title: post.title,
|
||||||
|
font: font,
|
||||||
|
lang: lang
|
||||||
|
};
|
||||||
|
{{ if .Post.Slug }}
|
||||||
|
var url = "/api/collections/{{.EditCollection.Alias}}/posts/{{.Post.Id}}";
|
||||||
|
{{ else if .Post.Id }}
|
||||||
|
var url = "/api/posts/{{.Post.Id}}";
|
||||||
|
if (typeof token === 'undefined' || !token) {
|
||||||
|
token = "";
|
||||||
|
}
|
||||||
|
params.token = token;
|
||||||
|
{{ else }}
|
||||||
|
var url = "/api/posts";
|
||||||
|
var postTarget = H.get('postTarget', 'anonymous');
|
||||||
|
if (postTarget != 'anonymous') {
|
||||||
|
url = "/api/collections/" + postTarget + "/posts";
|
||||||
|
}
|
||||||
|
params.crosspost = JSON.parse(xpostTarg);
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
http.open("POST", url, true);
|
||||||
|
|
||||||
|
// Send the proper header information along with the request
|
||||||
|
http.setRequestHeader("Content-type", "application/json");
|
||||||
|
|
||||||
|
http.onreadystatechange = function() {
|
||||||
|
if (http.readyState == 4) {
|
||||||
|
publishing = false;
|
||||||
|
if (http.status == 200 || http.status == 201) {
|
||||||
|
data = JSON.parse(http.responseText);
|
||||||
|
id = data.data.id;
|
||||||
|
nextURL = '{{if .SingleUser}}/d{{end}}/'+id;
|
||||||
|
|
||||||
|
{{ if not .Post.Id }}
|
||||||
|
// Post created
|
||||||
|
if (postTarget != 'anonymous') {
|
||||||
|
nextURL = {{if not .SingleUser}}'/'+postTarget+{{end}}'/'+data.data.slug;
|
||||||
|
}
|
||||||
|
editToken = data.data.token;
|
||||||
|
|
||||||
|
{{ if not .User }}if (postTarget == 'anonymous') {
|
||||||
|
// Save the data
|
||||||
|
var posts = JSON.parse(H.get('posts', '[]'));
|
||||||
|
|
||||||
|
{{if .Post.Id}}var newPost = H.createPost("{{.Post.Id}}", token, content);
|
||||||
|
for (var i=0; i<posts.length; i++) {
|
||||||
|
if (posts[i].id == "{{.Post.Id}}") {
|
||||||
|
posts[i].title = newPost.title;
|
||||||
|
posts[i].summary = newPost.summary;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextURL = "/pad/posts";{{else}}posts.push(H.createPost(id, editToken, content));{{end}}
|
||||||
|
|
||||||
|
H.set('posts', JSON.stringify(posts));
|
||||||
|
}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
justPublished = true;
|
||||||
|
if (draftDoc != 'lastDoc') {
|
||||||
|
H.remove(draftDoc);
|
||||||
|
{{if .Editing}}H.remove('draft{{.Post.Id}}font');{{end}}
|
||||||
|
} else {
|
||||||
|
H.set(draftDoc, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
{{if .EditCollection}}
|
||||||
|
window.location = '{{.EditCollection.CanonicalURL}}{{.Post.Slug}}';
|
||||||
|
{{else}}
|
||||||
|
window.location = nextURL;
|
||||||
|
{{end}}
|
||||||
|
} else {
|
||||||
|
$btnPublish.el.children[0].textContent = 'send';
|
||||||
|
alert("Failed to post. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.send(JSON.stringify(params));
|
||||||
|
};
|
||||||
|
|
||||||
|
setButtonStates();
|
||||||
|
$writer.on('keyup input', function() {
|
||||||
|
setButtonStates();
|
||||||
|
clearTimeout(typingTimer);
|
||||||
|
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
||||||
|
}, false);
|
||||||
|
$writer.on('keydown', function(e) {
|
||||||
|
clearTimeout(typingTimer);
|
||||||
|
if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) {
|
||||||
|
$btnPublish.el.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$btnPublish.on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!publishing && $content.el.innerText) {
|
||||||
|
var content = $content.el.innerText;
|
||||||
|
publish(content, selectedFont);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
H.getEl('toggle-theme').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var newTheme = 'light';
|
||||||
|
if (document.body.className == 'light') {
|
||||||
|
newTheme = 'dark';
|
||||||
|
}
|
||||||
|
toggleTheme();
|
||||||
|
});
|
||||||
|
|
||||||
|
var targets = document.querySelectorAll('#target li.target a');
|
||||||
|
for (var i=0; i<targets.length; i++) {
|
||||||
|
targets[i].addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var targetName = this.href.substring(this.href.indexOf('#')+1);
|
||||||
|
H.set('postTarget', targetName);
|
||||||
|
|
||||||
|
document.querySelector('#target li.target.selected').classList.remove('selected');
|
||||||
|
this.parentElement.classList.add('selected');
|
||||||
|
var newText = this.innerText.split(' ');
|
||||||
|
newText.shift();
|
||||||
|
document.getElementById('target-name').innerText = newText.join(' ');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var postTarget = H.get('postTarget', '{{if .Blogs}}{{$blog := index .Blogs 0}}{{$blog.Alias}}{{else}}anonymous{{end}}');
|
||||||
|
if (location.hash != '') {
|
||||||
|
postTarget = location.hash.substring(1);
|
||||||
|
// TODO: pushState to /pad (or whatever the URL is) so we live on a clean URL
|
||||||
|
location.hash = '';
|
||||||
|
}
|
||||||
|
var pte = document.querySelector('#target li.target#blog-'+postTarget+' a');
|
||||||
|
if (pte != null) {
|
||||||
|
pte.click();
|
||||||
|
} else {
|
||||||
|
postTarget = 'anonymous';
|
||||||
|
H.set('postTarget', postTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sansLoaded = false;
|
||||||
|
WebFontConfig = {
|
||||||
|
custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] }
|
||||||
|
};
|
||||||
|
var loadSans = function() {
|
||||||
|
if (sansLoaded) return;
|
||||||
|
sansLoaded = true;
|
||||||
|
WebFontConfig.custom.families.push('Open+Sans:400,700:latin');
|
||||||
|
try {
|
||||||
|
(function() {
|
||||||
|
var wf=document.createElement('script');
|
||||||
|
wf.src = '/js/webfont.js';
|
||||||
|
wf.type='text/javascript';
|
||||||
|
wf.async='true';
|
||||||
|
var s=document.getElementsByTagName('script')[0];
|
||||||
|
s.parentNode.insertBefore(wf, s);
|
||||||
|
})();
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
var fonts = document.querySelectorAll('nav#font-picker a.font');
|
||||||
|
for (var i=0; i<fonts.length; i++) {
|
||||||
|
fonts[i].addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectedFont = this.href.substring(this.href.indexOf('#')+1);
|
||||||
|
// TODO: don't change classes on the editor window
|
||||||
|
//$writer.el.className = selectedFont;
|
||||||
|
document.querySelector('nav#font-picker li.selected').classList.remove('selected');
|
||||||
|
this.parentElement.classList.add('selected');
|
||||||
|
H.set('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', selectedFont);
|
||||||
|
if (selectedFont == 'sans') {
|
||||||
|
loadSans();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var selectedFont = H.get('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', '{{.Post.Font}}');
|
||||||
|
var sfe = document.querySelector('nav#font-picker a.font.'+selectedFont);
|
||||||
|
if (sfe != null) {
|
||||||
|
sfe.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
var doneTyping = function() {
|
||||||
|
if (draftDoc == 'lastDoc' || $content.el.innerText != origDoc) {
|
||||||
|
H.saveText($content, draftDoc);
|
||||||
|
updateWordCount();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('beforeunload', function(e) {
|
||||||
|
if (draftDoc != 'lastDoc' && $content.el.innerText == origDoc) {
|
||||||
|
H.remove(draftDoc);
|
||||||
|
} else if (!justPublished) {
|
||||||
|
doneTyping();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
(function() {
|
||||||
|
var wf=document.createElement('script');
|
||||||
|
wf.src = '/js/webfont.js';
|
||||||
|
wf.type='text/javascript';
|
||||||
|
wf.async='true';
|
||||||
|
var s=document.getElementsByTagName('script')[0];
|
||||||
|
s.parentNode.insertBefore(wf, s);
|
||||||
|
})();
|
||||||
|
} catch (e) {
|
||||||
|
// whatevs
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<link href="/css/icons.css" rel="stylesheet">
|
||||||
|
</body>
|
||||||
|
</html>{{end}}
|
Loading…
Reference in New Issue