Skip to content

Commit a02db28

Browse files
authored
Tiptap RTE: Text Indent extension + toolbar items (#18672)
* Tiptap: Text Indent extension * Updates indent manifest icons
1 parent e9c97f8 commit a02db28

File tree

7 files changed

+184
-0
lines changed

7 files changed

+184
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/* This Source Code has been derived from Tiptiz.
2+
* https://github.com/tiptiz/editor/blob/main/packages/tiptiz-extension-indent/src/indent.ts
3+
* SPDX-License-Identifier: MIT
4+
* Copyright © 2024 Owen Kriz.
5+
* Modifications are licensed under the MIT License.
6+
*/
7+
8+
import type { Dispatch } from '@tiptap/core';
9+
import type { EditorState, Transaction } from '@tiptap/pm/state';
10+
11+
import { Extension } from '@tiptap/core';
12+
import { AllSelection, TextSelection } from '@tiptap/pm/state';
13+
14+
export interface TextIndentOptions {
15+
minLevel: number;
16+
maxLevel: number;
17+
types: Array<string>;
18+
}
19+
20+
export const TextIndent = Extension.create<TextIndentOptions>({
21+
name: 'textIndent',
22+
23+
addOptions() {
24+
return {
25+
minLevel: 0,
26+
maxLevel: 5,
27+
types: ['heading', 'paragraph', 'listItem', 'taskItem'],
28+
};
29+
},
30+
31+
addGlobalAttributes() {
32+
return [
33+
{
34+
types: this.options.types,
35+
attributes: {
36+
indent: {
37+
default: null,
38+
parseHTML: (element) => {
39+
const minLevel = this.options.minLevel;
40+
const maxLevel = this.options.maxLevel;
41+
const indent = element.style.textIndent;
42+
return indent ? Math.max(minLevel, Math.min(maxLevel, parseInt(indent, 10))) : null;
43+
},
44+
renderHTML: (attributes) => {
45+
if (!attributes.indent) return {};
46+
return {
47+
style: `text-indent: ${attributes.indent}rem;`,
48+
};
49+
},
50+
},
51+
},
52+
},
53+
];
54+
},
55+
56+
addCommands() {
57+
const updateNodeIndentMarkup = (tr: Transaction, pos: number, delta: number) => {
58+
const node = tr.doc.nodeAt(pos);
59+
if (!node) return tr;
60+
61+
const minLevel = this.options.minLevel;
62+
const maxLevel = this.options.maxLevel;
63+
64+
let level = (node.attrs.indent || 0) + delta;
65+
level = Math.max(minLevel, Math.min(maxLevel, parseInt(level, 10)));
66+
67+
if (level === node.attrs.indent) return tr;
68+
69+
return tr.setNodeMarkup(pos, node.type, { ...node.attrs, indent: level }, node.marks);
70+
};
71+
72+
const updateIndentLevel = (tr: Transaction, delta: number) => {
73+
if (tr.selection instanceof TextSelection || tr.selection instanceof AllSelection) {
74+
const { from, to } = tr.selection;
75+
tr.doc.nodesBetween(from, to, (node, pos) => {
76+
if (this.options.types.includes(node.type.name)) {
77+
tr = updateNodeIndentMarkup(tr, pos, delta);
78+
return false;
79+
}
80+
return true;
81+
});
82+
}
83+
return tr;
84+
};
85+
86+
type CommanderArgs = {
87+
tr: Transaction;
88+
state: EditorState;
89+
dispatch: Dispatch;
90+
};
91+
92+
const commanderFactory = (direction: number) => () =>
93+
function chainHandler({ tr, state, dispatch }: CommanderArgs) {
94+
const { selection } = state;
95+
tr.setSelection(selection);
96+
tr = updateIndentLevel(tr, direction);
97+
if (tr.docChanged) {
98+
if (dispatch instanceof Function) dispatch(tr);
99+
return true;
100+
}
101+
return false;
102+
};
103+
104+
return {
105+
textIndent: commanderFactory(1),
106+
textOutdent: commanderFactory(-1),
107+
};
108+
},
109+
});
110+
111+
declare module '@tiptap/core' {
112+
interface Commands<ReturnType> {
113+
textIndent: {
114+
textIndent: () => ReturnType;
115+
textOutdent: () => ReturnType;
116+
};
117+
}
118+
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export * from './extensions/tiptap-figure.extension.js';
3535
export * from './extensions/tiptap-span.extension.js';
3636
export * from './extensions/tiptap-html-global-attributes.extension.js';
3737
export * from './extensions/tiptap-text-direction-extension.js';
38+
export * from './extensions/tiptap-text-indent-extension.js';
3839
export * from './extensions/tiptap-trailing-node.extension.js';
3940
export * from './extensions/tiptap-umb-embedded-media.extension.js';
4041
export * from './extensions/tiptap-umb-image.extension.js';

src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,7 @@ export const data: Array<UmbMockDataTypeModel> = [
10371037
'Umb.Tiptap.Table',
10381038
'Umb.Tiptap.TextAlign',
10391039
'Umb.Tiptap.TextDirection',
1040+
'Umb.Tiptap.TextIndent',
10401041
'Umb.Tiptap.Underline',
10411042
],
10421043
},
@@ -1069,6 +1070,7 @@ export const data: Array<UmbMockDataTypeModel> = [
10691070
'Umb.Tiptap.Toolbar.TextDirectionRtl',
10701071
'Umb.Tiptap.Toolbar.TextDirectionLtr',
10711072
],
1073+
['Umb.Tiptap.Toolbar.TextIndent', 'Umb.Tiptap.Toolbar.TextOutdent'],
10721074
[
10731075
'Umb.Tiptap.Toolbar.BulletList',
10741076
'Umb.Tiptap.Toolbar.OrderedList',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { UmbTiptapExtensionApiBase } from '../base.js';
2+
import { TextIndent } from '@umbraco-cms/backoffice/external/tiptap';
3+
4+
export default class UmbTiptapTextIndentExtensionApi extends UmbTiptapExtensionApiBase {
5+
getTiptapExtensions = () => [
6+
TextIndent.configure({
7+
types: ['div', 'heading', 'paragraph', 'blockquote', 'listItem', 'orderedList', 'bulletList'],
8+
}),
9+
];
10+
}

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

+37
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,17 @@ const coreExtensions: Array<ManifestTiptapExtension> = [
161161
group: '#tiptap_extGroup_media',
162162
},
163163
},
164+
{
165+
type: 'tiptapExtension',
166+
alias: 'Umb.Tiptap.TextIndent',
167+
name: 'Text Indent Tiptap Extension',
168+
api: () => import('./core/text-indent.tiptap-api.js'),
169+
meta: {
170+
icon: 'icon-science',
171+
label: 'Text Indent',
172+
group: '#tiptap_extGroup_formatting',
173+
},
174+
},
164175
];
165176

