mirror of
https://github.com/writefreely/writefreely
synced 2024-11-10 11:24:13 +00:00
WIP: implement WYSIWYG editor w/ prosemirror
This commit is contained in:
parent
cb1553d67e
commit
ee712bbfaa
6 changed files with 760 additions and 1297 deletions
1149
prose/dist/prose.bundle.js
vendored
1149
prose/dist/prose.bundle.js
vendored
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() }
|
||||
// }
|
||||
|
||||
import {Schema} from "prosemirror-model"
|
||||
import {EditorView} from "prosemirror-view"
|
||||
import {EditorState, Plugin} from "prosemirror-state"
|
||||
import {defaultMarkdownParser,
|
||||
import {schema, defaultMarkdownParser,
|
||||
defaultMarkdownSerializer} from "prosemirror-markdown"
|
||||
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 {
|
||||
constructor(target, content) {
|
||||
this.view = new EditorView(target, {
|
||||
|
|
365
static/css/prose.css
Normal file
365
static/css/prose.css
Normal file
|
@ -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 = {
|
||||
getQEl: function(elementQuery) {
|
||||
return new Element(document.querySelector(elementQuery));
|
||||
},
|
||||
getEl: function(elementId) {
|
||||
return new Element(document.getElementById(elementId));
|
||||
},
|
||||
|
@ -124,6 +127,17 @@ var H = {
|
|||
}
|
||||
$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) {
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
|
|
2
static/js/prose.bundle.js
Normal file
2
static/js/prose.bundle.js
Normal file
File diff suppressed because one or more lines are too long
376
templates/wysiwyg.tmpl
Normal file
376
templates/wysiwyg.tmpl
Normal file
|
@ -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 a new issue