Skip to content

Instantly share code, notes, and snippets.

@foxy4096
Last active December 12, 2023 11:40
Show Gist options
  • Save foxy4096/ee9e7ed0be82814a9d56af52b149a5c8 to your computer and use it in GitHub Desktop.
Save foxy4096/ee9e7ed0be82814a9d56af52b149a5c8 to your computer and use it in GitHub Desktop.
Snippets
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css");
.emojione {
width: 30px;
padding-left: 6px;
display: inline-flex;
vertical-align: text-top;
}
.tabbed-set {
position: relative;
display: flex;
flex-wrap: wrap;
margin: 1em 0;
border-radius: 0.1rem;
}
.tabbed-set > input {
display: none;
}
.tabbed-set label {
width: auto;
padding: 0.9375em 1.25em 0.78125em;
font-weight: 700;
font-size: 0.84em;
white-space: nowrap;
border-bottom: 0.15rem solid transparent;
border-top-left-radius: 0.1rem;
border-top-right-radius: 0.1rem;
cursor: pointer;
transition: background-color 250ms, color 250ms;
}
.tabbed-set .tabbed-content {
width: 100%;
display: none;
box-shadow: 0 -0.05rem #ddd;
}
.tabbed-set input {
position: absolute;
opacity: 0;
}
.tabbed-set input:checked:nth-child(n + 1) + label {
color: rgb(0, 140, 255);
border-color: rgb(0, 140, 255);
}
@media screen {
.tabbed-set input:nth-child(n + 1):checked + label + .tabbed-content {
order: 99;
display: block;
}
}
@media print {
.tabbed-content {
display: contents;
}
}
#mdout {
background: #0f0f0f;
border: 1px #363636 solid;
border-top: none;
padding: 1em;
max-height: 200px; /* Set your desired max height */
overflow-x: auto;
word-break: break-all;
}
#mdout:empty {
display: none;
}
.button,
input,
textarea,
select {
border-radius: 0% !important;
}
.modal-background {
background-color: rgba(44, 44, 44, 0.493) !important;
}
.editor-toolbar {
display: flex;
flex-wrap: wrap;
border: 1px #363636 solid;
border-bottom: none;
justify-content: flex-start;
background-color: #0a0a0a;
padding: 10px;
}
#editor {
box-sizing: border-box;
border-top: none;
box-shadow: none;
width: 100% !important;
resize: both;
}
.editor-button {
padding: 5px 10px;
cursor: pointer;
margin-right: 10px;
background-color: inherit;
color: #fff;
border: none;
border-radius: 5px;
margin-bottom: 5px;
font-size: large;
}
.editor-button:hover {
background-color: #3b414d;
}
#editor:hover {
border-top: none !important;
box-shadow: none !important;
}
#mdout > ul {
margin-left: 5px !important;
}
#mdout > ul > li {
list-style: disc !important;
}
#mdout, #editor {
width: 100%; /* Allow both elements to expand to their container's width */
box-sizing: border-box; /* Include padding and borders in the width calculation */
}
from django.utils.safestring import mark_safe
from django.forms.widgets import Textarea
from django.template.loader import render_to_string
from apps.core.templatetags.convert_markdown import convert_markdown
from apps.account.templatetags.user_mention import user_mention
class MarkdownWidget(Textarea):
"""
Custom widget for a textarea with HTMX attributes for live Markdown conversion.
Additionally, it wraps the textarea in a <div> container.
Args:
hx_vars (str, optional): The value of the `hx-vars` attribute for HTMX configuration.
*args: Additional positional arguments passed to the parent class constructor.
**kwargs: Additional keyword arguments passed to the parent class constructor.
Attributes:
hx_vars (str): The value of the `hx-vars` attribute for HTMX configuration.
Usage:
This widget can be used to render a textarea with HTMX attributes for live Markdown conversion.
Example:
```
bio_widget = MarkdownWidget(
hx_vars="name:'bio'"
)
```
"""
def __init__(
self,
*args,
**kwargs,
):
super().__init__(
*args,
**kwargs,
)
self.attrs.update(
{
"hx-target": "#mdout",
"hx-trigger": "keyup delay:500ms changed, insertMarkdown",
"hx-post": "/convert/md/",
"style": "font-family: monospace; width: 650px; height: 200px;",
}
)
def render(self, name, value, attrs=None, renderer=None):
"""
Render the widget.
Args:
name (str): The name of the HTML input element.
value (str): The current value of the input.
attrs (dict, optional): Additional HTML attributes for the input element.
renderer (str, optional): The renderer for rendering the widget.
Returns:
str: The HTML rendering of the widget.
"""
attrs.update({"hx-vars": f"name:'{name}'", "id": f"id_{name}"})
rendered_textarea = super().render(name, value, attrs)
toolbar = render_to_string("widgets/markdown_toolbar.html", {"name": name})
initials = user_mention(convert_markdown(value or ""))
div_element = f'<div class="content" id="mdout">{initials}</div>'
return mark_safe(f"<div>{toolbar}{rendered_textarea} {div_element}</div>")
@foxy4096
Copy link
Author

