Add a sidebar for the editor

- The layout now uses CSS grids
- We try to generate as much HTML as possible on the server, instead of using the DOM
- Placeholders are in pure CSS now!

You can't publish articles anymore, but it looks nice!!
This commit is contained in:
Ana Gelez 2019-08-02 23:10:05 +02:00
parent 5d03331f0c
commit 4142e73018
3 changed files with 127 additions and 130 deletions

View File

@ -63,33 +63,7 @@ impl From<stdweb::private::ConversionError> for EditorError {
} }
} }
fn init_widget( fn filter_paste(elt: &Element) {
parent: &Element,
tag: &'static str,
placeholder_text: String,
content: String,
disable_return: bool,
) -> Result<HtmlElement, EditorError> {
let widget = placeholder(make_editable(tag).try_into()?, &placeholder_text);
if !content.is_empty() {
widget.dataset().insert("edited", "true")?;
}
widget.append_child(&document().create_text_node(&content));
if disable_return {
widget.add_event_listener(no_return);
}
parent.append_child(&widget);
// We need to do that to make sure the placeholder is correctly rendered
widget.focus();
widget.blur();
filter_paste(&widget);
Ok(widget)
}
fn filter_paste(elt: &HtmlElement) {
// Only insert text when pasting something // Only insert text when pasting something
js! { js! {
@{&elt}.addEventListener("paste", function (evt) { @{&elt}.addEventListener("paste", function (evt) {
@ -127,43 +101,32 @@ pub fn init() -> Result<(), EditorError> {
fn init_editor() -> Result<(), EditorError> { fn init_editor() -> Result<(), EditorError> {
if let Some(ed) = document().get_element_by_id("plume-editor") { if let Some(ed) = document().get_element_by_id("plume-editor") {
document().body()?.set_attribute("id", "editor")?;
let aside = document().get_element_by_id("plume-editor-aside")?;
// Show the editor // Show the editor
js! { @{&ed}.style.display = "block"; }; js! {
@{&ed}.style.display = "grid";
@{&aside}.style.display = "block";
};
// And hide the HTML-only fallback // And hide the HTML-only fallback
let old_ed = document().get_element_by_id("plume-fallback-editor")?; let old_ed = document().get_element_by_id("plume-fallback-editor")?;
let old_title = document().get_element_by_id("plume-editor-title")?;
js! { js! {
@{&old_ed}.style.display = "none"; @{&old_ed}.style.display = "none";
@{&old_title}.style.display = "none";
}; };
// Get content from the old editor (when editing an article for instance)
let title_val = get_elt_value("title");
let subtitle_val = get_elt_value("subtitle");
let content_val = get_elt_value("editor-content");
// And pre-fill the new editor with this values // And pre-fill the new editor with this values
let title = init_widget( let title = document().get_element_by_id("editor-title")?;
&ed, let subtitle = document().get_element_by_id("editor-subtitle")?;
"h1", let content = document().get_element_by_id("editor-default-paragraph")?;
i18n!(CATALOG, "Enter your title"),
title_val, title.add_event_listener(no_return);
true, subtitle.add_event_listener(no_return);
)?;
let subtitle = init_widget( filter_paste(&title);
&ed, filter_paste(&subtitle);
"h2", filter_paste(&content);
i18n!(CATALOG, "Enter a subtitle, or a summary"),
subtitle_val,
true,
)?;
let content = init_widget(
&ed,
"article",
i18n!(CATALOG, "Write your article here. Markdown is supported."),
content_val.clone(),
false,
)?;
js! { @{&content}.innerHTML = @{content_val}; };
// character counter // character counter
content.add_event_listener(mv!(content => move |_: KeyDownEvent| { content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
@ -178,19 +141,27 @@ fn init_editor() -> Result<(), EditorError> {
}), 0); }), 0);
})); }));
document().get_element_by_id("publish")?.add_event_listener( document()
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| { .get_element_by_id("publish")?
let popup = document().get_element_by_id("publish-popup").or_else(|| .add_event_listener(|_: ClickEvent| {
init_popup(&title, &subtitle, &content, &old_ed).ok() let publish_page = document().get_element_by_id("publish-page").unwrap();
).unwrap(); let options_page = document().get_element_by_id("options-page").unwrap();
let bg = document().get_element_by_id("popup-bg").or_else(|| js! {
init_popup_bg().ok() @{&options_page}.style.display = "none";
).unwrap(); @{&publish_page}.style.display = "flex";
};
});
popup.class_list().add("show").unwrap(); document()
bg.class_list().add("show").unwrap(); .get_element_by_id("cancel-publish")?
}), .add_event_listener(|_: ClickEvent| {
); let publish_page = document().get_element_by_id("publish-page").unwrap();
let options_page = document().get_element_by_id("options-page").unwrap();
js! {
@{&publish_page}.style.display = "none";
@{&options_page}.style.display = "flex";
};
});
show_errors(); show_errors();
setup_close_button(); setup_close_button();
@ -332,7 +303,7 @@ fn init_popup_bg() -> Result<Element, EditorError> {
Ok(bg) Ok(bg)
} }
fn chars_left(selector: &str, content: &HtmlElement) -> Option<i32> { fn chars_left(selector: &str, content: &Element) -> Option<i32> {
match document().query_selector(selector) { match document().query_selector(selector) {
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| { Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
if let Some(len) = form if let Some(len) = form
@ -392,35 +363,6 @@ fn make_editable(tag: &'static str) -> Element {
elt elt
} }
fn placeholder(elt: HtmlElement, text: &str) -> HtmlElement {
elt.dataset().insert("placeholder", text).unwrap();
elt.dataset().insert("edited", "false").unwrap();
elt.add_event_listener(mv!(elt => move |_: FocusEvent| {
if elt.dataset().get("edited").unwrap().as_str() != "true" {
clear_children(&elt);
}
}));
elt.add_event_listener(mv!(elt => move |_: BlurEvent| {
if elt.dataset().get("edited").unwrap().as_str() != "true" {
clear_children(&elt);
let ph = document().create_element("span").expect("Couldn't create placeholder");
ph.class_list().add("placeholder").expect("Couldn't add class");
ph.append_child(&document().create_text_node(&elt.dataset().get("placeholder").unwrap_or_default()));
elt.append_child(&ph);
}
}));
elt.add_event_listener(mv!(elt => move |_: KeyUpEvent| {
elt.dataset().insert("edited", if elt.inner_text().trim_matches('\n').is_empty() {
"false"
} else {
"true"
}).expect("Couldn't update edition state");
}));
elt
}
fn clear_children(elt: &HtmlElement) { fn clear_children(elt: &HtmlElement) {
for child in elt.child_nodes() { for child in elt.child_nodes() {
elt.remove_child(&child).unwrap(); elt.remove_child(&child).unwrap();

View File

@ -364,48 +364,54 @@ main .article-meta {
} }
#plume-editor { #plume-editor {
header { margin: 0;
grid: 50px 1fr / 1fr auto 20%;
min-height: 80vh;
& > header {
display: flex; display: flex;
flex-direction: row-reverse;
background: transparent;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
position: fixed;
width: 60%;
padding: 0px 20px; padding: 0px 20px;
border: 1px solid $purple; border-bottom: 1px solid $purple;
margin-top: -100px;
max-height: 90px; max-height: 90px;
background: $background; background: $background;
button { grid-column: 1 / 3;
flex: 0 0 10em; grid-row: 1 / 1;
font-size: 1.25em;
margin: .5em 0em .5em 1em;
}
} }
& > * { #edition-area {
margin: 0;
max-width: none;
max-height: 90vh;
overflow-y: auto;
}
#edition-area > * {
min-height: 1em; min-height: 1em;
outline: none; outline: none;
margin-left: 20%;
margin-bottom: 0.5em; margin-bottom: 0.5em;
padding-right: 5%;
&:empty::before {
content: attr(data-placeholder);
display: none;
color: transparentize($black, 0.6);
cursor: text;
} }
& > h1 { &:empty:not(:focus)::before {
display: inline;
}
}
#edition-area > h1 {
margin-top: 110px; margin-top: 110px;
} }
.placeholder { #edition-area > *[contenteditable] {
color: transparentize($black, 0.6);
}
article {
max-width: none;
min-height: 2em;
}
& > *[contenteditable] {
margin-left: -20px;
padding-left: 18px; padding-left: 18px;
border-left: 2px solid transparent; border-left: 2px solid transparent;
transition: border-left-color 0.1s ease-in; transition: border-left-color 0.1s ease-in;
@ -414,6 +420,34 @@ main .article-meta {
border-left-color: transparentize($black, 0.6); border-left-color: transparentize($black, 0.6);
} }
} }
aside {
background: $gray;
margin: 0;
flex: 0 0 15%;
padding: 0 1em;
grid-row: 1 / 4;
& > * {
display: flex;
flex-direction: column;
}
label {
margin: 2em 0 .5em;
}
button {
font-size: 1.25em;
margin: 0;
}
}
}
body#editor {
footer {
margin-top: 0;
}
} }
.popup { .popup {

View File

@ -12,19 +12,40 @@
@(ctx: BaseContext, title: String, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>, content_len: u64) @(ctx: BaseContext, title: String, blog: Blog, editing: bool, form: &NewPostForm, is_draft: bool, article: Option<Post>, errors: ValidationErrors, medias: Vec<Media>, content_len: u64)
@:base(ctx, title.clone(), {}, {}, { @:base(ctx, title.clone(), {}, {}, {
<h1 id="plume-editor-title" dir="auto">@title</h1>
<div id="plume-editor" style="display: none;" dir="auto"> <div id="plume-editor" style="display: none;" dir="auto">
<header> <header>
<button id="publish" class="button">@i18n!(ctx.1, "Publish")</button>
<p id="char-count">@content_len</p>
<a href="#" id="close-editor">@i18n!(ctx.1, "Classic editor (any changes will be lost)")</a> <a href="#" id="close-editor">@i18n!(ctx.1, "Classic editor (any changes will be lost)")</a>
<p id="char-count">@content_len</p>
</header> </header>
<article id="edition-area">
<h1 contenteditable id="editor-title" data-placeholder="@i18n!(ctx.1, "Type your title")">@form.title</h1>
<h2 contenteditable id="editor-subtitle" data-placeholder="@i18n!(ctx.1, "Type a subtitle or a summary")">@form.subtitle</h2>
<p contenteditable id="editor-default-paragraph" data-placeholder="@i18n!(ctx.1, "Write your article. You can use Markdown.")">@form.content</p>
</article>
<aside id="plume-editor-aside" style="display: none;">
<div id="options-page">
<button id="publish" class="button">@i18n!(ctx.1, "Publish")</button>
@input!(ctx.1, tags (optional text), "Tags, separated by commas", form, errors.clone(), "")
@input!(ctx.1, license (optional text), "License", "Leave it empty to reserve all rights", form, errors.clone(), "")
@:image_select(ctx, "cover", i18n!(ctx.1, "Illustration"), true, medias.clone(), form.cover)
</div> </div>
<div id="publish-page" style="display: none">
<a href="#" id="cancel-publish">@i18n!(ctx.1, "Cancel")</a>
<p>@i18n!(ctx.1, "You are about to publish this article in {}"; &blog.title)</p>
<button id="confirm-publish" class="button">@i18n!(ctx.1, "Confirm publication")</button>
<button id="draft" class="button secondary">@i18n!(ctx.1, "Save as draft")</button>
</div>
</aside>
</div>
@if let Some(article) = article { @if let Some(article) = article {
<form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::update: blog = blog.actor_id, slug = &article.slug)" content-size="@content_len"> <form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::update: blog = blog.actor_id, slug = &article.slug)" content-size="@content_len">
} else { } else {
<form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::new: blog = blog.actor_id)" content-size="@content_len"> <form id="plume-fallback-editor" class="new-post" method="post" action="@uri!(posts::new: blog = blog.actor_id)" content-size="@content_len">
} }
<h1 id="plume-editor-title" dir="auto">@title</h1>
@input!(ctx.1, title (text), "Title", form, errors.clone(), "required") @input!(ctx.1, title (text), "Title", form, errors.clone(), "required")
@input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "") @input!(ctx.1, subtitle (optional text), "Subtitle", form, errors.clone(), "")