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
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): 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).
![Example](https://dl.dropboxusercontent.com/u/2229134/django-markdownx.gif)
## 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. 1. Install *django-markdownx* package.
```python ```python
pip install django-markdownx pip install django-markdownx
``` ```
1. Add *markdownx* to your *INSTALLED_APPS*. 1. Add *markdownx* to your *INSTALLED_APPS*.
```python ```python
#settings.py #settings.py
INSTALLED_APPS = ( INSTALLED_APPS = (
[...] [...]
'markdownx', 'markdownx',
``` ```
1. Add *url* pattern to your *urls.py*. 1. Add *url* pattern to your *urls.py*.
```python ```python
#urls.py #urls.py
urlpatterns = [ urlpatterns = [
[...] [...]
url(r'^markdownx/', include('markdownx.urls')), 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 ```python
#forms.py python manage.py collectstatic
from django import forms ```
from markdownx.widgets import MarkdownxInput
class MyForm(forms.ModelForm):
content = forms.CharField(widget=MarkdownxInput)
```
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 1. Model
<form method="POST" action="">{% csrf_token %}
[...] ```python
</form> #models.py
{{ form.media }} 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 # Customization
## Settings ## Settings
@ -81,113 +120,130 @@ Place settings in your *settings.py* to override default values:
```python ```python
#settings.py #settings.py
MARKDOWNX_MARKDOWN_KWARGS = dict() MARKDOWNX_MARKDOWN_EXTENSIONS = []
MARKDOWNX_MEDIA_PATH = 'markdownx/' # subdirectory, where images will be stored in MEDIA_ROOT folder MARKDOWNX_MEDIA_PATH = 'markdownx/' # subdirectory, where images will be stored in MEDIA_ROOT folder
MARKDOWNX_MAX_UPLOAD_SIZE = 52428800 # 50MB MARKDOWNX_UPLOAD_MAX_SIZE = 52428800 # 50MB
MARKDOWNX_CONTENT_TYPES = ['image/jpeg', 'image/png'] MARKDOWNX_UPLOAD_CONTENT_TYPES = ['image/jpeg', 'image/png']
MARKDOWNX_IMAGE_SIZE = {'size': (500, 500), 'quality': 90,} 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 * **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) * **quality** default: `None` image quality, from `0` (full compression) to `100` (no compression)
* **crop** default: `False` if `True` use `size` to crop final image * **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 * **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 ```html
<div id="markdownx"> <div class="markdownx">
<h6>{% trans "Editor" %}</h6>
{{ markdownx_editor }} {{ markdownx_editor }}
<h6>{% trans "Preview" %}</h6> <div class="markdownx-preview"></div>
<div id="markdownx_preview"></div>
</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 ```html
<div class="row" id="markdownx"> <div class="markdownx row">
<div class="col-sm-6"> <div class="col-md-6">
<h6>{% trans "Editor" %}</h6>
{{ markdownx_editor }} {{ markdownx_editor }}
</div> </div>
<div class="col-sm-6"> <div class="col-md-6">
<h6>{% trans "Preview" %}</h6> <div class="markdownx-preview"></div>
<div id="markdownx_preview"></div>
</div> </div>
</div> </div>
``` ```
# Dependencies # Dependencies
* jQuery AJAX upload and JS functionality * Markdown
* Pillow
# TODO * jQuery
* custom URL upload link
* custom media path function
* python 3 compatibility
* tests
# Changelog # 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 * Path fix by argaen
### v0.4.1 ###### v0.4.1
* Better editor height updates * Better editor height updates
* Refresh preview on image upload * Refresh preview on image upload
* Small JS code fixes * Small JS code fixes
### v0.4.0 ###### v0.4.0
* editor auto height * editor auto height
### v0.3.1 ###### v0.3.1
* JS event fix * JS event fix
### v0.3.0 ###### v0.3.0
* version bump * version bump
### v0.2.9 ###### v0.2.9
* Removed any inlcuded css * Removed any inlcuded css
* Removed JS markdown compiler (full python support now with Markdown lib) * Removed JS markdown compiler (full python support now with Markdown lib)
### v0.2.0 ###### v0.2.0
* Allow to paste tabs using Tab button * Allow to paste tabs using Tab button
### v0.1.4 ###### v0.1.4
* package data fix * package data fix
### v0.1.3 ###### v0.1.3
* README.md fix on PyPi * README.md fix on PyPi
### v0.1.2 ###### v0.1.2
* critical setuptools fix * critical setuptools fix
### v0.1.1 ###### v0.1.1
* change context name `editor` to `markdownx_editor` for better consistency * change context name `editor` to `markdownx_editor` for better consistency
### v0.1.0 ###### v0.1.0
* init * 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 # 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. **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 .utils import scale_and_crop
from .settings import ( from .settings import (
MARKDOWNX_IMAGE_SIZE, MARKDOWNX_IMAGE_MAX_SIZE,
MARKDOWNX_MEDIA_PATH, MARKDOWNX_MEDIA_PATH,
MARKDOWNX_CONTENT_TYPES, MARKDOWNX_UPLOAD_CONTENT_TYPES,
MARKDOWNX_MAX_UPLOAD_SIZE, MARKDOWNX_UPLOAD_MAX_SIZE,
) )
class ImageForm(forms.Form): class ImageForm(forms.Form):
image = forms.ImageField() image = forms.ImageField()
def save(self, commit=True): 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() thumb_io = StringIO.StringIO()
img.save(thumb_io, self.files['image'].content_type.split('/')[-1].upper()) img.save(thumb_io, self.files['image'].content_type.split('/')[-1].upper())
file_name = str(self.files['image']) file_name = str(self.files['image'])
img = InMemoryUploadedFile(thumb_io, "image", file_name, self.files['image'].content_type, thumb_io.len, None) 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) unique_file_name = self.get_unique_file_name(file_name)
full_path = os.path.join(settings.MEDIA_ROOT, MARKDOWNX_MEDIA_PATH, unique_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)): if not os.path.exists(os.path.dirname(full_path)):
os.makedirs(os.path.dirname(full_path)) os.makedirs(os.path.dirname(full_path))
destination = open(full_path, 'wb+') destination = open(full_path, 'wb+')
for chunk in img.chunks(): for chunk in img.chunks():
destination.write(chunk) destination.write(chunk)
@ -47,9 +49,9 @@ class ImageForm(forms.Form):
def clean(self): def clean(self):
upload = self.cleaned_data['image'] upload = self.cleaned_data['image']
content_type = upload.content_type content_type = upload.content_type
if content_type in MARKDOWNX_CONTENT_TYPES: if content_type in MARKDOWNX_UPLOAD_CONTENT_TYPES:
if upload._size > MARKDOWNX_MAX_UPLOAD_SIZE: if upload._size > MARKDOWNX_UPLOAD_MAX_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)}) 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: else:
raise forms.ValidationError(_('File type is not supported')) raise forms.ValidationError(_('File type is not supported'))

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,27 +17,19 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: forms.py:52 #: forms.py:54
#, python-format #, python-format
msgid "Please keep filesize under %(max)s. Current filesize %(current)s" msgid "Please keep filesize under %(max)s. Current filesize %(current)s"
msgstr "" msgstr ""
#: forms.py:54 #: forms.py:56
msgid "File type is not supported" msgid "File type is not supported"
msgstr "" msgstr ""
#: settings.py:13 #: settings.py:17
msgid "English" msgid "English"
msgstr "" msgstr ""
#: settings.py:14 #: settings.py:18
msgid "Polish" msgid "Polish"
msgstr "" 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 " "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n" "|| n%100>=20) ? 1 : 2);\n"
#: forms.py:52 #: forms.py:54
#, python-format #, python-format
msgid "Please keep filesize under %(max)s. Current filesize %(current)s" 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" 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" msgid "File type is not supported"
msgstr "Typ pliku nie jest obsługiwany" msgstr "Typ pliku nie jest obsługiwany"
#: settings.py:13 #: settings.py:17
msgid "English" msgid "English"
msgstr "Angielski" msgstr ""
#: settings.py:14 #: settings.py:18
msgid "Polish" msgid "Polish"
msgstr "Polski" msgstr ""
#: 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"

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.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
# markdown.markdown kwargs # Markdown extensions
MARKDOWNX_MARKDOWN_KWARGS = getattr(settings, 'MARKDOWNX_MARKDOWN_KWARGS', dict()) MARKDOWNX_MARKDOWN_EXTENSIONS = getattr(settings, 'MARKDOWNX_MARKDOWN_EXTENSIONS', [])
# path # Media path
MARKDOWNX_MEDIA_PATH = getattr(settings, 'MARKDOWNX_MEDIA_PATH', 'markdownx/') MARKDOWNX_MEDIA_PATH = getattr(settings, 'MARKDOWNX_MEDIA_PATH', 'markdownx/')
# image # Image
MARKDOWNX_MAX_UPLOAD_SIZE = getattr(settings, 'MARKDOWNX_MAX_UPLOAD_SIZE', 52428800) # 50MB MARKDOWNX_UPLOAD_MAX_SIZE = getattr(settings, 'MARKDOWNX_UPLOAD_MAX_SIZE', 52428800) # 50MB
MARKDOWNX_CONTENT_TYPES = getattr(settings, 'MARKDOWNX_CONTENT_TYPES', ['image/jpeg', 'image/png']) MARKDOWNX_UPLOAD_CONTENT_TYPES = getattr(settings, 'MARKDOWNX_UPLOAD_CONTENT_TYPES', ['image/jpeg', 'image/png'])
MARKDOWNX_IMAGE_SIZE = getattr(settings, 'MARKDOWNX_IMAGE_SIZE', {'size': (500, 500), 'quality': 90,}) 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', ( LANGUAGES = getattr(settings, 'LANGUAGES', (
('en', _('English')), ('en', _('English')),
('pl', _('Polish')), ('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 class="markdownx">
<div id="markdownx">
<h6>{% trans "Editor" %}</h6>
{{ markdownx_editor }} {{ markdownx_editor }}
<h6>{% trans "Preview" %}</h6> <div class="markdownx-preview"></div>
<div id="markdownx_preview"></div>
</div> </div>

View file

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

View file

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