diff --git a/README.md b/README.md index 7f0c1ad..027fff1 100755 --- a/README.md +++ b/README.md @@ -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 + + [...] + + + ``` -1. Include the form's required media in the template using *{{ form.media }}*. +# Usage - ```html -
{% csrf_token %} - [...] -
- {{ 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 +
{% csrf_token %} + {{ 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 +
{% csrf_token %} + {{ 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 - - [...] - - - ``` - - # 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 -
-
{% trans "Editor" %}
+
{{ markdownx_editor }} -
{% trans "Preview" %}
-
+
``` - -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 -
-
-
{% trans "Editor" %}
+
+
{{ markdownx_editor }}
-
-
{% trans "Preview" %}
-
+
+
``` # 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. diff --git a/django-markdownx-preview.gif b/django-markdownx-preview.gif new file mode 100644 index 0000000..9815b5c Binary files /dev/null and b/django-markdownx-preview.gif differ diff --git a/markdownx/admin.py b/markdownx/admin.py new file mode 100644 index 0000000..b8e07e0 --- /dev/null +++ b/markdownx/admin.py @@ -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} + } diff --git a/markdownx/fields.py b/markdownx/fields.py new file mode 100644 index 0000000..074b93d --- /dev/null +++ b/markdownx/fields.py @@ -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 diff --git a/markdownx/forms.py b/markdownx/forms.py index ab82efe..6a7fc5b 100755 --- a/markdownx/forms.py +++ b/markdownx/forms.py @@ -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')) diff --git a/markdownx/locale/en/LC_MESSAGES/django.mo b/markdownx/locale/en/LC_MESSAGES/django.mo index 63e96b4..f087204 100644 Binary files a/markdownx/locale/en/LC_MESSAGES/django.mo and b/markdownx/locale/en/LC_MESSAGES/django.mo differ diff --git a/markdownx/locale/en/LC_MESSAGES/django.po b/markdownx/locale/en/LC_MESSAGES/django.po index 77e5404..fe30718 100644 --- a/markdownx/locale/en/LC_MESSAGES/django.po +++ b/markdownx/locale/en/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: LANGUAGE \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 "" diff --git a/markdownx/locale/pl/LC_MESSAGES/django.mo b/markdownx/locale/pl/LC_MESSAGES/django.mo index 60c32c4..3543393 100644 Binary files a/markdownx/locale/pl/LC_MESSAGES/django.mo and b/markdownx/locale/pl/LC_MESSAGES/django.mo differ diff --git a/markdownx/locale/pl/LC_MESSAGES/django.po b/markdownx/locale/pl/LC_MESSAGES/django.po index fe1dfb6..524832d 100644 --- a/markdownx/locale/pl/LC_MESSAGES/django.po +++ b/markdownx/locale/pl/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: LANGUAGE \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 "" diff --git a/markdownx/models.py b/markdownx/models.py new file mode 100644 index 0000000..201287c --- /dev/null +++ b/markdownx/models.py @@ -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) diff --git a/markdownx/settings.py b/markdownx/settings.py index 5df1283..d2d2e3e 100755 --- a/markdownx/settings.py +++ b/markdownx/settings.py @@ -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')), diff --git a/markdownx/static/js/markdownx.js b/markdownx/static/js/markdownx.js deleted file mode 100644 index 5ceba07..0000000 --- a/markdownx/static/js/markdownx.js +++ /dev/null @@ -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(); -}); diff --git a/markdownx/static/markdownx/admin/css/markdownx.css b/markdownx/static/markdownx/admin/css/markdownx.css new file mode 100644 index 0000000..e6e29b4 --- /dev/null +++ b/markdownx/static/markdownx/admin/css/markdownx.css @@ -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; } diff --git a/markdownx/static/markdownx/js/markdownx.js b/markdownx/static/markdownx/js/markdownx.js new file mode 100644 index 0000000..6969117 --- /dev/null +++ b/markdownx/static/markdownx/js/markdownx.js @@ -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); diff --git a/markdownx/templates/markdownx/widget.html b/markdownx/templates/markdownx/widget.html index 8d1c21f..b795aac 100755 --- a/markdownx/templates/markdownx/widget.html +++ b/markdownx/templates/markdownx/widget.html @@ -1,7 +1,4 @@ -{% load i18n %} -
-
{% trans "Editor" %}
+
{{ markdownx_editor }} -
{% trans "Preview" %}
-
+
diff --git a/markdownx/views.py b/markdownx/views.py index dd38911..e2d8421 100755 --- a/markdownx/views.py +++ b/markdownx/views.py @@ -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) diff --git a/markdownx/widgets.py b/markdownx/widgets.py index 1a77e79..a462d80 100755 --- a/markdownx/widgets.py +++ b/markdownx/widgets.py @@ -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', )