@semantic-components/editor 0.3.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +11 -4
  2. package/eslint.config.mjs +48 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +10 -134
  5. package/project.json +28 -0
  6. package/src/index.ts +1 -0
  7. package/src/lib/components/editor/README.md +354 -0
  8. package/src/lib/components/editor/editor-align-center-button.ts +45 -0
  9. package/src/lib/components/editor/editor-align-justify-button.ts +45 -0
  10. package/src/lib/components/editor/editor-align-left-button.ts +44 -0
  11. package/src/lib/components/editor/editor-align-right-button.ts +44 -0
  12. package/src/lib/components/editor/editor-blockquote-button.ts +44 -0
  13. package/src/lib/components/editor/editor-bold-button.ts +44 -0
  14. package/src/lib/components/editor/editor-bullet-list-button.ts +44 -0
  15. package/src/lib/components/editor/editor-char-count.ts +42 -0
  16. package/src/lib/components/editor/editor-clear-formatting-button.ts +42 -0
  17. package/src/lib/components/editor/editor-code-button.ts +52 -0
  18. package/src/lib/components/editor/editor-content.ts +107 -0
  19. package/src/lib/components/editor/editor-count.ts +28 -0
  20. package/src/lib/components/editor/editor-footer.ts +30 -0
  21. package/src/lib/components/editor/editor-header.ts +27 -0
  22. package/src/lib/components/editor/editor-heading-select.ts +48 -0
  23. package/src/lib/components/editor/editor-horizontal-rule-button.ts +42 -0
  24. package/src/lib/components/editor/editor-italic-button.ts +44 -0
  25. package/src/lib/components/editor/editor-link-button.ts +58 -0
  26. package/src/lib/components/editor/editor-numbered-list-button.ts +44 -0
  27. package/src/lib/components/editor/editor-redo-button.ts +42 -0
  28. package/src/lib/components/editor/editor-separator.ts +25 -0
  29. package/src/lib/components/editor/editor-strikethrough-button.ts +44 -0
  30. package/src/lib/components/editor/editor-toolbar-group.ts +27 -0
  31. package/src/lib/components/editor/editor-toolbar.ts +32 -0
  32. package/src/lib/components/editor/editor-underline-button.ts +44 -0
  33. package/src/lib/components/editor/editor-undo-button.ts +42 -0
  34. package/src/lib/components/editor/editor-word-count.ts +43 -0
  35. package/src/lib/components/editor/editor.ts +211 -0
  36. package/src/lib/components/editor/index.ts +45 -0
  37. package/src/lib/components/index.ts +1 -0
  38. package/src/lib/styles/editor.css +94 -0
  39. package/src/lib/styles/index.css +1 -0
  40. package/tsconfig.json +28 -0
  41. package/tsconfig.lib.json +12 -0
  42. package/tsconfig.lib.prod.json +7 -0
  43. package/fesm2022/semantic-components-editor.mjs +0 -4721
  44. package/fesm2022/semantic-components-editor.mjs.map +0 -1
  45. package/index.d.ts +0 -385
