mirror of
https://github.com/Hopiu/django-markdownx.git
synced 2026-03-16 21:40:24 +00:00
Works for 1.0 version
This commit is contained in:
parent
e070093a57
commit
a14b22fda0
17 changed files with 559 additions and 333 deletions
240
README.md
240
README.md
|
|
@ -1,78 +1,117 @@
|
|||
[](https://pypi.python.org/pypi/django-markdownx/)
|
||||
[](https://pypi.python.org/pypi/django-markdownx/)
|
||||
[](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):
|
||||

|
||||
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
|
||||
|
||||

|
||||
|
||||
*(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.
|
||||
|
|
|
|||
BIN
django-markdownx-preview.gif
Normal file
BIN
django-markdownx-preview.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 MiB |
12
markdownx/admin.py
Normal file
12
markdownx/admin.py
Normal 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
23
markdownx/fields.py
Normal 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
|
||||
|
|
@ -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'))
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 ""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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
11
markdownx/models.py
Normal 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)
|
||||
|
|
@ -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')),
|
||||
|
|
|
|||
|
|
@ -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 = "";
|
||||
|
||||
$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();
|
||||
});
|
||||
124
markdownx/static/markdownx/admin/css/markdownx.css
Normal file
124
markdownx/static/markdownx/admin/css/markdownx.css
Normal 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; }
|
||||
179
markdownx/static/markdownx/js/markdownx.js
Normal file
179
markdownx/static/markdownx/js/markdownx.js
Normal 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 = "";
|
||||
|
||||
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);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue