@semantic-components/editor 0.4.0 → 0.62.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.
- package/README.md +11 -4
- package/eslint.config.mjs +48 -0
- package/ng-package.json +8 -0
- package/package.json +14 -122
- package/project.json +28 -0
- package/src/index.ts +1 -0
- package/src/lib/components/editor/README.md +354 -0
- package/src/lib/components/editor/editor-align-center-button.ts +45 -0
- package/src/lib/components/editor/editor-align-justify-button.ts +45 -0
- package/src/lib/components/editor/editor-align-left-button.ts +44 -0
- package/src/lib/components/editor/editor-align-right-button.ts +44 -0
- package/src/lib/components/editor/editor-blockquote-button.ts +44 -0
- package/src/lib/components/editor/editor-bold-button.ts +44 -0
- package/src/lib/components/editor/editor-bullet-list-button.ts +44 -0
- package/src/lib/components/editor/editor-char-count.ts +42 -0
- package/src/lib/components/editor/editor-clear-formatting-button.ts +42 -0
- package/src/lib/components/editor/editor-code-button.ts +52 -0
- package/src/lib/components/editor/editor-content.ts +107 -0
- package/src/lib/components/editor/editor-count.ts +28 -0
- package/src/lib/components/editor/editor-footer.ts +30 -0
- package/src/lib/components/editor/editor-header.ts +27 -0
- package/src/lib/components/editor/editor-heading-select.ts +48 -0
- package/src/lib/components/editor/editor-horizontal-rule-button.ts +42 -0
- package/src/lib/components/editor/editor-italic-button.ts +44 -0
- package/src/lib/components/editor/editor-link-button.ts +58 -0
- package/src/lib/components/editor/editor-numbered-list-button.ts +44 -0
- package/src/lib/components/editor/editor-redo-button.ts +42 -0
- package/src/lib/components/editor/editor-separator.ts +25 -0
- package/src/lib/components/editor/editor-strikethrough-button.ts +44 -0
- package/src/lib/components/editor/editor-toolbar-group.ts +27 -0
- package/src/lib/components/editor/editor-toolbar.ts +32 -0
- package/src/lib/components/editor/editor-underline-button.ts +44 -0
- package/src/lib/components/editor/editor-undo-button.ts +42 -0
- package/src/lib/components/editor/editor-word-count.ts +43 -0
- package/src/lib/components/editor/editor.ts +211 -0
- package/src/lib/components/editor/index.ts +45 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/styles/editor.css +94 -0
- package/src/lib/styles/index.css +1 -0
- package/tsconfig.json +28 -0
- package/tsconfig.lib.json +12 -0
- package/tsconfig.lib.prod.json +7 -0
- package/fesm2022/semantic-components-editor.mjs +0 -4721
- package/fesm2022/semantic-components-editor.mjs.map +0 -1
- 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
|
+
}
|