Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Node, mergeAttributes } from '@tiptap/core';

export interface DivOptions {
/**
* HTML attributes to add to the element.
* @default {}
* @example { class: 'foo' }
*/
HTMLAttributes: Record<string, any>;
}

export const Div = Node.create<DivOptions>({
name: 'div',

priority: 50,

group: 'block',

content: 'inline*',

addOptions() {
return { HTMLAttributes: {} };
},

parseHTML() {
return [{ tag: 'div' }];
},

renderHTML({ HTMLAttributes }) {
return ['div', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Extension } from '@tiptap/core';

/**
* Converts camelCase to kebab-case.
* @param {string} str - The string to convert.
* @returns {string} The converted string.
*/
function camelCaseToKebabCase(str: string): string {
return str.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase());
}

export interface HtmlGlobalAttributesOptions {
/**
* The types where the text align attribute can be applied.
* @default []
* @example ['heading', 'paragraph']
*/
types: Array<string>;
}

export const HtmlGlobalAttributes = Extension.create<HtmlGlobalAttributesOptions>({
name: 'htmlGlobalAttributes',

addOptions() {
return { types: [] };
},

addGlobalAttributes() {
return [
{
types: this.options.types,
attributes: {
class: {},
dataset: {
parseHTML: (element) => element.dataset,
renderHTML: (attributes) => {
const keys = attributes.dataset ? Object.keys(attributes.dataset) : [];
if (!keys.length) return {};
const dataAtrrs: Record<string, string> = {};
keys.forEach((key) => {
dataAtrrs['data-' + camelCaseToKebabCase(key)] = attributes.dataset[key];
});
return dataAtrrs;
},
},
id: {},
style: {},
},
},
];
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Node, mergeAttributes } from '@tiptap/core';

export interface SpanOptions {
/**
* HTML attributes to add to the element.
* @default {}
* @example { class: 'foo' }
*/
HTMLAttributes: Record<string, any>;
}

export const Span = Node.create<SpanOptions>({
name: 'span',

group: 'inline',

inline: true,

content: 'inline*',

addOptions() {
return { HTMLAttributes: {} };
},

parseHTML() {
return [{ tag: 'span' }];
},

renderHTML({ HTMLAttributes }) {
return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const umbEmbeddedMedia = Node.create({
inline() {
return this.options.inline;
},

atom: true,
marks: '',
draggable: true,
Expand All @@ -19,12 +20,18 @@ export const umbEmbeddedMedia = Node.create({
'data-embed-height': { default: 240 },
'data-embed-url': { default: null },
'data-embed-width': { default: 360 },
markup: { default: null },
markup: { default: null, parseHTML: (element) => element.innerHTML },
};
},

parseHTML() {
return [{ tag: 'div', class: 'umb-embed-holder', getAttrs: (node) => ({ markup: node.innerHTML }) }];
return [
{
tag: 'div',
priority: 100,
getAttrs: (dom) => dom.classList.contains('umb-embed-holder') && null,
},
];
},

renderHTML({ HTMLAttributes }) {
Expand Down
5 changes: 4 additions & 1 deletion src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ export { TextAlign } from '@tiptap/extension-text-align';
export { Underline } from '@tiptap/extension-underline';

// CUSTOM EXTENSIONS
export * from './extensions/tiptap-umb-embedded-media.extension.js';
export * from './extensions/tiptap-div.extension.js';
export * from './extensions/tiptap-figcaption.extension.js';
export * from './extensions/tiptap-figure.extension.js';
export * from './extensions/tiptap-span.extension.js';
export * from './extensions/tiptap-html-global-attributes.extension.js';
export * from './extensions/tiptap-umb-embedded-media.extension.js';
export * from './extensions/tiptap-umb-image.extension.js';
export * from './extensions/tiptap-umb-link.extension.js';
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { UmbTiptapToolbarValue } from '../types.js';
import { css, customElement, html, property, state, when } from '@umbraco-cms/backoffice/external/lit';
import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { Editor, Placeholder, StarterKit, TextStyle } from '@umbraco-cms/backoffice/external/tiptap';
import { Editor } from '@umbraco-cms/backoffice/external/tiptap';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation';
Expand All @@ -12,39 +12,23 @@ import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/
import './tiptap-hover-menu.element.js';
import './tiptap-toolbar.element.js';

const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials';

@customElement('umb-input-tiptap')
export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof UmbLitElement, string>(UmbLitElement) {
readonly #requiredExtensions = [
StarterKit,
Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === 'heading') {
return this.localize.term('placeholders_rteHeading');
}

return this.localize.term('placeholders_rteParagraph');
},
}),
TextStyle,
];

@state()
private readonly _extensions: Array<UmbTiptapExtensionApi> = [];

@property({ type: String })
override set value(value: string) {
this.#markup = value;
this.#value = value;

// Try to set the value to the editor if it is ready.
if (this._editor) {
this._editor.commands.setContent(value);
}
}
override get value() {
return this.#markup;
return this.#value;
}

#markup = '';
#value = '';

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

@state()
private readonly _extensions: Array<UmbTiptapExtensionApi> = [];

@state()
_toolbar: UmbTiptapToolbarValue = [[[]]];

Expand All @@ -76,7 +63,13 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
async #loadExtensions() {
await new Promise<void>((resolve) => {
this.observe(umbExtensionsRegistry.byType('tiptapExtension'), async (manifests) => {
const enabledExtensions = this.configuration?.getValueByAlias<string[]>('extensions') ?? [];
let enabledExtensions = this.configuration?.getValueByAlias<string[]>('extensions') ?? [];

// Ensures that the "Rich Text Essentials" extension is always enabled. [LK]
if (!enabledExtensions.includes(TIPTAP_CORE_EXTENSION_ALIAS)) {
enabledExtensions = [TIPTAP_CORE_EXTENSION_ALIAS, ...enabledExtensions];
}

for (const manifest of manifests) {
if (manifest.api) {
const extension = await loadManifestApi(manifest.api);
Expand Down Expand Up @@ -114,13 +107,13 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
this._editor = new Editor({
element: element,
editable: !this.readonly,
extensions: [...this.#requiredExtensions, ...extensions],
content: this.#markup,
extensions: extensions,
content: this.#value,
onBeforeCreate: ({ editor }) => {
this._extensions.forEach((ext) => ext.setEditor(editor));
},
onUpdate: ({ editor }) => {
this.#markup = editor.getHTML();
this.#value = editor.getHTML();
this.dispatchEvent(new UmbChangeEvent());
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { UmbTiptapExtensionApiBase } from '../base.js';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import {
Div,
HtmlGlobalAttributes,
Placeholder,
Span,
StarterKit,
TextStyle,
} from '@umbraco-cms/backoffice/external/tiptap';

export default class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase {
#localize = new UmbLocalizationController(this);

getTiptapExtensions = () => [
StarterKit,
Placeholder.configure({
placeholder: ({ node }) => {
return this.#localize.term(
node.type.name === 'heading' ? 'placeholders_rteHeading' : 'placeholders_rteParagraph',
);
},
}),
TextStyle,
HtmlGlobalAttributes.configure({
types: [
'bold',
'blockquote',
'bulletList',
'codeBlock',
'div',
'figcaption',
'figure',
'heading',
'horizontalRule',
'italic',
'image',
'link',
'orderedList',
'paragraph',
'span',
'strike',
'subscript',
'superscript',
'table',
'tableHeader',
'tableRow',
'tableCell',
'textStyle',
'underline',
'umbLink',
],
}),
Div,
Span,
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ const kinds: Array<UmbExtensionManifestKind> = [
];

const coreExtensions: Array<ManifestTiptapExtension> = [
{
type: 'tiptapExtension',
alias: 'Umb.Tiptap.RichTextEssentials',
name: 'Rich Text Essentials Tiptap Extension',
api: () => import('./core/rich-text-essentials.tiptap-api.js'),
weight: 1000,
meta: {
icon: 'icon-browser-window',
label: 'Rich Text Essentials',
group: '#tiptap_extGroup_formatting',
description: 'This is a core extension, it is always enabled by default.',
},
},
{
type: 'tiptapExtension',
alias: 'Umb.Tiptap.Embed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface MetaTiptapExtension {
icon: string;
label: string;
group: string;
description?: string;
}

declare global {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ type UmbTiptapExtensionGroup = {
const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials';
const TIPTAP_BLOCK_EXTENSION_ALIAS = 'Umb.Tiptap.Block';

const elementName = 'umb-property-editor-ui-tiptap-extensions-configuration';

@customElement(elementName)
@customElement('umb-property-editor-ui-tiptap-extensions-configuration')
export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement
extends UmbLitElement
implements UmbPropertyEditorUiElement
Expand Down Expand Up @@ -101,16 +99,13 @@ export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement
this.observe(umbExtensionsRegistry.byType('tiptapExtension'), (extensions) => {
this._extensions = extensions
.sort((a, b) => a.alias.localeCompare(b.alias))
.map((ext) => ({ alias: ext.alias, label: ext.meta.label, icon: ext.meta.icon, group: ext.meta.group }));

// Hardcoded core extension
this._extensions.unshift({
alias: TIPTAP_CORE_EXTENSION_ALIAS,
label: 'Rich Text Essentials',
icon: 'icon-browser-window',
group: '#tiptap_extGroup_formatting',
description: 'This is a core extension, it is always enabled by default.',
});
.map((ext) => ({
alias: ext.alias,
label: ext.meta.label,
icon: ext.meta.icon,
group: ext.meta.group,
description: ext.meta.description,
}));

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

declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbPropertyEditorUiTiptapExtensionsConfigurationElement;
'umb-property-editor-ui-tiptap-extensions-configuration': UmbPropertyEditorUiTiptapExtensionsConfigurationElement;
}
}
Loading
Loading