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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user