Implement theme picker UI
This commit is contained in:
parent
baaab6ebc8
commit
3bbe5840ee
5 changed files with 94 additions and 11 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()))
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue