diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 1428b961d7f1..def637a8b3d3 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2737,6 +2737,8 @@ export default { config_overlaySize_description: 'Select the width of the overlay (link picker).', }, tiptap: { + anchor: 'Anchor', + anchor_input: 'Enter an anchor ID', config_dimensions_description: 'Set the maximum width and height of the editor. This excludes the toolbar height.', config_extensions: 'Capabilities', config_toolbar: 'Toolbar', diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-anchor.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-anchor.extension.ts new file mode 100644 index 000000000000..0638768517f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-anchor.extension.ts @@ -0,0 +1,54 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +export const Anchor = Node.create({ + name: 'anchor', + + atom: true, + draggable: true, + inline: true, + group: 'inline', + marks: '', + selectable: true, + + addAttributes() { + return { + id: {}, + }; + }, + + addNodeView() { + return ({ HTMLAttributes }) => { + const dom = document.createElement('span'); + dom.setAttribute('data-umb-anchor', ''); + dom.setAttribute('title', HTMLAttributes.id); + + const icon = document.createElement('uui-icon'); + icon.setAttribute('name', 'icon-anchor'); + + dom.appendChild(icon); + + return { dom }; + }; + }, + + addOptions() { + return { + HTMLAttributes: { + id: 'id', + }, + }; + }, + + parseHTML() { + return [ + { + tag: 'a[id]', + getAttrs: (element) => (element.innerHTML === '' ? {} : false), + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)]; + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts index f85780ae9dc7..af5bf8ba0908 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -28,6 +28,7 @@ export { TextAlign } from '@tiptap/extension-text-align'; export { Underline } from '@tiptap/extension-underline'; // CUSTOM EXTENSIONS +export * from './extensions/tiptap-anchor.extension.js'; export * from './extensions/tiptap-div.extension.js'; export * from './extensions/tiptap-figcaption.extension.js'; export * from './extensions/tiptap-figure.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 3cf3fb770bd1..79ef5e95d987 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1075,7 +1075,7 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.Blockquote', 'Umb.Tiptap.Toolbar.HorizontalRule', ], - ['Umb.Tiptap.Toolbar.Link', 'Umb.Tiptap.Toolbar.Unlink'], + ['Umb.Tiptap.Toolbar.Anchor', 'Umb.Tiptap.Toolbar.Link', 'Umb.Tiptap.Toolbar.Unlink'], ['Umb.Tiptap.Toolbar.Table', 'Umb.Tiptap.Toolbar.MediaPicker', 'Umb.Tiptap.Toolbar.EmbeddedMedia'], ], ], diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index 98ea26928fed..45be5334cf62 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -911,10 +911,10 @@ export const data: Array = [ value: { blocks: undefined, markup: ` +

Here is a link for all HTML tags.

Some value for the RTE with an external link and an internal link.

-

All HTML tags

This is a plain old span tag. Hello world. diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.element.ts new file mode 100644 index 000000000000..e86a99a458ff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.element.ts @@ -0,0 +1,84 @@ +import type { UmbTiptapAnchorModalData, UmbTiptapAnchorModalValue } from './anchor-modal.token.js'; +import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-tiptap-anchor-modal') +export class UmbTiptapAnchorModalElement extends UmbModalBaseElement< + UmbTiptapAnchorModalData, + UmbTiptapAnchorModalValue +> { + async #onSubmit(event: SubmitEvent) { + event.preventDefault(); + + const form = event.target as HTMLFormElement; + if (!form) return; + + const isValid = form.checkValidity(); + if (!isValid) return; + + const formData = new FormData(form); + const name = formData.get('name') as string; + + this.value = name; + this._submitModal(); + } + + override render() { + const label = this.localize.term('tiptap_anchor_input'); + return html` + + +
+ + ${label} + + +
+
+ + +
+ `; + } + + static override styles = [ + css` + :host { + --umb-body-layout-color-background: var(--uui-color-surface); + } + + uui-dialog-layout { + width: var(--uui-size-100); + } + + uui-input { + width: 100%; + } + `, + ]; +} + +export { UmbTiptapAnchorModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-anchor-modal': UmbTiptapAnchorModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.token.ts new file mode 100644 index 000000000000..68e763d3767a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/anchor-modal.token.ts @@ -0,0 +1,15 @@ +import { UMB_TIPTAP_ANCHOR_MODAL_ALIAS } from './constants.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export type UmbTiptapAnchorModalData = { + id?: string; +}; + +export type UmbTiptapAnchorModalValue = string; + +export const UMB_TIPTAP_ANCHOR_MODAL = new UmbModalToken( + UMB_TIPTAP_ANCHOR_MODAL_ALIAS, + { + modal: { type: 'dialog' }, + }, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/constants.ts new file mode 100644 index 000000000000..16efc1e72606 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/constants.ts @@ -0,0 +1 @@ +export const UMB_TIPTAP_ANCHOR_MODAL_ALIAS = 'Umb.Modal.Tiptap.Anchor'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/index.ts new file mode 100644 index 000000000000..2bdc2fe35edd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/index.ts @@ -0,0 +1 @@ +export * from './anchor-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/manifests.ts new file mode 100644 index 000000000000..99bbb4a2b52d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/anchor-modal/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_TIPTAP_ANCHOR_MODAL_ALIAS } from './constants.js'; +import type { ManifestModal } from '@umbraco-cms/backoffice/modal'; + +export const manifests: Array = [ + { + type: 'modal', + alias: UMB_TIPTAP_ANCHOR_MODAL_ALIAS, + name: 'Tiptap Anchor Modal', + element: () => import('./anchor-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts index 8f4aa2d10deb..3f50b33fda00 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/index.ts @@ -1,3 +1,4 @@ export * from './input-tiptap/index.js'; +export * from './anchor-modal/index.js'; export * from './cascading-menu-popover/cascading-menu-popover.element.js'; export * from './character-map/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts index 99bde2c0551b..a81acb72c3c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/manifests.ts @@ -1,5 +1,7 @@ +import { manifests as anchorModal } from './anchor-modal/manifests.js'; import { manifests as characterMap } from './character-map/manifests.js'; export const manifests: Array = [ + ...anchorModal, ...characterMap, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts index 12354824c404..8613d3f9b4f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/constants.ts @@ -1,2 +1,3 @@ +export * from './components/anchor-modal/constants.js'; export * from './components/character-map/constants.js'; export * from './property-editors/tiptap/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts index e51609936dbd..9a3e2fb22baa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts @@ -1,10 +1,18 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; import { css } from '@umbraco-cms/backoffice/external/lit'; -import { Div, HtmlGlobalAttributes, Span, StarterKit, TrailingNode } from '@umbraco-cms/backoffice/external/tiptap'; +import { + Anchor, + Div, + HtmlGlobalAttributes, + Span, + StarterKit, + TrailingNode, +} from '@umbraco-cms/backoffice/external/tiptap'; export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase { getTiptapExtensions = () => [ StarterKit, + Anchor, Div, Span, HtmlGlobalAttributes.configure({ @@ -76,6 +84,19 @@ export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionA padding: 0; } } + + span[data-umb-anchor] { + &.ProseMirror-selectednode { + border-radius: var(--uui-border-radius); + outline: 2px solid var(--uui-color-selected); + } + + uui-icon { + height: 1rem; + width: 1rem; + vertical-align: text-bottom; + } + } `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 28fadd642c98..b7584dec6ae0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -399,6 +399,18 @@ const toolbarExtensions: Array = [ label: 'Ordered List', }, }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Anchor', + name: 'Anchor Tiptap Extension', + api: () => import('./toolbar/anchor.tiptap-toolbar-api.js'), + meta: { + alias: 'anchor', + icon: 'icon-anchor', + label: '#tiptap_anchor', + }, + }, { type: 'tiptapToolbarExtension', kind: 'button', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/anchor.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/anchor.tiptap-toolbar-api.ts new file mode 100644 index 000000000000..42a8d5f96a31 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/anchor.tiptap-toolbar-api.ts @@ -0,0 +1,24 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import { UMB_TIPTAP_ANCHOR_MODAL } from '../../components/anchor-modal/index.js'; +import { Anchor } from '@umbraco-cms/backoffice/external/tiptap'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarAnchorExtensionApi extends UmbTiptapToolbarElementApiBase { + override async execute(editor?: Editor) { + const attrs = editor?.getAttributes(Anchor.name); + if (!attrs) return; + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modal = modalManager.open(this, UMB_TIPTAP_ANCHOR_MODAL, { data: { id: attrs?.id } }); + if (!modal) return; + + const result = await modal.onSubmit().catch(() => undefined); + if (!result) return; + + editor + ?.chain() + .insertContent({ type: Anchor.name, attrs: { id: result } }) + .run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts index 7c20d0f49bf8..2b8b19e0438e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts @@ -56,7 +56,7 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase