├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── Makefile ├── README.md ├── django_tiptap ├── __init__.py ├── apps.py ├── config.py ├── fields.py ├── static │ └── django_tiptap │ │ └── css │ │ └── styles.css ├── templates │ └── forms │ │ └── tiptap_textarea.html └── widgets.py ├── django_tiptap_demo ├── __init__.py ├── demo_app │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── static │ │ └── my_custom_extension │ │ │ └── my_custom_extension.js │ ├── templates │ │ └── my_custom_extension │ │ │ ├── my_custom_extension.css │ │ │ ├── my_custom_extension_button_config.js │ │ │ └── my_custom_extension_toolbar_include.html │ └── tests.py ├── manage.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── pyproject.toml ├── setup.cfg └── tox.ini /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Black, isort, pytest 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.7, 3.8, 3.9] 11 | 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | python -m pip install black pytest-coverage pytest tox tox-gh-actions 25 | 26 | - name: Run tox 27 | run: tox 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # editor things 132 | .vscode 133 | 134 | # macos 135 | .DS_Store 136 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/timothycrosley/isort 3 | rev: 5.6.1 4 | hooks: 5 | - id: isort 6 | - repo: https://github.com/ambv/black 7 | rev: 19.10b0 8 | hooks: 9 | - id: black 10 | exclude: django_tiptap/templates/forms/tiptap_textarea.html 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: v2.3.0 13 | hooks: 14 | - id: check-yaml 15 | - id: end-of-file-fixer 16 | - id: trailing-whitespace 17 | args: [--markdown-linebreak-ext=md] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Legislayer GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: superuser 3 | superuser: 4 | DJANGO_SUPERUSER_USERNAME=admin \ 5 | DJANGO_SUPERUSER_PASSWORD=admin \ 6 | DJANGO_SUPERUSER_EMAIL="admin@admin.com" \ 7 | python manage.py createsuperuser --noinput 8 | 9 | .PHONY: demo-app 10 | demo-app: 11 | python manage.py migrate 12 | make superuser 13 | python manage.py runserver -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django_tiptap 2 | 3 | Django admin TipTap integration. Provides a `TipTapTextField` and `TipTapWidget` that 4 | allow you to use TipTap in your Django forms and admin pages. 5 | 6 | For more information about TipTap, check out [tiptap.dev](https://www.tiptap.dev/). 7 | 8 | https://user-images.githubusercontent.com/45892659/136865410-f13de924-8bb5-4729-b469-6e061a507ab5.mp4 9 | 10 | # Installation 11 | 12 | 1. Install or add django_tiptap to your PythonPath: 13 | 14 | ```bash 15 | pip install django_tiptap 16 | ``` 17 | 18 | 2. Add django_tiptap to your `INSTALLED_APPS` in Djangos `settings.py`: 19 | 20 | ```python 21 | INSTALLED_APPS = [ 22 | # ... 23 | "django_tiptap", 24 | ] 25 | ``` 26 | 27 | # Usage 28 | 29 | ## Field 30 | 31 | To add HTML WYSIWYG text editing capabilities to your models use the `TipTapTextField` 32 | model field. It is a subclass of Djangos `TextField` configured to use the 33 | `TipTapWidget` by default. 34 |
35 | 36 | ```python 37 | from django.db import models 38 | 39 | from django_tiptap.fields import TipTapTextField 40 | 41 | 42 | class Note(models.Model): 43 | content = TipTapTextField() 44 | ``` 45 | 46 | ## Widget 47 | 48 | You can also use the `TipTapWidget` directly when defining a custom form. 49 |
50 | 51 | ```python 52 | 53 | from django import forms 54 | from django.contrib import admin 55 | from django_tiptap.widgets import TipTapWidget 56 | 57 | from demo_app.models import Note 58 | 59 | class NoteAdminForm(forms.ModelForm): 60 | content = forms.CharField(widget=TipTapWidget()) 61 | 62 | class Meta: 63 | model = Note 64 | fields = '__all__' 65 | 66 | class NoteAdmin(admin.ModelAdmin): 67 | form = NoteAdminForm 68 | 69 | admin.site.register(Note, NoteAdmin) 70 | ``` 71 | 72 | ## Configuration 73 | 74 | You can configure some of the editor behaviour by modifying the `DJANGO_TIPTAP_CONFIG` dict in `settings.py`. 75 | 76 | ```python 77 | DJANGO_TIPTAP_CONFIG = { 78 | "width": "500px", 79 | "height": "500px", 80 | "extensions": [ 81 | # to see what each extension does, refer to [tiptap.dev](https://www.tiptap.dev/) 82 | "bold", 83 | "italic", 84 | "underline", 85 | "strikethrough", 86 | "h1", 87 | "h2", 88 | "h3", 89 | "h4", 90 | "h5", 91 | "h6", 92 | "textAlign", 93 | "indent", 94 | "table", 95 | "bulletList", 96 | "orderedList", 97 | "typography", 98 | "clearFormat" 99 | ], 100 | "placeholderText": "Begin typing here...", # set None to skip display 101 | "unsavedChangesWarningText": "You have unsaved changes", # set None to skip display 102 | "lang": "EN", # if you want to use default tooltips and translations, use this. Valid Options => EN/DE(for now) 103 | "tooltips": { 104 | # if you want to use your custom tooltips(maybe because you don't prefer default or the language you want isn't there) 105 | "bold": "Bold | (ctrl / ⌘) + B", 106 | "italic": "Italic | (ctrl / ⌘) + I", 107 | "underline": "Underline | (ctrl / ⌘) + U", 108 | "strike": "Strikethrough | (ctrl / ⌘) + shift + X", 109 | "h1": "Header 1 | (ctrl + alt) / (⌘ + ⌥) + 1", 110 | "h2": "Header 2 | (ctrl + alt) / (⌘ + ⌥) + 2", 111 | "h3": "Header 3 | (ctrl + alt) / (⌘ + ⌥) + 3", 112 | "h4": "Header 4 | (ctrl + alt) / (⌘ + ⌥) + 4", 113 | "h5": "Header 5 | (ctrl + alt) / (⌘ + ⌥) + 5", 114 | "h6": "Header 6 | (ctrl + alt) / (⌘ + ⌥) + 6", 115 | "alignLeft": "Align Left | (ctrl + shift ⇧) / (⌘ + shift ⇧) + L", 116 | "alignCenter": "Align Center | (ctrl + shift ⇧) / (⌘ + shift ⇧) + E", 117 | "alignRight": "Align Right | (ctrl + shift ⇧) / (⌘ + shift ⇧) + R", 118 | "alignJustify": "Justify | (ctrl + shift ⇧) / (⌘ + shift ⇧) + J", 119 | "indent": "Indent (Tab ↹)", 120 | "outdent": "Outdent (shift ⇧ + Tab ↹)", 121 | "bulletList": "Bullet List | (ctrl + shift ⇧) / (⌘ + shift ⇧) + 8", 122 | "orderedList": "Numbered List | (ctrl + shift ⇧) / (⌘ + shift ⇧) + 7", 123 | "addTable": "Add Table", 124 | "deleteTable": "Delete Table", 125 | "addColumnBefore": "Add Column Before", 126 | "addColumnAfter": "Add Column After", 127 | "deleteColumn": "Delete Column", 128 | "addRowBefore": "Add Row Before", 129 | "addRowAfter": "Add Row After", 130 | "deleteRow": "Delete Row", 131 | "mergeCells": "Merge Cells", 132 | "splitCell": "Split Cell", 133 | "toggleHeaderColumn": "Toggle Header Column", 134 | "toggleHeaderRow": "Toggle Header Row", 135 | "toggleHeaderCell": "Toggle Header Cell", 136 | "clearFormat": "Clear Format", 137 | }, 138 | "translations": { 139 | # if the lang you defined exists in the default langs, then no need to define translations 140 | "row": "Row", 141 | "column": "Column", 142 | "add": "Add" 143 | }, 144 | "custom_extensions": [], 145 | "tiptapOutputFormat": "html", # options : "html", "json" 146 | 147 | } 148 | ``` 149 | 150 | ### Custom extensions 151 | 152 | You can specify custom tiptap extensions that should be loaded using the `custom_extensions` config list. 153 | This list contains dictionaries with the following options: 154 | 155 | - `source_static` or `source_url`: where to load the extension from (**mandatory**) 156 | - `module_name` the name of the extension (**mandatory**) 157 | - `configuration_statement` how to configure the extension. Defaults to the `module_name` if not set 158 | - `toolbar_include` path of a html-template to include into the toolbar 159 | - `buttonsconfig_include` path of a js-template to include into the tiptap buttonsconfig 160 | - `css_include` path of a css-template to include into the textarea 161 | 162 | `django_tiptap_demo` contains an example of a custom extension 163 | 164 | # Contributing 165 | 166 | This project is a very rough draft of what a TipTap Editor experience in Django could 167 | look like. If you're missing a feature and want to contribute to this project you are more than 168 | welcome to! 169 | 170 | ## Development 171 | 172 | 1. We use black and isort to auto-format the code base. Both are set up as pre-commit hooks and in the tox testmatrix. 173 | 174 | ```bash 175 | pip install black isort pre-commit 176 | pre-commit install 177 | ``` 178 | 179 | 2. For development purposes it is encouraged to add the `django_tiptap` and 180 | `django_tiptap_demo` modules to your PythonPath. You can either configure this via 181 | your shell of choice or through your IDE. 182 | VSCode users can use the following setting to automatically add the current workspace 183 | to the PythonPath. If you're an OSX/MacOS user, replace `env.linux` with `env.osx`. 184 | 185 | ```json 186 | "terminal.integrated.env.linux": { 187 | "PYTHONPATH": "${workspaceFolder}" 188 | }, 189 | ``` 190 | 191 | 3. Use `make demo-app` to start a Django development server running a demo application. 192 | This is a quick way to check out the `TipTapTextField` on the Django admin page. 193 | 194 | - runs all migrations 195 | - creates a superuser, user: admin, password: admin 196 | - runs the django development server 197 | 198 | Go to http://127.0.0.1:8000/admin/demo_app/note/add/ to interact with the `TipTapTextField` 199 | -------------------------------------------------------------------------------- /django_tiptap/__init__.py: -------------------------------------------------------------------------------- 1 | """TipTap Editor in Django 🚀""" 2 | 3 | __version__ = "0.0.18" 4 | -------------------------------------------------------------------------------- /django_tiptap/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TiptapConfig(AppConfig): 5 | name = "django_tiptap" 6 | -------------------------------------------------------------------------------- /django_tiptap/config.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | TIPTAP_DEFAULT_CONFIG = { 4 | "width": "500px", 5 | "height": "500px", 6 | "extensions": [ 7 | "bold", 8 | "italic", 9 | "underline", 10 | "strikethrough", 11 | "h1", 12 | "h2", 13 | "h3", 14 | "h4", 15 | "h5", 16 | "h6", 17 | "textAlign", 18 | "indent", 19 | "table", 20 | "bulletList", 21 | "orderedList", 22 | "typography", 23 | "clearFormat", 24 | "jinjaSyntaxHighlight", 25 | "link", 26 | ], 27 | "placeholderText": "Begin typing here...", 28 | "unsavedChangesWarningText": None, 29 | "lang": "EN", 30 | "custom_extensions": [], 31 | "tiptapOutputFomat": "html", 32 | } 33 | 34 | TIPTAP_DEFAULT_TOOLTIPS = { 35 | "EN": { 36 | "bold": "Bold | (ctrl / ⌘) + B", 37 | "italic": "Italic | (ctrl / ⌘) + I", 38 | "underline": "Underline | (ctrl / ⌘) + U", 39 | "strike": "Strikethrough | (ctrl / ⌘) + shift + X", 40 | "h1": "Header 1 | (ctrl + alt) / (⌘ + ⌥) + 1", 41 | "h2": "Header 2 | (ctrl + alt) / (⌘ + ⌥) + 2", 42 | "h3": "Header 3 | (ctrl + alt) / (⌘ + ⌥) + 3", 43 | "h4": "Header 4 | (ctrl + alt) / (⌘ + ⌥) + 4", 44 | "h5": "Header 5 | (ctrl + alt) / (⌘ + ⌥) + 5", 45 | "h6": "Header 6 | (ctrl + alt) / (⌘ + ⌥) + 6", 46 | "alignLeft": "Align Left | (ctrl + shift ⇧) / (⌘ + shift ⇧) + L", 47 | "alignCenter": "Align Center | (ctrl + shift ⇧) / (⌘ + shift ⇧) + E", 48 | "alignRight": "Align Right | (ctrl + shift ⇧) / (⌘ + shift ⇧) + R", 49 | "alignJustify": "Justify | (ctrl + shift ⇧) / (⌘ + shift ⇧) + J", 50 | "indent": "Indent (Tab ↹)", 51 | "outdent": "Outdent (shift ⇧ + Tab ↹)", 52 | "bulletList": "Bullet List | (ctrl + shift ⇧) / (⌘ + shift ⇧) + 8", 53 | "orderedList": "Numbered List | (ctrl + shift ⇧) / (⌘ + shift ⇧) + 7", 54 | "addTable": "Add Table", 55 | "deleteTable": "Delete Table", 56 | "addColumnBefore": "Add Column Before", 57 | "addColumnAfter": "Add Column After", 58 | "deleteColumn": "Delete Column", 59 | "addRowBefore": "Add Row Before", 60 | "addRowAfter": "Add Row After", 61 | "deleteRow": "Delete Row", 62 | "mergeCells": "Merge Cells", 63 | "splitCell": "Split Cell", 64 | "toggleHeaderColumn": "Toggle Header Column", 65 | "toggleHeaderRow": "Toggle Header Row", 66 | "toggleHeaderCell": "Toggle Header Cell", 67 | "toggleTableBorders": "Toggle Table Borders", 68 | "clearFormat": "Clear Format", 69 | "jinjaHighlight": "Jinja Highlight", 70 | "rows": "Rows", 71 | "columns": "Columns", 72 | "spellcheck": "Toggle Spellcheck", 73 | "link": "Add Link", 74 | }, 75 | "DE": { 76 | "bold": "Fett | (ctrl / ⌘) + B", 77 | "italic": "Kursiv | (ctrl / ⌘) + I", 78 | "underline": "Unterstrichen | (ctrl / ⌘) + U", 79 | "strike": "Durchstreichen | (ctrl / ⌘) + Umschalt + X", 80 | "h1": "Überschrift 1 | (ctrl + alt) / (⌘ + ⌥) + 1", 81 | "h2": "Überschrift 2 | (ctrl + alt) / (⌘ + ⌥) + 2", 82 | "h3": "Überschrift 3 | (ctrl + alt) / (⌘ + ⌥) + 3", 83 | "h4": "Überschrift 4 | (ctrl + alt) / (⌘ + ⌥) + 4", 84 | "h5": "Überschrift 5 | (ctrl + alt) / (⌘ + ⌥) + 5", 85 | "h6": "Überschrift 6 | (ctrl + alt) / (⌘ + ⌥) + 6", 86 | "alignLeft": "Linksbündig | (ctrl + Umschalt ⇧) / (⌘ + Umschalt ⇧) + L", 87 | "alignCenter": "Zentriert | (ctrl + Umschalt ⇧) / (⌘ + Umschalt ⇧) + E", 88 | "alignRight": "Rechtsbündig | (ctrl + Umschalt ⇧) / (⌘ + Umschalt ⇧) + R", 89 | "alignJustify": "Blocksatz | (ctrl + Umschalt ⇧) / (⌘ + Umschalt ⇧) + J", 90 | "indent": "Einrücken (Tab ↹)", 91 | "outdent": "Ausrücken (Umschalt ⇧ + Tab ↹)", 92 | "bulletList": "Aufzählungsliste | (ctrl + Umschalt ⇧) / (⌘ + Umschalt ⇧) + 8", 93 | "orderedList": "Nummerierte Liste | (ctrl + Umschalt ⇧) / (⌘ + Umschalt ⇧) + 7", 94 | "addTable": "Tabelle hinzufügen", 95 | "deleteTable": "Tabelle löschen", 96 | "addColumnBefore": "Spalte links einfügen", 97 | "addColumnAfter": "Spalte rechts einfügen", 98 | "deleteColumn": "Spalte löschen", 99 | "addRowBefore": "Zeile darüber einfügen", 100 | "addRowAfter": "Zeile darunter einfügen", 101 | "deleteRow": "Zeile löschen", 102 | "mergeCells": "Zellen zusammenführen", 103 | "splitCell": "Zelle aufteilen", 104 | "toggleHeaderColumn": "Kopfspalte ein/ausschalten", 105 | "toggleHeaderRow": "Kopfzeile ein/auschalten", 106 | "toggleHeaderCell": "Kopfzelle ein/auschalten", 107 | "toggleTableBorders": "Ränder ein/ausblenden", 108 | "clearFormat": "Formatierung entfernen", 109 | "jinjaHighlight": "Jinja Markierungen", 110 | "rows": "Zeilen", 111 | "columns": "Column", 112 | "spellcheck": "Rechtschreibprüfung ein/ausschalten", 113 | "link": "Link einfügen", 114 | }, 115 | } 116 | 117 | TIPTAP_DEFAULT_TRANSLATIONS = { 118 | "EN": {"row": "Row", "column": "Column", "add": "Add"}, 119 | "DE": {"row": "Zeile", "column": "Spalte", "add": "Hinzufügen"}, 120 | } 121 | 122 | 123 | def getUpdatedContextForProperty(context: dict, property: str) -> Dict[str, Any]: 124 | ctx = context.copy() 125 | 126 | if property in ctx["widget"]["config"]: 127 | return ctx 128 | elif "lang" in ctx["widget"]["config"]: 129 | langs: list = ["EN", "DE"] 130 | givenLang: str = ctx["widget"]["config"].get("lang") 131 | 132 | if givenLang.upper() in langs: 133 | ctx["widget"]["config"][property] = TIPTAP_DEFAULT_TOOLTIPS.copy()[ 134 | givenLang.upper() 135 | ] 136 | else: 137 | ctx["widget"]["config"][property] = TIPTAP_DEFAULT_TOOLTIPS.copy()["EN"] 138 | print( 139 | "\n *** The language given to django_tiptap was not found, " 140 | "using english as default. *** \n" 141 | ) 142 | else: 143 | ctx["widget"]["config"][property] = TIPTAP_DEFAULT_TOOLTIPS.copy()["EN"] 144 | 145 | return ctx 146 | -------------------------------------------------------------------------------- /django_tiptap/fields.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.forms import fields 3 | 4 | from .widgets import TipTapWidget 5 | 6 | 7 | class TipTapTextFormField(fields.CharField): 8 | def __init__(self, *args, **kwargs): 9 | kwargs.update({"widget": TipTapWidget()}) 10 | super().__init__(*args, **kwargs) 11 | 12 | 13 | class TipTapTextField(models.TextField): 14 | def formfield(self, **kwargs): 15 | return super().formfield(form_class=TipTapTextFormField, **kwargs) 16 | -------------------------------------------------------------------------------- /django_tiptap/static/django_tiptap/css/styles.css: -------------------------------------------------------------------------------- 1 | .text-black { 2 | color: black; 3 | font-size: 16px !important; 4 | } 5 | 6 | .display-flex { 7 | display: -webkit-box; 8 | display: -ms-flexbox; 9 | display: flex; 10 | } 11 | 12 | .flex-align-center { 13 | -webkit-box-align: center; 14 | -ms-flex-align: center; 15 | align-items: center; 16 | } 17 | 18 | .flex-direction-row { 19 | -webkit-box-orient: horizontal; 20 | -webkit-box-direction: normal; 21 | -ms-flex-direction: row; 22 | flex-direction: row; 23 | } 24 | 25 | .flex-direction-column { 26 | -webkit-box-orient: vertical; 27 | -webkit-box-direction: normal; 28 | -ms-flex-direction: column; 29 | flex-direction: column; 30 | } 31 | 32 | .flex-content-space-between { 33 | -webkit-box-pack: justify; 34 | -ms-flex-pack: justify; 35 | justify-content: space-between; 36 | } 37 | 38 | .the-editor-area { 39 | display: flex; 40 | flex-direction: column; 41 | border: 2px solid grey; 42 | resize: both; 43 | overflow-x: auto; 44 | overflow-y: hidden; 45 | min-height: fit-content; 46 | max-width: 65vw; 47 | background: white; 48 | color: black; 49 | position: relative; 50 | border-radius: 6px; 51 | } 52 | 53 | .the-editor-area::after { 54 | content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' style='width:24px;height:24px' viewBox='0 0 24 24'%3E%3Cpath d='M22,22H20V20H22V22M22,18H20V16H22V18M18,22H16V20H18V22M18,18H16V16H18V18M14,22H12V20H14V22M22,14H20V12H22V14Z' /%3E%3C/svg%3E"); 55 | position: absolute; 56 | color: white; 57 | bottom: 0; 58 | right: 0; 59 | width: 1em; 60 | height: 1em; 61 | z-index: 10000; 62 | display: flex; 63 | justify-content: center; 64 | align-items: center; 65 | transform: scale(2) translate(-12px, -2px); 66 | font-size: 1.3em; 67 | } 68 | 69 | .the-editor-area::-webkit-resizer { 70 | transform: scale(3); 71 | } 72 | 73 | .editor-content-rendered { 74 | max-height: 85vh; 75 | overflow-y: auto; 76 | } 77 | 78 | .editor-content-rendered.jinjaSyntaxHighlightActivated .jinja-syntax { 79 | background: rgba(0, 255, 0, 0.205) !important; 80 | border-bottom: 1px solid rgb(0, 38, 255) !important; 81 | margin-bottom: -1px !important; 82 | } 83 | 84 | .menubar { 85 | display: none; 86 | } 87 | 88 | .menubar.show { 89 | display: -webkit-box; 90 | display: -ms-flexbox; 91 | display: flex; 92 | -ms-flex-wrap: wrap; 93 | flex-wrap: wrap; 94 | justify-content: center; 95 | border-bottom: 2px solid grey; 96 | padding: 1rem 0.5rem; 97 | } 98 | 99 | .menubar button { 100 | margin: 0 4px 2px 2px; 101 | height: 30px; 102 | min-width: 30px; 103 | font-size: 1.3em; 104 | background-color: white; 105 | border: none; 106 | border-radius: 4px; 107 | outline: none; 108 | cursor: pointer; 109 | } 110 | 111 | .menubar button:not([id$="-addTable"]) { 112 | display: flex; 113 | justify-content: center; 114 | align-items: center; 115 | } 116 | 117 | .menubar button:hover { 118 | background-color: rgba(87, 87, 87, 0.199); 119 | -webkit-transition: none; 120 | transition: none; 121 | } 122 | 123 | .menubar button.disabled:hover { 124 | cursor: not-allowed; 125 | } 126 | 127 | .menubar button.disabled svg { 128 | fill: #ced4da; 129 | } 130 | 131 | .menubar button.is-active { 132 | background-color: #586b84; 133 | } 134 | 135 | .menubar button.is-active svg { 136 | fill: white; 137 | } 138 | 139 | .menubar button[id$="-addTable"] svg { 140 | transform: translateY(2px); 141 | } 142 | 143 | .menubar button[id$="-addTable"]:hover .table-config { 144 | display: -webkit-box; 145 | display: -ms-flexbox; 146 | display: flex; 147 | -webkit-box-orient: vertical; 148 | -webkit-box-direction: normal; 149 | -ms-flex-direction: column; 150 | flex-direction: column; 151 | cursor: initial; 152 | } 153 | 154 | .menubar button[id$="-addTable"]:hover .table-config .add-table-button { 155 | font-size: 1.2rem; 156 | margin: 5px 0 5px 0; 157 | background-color: #586b84; 158 | cursor: pointer; 159 | border-radius: 6px; 160 | color: white; 161 | padding: 4px; 162 | } 163 | 164 | .menubar button[id$="-addTable"] .table-config { 165 | display: none; 166 | position: absolute; 167 | background-color: white; 168 | z-index: 10000; 169 | -webkit-box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3); 170 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3); 171 | padding: 1em; 172 | border-radius: 4px; 173 | transform: translate(-1em, 0); 174 | } 175 | 176 | .menubar button[id$="-addTable"] .table-config input { 177 | height: 1.5rem; 178 | border-radius: 6px; 179 | border: 2px solid grey; 180 | font-size: 1.2rem; 181 | min-width: 100px; 182 | padding: 4px; 183 | } 184 | 185 | .menubar button[id$="-addTable"] .table-config input:first-of-type { 186 | margin-right: 4px; 187 | } 188 | 189 | .menubar button[id$="-addTable"] .table-config input:last-of-type { 190 | margin-left: 4px; 191 | } 192 | 193 | .menubar button[id$="-addTable"] .table-config .inputs-container { 194 | -webkit-box-align: center; 195 | -ms-flex-align: center; 196 | align-items: center; 197 | margin-bottom: 1em; 198 | } 199 | 200 | .table-dimension-input { 201 | width: 40%; 202 | } 203 | 204 | .menubar 205 | button[id$="-addTable"] 206 | .table-config 207 | .inputs-container 208 | .table-dimension-input { 209 | color: black; 210 | background-color: white; 211 | } 212 | 213 | .not-saved-text { 214 | display: none; 215 | } 216 | 217 | .not-saved-text.show { 218 | display: inline; 219 | color: #ee0e29; 220 | font-weight: bold; 221 | font-size: 1.3em; 222 | height: 100%; 223 | align-self: center; 224 | margin: 0 0 0 0.5rem; 225 | } 226 | 227 | .not-saved-text-spacer { 228 | display: none; 229 | } 230 | 231 | .not-saved-text-spacer.show { 232 | display: block; 233 | } 234 | 235 | .ProseMirror { 236 | color: black !important; 237 | padding: 1rem 1.75rem; 238 | outline: none !important; 239 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 240 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 241 | margin: 0 !important; 242 | } 243 | 244 | .ProseMirror * { 245 | color: black !important; 246 | } 247 | 248 | .ProseMirror p { 249 | font-size: 16px !important; 250 | } 251 | 252 | .ProseMirror h1 { 253 | display: block !important; 254 | font-size: 2em !important; 255 | margin-block-start: 0.67em !important; 256 | margin-block-end: 0.67em !important; 257 | margin-inline-start: 0px !important; 258 | margin-inline-end: 0px !important; 259 | font-weight: bold !important; 260 | } 261 | 262 | .ProseMirror h2 { 263 | padding: 0; 264 | display: block !important; 265 | font-size: 1.5em !important; 266 | margin-block-start: 0.83em !important; 267 | margin-block-end: 0.83em !important; 268 | margin-inline-start: 0px !important; 269 | margin-inline-end: 0px !important; 270 | font-weight: bold !important; 271 | background: white !important; 272 | } 273 | 274 | .ProseMirror h3 { 275 | display: block !important; 276 | font-size: 1.17em !important; 277 | margin-block-start: 1em !important; 278 | margin-block-end: 1em !important; 279 | margin-inline-start: 0px !important; 280 | margin-inline-end: 0px !important; 281 | font-weight: bold !important; 282 | padding: 0 !important; 283 | } 284 | 285 | .ProseMirror > * + * { 286 | margin-top: 0.75em; 287 | } 288 | 289 | .ProseMirror p.is-editor-empty:first-child::before { 290 | content: attr(data-placeholder); 291 | float: left; 292 | color: #a8a8a8; 293 | pointer-events: none; 294 | height: 0; 295 | } 296 | 297 | .ProseMirror ol { 298 | padding-left: 0px !important; 299 | list-style-type: decimal !important; 300 | } 301 | 302 | .ProseMirror ol li::marker { 303 | font-size: 16px !important; 304 | } 305 | 306 | .ProseMirror ol > li > ol { 307 | padding-left: 10px !important; 308 | margin-left: 10px !important; 309 | list-style-type: lower-latin !important; 310 | } 311 | 312 | .ProseMirror ol > li > ol > li > ol { 313 | padding-left: 10px !important; 314 | margin-left: 10px !important; 315 | list-style-type: lower-roman !important; 316 | } 317 | 318 | .ProseMirror ol > li > ol > li > ol > li > ol { 319 | padding-left: 10px !important; 320 | margin-left: 10px !important; 321 | list-style-type: upper-latin !important; 322 | } 323 | 324 | .ProseMirror ol > li > ol > li > ol > li > ol > li > ol { 325 | padding-left: 10px !important; 326 | margin-left: 10px !important; 327 | list-style-type: upper-roman !important; 328 | } 329 | 330 | .ProseMirror ul { 331 | list-style-type: disc !important; 332 | margin-block-start: 0 !important; 333 | margin-block-end: 0 !important; 334 | margin-inline-start: 0px !important; 335 | margin-inline-end: 0px !important; 336 | } 337 | 338 | .ProseMirror ul li { 339 | list-style: disc !important; 340 | } 341 | 342 | .ProseMirror ul > li > ul { 343 | padding-left: 10px !important; 344 | margin-left: 10px !important; 345 | list-style-type: disc !important; 346 | } 347 | 348 | .ProseMirror { 349 | margin: 1rem 0; 350 | } 351 | 352 | .ProseMirror > * + * { 353 | margin-top: 0.75em; 354 | } 355 | 356 | .ProseMirror h1, 357 | .ProseMirror h2, 358 | .ProseMirror h3, 359 | .ProseMirror h4, 360 | .ProseMirror h5, 361 | .ProseMirror h6 { 362 | line-height: 1.1; 363 | text-transform: none; 364 | padding: 0; 365 | } 366 | 367 | .ProseMirror pre { 368 | background: #0d0d0d !important; 369 | color: #fff !important; 370 | font-family: "JetBrainsMono", monospace !important; 371 | padding: 0.75rem 1rem !important; 372 | border-radius: 0.5rem !important; 373 | } 374 | 375 | .ProseMirror pre code { 376 | color: #fff !important; 377 | padding: 0 !important; 378 | background: none !important; 379 | font-size: 0.8rem !important; 380 | } 381 | 382 | .ProseMirror img { 383 | max-width: 100%; 384 | height: auto; 385 | } 386 | 387 | .ProseMirror blockquote { 388 | padding-left: 1rem; 389 | border-left: 2px solid rgba(13, 13, 13, 0.1); 390 | } 391 | 392 | .ProseMirror hr { 393 | border: none; 394 | border-top: 2px solid rgba(13, 13, 13, 0.1); 395 | margin: 2rem 0; 396 | } 397 | 398 | .ProseMirror a { 399 | text-decoration: underline; 400 | } 401 | 402 | /* Table-specific styling */ 403 | .ProseMirror table { 404 | border-collapse: collapse; 405 | table-layout: fixed; 406 | width: 100%; 407 | margin: 0; 408 | overflow: hidden; 409 | } 410 | 411 | .ProseMirror table td, 412 | .ProseMirror table th, 413 | .ProseMirror table[show_borders="false"]:hover td, 414 | .ProseMirror table[show_borders="false"]:hover th { 415 | min-width: 1em; 416 | border: 2px solid #ced4da; 417 | padding: 3px 5px; 418 | vertical-align: top; 419 | box-sizing: border-box; 420 | position: relative; 421 | } 422 | 423 | .ProseMirror table[show_borders="false"] td, 424 | .ProseMirror table[show_borders="false"] th { 425 | border: none; 426 | box-sizing: border-box; 427 | } 428 | 429 | .ProseMirror table td > *, 430 | .ProseMirror table th > * { 431 | margin-bottom: 0; 432 | } 433 | 434 | .ProseMirror table th { 435 | font-weight: bold; 436 | text-align: left; 437 | background-color: #f1f3f5; 438 | } 439 | 440 | .ProseMirror tr { 441 | background-color: white !important; 442 | } 443 | 444 | .ProseMirror table .selectedCell:after { 445 | z-index: 2; 446 | position: absolute; 447 | content: ""; 448 | left: 0; 449 | right: 0; 450 | top: 0; 451 | bottom: 0; 452 | background: rgba(200, 200, 255, 0.4); 453 | pointer-events: none; 454 | } 455 | 456 | .ProseMirror table .column-resize-handle { 457 | position: absolute; 458 | right: -2px; 459 | top: 0; 460 | bottom: -2px; 461 | width: 4px; 462 | background-color: #adf; 463 | pointer-events: none; 464 | } 465 | 466 | .tableWrapper { 467 | overflow-x: auto; 468 | } 469 | 470 | .resize-cursor { 471 | cursor: ew-resize; 472 | cursor: col-resize; 473 | } 474 | 475 | div[data-tippy-root] { 476 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, 477 | Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 478 | } 479 | 480 | .selectedCell:after { 481 | z-index: 2; 482 | position: absolute; 483 | content: ""; 484 | left: 0; 485 | right: 0; 486 | top: 0; 487 | bottom: 0; 488 | background: rgba(200, 200, 255, 0.4); 489 | pointer-events: none; 490 | } 491 | 492 | .column-resize-handle { 493 | position: absolute; 494 | right: -2px; 495 | top: 0; 496 | bottom: -2px; 497 | width: 4px; 498 | background-color: #adf; 499 | pointer-events: none; 500 | cursor: col-resize !important; 501 | } 502 | 503 | .tableWrapper { 504 | padding: 1rem 0; 505 | overflow-x: auto; 506 | } 507 | 508 | .ProseMirror.resize-cursor { 509 | cursor: ew-resize !important; 510 | cursor: col-resize !important; 511 | } 512 | 513 | .spacer { 514 | height: 30px; 515 | border-right: 2px solid grey; 516 | margin: 0 6px 0 6px; 517 | } 518 | -------------------------------------------------------------------------------- /django_tiptap/templates/forms/tiptap_textarea.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 7 | 8 |
9 | 356 | 357 |
358 |
359 | 360 | 361 | 362 | 1614 | 1621 | -------------------------------------------------------------------------------- /django_tiptap/widgets.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from django import forms 4 | from django.conf import settings 5 | 6 | from .config import TIPTAP_DEFAULT_CONFIG, getUpdatedContextForProperty 7 | 8 | 9 | class TipTapWidget(forms.Textarea): 10 | class Media: 11 | css = {"all": ("django_tiptap/css/styles.css",)} 12 | 13 | template_name = "forms/tiptap_textarea.html" 14 | 15 | def __init__(self, config: dict = None, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | self.config = self.set_config(config) 18 | 19 | @staticmethod 20 | def set_config(config: dict = None) -> dict: 21 | # Take default config and update it with the user config from settings.py. 22 | 23 | config = config.copy() if config else TIPTAP_DEFAULT_CONFIG.copy() 24 | django_settings_config = getattr(settings, "DJANGO_TIPTAP_CONFIG", {}) 25 | config.update(django_settings_config) 26 | 27 | return config 28 | 29 | def get_context(self, *args, **kwargs) -> Dict[str, Any]: 30 | context = super().get_context(*args, **kwargs) 31 | context["widget"]["config"] = self.config 32 | 33 | context = getUpdatedContextForProperty(context, "tooltips") 34 | 35 | context = getUpdatedContextForProperty(context, "translations") 36 | 37 | return context 38 | -------------------------------------------------------------------------------- /django_tiptap_demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-tiptap/django_tiptap/38b314f3b7ec0239a83752d2c857d0076e4cb4d0/django_tiptap_demo/__init__.py -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-tiptap/django_tiptap/38b314f3b7ec0239a83752d2c857d0076e4cb4d0/django_tiptap_demo/demo_app/__init__.py -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Note 4 | 5 | # Register your models here. 6 | admin.site.register(Note) 7 | -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.4 on 2022-12-09 10:55 2 | 3 | from django.db import migrations, models 4 | 5 | import django_tiptap.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Note", 17 | fields=[ 18 | ( 19 | "id", 20 | models.BigAutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("content", django_tiptap.fields.TipTapTextField()), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-tiptap/django_tiptap/38b314f3b7ec0239a83752d2c857d0076e4cb4d0/django_tiptap_demo/demo_app/migrations/__init__.py -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from django_tiptap.fields import TipTapTextField 4 | 5 | 6 | # Create your models here. 7 | class Note(models.Model): 8 | content = TipTapTextField() 9 | -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/static/my_custom_extension/my_custom_extension.js: -------------------------------------------------------------------------------- 1 | import { Extension } from 'https://cdn.skypack.dev/pin/@tiptap/core@v2.0.0-beta.29-ehyNSRjShPZE49ePTtXD/mode=imports,min/optimized/@tiptap/core.js' 2 | 3 | export const MyCustomExtension = Extension.create({ 4 | name: 'mycustomExtension', 5 | 6 | // Your code goes here. 7 | }) 8 | -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/templates/my_custom_extension/my_custom_extension.css: -------------------------------------------------------------------------------- 1 | /* css goes here */ 2 | -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/templates/my_custom_extension/my_custom_extension_button_config.js: -------------------------------------------------------------------------------- 1 | ['{{ widget.name }}-myCustomExtension']: { 2 | command: (editor) => { 3 | // your code here 4 | }, 5 | canBeActive: false, 6 | tooltip: '{{ widget.config.tooltips.myCustomExtension }}', 7 | } 8 | -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/templates/my_custom_extension/my_custom_extension_toolbar_include.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /django_tiptap_demo/demo_app/tests.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def fix(): 6 | return "Hello" 7 | 8 | 9 | def test_dummy(fix): 10 | assert fix 11 | -------------------------------------------------------------------------------- /django_tiptap_demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_tiptap_demo.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /django_tiptap_demo/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_tiptap project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "django-insecure-qx8%yn$yy1@!^+xxzjp-oiu-1=hao4yucza5ljym!zxuadu8)7" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "django_tiptap", 41 | "django_tiptap_demo.demo_app", 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | "django.middleware.security.SecurityMiddleware", 46 | "django.contrib.sessions.middleware.SessionMiddleware", 47 | "django.middleware.common.CommonMiddleware", 48 | "django.middleware.csrf.CsrfViewMiddleware", 49 | "django.contrib.auth.middleware.AuthenticationMiddleware", 50 | "django.contrib.messages.middleware.MessageMiddleware", 51 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 52 | ] 53 | 54 | ROOT_URLCONF = "django_tiptap_demo.urls" 55 | 56 | TEMPLATES = [ 57 | { 58 | "BACKEND": "django.template.backends.django.DjangoTemplates", 59 | "DIRS": [], 60 | "APP_DIRS": True, 61 | "OPTIONS": { 62 | "context_processors": [ 63 | "django.template.context_processors.debug", 64 | "django.template.context_processors.request", 65 | "django.contrib.auth.context_processors.auth", 66 | "django.contrib.messages.context_processors.messages", 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = "django_tiptap_demo.wsgi.application" 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 77 | 78 | DATABASES = { 79 | "default": { 80 | "ENGINE": "django.db.backends.sqlite3", 81 | "NAME": str(BASE_DIR / "db.sqlite3"), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 92 | }, 93 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 94 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 95 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 96 | ] 97 | 98 | 99 | # Internationalization 100 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 101 | 102 | LANGUAGE_CODE = "en-us" 103 | 104 | TIME_ZONE = "UTC" 105 | 106 | USE_I18N = True 107 | 108 | USE_L10N = True 109 | 110 | USE_TZ = True 111 | 112 | 113 | # Static files (CSS, JavaScript, Images) 114 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 115 | 116 | STATIC_URL = "/static/" 117 | 118 | # Default primary key field type 119 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 120 | 121 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 122 | 123 | 124 | DJANGO_TIPTAP_CONFIG = { 125 | "width": "500px", 126 | "height": "500px", 127 | "extensions": [ 128 | "bold", 129 | "italic", 130 | "underline", 131 | "strikethrough", 132 | "h1", 133 | "h2", 134 | "h3", 135 | "h4", 136 | "h5", 137 | "h6", 138 | "textAlign", 139 | "indent", 140 | "table", 141 | "bulletList", 142 | "orderedList", 143 | "typography", 144 | "clearFormat", 145 | "jinjaSyntaxHighlight", 146 | "hardBreak", 147 | "link", 148 | ], 149 | "placeholderText": "Begin typing here...", # set None to skip display 150 | "unsavedChangesWarningText": "You have unsaved changes!", # set None to skip display 151 | "lang": "EN", 152 | "custom_extensions": [ 153 | { 154 | "source_static": "my_custom_extension/my_custom_extension.js", 155 | "module_name": "MyCustomExtension", 156 | "toolbar_include": "my_custom_extension/my_custom_extension_toolbar_include.html", 157 | "buttonsconfig_include": "my_custom_extension/my_custom_extension_button_config.js", 158 | "css_include": "my_custom_extension/my_custom_extension.css", 159 | } 160 | ], 161 | "tiptapOutputFormat": "html", 162 | } 163 | -------------------------------------------------------------------------------- /django_tiptap_demo/urls.py: -------------------------------------------------------------------------------- 1 | """django_tiptap URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import path 20 | from django.views.generic.base import RedirectView 21 | 22 | urlpatterns = [ 23 | path("admin/", admin.site.urls, name="admin"), 24 | path("", RedirectView.as_view(url="admin")), 25 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 26 | -------------------------------------------------------------------------------- /django_tiptap_demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_tiptap project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_tiptap_demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_tiptap_demo.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=2,<4", "setuptools"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "django_tiptap" 7 | author = "Jeet Mandaliya & Max Patzelt" 8 | author-email = "django.tiptap@gmail.com" 9 | classifiers = [ "License :: OSI Approved :: MIT License",] 10 | description-file = "README.md" 11 | requires = [ 12 | "Django >= 2.2", 13 | ] 14 | requires-python=">=3.5" 15 | 16 | [tool.flit.metadata.requires-extra] 17 | dev = [ 18 | "black", 19 | "isort", 20 | "pre-commit", 21 | ] 22 | 23 | test = [ 24 | "pytest", 25 | ] 26 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | 4 | [isort] 5 | multi_line_output = 3 6 | include_trailing_comma = True 7 | force_grid_wrap = 0 8 | use_parentheses = True 9 | ensure_newline_before_comments = True 10 | line_length = 88 11 | 12 | [tool:pytest] 13 | python_files = tests.py test_*.py *_tests.py 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | isolated_build = True 3 | skip_missing_interpreters=True 4 | envlist= 5 | py37-django{22,30,31,32} 6 | py38-django{22,30,31,32} 7 | py39-django{22,30,31,32} 8 | 9 | [gh-actions] 10 | python = 11 | 3.7: py37 12 | 3.8: py38 13 | 3.9: py39 14 | 15 | [testenv] 16 | setenv= 17 | DJANGO_SETTINGS_MODULE=django_tiptap_demo.settings 18 | deps = 19 | black 20 | isort 21 | pytest 22 | pytest-coverage 23 | django22: Django>=2.2,<2.3 24 | django30: Django>=3.0,<3.1 25 | django31: Django>=3.1,<3.2 26 | django32: Django>=3.2,<3.3 27 | commands = 28 | isort --profile=black django_tiptap django_tiptap_demo 29 | black . 30 | {envbindir}/coverage erase 31 | {envbindir}/coverage run --include=django_tiptap_demo/* -m pytest -ra django_tiptap_demo/ 32 | {envbindir}/coverage report -m 33 | --------------------------------------------------------------------------------