WIP: Feature: Markdown preview for markdown-supported fields #1
78
common/static/common/scripts/markdown-preview.js
Normal file
78
common/static/common/scripts/markdown-preview.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
const activeTabCls = "is-active"
|
||||||
|
|
||||||
|
function initMarkdownPreview(fieldName, texts) {
|
||||||
|
const field = document.querySelector(`textarea.form-control[name="${fieldName}"]`);
|
||||||
|
const markdown = document.querySelector(`#markdown-preview-${fieldName}`);
|
||||||
|
const tokenInput = document.querySelector(`input[name=csrfmiddlewaretoken]`)
|
||||||
|
const writeTab = document.querySelector(`#tab-write-${fieldName}`);
|
||||||
|
const previewTab = document.querySelector(`#tab-preview-${fieldName}`);
|
||||||
|
const write = document.querySelector(`#write-${fieldName}`);
|
||||||
|
const preview = document.querySelector(`#preview-${fieldName}`);
|
||||||
|
let lastText; // To avoid calling multiple times unchanged text
|
||||||
|
|
||||||
|
function toggleActiveTab() {
|
||||||
|
writeTab.classList.toggle(activeTabCls);
|
||||||
|
previewTab.classList.toggle(activeTabCls);
|
||||||
|
}
|
||||||
|
|
||||||
|
function opacity_75(text) {
|
||||||
|
return `<span class="opacity-75">${text}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nothingToPreview() {
|
||||||
|
markdown.innerHTML = opacity_75(texts.nothing_to_preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTabs(on, off) {
|
||||||
|
on.style.display = "block";
|
||||||
|
off.style.display = "none";
|
||||||
|
toggleActiveTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewTab && field) {
|
||||||
|
previewTab.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!previewTab.classList.contains(activeTabCls)) {
|
||||||
|
switchTabs(preview, write)
|
||||||
|
if (field.value !== lastText) {
|
||||||
|
markdown.innerHTML = opacity_75(texts.loading);
|
||||||
|
lastText = field.value;
|
||||||
|
if (field.value.trim()) {
|
||||||
|
fetch("/markdown/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-CSRFToken": tokenInput.value,
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
text: field.value,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
const preview_markdown = data.markdown;
|
||||||
|
if (preview_markdown.trim()) {
|
||||||
|
markdown.innerHTML = preview_markdown;
|
||||||
|
} else {
|
||||||
|
nothingToPreview();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
lastText = undefined;
|
||||||
|
markdown.innerHTML = opacity_75(texts.error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
nothingToPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (writeTab) {
|
||||||
|
writeTab.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!writeTab.classList.contains(activeTabCls)) {
|
||||||
|
switchTabs(write, preview)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
{% load common %}
|
{% load i18n common %}
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
{% with type=field.field.widget.input_type classes=classes|default:"" placeholder=placeholder|default:"" %}
|
{% with type=field.field.widget.input_type classes=classes|default:"" placeholder=placeholder|default:"" %}
|
||||||
{% with field=field|remove_cols_rows|add_classes:classes|set_placeholder:placeholder %}
|
{% with field=field|remove_cols_rows|add_classes:classes|set_placeholder:placeholder %}
|
||||||
|
{% with is_markdown=field.name|is_markdown_field %}
|
||||||
{% autoescape off %}
|
{% autoescape off %}
|
||||||
{% firstof label field.label as label %}
|
{% firstof label field.label as label %}
|
||||||
{% firstof help_text field.help_text as help_text %}
|
{% firstof help_text field.help_text as help_text %}
|
||||||
@ -27,6 +28,16 @@
|
|||||||
{% if field.field.required or required %}<span class="form-required-indicator">*</span>{% endif %}
|
{% if field.field.required or required %}<span class="form-required-indicator">*</span>{% endif %}
|
||||||
</label>
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if is_markdown and not field.is_hidden %}
|
||||||
|
<div class="hero-tabs mb-2">
|
||||||
|
<a id="tab-write-{{field.name}}" href="#write-{{field.name}}" class="is-active">{% trans 'Write' %}</a>
|
||||||
|
<a id="tab-preview-{{field.name}}" href="#preview-{{field.name}}">{% trans 'Preview' %}</a>
|
||||||
|
</div>
|
||||||
|
<section id="preview-{{field.name}}" style="display: none;border-bottom: thin solid rgba(255, 255, 255, 0.1);">
|
||||||
|
<div id="markdown-preview-{{field.name}}" class="style-rich-text px-3 pt-2 pb-3">{% trans 'Loading...' %}</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if icon %}
|
{% if icon %}
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
@ -36,7 +47,9 @@
|
|||||||
{{ field }}
|
{{ field }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field }}
|
<section id="write-{{field.name}}">
|
||||||
|
{{ field }}
|
||||||
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -47,6 +60,22 @@
|
|||||||
{% if field.errors %}
|
{% if field.errors %}
|
||||||
<div class="invalid-feedback">{{ field.errors }}</div>
|
<div class="invalid-feedback">{{ field.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% if is_markdown and not field.is_hidden %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initMarkdownPreview("{{field.name}}", {
|
||||||
|
loading: "{% trans 'Loading...' %}",
|
||||||
|
error: "{% trans 'Error getting preview...' %}",
|
||||||
|
nothing_to_preview: "{% trans 'Nothing to preview...' %}"
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock scripts %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
|
@ -230,3 +230,7 @@ def to_int(value):
|
|||||||
return int(value)
|
return int(value)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def is_markdown_field(name: str) -> bool:
|
||||||
|
return name in ['description', 'release_notes', 'message']
|
||||||
|
@ -21,6 +21,7 @@ urlpatterns = [
|
|||||||
name='api-internal',
|
name='api-internal',
|
||||||
),
|
),
|
||||||
# Public API
|
# Public API
|
||||||
|
path('markdown/', api.MarkdownRenderApi.as_view(), name='markdown-rendering'),
|
||||||
path('api/v1/extensions/', api.ExtensionsAPIView.as_view(), name='api'),
|
path('api/v1/extensions/', api.ExtensionsAPIView.as_view(), name='api'),
|
||||||
path(
|
path(
|
||||||
'api/v1/extensions/<str:extension_id>/versions/upload/',
|
'api/v1/extensions/<str:extension_id>/versions/upload/',
|
||||||
|
@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
@ -13,6 +14,7 @@ from rest_framework.views import APIView
|
|||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from common.compare import is_in_version_range, version
|
from common.compare import is_in_version_range, version
|
||||||
|
from common.markdown import render as render_markdown
|
||||||
from extensions.models import Extension, Platform
|
from extensions.models import Extension, Platform
|
||||||
from extensions.utils import clean_json_dictionary_from_optional_fields
|
from extensions.utils import clean_json_dictionary_from_optional_fields
|
||||||
from files.forms import FileFormSkipAgreed
|
from files.forms import FileFormSkipAgreed
|
||||||
@ -273,3 +275,10 @@ def extensions_awaiting_review(request):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return JsonResponse(response, safe=False)
|
return JsonResponse(response, safe=False)
|
||||||
|
|
||||||
|
class MarkdownRenderApi(View):
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
text = request.POST.get('text')
|
||||||
|
rendered_markdown = render_markdown(text)
|
||||||
|
return JsonResponse({'markdown': rendered_markdown})
|
||||||
|
Loading…
Reference in New Issue
Block a user