Commit 3898a13b authored by Skia's avatar Skia

Merge branch 'makdown-editor' into 'master'

Add a nice markdown editor

See merge request !184
parents d0771f3e 3dda8eaf
Pipeline #1676 passed with stage
in 10 minutes and 8 seconds
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from 'core/macros_pages.jinja' import markdown_preview_script, page_edit_form %} {% from 'core/macros_pages.jinja' import page_edit_form %}
{% block head %}
{{ super() }}
{{ markdown_preview_script(csrf_token) }}
{% endblock %}
{% block content %} {% block content %}
{{ page_edit_form(page, form, url('club:club_edit_page', club_id=page.club.id), csrf_token) }} {{ page_edit_form(page, form, url('club:club_edit_page', club_id=page.club.id), csrf_token) }}
......
...@@ -49,7 +49,7 @@ from core.views import ( ...@@ -49,7 +49,7 @@ from core.views import (
CanCreateMixin, CanCreateMixin,
QuickNotifMixin, QuickNotifMixin,
) )
from core.views.forms import SelectDateTime from core.views.forms import SelectDateTime, MarkdownInput
from core.models import Notification, RealGroup, User from core.models import Notification, RealGroup, User
from club.models import Club, Mailing from club.models import Club, Mailing
...@@ -167,19 +167,25 @@ class ComEditView(ComTabsMixin, CanEditPropMixin, UpdateView): ...@@ -167,19 +167,25 @@ class ComEditView(ComTabsMixin, CanEditPropMixin, UpdateView):
class AlertMsgEditView(ComEditView): class AlertMsgEditView(ComEditView):
fields = ["alert_msg"] form_class = modelform_factory(
Sith, fields=["alert_msg"], widgets={"alert_msg": MarkdownInput}
)
current_tab = "alert" current_tab = "alert"
success_url = reverse_lazy("com:alert_edit") success_url = reverse_lazy("com:alert_edit")
class InfoMsgEditView(ComEditView): class InfoMsgEditView(ComEditView):
fields = ["info_msg"] form_class = modelform_factory(
Sith, fields=["info_msg"], widgets={"info_msg": MarkdownInput}
)
current_tab = "info" current_tab = "info"
success_url = reverse_lazy("com:info_edit") success_url = reverse_lazy("com:info_edit")
class IndexEditView(ComEditView): class IndexEditView(ComEditView):
fields = ["index_page"] form_class = modelform_factory(
Sith, fields=["index_page"], widgets={"index_page": MarkdownInput}
)
current_tab = "index" current_tab = "index"
success_url = reverse_lazy("com:index_edit") success_url = reverse_lazy("com:index_edit")
...@@ -197,7 +203,12 @@ class NewsForm(forms.ModelForm): ...@@ -197,7 +203,12 @@ class NewsForm(forms.ModelForm):
class Meta: class Meta:
model = News model = News
fields = ["type", "title", "club", "summary", "content", "author"] fields = ["type", "title", "club", "summary", "content", "author"]
widgets = {"author": forms.HiddenInput, "type": forms.RadioSelect} widgets = {
"author": forms.HiddenInput,
"type": forms.RadioSelect,
"summary": MarkdownInput,
"content": MarkdownInput,
}
start_date = forms.DateTimeField( start_date = forms.DateTimeField(
["%Y-%m-%d %H:%M:%S"], ["%Y-%m-%d %H:%M:%S"],
...@@ -461,6 +472,12 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi ...@@ -461,6 +472,12 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
Weekmail, Weekmail,
fields=["title", "intro", "joke", "protip", "conclusion"], fields=["title", "intro", "joke", "protip", "conclusion"],
help_texts={"title": _("Delete and save to regenerate")}, help_texts={"title": _("Delete and save to regenerate")},
widgets={
"intro": MarkdownInput,
"joke": MarkdownInput,
"protip": MarkdownInput,
"conclusion": MarkdownInput,
},
) )
success_url = reverse_lazy("com:weekmail") success_url = reverse_lazy("com:weekmail")
current_tab = "weekmail" current_tab = "weekmail"
...@@ -533,7 +550,11 @@ class WeekmailArticleEditView( ...@@ -533,7 +550,11 @@ class WeekmailArticleEditView(
"""Edit an article""" """Edit an article"""
model = WeekmailArticle model = WeekmailArticle
fields = ["title", "club", "content"] form_class = modelform_factory(
WeekmailArticle,
fields=["title", "club", "content"],
widgets={"content": MarkdownInput},
)
pk_url_kwarg = "article_id" pk_url_kwarg = "article_id"
template_name = "core/edit.jinja" template_name = "core/edit.jinja"
success_url = reverse_lazy("com:weekmail") success_url = reverse_lazy("com:weekmail")
...@@ -545,7 +566,11 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView): ...@@ -545,7 +566,11 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
"""Post an article""" """Post an article"""
model = WeekmailArticle model = WeekmailArticle
fields = ["title", "club", "content"] form_class = modelform_factory(
WeekmailArticle,
fields=["title", "club", "content"],
widgets={"content": MarkdownInput},
)
template_name = "core/create.jinja" template_name = "core/create.jinja"
success_url = reverse_lazy("core:user_tools") success_url = reverse_lazy("core:user_tools")
quick_notif_url_arg = "qn_weekmail_new_article" quick_notif_url_arg = "qn_weekmail_new_article"
......
...@@ -43,3 +43,22 @@ $( function() { ...@@ -43,3 +43,22 @@ $( function() {
function display_notif() { function display_notif() {
$('#header_notif').toggle().parent().toggleClass("white"); $('#header_notif').toggle().parent().toggleClass("white");
} }
// You can't get the csrf token from the template in a widget
// We get it from a cookie as a workaround, see this link
// https://docs.djangoproject.com/en/2.0/ref/csrf/#ajax
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i <ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
...@@ -1644,26 +1644,6 @@ label { ...@@ -1644,26 +1644,6 @@ label {
} }
} }
.markdown_editor {
margin-top: 5px;
}
.markdown_editor a {
border: solid 1px $black-color;
padding: 2px;
min-width: 1em;
display: inline-block;
text-align: center;
margin: 0px 1px;
}
.markdown_editor a:hover {
text-decoration: none;
cursor: pointer;
box-shadow: 0px 0px 1px 1px $secondary-light-color;
transition: all 0.1s linear;
}
/*--------------------------------JQuery-------------------------------*/ /*--------------------------------JQuery-------------------------------*/
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header
......
...@@ -16,6 +16,11 @@ ...@@ -16,6 +16,11 @@
{% else %} {% else %}
<link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}"> <link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}">
{% endif %} {% endif %}
<!-- Jquery declared here to be accessible in every django widgets -->
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
<!-- Put here to always have acces to those functions on django widgets -->
<script src="{{ static('core/js/script.js') }}"></script>
{% endblock %} {% endblock %}
</head> </head>
...@@ -248,14 +253,12 @@ ...@@ -248,14 +253,12 @@
{% endblock %} {% endblock %}
--> -->
{% block script %} {% block script %}
<script src="{{ static('core/js/jquery-3.1.0.min.js') }}"></script>
<script src="{{ static('core/js/ui/jquery-ui.min.js') }}"></script> <script src="{{ static('core/js/ui/jquery-ui.min.js') }}"></script>
<script src="{{ static('core/js/ui/i18n/datepicker-fr.js') }}"></script> <script src="{{ static('core/js/ui/i18n/datepicker-fr.js') }}"></script>
<script src="{{ static('core/js/jquery.datetimepicker.full.min.js') }}"></script> <script src="{{ static('core/js/jquery.datetimepicker.full.min.js') }}"></script>
<script src="{{ static('core/js/multiple-select.js') }}"></script> <script src="{{ static('core/js/multiple-select.js') }}"></script>
<script src="{{ static('ajax_select/js/ajax_select.js') }}"></script> <script src="{{ static('ajax_select/js/ajax_select.js') }}"></script>
<script src="{{ url('javascript-catalog') }}"></script> <script src="{{ url('javascript-catalog') }}"></script>
<script src="{{ static('core/js/script.js') }}"></script>
<script> <script>
$('.select_single').multipleSelect({ $('.select_single').multipleSelect({
single: true, single: true,
...@@ -289,73 +292,8 @@ $(document).keydown(function (e) { ...@@ -289,73 +292,8 @@ $(document).keydown(function (e) {
jQuery.datetimepicker.setLocale('{{ request.LANGUAGE_CODE|lower }}'); jQuery.datetimepicker.setLocale('{{ request.LANGUAGE_CODE|lower }}');
$('.select_datetime').datetimepicker({ $('.select_datetime').datetimepicker({
format: 'Y-m-d H:i:s', format: 'Y-m-d H:i:s',
});
function add_syntax(e, choice) {
ta = $(e).parent().children('textarea')[0];
ta.focus();
var start = ta.selectionStart;
var end = ta.selectionEnd;
var before = ta.value.substring(0, start);
var after = ta.value.substring(end);
var between = ta.value.substring(start, end);
switch (choice) {
case "bold":
ta.value = before + "**" + between + "**" + after;
ta.selectionEnd = end + 2;
break;
case "italic":
ta.value = before + "*" + between + "*" + after;
ta.selectionEnd = end + 1;
break;
case "underline":
ta.value = before + "__" + between + "__" + after;
ta.selectionEnd = end + 2;
break;
case "strike":
ta.value = before + "~~" + between + "~~" + after;
ta.selectionEnd = end + 2;
break;
case "sub":
ta.value = before + "<sub>" + between + "</sub>" + after;
ta.selectionEnd = end + 5;
break;
case "sup":
ta.value = before + "<sup>" + between + "</sup>" + after;
ta.selectionEnd = end + 5;
break;
case "link":
if (between === "") {
between = "https://";
}
name = "{% trans %}name{% endtrans %}";
ta.value = before + "[" + name + "](" + between + ")" + after;
ta.selectionStart = start + 1;
ta.selectionEnd = start + 1 + name.length;
break;
case "image":
if (between === "") {
between = "{% trans %}https://path/to/image.gif{% endtrans %}";
}
alt = "{% trans %}alternative text{% endtrans %}";
ta.value = before + "![" + alt + "](" + between + "?42% \"{% trans %}Title{% endtrans %}\")" + after;
ta.selectionStart = start + 2;
ta.selectionEnd = start + 2 + alt.length;
break;
}
}
$(document).ready(function() {
editor = $('.markdown_editor');
editor.prepend('<a onclick="javascript:add_syntax(this, \'image\')">{% trans %}Image{% endtrans %}</a>');
editor.prepend('<a onclick="javascript:add_syntax(this, \'link\')">{% trans %}Link{% endtrans %}</a>');
editor.prepend('<a onclick="javascript:add_syntax(this, \'sup\')"><sup>{% trans %}sup{% endtrans %}</sup></a>');
editor.prepend('<a onclick="javascript:add_syntax(this, \'sub\')"><sub>{% trans %}sub{% endtrans %}</sub></a>');
editor.prepend('<a onclick="javascript:add_syntax(this, \'strike\')"><del>{% trans %}S{% endtrans %}</del></a>');
editor.prepend('<a onclick="javascript:add_syntax(this, \'underline\')"><u>{% trans %}U{% endtrans %}</u></a>');
editor.prepend('<a onclick="javascript:add_syntax(this, \'italic\')"><i>{% trans %}I{% endtrans %}</i></a>');
editor.prepend('<a onclick="javascript:add_syntax(this, \'bold\')"><b>{% trans %}B{% endtrans %}</b></a>');
}); });
</script> </script>
{% endblock %} {% endblock %}
</body> </body>
......
...@@ -22,29 +22,6 @@ ...@@ -22,29 +22,6 @@
<form action="{{ url }}" method="post"> <form action="{{ url }}" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ token }}"> <input type="hidden" name="csrfmiddlewaretoken" value="{{ token }}">
{{ form.as_p() }} {{ form.as_p() }}
{{ markdown_preview_button() }}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form> </form>
<div id="preview" class="page_content">
</div>
{% endmacro %} {% endmacro %}
{% macro markdown_preview_script(token) %}
<script>
function make_preview() {
text = $("#id_content").val();
console.log("Rendering text: " + text);
$.ajax({
url: "{{ url('api:api_markdown') }}",
method: "POST",
data: { text: text, csrfmiddlewaretoken: "{{ token }}"}
}).done(function (msg) {
$("#preview").html(msg);
});
}
</script>
{% endmacro %}
{% macro markdown_preview_button() %}
<p><input type="button" value="{% trans %}Preview{% endtrans %}" onclick="javascript:make_preview();" /></p>
{% endmacro %}
\ No newline at end of file
<div>
{# Depends on this package https://github.com/sparksuite/simplemde-markdown-editor #}
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
{# The simplemde script can be included twice, it's safe in the code #}
<script src="{{ statics.js }}"> </script>
<script type="text/javascript">
var css = "{{ statics.css }}";
var lastAPICall;
// Only import the css once
if (!document.head.innerHTML.includes(css)){
document.head.innerHTML += '<link rel="stylesheet" href="' + css + '">';
}
// Custom markdown parser
function customMarkdownParser(plainText, preview) {
$.ajax({
url: "{{ markdown_api_url }}",
method: "POST",
data: { text: plainText, csrfmiddlewaretoken: getCookie('csrftoken') },
}).done(function (msg) {
preview.innerHTML = msg;
});
}
// Pretty markdown input
var simplemde = new SimpleMDE({
element: document.getElementById("{{ widget.attrs.id }}"),
spellChecker: false,
previewRender: function(plainText, preview){ // Async method
clearTimeout(lastAPICall);
lastAPICall = setTimeout(function (plainText, preview){
customMarkdownParser(plainText, preview);
}, 300, plainText, preview);
return preview.innerHTML;
},
forceSync: true, // Avoid validation error on generic create view
toolbar: [
{
name: "heading-smaller",
action: SimpleMDE.toggleHeadingSmaller,
className: "fa fa-header",
title: "{{ translations.heading_smaller }}"
},
{
name: "italic",
action: SimpleMDE.toggleItalic,
className: "fa fa-italic",
title: "{{ translations.italic }}"
},
{
name: "bold",
action: SimpleMDE.toggleBold,
className: "fa fa-bold",
title: "{{ translations.bold }}"
},
{
name: "strikethrough",
action: SimpleMDE.toggleStrikethrough,
className: "fa fa-strikethrough",
title: "{{ translations.strikethrough }}"
},
{
name: "underline",
action: function customFunction(editor){
var cm = editor.codemirror;
cm.replaceSelection('__' + cm.getSelection() + '__');
},
className: "fa fa-underline",
title: "{{ translations.underline }}"
},
{
name: "superscript",
action: function customFunction(editor){
var cm = editor.codemirror;
cm.replaceSelection('<sup>' + cm.getSelection() + '</sup>');
},
className: "fa fa-superscript",
title: "{{ translations.superscript }}"
},
{
name: "subscript",
action: function customFunction(editor){
var cm = editor.codemirror;
cm.replaceSelection('<sub>' + cm.getSelection() + '</sub>');
},
className: "fa fa-subscript",
title: "{{ translations.subscript }}"
},
{
name: "code",
action: SimpleMDE.toggleCodeBlock,
className: "fa fa-code",
title: "{{ translations.code }}"
},
"|",
{
name: "quote",
action: SimpleMDE.toggleBlockquote,
className: "fa fa-quote-left",
title: "{{ translations.quote }}"
},
{
name: "unordered-list",
action: SimpleMDE.toggleUnorderedList,
className: "fa fa-list-ul",
title: "{{ translations.unordered_list }}"
},
{
name: "ordered-list",
action: SimpleMDE.toggleOrderedList,
className: "fa fa-list-ol",
title: "{{ translations.ordered_list }}"
},
"|",
{
name: "link",
action: SimpleMDE.drawLink,
className: "fa fa-link",
title: "{{ translations.link }}"
},
{
name: "image",
action: SimpleMDE.drawImage,
className: "fa fa-picture-o",
title: "{{ translations.image }}"
},
{
name: "table",
action: SimpleMDE.drawTable,
className: "fa fa-table",
title: "{{ translations.table }}"
},
"|",
{
name: "clean-block",
action: SimpleMDE.cleanBlock,
className: "fa fa-eraser fa-clean-block",
title: "{{ translations.clean_block }}"
},
"|",
{
name: "preview",
action: SimpleMDE.togglePreview,
className: "fa fa-eye no-disable",
title: "{{ translations.preview }}"
},
{
name: "side-by-side",
action: SimpleMDE.toggleSideBySide,
className: "fa fa-columns no-disable no-mobile",
title: "{{ translations.side_by_side }}"
},
{
name: "fullscreen",
action: SimpleMDE.toggleFullScreen,
className: "fa fa-arrows-alt no-disable no-mobile",
title: "{{ translations.fullscreen }}"
},
"|",
{
name: "guide",
action: "/page/Aide_sur_la_syntaxe",
className: "fa fa-question-circle",
title: "{{ translations.guide }}"
},
]
});
</script>
</div>
\ No newline at end of file
{% extends "core/page.jinja" %} {% extends "core/page.jinja" %}
{% from 'core/macros_pages.jinja' import markdown_preview_script, page_edit_form %} {% from 'core/macros_pages.jinja' import page_edit_form %}
{% block head %}
{{ super() }}
{{ markdown_preview_script(csrf_token) }}
{% endblock %}
{% block page %} {% block page %}
{{ page_edit_form(page, form, url('core:page_edit', page_name=page.get_full_name()), csrf_token) }} {{ page_edit_form(page, form, url('core:page_edit', page_name=page.get_full_name()), csrf_token) }}
......
...@@ -26,6 +26,8 @@ from django.contrib.auth.forms import UserCreationForm, AuthenticationForm ...@@ -26,6 +26,8 @@ from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.templatetags.static import static
from django.core.urlresolvers import reverse
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms import ( from django.forms import (
CheckboxSelectMultiple, CheckboxSelectMultiple,
...@@ -90,19 +92,38 @@ class SelectDate(DateInput): ...@@ -90,19 +92,38 @@ class SelectDate(DateInput):
class MarkdownInput(Textarea): class MarkdownInput(Textarea):
def render(self, name, value, attrs=None): template_name = "core/markdown_textarea.jinja"
output = (
'<p><a href="%(syntax_url)s">%(help_text)s</a></p>' def get_context(self, name, value, attrs):
'<div class="markdown_editor">%(content)s</div>' context = super(MarkdownInput, self).get_context(name, value, attrs)
% {
"syntax_url": Page.get_page_by_full_name( context["statics"] = {
settings.SITH_CORE_PAGE_SYNTAX "js": static("core/simplemde/simplemde.min.js"),
).get_absolute_url(), "css": static("core/simplemde/simplemde.min.css"),
"help_text": _("Help on the syntax"), }
"content": super(MarkdownInput, self).render(name, value, attrs), context["translations"] = {
} "heading_smaller": _("Heading"),
) "italic": _("Italic"),
return output "bold": _("Bold"),
"strikethrough": _("Strikethrough"),
"underline": _("Underline"),
"superscript": _("Superscript"),
"subscript": _("Subscript"),
"code": _("Code"),
"quote": _("Quote"),
"unordered_list": _("Unordered list"),
"ordered_list": _("Ordered list"),
"image": _("Insert image"),
"link": _("Insert link"),
"table": _("Insert table"),
"clean_block": _("Clean block"),
"preview": _("Toggle preview"),
"side_by_side": _("Toggle side by side"),
"fullscreen": _("Toggle fullscreen"),
"guide": _("Markdown guide"),
}
context["markdown_api_url"] = reverse("api:api_markdown")
return context
class SelectFile(TextInput): class SelectFile(TextInput):
......
...@@ -12,7 +12,7 @@ from django import forms ...@@ -12,7 +12,7 @@ from django import forms
from core.views import CanViewMixin, CanEditMixin, CanCreateMixin from core.views import CanViewMixin, CanEditMixin, CanCreateMixin
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from core.views.forms import SelectDateTime from core.views.forms import SelectDateTime, MarkdownInput
from election.models import Election, Role, Candidature, ElectionList, Vote from election.models import Election, Role, Candidature, ElectionList, Vote
from ajax_select.fields import AutoCompleteSelectField from ajax_select.fields import AutoCompleteSelectField
...@@ -67,7 +67,7 @@ class CandidateForm(forms.ModelForm): ...@@ -67,7 +67,7 @@ class CandidateForm(forms.ModelForm):
class Meta: class Meta:
model = Candidature model = Candidature
fields = ["user", "role", "program", "election_list"] fields = ["user", "role", "program", "election_list"]
widgets = {"program": forms.Textarea} widgets = {"program": MarkdownInput}
user = AutoCompleteSelectField( user = AutoCompleteSelectField(
"users", label=_("User to candidate"), help_text=None, required=True "users", label=_("User to candidate"), help_text=None, required=True
......
...@@ -30,26 +30,8 @@ ...@@ -30,26 +30,8 @@
<form action="" method="post" enctype="multipart/form-data"> <form action="" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_p() }} {{ form.as_p() }}
<p><input type="button" value="{% trans %}Preview{% endtrans %}" onclick="javascript:make_preview();" /></p>
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form> </form>
<div id="preview_message" class="message" style="display: none;">
<div class="msg_author">
{% if user.avatar_pict %}
<img src="{{ user.avatar_pict.get_download_url() }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
{% else %}
<img src="{{ static('core/img/unknown.jpg') }}" alt="{% trans %}Profile{% endtrans %}" id="picture" />
{% endif %}
<br/>
<strong><a href="{{ user.get_absolute_url() }}">{{ user.get_short_name() }}</a></strong>
</div>
<div class="msg_content">
<hr>
<div id="preview" class="ib"></div>
<div class="forum_signature">{{ user.forum_signature|markdown }}</div>
</div>
</div>
<hr> <hr>
{% if topic %} {% if topic %}
...@@ -62,26 +44,3 @@ ...@@ -62,26 +44,3 @@
</div> </div>
{% endblock %} {% endblock %}
{% block script %}
{{ super() }}
<script>
function make_preview() {
$("#preview_message").hide(300);
text = $("#id_message").val();
console.log("Rendering text: " + text);
$.ajax({
url: "{{ url('api:api_markdown') }}",
method: "POST",
data: { text: text, csrfmiddlewaretoken: "{{ csrf_token }}"}
}).done(function (msg) {
$("#preview").html(msg);
$("#preview_message").show(300);
});
}
</script>
{% endblock %}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment