replace button link block with a separate button link tool inside draftail

This commit is contained in:
2026-07-05 00:51:30 +02:00
parent b0698247b6
commit 6f1d531d5d
25 changed files with 620 additions and 219 deletions
-48
View File
@@ -1,4 +1,3 @@
from django.core.exceptions import ValidationError
from grapple.helpers import register_streamfield_block
from grapple.models import (
GraphQLImage,
@@ -8,7 +7,6 @@ from grapple.models import (
GraphQLString,
)
from wagtail import blocks
from wagtail.blocks import StructBlockValidationError
from wagtail.embeds.blocks import EmbedBlock
from wagtail.images.blocks import ImageChooserBlock
@@ -283,51 +281,6 @@ class ScheduleBlock(blocks.StructBlock):
label = "Timeplan"
@register_streamfield_block
class ButtonLinkBlock(blocks.StructBlock):
text = blocks.CharBlock(
max_length=64,
required=True,
label="Tekst",
help_text="Teksten som vises på knappen.",
)
page = blocks.PageChooserBlock(
required=False,
label="Intern side",
help_text="Velg en underside på denne nettsiden.",
)
anchor = blocks.CharBlock(
max_length=64,
required=False,
label="Anker",
help_text="Anker på siden, uten «#» (valgfritt)",
)
external_url = blocks.URLBlock(
required=False,
label="Ekstern lenke",
)
graphql_fields = [
GraphQLString("text", required=True),
GraphQLPage("page"),
GraphQLString("anchor"),
GraphQLString("external_url"),
]
def clean(self, value):
result = super().clean(value)
if value.get("page") and value.get("external_url"):
error = ValidationError(
"Velg enten en intern side eller en ekstern lenke, ikke begge."
)
raise StructBlockValidationError(block_errors={"page": error, "external_url": error})
return result
class Meta:
icon = "link"
label = "Knapp med lenke"
BASE_BLOCKS = [
("paragraph", blocks.RichTextBlock(label="Rik tekst")),
("image", ImageWithTextBlock()),
@@ -338,7 +291,6 @@ BASE_BLOCKS = [
("accordion", AccordionBlock()),
("fact_box", FactBoxBlock()),
("schedule", ScheduleBlock()),
("button_link", ButtonLinkBlock()),
("embed", EmbedBlock()),
("raw_html", blocks.RawHTMLBlock()),
]
+92
View File
@@ -0,0 +1,92 @@
"""Inline "button" link for Draftail rich text fields.
Stored as `<a linktype="button" data-href="...">label</a>`. The linktype (not a
plain href) avoids colliding with the built-in link feature, and lets
expand_db_html rewrite it to `<a class="button" href="...">` for the front end.
Wired up in wagtail_hooks.py.
"""
from django.urls import reverse_lazy
from django.utils.html import escape
from draftjs_exporter.dom import DOM
from wagtail.admin.rich_text.converters.html_to_contentstate import (
InlineEntityElementHandler,
)
from wagtail.admin.rich_text.editors.draftail import features as draftail_features
from wagtail.rich_text import LinkHandler
FEATURE_NAME = "button-link"
ENTITY_TYPE = "BUTTON"
def button_link_entity_decorator(props):
"""ContentState to DB HTML (keeps just the chosen URL)."""
return DOM.create_element(
"a",
{"linktype": "button", "data-href": props.get("url", "")},
props["children"],
)
class ButtonLinkEntityElementHandler(InlineEntityElementHandler):
"""DB HTML to ContentState."""
mutability = "MUTABLE"
def get_attribute_data(self, attrs):
return {"url": attrs.get("data-href", "")}
class ButtonLinkRichTextHandler(LinkHandler):
"""DB HTML to front-end HTML (expand_db_html)."""
identifier = "button"
@classmethod
def expand_db_attributes(cls, attrs):
href = escape(attrs.get("data-href", ""))
return f'<a class="button" href="{href}">'
def register_button_link_feature(features):
features.register_editor_plugin(
"draftail",
FEATURE_NAME,
draftail_features.EntityFeature(
{
"type": ENTITY_TYPE,
"icon": "arrow-right-full",
"description": "Knapp",
# Reuse the link chooser modal + payload, so buttons work like links.
"attributes": ["url", "id", "parentId"],
"allowlist": {"href": "^(http:|https:|mailto:|tel:|#|/|undefined$)"},
"chooserUrls": {
"pageChooser": reverse_lazy("wagtailadmin_choose_page"),
"externalLinkChooser": reverse_lazy("wagtailadmin_choose_page_external_link"),
"emailLinkChooser": reverse_lazy("wagtailadmin_choose_page_email_link"),
"phoneLinkChooser": reverse_lazy("wagtailadmin_choose_page_phone_link"),
"anchorLinkChooser": reverse_lazy("wagtailadmin_choose_page_anchor_link"),
},
},
js=[
# Gives LinkModalWorkflowSource its PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS.
"wagtailadmin/js/page-chooser-modal.js",
"js/draftail-button-link.js",
],
css={"all": ["css/draftail-button-link.css"]},
),
)
features.register_converter_rule(
"contentstate",
FEATURE_NAME,
{
"from_database_format": {
'a[linktype="button"]': ButtonLinkEntityElementHandler(ENTITY_TYPE),
},
"to_database_format": {
"entity_decorators": {ENTITY_TYPE: button_link_entity_decorator},
},
},
)
features.register_link_type(ButtonLinkRichTextHandler)
features.default_features.append(FEATURE_NAME)
@@ -0,0 +1,17 @@
/* Styles the button-link entity as a button in the editor. Front end uses the global .button class. */
.button-link-entity .TooltipEntity {
display: inline-flex;
align-items: center;
gap: 0.25em;
padding: 0.1em 0.7em;
border: 1.5px solid currentColor;
border-radius: 999px;
font-weight: 600;
text-decoration: none;
line-height: 1.4;
}
.button-link-entity .TooltipEntity__icon {
width: 0.85em;
height: 0.85em;
}
@@ -0,0 +1,40 @@
/*
* Draftail plugin for the "button-link" feature (see dnscms/rich_text.py).
* Reuses Wagtail's link chooser (source) and edit/remove tooltip (decorator),
* so buttons insert and edit like normal links. No build step: uses Wagtail's
* globals and React.createElement.
*/
(function () {
"use strict";
var draftail = window.draftail;
var React = window.React;
var Icon = window.wagtail.components.Icon;
var TooltipEntity = draftail.TooltipEntity;
// Wrap TooltipEntity so CSS can style it as a button
function ButtonLink(props) {
var url = props.contentState.getEntity(props.entityKey).getData().url || null;
return React.createElement(
"span",
{ className: "button-link-entity" },
React.createElement(
TooltipEntity,
Object.assign({}, props, {
icon: React.createElement(Icon, { name: "arrow-right-full" }),
label: url || "",
url: url,
})
)
);
}
draftail.registerPlugin(
{
type: "BUTTON",
source: draftail.LinkModalWorkflowSource,
decorator: ButtonLink,
},
"entityTypes"
);
})();
+7
View File
@@ -5,12 +5,19 @@ from wagtail import hooks
from wagtail.models import Page
from wagtail.search.backends import get_search_backend
from dnscms.rich_text import register_button_link_feature
@hooks.register("register_rich_text_features")
def enable_additional_rich_text_features(features):
features.default_features.extend(["h5", "h6", "blockquote"])
@hooks.register("register_rich_text_features")
def register_button_link(features):
register_button_link_feature(features)
@hooks.register("register_schema_query")
def override_search_resolver(query_mixins):
"""