Skip to content

Commit d900022

Browse files
authored
Feature: Tiptap: Generic markup support (#18124)
* Adds "HTML Global Attributes" Tiptap extension This is to add `class`, `id` and `data-*` attributes to any markup within Tiptap contents. * Adds "Span" element Tiptap extension to support generic markup modifications. * Adds "Div" element Tiptap extension to support generic markup modifications. Also modifies "umbEmbeddedMedia" to check explicitly for the `umb-embed-holder` class name. This is to differentiate from the generic `div` tag. * Adds "Rich Text Essentials" Tiptap extension Previously this was a faux extension, but it is now real. This extension adds the core extensions for Umbraco RTE support. e.g. StarterKit, et al, and the new global attributes and generic elements. * Reverts `elementName` constant
1 parent 1e89b5a commit d900022

File tree

11 files changed

+230
-47
lines changed

11 files changed

+230
-47
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Node, mergeAttributes } from '@tiptap/core';
2+
3+
export interface DivOptions {
4+
/**
5+
* HTML attributes to add to the element.
6+
* @default {}
7+
* @example { class: 'foo' }
8+
*/
9+
HTMLAttributes: Record<string, any>;
10+
}
11+
12+
export const Div = Node.create<DivOptions>({
13+
name: 'div',
14+
15+
priority: 50,
16+
17+
group: 'block',
18+
19+
content: 'inline*',
20+
21+
addOptions() {
22+
return { HTMLAttributes: {} };
23+
},
24+
25+
parseHTML() {
26+
return [{ tag: 'div' }];
27+
},
28+
29+
renderHTML({ HTMLAttributes }) {
30+
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
31+
},
32+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Extension } from '@tiptap/core';
2+
3+
/**
4+
* Converts camelCase to kebab-case.
5+
* @param {string} str - The string to convert.
6+
* @returns {string} The converted string.
7+
*/
8+
function camelCaseToKebabCase(str: string): string {
9+
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase());
10+
}
11+
12+
export interface HtmlGlobalAttributesOptions {
13+
/**
14+
* The types where the text align attribute can be applied.
15+
* @default []
16+
* @example ['heading', 'paragraph']
17+
*/
18+
types: Array<string>;
19+
}
20+
21+
export const HtmlGlobalAttributes = Extension.create<HtmlGlobalAttributesOptions>({
22+
name: 'htmlGlobalAttributes',
23+
24+
addOptions() {
25+
return { types: [] };
26+
},
27+
28+
addGlobalAttributes() {
29+
return [
30+
{
31+
types: this.options.types,
32+
attributes: {
33+
class: {},
34+
dataset: {
35+
parseHTML: (element) => element.dataset,
36+
renderHTML: (attributes) => {
37+
const keys = attributes.dataset ? Object.keys(attributes.dataset) : [];
38+
if (!keys.length) return {};
39+
const dataAtrrs: Record<string, string> = {};
40+
keys.forEach((key) => {
41+
dataAtrrs['data-' + camelCaseToKebabCase(key)] = attributes.dataset[key];
42+
});
43+
return dataAtrrs;
44+
},
45+
},
46+
id: {},
47+
style: {},
48+
},
49+
},
50+
];
51+
},
52+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Node, mergeAttributes } from '@tiptap/core';
2+
3+
export interface SpanOptions {
4+
/**
5+
* HTML attributes to add to the element.
6+
* @default {}
7+
* @example { class: 'foo' }
8+
*/
9+
HTMLAttributes: Record<string, any>;
10+
}
11+
12+
export const Span = Node.create<SpanOptions>({
13+
name: 'span',
14+
15+
group: 'inline',
16+
17+
inline: true,
18+
19+
content: 'inline*',
20+
21+
addOptions() {
22+
return { HTMLAttributes: {} };
23+
},
24+
25+
parseHTML() {
26+
return [{ tag: 'span' }];
27+
},
28+
29+
renderHTML({ HTMLAttributes }) {
30+
return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
31+
},
32+
});

src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const umbEmbeddedMedia = Node.create({
88
inline() {
99
return this.options.inline;
1010
},
11+
1112
atom: true,
1213
marks: '',
1314
draggable: true,
@@ -19,12 +20,18 @@ export const umbEmbeddedMedia = Node.create({
1920
'data-embed-height': { default: 240 },
2021
'data-embed-url': { default: null },
2122
'data-embed-width': { default: 360 },
22-
markup: { default: null },
23+
markup: { default: null, parseHTML: (element) => element.innerHTML },
2324
};
2425
},
2526

2627
parseHTML() {
27-
return [{ tag: 'div', class: 'umb-embed-holder', getAttrs: (node) => ({ markup: node.innerHTML }) }];
28+
return [
29+
{
30+
tag: 'div',
31+
priority: 100,
32+
getAttrs: (dom) => dom.classList.contains('umb-embed-holder') && null,
33+
},
34+
];
2835
},
2936

3037
renderHTML({ HTMLAttributes }) {

src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ export { TextAlign } from '@tiptap/extension-text-align';
2828
export { Underline } from '@tiptap/extension-underline';
2929

3030
// CUSTOM EXTENSIONS
31-
export * from './extensions/tiptap-umb-embedded-media.extension.js';
31+
export * from './extensions/tiptap-div.extension.js';
3232
export * from './extensions/tiptap-figcaption.extension.js';
3333
export * from './extensions/tiptap-figure.extension.js';
34+
export * from './extensions/tiptap-span.extension.js';
35+
export * from './extensions/tiptap-html-global-attributes.extension.js';
36+
export * from './extensions/tiptap-umb-embedded-media.extension.js';
3437
export * from './extensions/tiptap-umb-image.extension.js';
3538
export * from './extensions/tiptap-umb-link.extension.js';

src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { UmbTiptapToolbarValue } from '../types.js';
33
import { css, customElement, html, property, state, when } from '@umbraco-cms/backoffice/external/lit';
44
import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
55
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
6-
import { Editor, Placeholder, StarterKit, TextStyle } from '@umbraco-cms/backoffice/external/tiptap';
6+
import { Editor } from '@umbraco-cms/backoffice/external/tiptap';
77
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
88
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
99
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
@@ -12,39 +12,23 @@ import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/
1212
import './tiptap-hover-menu.element.js';
1313
import './tiptap-toolbar.element.js';
1414

15+
const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials';
16+
1517
@customElement('umb-input-tiptap')
1618
export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof UmbLitElement, string>(UmbLitElement) {
17-
readonly #requiredExtensions = [
18-
StarterKit,
19-
Placeholder.configure({
20-
placeholder: ({ node }) => {
21-
if (node.type.name === 'heading') {
22-
return this.localize.term('placeholders_rteHeading');
23-
}
24-
25-
return this.localize.term('placeholders_rteParagraph');
26-
},
27-
}),
28-
TextStyle,
29-
];
30-
31-
@state()
32-
private readonly _extensions: Array<UmbTiptapExtensionApi> = [];
33-
3419
@property({ type: String })
3520
override set value(value: string) {
36-
this.#markup = value;
21+
this.#value = value;
3722

3823
// Try to set the value to the editor if it is ready.
3924
if (this._editor) {
4025
this._editor.commands.setContent(value);
4126
}
4227
}
4328
override get value() {
44-
return this.#markup;
29+
return this.#value;
4530
}
46-
47-
#markup = '';
31+
#value = '';
4832

4933
@property({ attribute: false })
5034
configuration?: UmbPropertyEditorConfigCollection;
@@ -58,6 +42,9 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
5842
@state()
5943
private _editor?: Editor;
6044

45+
@state()
46+
private readonly _extensions: Array<UmbTiptapExtensionApi> = [];
47+
6148
@state()
6249
_toolbar: UmbTiptapToolbarValue = [[[]]];
6350

@@ -76,7 +63,13 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
7663
async #loadExtensions() {
7764
await new Promise<void>((resolve) => {
7865
this.observe(umbExtensionsRegistry.byType('tiptapExtension'), async (manifests) => {
79-
const enabledExtensions = this.configuration?.getValueByAlias<string[]>('extensions') ?? [];
66+
let enabledExtensions = this.configuration?.getValueByAlias<string[]>('extensions') ?? [];
67+
68+
// Ensures that the "Rich Text Essentials" extension is always enabled. [LK]
69+
if (!enabledExtensions.includes(TIPTAP_CORE_EXTENSION_ALIAS)) {
70+
enabledExtensions = [TIPTAP_CORE_EXTENSION_ALIAS, ...enabledExtensions];
71+
}
72+
8073
for (const manifest of manifests) {
8174
if (manifest.api) {
8275
const extension = await loadManifestApi(manifest.api);
@@ -114,13 +107,13 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
114107
this._editor = new Editor({
115108
element: element,
116109
editable: !this.readonly,
117-
extensions: [...this.#requiredExtensions, ...extensions],
118-
content: this.#markup,
110+
extensions: extensions,
111+
content: this.#value,
119112
onBeforeCreate: ({ editor }) => {
120113
this._extensions.forEach((ext) => ext.setEditor(editor));
121114
},
122115
onUpdate: ({ editor }) => {
123-
this.#markup = editor.getHTML();
116+
this.#value = editor.getHTML();
124117
this.dispatchEvent(new UmbChangeEvent());
125118
},
126119
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { UmbTiptapExtensionApiBase } from '../base.js';
2+
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
3+
import {
4+
Div,
5+
HtmlGlobalAttributes,
6+
Placeholder,
7+
Span,
8+
StarterKit,
9+
TextStyle,
10+
} from '@umbraco-cms/backoffice/external/tiptap';
11+
12+
export default class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase {
13+
#localize = new UmbLocalizationController(this);
14+
15+
getTiptapExtensions = () => [
16+
StarterKit,
17+
Placeholder.configure({
18+
placeholder: ({ node }) => {
19+
return this.#localize.term(
20+
node.type.name === 'heading' ? 'placeholders_rteHeading' : 'placeholders_rteParagraph',
21+
);
22+
},
23+
}),
24+
TextStyle,
25+
HtmlGlobalAttributes.configure({
26+
types: [
27+
'bold',
28+
'blockquote',
29+
'bulletList',
30+
'codeBlock',
31+
'div',
32+
'figcaption',
33+
'figure',
34+
'heading',
35+
'horizontalRule',
36+
'italic',
37+
'image',
38+
'link',
39+
'orderedList',
40+
'paragraph',
41+
'span',
42+
'strike',
43+
'subscript',
44+
'superscript',
45+
'table',
46+
'tableHeader',
47+
'tableRow',
48+
'tableCell',
49+
'textStyle',
50+
'underline',
51+
'umbLink',
52+
],
53+
}),
54+
Div,
55+
Span,
56+
];
57+
}

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ const kinds: Array<UmbExtensionManifestKind> = [
1515
];
1616

1717
const coreExtensions: Array<ManifestTiptapExtension> = [
18+
{
19+
type: 'tiptapExtension',
20+
alias: 'Umb.Tiptap.RichTextEssentials',
21+
name: 'Rich Text Essentials Tiptap Extension',
22+
api: () => import('./core/rich-text-essentials.tiptap-api.js'),
23+
weight: 1000,
24+
meta: {
25+
icon: 'icon-browser-window',
26+
label: 'Rich Text Essentials',
27+
group: '#tiptap_extGroup_formatting',
28+
description: 'This is a core extension, it is always enabled by default.',
29+
},
30+
},
1831
{
1932
type: 'tiptapExtension',
2033
alias: 'Umb.Tiptap.Embed',

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap.extension.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface MetaTiptapExtension {
1111
icon: string;
1212
label: string;
1313
group: string;
14+
description?: string;
1415
}
1516

1617
declare global {

src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ type UmbTiptapExtensionGroup = {
3939
const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials';
4040
const TIPTAP_BLOCK_EXTENSION_ALIAS = 'Umb.Tiptap.Block';
4141

42-
const elementName = 'umb-property-editor-ui-tiptap-extensions-configuration';
43-
44-
@customElement(elementName)
42+
@customElement('umb-property-editor-ui-tiptap-extensions-configuration')
4543
export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement
4644
extends UmbLitElement
4745
implements UmbPropertyEditorUiElement
@@ -101,16 +99,13 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement
10199
this.observe(umbExtensionsRegistry.byType('tiptapExtension'), (extensions) => {
102100
this._extensions = extensions
103101
.sort((a, b) => a.alias.localeCompare(b.alias))
104-
.map((ext) => ({ alias: ext.alias, label: ext.meta.label, icon: ext.meta.icon, group: ext.meta.group }));
105-
106-
// Hardcoded core extension
107-
this._extensions.unshift({
108-
alias: TIPTAP_CORE_EXTENSION_ALIAS,
109-
label: 'Rich Text Essentials',
110-
icon: 'icon-browser-window',
111-
group: '#tiptap_extGroup_formatting',
112-
description: 'This is a core extension, it is always enabled by default.',
113-
});
102+
.map((ext) => ({
103+
alias: ext.alias,
104+
label: ext.meta.label,
105+
icon: ext.meta.icon,
106+
group: ext.meta.group,
107+
description: ext.meta.description,
108+
}));
114109

115110
if (!this.value) {
116111
// The default value is all extensions enabled
@@ -226,6 +221,6 @@ export { UmbPropertyEditorUiTiptapExtensionsConfigurationElement as element };
226221

227222
declare global {
228223
interface HTMLElementTagNameMap {
229-
[elementName]: UmbPropertyEditorUiTiptapExtensionsConfigurationElement;
224+
'umb-property-editor-ui-tiptap-extensions-configuration': UmbPropertyEditorUiTiptapExtensionsConfigurationElement;
230225
}
231226
}

0 commit comments

Comments
 (0)