forked from rc/aircox
integrate tiptap text editor
This commit is contained in:
parent
55123c386d
commit
93fba2d46d
|
@ -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")
|
||||
|
|
|
@ -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
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
{% 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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
132
assets/src/components/AEditor.vue
Normal file
132
assets/src/components/AEditor.vue
Normal 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>
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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); }
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user