@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.
- package/README.md +11 -4
- package/eslint.config.mjs +48 -0
- package/ng-package.json +7 -0
- package/package.json +10 -134
- 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,45 @@
|
|
|
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-align-justify]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-align-justify',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.aria-pressed]': 'editor.alignment() === "justify"',
|
|
23
|
+
'[attr.title]': '"Justify"',
|
|
24
|
+
'(click)': 'onClick()',
|
|
25
|
+
},
|
|
26
|
+
encapsulation: ViewEncapsulation.None,
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
})
|
|
29
|
+
export class ScEditorAlignJustifyButton {
|
|
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.alignment() === 'justify' &&
|
|
37
|
+
'bg-accent text-accent-foreground',
|
|
38
|
+
this.classInput(),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
onClick(): void {
|
|
43
|
+
this.editor.execCommand('justifyFull');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -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-align-left]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-align-left',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.aria-pressed]': 'editor.alignment() === "left"',
|
|
23
|
+
'[attr.title]': '"Align left"',
|
|
24
|
+
'(click)': 'onClick()',
|
|
25
|
+
},
|
|
26
|
+
encapsulation: ViewEncapsulation.None,
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
})
|
|
29
|
+
export class ScEditorAlignLeftButton {
|
|
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.alignment() === 'left' && 'bg-accent text-accent-foreground',
|
|
37
|
+
this.classInput(),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
onClick(): void {
|
|
42
|
+
this.editor.execCommand('justifyLeft');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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-align-right]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-align-right',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.aria-pressed]': 'editor.alignment() === "right"',
|
|
23
|
+
'[attr.title]': '"Align right"',
|
|
24
|
+
'(click)': 'onClick()',
|
|
25
|
+
},
|
|
26
|
+
encapsulation: ViewEncapsulation.None,
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
})
|
|
29
|
+
export class ScEditorAlignRightButton {
|
|
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.alignment() === 'right' && 'bg-accent text-accent-foreground',
|
|
37
|
+
this.classInput(),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
onClick(): void {
|
|
42
|
+
this.editor.execCommand('justifyRight');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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-blockquote]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-blockquote',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.aria-pressed]': 'editor.isBlockquote()',
|
|
23
|
+
'[attr.title]': '"Blockquote"',
|
|
24
|
+
'(click)': 'onClick()',
|
|
25
|
+
},
|
|
26
|
+
encapsulation: ViewEncapsulation.None,
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
})
|
|
29
|
+
export class ScEditorBlockquoteButton {
|
|
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.isBlockquote() && 'bg-accent text-accent-foreground',
|
|
37
|
+
this.classInput(),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
onClick(): void {
|
|
42
|
+
this.editor.execCommand('formatBlock', 'blockquote');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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-bold]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-bold',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.aria-pressed]': 'editor.isBold()',
|
|
23
|
+
'[attr.title]': '"Bold (Ctrl+B)"',
|
|
24
|
+
'(click)': 'onClick()',
|
|
25
|
+
},
|
|
26
|
+
encapsulation: ViewEncapsulation.None,
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
})
|
|
29
|
+
export class ScEditorBoldButton {
|
|
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.isBold() && 'bg-accent text-accent-foreground',
|
|
37
|
+
this.classInput(),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
onClick(): void {
|
|
42
|
+
this.editor.execCommand('bold');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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-bullet-list]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-bullet-list',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.aria-pressed]': 'editor.isUnorderedList()',
|
|
23
|
+
'[attr.title]': '"Bullet list"',
|
|
24
|
+
'(click)': 'onClick()',
|
|
25
|
+
},
|
|
26
|
+
encapsulation: ViewEncapsulation.None,
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
})
|
|
29
|
+
export class ScEditorBulletListButton {
|
|
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.isUnorderedList() && 'bg-accent text-accent-foreground',
|
|
37
|
+
this.classInput(),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
onClick(): void {
|
|
42
|
+
this.editor.execCommand('insertUnorderedList');
|
|
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: 'span[sc-editor-char-count]',
|
|
14
|
+
template: `
|
|
15
|
+
{{ charCount() }} characters
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-char-count',
|
|
19
|
+
'[class]': 'class()',
|
|
20
|
+
},
|
|
21
|
+
encapsulation: ViewEncapsulation.None,
|
|
22
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
23
|
+
})
|
|
24
|
+
export class ScEditorCharCount {
|
|
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 charCount = computed(() => {
|
|
31
|
+
const text = this.getPlainText();
|
|
32
|
+
return text.length;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
private getPlainText(): string {
|
|
36
|
+
const editorInstance = this.editor.editorInstance();
|
|
37
|
+
if (editorInstance) {
|
|
38
|
+
return editorInstance.getText();
|
|
39
|
+
}
|
|
40
|
+
return this.editor.contentElement()?.textContent || '';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -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-clear-formatting]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-clear-formatting',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.title]': '"Clear formatting"',
|
|
23
|
+
'(click)': 'onClick()',
|
|
24
|
+
},
|
|
25
|
+
encapsulation: ViewEncapsulation.None,
|
|
26
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
27
|
+
})
|
|
28
|
+
export class ScEditorClearFormattingButton {
|
|
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('removeFormat');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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-code]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-code',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.title]': '"Inline code"',
|
|
23
|
+
'(click)': 'onClick()',
|
|
24
|
+
},
|
|
25
|
+
encapsulation: ViewEncapsulation.None,
|
|
26
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
27
|
+
})
|
|
28
|
+
export class ScEditorCodeButton {
|
|
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
|
+
if (selectedText) {
|
|
46
|
+
const code = `<code>${selectedText}</code>`;
|
|
47
|
+
this.editor.execCommand('insertHTML', code);
|
|
48
|
+
} else {
|
|
49
|
+
this.editor.execCommand('insertHTML', '<code> </code>');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterNextRender,
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
computed,
|
|
6
|
+
DestroyRef,
|
|
7
|
+
effect,
|
|
8
|
+
ElementRef,
|
|
9
|
+
inject,
|
|
10
|
+
input,
|
|
11
|
+
model,
|
|
12
|
+
output,
|
|
13
|
+
signal,
|
|
14
|
+
ViewEncapsulation,
|
|
15
|
+
} from '@angular/core';
|
|
16
|
+
import { cn } from '@semantic-components/ui';
|
|
17
|
+
import { SC_EDITOR } from './editor';
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
selector: 'div[sc-editor-content]',
|
|
21
|
+
template: ``, // Empty - host element is the content div
|
|
22
|
+
host: {
|
|
23
|
+
'data-slot': 'editor-content',
|
|
24
|
+
'[class]': 'class()',
|
|
25
|
+
'[attr.aria-label]': 'ariaLabel()',
|
|
26
|
+
},
|
|
27
|
+
encapsulation: ViewEncapsulation.None,
|
|
28
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
|
+
})
|
|
30
|
+
export class ScEditorContent {
|
|
31
|
+
readonly editor = inject(SC_EDITOR);
|
|
32
|
+
private readonly elementRef = inject(ElementRef<HTMLElement>);
|
|
33
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
34
|
+
|
|
35
|
+
readonly value = model<string>('');
|
|
36
|
+
readonly placeholder = input<string>('Start typing...');
|
|
37
|
+
readonly ariaLabel = input<string>('Rich text editor');
|
|
38
|
+
readonly classInput = input<string>('', { alias: 'class' });
|
|
39
|
+
|
|
40
|
+
readonly focus = output<void>();
|
|
41
|
+
readonly blur = output<void>();
|
|
42
|
+
|
|
43
|
+
private readonly isFocused = signal(false);
|
|
44
|
+
private isInitialized = false;
|
|
45
|
+
|
|
46
|
+
protected readonly class = computed(() =>
|
|
47
|
+
cn(
|
|
48
|
+
'block outline-none overflow-y-auto min-h-[150px] max-h-[400px] p-4 prose prose-sm max-w-none dark:prose-invert',
|
|
49
|
+
this.editor.disabled() && 'pointer-events-none opacity-50',
|
|
50
|
+
this.editor.readonly() && 'cursor-default',
|
|
51
|
+
this.classInput(),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
constructor() {
|
|
56
|
+
// Watch for external value changes (must be in constructor for injection context)
|
|
57
|
+
effect(() => {
|
|
58
|
+
const newValue = this.value();
|
|
59
|
+
const editorInstance = this.editor.editorInstance();
|
|
60
|
+
if (!editorInstance || !this.isInitialized) return;
|
|
61
|
+
|
|
62
|
+
const currentValue = editorInstance.getHTML();
|
|
63
|
+
|
|
64
|
+
if (newValue !== currentValue) {
|
|
65
|
+
editorInstance.commands.setContent(newValue);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterNextRender(() => {
|
|
70
|
+
const element = this.elementRef.nativeElement;
|
|
71
|
+
|
|
72
|
+
// Initialize Tiptap editor through parent directive
|
|
73
|
+
this.editor.initializeEditor(element, this.value(), this.placeholder());
|
|
74
|
+
|
|
75
|
+
// Watch for editor content changes
|
|
76
|
+
const editorInstance = this.editor.editorInstance();
|
|
77
|
+
if (editorInstance) {
|
|
78
|
+
editorInstance.on('update', ({ editor }) => {
|
|
79
|
+
const html = editor.getHTML();
|
|
80
|
+
// Clean up empty content
|
|
81
|
+
const cleaned = html === '<p></p>' ? '' : html;
|
|
82
|
+
|
|
83
|
+
if (cleaned !== this.value()) {
|
|
84
|
+
this.value.set(cleaned);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Handle focus events
|
|
89
|
+
editorInstance.on('focus', () => {
|
|
90
|
+
this.isFocused.set(true);
|
|
91
|
+
this.focus.emit();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
editorInstance.on('blur', () => {
|
|
95
|
+
this.isFocused.set(false);
|
|
96
|
+
this.blur.emit();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.isInitialized = true;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.destroyRef.onDestroy(() => {
|
|
104
|
+
this.editor.destroyEditor();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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-count]',
|
|
12
|
+
template: `
|
|
13
|
+
<ng-content />
|
|
14
|
+
`,
|
|
15
|
+
host: {
|
|
16
|
+
'data-slot': 'editor-count',
|
|
17
|
+
'[class]': 'class()',
|
|
18
|
+
},
|
|
19
|
+
encapsulation: ViewEncapsulation.None,
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
})
|
|
22
|
+
export class ScEditorCount {
|
|
23
|
+
readonly classInput = input<string>('', { alias: 'class' });
|
|
24
|
+
|
|
25
|
+
protected readonly class = computed(() =>
|
|
26
|
+
cn('flex items-center justify-end gap-4', this.classInput()),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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-footer]',
|
|
12
|
+
template: `
|
|
13
|
+
<ng-content />
|
|
14
|
+
`,
|
|
15
|
+
host: {
|
|
16
|
+
'data-slot': 'editor-footer',
|
|
17
|
+
'[class]': 'class()',
|
|
18
|
+
},
|
|
19
|
+
encapsulation: ViewEncapsulation.None,
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
})
|
|
22
|
+
export class ScEditorFooter {
|
|
23
|
+
readonly classInput = input<string>('', { alias: 'class' });
|
|
24
|
+
protected readonly class = computed(() =>
|
|
25
|
+
cn(
|
|
26
|
+
'px-3 py-1.5 border-t text-xs text-muted-foreground bg-muted/30',
|
|
27
|
+
this.classInput(),
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -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-header]',
|
|
12
|
+
template: `
|
|
13
|
+
<ng-content />
|
|
14
|
+
`,
|
|
15
|
+
host: {
|
|
16
|
+
'data-slot': 'editor-header',
|
|
17
|
+
'[class]': 'class()',
|
|
18
|
+
},
|
|
19
|
+
encapsulation: ViewEncapsulation.None,
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
})
|
|
22
|
+
export class ScEditorHeader {
|
|
23
|
+
readonly classInput = input<string>('', { alias: 'class' });
|
|
24
|
+
protected readonly class = computed(() =>
|
|
25
|
+
cn('px-4 py-3 border-b bg-muted/30', this.classInput()),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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, ScEditorHeading } from './editor';
|
|
11
|
+
|
|
12
|
+
@Component({
|
|
13
|
+
selector: 'select[sc-editor-heading]',
|
|
14
|
+
template: `
|
|
15
|
+
<option value="p">Paragraph</option>
|
|
16
|
+
<option value="h1">Heading 1</option>
|
|
17
|
+
<option value="h2">Heading 2</option>
|
|
18
|
+
<option value="h3">Heading 3</option>
|
|
19
|
+
<option value="h4">Heading 4</option>
|
|
20
|
+
<option value="h5">Heading 5</option>
|
|
21
|
+
<option value="h6">Heading 6</option>
|
|
22
|
+
`,
|
|
23
|
+
host: {
|
|
24
|
+
'data-slot': 'editor-heading',
|
|
25
|
+
'[class]': 'class()',
|
|
26
|
+
'[disabled]': 'editor.disabled()',
|
|
27
|
+
'[value]': 'editor.currentHeading()',
|
|
28
|
+
'(change)': 'onChange($event)',
|
|
29
|
+
},
|
|
30
|
+
encapsulation: ViewEncapsulation.None,
|
|
31
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
32
|
+
})
|
|
33
|
+
export class ScEditorHeadingSelect {
|
|
34
|
+
readonly editor = inject(SC_EDITOR);
|
|
35
|
+
readonly classInput = input<string>('', { alias: 'class' });
|
|
36
|
+
|
|
37
|
+
protected readonly class = computed(() =>
|
|
38
|
+
cn(
|
|
39
|
+
'appearance-none pl-2 pr-6 py-1 text-sm rounded border-0 bg-transparent hover:bg-accent cursor-pointer',
|
|
40
|
+
this.classInput(),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
onChange(event: Event): void {
|
|
45
|
+
const value = (event.target as HTMLSelectElement).value as ScEditorHeading;
|
|
46
|
+
this.editor.execCommand('formatBlock', value === 'p' ? 'p' : value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -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-horizontal-rule]',
|
|
14
|
+
template: `
|
|
15
|
+
<ng-content />
|
|
16
|
+
`,
|
|
17
|
+
host: {
|
|
18
|
+
'data-slot': 'editor-horizontal-rule',
|
|
19
|
+
type: 'button',
|
|
20
|
+
'[class]': 'class()',
|
|
21
|
+
'[disabled]': 'editor.disabled()',
|
|
22
|
+
'[attr.title]': '"Horizontal line"',
|
|
23
|
+
'(click)': 'onClick()',
|
|
24
|
+
},
|
|
25
|
+
encapsulation: ViewEncapsulation.None,
|
|
26
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
27
|
+
})
|
|
28
|
+
export class ScEditorHorizontalRuleButton {
|
|
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('insertHorizontalRule');
|
|
41
|
+
}
|
|
42
|
+
}
|