Implement theme picker UI

This commit is contained in:
Magnus Hoff 2018-09-24 08:43:36 +02:00
parent baaab6ebc8
commit 3bbe5840ee
5 changed files with 94 additions and 11 deletions

View file

@ -8,16 +8,17 @@ function autosizeTextarea(textarea, shadow) {
function queryArgsFromForm(form) { function queryArgsFromForm(form) {
const items = []; const items = [];
for (const {name, value} of form.elements) { for (const {name, value, type, checked} of form.elements) {
if (!name) continue; if (!name) continue;
if (type === "radio" && !checked) continue;
items.push(encodeURIComponent(name) + '=' + encodeURIComponent(value)); items.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
} }
return items.join('&'); return items.join('&');
} }
function isEdited(form) { function isEdited(form) {
for (const {name, value, defaultValue} of form.elements) { for (const {name, value, defaultValue, checked, defaultChecked} of form.elements) {
if (name && (value !== defaultValue)) return true; if (name && ((value !== defaultValue) || (checked !== defaultChecked))) return true;
} }
return false; return false;
} }
@ -60,6 +61,7 @@ function confirmDiscard() {
let hasBeenOpen = false; let hasBeenOpen = false;
function openEditor() { function openEditor() {
const bodyElement = document.querySelector("body");
const container = document.querySelector(".container"); const container = document.querySelector(".container");
const rendered = container.querySelector(".rendered"); const rendered = container.querySelector(".rendered");
const editor = container.querySelector(".editor"); const editor = container.querySelector(".editor");
@ -119,6 +121,7 @@ function openEditor() {
.then(result => { .then(result => {
// Update url-bar, page title and footer // Update url-bar, page title and footer
window.history.replaceState(null, result.title, result.slug == "" ? "." : result.slug); window.history.replaceState(null, result.title, result.slug == "" ? "." : result.slug);
// TODO Cancel-link URL should be updated to new slug
document.querySelector("title").textContent = result.title; document.querySelector("title").textContent = result.title;
lastUpdated.innerHTML = result.last_updated; lastUpdated.innerHTML = result.last_updated;
lastUpdated.classList.remove("missing"); lastUpdated.classList.remove("missing");
@ -129,10 +132,14 @@ function openEditor() {
form.elements.title.value = result.title; form.elements.title.value = result.title;
shadow.value = textarea.value = result.body; shadow.value = textarea.value = result.body;
form.querySelector(`.theme-picker--option[value=${JSON.stringify(result.theme)}]`).checked = true;
bodyElement.className = `theme-${result.theme}`;
// Update form: // Update form:
form.elements.base_revision.value = result.revision; form.elements.base_revision.value = result.revision;
for (const element of form.elements) { for (const element of form.elements) {
element.defaultValue = element.value; element.defaultValue = element.value;
element.defaultChecked = element.checked;
} }
if (!result.conflict) { if (!result.conflict) {
@ -196,6 +203,13 @@ function openEditor() {
doSave(); doSave();
} }
}); });
const themeOptions = form.querySelectorAll(".theme-picker--option");
for (let themeOption of themeOptions) {
themeOption.addEventListener("click", function (ev) {
bodyElement.className = `theme-${ev.target.value}`;
});
}
} }
document document

View file

@ -329,6 +329,49 @@ h1>input {
transition-timing-function: cubic-bezier(.17,.84,.44,1); transition-timing-function: cubic-bezier(.17,.84,.44,1);
} }
.theme-picker {
position: absolute;
top: 0;
left: 0;
right: 0;
display: flex;
}
.theme-picker--option {
/* reset */
-webkit-appearance: none;
-moz-appearance: none;
-o-appearance: none;
-ms-appearance: none;
appearance: none;
border: none;
border-radius: 0;
margin: 0;
padding: 0;
height: 20px;
background: var(--theme-main);
color: var(--theme-text);
flex-grow: 1;
position: relative;
}
.theme-picker--option:checked::after {
content: " ";
display: block;
background: white;
border-radius: 5px;
width: 10px;
height: 10px;
position: absolute;
top: calc(50% - 5px);
left: calc(50% - 5px);
}
.button { .button {
border-radius: 2px; border-radius: 2px;

View file

@ -11,11 +11,16 @@ use mimes::*;
use rendering::render_markdown; use rendering::render_markdown;
use site::Layout; use site::Layout;
use state::{State, UpdateResult, RebaseConflict}; use state::{State, UpdateResult, RebaseConflict};
use theme::Theme; use theme::{self, Theme};
use web::{Resource, ResponseFuture}; use web::{Resource, ResponseFuture};
use super::changes_resource::QueryParameters; use super::changes_resource::QueryParameters;
struct SelectableTheme {
theme: Theme,
selected: bool,
}
#[derive(BartDisplay)] #[derive(BartDisplay)]
#[template="templates/article.html"] #[template="templates/article.html"]
struct Template<'a> { struct Template<'a> {
@ -27,7 +32,7 @@ struct Template<'a> {
title: &'a str, title: &'a str,
raw: &'a str, raw: &'a str,
rendered: String, rendered: String,
theme: Theme, themes: &'a [SelectableTheme],
} }
impl<'a> Template<'a> { impl<'a> Template<'a> {
@ -118,7 +123,10 @@ impl Resource for ArticleResource {
title: &data.title, title: &data.title,
raw: &data.body, raw: &data.body,
rendered: render_markdown(&data.body), rendered: render_markdown(&data.body),
theme: data.theme, themes: &theme::THEMES.iter().map(|&x| SelectableTheme {
theme: x,
selected: x == data.theme,
}).collect::<Vec<_>>(),
}, },
}.to_string())) }.to_string()))
})) }))
@ -259,7 +267,10 @@ impl Resource for ArticleResource {
title: &title, title: &title,
raw: &body, raw: &body,
rendered: render_markdown(&body), rendered: render_markdown(&body),
theme, themes: &theme::THEMES.iter().map(|&x| SelectableTheme {
theme: x,
selected: x == theme,
}).collect::<Vec<_>>(),
}, },
}.to_string()) }.to_string())
) )

