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