mirror of https://github.com/writeas/writefreely
commit
f2e3cd8bd7
@ -1,28 +0,0 @@ |
||||
[server] |
||||
hidden_host = |
||||
port = 8080 |
||||
|
||||
[database] |
||||
type = mysql |
||||
username = root |
||||
password = changeme |
||||
database = writefreely |
||||
host = db |
||||
port = 3306 |
||||
tls = false |
||||
|
||||
[app] |
||||
site_name = WriteFreely Example Blog! |
||||
host = http://localhost:8080 |
||||
theme = write |
||||
disable_js = false |
||||
webfonts = true |
||||
single_user = true |
||||
open_registration = false |
||||
min_username_len = 3 |
||||
max_blogs = 1 |
||||
federation = true |
||||
public_stats = true |
||||
private = false |
||||
update_checks = true |
||||
|
@ -1,32 +1,47 @@ |
||||
version: "3" |
||||
|
||||
volumes: |
||||
web-keys: |
||||
db-data: |
||||
|
||||
networks: |
||||
external_writefreely: |
||||
internal_writefreely: |
||||
internal: true |
||||
|
||||
services: |
||||
web: |
||||
build: . |
||||
writefreely-web: |
||||
container_name: "writefreely-web" |
||||
image: "writefreely:latest" |
||||
|
||||
volumes: |
||||
- "web-data:/go/src/app" |
||||
- "./config.ini.example:/go/src/app/config.ini" |
||||
- "web-keys:/go/keys" |
||||
- "./config.ini:/go/config.ini" |
||||
|
||||
networks: |
||||
- "internal_writefreely" |
||||
- "external_writefreely" |
||||
|
||||
ports: |
||||
- "8080:8080" |
||||
networks: |
||||
- writefreely |
||||
|
||||
depends_on: |
||||
- db |
||||
- "writefreely-db" |
||||
|
||||
restart: unless-stopped |
||||
db: |
||||
|
||||
writefreely-db: |
||||
container_name: "writefreely-db" |
||||
image: "mariadb:latest" |
||||
|
||||
volumes: |
||||
- "./schema.sql:/tmp/schema.sql" |
||||
- db-data:/var/lib/mysql/data |
||||
- "db-data:/var/lib/mysql/data" |
||||
|
||||
networks: |
||||
- writefreely |
||||
- "internal_writefreely" |
||||
|
||||
environment: |
||||
- MYSQL_DATABASE=writefreely |
||||
- MYSQL_ROOT_PASSWORD=changeme |
||||
restart: unless-stopped |
||||
|
||||
volumes: |
||||
web-data: |
||||
db-data: |
||||
|
||||
networks: |
||||
writefreely: |
||||
restart: unless-stopped |
||||
|
@ -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%; |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
@import "prose-editor"; |
||||
@import "pad-theme"; |
||||
@import "resources"; |
||||
@import "lib/elements"; |
@ -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; |
@ -0,0 +1,35 @@ |
||||
package writefreely_test |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/guregu/null/zero" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/writeas/writefreely" |
||||
) |
||||
|
||||
func TestPostSummary(t *testing.T) { |
||||
testCases := map[string]struct { |
||||
given writefreely.Post |
||||
expected string |
||||
}{ |
||||
"no special chars": {givenPost("Content."), "Content."}, |
||||
"HTML content": {givenPost("Content <p>with a</p> paragraph."), "Content with a paragraph."}, |
||||
"content with escaped char": {givenPost("Content's all OK."), "Content's all OK."}, |
||||
"multiline content": {givenPost(`Content |
||||
in |
||||
multiple |
||||
lines.`), "Content in multiple lines."}, |
||||
} |
||||
|
||||
for name, test := range testCases { |
||||
t.Run(name, func(t *testing.T) { |
||||
actual := test.given.Summary() |
||||
assert.Equal(t, test.expected, actual) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func givenPost(content string) writefreely.Post { |
||||
return writefreely.Post{Title: zero.StringFrom("Title"), Content: content} |
||||
} |
@ -0,0 +1,8 @@ |
||||
module.exports = { |
||||
"presets": [ |
||||
["@babel/env", { |
||||
"modules": false |
||||
}] |
||||
], |
||||
"plugins": ["@babel/plugin-syntax-dynamic-import"] |
||||
} |
@ -0,0 +1,4 @@ |
||||
{ |
||||
"tabWidth": 2, |
||||
"useTabs": false |
||||
} |
@ -0,0 +1,3 @@ |
||||
all : |
||||
npm install
|
||||
npm run-script build
|
@ -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_ |
@ -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 {}; |
||||
}, |
||||
}, |
||||
} |
||||
); |
@ -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, |
||||
}, |
||||
} |
||||
); |
@ -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; |
||||
}; |
File diff suppressed because it is too large
Load Diff
@ -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" |
||||
} |
@ -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> |
@ -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); |
@ -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, |
||||
}); |
@ -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'] |
||||
} |
||||
} |
||||
} |
||||
] |
||||
} |
||||
} |
File diff suppressed because one or more lines are too long
@ -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 new issue