Implement support for editing page title
This commit is contained in:
parent
e82350e499
commit
3feed530ff
6 changed files with 82 additions and 29 deletions
|
@ -15,9 +15,9 @@ function queryArgsFromForm(form) {
|
|||
|
||||
let hasBeenOpen = false;
|
||||
function openEditor() {
|
||||
const article = document.querySelector("article");
|
||||
const rendered = article.querySelector(".rendered");
|
||||
const editor = article.querySelector(".editor");
|
||||
const container = document.querySelector(".container");
|
||||
const rendered = container.querySelector(".rendered");
|
||||
const editor = container.querySelector(".editor");
|
||||
const textarea = editor.querySelector('textarea[name="body"]');
|
||||
const shadow = editor.querySelector('textarea.shadow-control');
|
||||
const form = editor.querySelector("form");
|
||||
|
@ -29,7 +29,7 @@ function openEditor() {
|
|||
|
||||
textarea.style.height = rendered.clientHeight + "px";
|
||||
|
||||
article.classList.add('edit');
|
||||
container.classList.add('edit');
|
||||
|
||||
autosizeTextarea(textarea, shadow);
|
||||
|
||||
|
@ -63,11 +63,23 @@ function openEditor() {
|
|||
if (!response.ok) throw new Error("Unexpected status code (" + response.status + ")");
|
||||
|
||||
const result = await response.json();
|
||||
form.elements.base_revision.value = result.revision;
|
||||
|
||||
// Update url-bar, page title and footer
|
||||
window.history.replaceState(null, result.title, result.slug);
|
||||
document.querySelector("title").textContent = result.title;
|
||||
revision.textContent = result.revision;
|
||||
lastUpdated.textContent = result.created;
|
||||
|
||||
// Update body:
|
||||
rendered.innerHTML = result.rendered;
|
||||
article.classList.remove('edit');
|
||||
|
||||
// Update form:
|
||||
form.elements.base_revision.value = result.revision;
|
||||
for (const element of form.elements) {
|
||||
element.defaultValue = element.value;
|
||||
}
|
||||
|
||||
container.classList.remove('edit');
|
||||
|
||||
textarea.disabled = false;
|
||||
}()
|
||||
|
@ -82,7 +94,7 @@ function openEditor() {
|
|||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
article.classList.remove('edit');
|
||||
container.classList.remove('edit');
|
||||
form.reset();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ body {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
article {
|
||||
.container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
@ -146,6 +146,15 @@ textarea {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1>input {
|
||||
font: inherit;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shadow-control {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
|
|
|
@ -86,6 +86,7 @@ impl Resource for ArticleResource {
|
|||
revision: i32,
|
||||
created: &'a chrono::DateTime<Local>,
|
||||
|
||||
slug: &'a str,
|
||||
title: &'a str,
|
||||
raw: &'a str,
|
||||
rendered: String,
|
||||
|
@ -106,6 +107,7 @@ impl Resource for ArticleResource {
|
|||
article_id: data.article_id,
|
||||
revision: data.revision,
|
||||
created: &Local.from_utc_datetime(&data.created),
|
||||
slug: &data.slug,
|
||||
title: &data.title,
|
||||
raw: &data.body,
|
||||
rendered: render_markdown(&data.body),
|
||||
|
@ -125,12 +127,22 @@ impl Resource for ArticleResource {
|
|||
#[derive(Deserialize)]
|
||||
struct UpdateArticle {
|
||||
base_revision: i32,
|
||||
title: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article_revision_contents.html"]
|
||||
struct Template<'a> {
|
||||
title: &'a str,
|
||||
rendered: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PutResponse<'a> {
|
||||
slug: &'a str,
|
||||
revision: i32,
|
||||
title: &'a str,
|
||||
rendered: &'a str,
|
||||
created: &'a str,
|
||||
}
|
||||
|
@ -143,15 +155,20 @@ impl Resource for ArticleResource {
|
|||
.map_err(Into::into)
|
||||
})
|
||||
.and_then(move |update: UpdateArticle| {
|
||||
self.state.update_article(self.article_id, update.base_revision, update.body)
|
||||
self.state.update_article(self.article_id, update.base_revision, update.title, update.body)
|
||||
})
|
||||
.and_then(|updated| {
|
||||
futures::finished(Response::new()
|
||||
.with_status(hyper::StatusCode::Ok)
|
||||
.with_header(ContentType(APPLICATION_JSON.clone()))
|
||||
.with_body(serde_json::to_string(&PutResponse {
|
||||
slug: &updated.slug,
|
||||
revision: updated.revision,
|
||||
rendered: &render_markdown(&updated.body),
|
||||
title: &updated.title,
|
||||
rendered: &Template {
|
||||
title: &updated.title,
|
||||
rendered: render_markdown(&updated.body),
|
||||
}.to_string(),
|
||||
created: &Local.from_utc_datetime(&updated.created).to_string(),
|
||||
}).expect("Should never fail"))
|
||||
)
|
||||
|
|
10
src/state.rs
10
src/state.rs
|
@ -26,7 +26,7 @@ pub enum SlugLookup {
|
|||
Redirect(String),
|
||||
}
|
||||
|
||||
fn decide_slug(conn: &SqliteConnection, prev_title: &str, title: &str, prev_slug: &str) -> Result<String, Error> {
|
||||
fn decide_slug(conn: &SqliteConnection, article_id: i32, prev_title: &str, title: &str, prev_slug: &str) -> Result<String, Error> {
|
||||
if title == prev_title {
|
||||
return Ok(prev_slug.to_owned());
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ fn decide_slug(conn: &SqliteConnection, prev_title: &str, title: &str, prev_slug
|
|||
|
||||
loop {
|
||||
let slug_in_use = article_revisions::table
|
||||
.filter(article_revisions::article_id.ne(article_id))
|
||||
.filter(article_revisions::slug.eq(&slug))
|
||||
.filter(article_revisions::latest.eq(true))
|
||||
.count()
|
||||
|
@ -124,7 +125,9 @@ impl State {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn update_article(&self, article_id: i32, base_revision: i32, body: String) -> CpuFuture<models::ArticleRevision, Error> {
|
||||
pub fn update_article(&self, article_id: i32, base_revision: i32, title: String, body: String)
|
||||
-> CpuFuture<models::ArticleRevision, Error>
|
||||
{
|
||||
let connection_pool = self.connection_pool.clone();
|
||||
|
||||
self.cpu_pool.spawn_fn(move || {
|
||||
|
@ -150,8 +153,7 @@ impl State {
|
|||
}
|
||||
let new_revision = base_revision + 1;
|
||||
|
||||
let title = prev_title.clone(); // TODO Have title be a parameter to this function
|
||||
let slug = decide_slug(&*conn, &prev_title, &title, &prev_slug)?;
|
||||
let slug = decide_slug(&*conn, article_id, &prev_title, &title, &prev_slug)?;
|
||||
|
||||
#[derive(Insertable)]
|
||||
#[table_name="article_revisions"]
|
||||
|
|
|
@ -1,25 +1,31 @@
|
|||
<script src="_assets/script-{{script_js_checksum}}.js" defer></script>
|
||||
|
||||
<div class="container">
|
||||
<div class="rendered">
|
||||
{{>article_revision_contents.html}}
|
||||
</div>
|
||||
|
||||
<div class="editor">
|
||||
<form action="" method="POST">
|
||||
|
||||
<header>
|
||||
<h1>{{title}}</h1>
|
||||
<h1><input autocomplete=off type=text name=title value="{{title}}"></h1>
|
||||
</header>
|
||||
|
||||
<article>
|
||||
<div class="rendered">
|
||||
{{{rendered}}}
|
||||
</div>
|
||||
<div class="editor">
|
||||
<form action="" method="POST">
|
||||
<input autocomplete=off type=hidden name=base_revision value="{{revision}}">
|
||||
<textarea autocomplete=off name=body>{{raw}}</textarea>
|
||||
<textarea autocomplete=off class="shadow-control"></textarea>
|
||||
</article>
|
||||
|
||||
<div class="editor-controls">
|
||||
<a class="cancel" href="{{article_id}}">Cancel</a>
|
||||
<a class="cancel" href="{{slug}}">Cancel</a>
|
||||
<button type=submit>Save</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p><a id="openEditor" href="?editor">Edit</a></p>
|
||||
|
|
7
templates/article_revision_contents.html
Normal file
7
templates/article_revision_contents.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<header>
|
||||
<h1>{{title}}</h1>
|
||||
</header>
|
||||
|
||||
<article>
|
||||
{{{rendered}}}
|
||||
</article>
|
Loading…
Reference in a new issue