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(
|
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();
|
||||||
|
@ -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;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > * {
|
#edition-area > h1 {
|
||||||
min-height: 1em;
|
|
||||||
outline: none;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > 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 {
|
||||||
|
@ -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 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>
|
</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(), "")
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user