166177
const toolbarExtensions: Array<UmbExtensionManifest> = [
@@ -606,6 +617,32 @@ const toolbarExtensions: Array<UmbExtensionManifest> = [
606617
label: '#tiptap_charmap',
607618
},
608619
},
620+
{
621+
type: 'tiptapToolbarExtension',
622+
kind: 'button',
623+
alias: 'Umb.Tiptap.Toolbar.TextIndent',
624+
name: 'Text Indent Tiptap Extension',
625+
api: () => import('./toolbar/text-indent.tiptap-toolbar-api.js'),
626+
forExtensions: ['Umb.Tiptap.TextIndent'],
627+
meta: {
628+
alias: 'indent',
629+
icon: 'icon-indent',
630+
label: 'Indent',
631+
},
632+
},
633+
{
634+
type: 'tiptapToolbarExtension',
635+
kind: 'button',
636+
alias: 'Umb.Tiptap.Toolbar.TextOutdent',
637+
name: 'Text Outdent Tiptap Extension',
638+
api: () => import('./toolbar/text-outdent.tiptap-toolbar-api.js'),
639+
forExtensions: ['Umb.Tiptap.TextIndent'],
640+
meta: {
641+
alias: 'outdent',
642+
icon: 'icon-outdent',
643+
label: 'Outdent',
644+
},
645+
},
609646
];
610647

611648
const extensions = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { UmbTiptapToolbarElementApiBase } from '../base.js';
2+
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
3+
4+
export default class UmbTiptapToolbarTextIndentExtensionApi extends UmbTiptapToolbarElementApiBase {
5+
override execute(editor?: Editor) {
6+
editor?.chain().focus().textIndent().run();
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { UmbTiptapToolbarElementApiBase } from '../base.js';
2+
import type { Editor } from '@umbraco-cms/backoffice/external/tiptap';
3+
4+
export default class UmbTiptapToolbarTextOutdentExtensionApi extends UmbTiptapToolbarElementApiBase {
5+
override execute(editor?: Editor) {
6+
editor?.chain().focus().textOutdent().run();
7+
}
8+
}

0 commit comments

Comments
 (0)