Install and use ToastUi markdown editor

Replace all usages of pagedown-bootstrap editor with the new editor.
Add styles to ensure the editor preview matches the final output.
This commit is contained in:
Julia Casamitjana
2024-04-11 10:34:37 +02:00
committed by Dominic Sauer
parent 96f5f1f8d7
commit 9c71c6667a
10 changed files with 362 additions and 37 deletions

View File

@ -0,0 +1,77 @@
/**
* ToastUi editor initializer
*
* This script transforms form textareas created with
* "PagedownFormBuilder" into ToastUi markdown editors.
*
*/
const initializeMarkdownEditors = () => {
const editors = document.querySelectorAll(
'[data-behavior="markdown-editor-widget"]'
);
editors.forEach((editor) => {
const formInput = document.querySelector(`#${editor.dataset.id}`);
if (!editor || !formInput) return;
const toastEditor = new ToastUi({
el: editor,
theme: window.getCurrentTheme(),
initialValue: formInput.value,
placeholder: formInput.placeholder,
extendedAutolinks: true,
linkAttributes: {
target: "_blank",
},
previewHighlight: false,
height: "400px",
autofocus: false,
usageStatistics: false,
language: I18n.locale,
toolbarItems: [
["heading", "bold", "italic"],
["link", "quote", "code", "codeblock"],
["ul", "ol"],
],
initialEditType: "markdown",
events: {
change: () => {
// Keep real form <textarea> in sync
const content = toastEditor.getMarkdown();
formInput.value = content;
},
},
});
// Prevent user from drag'n'dropping images in the editor
toastEditor.removeHook("addImageBlobHook");
// Delegate focus from form input to toast ui editor
formInput.addEventListener("focus", () => {
toastEditor.focus();
});
});
};
const setMarkdownEditorTheme = (theme) => {
const editors = document.querySelectorAll(".toastui-editor-defaultUI");
editors.forEach((editor) => {
const hasDarkTheme = editor.classList.contains("toastui-editor-dark");
if (
(hasDarkTheme && theme === "light") ||
(!hasDarkTheme && theme === "dark")
) {
editor.classList.toggle("toastui-editor-dark");
}
});
};
$(document).on("turbolinks:load", function () {
initializeMarkdownEditors();
});
$(document).on("theme:change", function (event) {
const newTheme = event.detail.currentTheme;
setMarkdownEditorTheme(newTheme);
});

View File

@ -3,7 +3,12 @@ h1 {
margin-bottom: 0.5em;
}
h1, h2, h3, h4, h5, h6 {
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 500;
}
@ -12,26 +17,36 @@ h1, h2, h3, h4, h5, h6 {
color: var(--bs-dark-text-emphasis);
}
a:not(.dropdown-item, .dropdown-toggle, .dropdown-link, .btn, .page-link), .btn-link {
a:not(.dropdown-item, .dropdown-toggle, .dropdown-link, .btn, .page-link),
.btn-link {
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
i.fa-solid, i.fa-regular, i.fa-solid {
i.fa-solid,
i.fa-regular,
i.fa-solid {
margin-right: 0.5em;
}
pre, .output-element {
pre,
.output-element {
background-color: var(--bs-light-bg-subtle);
margin: 0;
padding: .25rem!important;
padding: 0.25rem !important;
border: 1px solid var(--bs-border-color-translucent);
}
blockquote {
border-left: 4px solid var(--bs-secondary-bg);
color: var(--bs-secondary-text-emphasis);
margin: 14px 0;
padding: 0 16px;
}
span.caret {
margin-left: 0.5em;
}
@ -125,19 +140,25 @@ html[data-bs-theme="light"] {
}
@-webkit-keyframes sk-rotateplane {
0% { -webkit-transform: perspective(120px) }
50% { -webkit-transform: perspective(120px) rotateY(180deg) }
100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) }
0% {
-webkit-transform: perspective(120px);
}
50% {
-webkit-transform: perspective(120px) rotateY(180deg);
}
100% {
-webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg);
}
}
@keyframes sk-rotateplane {
0% {
transform: perspective(120px) rotateX(0deg) rotateY(0deg);
-webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)
-webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg);
}
50% {
transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
-webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)
-webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
}
100% {
transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);

View File

@ -1,41 +1,31 @@
# frozen_string_literal: true
class PagedownFormBuilder < ActionView::Helpers::FormBuilder
def pagedown(method, args)
def pagedown(method, args = {})
# Adopt simple form builder to work with form_for
@attribute_name = method
@input_html_options = args[:input_html]
@template.capture do
@template.concat wmd_button_bar
@template.concat wmd_textarea
@template.concat wmd_preview if show_wmd_preview?
@template.concat form_textarea
@template.concat @template.tag.div(class: 'markdown-editor', data: {behavior: 'markdown-editor-widget', id: label_target})
end
end
private
def wmd_button_bar
@template.tag.div(nil, id: "wmd-button-bar-#{base_id}")
end
def wmd_textarea
def form_textarea
@template.text_area @object_name, @attribute_name,
**@input_html_options,
class: 'form-control wmd-input',
id: "wmd-input-#{base_id}"
end
def wmd_preview
@template.tag.div(nil, class: 'wmd-preview',
id: "wmd-preview-#{base_id}")
end
def show_wmd_preview?
@input_html_options[:preview].present?
**(@input_html_options || {}),
id: label_target,
class: 'd-none'
end
def base_id
options[:pagedown_id_suffix] || @attribute_name
options[:markdown_id_suffix] || @attribute_name
end
def label_target
"markdown-input-#{base_id}"
end
end

