Add editor

This includes the '/' route handler
This commit is contained in:
Matt Baer 2018-11-08 00:11:42 -05:00
parent a30fc5b52b
commit 86e7ba2579
21 changed files with 787 additions and 0 deletions

21
app.go
View File

@ -8,6 +8,7 @@ import (
"net/http"
"os"
"os/signal"
"regexp"
"syscall"
"github.com/gorilla/mux"
@ -37,6 +38,25 @@ type app struct {
sessionStore *sessions.CookieStore
}
// handleViewHome shows page at root path. Will be the Pad if logged in and the
// catch-all landing page otherwise.
func handleViewHome(app *app, w http.ResponseWriter, r *http.Request) error {
if app.cfg.App.SingleUser {
// Render blog index
return handleViewCollection(app, w, r)
}
// Multi-user instance
u := getUserSession(app, r)
if u != nil {
// User is logged in, so show the Pad
return handleViewPad(app, w, r)
}
// Show landing page
return renderPage(w, "landing.tmpl", pageForReq(app, r))
}
func pageForReq(app *app, r *http.Request) page.StaticPage {
p := page.StaticPage{
AppCfg: app.cfg.App,
@ -67,6 +87,7 @@ func pageForReq(app *app, r *http.Request) page.StaticPage {
}
var shttp = http.NewServeMux()
var fileRegex = regexp.MustCompile("/([^/]*\\.[^/]*)$")
func Serve() {
debugPtr := flag.Bool("debug", false, "Enables debug logging.")

144
pad.go Normal file
View File

@ -0,0 +1,144 @@
package writefreely
import (
"github.com/gorilla/mux"
"github.com/writeas/impart"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/page"
"net/http"
"strings"
)
func handleViewPad(app *app, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
action := vars["action"]
slug := vars["slug"]
collAlias := vars["collection"]
appData := &struct {
page.StaticPage
Post *RawPost
User *User
Blogs *[]Collection
Editing bool // True if we're modifying an existing post
EditCollection *Collection // Collection of the post we're editing, if any
}{
StaticPage: pageForReq(app, r),
Post: &RawPost{Font: "norm"},
User: getUserSession(app, r),
}
var err error
if appData.User != nil {
appData.Blogs, err = app.db.GetPublishableCollections(appData.User)
if err != nil {
log.Error("Unable to get user's blogs for Pad: %v", err)
}
}
padTmpl := "pad"
if action == "" && slug == "" {
// Not editing any post; simply render the Pad
if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil {
log.Error("Unable to execute template: %v", err)
}
return nil
}
// Retrieve post information for editing
appData.Editing = true
// Make sure this isn't cached, so user doesn't accidentally lose data
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "Thu, 04 Oct 1990 20:00:00 GMT")
if slug != "" {
appData.Post = getRawCollectionPost(app, slug, collAlias)
if appData.Post.OwnerID != appData.User.ID {
// TODO: add ErrForbiddenEditPost message to flashes
return impart.HTTPError{http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/edit")]}
}
appData.EditCollection, err = app.db.GetCollectionForPad(collAlias)
if err != nil {
return err
}
} else {
// Editing a floating article
appData.Post = getRawPost(app, action)
appData.Post.Id = action
}
if appData.Post.Gone {
return ErrPostUnpublished
} else if appData.Post.Found && appData.Post.Content != "" {
// Got the post
} else if appData.Post.Found {
return ErrPostFetchError
} else {
return ErrPostNotFound
}
if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil {
log.Error("Unable to execute template: %v", err)
}
return nil
}
func handleViewMeta(app *app, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
action := vars["action"]
slug := vars["slug"]
collAlias := vars["collection"]
appData := &struct {
page.StaticPage
Post *RawPost
User *User
EditCollection *Collection // Collection of the post we're editing, if any
Flashes []string
NeedsToken bool
}{
StaticPage: pageForReq(app, r),
Post: &RawPost{Font: "norm"},
User: getUserSession(app, r),
}
var err error
if action == "" && slug == "" {
return ErrPostNotFound
}
// Make sure this isn't cached, so user doesn't accidentally lose data
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "Thu, 28 Jul 1989 12:00:00 GMT")
if slug != "" {
appData.Post = getRawCollectionPost(app, slug, collAlias)
if appData.Post.OwnerID != appData.User.ID {
// TODO: add ErrForbiddenEditPost message to flashes
return impart.HTTPError{http.StatusFound, r.URL.Path[:strings.LastIndex(r.URL.Path, "/meta")]}
}
appData.EditCollection, err = app.db.GetCollectionForPad(collAlias)
if err != nil {
return err
}
} else {
// Editing a floating article
appData.Post = getRawPost(app, action)
appData.Post.Id = action
}
appData.NeedsToken = appData.User == nil || appData.User.ID != appData.Post.OwnerID
if appData.Post.Gone {
return ErrPostUnpublished
} else if appData.Post.Found && appData.Post.Content != "" {
// Got the post
} else if appData.Post.Found {
return ErrPostFetchError
} else {
return ErrPostNotFound
}
appData.Flashes, _ = getSessionFlashes(app, w, r, nil)
if err = templates["edit-meta"].ExecuteTemplate(w, "edit-meta", appData); err != nil {
log.Error("Unable to execute template: %v", err)
}
return nil
}

View File

@ -45,6 +45,12 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
posts.HandleFunc("/claim", handler.All(addPost)).Methods("POST")
posts.HandleFunc("/disperse", handler.All(dispersePost)).Methods("POST")
if cfg.App.SingleUser {
write.HandleFunc("/me/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
} else {
write.HandleFunc("/new", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
}
// All the existing stuff
write.HandleFunc("/{action}/edit", handler.Web(handleViewPad, UserLevelOptional)).Methods("GET")
write.HandleFunc("/{action}/meta", handler.Web(handleViewMeta, UserLevelOptional)).Methods("GET")
@ -54,4 +60,5 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto
// Posts
write.HandleFunc("/{post}", handler.Web(handleViewPost, UserLevelOptional))
}
write.HandleFunc("/", handler.Web(handleViewHome, UserLevelOptional))
}

BIN
static/img/ic_blogs@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

BIN
static/img/ic_edit@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

BIN
static/img/ic_font@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

BIN
static/img/ic_info@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

BIN
static/img/ic_list@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

BIN
static/img/ic_send@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

257
static/js/h.js Normal file
View File

@ -0,0 +1,257 @@
/**
* H.js
*
* Lightweight, extremely bare-bones library for manipulating the DOM and
* saving some typing.
*/
var Element = function(domElement) {
this.el = domElement;
};
/**
* Creates a toggle button that adds / removes the given class name from the
* given element.
*
* @param {Element} $el - The element to modify.
* @param {string} onClass - The class to add to the given element.
* @param {function} onFunc - Additional actions when toggling on.
* @param {function} offFunc - Additional actions when toggling off.
*/
Element.prototype.createToggle = function($el, onClass, onFunc, offFunc) {
this.on('click', function(e) {
if ($el.el.className === '') {
$el.el.className = onClass;
onFunc(new Element(this), e);
} else {
$el.el.className = '';
offFunc(new Element(this), e);
}
e.preventDefault();
}, false);
};
Element.prototype.on = function(event, func) {
events = event.split(' ');
var el = this.el;
if (el == null) {
console.error("Error: element for event is null");
return;
}
var addEvent = function(e) {
if (el.addEventListener) {
el.addEventListener(e, func, false);
} else if (el.attachEvent) {
el.attachEvent(e, func);
}
};
if (events.length === 1) {
addEvent(event);
} else {
for(var i=0; i<events.length; i++) {
addEvent(events[i]);
}
}
};
Element.prototype.setClass = function(className) {
if (this.el == null) {
console.error("Error: element to set class on is null");
return;
}
this.el.className = className;
};
Element.prototype.removeClass = function(className) {
if (this.el == null) {
console.error("Error: element to remove class on is null");
return;
}
var regex = new RegExp(' ?' + className, 'g');
this.el.className = this.el.className.replace(regex, '');
};
Element.prototype.text = function(text, className) {
if (this.el == null) {
console.error("Error: element for setting text is null");
return;
}
if (this.el.textContent !== text) {
this.el.textContent = text;
if (typeof className !== 'undefined') {
this.el.className = this.el.className + ' ' + className;
}
}
};
Element.prototype.insertAfter = function(newNode) {
if (this.el == null) {
console.error("Error: element for insertAfter is null");
return;
}
this.el.parentNode.insertBefore(newNode, this.el.nextSibling);
};
Element.prototype.remove = function() {
if (this.el == null) {
console.error("Didn't remove element");
return;
}
this.el.parentNode.removeChild(this.el);
};
Element.prototype.hide = function() {
if (this.el == null) {
console.error("Didn't hide element");
return;
}
this.el.className += ' effect fade-out';
};
Element.prototype.show = function() {
if (this.el == null) {
console.error("Didn't show element");
return;
}
this.el.className += ' effect';
};
var H = {
getEl: function(elementId) {
return new Element(document.getElementById(elementId));
},
save: function($el, key) {
localStorage.setItem(key, $el.el.value);
},
load: function($el, key, onlyLoadPopulated) {
var val = localStorage.getItem(key);
if (onlyLoadPopulated && val == null) {
// Do nothing
return;
}
$el.el.value = val;
},
set: function(key, value) {
localStorage.setItem(key, value);
},
get: function(key, defaultValue) {
var val = localStorage.getItem(key);
if (val == null) {
val = defaultValue;
}
return val;
},
remove: function(key) {
localStorage.removeItem(key);
},
exists: function(key) {
return localStorage.getItem(key) !== null;
},
createPost: function(id, editToken, content, created) {
var summaryLen = 200;
var titleLen = 80;
var getPostMeta = function(content) {
var eol = content.indexOf("\n");
if (content.indexOf("# ") === 0) {
// Title is in the format:
//
// # Some title
var summary = content.substring(eol).trim();
if (summary.length > summaryLen) {
summary = summary.substring(0, summaryLen) + "...";
}
return {
title: content.substring("# ".length, eol),
summary: summary,
};
}
var blankLine = content.indexOf("\n\n");
if (blankLine !== -1 && blankLine <= eol && blankLine <= titleLen) {
// Title is in the format:
//
// Some title
//
// The body starts after that blank line above it.
var summary = content.substring(blankLine).trim();
if (summary.length > summaryLen) {
summary = summary.substring(0, summaryLen) + "...";
}
return {
title: content.substring(0, blankLine),
summary: summary,
};
}
// TODO: move this to the beginning
var title = content.trim();
var summary = "";
if (title.length > titleLen) {
// Content can't fit in the title, so figure out the summary
summary = title;
title = "";
if (summary.length > summaryLen) {
summary = summary.substring(0, summaryLen) + "...";
}
} else if (eol > 0) {
summary = title.substring(eol+1);
title = title.substring(0, eol);
}
return {
title: title,
summary: summary
};
};
var post = getPostMeta(content);
post.id = id;
post.token = editToken;
post.created = created ? new Date(created) : new Date();
post.client = "Pad";
return post;
},
getTitleStrict: function(content) {
var eol = content.indexOf("\n");
var title = "";
var newContent = content;
if (content.indexOf("# ") === 0) {
// Title is in the format:
// # Some title
if (eol !== -1) {
// First line should start with # and end with \n
newContent = content.substring(eol).leftTrim();
title = content.substring("# ".length, eol);
}
}
return {
title: title,
content: newContent
};
},
};
var He = {
create: function(name) {
return document.createElement(name);
},
get: function(id) {
return document.getElementById(id);
},
$: function(selector) {
var els = document.querySelectorAll(selector);
return els;
},
postJSON: function(url, params, callback) {
var http = new XMLHttpRequest();
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) {
callback(http.status, JSON.parse(http.responseText));
}
}
http.send(JSON.stringify(params));
},
};
String.prototype.leftTrim = function() {
return this.replace(/^\s+/,"");
};

358
templates/pad.tmpl Normal file
View File

@ -0,0 +1,358 @@
{{define "pad"}}<!DOCTYPE HTML>
<html>
<head>
<title>{{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} &mdash; {{.SiteName}}</title>
<link rel="stylesheet" type="text/css" href="/css/write.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="google" value="notranslate">
</head>
<body id="pad" class="light">
<div id="overlay"></div>
<textarea id="writer" placeholder="Write..." class="{{.Post.Font}}" autofocus>{{if .Post.Title}}# {{.Post.Title}}
{{end}}{{.Post.Content}}</textarea>
<header id="tools">
<div id="clip">
<h1>{{if .User}}<a href="/me/c/" title="View blogs"><img class="ic-24dp" src="/img/ic_blogs_dark@2x.png" /></a>{{else}}<a href="/">w<span class="if-room">rite.as</span></a>{{end}}
</h1>
<nav id="target" class=""><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>
<li class="target selected" id="anonymous"><a href="#anonymous"><i class="material-icons md-18">description</i> <em>Draft</em></a></li>
{{if .Blogs}}{{range .Blogs}}
<li class="target" id="blog-{{.Alias}}"><a href="#{{.Alias}}"><i class="material-icons md-18">public</i> {{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a></li>
{{end}}{{end}}
<li id="user-separator" class="separator"><hr /></li>
<li><a href="/me/c/"><i class="material-icons md-18">library_books</i> View Blogs</a></li>
<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}}/{{.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 {
newTheme = 'light';
document.body.className = document.body.className.replace(/(?:^|\s)dark(?!\S)/g, newTheme);
for (var i=0; i<btns.length; i++) {
btns[i].src = btns[i].src.replace('@2x.png', '_dark@2x.png');
}
}
H.set('padTheme', newTheme);
}
if (H.get('padTheme', 'light') != 'light') {
toggleTheme();
}
var $writer = H.getEl('writer');
var $btnPublish = H.getEl('publish');
var $wc = H.getEl("wc");
var updateWordCount = function() {
var words = 0;
var val = $writer.el.value.trim();
if (val != '') {
words = $writer.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 ($writer.el.value.length === 0 || (draftDoc != 'lastDoc' && $writer.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}}
H.load($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 publish = function(content, font) {
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}}
if (!token) {
alert("You don't have permission to update this post.");
return;
}
if ($btnPublish.el.className == 'disabled') {
return;
}
{{end}}
$btnPublish.el.children[0].textContent = 'more_horiz';
publishing = true;
var xpostTarg = H.get('crosspostTarget', '[]');
var http = new XMLHttpRequest();
var lang = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage);
lang = lang.substring(0, 2);
var post = H.getTitleStrict(content);
var params = {
body: post.content,
title: post.title,
font: font,
lang: lang
};
{{ if .Post.Slug }}
var url = "/api/collections/{{.EditCollection.Alias}}/posts/{{.Post.Id}}";
{{ else if .Post.Id }}
var url = "/api/posts/{{.Post.Id}}";
if (typeof token === 'undefined' || !token) {
token = "";
}
params.token = token;
{{ else }}
var url = "/api/posts";
var postTarget = H.get('postTarget', 'anonymous');
if (postTarget != 'anonymous') {
url = "/api/collections/" + postTarget + "/posts";
}
params.crosspost = JSON.parse(xpostTarg);
{{ end }}
http.open("POST", url, true);
// Send the proper header information along with the request
http.setRequestHeader("Content-type", "application/json");
http.onreadystatechange = function() {
if (http.readyState == 4) {
publishing = false;
if (http.status == 200 || http.status == 201) {
data = JSON.parse(http.responseText);
id = data.data.id;
nextURL = '/'+id;
{{ if not .Post.Id }}
// Post created
if (postTarget != 'anonymous') {
nextURL = '/'+postTarget+'/'+data.data.slug;
}
editToken = data.data.token;
{{ if not .User }}if (postTarget == 'anonymous') {
// Save the data
var posts = JSON.parse(H.get('posts', '[]'));
{{if .Post.Id}}var newPost = H.createPost("{{.Post.Id}}", token, content);
for (var i=0; i<posts.length; i++) {
if (posts[i].id == "{{.Post.Id}}") {
posts[i].title = newPost.title;
posts[i].summary = newPost.summary;
break;
}
}
nextURL = "/pad/posts";{{else}}posts.push(H.createPost(id, editToken, content));{{end}}
H.set('posts', JSON.stringify(posts));
}
{{ end }}
{{ end }}
justPublished = true;
if (draftDoc != 'lastDoc') {
H.remove(draftDoc);
{{if .Editing}}H.remove('draft{{.Post.Id}}font');{{end}}
} else {
H.set(draftDoc, '');
}
{{if .EditCollection}}
window.location = '{{.EditCollection.CanonicalURL}}{{.Post.Slug}}';
{{else}}
window.location = nextURL;
{{end}}
} else {
$btnPublish.el.children[0].textContent = 'send';
alert("Failed to post. Please try again.");
}
}
}
http.send(JSON.stringify(params));
};
setButtonStates();
$writer.on('keyup input', function() {
setButtonStates();
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
}, false);
$writer.on('keydown', function(e) {
clearTimeout(typingTimer);
if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) {
$btnPublish.el.click();
}
});
$btnPublish.on('click', function(e) {
e.preventDefault();
if (!publishing && $writer.el.value) {
var content = $writer.el.value;
publish(content, selectedFont);
}
});
H.getEl('toggle-theme').on('click', function(e) {
e.preventDefault();
var newTheme = 'light';
if (document.body.className == 'light') {
newTheme = 'dark';
}
toggleTheme();
});
var targets = document.querySelectorAll('#target li.target a');
for (var i=0; i<targets.length; i++) {
targets[i].addEventListener('click', function(e) {
e.preventDefault();
var targetName = this.href.substring(this.href.indexOf('#')+1);
H.set('postTarget', targetName);
document.querySelector('#target li.target.selected').classList.remove('selected');
this.parentElement.classList.add('selected');
var newText = this.innerText.split(' ');
newText.shift();
document.getElementById('target-name').innerText = newText.join(' ');
});
}
var postTarget = H.get('postTarget', 'anonymous');
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);
$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' || $writer.el.value != origDoc) {
H.save($writer, draftDoc);
updateWordCount();
}
};
window.addEventListener('beforeunload', function(e) {
if (draftDoc != 'lastDoc' && $writer.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>
<link href="/css/icons.css" rel="stylesheet">
</body>
</html>{{end}}