integrate tiptap text editor

This commit is contained in:
bkfox 2024-04-30 18:29:34 +02:00
parent 55123c386d
commit 93fba2d46d
21 changed files with 327 additions and 149 deletions

View File

@ -2,6 +2,7 @@ import os
import inspect
from bleach import sanitizer
from django.conf import settings as d_settings
@ -179,5 +180,10 @@ class Settings(BaseSettings):
ALLOW_COMMENTS = True
"""Allow comments."""
# ---- bleach
ALLOWED_TAGS = [*sanitizer.ALLOWED_TAGS, "br", "p", "h3", "h4", "h5"]
ALLOWED_ATTRIBUTES = sanitizer.ALLOWED_ATTRIBUTES
ALLOWED_PROTOCOLS = sanitizer.ALLOWED_PROTOCOLS
settings = Settings("AIRCOX")

View File

@ -13,6 +13,7 @@ from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
from model_utils.managers import InheritanceQuerySet
from ..conf import settings
from .station import Station
__all__ = (
@ -120,6 +121,14 @@ class BasePage(Renderable, models.Model):
return "{}".format(self.title or self.pk)
def save(self, *args, **kwargs):
if self.content:
self.content = bleach.clean(
self.content,
tags=settings.ALLOWED_TAGS,
attributes=settings.ALLOWED_ATTRIBUTES,
protocols=settings.ALLOWED_PROTOCOLS,
)
if not self.slug:
self.slug = slugify(self.title)[:100]
count = Page.objects.filter(slug__startswith=self.slug).count()
@ -165,17 +174,6 @@ class BasePage(Renderable, models.Model):
headline[-1] += suffix
return mark_safe(" ".join(headline))
_url_re = re.compile(
"((http|https)\:\/\/)?[a-zA-Z0-9\.\/\?\:@\-_=#]+\.([a-zA-Z]){2,6}([a-zA-Z0-9\.\&\/\?\:@\-_=#])*"
)
@cached_property
def display_content(self):
if "<p>" in self.content:
return self.content
content = self._url_re.sub(r'<a href="\1" target="new">\1</a>', self.content)
return content.replace("\n\n", "\n").replace("\n", "<br>")
@classmethod
def get_init_kwargs_from(cls, page, **kwargs):
kwargs.setdefault("cover", page.cover)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -136,9 +136,9 @@ Usefull context:
{% block content-container %}
{% if page and page.content %}
<section class="container content page-content">
<section class="container no-reset content page-content">
{% block content %}
{{ page.display_content|safe }}
{{ page.content|safe }}
{% endblock %}
</section>
{% endif %}

View File

@ -18,6 +18,12 @@ aircox.labels = {% inline_labels %}
{% endif %}
{% endblock %}
{% block title-container %}
{{ block.super }}
{% block page-actions %}
{% include "aircox/widgets/page_actions.html" %}
{% endblock %}
{% endblock %}
{% block content-container %}
<a-select-file ref="cover-select"
@ -102,8 +108,11 @@ aircox.labels = {% inline_labels %}
<div>
<div class="field {% if field.name != "content" %}is-horizontal{% endif %}">
<label class="label">{{ field.label }}</label>
<div class="control clear-unset">
<div class="control clear-unset no-reset">
<a-editor name="{{ field.name }}" initial="{{ field.value }}"/>
{% comment %}
<textarea name="{{ field.name }}" class="is-fullwidth height-25">{{ field.value|default:""|striptags|safe }}</textarea>
{% endcomment %}
</div>
<p class="help">{{ field.help_text }}</p>
</div>

View File

@ -2,24 +2,24 @@
{% block user-actions-container %}
{% if user.is_authenticated %}
{{ object.get_status_display }}
{{ object.get_status_display|capfirst }}
{% if object.pub_date %}
({{ object.pub_date|date:"d/m/Y H:i" }})
{% endif %}
{% endif %}
{% if user.is_authenticated and can_edit %}
{% if user.is_authenticated %}
{% with request.resolver_match.view_name as view_name %}
&nbsp;
{% if "-edit" in view_name %}
{% if request.path != object.get_absolute_url %}
<a href="{% url view_name|detail_view page.slug %}" target="_self" title="{% translate 'View' %} {{ page }}">
<span class="icon">
<i class="fa-regular fa-eye"></i>
</span>
<span>{% translate 'View' %} </span>
</a>
{% else %}
{% elif can_edit %}
<a href="{% url view_name|edit_view page.pk %}" target="_self" title="{% translate 'Edit' %} {{ page }}">
<span class="icon">
<i class="fa-solid fa-pencil"></i>

View File

@ -55,6 +55,9 @@ class EpisodeUpdateView(UserPassesTestMixin, VueFormDataMixin, PageUpdateView):
form_class = forms.EpisodeForm
template_name = "aircox/episode_form.html"
def can_edit(self, obj):
return self.test_func()
def test_func(self):
obj = self.get_object()
return permissions.program.can(self.request.user, "update", obj)

View File

@ -33,6 +33,17 @@ def attach(cls):
return cls
class CanEditMixin:
"""Add context 'can_edit' set to True when object is editable by user."""
def can_edit(self, object):
"""Return True if user can edit current page."""
return False
def get_context_data(self, **kwargs):
return super().get_context_data(can_edit=self.can_edit(self.object), **kwargs)
class BasePageMixin:
category = None
@ -156,16 +167,12 @@ class PageListView(FiltersMixin, BasePageListView):
return super().get_context_data(**kwargs)
class PageDetailView(BasePageDetailView):
class PageDetailView(CanEditMixin, BasePageDetailView):
"""Base view class for pages."""
template_name = None
context_object_name = "page"
def can_edit(self, object):
"""Return True if user can edit current page."""
return False
def get_template_names(self):
return super().get_template_names() + ["aircox/page_detail.html"]
@ -185,7 +192,6 @@ class PageDetailView(BasePageDetailView):
if related:
related = related[: self.related_count]
kwargs["related_objects"] = related
kwargs["can_edit"] = self.can_edit(self.object)
return super().get_context_data(**kwargs)
def get_comment_form(self):
@ -210,7 +216,7 @@ class PageDetailView(BasePageDetailView):
return self.get(request, *args, **kwargs)
class PageCreateView(BaseView, CreateView):
class PageCreateView(CanEditMixin, BaseView, CreateView):
def get_page(self):
return self.object
@ -218,7 +224,7 @@ class PageCreateView(BaseView, CreateView):
return self.request.path
class PageUpdateView(BaseView, UpdateView):
class PageUpdateView(CanEditMixin, BaseView, UpdateView):
def get_page(self):
return self.object

View File

@ -71,6 +71,9 @@ class ProgramCreateView(PermissionRequiredMixin, ProgramEditMixin, page.PageCrea
class ProgramUpdateView(UserPassesTestMixin, ProgramEditMixin, page.PageUpdateView):
def can_edit(self, obj):
return self.test_func()
def test_func(self):
obj = self.get_object()
return permissions.program.can(self.request.user, "update", obj)

View File

@ -20,6 +20,11 @@
"vue": "^3.4.21"
},
"devDependencies": {
"@tiptap/extension-link": "^2.3.0",
"@tiptap/extension-underline": "^2.3.0",
"@tiptap/pm": "^2.3.0",
"@tiptap/starter-kit": "^2.3.0",
"@tiptap/vue-3": "^2.3.0",
"@vitejs/plugin-vue": "^5.0.4",
"bulma": "^0.9.4",
"eslint": "^7.32.0",

View File

@ -0,0 +1,132 @@
<template>
<input ref="input" type="hidden" :name="name" :value="value"/>
<div class="">
<template v-for="group, index in menu" :key="index">
<div class="button-group d-inline-block mr-3">
<template v-for="info, index in group" :key="index">
<button type="button" class="button square smaller" :title="info.label" @click="edit(info.action, ...(info.args || []))">
<span class="icon"><i :class="info.icon"/></span>
</button>
</template>
</div>
</template>
<div class="button-group d-inline-block">
<div class="dropdown is-hoverable">
<div class="dropdown-trigger">
<button type="button" class="button square smaller">
<span class="icon"><i class="fa fa-link"/></span>
</button>
</div>
<div class="dropdown-menu" style="min-width: 20rem; margin-top: -0.2rem;">
<div class="dropdown-content p-3">
<div class="field">
<label class="label">Lien</label>
<div class="control">
<input ref="link-url" type="text" class="input" placeholder="lien"/>
</div>
</div>
<div class="has-text-right">
<button type="button" class="button secondary"
@click="edit('setLink', {href:$refs['link-url'].value})">
Ajouter le lien
</button>
</div>
</div>
</div>
</div>
<button type="button" class="button square smaller" title="Remove link" @click="edit('unsetLink')">
<span class="icon"><i class="fa fa-link-slash"/></span>
</button>
</div>
</div>
<editor-content class="editor" v-if="editor" :editor="editor" />
</template>
<style>
.editor .tiptap {
border: 1px black solid;
padding: 0.3em;
}
.editor .tiptap ul, .editor .tiptap ol {
margin-left: 1.3em;
}
.editor .tiptap ul { list-style: disc }
</style>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import Link from '@tiptap/extension-link'
export default {
components: {EditorContent},
props: {
config: {type: Object, default: (() => {})},
//! Input field name.
name: String,
//! Initial input value
initial: String,
},
data() {
return {
editor: null,
menu: [
[
{label: "Bold", icon: "fa fa-bold", action: "toggleBold" },
{label: "Italic", icon: "fa fa-italic", action: "toggleItalic" },
{label: "Underline", icon: "fa fa-underline", action: "toggleUnderline" },
{label: "Strike", icon: "fa fa-strikethrough", action: "toggleStrike" },
],[
{label: "List", icon: "fa fa-list", action: "toggleBulletList" },
{label: "Ordered List", icon: "fa fa-list-ol", action: "toggleOrderedList" },
],[
{label: "Heading 1", icon: "fa fa-h", action: "setHeading", args: [{level:3}] },
{label: "Heading 2", icon: "fa fa-h smaller", action: "toggleHeading", args: [{level:4}] },
// {label: "Heading 3", icon: "fa fa-h small", action: "toggleHeading", args: [{level:5}] },
],
]
}
},
computed: {
value() { return this.editor && this.editor.getHTML() },
},
methods: {
chain(action, ...args) {
let chain = this.editor.chain().focus()
return chain[action](...args)
},
edit(action, ...args) {
this.chain(action, ...args).run()
},
setLink() {
this.edit("setLink", {href: this.$refs['link-url']})
},
},
mounted() {
this.editor = new Editor({
content: this.initial || "",
injectCss: false,
extensions: [
StarterKit.configure({
heading: {
levels: [3, 4, 5]
}
}),
Underline,
Link.configure({autolink: true}),
],
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>

View File

@ -6,6 +6,7 @@ import AStreamer from './AStreamer.vue'
import AFormSet from './AFormSet.vue'
import ATrackListEditor from './ATrackListEditor.vue'
import ASoundListEditor from './ASoundListEditor.vue'
import AEditor from './AEditor.vue'
import AManyToManyEdit from "./AManyToManyEdit.vue"
@ -15,7 +16,7 @@ import base from "./index.js"
export const admin = {
...base,
AManyToManyEdit,
AFileUpload, ASelectFile,
AFileUpload, ASelectFile, AEditor,
AFormSet, ATrackListEditor, ASoundListEditor,
AStatistics, AStreamer,
}

View File

@ -3,7 +3,8 @@
:root {
--title-1-sz: 1.6rem;
--title-2-sz: 1.4rem;
--title-3-sz: 1.2rem;
--title-3-sz: 1.3rem;
--title-4-sz: 1.2rem;
--subtitle-1-sz: 1.6rem;
--subtitle-2-sz: 1.4rem;
--subtitle-3-sz: 1.2rem;
@ -96,6 +97,14 @@
}
// ---- headings
.no-reset h1 { font-size: var(--title-1-sz); }
.no-reset h2 { font-size: var(--title-2-sz); }
.no-reset h3 { font-size: var(--title-3-sz); }
.no-reset h3 { font-size: var(--title-3-sz); }
.no-reset h4 { font-size: var(--title-4-sz); }
.no-reset h5 { font-size: var(--title-5-sz); }
.title, .header.preview .title {
&.is-1 { font-size: var(--title-1-sz); }
&.is-2 { font-size: var(--title-2-sz); }
@ -215,6 +224,10 @@
&:last-child { border-right: 0px; }
}
}
.button-group + .button-group {
border-left: 1px solid var(--text-color-light);
}
}

View File

@ -138,6 +138,9 @@
.bg-secondary-light { background-color: var(--secondary-color-light); }
.bg-transparent { background-color: transparent; }
.border { border: 1px solid var(--text-color); }
.border-main { border: 1px solid var(--main-color); }
.border-secondary { border: 1px solid var(--secondary-color); }
.border-bottom-main { border-bottom: 1px solid var(--main-color); }
.border-bottom-secondary { border-bottom: 1px solid var(--secondary-color); }

View File

@ -77,99 +77,6 @@ except Exception:
# -- django-taggit
TAGGIT_CASE_INSENSITIVE = True
# -- django-CKEditor
CKEDITOR_CONFIGS = {
"default": {
"format_tags": "h1;h2;h3;p;pre",
# 'skin': 'office2013',
"toolbar_Custom": [
{
"name": "editing",
"items": [
"Undo",
"Redo",
"-",
"Find",
"Replace",
"-",
"Source",
],
},
{
"name": "basicstyles",
"items": [
"Bold",
"Italic",
"Underline",
"Strike",
"Subscript",
"Superscript",
"-",
"RemoveFormat",
],
},
{
"name": "paragraph",
"items": [
"NumberedList",
"BulletedList",
"-",
"Outdent",
"Indent",
"-",
"Blockquote",
"CreateDiv",
"-",
"JustifyLeft",
"JustifyCenter",
"JustifyRight",
"JustifyBlock",
"-",
],
},
"/",
{"name": "links", "items": ["Link", "Unlink", "Anchor"]},
{
"name": "insert",
"items": [
"Image",
"Table",
"HorizontalRule",
"SpecialChar",
"PageBreak",
"Iframe",
],
},
{
"name": "styles",
"items": ["Styles", "Format", "Font", "FontSize"],
},
{"name": "colors", "items": ["TextColor", "BGColor"]},
"/", # put this to force next toolbar on new line
],
"toolbar": "Custom",
"extraPlugins": ",".join(
[
"uploadimage",
"div",
"autolink",
"autoembed",
"embedsemantic",
"embed",
"iframe",
"iframedialog",
"autogrow",
"widget",
"lineutils",
"dialog",
"dialogui",
"elementspath",
]
),
},
}
CKEDITOR_UPLOAD_PATH = "uploads/"
# -- easy_thumbnails
THUMBNAIL_PROCESSORS = (
@ -190,8 +97,6 @@ INSTALLED_APPS = (
"rest_framework",
"django_filters",
"content_editor",
"ckeditor",
"ckeditor_uploader",
"easy_thumbnails",
"filer",
"taggit",

View File

@ -8,7 +8,6 @@ django-filer~=3.1
django-honeypot~=1.0
django-taggit~=3.0
django-admin-sortable2~=2.1
django-ckeditor~=6.4
bleach~=5.0
easy-thumbnails~=2.8
tzlocal~=4.2