View File

@ -0,0 +1,7 @@
/* eslint no-console:0 */
// JS
import ToastUi from "@toast-ui/editor";
// Import German locales (english ones are included by default)
import "@toast-ui/editor/dist/i18n/de-de";
window.ToastUi = ToastUi

View File

@ -0,0 +1,71 @@
@import "@toast-ui/editor/dist/toastui-editor.css";
@import "@toast-ui/editor/dist/theme/toastui-editor-dark.css";
.markdown-editor {
* {
box-sizing: content-box;
}
}
// /*------------------------------------*\
// $ToastUI overrides
// \*------------------------------------*/
// Overrides for the preview section to match real output styles
.toastui-editor-contents {
font-size: 14px;
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0.5em 0 0.5em 0;
padding: 0;
border-bottom: 0;
font-weight: normal;
}
blockquote {
border-left: 4px solid var(--bs-secondary-bg);
p {
color: var(--bs-secondary-text-emphasis);
}
}
ul > li::before {
content: none;
}
ul {
list-style-type: disc;
}
ul ul {
list-style-type: circle;
}
ol > li::before {
color: black;
}
code {
color: var(--bs-code-color);
background-color: transparent !important;
}
pre {
border: 1px solid var(--bs-border-color-translucent);
}
del {
color: var(--bs-secondary-color);
}
}
// Ensure multiline backquotes have same text color in "write" mode
.toastui-editor-md-marked-text,
.toastui-editor-md-block-quote {
color: var(--bs-secondary-text-emphasis);
}

View File

@ -3,6 +3,8 @@
Otherwise, code might not be highlighted correctly (race condition)
meta name='turbolinks-visit-control' content='reload'
- append_javascript_pack_tag('sortable')
- append_javascript_pack_tag('toast-ui')
- append_stylesheet_pack_tag('toast-ui')
- execution_environments = ExecutionEnvironment.where.not(file_type_id: nil).select(:file_type_id, :id)
- file_types = FileType.where.not(file_extension: nil).select(:file_extension, :id)
@ -18,7 +20,7 @@
.help-block.form-text == t('.hints.internal_title')
.mb-3
= f.label(:description, class: 'form-label')
= f.pagedown :description, input_html: {preview: true, rows: 10}
= f.pagedown :description
.mb-3
= f.label(:execution_environment_id, class: 'form-label')
= f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {include_blank: t('exercises.form.none')}, class: 'form-control')

View File

@ -1,3 +1,10 @@
- content_for :head do
// Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326.
Otherwise, code might not be highlighted correctly (race condition)
meta name='turbolinks-visit-control' content='reload'
- append_javascript_pack_tag('toast-ui')
- append_stylesheet_pack_tag('toast-ui')
= form_for(@proxy_exercise, multipart: true, builder: PagedownFormBuilder) do |f|
= render('shared/form_errors', object: @proxy_exercise)
.mb-3
@ -5,7 +12,7 @@
= f.text_field(:title, class: 'form-control', required: true)
.mb-3
= f.label(:description, class: 'form-label')
= f.pagedown :description, input_html: {preview: true, rows: 10}
= f.pagedown :description
.mb-3
= f.label(:algorithm, class: 'form-label')
= f.collection_select(:algorithm, ProxyExercise.algorithms.map {|algorithm, _id| [t("activerecord.attributes.proxy_exercise.algorithm_type.#{algorithm}"), algorithm] }, :second, :first, {}, class: 'form-control form-control-sm')

View File

@ -1,3 +1,10 @@
- content_for :head do
// Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326.
Otherwise, code might not be highlighted correctly (race condition)
meta name='turbolinks-visit-control' content='reload'
- append_javascript_pack_tag('toast-ui')
- append_stylesheet_pack_tag('toast-ui')
= form_for(@tip, builder: PagedownFormBuilder) do |f|
= render('shared/form_errors', object: @tip)
.mb-3
@ -5,7 +12,7 @@
= f.text_field(:title, class: 'form-control', required: false)
.mb-3
= f.label(:description, class: 'form-label')
= f.pagedown :description, input_html: {preview: true, rows: 5}
= f.pagedown :description
.mb-3
= f.label(:file_type_id, t('activerecord.attributes.file.file_type_id'), class: 'form-label')
= f.collection_select(:file_type_id, @file_types, :id, :name, {include_blank: true}, class: 'form-control')