diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-text-indent-extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-text-indent-extension.ts new file mode 100644 index 000000000000..7df5d4043e2b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-text-indent-extension.ts @@ -0,0 +1,118 @@ +/* This Source Code has been derived from Tiptiz. + * https://github.com/tiptiz/editor/blob/main/packages/tiptiz-extension-indent/src/indent.ts + * SPDX-License-Identifier: MIT + * Copyright © 2024 Owen Kriz. + * Modifications are licensed under the MIT License. + */ + +import type { Dispatch } from '@tiptap/core'; +import type { EditorState, Transaction } from '@tiptap/pm/state'; + +import { Extension } from '@tiptap/core'; +import { AllSelection, TextSelection } from '@tiptap/pm/state'; + +export interface TextIndentOptions { + minLevel: number; + maxLevel: number; + types: Array; +} + +export const TextIndent = Extension.create({ + name: 'textIndent', + + addOptions() { + return { + minLevel: 0, + maxLevel: 5, + types: ['heading', 'paragraph', 'listItem', 'taskItem'], + }; + }, + + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + indent: { + default: null, + parseHTML: (element) => { + const minLevel = this.options.minLevel; + const maxLevel = this.options.maxLevel; + const indent = element.style.textIndent; + return indent ? Math.max(minLevel, Math.min(maxLevel, parseInt(indent, 10))) : null; + }, + renderHTML: (attributes) => { + if (!attributes.indent) return {}; + return { + style: `text-indent: ${attributes.indent}rem;`, + }; + }, + }, + }, + }, + ]; + }, + + addCommands() { + const updateNodeIndentMarkup = (tr: Transaction, pos: number, delta: number) => { + const node = tr.doc.nodeAt(pos); + if (!node) return tr; + + const minLevel = this.options.minLevel; + const maxLevel = this.options.maxLevel; + + let level = (node.attrs.indent || 0) + delta; + level = Math.max(minLevel, Math.min(maxLevel, parseInt(level, 10))); + + if (level === node.attrs.indent) return tr; + + return tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent: level }, node.marks); + }; + + const updateIndentLevel = (tr: Transaction, delta: number) => { + if (tr.selection instanceof TextSelection || tr.selection instanceof AllSelection) { + const { from, to } = tr.selection; + tr.doc.nodesBetween(from, to, (node, pos) => { + if (this.options.types.includes(node.type.name)) { + tr = updateNodeIndentMarkup(tr, pos, delta); + return false; + } + return true; + }); + } + return tr; + }; + + type CommanderArgs = { + tr: Transaction; + state: EditorState; + dispatch: Dispatch; + }; + + const commanderFactory = (direction: number) => () => + function chainHandler({ tr, state, dispatch }: CommanderArgs) { + const { selection } = state; + tr.setSelection(selection); + tr = updateIndentLevel(tr, direction); + if (tr.docChanged) { + if (dispatch instanceof Function) dispatch(tr); + return true; + } + return false; + }; + + return { + textIndent: commanderFactory(1), + textOutdent: commanderFactory(-1), + }; + }, +}); + +declare module '@tiptap/core' { + interface Commands { + textIndent: { + textIndent: () => ReturnType; + textOutdent: () => ReturnType; + }; + } +} 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 af5bf8ba0908..16f06c87da1e 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -35,6 +35,7 @@ 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-text-direction-extension.js'; +export * from './extensions/tiptap-text-indent-extension.js'; export * from './extensions/tiptap-trailing-node.extension.js'; export * from './extensions/tiptap-umb-embedded-media.extension.js'; export * from './extensions/tiptap-umb-image.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 79ef5e95d987..32f692eaaf16 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 @@ -1037,6 +1037,7 @@ export const data: Array = [ 'Umb.Tiptap.Table', 'Umb.Tiptap.TextAlign', 'Umb.Tiptap.TextDirection', + 'Umb.Tiptap.TextIndent', 'Umb.Tiptap.Underline', ], }, @@ -1069,6 +1070,7 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.TextDirectionRtl', 'Umb.Tiptap.Toolbar.TextDirectionLtr', ], + ['Umb.Tiptap.Toolbar.TextIndent', 'Umb.Tiptap.Toolbar.TextOutdent'], [ 'Umb.Tiptap.Toolbar.BulletList', 'Umb.Tiptap.Toolbar.OrderedList', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/text-indent.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/text-indent.tiptap-api.ts new file mode 100644 index 000000000000..407729d6be0b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/text-indent.tiptap-api.ts @@ -0,0 +1,10 @@ +import { UmbTiptapExtensionApiBase } from '../base.js'; +import { TextIndent } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapTextIndentExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [ + TextIndent.configure({ + types: ['div', 'heading', 'paragraph', 'blockquote', 'listItem', 'orderedList', 'bulletList'], + }), + ]; +} 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 b7584dec6ae0..28c2baf3e070 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 @@ -161,6 +161,17 @@ const coreExtensions: Array = [ group: '#tiptap_extGroup_media', }, }, + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.TextIndent', + name: 'Text Indent Tiptap Extension', + api: () => import('./core/text-indent.tiptap-api.js'), + meta: { + icon: 'icon-science', + label: 'Text Indent', + group: '#tiptap_extGroup_formatting', + }, + }, ]; const toolbarExtensions: Array = [ @@ -606,6 +617,32 @@ const toolbarExtensions: Array = [ label: '#tiptap_charmap', }, }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextIndent', + name: 'Text Indent Tiptap Extension', + api: () => import('./toolbar/text-indent.tiptap-toolbar-api.js'), + forExtensions: ['Umb.Tiptap.TextIndent'], + meta: { + alias: 'indent', + icon: 'icon-indent', + label: 'Indent', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextOutdent', + name: 'Text Outdent Tiptap Extension', + api: () => import('./toolbar/text-outdent.tiptap-toolbar-api.js'), + forExtensions: ['Umb.Tiptap.TextIndent'], + meta: { + alias: 'outdent', + icon: 'icon-outdent', + label: 'Outdent', + }, + }, ]; const extensions = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-indent.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-indent.tiptap-toolbar-api.ts new file mode 100644 index 000000000000..31950ba778d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-indent.tiptap-toolbar-api.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTextIndentExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().textIndent().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-outdent.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-outdent.tiptap-toolbar-api.ts new file mode 100644 index 000000000000..2c1c0167bdde --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-outdent.tiptap-toolbar-api.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTextOutdentExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().textOutdent().run(); + } +}