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:
parent
5d03331f0c
commit
4142e73018
@ -63,33 +63,7 @@ impl From<stdweb::private::ConversionError> for EditorError {
|
||||
}
|
||||
}
|
||||
|
||||
fn init_widget(
|
||||
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) {
|
||||
fn filter_paste(elt: &Element) {
|
||||
// Only insert text when pasting something
|
||||
js! {
|
||||
@{&elt}.addEventListener("paste", function (evt) {
|
||||
@ -127,43 +101,32 @@ pub fn init() -> Result<(), EditorError> {
|
||||
|
||||
fn init_editor() -> Result<(), EditorError> {
|
||||
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
|
||||
js! { @{&ed}.style.display = "block"; };
|
||||
js! {
|
||||
@{&ed}.style.display = "grid";
|
||||
@{&aside}.style.display = "block";
|
||||
};
|
||||
// And hide the HTML-only fallback
|
||||
let old_ed = document().get_element_by_id("plume-fallback-editor")?;
|
||||
let old_title = document().get_element_by_id("plume-editor-title")?;
|
||||
js! {
|
||||
@{&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
|
||||
let title = init_widget(
|
||||
&ed,
|
||||
"h1",
|
||||
i18n!(CATALOG, "Enter your title"),
|
||||
title_val,
|
||||
true,
|
||||
)?;
|
||||
let subtitle = init_widget(
|
||||
&ed,
|
||||
"h2",
|
||||
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}; };
|
||||
let title = document().get_element_by_id("editor-title")?;
|
||||
let subtitle = document().get_element_by_id("editor-subtitle")?;
|
||||
let content = document().get_element_by_id("editor-default-paragraph")?;
|
||||
|
||||
title.add_event_listener(no_return);
|
||||
subtitle.add_event_listener(no_return);
|
||||
|
||||
filter_paste(&title);
|
||||
filter_paste(&subtitle);
|
||||
filter_paste(&content);
|
||||
|
||||
// character counter
|
||||
content.add_event_listener(mv!(content => move |_: KeyDownEvent| {
|
||||
@ -178,19 +141,27 @@ fn init_editor() -> Result<(), EditorError> {
|
||||
}), 0);
|
||||
}));
|
||||
|
||||
document().get_element_by_id("publish")?.add_event_listener(
|
||||
mv!(title, subtitle, content, old_ed => move |_: ClickEvent| {
|
||||
let popup = document().get_element_by_id("publish-popup").or_else(||
|
||||
init_popup(&title, &subtitle, &content, &old_ed).ok()
|
||||
).unwrap();
|
||||
let bg = document().get_element_by_id("popup-bg").or_else(||
|
||||
init_popup_bg().ok()
|
||||
).unwrap();
|
||||
document()
|
||||
.get_element_by_id("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! {
|
||||
@{&options_page}.style.display = "none";
|
||||
@{&publish_page}.style.display = "flex";
|
||||
};
|
||||
});
|
||||
|
||||
popup.class_list().add("show").unwrap();
|
||||
bg.class_list().add("show").unwrap();
|
||||
}),
|
||||
);
|
||||
document()
|
||||
.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();
|
||||
setup_close_button();
|
||||
@ -332,7 +303,7 @@ fn init_popup_bg() -> Result<Element, EditorError> {
|
||||
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) {
|
||||
Ok(Some(form)) => HtmlElement::try_from(form).ok().and_then(|form| {
|
||||
if let Some(len) = form
|
||||
@ -392,35 +363,6 @@ fn make_editable(tag: &'static str) -> Element {
|
||||
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) {
|
||||
for child in elt.child_nodes() {
|
||||
elt.remove_child(&child).unwrap();
|
||||
|
@ -364,48 +364,54 @@ main .article-meta {
|
||||
}
|
||||
|
||||
#plume-editor {
|
||||
header {
|
||||
margin: 0;
|
||||
grid: 50px 1fr / 1fr auto 20%;
|
||||
min-height: 80vh;
|
||||
|
||||
& > header {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
background: transparent;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: fixed;
|
||||
width: 60%;
|
||||
padding: 0px 20px;
|
||||
border: 1px solid $purple;
|
||||
margin-top: -100px;
|
||||
border-bottom: 1px solid $purple;
|
||||
max-height: 90px;
|
||||
background: $background;
|
||||
|
||||
button {
|
||||
flex: 0 0 10em;
|
||||
font-size: 1.25em;
|
||||
margin: .5em 0em .5em 1em;
|
||||
grid-column: 1 / 3;
|
||||
grid-row: 1 / 1;
|
||||
}
|
||||
|
||||
#edition-area {
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#edition-area > * {
|
||||
min-height: 1em;
|
||||
outline: none;
|
||||
margin-left: 20%;
|
||||
margin-bottom: 0.5em;
|
||||
padding-right: 5%;
|
||||
|
||||
&:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
display: none;
|
||||
color: transparentize($black, 0.6);
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&:empty:not(:focus)::before {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
& > * {
|
||||
min-height: 1em;
|
||||
outline: none;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
& > h1 {
|
||||
#edition-area > h1 {
|
||||
margin-top: 110px;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: transparentize($black, 0.6);
|
||||
}
|
||||
|
||||
article {
|
||||
max-width: none;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
& > *[contenteditable] {
|
||||
margin-left: -20px;
|
||||
#edition-area > *[contenteditable] {
|
||||
padding-left: 18px;
|
||||
border-left: 2px solid transparent;
|
||||
transition: border-left-color 0.1s ease-in;
|
||||
@ -414,6 +420,34 @@ main .article-meta {
|
||||
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 {
|
||||
|
@ -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)
|
||||
|
||||
@:base(ctx, title.clone(), {}, {}, {
|
||||
<h1 id="plume-editor-title" dir="auto">@title</h1>
|
||||
<div id="plume-editor" style="display: none;" dir="auto">
|
||||
<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>
|
||||
<p id="char-count">@content_len</p>
|
||||
</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 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 {
|
||||
<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 {
|
||||
<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, subtitle (optional text), "Subtitle", form, errors.clone(), "")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user