@@ -0,0 +1,44 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ inject,
7
+ ViewEncapsulation,
8
+ } from '@angular/core';
9
+ import { cn } from '@semantic-components/ui';
10
+ import { SC_EDITOR } from './editor';
11
+
12
+ @Component({
13
+ selector: 'button[sc-editor-italic]',
14
+ template: `
15
+ <ng-content />
16
+ `,
17
+ host: {
18
+ 'data-slot': 'editor-italic',
19
+ type: 'button',
20
+ '[class]': 'class()',
21
+ '[disabled]': 'editor.disabled()',
22
+ '[attr.aria-pressed]': 'editor.isItalic()',
23
+ '[attr.title]': '"Italic (Ctrl+I)"',
24
+ '(click)': 'onClick()',
25
+ },
26
+ encapsulation: ViewEncapsulation.None,
27
+ changeDetection: ChangeDetectionStrategy.OnPush,
28
+ })
29
+ export class ScEditorItalicButton {
30
+ readonly editor = inject(SC_EDITOR);
31
+ readonly classInput = input<string>('', { alias: 'class' });
32
+
33
+ protected readonly class = computed(() =>
34
+ cn(
35
+ 'p-1.5 rounded hover:bg-accent disabled:opacity-50 [&_svg]:size-4',
36
+ this.editor.isItalic() && 'bg-accent text-accent-foreground',
37
+ this.classInput(),
38
+ ),
39
+ );
40
+
41
+ onClick(): void {
42
+ this.editor.execCommand('italic');
43
+ }
44
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ inject,
7
+ ViewEncapsulation,
8
+ } from '@angular/core';
9
+ import { cn } from '@semantic-components/ui';
10
+ import { SC_EDITOR } from './editor';
11
+
12
+ @Component({
13
+ selector: 'button[sc-editor-link]',
14
+ template: `
15
+ <ng-content />
16
+ `,
17
+ host: {
18
+ 'data-slot': 'editor-link',
19
+ type: 'button',
20
+ '[class]': 'class()',
21
+ '[disabled]': 'editor.disabled()',
22
+ '[attr.title]': '"Insert link (Ctrl+K)"',
23
+ '(click)': 'onClick()',
24
+ },
25
+ encapsulation: ViewEncapsulation.None,
26
+ changeDetection: ChangeDetectionStrategy.OnPush,
27
+ })
28
+ export class ScEditorLinkButton {
29
+ readonly editor = inject(SC_EDITOR);
30
+ readonly classInput = input<string>('', { alias: 'class' });
31
+
32
+ protected readonly class = computed(() =>
33
+ cn(
34
+ 'p-1.5 rounded hover:bg-accent disabled:opacity-50 [&_svg]:size-4',
35
+ this.classInput(),
36
+ ),
37
+ );
38
+
39
+ onClick(): void {
40
+ if (this.editor.disabled() || this.editor.readonly()) return;
41
+
42
+ const selection = window.getSelection();
43
+ const selectedText = selection?.toString() || '';
44
+
45
+ const url = prompt('Enter URL:', 'https://');
46
+ if (url) {
47
+ if (selectedText) {
48
+ this.editor.execCommand('createLink', url);
49
+ } else {
50
+ const linkText = prompt('Enter link text:', 'Link');
51
+ if (linkText) {
52
+ const link = `<a href="${url}" target="_blank" rel="noopener noreferrer">${linkText}</a>`;
53
+ this.editor.execCommand('insertHTML', link);
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,44 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ inject,
7
+ ViewEncapsulation,
8
+ } from '@angular/core';
9
+ import { cn } from '@semantic-components/ui';
10
+ import { SC_EDITOR } from './editor';
11
+
12
+ @Component({
13
+ selector: 'button[sc-editor-numbered-list]',
14
+ template: `
15
+ <ng-content />
16
+ `,
17
+ host: {
18
+ 'data-slot': 'editor-numbered-list',
19
+ type: 'button',
20
+ '[class]': 'class()',
21
+ '[disabled]': 'editor.disabled()',
22
+ '[attr.aria-pressed]': 'editor.isOrderedList()',
23
+ '[attr.title]': '"Numbered list"',
24
+ '(click)': 'onClick()',
25
+ },
26
+ encapsulation: ViewEncapsulation.None,
27
+ changeDetection: ChangeDetectionStrategy.OnPush,
28
+ })
29
+ export class ScEditorNumberedListButton {
30
+ readonly editor = inject(SC_EDITOR);
31
+ readonly classInput = input<string>('', { alias: 'class' });
32
+
33
+ protected readonly class = computed(() =>
34
+ cn(
35
+ 'p-1.5 rounded hover:bg-accent disabled:opacity-50 [&_svg]:size-4',
36
+ this.editor.isOrderedList() && 'bg-accent text-accent-foreground',
37
+ this.classInput(),
38
+ ),
39
+ );
40
+
41
+ onClick(): void {
42
+ this.editor.execCommand('insertOrderedList');
43
+ }
44
+ }
@@ -0,0 +1,42 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ inject,
7
+ ViewEncapsulation,
8
+ } from '@angular/core';
9
+ import { cn } from '@semantic-components/ui';
10
+ import { SC_EDITOR } from './editor';
11
+
12
+ @Component({
13
+ selector: 'button[sc-editor-redo]',
14
+ template: `
15
+ <ng-content />
16
+ `,
17
+ host: {
18
+ 'data-slot': 'editor-redo',
19
+ type: 'button',
20
+ '[class]': 'class()',
21
+ '[disabled]': 'editor.disabled() || !editor.canRedo()',
22
+ '[attr.title]': '"Redo (Ctrl+Y)"',
23
+ '(click)': 'onClick()',
24
+ },
25
+ encapsulation: ViewEncapsulation.None,
26
+ changeDetection: ChangeDetectionStrategy.OnPush,
27
+ })
28
+ export class ScEditorRedoButton {
29
+ readonly editor = inject(SC_EDITOR);
30
+ readonly classInput = input<string>('', { alias: 'class' });
31
+
32
+ protected readonly class = computed(() =>
33
+ cn(
34
+ 'p-1.5 rounded hover:bg-accent disabled:opacity-50 [&_svg]:size-4',
35
+ this.classInput(),
36
+ ),
37
+ );
38
+
39
+ onClick(): void {
40
+ this.editor.execCommand('redo');
41
+ }
42
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ ViewEncapsulation,
7
+ } from '@angular/core';
8
+ import { cn } from '@semantic-components/ui';
9
+
10
+ @Component({
11
+ selector: 'div[sc-editor-separator]',
12
+ template: ``,
13
+ host: {
14
+ 'data-slot': 'editor-separator',
15
+ '[class]': 'class()',
16
+ },
17
+ encapsulation: ViewEncapsulation.None,
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ })
20
+ export class ScEditorSeparator {
21
+ readonly classInput = input<string>('', { alias: 'class' });
22
+ protected readonly class = computed(() =>
23
+ cn('w-px h-6 bg-border mx-1', this.classInput()),
24
+ );
25
+ }
@@ -0,0 +1,44 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ inject,
7
+ ViewEncapsulation,
8
+ } from '@angular/core';
9
+ import { cn } from '@semantic-components/ui';
10
+ import { SC_EDITOR } from './editor';
11
+
12
+ @Component({
13
+ selector: 'button[sc-editor-strikethrough]',
14
+ template: `
15
+ <ng-content />
16
+ `,
17
+ host: {
18
+ 'data-slot': 'editor-strikethrough',
19
+ type: 'button',
20
+ '[class]': 'class()',
21
+ '[disabled]': 'editor.disabled()',
22
+ '[attr.aria-pressed]': 'editor.isStrikethrough()',
23
+ '[attr.title]': '"Strikethrough"',
24
+ '(click)': 'onClick()',
25
+ },
26
+ encapsulation: ViewEncapsulation.None,
27
+ changeDetection: ChangeDetectionStrategy.OnPush,
28
+ })
29
+ export class ScEditorStrikethroughButton {
30
+ readonly editor = inject(SC_EDITOR);
31
+ readonly classInput = input<string>('', { alias: 'class' });
32
+
33
+ protected readonly class = computed(() =>
34
+ cn(
35
+ 'p-1.5 rounded hover:bg-accent disabled:opacity-50 [&_svg]:size-4',
36
+ this.editor.isStrikethrough() && 'bg-accent text-accent-foreground',
37
+ this.classInput(),
38
+ ),
39
+ );
40
+
41
+ onClick(): void {
42
+ this.editor.execCommand('strikethrough');
43
+ }
44
+ }
@@ -0,0 +1,27 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ ViewEncapsulation,
7
+ } from '@angular/core';
8
+ import { cn } from '@semantic-components/ui';
9
+
10
+ @Component({
11
+ selector: 'div[sc-editor-toolbar-group]',
12
+ template: `
13
+ <ng-content />
14
+ `,
15
+ host: {
16
+ 'data-slot': 'editor-toolbar-group',
17
+ '[class]': 'class()',
18
+ },
19
+ encapsulation: ViewEncapsulation.None,
20
+ changeDetection: ChangeDetectionStrategy.OnPush,
21
+ })
22
+ export class ScEditorToolbarGroup {
23
+ readonly classInput = input<string>('', { alias: 'class' });
24
+ protected readonly class = computed(() =>
25
+ cn('flex items-center gap-1', this.classInput()),
26
+ );
27
+ }
@@ -0,0 +1,32 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ ViewEncapsulation,
7
+ } from '@angular/core';
8
+ import { cn } from '@semantic-components/ui';
9
+
10
+ @Component({
11
+ selector: 'div[sc-editor-toolbar]',
12
+ template: `
13
+ <ng-content />
14
+ `,
15
+ host: {
16
+ 'data-slot': 'editor-toolbar',
17
+ role: 'toolbar',
18
+ '[attr.aria-label]': '"Text formatting"',
19
+ '[class]': 'class()',
20
+ },
21
+ encapsulation: ViewEncapsulation.None,
22
+ changeDetection: ChangeDetectionStrategy.OnPush,
23
+ })
24
+ export class ScEditorToolbar {
25
+ readonly classInput = input<string>('', { alias: 'class' });
26
+ protected readonly class = computed(() =>
27
+ cn(
28
+ 'flex flex-wrap items-center gap-1 p-2 border-b bg-muted/30',
29
+ this.classInput(),
30
+ ),
31
+ );
32
+ }
@@ -0,0 +1,44 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ inject,
7
+ ViewEncapsulation,
8
+ } from '@angular/core';
9
+ import { cn } from '@semantic-components/ui';
10
+ import { SC_EDITOR } from './editor';
11
+
12
+ @Component({
13
+ selector: 'button[sc-editor-underline]',
14
+ template: `
15
+ <ng-content />
16
+ `,
17
+ host: {
18
+ 'data-slot': 'editor-underline',
19
+ type: 'button',
20
+ '[class]': 'class()',
21
+ '[disabled]': 'editor.disabled()',
22
+ '[attr.aria-pressed]': 'editor.isUnderline()',
23
+ '[attr.title]': '"Underline (Ctrl+U)"',
24
+ '(click)': 'onClick()',
25
+ },
26
+ encapsulation: ViewEncapsulation.None,
27
+ changeDetection: ChangeDetectionStrategy.OnPush,
28
+ })
29
+ export class ScEditorUnderlineButton {
30
+ readonly editor = inject(SC_EDITOR);
31
+ readonly classInput = input<string>('', { alias: 'class' });
32
+
33
+ protected readonly class = computed(() =>
34
+ cn(
35
+ 'p-1.5 rounded hover:bg-accent disabled:opacity-50 [&_svg]:size-4',
36
+ this.editor.isUnderline() && 'bg-accent text-accent-foreground',
37
+ this.classInput(),
38
+ ),
39
+ );
40
+
41
+ onClick(): void {
42
+ this.editor.execCommand('underline');
43
+ }
44
+ }
@@ -0,0 +1,42 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ inject,
7
+ ViewEncapsulation,
8
+ } from '@angular/core';
9
+ import { cn } from '@semantic-components/ui';
10
+ import { SC_EDITOR } from './editor';
11
+
12
+ @Component({
13
+ selector: 'button[sc-editor-undo]',
14
+ template: `
15
+ <ng-content />
16
+ `,
17
+ host: {
18
+ 'data-slot': 'editor-undo',
19
+ type: 'button',
20
+ '[class]': 'class()',
21
+ '[disabled]': 'editor.disabled() || !editor.canUndo()',
22
+ '[attr.title]': '"Undo (Ctrl+Z)"',
23
+ '(click)': 'onClick()',
24
+ },
25
+ encapsulation: ViewEncapsulation.None,
26
+ changeDetection: ChangeDetectionStrategy.OnPush,
27
+ })
28
+ export class ScEditorUndoButton {
29
+ readonly editor = inject(SC_EDITOR);
30
+ readonly classInput = input<string>('', { alias: 'class' });
31
+
32
+ protected readonly class = computed(() =>
33
+ cn(
34
+ 'p-1.5 rounded hover:bg-accent disabled:opacity-50 [&_svg]:size-4',
35
+ this.classInput(),
36
+ ),
37
+ );
38
+
39
+ onClick(): void {
40
+ this.editor.execCommand('undo');
41
+ }
42
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ Component,
3
+ ChangeDetectionStrategy,
4
+ computed,
5
+ input,
6
+ inject,
7
+ ViewEncapsulation,
8
+ } from '@angular/core';
9
+ import { cn } from '@semantic-components/ui';
10
+ import { SC_EDITOR } from './editor';
11
+
12
+ @Component({
13
+ selector: 'span[sc-editor-word-count]',
14
+ template: `
15
+ {{ wordCount() }} words
16
+ `,
17
+ host: {
18
+ 'data-slot': 'editor-word-count',
19
+ '[class]': 'class()',
20
+ },
21
+ encapsulation: ViewEncapsulation.None,
22
+ changeDetection: ChangeDetectionStrategy.OnPush,
23
+ })
24
+ export class ScEditorWordCount {
25
+ readonly editor = inject(SC_EDITOR);
26
+ readonly classInput = input<string>('', { alias: 'class' });
27
+
28
+ protected readonly class = computed(() => cn('', this.classInput()));
29
+
30
+ protected readonly wordCount = computed(() => {
31
+ const text = this.getPlainText().trim();
32
+ if (!text) return 0;
33
+ return text.split(/\s+/).filter(Boolean).length;
34
+ });
35
+
36
+ private getPlainText(): string {
37
+ const editorInstance = this.editor.editorInstance();
38
+ if (editorInstance) {
39
+ return editorInstance.getText();
40
+ }
41
+ return this.editor.contentElement()?.textContent || '';
42
+ }
43
+ }
@@ -0,0 +1,211 @@
1
+ import { Directive, InjectionToken, input, signal } from '@angular/core';
2
+ import { Editor } from '@tiptap/core';
3
+ import Placeholder from '@tiptap/extension-placeholder';
4
+ import TextAlign from '@tiptap/extension-text-align';
5
+ import StarterKit from '@tiptap/starter-kit';
6
+
7
+ export type ScEditorAlignment = 'left' | 'center' | 'right' | 'justify';
8
+ export type ScEditorHeading = 'p' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
9
+
10
+ export const SC_EDITOR = new InjectionToken<ScEditor>('SC_EDITOR');
11
+
12
+ @Directive({
13
+ selector: '[sc-editor]',
14
+ exportAs: 'scEditor',
15
+ providers: [{ provide: SC_EDITOR, useExisting: ScEditor }],
16
+ host: {
17
+ 'data-slot': 'editor',
18
+ '[attr.data-disabled]': 'disabled() || null',
19
+ },
20
+ })
21
+ export class ScEditor {
22
+ readonly disabled = input<boolean>(false);
23
+ readonly readonly = input<boolean>(false);
24
+
25
+ // State signals
26
+ readonly isBold = signal(false);
27
+ readonly isItalic = signal(false);
28
+ readonly isUnderline = signal(false);
29
+ readonly isStrikethrough = signal(false);
30
+ readonly isOrderedList = signal(false);
31
+ readonly isUnorderedList = signal(false);
32
+ readonly isBlockquote = signal(false);
33
+ readonly alignment = signal<ScEditorAlignment>('left');
34
+ readonly currentHeading = signal<ScEditorHeading>('p');
35
+ readonly canUndo = signal(false);
36
+ readonly canRedo = signal(false);
37
+
38
+ // Reference to content element
39
+ readonly contentElement = signal<HTMLElement | null>(null);
40
+
41
+ // Tiptap editor instance
42
+ readonly editorInstance = signal<Editor | null>(null);
43
+
44
+ // Initialize Tiptap editor
45
+ initializeEditor(
46
+ element: HTMLElement,
47
+ initialContent: string,
48
+ placeholder = 'Start typing...',
49
+ ): void {
50
+ const editor = new Editor({
51
+ element,
52
+ extensions: [
53
+ StarterKit.configure({
54
+ link: {
55
+ openOnClick: false,
56
+ HTMLAttributes: {
57
+ target: '_blank',
58
+ rel: 'noopener noreferrer',
59
+ },
60
+ },
61
+ }),
62
+ TextAlign.configure({
63
+ types: ['heading', 'paragraph'],
64
+ }),
65
+ Placeholder.configure({
66
+ placeholder,
67
+ }),
68
+ ],
69
+ content: initialContent,
70
+ editable: !this.disabled() && !this.readonly(),
71
+ onUpdate: () => {
72
+ this.updateToolbarState();
73
+ },
74
+ onSelectionUpdate: () => {
75
+ this.updateToolbarState();
76
+ },
77
+ });
78
+
79
+ this.editorInstance.set(editor);
80
+ this.contentElement.set(element);
81
+ }
82
+
83
+ // Execute a formatting command
84
+ execCommand(command: string, value?: string): void {
85
+ if (this.disabled() || this.readonly()) return;
86
+
87
+ const editor = this.editorInstance();
88
+ if (!editor) return;
89
+
90
+ editor.chain().focus();
91
+
92
+ switch (command) {
93
+ case 'bold':
94
+ editor.chain().focus().toggleBold().run();
95
+ break;
96
+ case 'italic':
97
+ editor.chain().focus().toggleItalic().run();
98
+ break;
99
+ case 'underline':
100
+ editor.chain().focus().toggleUnderline().run();
101
+ break;
102
+ case 'strikethrough':
103
+ editor.chain().focus().toggleStrike().run();
104
+ break;
105
+ case 'insertUnorderedList':
106
+ editor.chain().focus().toggleBulletList().run();
107
+ break;
108
+ case 'insertOrderedList':
109
+ editor.chain().focus().toggleOrderedList().run();
110
+ break;
111
+ case 'justifyLeft':
112
+ editor.chain().focus().setTextAlign('left').run();
113
+ break;
114
+ case 'justifyCenter':
115
+ editor.chain().focus().setTextAlign('center').run();
116
+ break;
117
+ case 'justifyRight':
118
+ editor.chain().focus().setTextAlign('right').run();
119
+ break;
120
+ case 'justifyFull':
121
+ editor.chain().focus().setTextAlign('justify').run();
122
+ break;
123
+ case 'formatBlock':
124
+ if (value === 'blockquote') {
125
+ editor.chain().focus().toggleBlockquote().run();
126
+ } else if (value && value !== 'p') {
127
+ const level = parseInt(value.replace('h', '')) as
128
+ | 1
129
+ | 2
130
+ | 3
131
+ | 4
132
+ | 5
133
+ | 6;
134
+ editor.chain().focus().setHeading({ level }).run();
135
+ } else {
136
+ editor.chain().focus().setParagraph().run();
137
+ }
138
+ break;
139
+ case 'createLink':
140
+ if (value) {
141
+ editor.chain().focus().setLink({ href: value }).run();
142
+ }
143
+ break;
144
+ case 'insertHTML':
145
+ if (value) {
146
+ editor.chain().focus().insertContent(value).run();
147
+ }
148
+ break;
149
+ case 'insertHorizontalRule':
150
+ editor.chain().focus().setHorizontalRule().run();
151
+ break;
152
+ case 'removeFormat':
153
+ editor.chain().focus().clearNodes().unsetAllMarks().run();
154
+ break;
155
+ case 'undo':
156
+ editor.chain().focus().undo().run();
157
+ break;
158
+ case 'redo':
159
+ editor.chain().focus().redo().run();
160
+ break;
161
+ }
162
+ }
163
+
164
+ // Destroy editor
165
+ destroyEditor(): void {
166
+ this.editorInstance()?.destroy();
167
+ this.editorInstance.set(null);
168
+ }
169
+
170
+ // Update toolbar state based on current selection
171
+ updateToolbarState(): void {
172
+ const editor = this.editorInstance();
173
+ if (!editor) return;
174
+
175
+ this.isBold.set(editor.isActive('bold'));
176
+ this.isItalic.set(editor.isActive('italic'));
177
+ this.isUnderline.set(editor.isActive('underline'));
178
+ this.isStrikethrough.set(editor.isActive('strike'));
179
+ this.isOrderedList.set(editor.isActive('orderedList'));
180
+ this.isUnorderedList.set(editor.isActive('bulletList'));
181
+ this.isBlockquote.set(editor.isActive('blockquote'));
182
+
183
+ // Check alignment
184
+ if (editor.isActive({ textAlign: 'left' })) {
185
+ this.alignment.set('left');
186
+ } else if (editor.isActive({ textAlign: 'center' })) {
187
+ this.alignment.set('center');
188
+ } else if (editor.isActive({ textAlign: 'right' })) {
189
+ this.alignment.set('right');
190
+ } else if (editor.isActive({ textAlign: 'justify' })) {
191
+ this.alignment.set('justify');
192
+ } else {
193
+ this.alignment.set('left'); // default
194
+ }
195
+
196
+ // Check heading level
197
+ for (let level = 1; level <= 6; level++) {
198
+ if (editor.isActive('heading', { level })) {
199
+ this.currentHeading.set(`h${level}` as ScEditorHeading);
200
+ this.canUndo.set(editor.can().undo());
201
+ this.canRedo.set(editor.can().redo());
202
+ return;
203
+ }
204
+ }
205
+ this.currentHeading.set('p');
206
+
207
+ // Check undo/redo availability
208
+ this.canUndo.set(editor.can().undo());
209
+ this.canRedo.set(editor.can().redo());
210
+ }
211
+ }