mirror of
https://github.com/writefreely/writefreely
synced 2024-11-10 11:24:13 +00:00
commit
fbb67bc9ef
27 changed files with 17646 additions and 31 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
node_modules
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
|
|
1
Makefile
1
Makefile
|
@ -131,6 +131,7 @@ release-docker :
|
|||
|
||||
ui : force_look
|
||||
cd less/; $(MAKE) $(MFLAGS)
|
||||
cd prose/; $(MAKE) $(MFLAGS)
|
||||
|
||||
assets : generate
|
||||
go-bindata -pkg writefreely -ignore=\\.gitignore -tags="!wflib" schema.sql sqlite.sql
|
||||
|
|
|
@ -5,6 +5,7 @@ all :
|
|||
lessc app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css
|
||||
lessc fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css
|
||||
lessc icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css
|
||||
lessc prose.less --clean-css="--s1 --advanced" $(CSSDIR)prose.css
|
||||
|
||||
install :
|
||||
./install-less.sh
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
@import "admin";
|
||||
@import "login";
|
||||
@import "pages/error";
|
||||
@import "resources";
|
||||
@import "lib/elements";
|
||||
@import "lib/material";
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
@primary: rgb(114, 120, 191);
|
||||
@secondary: rgb(114, 191, 133);
|
||||
@subheaders: #444;
|
||||
@headerTextColor: black;
|
||||
@sansFont: 'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
@serifFont: Lora, 'Palatino Linotype', 'Book Antiqua', 'New York', 'DejaVu serif', serif;
|
||||
@monoFont: Hack, consolas, Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace, monospace;
|
||||
@dangerCol: #e21d27;
|
||||
@errUrgentCol: #ecc63c;
|
||||
@proSelectedCol: #71D571;
|
||||
@textLinkColor: rgb(0, 0, 238);
|
||||
|
||||
@accent: #767676;
|
||||
|
||||
body {
|
||||
font-family: @serifFont;
|
||||
font-size-adjust: 0.5;
|
||||
|
@ -529,7 +515,7 @@ pre, body#post article, #post .alert, #subpage .alert, body#collection article,
|
|||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article p {
|
||||
textarea, input#title, pre, body#post article, body#collection article p {
|
||||
&.norm, &.sans, &.wrap {
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap; /* CSS 3 */
|
||||
|
@ -539,7 +525,7 @@ textarea, pre, body#post article, body#collection article p {
|
|||
word-wrap: break-word; /* Internet Explorer 5.5+ */
|
||||
}
|
||||
}
|
||||
textarea, pre, body#post article, body#collection article, body#subpage article, span, .font {
|
||||
textarea, input#title, pre, body#post article, body#collection article, body#subpage article, span, .font {
|
||||
&.norm {
|
||||
font-family: @serifFont;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Install Less via npm
|
||||
if [ ! -e "$(which lessc)" ]; then
|
||||
sudo npm install -g less
|
||||
sudo npm install -g less@3.5.3
|
||||
sudo npm install -g less-plugin-clean-css
|
||||
else
|
||||
echo LESS $(npm view less version 2>&1 | grep -v WARN) is installed
|
||||
|
|
|
@ -188,18 +188,18 @@ body#pad, body#pad-sub {
|
|||
body#pad {
|
||||
.pad-theme-transition;
|
||||
|
||||
textarea {
|
||||
textarea, #title {
|
||||
.pad-theme-transition;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
textarea {
|
||||
textarea, #title, #editor {
|
||||
background-color: @darkBG;
|
||||
color: @darkTextColor;
|
||||
}
|
||||
}
|
||||
&.light {
|
||||
textarea {
|
||||
textarea, #title, #editor {
|
||||
background-color: @lightBG;
|
||||
color: @lightTextColor;
|
||||
}
|
||||
|
|
|
@ -256,7 +256,7 @@ body#pad {
|
|||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
textarea {
|
||||
textarea, #title {
|
||||
position: fixed !important;
|
||||
top: 3em;
|
||||
right: 0;
|
||||
|
@ -385,6 +385,14 @@ body#pad .alert {
|
|||
top: 2.25em;
|
||||
padding-top: 0.25em;
|
||||
}
|
||||
&.classic {
|
||||
#editor {
|
||||
top: 5.25em;
|
||||
}
|
||||
#title {
|
||||
top: 3.5rem;
|
||||
}
|
||||
}
|
||||
#tools {
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
@ -438,8 +446,8 @@ body#pad .alert {
|
|||
}
|
||||
|
||||
@media all and (min-width: 50em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
@ -450,8 +458,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 60em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 15%;
|
||||
padding-right: 15%;
|
||||
}
|
||||
|
@ -462,8 +470,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 70em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 20%;
|
||||
padding-right: 20%;
|
||||
}
|
||||
|
@ -474,8 +482,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 85em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 25%;
|
||||
padding-right: 25%;
|
||||
}
|
||||
|
@ -486,8 +494,8 @@ body#pad .alert {
|
|||
}
|
||||
}
|
||||
@media all and (min-width: 105em) {
|
||||
body#pad {
|
||||
textarea {
|
||||
body#pad, body#pad.classic {
|
||||
textarea, #title {
|
||||
padding-left: 30%;
|
||||
padding-right: 30%;
|
||||
}
|
||||
|
|
450
less/prose-editor.less
Normal file
450
less/prose-editor.less
Normal file
|
@ -0,0 +1,450 @@
|
|||
body#pad.classic {
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
#editor {
|
||||
top: 4em;
|
||||
}
|
||||
#title {
|
||||
top: 4.25rem;
|
||||
bottom: unset;
|
||||
height: auto;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border: 0;
|
||||
}
|
||||
#tools {
|
||||
#belt {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
#target {
|
||||
ul {
|
||||
a {
|
||||
padding: 0 0.5em !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 0;
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
.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: 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;
|
||||
}
|
||||
|
||||
.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) {
|
||||
#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%;
|
||||
}
|
||||
}
|
4
less/prose.less
Normal file
4
less/prose.less
Normal file
|
@ -0,0 +1,4 @@
|
|||
@import "prose-editor";
|
||||
@import "pad-theme";
|
||||
@import "resources";
|
||||
@import "lib/elements";
|
13
less/resources.less
Normal file
13
less/resources.less
Normal file
|
@ -0,0 +1,13 @@
|
|||
@primary: rgb(114, 120, 191);
|
||||
@secondary: rgb(114, 191, 133);
|
||||
@subheaders: #444;
|
||||
@headerTextColor: black;
|
||||
@sansFont: 'Open Sans', 'Segoe UI', Tahoma, Arial, sans-serif;
|
||||
@serifFont: Lora, 'Palatino Linotype', 'Book Antiqua', 'New York', 'DejaVu serif', serif;
|
||||
@monoFont: Hack, consolas, Menlo-Regular, Menlo, Monaco, 'ubuntu mono', monospace, monospace;
|
||||
@dangerCol: #e21d27;
|
||||
@errUrgentCol: #ecc63c;
|
||||
@proSelectedCol: #71D571;
|
||||
@textLinkColor: rgb(0, 0, 238);
|
||||
|
||||
@accent: #767676;
|
8
prose/.babelrc.js
Normal file
8
prose/.babelrc.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
"presets": [
|
||||
["@babel/env", {
|
||||
"modules": false
|
||||
}]
|
||||
],
|
||||
"plugins": ["@babel/plugin-syntax-dynamic-import"]
|
||||
}
|
4
prose/.prettierrc
Normal file
4
prose/.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
3
prose/Makefile
Normal file
3
prose/Makefile
Normal file
|
@ -0,0 +1,3 @@
|
|||
all :
|
||||
npm install
|
||||
npm run-script build
|
7
prose/README.md
Normal file
7
prose/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Building
|
||||
|
||||
* Run `npm install` to download dependencies.
|
||||
* Run `npm run-script build` to build a production script in `../static/js/` or run
|
||||
`npm run develop` to build and watch for changes. You can use `prose.html`
|
||||
to test your development changes.
|
||||
* Manually copy the file `prose.bundle.js` to `static/js/`. _To be automated_
|
57
prose/markdownParser.js
Normal file
57
prose/markdownParser.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { MarkdownParser } from "prosemirror-markdown";
|
||||
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 {};
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
123
prose/markdownSerializer.js
Normal file
123
prose/markdownSerializer.js
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { MarkdownSerializer } from "prosemirror-markdown";
|
||||
|
||||
function backticksFor(node, side) {
|
||||
const ticks = /`+/g;
|
||||
let m;
|
||||
let len = 0;
|
||||
if (node.isText)
|
||||
while ((m = ticks.exec(node.text))) len = Math.max(len, m[0].length);
|
||||
let result = len > 0 && side > 0 ? " `" : "`";
|
||||
for (let i = 0; i < len; i++) result += "`";
|
||||
if (len > 0 && side < 0) result += " ";
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPlainURL(link, parent, index, side) {
|
||||
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;
|
||||
const content = parent.child(index + (side < 0 ? -1 : 0));
|
||||
if (
|
||||
!content.isText ||
|
||||
content.text != link.attrs.href ||
|
||||
content.marks[content.marks.length - 1] != link
|
||||
)
|
||||
return false;
|
||||
if (index == (side < 0 ? 1 : parent.childCount - 1)) return true;
|
||||
const next = parent.child(index + (side < 0 ? -2 : 1));
|
||||
return !link.isInSet(next.marks);
|
||||
}
|
||||
|
||||
export const writeAsMarkdownSerializer = new MarkdownSerializer(
|
||||
{
|
||||
readmore(state, node) {
|
||||
state.write("<!--more-->\n");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
// blockquote(state, node) {
|
||||
// state.wrapBlock("> ", undefined, node, () => state.renderContent(node));
|
||||
// },
|
||||
code_block(state, node) {
|
||||
state.write(`\`\`\`${node.attrs.params || ""}\n`);
|
||||
state.text(node.textContent, false);
|
||||
state.ensureNewLine();
|
||||
state.write("```");
|
||||
state.closeBlock(node);
|
||||
},
|
||||
heading(state, node) {
|
||||
state.write(`${state.repeat("#", node.attrs.level)} `);
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
bullet_list(state, node) {
|
||||
state.renderList(node, " ", () => `${node.attrs.bullet || "*"} `);
|
||||
},
|
||||
ordered_list(state, node) {
|
||||
const start = node.attrs.order || 1;
|
||||
const maxW = String(start + node.childCount - 1).length;
|
||||
const space = state.repeat(" ", maxW + 2);
|
||||
state.renderList(node, space, (i) => {
|
||||
const nStr = String(start + i);
|
||||
return `${state.repeat(" ", maxW - nStr.length) + nStr}. `;
|
||||
});
|
||||
},
|
||||
list_item(state, node) {
|
||||
state.renderContent(node);
|
||||
},
|
||||
paragraph(state, node) {
|
||||
state.renderInline(node);
|
||||
state.closeBlock(node);
|
||||
},
|
||||
|
||||
image(state, node) {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
},
|
||||
text(state, node) {
|
||||
state.text(node.text || "");
|
||||
},
|
||||
},
|
||||
{
|
||||
em: {
|
||||
open: "*",
|
||||
close: "*",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
strong: {
|
||||
open: "**",
|
||||
close: "**",
|
||||
mixable: true,
|
||||
expelEnclosingWhitespace: true,
|
||||
},
|
||||
link: {
|
||||
open(_state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, 1) ? "<" : "[";
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
return isPlainURL(mark, parent, index, -1)
|
||||
? ">"
|
||||
: `](${state.esc(mark.attrs.href)}${
|
||||
mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ""
|
||||
})`;
|
||||
},
|
||||
},
|
||||
code: {
|
||||
open(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index), -1);
|
||||
},
|
||||
close(_state, _mark, parent, index) {
|
||||
return backticksFor(parent.child(index - 1), 1);
|
||||
},
|
||||
escape: false,
|
||||
},
|
||||
}
|
||||
);
|
32
prose/menu.js
Normal file
32
prose/menu.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { MenuItem } from "prosemirror-menu";
|
||||
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;
|
||||
}
|
||||
|
||||
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())
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const getMenu = () => {
|
||||
const menuContent = [
|
||||
...buildMenuItems(writeFreelySchema).fullMenu,
|
||||
[ReadMoreItem],
|
||||
];
|
||||
return menuContent;
|
||||
};
|
16278
prose/package-lock.json
generated
Normal file
16278
prose/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
prose/package.json
Normal file
32
prose/package.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "prose",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "prose.js",
|
||||
"dependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"markdown-it": "^12.0.4",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-keymap": "^1.1.4",
|
||||
"prosemirror-markdown": "github:VV-EE/prosemirror-markdown",
|
||||
"prosemirror-model": "^1.9.1",
|
||||
"prosemirror-state": "^1.3.2",
|
||||
"prosemirror-view": "^1.14.2",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.7",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"prettier": "^2.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"develop": "webpack --mode development --watch",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
14
prose/prose.html
Normal file
14
prose/prose.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<link rel="stylesheet" href="../static/css/prose.css" />
|
||||
<div id="editor" style="margin-bottom: 0"></div>
|
||||
<!-- <div style="text-align: center"> -->
|
||||
<!-- <label style="border-right: 1px solid silver"> -->
|
||||
<!-- Markdown <input type=radio name=inputformat value=markdown> </label> -->
|
||||
<!-- <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>
|
||||
<script src="dist/prose.bundle.js"></script>
|
118
prose/prose.js
Normal file
118
prose/prose.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
// 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 { 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");
|
||||
|
||||
// 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 = writeAsMarkdownParser.parse(
|
||||
// Replace all "solo" \n's with \\\n for correct markdown parsing
|
||||
// Can't use lookahead or lookbehind because it's not supported on Safari
|
||||
content.replace(/([^]{0,1})(\n)([^]{0,1})/g, (match, p1, p2, p3) => {
|
||||
return p1 !== "\n" && p3 !== "\n" ? p1 + "\\\n" + p3 : match;
|
||||
})
|
||||
);
|
||||
|
||||
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 = writeAsMarkdownSerializer
|
||||
.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);
|
||||
},
|
||||
});
|
||||
// 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);
|
21
prose/schema.js
Normal file
21
prose/schema.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
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" },
|
||||
"Read more...",
|
||||
],
|
||||
parseDOM: [{ tag: "div.editorreadmore" }],
|
||||
}),
|
||||
marks: schema.spec.marks,
|
||||
});
|
25
prose/webpack.config.js
Normal file
25
prose/webpack.config.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const path = require('path')
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
entry: __dirname + '/prose.js'
|
||||
},
|
||||
output: {
|
||||
filename: 'prose.bundle.js',
|
||||
path: path.resolve('..', 'static', 'js'),
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(nodue_modules|bower_components)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -110,12 +110,24 @@ Element.prototype.show = function() {
|
|||
|
||||
|
||||
var H = {
|
||||
getQEl: function(elementQuery) {
|
||||
return new Element(document.querySelector(elementQuery));
|
||||
},
|
||||
getEl: function(elementId) {
|
||||
return new Element(document.getElementById(elementId));
|
||||
},
|
||||
save: function($el, key) {
|
||||
localStorage.setItem(key, $el.el.value);
|
||||
},
|
||||
saveClassic: function($titleEl, $el, key) {
|
||||
var out = "";
|
||||
var title = $titleEl.el.value;
|
||||
if (title !== "") {
|
||||
out = "# "+title+"\n\n";
|
||||
}
|
||||
out += $el.el.value;
|
||||
localStorage.setItem(key, out);
|
||||
},
|
||||
load: function($el, key, onlyLoadPopulated, postUpdated) {
|
||||
var val = localStorage.getItem(key);
|
||||
if (onlyLoadPopulated && val == null) {
|
||||
|
@ -138,6 +150,20 @@ var H = {
|
|||
}
|
||||
return true;
|
||||
},
|
||||
loadClassic: function($titleEl, $el, key, onlyLoadPopulated) {
|
||||
var val = localStorage.getItem(key);
|
||||
if (onlyLoadPopulated && val == null) {
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
if (val.indexOf("# ") === 0) {
|
||||
var eol = val.indexOf("\n");
|
||||
title = val.substring("# ".length, eol);
|
||||
val = val.substring(eol+"\n\n".length);
|
||||
$titleEl.el.value = title;
|
||||
}
|
||||
$el.el.value = 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
400
templates/wysiwyg.tmpl
Normal file
400
templates/wysiwyg.tmpl
Normal file
|
@ -0,0 +1,400 @@
|
|||
{{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 classic">
|
||||
|
||||
<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> -->
|
||||
<input type="text" id="title" name="title" placeholder="Title..." {{if .Post.Title}}value="{{.Post.Title}}"{{end}} autofocus />
|
||||
<div id="editor" style="margin-bottom: 0"></div>
|
||||
|
||||
<div style="display: none"><textarea id="content"{{if .Post.Content }} value={{.Post.Content}}>{{.Post.Content}}{{else}}>{{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/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 $title = H.getEl('title');
|
||||
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.value.trim();
|
||||
if (val != '') {
|
||||
words = $content.el.value.trim().replace(/\s+/gi, ' ').split(' ').length;
|
||||
}
|
||||
val = $title.el.value.trim();
|
||||
if (val != '') {
|
||||
words += $title.el.value.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.value.length === 0 || (draftDoc != 'lastDoc' && $content.el.value == 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}}
|
||||
|
||||
// ProseMirror editor
|
||||
window.draftKey = draftDoc;
|
||||
|
||||
// H.loadClassic($title, $writer, 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(title, 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: 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();
|
||||
$title.on('keydown', function(e) {
|
||||
if (e.keyCode == 13) {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
$btnPublish.el.click();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
$writer.el.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
/*
|
||||
$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.value) {
|
||||
var title = $title.el.value;
|
||||
var content = $content.el.value;
|
||||
publish(title, 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
|
||||
//$title.el.className = selectedFont;
|
||||
//$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.value != origDoc) {
|
||||
H.saveClassic($title, $content, draftDoc);
|
||||
updateWordCount();
|
||||
}
|
||||
};
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (draftDoc != 'lastDoc' && $content.el.value == 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>
|
||||
<script src="/js/prose.bundle.js"></script>
|
||||
<link href="/css/icons.css" rel="stylesheet">
|
||||
</body>
|
||||
</html>{{end}}
|
Loading…
Reference in a new issue