├── .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 |