Works for 1.0 version

This commit is contained in:
Adrian 2015-09-06 09:41:45 +02:00
parent e070093a57
commit a14b22fda0
17 changed files with 559 additions and 333 deletions

240
README.md
View file

@ -1,78 +1,117 @@
[![Downloads](https://pypip.in/d/django-markdownx/badge.svg?period=month&style=flat)](https://pypi.python.org/pypi/django-markdownx/)
[![Latest Version](https://pypip.in/v/django-markdownx/badge.svg?style=flat)](https://pypi.python.org/pypi/django-markdownx/)
[![License](https://pypip.in/license/django-markdownx/badge.svg?style=flat)](https://pypi.python.org/pypi/django-markdownx/)
# django-markdownx
Django Markdownx is a markdown editor built for Django.
Django Markdownx is a markdown editor built for Django.
It is simply an extension of the Django's Textarea widget made for editing Markdown with a live preview. It also supports uploading images with drag&drop functionality and auto tag insertion.
It is simply an extension of the Django's Textarea widget made for editing Markdown with a live preview and image uploads. It supports uploading images (stored locally in `MEDIA_ROOT` folder! yay!) with drag&drop functionality and auto tag insertion. Also, django-markdownx supports multiple editors on one page.
**Preview** (using Bootstrap for layout and styling):
![Example](https://dl.dropboxusercontent.com/u/2229134/django-markdownx.gif)
Template is highly customizable, so you can easily use i.e. Bootstrap to layout editor pane and preview pane side by side (as in preview animation below).
## Quick Start
*Side note: Just to keep it simple, all UI editing controls are unwelcome this is Markdown editor not a web MS Word imitation.*
### Preview
![Preview](https://github.com/adi-/django-markdownx/blob/master/django-markdownx-preview.gif?raw=true "Preview")
*(using Bootstrap for layout and styling)*
# Quick Start
1. Install *django-markdownx* package.
```python
pip install django-markdownx
```
```python
pip install django-markdownx
```
1. Add *markdownx* to your *INSTALLED_APPS*.
```python
#settings.py
INSTALLED_APPS = (
[...]
'markdownx',
```
```python
#settings.py
INSTALLED_APPS = (
[...]
'markdownx',
```
1. Add *url* pattern to your *urls.py*.
```python
#urls.py
urlpatterns = [
[...]
url(r'^markdownx/', include('markdownx.urls')),
]
```
```python
#urls.py
urlpatterns = [
[...]
url(r'^markdownx/', include('markdownx.urls')),
]
```
1. Use *MarkdownxInput* widget in your *forms.py*.
1. Copy included *markdownx.js* and *markdownx.css* (for django admin styling) to your *STATIC_ROOT* folder.
```python
#forms.py
from django import forms
from markdownx.widgets import MarkdownxInput
class MyForm(forms.ModelForm):
content = forms.CharField(widget=MarkdownxInput)
```
```python
python manage.py collectstatic
```
1. Copy included *markdownx.js* to your *STATIC_ROOT* folder.
1. ...and don't forget to include *jQuery* in your html file.
python manage.py collectstatic
```html
<head>
[...]
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
</head>
```
1. Include the form's required media in the template using *{{ form.media }}*.
# Usage
```html
<form method="POST" action="">{% csrf_token %}
[...]
</form>
{{ form.media }}
```
1. Model
```python
#models.py
from markdownx.models import MarkdownxField
class MyModel(models.Model):
myfield = MarkdownxField()
```
...and then, include a form's required media in the template using *{{ form.media }}*.
```html
<form method="POST" action="">{% csrf_token %}
{{ form }}
</form>
{{ form.media }}
```
1. Form
```python
#forms.py
from markdownx.fields import MarkdownxFormField
class MyForm(forms.Form):
myfield = MarkdownxFormField()
```
...and then, include a form's required media in the template using *{{ form.media }}*.
```html
<form method="POST" action="">{% csrf_token %}
{{ form }}
</form>
{{ form.media }}
```
1. Django Admin
```python
from django.contrib import admin
from markdownx.admin import MarkdownxModelAdmin
from .models import MyModel
admin.site.register(MyModel, MarkdownxModelAdmin)
```
1. Include *[jQuery](https://code.jquery.com/)* in *base.html* file.
```html
<head>
[...]
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
</head>
```
# Customization
## Settings
@ -81,113 +120,130 @@ Place settings in your *settings.py* to override default values:
```python
#settings.py
MARKDOWNX_MARKDOWN_KWARGS = dict()
MARKDOWNX_MARKDOWN_EXTENSIONS = []
MARKDOWNX_MEDIA_PATH = 'markdownx/' # subdirectory, where images will be stored in MEDIA_ROOT folder
MARKDOWNX_MAX_UPLOAD_SIZE = 52428800 # 50MB
MARKDOWNX_CONTENT_TYPES = ['image/jpeg', 'image/png']
MARKDOWNX_IMAGE_SIZE = {'size': (500, 500), 'quality': 90,}
MARKDOWNX_UPLOAD_MAX_SIZE = 52428800 # 50MB
MARKDOWNX_UPLOAD_CONTENT_TYPES = ['image/jpeg', 'image/png']
MARKDOWNX_IMAGE_MAX_SIZE = {'size': (500, 500), 'quality': 90,}
MARKDOWNX_EDITOR_RESIZABLE = True # update editor's height to inner content height while typing
```
*MARKDOWNX_IMAGE_SIZE* dict properties:
**NOTE:** *MARKDOWNX_IMAGE_MAX_SIZE* dict properties:
* **size** (width, height). When `0` used, i.e.: (500,0), property will figure out proper height by itself
* **quality** default: `None` image quality, from `0` (full compression) to `100` (no compression)
* **crop** default: `False` if `True` use `size` to crop final image
* **upscale** default: `False` if image dimensions are smaller than those in `size`, upscale image to `size` dimensions
## Template
## Widget's template
Default template looks like:
Default widget's template looks like:
```html
<div id="markdownx">
<h6>{% trans "Editor" %}</h6>
<div class="markdownx">
{{ markdownx_editor }}
<h6>{% trans "Preview" %}</h6>
<div id="markdownx_preview"></div>
<div class="markdownx-preview"></div>
</div>
```
When you want to use *Bootstrap 3* and "real" side-by-side panes, just place *templates/markdownx/widget.html* file with:
When you want to use *Bootstrap 3* and side-by-side panes (as in preview image above), just place *templates/markdownx/widget.html* file in your project with:
```html
<div class="row" id="markdownx">
<div class="col-sm-6">
<h6>{% trans "Editor" %}</h6>
<div class="markdownx row">
<div class="col-md-6">
{{ markdownx_editor }}
</div>
<div class="col-sm-6">
<h6>{% trans "Preview" %}</h6>
<div id="markdownx_preview"></div>
<div class="col-md-6">
<div class="markdownx-preview"></div>
</div>
</div>
```
# Dependencies
* jQuery AJAX upload and JS functionality
# TODO
* custom URL upload link
* custom media path function
* python 3 compatibility
* tests
* Markdown
* Pillow
* jQuery
# Changelog
### v0.4.2
###### v1.0
* Warning: no backward compatibility
* Admin, Model and Form custom objects
* Django admin styles for compiled markdown
* Settings variables changed:
* MARKDOWNX_MAX_SIZE => MARKDOWNX_IMAGE_MAX_SIZE
* MARKDOWNX_MARKDOWN_KWARGS => MARKDOWNX_MARKDOWN_EXTENSIONS
* MARKDOWNX_MAX_UPLOADSIZE => MARKDOWNX_UPLOAD_MAX_SIZE
* MARKDOWNX_CONTENT_TYPES => MARKDOWNX_UPLOAD_CONTENT_TYPES
###### v0.4.2
* Path fix by argaen
### v0.4.1
###### v0.4.1
* Better editor height updates
* Refresh preview on image upload
* Small JS code fixes
### v0.4.0
###### v0.4.0
* editor auto height
### v0.3.1
###### v0.3.1
* JS event fix
### v0.3.0
###### v0.3.0
* version bump
### v0.2.9
###### v0.2.9
* Removed any inlcuded css
* Removed JS markdown compiler (full python support now with Markdown lib)
### v0.2.0
###### v0.2.0
* Allow to paste tabs using Tab button
### v0.1.4
###### v0.1.4
* package data fix
### v0.1.3
###### v0.1.3
* README.md fix on PyPi
### v0.1.2
###### v0.1.2
* critical setuptools fix
### v0.1.1
###### v0.1.1
* change context name `editor` to `markdownx_editor` for better consistency
### v0.1.0
###### v0.1.0
* init
# License
django-markdown is licensed under the open source BSD license
# TODO
* python 3 compatibility
* tests
Would be nice to have some help with those!
# Notes
**django-markdownx** was inspired by great [django-images](https://github.com/mirumee/django-images) and [django-bootstrap-markdown](http://thegoods.aj7may.com/django-bootstrap-markdown/) packages.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

12
markdownx/admin.py Normal file
View file

@ -0,0 +1,12 @@
from django.contrib import admin
from django.db import models
from .widgets import AdminMarkdownxWidget
from .models import MarkdownxField
class MarkdownxModelAdmin(admin.ModelAdmin):
formfield_overrides = {
MarkdownxField: {'widget': AdminMarkdownxWidget}
}

23
markdownx/fields.py Normal file
View file

@ -0,0 +1,23 @@
from django import forms
from .settings import MARKDOWNX_EDITOR_RESIZABLE
from .widgets import (
MarkdownxWidget,
AdminMarkdownxWidget,
)
class MarkdownxFormField(forms.CharField):
def __init__(self, *args, **kwargs):
super(MarkdownxFormField, self).__init__(*args, **kwargs)
if self.widget.__class__ != AdminMarkdownxWidget:
self.widget = MarkdownxWidget()
if self.widget.attrs.has_key('class'):
self.widget.attrs['class'] += ' markdownx-editor'
else:
self.widget.attrs.update({'class':'markdownx-editor'})
self.widget.attrs['data-markdownx-editor-resizable'] = MARKDOWNX_EDITOR_RESIZABLE

View file

@ -10,28 +10,30 @@ from django.template import defaultfilters as filters
from .utils import scale_and_crop
from .settings import (
MARKDOWNX_IMAGE_SIZE,
MARKDOWNX_IMAGE_MAX_SIZE,
MARKDOWNX_MEDIA_PATH,
MARKDOWNX_CONTENT_TYPES,
MARKDOWNX_MAX_UPLOAD_SIZE,
MARKDOWNX_UPLOAD_CONTENT_TYPES,
MARKDOWNX_UPLOAD_MAX_SIZE,
)
class ImageForm(forms.Form):
image = forms.ImageField()
def save(self, commit=True):
img = scale_and_crop(self.files['image'], **MARKDOWNX_IMAGE_SIZE)
img = scale_and_crop(self.files['image'], **MARKDOWNX_IMAGE_MAX_SIZE)
thumb_io = StringIO.StringIO()
img.save(thumb_io, self.files['image'].content_type.split('/')[-1].upper())
file_name = str(self.files['image'])
img = InMemoryUploadedFile(thumb_io, "image", file_name, self.files['image'].content_type, thumb_io.len, None)
unique_file_name = self.get_unique_file_name(file_name)
full_path = os.path.join(settings.MEDIA_ROOT, MARKDOWNX_MEDIA_PATH, unique_file_name)
if not os.path.exists(os.path.dirname(full_path)):
os.makedirs(os.path.dirname(full_path))
destination = open(full_path, 'wb+')
for chunk in img.chunks():
destination.write(chunk)
@ -47,9 +49,9 @@ class ImageForm(forms.Form):
def clean(self):
upload = self.cleaned_data['image']
content_type = upload.content_type
if content_type in MARKDOWNX_CONTENT_TYPES:
if upload._size > MARKDOWNX_MAX_UPLOAD_SIZE:
raise forms.ValidationError(_('Please keep filesize under %(max)s. Current filesize %(current)s') % {'max':filters.filesizeformat(MARKDOWNX_MAX_UPLOAD_SIZE), 'current':filters.filesizeformat(upload._size)})
if content_type in MARKDOWNX_UPLOAD_CONTENT_TYPES:
if upload._size > MARKDOWNX_UPLOAD_MAX_SIZE:
raise forms.ValidationError(_('Please keep filesize under %(max)s. Current filesize %(current)s') % {'max':filters.filesizeformat(MARKDOWNX_UPLOAD_MAX_SIZE), 'current':filters.filesizeformat(upload._size)})
else:
raise forms.ValidationError(_('File type is not supported'))

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-01 10:34+0000\n"
"POT-Creation-Date: 2015-09-06 07:38+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,27 +17,19 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: forms.py:52
#: forms.py:54
#, python-format
msgid "Please keep filesize under %(max)s. Current filesize %(current)s"
msgstr ""
#: forms.py:54
#: forms.py:56
msgid "File type is not supported"
msgstr ""
#: settings.py:13
#: settings.py:17
msgid "English"
msgstr ""
#: settings.py:14
#: settings.py:18
msgid "Polish"
msgstr ""
#: templates/markdownx/widget.html:5 templates/markdownx/widget.html.py:15
msgid "Editor"
msgstr ""
#: templates/markdownx/widget.html:9 templates/markdownx/widget.html.py:17
msgid "Preview"
msgstr ""

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-01 10:34+0000\n"
"POT-Creation-Date: 2015-09-06 07:38+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -19,27 +19,19 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
#: forms.py:52
#: forms.py:54
#, python-format
msgid "Please keep filesize under %(max)s. Current filesize %(current)s"
msgstr "Maksymalny rozmiar pliku wynosi %(max)s. Rozmiar obecnie wgrywanego pliku wynosi %(current)s"
#: forms.py:54
#: forms.py:56
msgid "File type is not supported"
msgstr "Typ pliku nie jest obsługiwany"
#: settings.py:13
#: settings.py:17
msgid "English"
msgstr "Angielski"
msgstr ""
#: settings.py:14
#: settings.py:18
msgid "Polish"
msgstr "Polski"
#: templates/markdownx/widget.html:5 templates/markdownx/widget.html.py:15
msgid "Editor"
msgstr "Edytor"
#: templates/markdownx/widget.html:9 templates/markdownx/widget.html.py:17
msgid "Preview"
msgstr "Podgląd"
msgstr ""

11
markdownx/models.py Normal file
View file

@ -0,0 +1,11 @@
from django.db import models
from .fields import MarkdownxFormField
class MarkdownxField(models.TextField):
def formfield(self, **kwargs):
defaults = {'form_class': MarkdownxFormField}
defaults.update(kwargs)
return super(MarkdownxField, self).formfield(**defaults)

View file

@ -1,18 +1,21 @@
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
# markdown.markdown kwargs
MARKDOWNX_MARKDOWN_KWARGS = getattr(settings, 'MARKDOWNX_MARKDOWN_KWARGS', dict())
# Markdown extensions
MARKDOWNX_MARKDOWN_EXTENSIONS = getattr(settings, 'MARKDOWNX_MARKDOWN_EXTENSIONS', [])
# path
# Media path
MARKDOWNX_MEDIA_PATH = getattr(settings, 'MARKDOWNX_MEDIA_PATH', 'markdownx/')
# image
MARKDOWNX_MAX_UPLOAD_SIZE = getattr(settings, 'MARKDOWNX_MAX_UPLOAD_SIZE', 52428800) # 50MB
MARKDOWNX_CONTENT_TYPES = getattr(settings, 'MARKDOWNX_CONTENT_TYPES', ['image/jpeg', 'image/png'])
MARKDOWNX_IMAGE_SIZE = getattr(settings, 'MARKDOWNX_IMAGE_SIZE', {'size': (500, 500), 'quality': 90,})
# Image
MARKDOWNX_UPLOAD_MAX_SIZE = getattr(settings, 'MARKDOWNX_UPLOAD_MAX_SIZE', 52428800) # 50MB
MARKDOWNX_UPLOAD_CONTENT_TYPES = getattr(settings, 'MARKDOWNX_UPLOAD_CONTENT_TYPES', ['image/jpeg', 'image/png'])
MARKDOWNX_IMAGE_MAX_SIZE = getattr(settings, 'MARKDOWNX_IMAGE_MAX_SIZE', {'size': (500, 500), 'quality': 90,})
# translations
# Editor
MARKDOWNX_EDITOR_RESIZABLE = getattr(settings, 'MARKDOWNX_EDITOR_RESIZABLE', False)
# Translations
LANGUAGES = getattr(settings, 'LANGUAGES', (
('en', _('English')),
('pl', _('Polish')),

View file

@ -1,168 +0,0 @@
$.fn.extend({
markdownx: function(options) {
var defaults = {};
var opts = $.extend(defaults, options);
var $this = $(this);
var $markdownx_editor = $this.find('#markdownx_editor');
var $markdownx_preview = $this.find('#markdownx_preview');
var ms;
var markdownify = function() {
clearTimeout(ms);
ms = setTimeout(getMarkdown, 500);
};
var getMarkdown = function() {
form = new FormData();
form.append("content", $markdownx_editor.val());
form.append("csrfmiddlewaretoken", getCookie('csrftoken'))
$.ajax({
type: 'POST',
url: '/markdownx/markdownify/',
data: form,
processData: false,
contentType: false,
success: function(response) {
$markdownx_preview.html(response);
updateHeight();
},
error: function(response) {
console.log("error", response);
},
});
}
var updateHeight = function() {
$markdownx_editor.innerHeight($markdownx_editor.prop('scrollHeight'))
}
var insertImage = function(image_path) {
var cursor_pos = $markdownx_editor.prop('selectionStart');
var text = $markdownx_editor.val();
var textBeforeCursor = text.substring(0, cursor_pos);
var textAfterCursor = text.substring(cursor_pos, text.length);
var textToInsert = "![](" + image_path + ")";
$markdownx_editor.val(textBeforeCursor + textToInsert + textAfterCursor);
$markdownx_editor.prop('selectionStart', cursor_pos + textToInsert.length);
$markdownx_editor.prop('selectionEnd', cursor_pos + textToInsert.length);
$markdownx_editor.keyup();
updateHeight();
markdownify();
}
var getCookie = function(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var sendFile = function(file) {
form = new FormData();
form.append("image", file);
form.append("csrfmiddlewaretoken", getCookie('csrftoken'))
$.ajax({
type: 'POST',
url: '/markdownx/upload/',
data: form,
processData: false,
contentType: false,
beforeSend: function() {
console.log("uploading...");
$markdownx_editor.fadeTo("fast", 0.3);
},
success: function(response) {
$markdownx_editor.fadeTo("fast", 1);
if (response['image_path']) {
insertImage(response['image_path']);
console.log("success", response);
} else
console.log('error: wrong response', response);
},
error: function(response) {
console.log("error", response);
$markdownx_editor.fadeTo("fast", 1 );
},
});
}
updateHeight();
markdownify();
$markdownx_editor.on('keydown', function(e) {
if (e.keyCode === 9) { // Tab
var start = this.selectionStart;
var end = this.selectionEnd;
var $this = $(this);
var value = $this.val();
$this.val(value.substring(0, start) + "\t" + value.substring(end));
this.selectionStart = this.selectionEnd = start + 1;
markdownify();
return false;
}
});
// On text change
$markdownx_editor.on('input propertychange', function() {
updateHeight();
markdownify();
});
// Upload functionality
$('html').on('dragenter dragover drop dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
});
$markdownx_editor.on('dragenter dragover', function(e) {
e.originalEvent.dataTransfer.dropEffect= 'copy';
e.preventDefault();
e.stopPropagation();
});
$markdownx_editor.on('dragleave', function(e) {
e.preventDefault();
e.stopPropagation();
});
$markdownx_editor.on('drop', function(e) {
if (e.originalEvent.dataTransfer){
if (e.originalEvent.dataTransfer.files.length) {
for (var i = 0; i < e.originalEvent.dataTransfer.files.length; i++) {
sendFile(e.originalEvent.dataTransfer.files[i]);
}
}
}
e.preventDefault();
e.stopPropagation();
});
}
});
$(document).ready(function() {
$('#markdownx').markdownx();
});

View file

@ -0,0 +1,124 @@
.markdownx {
display: inline-block;
}
.markdownx .markdownx-editor,
.markdownx .markdownx-preview {
margin-left: 0;
width: 610px;
}
.markdownx .markdownx-preview {
overflow-y: scroll;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-size: 100%;
font-size: 1em;
line-height: 1.5em;
}
.markdownx .markdownx-preview * {
line-height: 1.5;
}
/* Django admin overrides */
.markdownx .markdownx-preview a { color: #5b80b2; text-decoration:underline; }
.markdownx .markdownx-preview a:visited { color: #0b0080; }
.markdownx .markdownx-preview a:hover { color: #0645ad; }
.markdownx .markdownx-preview a:active { color:#faa700; }
.markdownx .markdownx-preview a:focus { outline: thin dotted; }
.markdownx .markdownx-preview a:hover, .markdownx .markdownx-preview a:active { outline: 0; }
.markdownx .markdownx-preview p {
margin: 1em 0;
padding: 0;
font-size: 14px;
}
.markdownx .markdownx-preview img { max-width:100%; }
.markdownx .markdownx-preview h1,
.markdownx .markdownx-preview h2,
.markdownx .markdownx-preview h3,
.markdownx .markdownx-preview h4,
.markdownx .markdownx-preview h5,
.markdownx .markdownx-preview h6 {
font-weight: normal;
color: #111;
margin-top: 0.75em;
margin-bottom: 0.75em;
padding: 0;
background: none;
}
.markdownx .markdownx-preview h4,
.markdownx .markdownx-preview h5,
.markdownx .markdownx-preview h6 { font-weight: bold; }
.markdownx .markdownx-preview h1 { font-size: 2.5em; }
.markdownx .markdownx-preview h2 { font-size: 2em; }
.markdownx .markdownx-preview h3 { font-size: 1.5em; }
.markdownx .markdownx-preview h4 { font-size: 1.2em; }
.markdownx .markdownx-preview h5 { font-size: 1em; }
.markdownx .markdownx-preview h6 { font-size: 0.9em; }
.markdownx .markdownx-preview blockquote {
color: #666666;
margin: 0;
padding-left: 1.5em;
border-left: 0.5em #eee solid;
}
.markdownx .markdownx-preview hr {
display: block;
height: 0px;
border: 0;
font-style: italic;
border-bottom: 1px solid #ccc;
margin: 20px 0;
padding: 0;
}
.markdownx .markdownx-preview pre,
.markdownx .markdownx-preview code,
.markdownx .markdownx-preview kbd,
.markdownx .markdownx-preview samp {
font-family: monospace, monospace;
font-size: 14px;
}
.markdownx .markdownx-preview code,
.markdownx .markdownx-preview pre {
margin: 0 2px;
padding: 0px 5px;
border: 1px solid #ddd;
background-color: #f8f8f8;
border-radius: 2px;
color: #444;
}
.markdownx .markdownx-preview pre {
margin: 1.5em 0 1.5em 0;
padding: 1em;
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
.markdownx .markdownx-preview pre code {
margin: 0;
padding: 0;
background: transparent;
border: none;
}
.markdownx .markdownx-preview b, .markdownx .markdownx-preview strong { font-weight: bold; }
.markdownx .markdownx-preview dfn { font-style: italic; }
.markdownx .markdownx-preview ins { background: #ff9; color: #000; text-decoration: none; }
.markdownx .markdownx-preview mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
.markdownx .markdownx-preview sub, .markdownx .markdownx-preview sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
.markdownx .markdownx-preview sup { top: -0.5em; }
.markdownx .markdownx-preview sub { bottom: -0.25em; }
.markdownx .markdownx-preview ul,
.markdownx .markdownx-preview ol {
margin: 1em 0 !important; padding: 0 0 0 2em !important;
}
.markdownx .markdownx-preview ul li,
.markdownx .markdownx-preview ol li {
font-size: 14px !important;
margin-bottom: 0.75em;
}
.markdownx .markdownx-preview li p:last-child { margin:0 }
.markdownx .markdownx-preview dd { margin: 0 0 0 2em; }
.markdownx .markdownx-preview img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
.markdownx .markdownx-preview table { border-collapse: collapse; border-spacing: 0; }
.markdownx .markdownx-preview th { background: none; background: #f8f8f8; font-size: 14px; }
.markdownx .markdownx-preview td { vertical-align: top; font-size: 14px; }

View file

@ -0,0 +1,179 @@
(function ($) {
if (!$) {
$ = django.jQuery
}
$.fn.markdownx = function() {
return this.each( function() {
var getMarkdown = function() {
form = new FormData();
form.append("content", markdownxEditor.val());
form.append("csrfmiddlewaretoken", getCookie('csrftoken'))
$.ajax({
type: 'POST',
url: '/markdownx/markdownify/',
data: form,
processData: false,
contentType: false,
success: function(response) {
markdownxPreview.html(response);
updateHeight();
},
error: function(response) {
console.log("error", response);
},
});
}
var updateHeight = function() {
if (isMarkdownxEditorResizable) {
markdownxEditor.innerHeight(markdownxEditor.prop('scrollHeight'));
}
}
var insertImage = function(image_path) {
var cursor_pos = markdownxEditor.prop('selectionStart');
var text = markdownxEditor.val();
var textBeforeCursor = text.substring(0, cursor_pos);
var textAfterCursor = text.substring(cursor_pos, text.length);
var textToInsert = "![](" + image_path + ")";
markdownxEditor.val(textBeforeCursor + textToInsert + textAfterCursor);
markdownxEditor.prop('selectionStart', cursor_pos + textToInsert.length);
markdownxEditor.prop('selectionEnd', cursor_pos + textToInsert.length);
markdownxEditor.keyup();
updateHeight();
markdownify();
}
var getCookie = function(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = $.trim(cookies[i]);
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var sendFile = function(file) {
form = new FormData();
form.append("image", file);
form.append("csrfmiddlewaretoken", getCookie('csrftoken'))
$.ajax({
type: 'POST',
url: '/markdownx/upload/',
data: form,
processData: false,
contentType: false,
beforeSend: function() {
console.log("uploading...");
markdownxEditor.fadeTo("fast", 0.3);
},
success: function(response) {
markdownxEditor.fadeTo("fast", 1);
if (response['image_path']) {
insertImage(response['image_path']);
console.log("success", response);
} else
console.log('error: wrong response', response);
},
error: function(response) {
console.log("error", response);
markdownxEditor.fadeTo("fast", 1 );
},
});
}
var timeout;
var markdownify = function() {
clearTimeout(timeout);
timeout = setTimeout(getMarkdown, 500);
};
// Events
var onKeyDownEvent = function(e) {
if (e.keyCode === 9) { // Tab
var start = this.selectionStart;
var end = this.selectionEnd;
$(this).val($(this).val().substring(0, start) + "\t" + $(this).val().substring(end));
this.selectionStart = this.selectionEnd = start + 1;
markdownify();
return false;
}
}
var onInputChangeEvent = function() {
updateHeight();
markdownify();
}
var onHtmlEvents = function(e) {
e.preventDefault();
e.stopPropagation();
}
var onDragEnterEvent = function(e) {
e.originalEvent.dataTransfer.dropEffect= 'copy';
e.preventDefault();
e.stopPropagation();
}
var onDragLeaveEvent = function(e) {
e.preventDefault();
e.stopPropagation();
}
var onDropEvent = function(e) {
if (e.originalEvent.dataTransfer){
if (e.originalEvent.dataTransfer.files.length) {
for (var i = 0; i < e.originalEvent.dataTransfer.files.length; i++) {
sendFile(e.originalEvent.dataTransfer.files[i]);
}
}
}
e.preventDefault();
e.stopPropagation();
}
// Init
var markdownxEditor = $(this).find('.markdownx-editor');
var markdownxPreview = $(this).find('.markdownx-preview');
var isMarkdownxEditorResizable = markdownxEditor.is("[data-markdownx-editor-resizable]");
$('html').on('dragenter.markdownx dragover.markdownx drop.markdownx dragleave.markdownx', onHtmlEvents);
markdownxEditor.on('keydown.markdownx', onKeyDownEvent);
markdownxEditor.on('input.markdownx propertychange.markdownx', onInputChangeEvent);
markdownxEditor.on('dragenter.markdownx dragover.markdownx', onDragEnterEvent);
markdownxEditor.on('dragleave.markdownx', onDragLeaveEvent);
markdownxEditor.on('drop.markdownx', onDropEvent);
updateHeight();
markdownify();
});
};
$(function() {
$('.markdownx').markdownx();
});
})(jQuery);

View file

@ -1,7 +1,4 @@
{% load i18n %}
<div id="markdownx">
<h6>{% trans "Editor" %}</h6>
<div class="markdownx">
{{ markdownx_editor }}
<h6>{% trans "Preview" %}</h6>
<div id="markdownx_preview"></div>
<div class="markdownx-preview"></div>
</div>

View file

@ -1,21 +1,22 @@
import markdown
from django.views.generic.edit import View, FormView
from django.http import HttpResponse, JsonResponse
import markdown
from . import forms
from .settings import MARKDOWNX_MARKDOWN_KWARGS
from .forms import ImageForm
from .settings import MARKDOWNX_MARKDOWN_EXTENSIONS
class MarkdownifyView(View):
def post(self, request, *args, **kwargs):
return HttpResponse(markdown.markdown(request.POST['content'], **MARKDOWNX_MARKDOWN_KWARGS))
return HttpResponse(markdown.markdown(request.POST['content'], extensions=MARKDOWNX_MARKDOWN_EXTENSIONS))
class ImageUploadView(FormView):
template_name = "dummy.html"
form_class = forms.ImageForm
form_class = ImageForm
success_url = '/'
def form_invalid(self, form):
@ -24,7 +25,7 @@ class ImageUploadView(FormView):
return JsonResponse(form.errors, status=400)
else:
return response
def form_valid(self, form):
image_path = form.save()
response = super(ImageUploadView, self).form_valid(form)

View file

@ -1,31 +1,33 @@
from django.conf import settings
from django.forms import Textarea
from django import forms
from django.template import Context
from django.template.loader import get_template
from django.contrib.admin import widgets
class MarkdownxInput(Textarea):
def __init__(self, attrs=None):
default_attrs = {
'id': 'markdownx_editor',
}
if attrs:
default_attrs.update(attrs)
super(Textarea, self).__init__(default_attrs)
class MarkdownxWidget(forms.Textarea):
def render(self, name, value, attrs=None):
textarea = Textarea.render(self, name, value)
widget = super(MarkdownxWidget, self).render(name, value, attrs)
t = get_template('markdownx/widget.html')
c = Context({
'markdownx_editor': textarea,
'markdownx_editor': widget,
})
return t.render(c)
class Media:
js = (
'js/markdownx.js',
'markdownx/js/markdownx.js',
)
class AdminMarkdownxWidget(MarkdownxWidget, widgets.AdminTextareaWidget):
class Media:
css = {
'all': ('markdownx/admin/css/markdownx.css',)
}
js = (
'markdownx/js/markdownx.js',
)