@panneau/editor 0.0.0-stage → 5.0.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -2
- package/dist/LinkDialog.d.ts +18 -0
- package/dist/LinkDialog.d.ts.map +1 -0
- package/dist/LinkDialog.js +220 -0
- package/dist/LinkDialog.js.map +1 -0
- package/dist/RichTextField.css +257 -0
- package/dist/RichTextField.d.ts +43 -0
- package/dist/RichTextField.d.ts.map +1 -0
- package/dist/RichTextField.js +361 -0
- package/dist/RichTextField.js.map +1 -0
- package/dist/RichTextField.module.js +28 -0
- package/dist/RichTextField.module.js.map +1 -0
- package/dist/Toolbar.d.ts +20 -0
- package/dist/Toolbar.d.ts.map +1 -0
- package/dist/Toolbar.js +203 -0
- package/dist/Toolbar.js.map +1 -0
- package/dist/buttons.d.ts +23 -0
- package/dist/buttons.d.ts.map +1 -0
- package/dist/buttons.js +117 -0
- package/dist/buttons.js.map +1 -0
- package/dist/extensions/attributes.d.ts +44 -0
- package/dist/extensions/attributes.d.ts.map +1 -0
- package/dist/extensions/attributes.js +138 -0
- package/dist/extensions/attributes.js.map +1 -0
- package/dist/extensions/index.d.ts +19 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +96 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/extensions/marks.d.ts +4 -0
- package/dist/extensions/marks.d.ts.map +1 -0
- package/dist/extensions/marks.js +62 -0
- package/dist/extensions/marks.js.map +1 -0
- package/dist/extensions/paste.d.ts +32 -0
- package/dist/extensions/paste.d.ts.map +1 -0
- package/dist/extensions/paste.js +131 -0
- package/dist/extensions/paste.js.map +1 -0
- package/dist/extensions/raw.d.ts +23 -0
- package/dist/extensions/raw.d.ts.map +1 -0
- package/dist/extensions/raw.js +269 -0
- package/dist/extensions/raw.js.map +1 -0
- package/dist/html.d.ts +21 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +51 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/plugin.d.ts +26 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +46 -0
- package/dist/plugin.js.map +1 -0
- package/dist/settings.d.ts +22 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +45 -0
- package/dist/settings.js.map +1 -0
- package/dist/styles.d.ts +16 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +86 -0
- package/dist/styles.js.map +1 -0
- package/package.json +71 -5
package/README.md
CHANGED
|
@@ -1,3 +1,130 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @panneau/editor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The rich text field of Panneau, on [TipTap 3](https://tiptap.dev): HTML storage, a toolbar and block styles set from the definition, and extensions from plugins ([decision 0007](../../docs/decisions/0007-rich-text.md)).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @panneau/editor@^5.0.0-alpha.0 @tiptap/core @tiptap/pm @tiptap/react react-aria-components react-intl
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
| Export | Content |
|
|
10
|
+
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
11
|
+
| `@panneau/editor` | `RichTextField`, `RichTextFieldEntry`, `editorPlugin`, `createEditorPlugin()`, `createRichTextExtensions()`, `normalizeRichText()`, `getRichTextOptions()`, `builtInButtons`, `DEFAULT_TOOLBAR`, `DEFAULT_HEADINGS`, and their types |
|
|
12
|
+
|
|
13
|
+
TipTap's core, ProseMirror (`@tiptap/pm`) and its React bindings are peer dependencies: the extensions of a project and the field share one copy of them.
|
|
14
|
+
|
|
15
|
+
## The field
|
|
16
|
+
|
|
17
|
+
`RichTextField` (the `richtext` type) edits HTML. Its value is a string, or `null` when the text is empty. Like every field, it is controlled: a value that comes from elsewhere (a reset, another user) replaces its content.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
import { editorPlugin } from '@panneau/editor';
|
|
21
|
+
import { FieldsProvider, builtInFields } from '@panneau/fields';
|
|
22
|
+
|
|
23
|
+
createPanneauApp({ plugins: [editorPlugin], … });
|
|
24
|
+
|
|
25
|
+
// Or with the pieces:
|
|
26
|
+
<FieldsProvider components={{ ...builtInFields, ...editorPlugin.fields }}>…</FieldsProvider>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Without the plugin, `DefinitionField` shows a placeholder for `richtext` fields, and keeps their value.
|
|
30
|
+
|
|
31
|
+
### Settings
|
|
32
|
+
|
|
33
|
+
The definition sets the field in its `settings` (`RichTextSettings` of `@panneau/protocol`), with no JavaScript:
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
{
|
|
37
|
+
"name": "body",
|
|
38
|
+
"type": "richtext",
|
|
39
|
+
"settings": {
|
|
40
|
+
"toolbar": [
|
|
41
|
+
"heading",
|
|
42
|
+
"style",
|
|
43
|
+
"|",
|
|
44
|
+
"bold",
|
|
45
|
+
"italic",
|
|
46
|
+
"link",
|
|
47
|
+
"bulletList",
|
|
48
|
+
"orderedList",
|
|
49
|
+
"|",
|
|
50
|
+
"logo",
|
|
51
|
+
],
|
|
52
|
+
"headings": [2, 3],
|
|
53
|
+
"styles": [{ "label": "Big paragraph", "class": "big", "element": "p" }],
|
|
54
|
+
"links": { "newTab": "external" },
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **`toolbar`** lists the buttons, in order, with `|` between groups: `heading` (paragraph or a heading level), `style`, `bold`, `italic`, `underline`, `strike`, `superscript`, `subscript`, `code`, `link`, `bulletList`, `orderedList`, `blockquote`, `horizontalRule`, `clear`, `undo`, `redo`, and the buttons of plugins. Names it doesn't know are skipped. Defaults to `DEFAULT_TOOLBAR`: text style, style, bold, italic, link and lists.
|
|
60
|
+
- **`headings`** are the levels that `heading` offers. Defaults to 2, 3 and 4.
|
|
61
|
+
- **`styles`** are block styles: a named class on an element, `p` by default, or `h1` to `h6`, `blockquote`, `ul`, `ol`, `li`. The `style` select puts one on the paragraphs and headings of the selection (or the closest quote, list or item), in place of the other styles of that element, and keeps their other classes: "Big paragraph" gives `<p class="big">`.
|
|
62
|
+
- **`links.newTab`**: `choose` (the default) adds "Open in a new tab" to the link dialog, `external` opens every link to another site (`https://…`, `//…`) in a new tab, and `never` opens none. A link in a new tab gets `target="_blank" rel="noopener noreferrer"`, and keeps its other relations (`nofollow`).
|
|
63
|
+
|
|
64
|
+
`getRichTextOptions(field)` reads these settings, and falls back to the defaults for settings of the wrong type. `RichTextField` takes them as props: `toolbar`, `headings`, `styles` and `newTab`.
|
|
65
|
+
|
|
66
|
+
### What the field keeps
|
|
67
|
+
|
|
68
|
+
The toolbar only limits what users add. The field reads everything content may hold, so that HTML written by another editor, CKEditor 4 or 5 in Panneau 4 projects, keeps working:
|
|
69
|
+
|
|
70
|
+
- **Until the text is edited, the value is not touched**: the field gives nothing back when it loads content.
|
|
71
|
+
- Once it is edited, the field writes HTML that renders the same:
|
|
72
|
+
- every heading level, whatever `headings` offers;
|
|
73
|
+
- the attributes of paragraphs, headings, quotes, lists, list items, links and marks (`class`, `style`, `id`, `lang`, `data-*`);
|
|
74
|
+
- marks with the element they were written with (`<b>` and `<strong>`, `<i>` and `<em>`, as CKEditor 5 writes italics);
|
|
75
|
+
- list items written without a `<p>` (`<li>Text</li>`), as CKEditor writes them;
|
|
76
|
+
- elements around text with their attributes: `<span style="…">`, `<abbr title="…">`, anchors.
|
|
77
|
+
- **HTML that the field doesn't edit is kept as it was written**, whitespace included: embedded media (`<figure class="media">` of CKEditor 5, with or without its preview), iframes, images, tables, `<div>`, `<pre>`, social posts with their script. The editor shows such a block by its element and what it points to (`figure.media`, its URL), and never renders its markup: its scripts don't run and its iframes don't load in the admin. It can be moved or removed.
|
|
78
|
+
- What changes, without changing what the HTML renders: entities become characters (`é` gives `é`), whitespace between blocks goes, attributes of a link come in their order (`href`, `target`, `rel`), and styles are written as a browser writes them (`color: rgb(231, 76, 60);`).
|
|
79
|
+
- **Nothing that would run a script reaches the admin** ([decision 0035](../../docs/decisions/0035-rich-text-kept-html.md)): kept HTML is only written to the value and the clipboard, never to the page, even when the editor unmounts. The elements the editor renders lose their event handlers (`onclick`), `srcdoc`, and URL attributes with a `javascript:`, `vbscript:` or `data:text/html` URL, when content loads and again when they are rendered; links to scripts lose their link. Styles of stored content show in the editor but can't cover the admin: the text contains its painting (`contain: paint`). They stay in the value. Sanitizing HTML for a site's pages is the site's job, or its backend's.
|
|
80
|
+
- **Pasted content is cleaned**: a page or a document brings its text, headings, lists, links and marks, not its styles, classes and wrappers (`<span style="font-size: 11pt">` and `<div>` of Google Docs or a web page). Bold and italic written as styles become marks. Content copied from a rich text field of the same page pastes in full, kept HTML included: the field marks what it copies with a random token, which no other page can write. Without it, the slice data of ProseMirror (`data-pm-slice`) is dropped.
|
|
81
|
+
|
|
82
|
+
`normalizeRichText(html, extensions)` gives the HTML the field would save for some HTML once edited. Run it on existing content before a migration, with the extensions of the project's plugin, to see what would change.
|
|
83
|
+
|
|
84
|
+
## Extensions and buttons of a project
|
|
85
|
+
|
|
86
|
+
A project adds TipTap extensions, for custom nodes such as an inline logo, and the toolbar buttons that insert them, through `createEditorPlugin()`. The `toolbar` setting of each field names the buttons it shows. The extensions are in every rich text field, so that content written with them reads everywhere.
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { Node, mergeAttributes } from '@tiptap/core';
|
|
90
|
+
import { Sparkles } from 'lucide-react';
|
|
91
|
+
|
|
92
|
+
import { type RichTextButton, createEditorPlugin } from '@panneau/editor';
|
|
93
|
+
|
|
94
|
+
const Logo = Node.create({
|
|
95
|
+
name: 'logo',
|
|
96
|
+
group: 'inline',
|
|
97
|
+
inline: true,
|
|
98
|
+
atom: true,
|
|
99
|
+
parseHTML: () => [{ tag: 'span[data-logo]' }],
|
|
100
|
+
renderHTML: ({ HTMLAttributes }) => [
|
|
101
|
+
'span',
|
|
102
|
+
mergeAttributes(HTMLAttributes, { class: 'logo', 'data-logo': '' }),
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const logoButton: RichTextButton = {
|
|
107
|
+
name: 'logo', // in the `toolbar` setting
|
|
108
|
+
label: 'Logo', // or a message descriptor of the plugin's locales
|
|
109
|
+
icon: Sparkles,
|
|
110
|
+
run: (editor) => editor.chain().focus().insertContent({ type: 'logo' }).run(),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
createPanneauApp({
|
|
114
|
+
plugins: [createEditorPlugin({ extensions: [Logo], buttons: [logoButton] })],
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- An extension with the name of a built-in one (`link`, `heading`) replaces it, and a button with the name of a built-in one replaces it.
|
|
119
|
+
- A button with `isActive` is a toggle; `isDisabled` greys it out for the selection.
|
|
120
|
+
- A project gives all its extensions to one plugin: a later plugin that provides `richtext` replaces the field.
|
|
121
|
+
- The nodes of extensions are read before the HTML that the field keeps: without the plugin, a logo stays in the content as it was written.
|
|
122
|
+
- A node renders its own attributes: one that keeps attributes from stored content filters them as the field does (no `on*` attributes, no `javascript:` URLs).
|
|
123
|
+
|
|
124
|
+
## The toolbar and the editor
|
|
125
|
+
|
|
126
|
+
- The toolbar is a React Aria `Toolbar`: arrow keys move between its controls. Its buttons are labelled and show their label as a tooltip, and its toggles are pressed when the selection has their mark. It doesn't show when the field is read-only.
|
|
127
|
+
- The text is a `textbox` labelled by the field's label, with its heading, lists and links styled from the `--panneau-*` tokens, like `Prose` of `@panneau/ui`.
|
|
128
|
+
- ProseMirror JSON storage, also allowed by decision 0007, is not available yet.
|
|
129
|
+
|
|
130
|
+
The package layout, scripts and API reports are explained in [`docs/packages.md`](../../docs/packages.md).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/core';
|
|
2
|
+
import type { NewTabLinks } from './settings.js';
|
|
3
|
+
/** Whether a URL points to another site: `https://…` or `//…`, as CKEditor decides it. */
|
|
4
|
+
export declare function isExternalUrl(url: string): boolean;
|
|
5
|
+
/** The `target` and `rel` of a link, keeping the relations that aren't about the new tab. */
|
|
6
|
+
export declare function getLinkTarget(newTab: boolean, rel: string | null): {
|
|
7
|
+
target: string | null;
|
|
8
|
+
rel: string | null;
|
|
9
|
+
};
|
|
10
|
+
export interface LinkDialogProps {
|
|
11
|
+
editor: Editor;
|
|
12
|
+
newTab: NewTabLinks;
|
|
13
|
+
isOpen: boolean;
|
|
14
|
+
onOpenChange: (isOpen: boolean) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Adds, changes or removes the link of the selection. */
|
|
17
|
+
export declare function LinkDialog({ editor, newTab, isOpen, onOpenChange }: LinkDialogProps): import("react").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=LinkDialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LinkDialog.d.ts","sourceRoot":"","sources":["../src/LinkDialog.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAQ3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,6FAA6F;AAC7F,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;;;EAMhE;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C;AAED,0DAA0D;AAC1D,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,eAAe,+BA8GnF"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import RichTextField_module_default from "./RichTextField.module.js";
|
|
2
|
+
import { c } from "react/compiler-runtime";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { editorMessages } from "@panneau/intl";
|
|
5
|
+
import { Button, Checkbox, Dialog, TextInput, usePanneauIntl } from "@panneau/ui";
|
|
6
|
+
import { isAllowedUri } from "@tiptap/extension-link";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
//#region src/LinkDialog.tsx
|
|
9
|
+
var NEW_TAB_REL = ["noopener", "noreferrer"];
|
|
10
|
+
/** Whether a URL points to another site: `https://…` or `//…`, as CKEditor decides it. */
|
|
11
|
+
function isExternalUrl(url) {
|
|
12
|
+
return /^(https?:)?\/\//i.test(url.trim());
|
|
13
|
+
}
|
|
14
|
+
/** The `target` and `rel` of a link, keeping the relations that aren't about the new tab. */
|
|
15
|
+
function getLinkTarget(newTab, rel) {
|
|
16
|
+
const others = (rel ?? "").split(/\s+/).filter((name) => name !== "" && !NEW_TAB_REL.includes(name));
|
|
17
|
+
const next = newTab ? [...NEW_TAB_REL, ...others] : others;
|
|
18
|
+
return {
|
|
19
|
+
target: newTab ? "_blank" : null,
|
|
20
|
+
rel: next.length > 0 ? next.join(" ") : null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Adds, changes or removes the link of the selection. */
|
|
24
|
+
function LinkDialog(t0) {
|
|
25
|
+
const $ = c(43);
|
|
26
|
+
const { editor, newTab, isOpen, onOpenChange } = t0;
|
|
27
|
+
const intl = usePanneauIntl();
|
|
28
|
+
const formId = useId();
|
|
29
|
+
let t1;
|
|
30
|
+
if ($[0] !== editor) {
|
|
31
|
+
t1 = editor.getAttributes("link");
|
|
32
|
+
$[0] = editor;
|
|
33
|
+
$[1] = t1;
|
|
34
|
+
} else t1 = $[1];
|
|
35
|
+
const current = t1;
|
|
36
|
+
const currentHref = typeof current.href === "string" ? current.href : "";
|
|
37
|
+
const currentRel = typeof current.rel === "string" ? current.rel : null;
|
|
38
|
+
const [href, setHref] = useState(currentHref);
|
|
39
|
+
const [openInNewTab, setOpenInNewTab] = useState(current.target === "_blank");
|
|
40
|
+
const [invalid, setInvalid] = useState(false);
|
|
41
|
+
const [opened, setOpened] = useState(isOpen);
|
|
42
|
+
if (opened !== isOpen) {
|
|
43
|
+
setOpened(isOpen);
|
|
44
|
+
if (isOpen) {
|
|
45
|
+
setHref(currentHref);
|
|
46
|
+
setOpenInNewTab(current.target === "_blank");
|
|
47
|
+
setInvalid(false);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let t2;
|
|
51
|
+
if ($[2] !== currentRel || $[3] !== editor || $[4] !== href || $[5] !== newTab || $[6] !== onOpenChange || $[7] !== openInNewTab) {
|
|
52
|
+
t2 = () => {
|
|
53
|
+
const url = href.trim();
|
|
54
|
+
if (url === "") {
|
|
55
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
56
|
+
onOpenChange(false);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (!isAllowedUri(url)) {
|
|
60
|
+
setInvalid(true);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const attributes = {
|
|
64
|
+
href: url,
|
|
65
|
+
...getLinkTarget(newTab === "choose" ? openInNewTab : newTab === "external" && isExternalUrl(url), currentRel)
|
|
66
|
+
};
|
|
67
|
+
if (editor.state.selection.empty && !editor.isActive("link")) editor.chain().focus().insertContent({
|
|
68
|
+
type: "text",
|
|
69
|
+
text: url,
|
|
70
|
+
marks: [{
|
|
71
|
+
type: "link",
|
|
72
|
+
attrs: attributes
|
|
73
|
+
}]
|
|
74
|
+
}).run();
|
|
75
|
+
else editor.chain().focus().extendMarkRange("link").setLink(attributes).run();
|
|
76
|
+
onOpenChange(false);
|
|
77
|
+
};
|
|
78
|
+
$[2] = currentRel;
|
|
79
|
+
$[3] = editor;
|
|
80
|
+
$[4] = href;
|
|
81
|
+
$[5] = newTab;
|
|
82
|
+
$[6] = onOpenChange;
|
|
83
|
+
$[7] = openInNewTab;
|
|
84
|
+
$[8] = t2;
|
|
85
|
+
} else t2 = $[8];
|
|
86
|
+
const apply = t2;
|
|
87
|
+
let t3;
|
|
88
|
+
if ($[9] !== intl) {
|
|
89
|
+
t3 = intl.formatMessage(editorMessages.linkTitle);
|
|
90
|
+
$[9] = intl;
|
|
91
|
+
$[10] = t3;
|
|
92
|
+
} else t3 = $[10];
|
|
93
|
+
let t4;
|
|
94
|
+
if ($[11] !== currentHref || $[12] !== editor || $[13] !== formId || $[14] !== intl) {
|
|
95
|
+
t4 = (close) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
96
|
+
currentHref !== "" ? /* @__PURE__ */ jsx(Button, {
|
|
97
|
+
appearance: "ghost",
|
|
98
|
+
tone: "danger",
|
|
99
|
+
className: RichTextField_module_default.removeLink,
|
|
100
|
+
onPress: () => {
|
|
101
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
102
|
+
close();
|
|
103
|
+
},
|
|
104
|
+
children: intl.formatMessage(editorMessages.linkRemove)
|
|
105
|
+
}) : null,
|
|
106
|
+
/* @__PURE__ */ jsx(Button, {
|
|
107
|
+
appearance: "outline",
|
|
108
|
+
onPress: close,
|
|
109
|
+
children: intl.formatMessage(editorMessages.linkCancel)
|
|
110
|
+
}),
|
|
111
|
+
/* @__PURE__ */ jsx(Button, {
|
|
112
|
+
type: "submit",
|
|
113
|
+
form: formId,
|
|
114
|
+
appearance: "solid",
|
|
115
|
+
tone: "accent",
|
|
116
|
+
children: intl.formatMessage(editorMessages.linkApply)
|
|
117
|
+
})
|
|
118
|
+
] });
|
|
119
|
+
$[11] = currentHref;
|
|
120
|
+
$[12] = editor;
|
|
121
|
+
$[13] = formId;
|
|
122
|
+
$[14] = intl;
|
|
123
|
+
$[15] = t4;
|
|
124
|
+
} else t4 = $[15];
|
|
125
|
+
let t5;
|
|
126
|
+
if ($[16] !== apply) {
|
|
127
|
+
t5 = (event) => {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
apply();
|
|
130
|
+
};
|
|
131
|
+
$[16] = apply;
|
|
132
|
+
$[17] = t5;
|
|
133
|
+
} else t5 = $[17];
|
|
134
|
+
let t6;
|
|
135
|
+
if ($[18] !== intl) {
|
|
136
|
+
t6 = intl.formatMessage(editorMessages.linkUrl);
|
|
137
|
+
$[18] = intl;
|
|
138
|
+
$[19] = t6;
|
|
139
|
+
} else t6 = $[19];
|
|
140
|
+
let t7;
|
|
141
|
+
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
|
142
|
+
t7 = (next) => {
|
|
143
|
+
setHref(next);
|
|
144
|
+
setInvalid(false);
|
|
145
|
+
};
|
|
146
|
+
$[20] = t7;
|
|
147
|
+
} else t7 = $[20];
|
|
148
|
+
let t8;
|
|
149
|
+
if ($[21] !== intl || $[22] !== invalid) {
|
|
150
|
+
t8 = invalid ? [intl.formatMessage(editorMessages.linkInvalid)] : [];
|
|
151
|
+
$[21] = intl;
|
|
152
|
+
$[22] = invalid;
|
|
153
|
+
$[23] = t8;
|
|
154
|
+
} else t8 = $[23];
|
|
155
|
+
let t9;
|
|
156
|
+
if ($[24] !== href || $[25] !== t6 || $[26] !== t8) {
|
|
157
|
+
t9 = /* @__PURE__ */ jsx(TextInput, {
|
|
158
|
+
label: t6,
|
|
159
|
+
type: "url",
|
|
160
|
+
inputMode: "url",
|
|
161
|
+
autoComplete: "off",
|
|
162
|
+
autoFocus: true,
|
|
163
|
+
value: href,
|
|
164
|
+
onChange: t7,
|
|
165
|
+
errors: t8
|
|
166
|
+
});
|
|
167
|
+
$[24] = href;
|
|
168
|
+
$[25] = t6;
|
|
169
|
+
$[26] = t8;
|
|
170
|
+
$[27] = t9;
|
|
171
|
+
} else t9 = $[27];
|
|
172
|
+
let t10;
|
|
173
|
+
if ($[28] !== intl || $[29] !== newTab || $[30] !== openInNewTab) {
|
|
174
|
+
t10 = newTab === "choose" ? /* @__PURE__ */ jsx(Checkbox, {
|
|
175
|
+
isSelected: openInNewTab,
|
|
176
|
+
onChange: setOpenInNewTab,
|
|
177
|
+
children: intl.formatMessage(editorMessages.linkNewTab)
|
|
178
|
+
}) : null;
|
|
179
|
+
$[28] = intl;
|
|
180
|
+
$[29] = newTab;
|
|
181
|
+
$[30] = openInNewTab;
|
|
182
|
+
$[31] = t10;
|
|
183
|
+
} else t10 = $[31];
|
|
184
|
+
let t11;
|
|
185
|
+
if ($[32] !== formId || $[33] !== t10 || $[34] !== t5 || $[35] !== t9) {
|
|
186
|
+
t11 = /* @__PURE__ */ jsxs("form", {
|
|
187
|
+
id: formId,
|
|
188
|
+
className: RichTextField_module_default.linkForm,
|
|
189
|
+
onSubmit: t5,
|
|
190
|
+
children: [t9, t10]
|
|
191
|
+
});
|
|
192
|
+
$[32] = formId;
|
|
193
|
+
$[33] = t10;
|
|
194
|
+
$[34] = t5;
|
|
195
|
+
$[35] = t9;
|
|
196
|
+
$[36] = t11;
|
|
197
|
+
} else t11 = $[36];
|
|
198
|
+
let t12;
|
|
199
|
+
if ($[37] !== isOpen || $[38] !== onOpenChange || $[39] !== t11 || $[40] !== t3 || $[41] !== t4) {
|
|
200
|
+
t12 = /* @__PURE__ */ jsx(Dialog, {
|
|
201
|
+
title: t3,
|
|
202
|
+
size: "sm",
|
|
203
|
+
isOpen,
|
|
204
|
+
onOpenChange,
|
|
205
|
+
footer: t4,
|
|
206
|
+
children: t11
|
|
207
|
+
});
|
|
208
|
+
$[37] = isOpen;
|
|
209
|
+
$[38] = onOpenChange;
|
|
210
|
+
$[39] = t11;
|
|
211
|
+
$[40] = t3;
|
|
212
|
+
$[41] = t4;
|
|
213
|
+
$[42] = t12;
|
|
214
|
+
} else t12 = $[42];
|
|
215
|
+
return t12;
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
export { LinkDialog, getLinkTarget, isExternalUrl };
|
|
219
|
+
|
|
220
|
+
//# sourceMappingURL=LinkDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LinkDialog.js","names":["Editor","isAllowedUri","useId","useState","editorMessages","messages","Button","Checkbox","Dialog","TextInput","usePanneauIntl","styles","NewTabLinks","NEW_TAB_REL","isExternalUrl","url","test","trim","getLinkTarget","newTab","rel","others","split","filter","name","includes","next","target","length","join","LinkDialogProps","editor","isOpen","onOpenChange","LinkDialog","t0","$","_c","intl","formId","t1","getAttributes","current","currentHref","href","currentRel","setHref","openInNewTab","setOpenInNewTab","invalid","setInvalid","opened","setOpened","t2","chain","focus","extendMarkRange","unsetLink","run","inNewTab","attributes","state","selection","empty","isActive","insertContent","type","text","marks","attrs","setLink","apply","t3","formatMessage","linkTitle","t4","close","removeLink","linkRemove","linkCancel","linkApply","t5","event","preventDefault","t6","linkUrl","t7","Symbol","for","t8","linkInvalid","t9","t10","linkNewTab","t11","linkForm","t12","_c","messages","_Fragment","styles"],"sources":["../src/LinkDialog.tsx"],"sourcesContent":["import type { Editor } from '@tiptap/core';\nimport { isAllowedUri } from '@tiptap/extension-link';\nimport { useId, useState } from 'react';\n\nimport { editorMessages as messages } from '@panneau/intl';\nimport { Button, Checkbox, Dialog, TextInput, usePanneauIntl } from '@panneau/ui';\n\nimport styles from './RichTextField.module.css';\nimport type { NewTabLinks } from './settings';\n\nconst NEW_TAB_REL = ['noopener', 'noreferrer'];\n\n/** Whether a URL points to another site: `https://…` or `//…`, as CKEditor decides it. */\nexport function isExternalUrl(url: string): boolean {\n return /^(https?:)?\\/\\//i.test(url.trim());\n}\n\n/** The `target` and `rel` of a link, keeping the relations that aren't about the new tab. */\nexport function getLinkTarget(newTab: boolean, rel: string | null) {\n const others = (rel ?? '')\n .split(/\\s+/)\n .filter((name) => name !== '' && !NEW_TAB_REL.includes(name));\n const next = newTab ? [...NEW_TAB_REL, ...others] : others;\n return { target: newTab ? '_blank' : null, rel: next.length > 0 ? next.join(' ') : null };\n}\n\nexport interface LinkDialogProps {\n editor: Editor;\n newTab: NewTabLinks;\n isOpen: boolean;\n onOpenChange: (isOpen: boolean) => void;\n}\n\n/** Adds, changes or removes the link of the selection. */\nexport function LinkDialog({ editor, newTab, isOpen, onOpenChange }: LinkDialogProps) {\n const intl = usePanneauIntl();\n const formId = useId();\n const current = editor.getAttributes('link');\n const currentHref = typeof current.href === 'string' ? current.href : '';\n const currentRel = typeof current.rel === 'string' ? current.rel : null;\n const [href, setHref] = useState(currentHref);\n const [openInNewTab, setOpenInNewTab] = useState(current.target === '_blank');\n const [invalid, setInvalid] = useState(false);\n const [opened, setOpened] = useState(isOpen);\n // Starts from the link of the selection each time it opens\n if (opened !== isOpen) {\n setOpened(isOpen);\n if (isOpen) {\n setHref(currentHref);\n setOpenInNewTab(current.target === '_blank');\n setInvalid(false);\n }\n }\n\n const apply = () => {\n const url = href.trim();\n if (url === '') {\n editor.chain().focus().extendMarkRange('link').unsetLink().run();\n onOpenChange(false);\n return;\n }\n // A `javascript:` URL, which the link would refuse\n if (!isAllowedUri(url)) {\n setInvalid(true);\n return;\n }\n const inNewTab =\n newTab === 'choose' ? openInNewTab : newTab === 'external' && isExternalUrl(url);\n const attributes = { href: url, ...getLinkTarget(inNewTab, currentRel) };\n if (editor.state.selection.empty && !editor.isActive('link')) {\n editor\n .chain()\n .focus()\n .insertContent({\n type: 'text',\n text: url,\n marks: [{ type: 'link', attrs: attributes }],\n })\n .run();\n } else {\n editor.chain().focus().extendMarkRange('link').setLink(attributes).run();\n }\n onOpenChange(false);\n };\n\n return (\n <Dialog\n title={intl.formatMessage(messages.linkTitle)}\n size=\"sm\"\n isOpen={isOpen}\n onOpenChange={onOpenChange}\n footer={(close) => (\n <>\n {currentHref !== '' ? (\n <Button\n appearance=\"ghost\"\n tone=\"danger\"\n className={styles.removeLink}\n onPress={() => {\n editor.chain().focus().extendMarkRange('link').unsetLink().run();\n close();\n }}\n >\n {intl.formatMessage(messages.linkRemove)}\n </Button>\n ) : null}\n <Button appearance=\"outline\" onPress={close}>\n {intl.formatMessage(messages.linkCancel)}\n </Button>\n <Button type=\"submit\" form={formId} appearance=\"solid\" tone=\"accent\">\n {intl.formatMessage(messages.linkApply)}\n </Button>\n </>\n )}\n >\n <form\n id={formId}\n className={styles.linkForm}\n onSubmit={(event) => {\n event.preventDefault();\n apply();\n }}\n >\n <TextInput\n label={intl.formatMessage(messages.linkUrl)}\n type=\"url\"\n inputMode=\"url\"\n autoComplete=\"off\"\n autoFocus\n value={href}\n onChange={(next) => {\n setHref(next);\n setInvalid(false);\n }}\n errors={invalid ? [intl.formatMessage(messages.linkInvalid)] : []}\n />\n {newTab === 'choose' ? (\n <Checkbox isSelected={openInNewTab} onChange={setOpenInNewTab}>\n {intl.formatMessage(messages.linkNewTab)}\n </Checkbox>\n ) : null}\n </form>\n </Dialog>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
.panneau-rich-text-field-field {
|
|
2
|
+
min-width: 0;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/* The box of the field, as the other controls draw it: the toolbar over the text. */
|
|
6
|
+
.panneau-rich-text-field-editor {
|
|
7
|
+
min-width: 0;
|
|
8
|
+
color: var(--panneau-color-text);
|
|
9
|
+
background-color: var(--panneau-input-bg, var(--panneau-color-surface));
|
|
10
|
+
border: var(--panneau-input-border-width, var(--panneau-border-width)) solid
|
|
11
|
+
var(--panneau-input-border-color, var(--panneau-color-border-control));
|
|
12
|
+
border-radius: var(--panneau-input-radius, var(--panneau-radius-md));
|
|
13
|
+
transition:
|
|
14
|
+
border-color var(--panneau-duration),
|
|
15
|
+
box-shadow var(--panneau-duration);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.panneau-rich-text-field-editor:hover {
|
|
19
|
+
border-color: var(--panneau-input-border-color-hover, var(--panneau-color-text-muted));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.panneau-rich-text-field-editor:has(.panneau-rich-text-field-content:focus) {
|
|
23
|
+
border-color: var(--panneau-color-focus);
|
|
24
|
+
box-shadow: 0 0 0 1px var(--panneau-color-focus);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.panneau-rich-text-field-editor[data-invalid] {
|
|
28
|
+
border-color: var(--panneau-color-danger);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.panneau-rich-text-field-editor[data-invalid]:has(.panneau-rich-text-field-content:focus) {
|
|
32
|
+
box-shadow: 0 0 0 1px var(--panneau-color-danger);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.panneau-rich-text-field-editor[data-disabled] {
|
|
36
|
+
cursor: not-allowed;
|
|
37
|
+
opacity: 0.6;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.panneau-rich-text-field-editor[data-disabled],
|
|
41
|
+
.panneau-rich-text-field-editor[data-readonly] {
|
|
42
|
+
background-color: var(--panneau-color-surface-sunken);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.panneau-rich-text-field-toolbar {
|
|
46
|
+
position: sticky;
|
|
47
|
+
top: 0;
|
|
48
|
+
z-index: 1;
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-wrap: wrap;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: var(--panneau-space-1);
|
|
53
|
+
padding: var(--panneau-space-1);
|
|
54
|
+
background-color: inherit;
|
|
55
|
+
border-bottom: var(--panneau-border-width) solid var(--panneau-color-border);
|
|
56
|
+
border-radius: inherit;
|
|
57
|
+
border-end-start-radius: 0;
|
|
58
|
+
border-end-end-radius: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.panneau-rich-text-field-separator {
|
|
62
|
+
align-self: stretch;
|
|
63
|
+
width: var(--panneau-border-width);
|
|
64
|
+
margin: var(--panneau-space-1) var(--panneau-space-1);
|
|
65
|
+
background-color: var(--panneau-color-border);
|
|
66
|
+
border: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.panneau-rich-text-field-button {
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
width: var(--panneau-control-height-sm);
|
|
74
|
+
height: var(--panneau-control-height-sm);
|
|
75
|
+
padding: 0;
|
|
76
|
+
color: var(--panneau-color-text);
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
background-color: transparent;
|
|
79
|
+
border: 0;
|
|
80
|
+
border-radius: var(--panneau-radius-sm);
|
|
81
|
+
transition: background-color var(--panneau-duration);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.panneau-rich-text-field-button[data-hovered] {
|
|
85
|
+
background-color: color-mix(in srgb, var(--panneau-color-text) 6%, transparent);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.panneau-rich-text-field-button[data-pressed],
|
|
89
|
+
.panneau-rich-text-field-button[data-selected] {
|
|
90
|
+
background-color: color-mix(in srgb, var(--panneau-color-text) 12%, transparent);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.panneau-rich-text-field-button[data-selected] {
|
|
94
|
+
color: var(--panneau-color-accent-text);
|
|
95
|
+
background-color: var(--panneau-color-accent-subtle);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.panneau-rich-text-field-button[data-focus-visible] {
|
|
99
|
+
outline: var(--panneau-focus-width) solid var(--panneau-color-focus);
|
|
100
|
+
outline-offset: 1px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.panneau-rich-text-field-button[data-disabled] {
|
|
104
|
+
cursor: not-allowed;
|
|
105
|
+
opacity: 0.45;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.panneau-rich-text-field-select {
|
|
109
|
+
min-width: 9rem;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* The text: the elements of rich content, as `Prose` styles them. */
|
|
113
|
+
.panneau-rich-text-field-content {
|
|
114
|
+
min-height: 8rem;
|
|
115
|
+
padding: var(--panneau-space-3);
|
|
116
|
+
font-size: var(--panneau-font-size-control);
|
|
117
|
+
line-height: var(--panneau-prose-line-height, 1.6);
|
|
118
|
+
overflow-wrap: break-word;
|
|
119
|
+
white-space: pre-wrap;
|
|
120
|
+
outline: none;
|
|
121
|
+
/* Nothing of the content paints outside of it: transforms, negative margins */
|
|
122
|
+
contain: paint;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* Styles of stored content show in the editor (a color, an alignment), but never take an element
|
|
127
|
+
* out of the text to cover the admin: they are kept as they are in the value.
|
|
128
|
+
*/
|
|
129
|
+
.panneau-rich-text-field-content :where([style]) {
|
|
130
|
+
position: static !important;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.panneau-rich-text-field-content > * + * {
|
|
134
|
+
margin-top: var(--panneau-prose-gap, var(--panneau-space-3));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.panneau-rich-text-field-content :where(h1, h2, h3, h4, h5, h6) {
|
|
138
|
+
font-family: var(--panneau-heading-font-family, var(--panneau-font-family-heading));
|
|
139
|
+
font-weight: var(--panneau-font-weight-bold);
|
|
140
|
+
line-height: var(--panneau-line-height-tight);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.panneau-rich-text-field-content :where(h1) {
|
|
144
|
+
font-size: var(--panneau-heading-1-font-size, 1.75rem);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.panneau-rich-text-field-content :where(h2) {
|
|
148
|
+
font-size: var(--panneau-heading-2-font-size, 1.375rem);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.panneau-rich-text-field-content :where(h3) {
|
|
152
|
+
font-size: var(--panneau-heading-3-font-size, 1.125rem);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.panneau-rich-text-field-content :where(h4, h5, h6) {
|
|
156
|
+
font-size: var(--panneau-heading-4-font-size, var(--panneau-font-size-lg));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.panneau-rich-text-field-content :where(ul, ol) {
|
|
160
|
+
padding-inline-start: var(--panneau-prose-list-indent, 1.5em);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.panneau-rich-text-field-content :where(ul) {
|
|
164
|
+
list-style: disc;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.panneau-rich-text-field-content :where(ol) {
|
|
168
|
+
list-style: decimal;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.panneau-rich-text-field-content :where(li + li, li > ul, li > ol) {
|
|
172
|
+
margin-top: var(--panneau-space-1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.panneau-rich-text-field-content :where(a) {
|
|
176
|
+
color: var(--panneau-prose-link-color, var(--panneau-color-link));
|
|
177
|
+
text-decoration: underline;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.panneau-rich-text-field-content :where(strong, b) {
|
|
181
|
+
font-weight: var(--panneau-font-weight-bold);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.panneau-rich-text-field-content :where(em, i) {
|
|
185
|
+
font-style: italic;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.panneau-rich-text-field-content :where(blockquote) {
|
|
189
|
+
padding-inline-start: var(--panneau-space-4);
|
|
190
|
+
color: var(--panneau-color-text-muted);
|
|
191
|
+
border-inline-start: 3px solid var(--panneau-color-border);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.panneau-rich-text-field-content :where(code) {
|
|
195
|
+
font-family: var(--panneau-font-family-mono);
|
|
196
|
+
font-size: 0.875em;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.panneau-rich-text-field-content :where(hr) {
|
|
200
|
+
border: 0;
|
|
201
|
+
border-top: var(--panneau-border-width) solid var(--panneau-color-border);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.panneau-rich-text-field-content .ProseMirror-selectednode {
|
|
205
|
+
outline: var(--panneau-focus-width) solid var(--panneau-color-focus);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.panneau-rich-text-field-content .is-editor-empty:first-child::before {
|
|
209
|
+
float: inline-start;
|
|
210
|
+
height: 0;
|
|
211
|
+
color: var(--panneau-color-text-muted);
|
|
212
|
+
pointer-events: none;
|
|
213
|
+
content: attr(data-placeholder);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* HTML kept as it was written: its element and what it points to, never its markup. */
|
|
217
|
+
.panneau-rich-text-field-html {
|
|
218
|
+
display: flex;
|
|
219
|
+
gap: var(--panneau-space-2);
|
|
220
|
+
align-items: baseline;
|
|
221
|
+
min-width: 0;
|
|
222
|
+
padding: var(--panneau-space-2) var(--panneau-space-3);
|
|
223
|
+
font-size: var(--panneau-font-size-sm);
|
|
224
|
+
color: var(--panneau-color-text-muted);
|
|
225
|
+
white-space: nowrap;
|
|
226
|
+
background-color: var(--panneau-color-surface-sunken);
|
|
227
|
+
border: var(--panneau-border-width) dashed var(--panneau-color-border);
|
|
228
|
+
border-radius: var(--panneau-radius-sm);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
span.panneau-rich-text-field-html {
|
|
232
|
+
display: inline-flex;
|
|
233
|
+
max-width: 100%;
|
|
234
|
+
padding: 0 var(--panneau-space-1);
|
|
235
|
+
vertical-align: baseline;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.panneau-rich-text-field-html > code {
|
|
239
|
+
flex: 0 0 auto;
|
|
240
|
+
font-family: var(--panneau-font-family-mono);
|
|
241
|
+
color: var(--panneau-color-text);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.panneau-rich-text-field-html > span {
|
|
245
|
+
min-width: 0;
|
|
246
|
+
overflow: hidden;
|
|
247
|
+
text-overflow: ellipsis;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.panneau-rich-text-field-link-form {
|
|
251
|
+
display: grid;
|
|
252
|
+
gap: var(--panneau-space-3);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.panneau-rich-text-field-remove-link {
|
|
256
|
+
margin-inline-end: auto;
|
|
257
|
+
}
|