View file

@ -58,6 +58,12 @@ impl Resource for NewArticleResource {
} }
fn get(self: Box<Self>) -> ResponseFuture { fn get(self: Box<Self>) -> ResponseFuture {
// TODO Remove duplication with article_resource.rs:
struct SelectableTheme {
theme: Theme,
selected: bool,
}
#[derive(BartDisplay)] #[derive(BartDisplay)]
#[template="templates/article.html"] #[template="templates/article.html"]
struct Template<'a> { struct Template<'a> {
@ -69,7 +75,7 @@ impl Resource for NewArticleResource {
title: &'a str, title: &'a str,
raw: &'a str, raw: &'a str,
rendered: &'a str, rendered: &'a str,
theme: Theme, themes: &'a [SelectableTheme],
} }
impl<'a> Template<'a> { impl<'a> Template<'a> {
fn script_js(&self) -> &'static str { fn script_js(&self) -> &'static str {
@ -95,13 +101,17 @@ impl Resource for NewArticleResource {
// Implicitly start in edit-mode when no slug is given. This // Implicitly start in edit-mode when no slug is given. This
// currently directly corresponds to the /_new endpoint // currently directly corresponds to the /_new endpoint
// TODO: Also start in edit mode when the ?edit query arg is given
edit: self.slug.is_none(), edit: self.slug.is_none(),
cancel_url: self.slug.as_ref().map(|x| &**x), cancel_url: self.slug.as_ref().map(|x| &**x),
title: &title, title: &title,
raw: "", raw: "",
rendered: EMPTY_ARTICLE_MESSAGE, rendered: EMPTY_ARTICLE_MESSAGE,
theme, themes: &theme::THEMES.iter().map(|&x| SelectableTheme {
theme: x,
selected: x == theme,
}).collect::<Vec<_>>(),
}, },
}.to_string())) }.to_string()))
})) }))

View file

@ -5,7 +5,7 @@
{{>article_contents.html}} {{>article_contents.html}}
</div> </div>
<form id="article-editor" action="" method="POST"> <form autocomplete="off" id="article-editor" action="" method="POST">
<div class="editor"> <div class="editor">
<div class="hero"> <div class="hero">
@ -14,9 +14,14 @@
</header> </header>
</div> </div>
<div class="theme-picker">
{{#themes}}
<input autocomplete="off" type="radio" name="theme" value="{{.theme}}"{{#.selected?}} checked{{/.selected}} class="theme-picker--option {{.theme.css_class()}} themed">
{{/themes}}
</div>
<article> <article>
<p> <p>
<input autocomplete=off type=hidden name=theme value="{{theme}}">
<input autocomplete=off type=hidden name=base_revision value="{{revision}}"> <input autocomplete=off type=hidden name=base_revision value="{{revision}}">
<textarea autocomplete=off name=body placeholder="Article goes here">{{raw}}</textarea> <textarea autocomplete=off name=body placeholder="Article goes here">{{raw}}</textarea>
<textarea autocomplete=off class="shadow-control"></textarea> <textarea autocomplete=off class="shadow-control"></textarea>