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) {
|
||||
const items = [];
|
||||
for (const {name, value} of form.elements) {
|
||||
for (const {name, value, type, checked} of form.elements) {
|
||||
if (!name) continue;
|
||||
if (type === "radio" && !checked) continue;
|
||||
items.push(encodeURIComponent(name) + '=' + encodeURIComponent(value));
|
||||
}
|
||||
return items.join('&');
|
||||
}
|
||||
|
||||
function isEdited(form) {
|
||||
for (const {name, value, defaultValue} of form.elements) {
|
||||
if (name && (value !== defaultValue)) return true;
|
||||
for (const {name, value, defaultValue, checked, defaultChecked} of form.elements) {
|
||||
if (name && ((value !== defaultValue) || (checked !== defaultChecked))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -60,6 +61,7 @@ function confirmDiscard() {
|
|||
|
||||
let hasBeenOpen = false;
|
||||
function openEditor() {
|
||||
const bodyElement = document.querySelector("body");
|
||||
const container = document.querySelector(".container");
|
||||
const rendered = container.querySelector(".rendered");
|
||||
const editor = container.querySelector(".editor");
|
||||
|
@ -119,6 +121,7 @@ function openEditor() {
|
|||
.then(result => {
|
||||
// Update url-bar, page title and footer
|
||||
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;
|
||||
lastUpdated.innerHTML = result.last_updated;
|
||||
lastUpdated.classList.remove("missing");
|
||||
|
@ -129,10 +132,14 @@ function openEditor() {
|
|||
form.elements.title.value = result.title;
|
||||
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:
|
||||
form.elements.base_revision.value = result.revision;
|
||||
for (const element of form.elements) {
|
||||
element.defaultValue = element.value;
|
||||
element.defaultChecked = element.checked;
|
||||
}
|
||||
|
||||
if (!result.conflict) {
|
||||
|
@ -196,6 +203,13 @@ function openEditor() {
|
|||
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
|
||||
|
|
|
@ -329,6 +329,49 @@ h1>input {
|
|||
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 {
|
||||
border-radius: 2px;
|
||||
|
||||
|
|
|
@ -11,11 +11,16 @@ use mimes::*;
|
|||
use rendering::render_markdown;
|
||||
use site::Layout;
|
||||
use state::{State, UpdateResult, RebaseConflict};
|
||||
use theme::Theme;
|
||||
use theme::{self, Theme};
|
||||
use web::{Resource, ResponseFuture};
|
||||
|
||||
use super::changes_resource::QueryParameters;
|
||||
|
||||
struct SelectableTheme {
|
||||
theme: Theme,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article.html"]
|
||||
struct Template<'a> {
|
||||
|
@ -27,7 +32,7 @@ struct Template<'a> {
|
|||
title: &'a str,
|
||||
raw: &'a str,
|
||||
rendered: String,
|
||||
theme: Theme,
|
||||
themes: &'a [SelectableTheme],
|
||||
}
|
||||
|
||||
impl<'a> Template<'a> {
|
||||
|
@ -118,7 +123,10 @@ impl Resource for ArticleResource {
|
|||
title: &data.title,
|
||||
raw: &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()))
|
||||
}))
|
||||
|
@ -259,7 +267,10 @@ impl Resource for ArticleResource {
|
|||
title: &title,
|
||||
raw: &body,
|
||||
rendered: render_markdown(&body),
|
||||
theme,
|
||||
themes: &theme::THEMES.iter().map(|&x| SelectableTheme {
|
||||
theme: x,
|
||||
selected: x == theme,
|
||||
}).collect::<Vec<_>>(),
|
||||
},
|
||||
}.to_string())
|
||||
)
|
||||
|
|
|
@ -58,6 +58,12 @@ impl Resource for NewArticleResource {
|
|||
}
|
||||
|
||||
fn get(self: Box<Self>) -> ResponseFuture {
|
||||
// TODO Remove duplication with article_resource.rs:
|
||||
struct SelectableTheme {
|
||||
theme: Theme,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
#[derive(BartDisplay)]
|
||||
#[template="templates/article.html"]
|
||||
struct Template<'a> {
|
||||
|
@ -69,7 +75,7 @@ impl Resource for NewArticleResource {
|
|||
title: &'a str,
|
||||
raw: &'a str,
|
||||
rendered: &'a str,
|
||||
theme: Theme,
|
||||
themes: &'a [SelectableTheme],
|
||||
}
|
||||
impl<'a> Template<'a> {
|
||||
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
|
||||
// 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(),
|
||||
|
||||
cancel_url: self.slug.as_ref().map(|x| &**x),
|
||||
title: &title,
|
||||
raw: "",
|
||||
rendered: EMPTY_ARTICLE_MESSAGE,
|
||||
theme,
|
||||
themes: &theme::THEMES.iter().map(|&x| SelectableTheme {
|
||||
theme: x,
|
||||
selected: x == theme,
|
||||
}).collect::<Vec<_>>(),
|
||||
},
|
||||
}.to_string()))
|
||||
}))
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{{>article_contents.html}}
|
||||
</div>
|
||||
|
||||
<form id="article-editor" action="" method="POST">
|
||||
<form autocomplete="off" id="article-editor" action="" method="POST">
|
||||
|
||||
<div class="editor">
|
||||
<div class="hero">
|
||||
|
@ -14,9 +14,14 @@
|
|||
</header>
|
||||
</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>
|
||||
<p>
|
||||
<input autocomplete=off type=hidden name=theme value="{{theme}}">
|
||||
<input autocomplete=off type=hidden name=base_revision value="{{revision}}">
|
||||
<textarea autocomplete=off name=body placeholder="Article goes here">{{raw}}</textarea>
|
||||
<textarea autocomplete=off class="shadow-control"></textarea>
|
||||
|
|
Loading…
Reference in a new issue