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" %}
{% from 'core/macros_pages.jinja' import markdown_preview_script, page_edit_form %}
{% block head %}
{{ super() }}
{{ markdown_preview_script(csrf_token) }}
{% endblock %}
{% from 'core/macros_pages.jinja' import page_edit_form %}
{% block content %}
{{ 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 (
CanCreateMixin,
QuickNotifMixin,
)
from core.views.forms import SelectDateTime
from core.views.forms import SelectDateTime, MarkdownInput
from core.models import Notification, RealGroup, User
from club.models import Club, Mailing
......@@ -167,19 +167,25 @@ class ComEditView(ComTabsMixin, CanEditPropMixin, UpdateView):
class AlertMsgEditView(ComEditView):
fields = ["alert_msg"]
form_class = modelform_factory(
Sith, fields=["alert_msg"], widgets={"alert_msg": MarkdownInput}
)
current_tab = "alert"
success_url = reverse_lazy("com:alert_edit")
class InfoMsgEditView(ComEditView):
fields = ["info_msg"]
form_class = modelform_factory(
Sith, fields=["info_msg"], widgets={"info_msg": MarkdownInput}
)
current_tab = "info"
success_url = reverse_lazy("com:info_edit")
class IndexEditView(ComEditView):
fields = ["index_page"]
form_class = modelform_factory(
Sith, fields=["index_page"], widgets={"index_page": MarkdownInput}
)
current_tab = "index"
success_url = reverse_lazy("com:index_edit")
......@@ -197,7 +203,12 @@ class NewsForm(forms.ModelForm):
class Meta:
model = News
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(
["%Y-%m-%d %H:%M:%S"],
......@@ -461,6 +472,12 @@ class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateVi
Weekmail,
fields=["title", "intro", "joke", "protip", "conclusion"],
help_texts={"title": _("Delete and save to regenerate")},
widgets={
"intro": MarkdownInput,
"joke": MarkdownInput,
"protip": MarkdownInput,
"conclusion": MarkdownInput,
},
)
success_url = reverse_lazy("com:weekmail")
current_tab = "weekmail"
......@@ -533,7 +550,11 @@ class WeekmailArticleEditView(
"""Edit an article"""
model = WeekmailArticle
fields = ["title", "club", "content"]
form_class = modelform_factory(
WeekmailArticle,
fields=["title", "club", "content"],
widgets={"content": MarkdownInput},
)
pk_url_kwarg = "article_id"
template_name = "core/edit.jinja"
success_url = reverse_lazy("com:weekmail")
......@@ -545,7 +566,11 @@ class WeekmailArticleCreateView(QuickNotifMixin, CreateView):
"""Post an article"""
model = WeekmailArticle
fields = ["title", "club", "content"]
form_class = modelform_factory(
WeekmailArticle,
fields=["title", "club", "content"],
widgets={"content": MarkdownInput},
)
template_name = "core/create.jinja"
success_url = reverse_lazy("core:user_tools")
quick_notif_url_arg = "qn_weekmail_new_article"
......
......@@ -43,3 +43,22 @@ $( function() {
function display_notif() {
$('#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 {
}
}
.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-------------------------------*/
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header
......
......@@ -16,6 +16,11 @@
{% else %}
<link rel="stylesheet" href="{{ static('core/font-awesome/css/font-awesome.min.css') }}">
{% 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 %}
</head>
......@@ -248,14 +253,12 @@
{% endblock %}
-->
{% 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/i18n/datepicker-fr.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('ajax_select/js/ajax_select.js') }}"></script>
<script src="{{ url('javascript-catalog') }}"></script>
<script src="{{ static('core/js/script.js') }}"></script>
<script>
$('.select_single').multipleSelect({
single: true,
......@@ -289,73 +292,8 @@ $(document).keydown(function (e) {
jQuery.datetimepicker.setLocale('{{ request.LANGUAGE_CODE|lower }}');
$('.select_datetime').datetimepicker({
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>
{% endblock %}
</body>
......
......@@ -22,29 +22,6 @@
<form action="{{ url }}" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="{{ token }}">
{{ form.as_p() }}
{{ markdown_preview_button() }}
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form>
<div id="preview" class="page_content">
</div>
{% 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" %}
{% from 'core/macros_pages.jinja' import markdown_preview_script, page_edit_form %}
{% block head %}
{{ super() }}
{{ markdown_preview_script(csrf_token) }}
{% endblock %}
{% from 'core/macros_pages.jinja' import page_edit_form %}
{% block page %}
{{ 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
from django import forms
from django.conf import settings
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.forms import (
CheckboxSelectMultiple,
......@@ -90,19 +92,38 @@ class SelectDate(DateInput):
class MarkdownInput(Textarea):
def render(self, name, value, attrs=None):
output = (
'<p><a href="%(syntax_url)s">%(help_text)s</a></p>'
'<div class="markdown_editor">%(content)s</div>'
% {
"syntax_url": Page.get_page_by_full_name(
settings.SITH_CORE_PAGE_SYNTAX
).get_absolute_url(),
"help_text": _("Help on the syntax"),
"content": super(MarkdownInput, self).render(name, value, attrs),
}
)
return output
template_name = "core/markdown_textarea.jinja"
def get_context(self, name, value, attrs):
context = super(MarkdownInput, self).get_context(name, value, attrs)
context["statics"] = {
"js": static("core/simplemde/simplemde.min.js"),
"css": static("core/simplemde/simplemde.min.css"),
}
context["translations"] = {
"heading_smaller": _("Heading"),
"italic": _("Italic"),
"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):
......
......@@ -12,7 +12,7 @@ from django import forms
from core.views import CanViewMixin, CanEditMixin, CanCreateMixin
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 ajax_select.fields import AutoCompleteSelectField
......@@ -67,7 +67,7 @@ class CandidateForm(forms.ModelForm):
class Meta:
model = Candidature
fields = ["user", "role", "program", "election_list"]
widgets = {"program": forms.Textarea}
widgets = {"program": MarkdownInput}
user = AutoCompleteSelectField(
"users", label=_("User to candidate"), help_text=None, required=True
......
......@@ -30,26 +30,8 @@
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ 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>
</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>
{% if topic %}
......@@ -62,26 +44,3 @@
</div>
{% 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