How do I configure it in my project?

Firstly add the htmx and main.css in the static folder and then link them in the html.

Then create a file widgets/markdown_toolbar.html and it should look like this

<div class="editor-toolbar">
        <button class="editor-button" type="button" onclick="insertMarkdown('**', '**')"><i
                        class="bi bi-type-bold"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('_', '_')"><i
                        class="bi bi-type-italic"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('## ', '')"><i
                        class="bi bi-type-h2"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('### ', '')"><i
                        class="bi bi-type-h3"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('1. ', '')"><i
                        class="bi bi-list-ol"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('- ', '')"><i
                        class="bi bi-list-ul"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('> ', '')"><i
                        class="bi bi-quote"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('`', '`')"><i
                        class="bi bi-code"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('```\n', '\n```')"><i
                        class="bi bi-file-earmark-code-fill"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('[Link Text](', ')')"><i
                        class="bi bi-link-45deg"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('![Alt Text](', ')')"><i
                        class="bi bi-image"></i></button>
        <button class="editor-button" type="button" onclick="insertMarkdown('---', '')"><i
                        class="bi bi-hr"></i></button>
</div>
<script>
        function insertMarkdown(startTag, endTag) {
                const editor = document.getElementById('id_{{ name }}');
                const selectionStart = editor.selectionStart;
                const selectionEnd = editor.selectionEnd;
                const selectedText = editor.value.substring(selectionStart, selectionEnd);
                const replacement = `${startTag}${selectedText}${endTag}`;
                editor.value = editor.value.substring(0, selectionStart) + replacement + editor.value.substring(selectionEnd);

                // Set the cursor position between the start and end tags
                editor.selectionStart = selectionStart + startTag.length;
                editor.selectionEnd = selectionStart + startTag.length + selectedText.length;

                editor.focus();

        }
</script>

Then in the forms.py you can use the markdown widget

from django import forms
from .models import Post

from apps.core.widgets import MarkdownWidget

class PostCreationForm(forms.ModelForm):
    """
    Form for creating a new post.
    """

    class Meta:
        model = Post
        fields = ["body", "status"]
        widgets = {
            "body": MarkdownWidget(),
        }

For using in the admin panel the admin.py should look like this

class PostAdmin(admin.ModelAdmin):
    # --Snippet--
    formfield_overrides = {models.TextField: {"widget": MarkdownWidget()}}
    class Media:
        css = {
            "all": ["css/main.css"],
        }
        js = ["js/htmx.js"]

Admin static files ref

@foxy4096
Copy link
Author

Even though instead of adding the static files from ModelAdmin, I manually overided the admin/base_site.html to add the main.css and htmx.js.

@Richie003
Copy link

Okay, thanks
I'll try this out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment