@redvars/peacock 3.6.0 → 3.6.1
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/dist/code-highlighter.js +1 -1
- package/dist/code-highlighter.js.map +1 -1
- package/dist/custom-elements-jsdocs.json +928 -96
- package/dist/custom-elements.json +1294 -379
- package/dist/{flow-designer-dZnLJOQT.js → flow-designer-DvTUrDp5.js} +3 -3
- package/dist/{flow-designer-dZnLJOQT.js.map → flow-designer-DvTUrDp5.js.map} +1 -1
- package/dist/{flow-designer-node-XMe-jlKg.js → flow-designer-node-BWrPuxAR.js} +2 -2
- package/dist/flow-designer-node-BWrPuxAR.js.map +1 -0
- package/dist/flow-designer-node.js +1 -1
- package/dist/flow-designer.js +2 -2
- package/dist/html-editor.js +27245 -87
- package/dist/html-editor.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/modal.js +1 -7
- package/dist/modal.js.map +1 -1
- package/dist/{navigation-rail-DyO0oAZU.js → navigation-rail-DTTkqohi.js} +763 -214
- package/dist/navigation-rail-DTTkqohi.js.map +1 -0
- package/dist/peacock-loader.js +12 -3
- package/dist/peacock-loader.js.map +1 -1
- package/dist/src/html-editor/html-editor.d.ts +44 -11
- package/dist/src/index.d.ts +2 -0
- package/dist/src/list/index.d.ts +2 -0
- package/dist/src/list/list-item.d.ts +35 -0
- package/dist/src/list/list.d.ts +28 -0
- package/dist/src/modal/modal.d.ts +1 -7
- package/dist/src/navigation-rail/navigation-rail.d.ts +3 -7
- package/dist/src/number-field/number-field.d.ts +2 -2
- package/dist/src/svg/index.d.ts +1 -0
- package/dist/src/svg/svg.d.ts +38 -0
- package/dist/src/toolbar/toolbar.d.ts +3 -3
- package/dist/toolbar.js +3 -3
- package/dist/toolbar.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -1
- package/readme.md +3 -3
- package/src/code-highlighter/code-highlighter.ts +1 -1
- package/src/flow-designer/flow-designer-node.ts +1 -1
- package/src/html-editor/html-editor.scss +44 -2
- package/src/html-editor/html-editor.ts +309 -94
- package/src/index.ts +2 -1
- package/src/list/index.ts +2 -0
- package/src/list/list-item.scss +111 -0
- package/src/list/list-item.ts +175 -0
- package/src/list/list.scss +24 -0
- package/src/list/list.ts +51 -0
- package/src/modal/modal.ts +1 -7
- package/src/navigation-rail/navigation-rail-item.scss +7 -38
- package/src/navigation-rail/navigation-rail-item.ts +1 -2
- package/src/navigation-rail/navigation-rail.scss +17 -21
- package/src/navigation-rail/navigation-rail.ts +6 -9
- package/src/number-field/number-field.ts +2 -2
- package/src/peacock-loader.ts +12 -0
- package/src/svg/index.ts +1 -0
- package/src/svg/svg.scss +91 -0
- package/src/svg/svg.ts +160 -0
- package/src/toolbar/toolbar.ts +3 -3
- package/dist/flow-designer-node-XMe-jlKg.js.map +0 -1
- package/dist/navigation-rail-DyO0oAZU.js.map +0 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@redvars/peacock",
|
|
3
3
|
"description": "The foundation for beautiful user interfaces",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "3.6.
|
|
5
|
+
"version": "3.6.1",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"module": "dist/index.js",
|
|
@@ -41,6 +41,12 @@
|
|
|
41
41
|
"test:watch": "tsc && concurrently -k -r \"tsc --watch --preserveWatchOutput\" \"wtr --watch\""
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@tiptap/core": "^2.11.3",
|
|
45
|
+
"@tiptap/extension-mention": "^2.11.3",
|
|
46
|
+
"@tiptap/extension-placeholder": "^2.11.3",
|
|
47
|
+
"@tiptap/extension-underline": "^2.11.3",
|
|
48
|
+
"@tiptap/pm": "^2.11.3",
|
|
49
|
+
"@tiptap/starter-kit": "^2.11.3",
|
|
44
50
|
"@floating-ui/dom": "^1.7.5",
|
|
45
51
|
"@types/prettier": "^3.0.0",
|
|
46
52
|
"d3": "^7.9.0",
|
package/readme.md
CHANGED
|
@@ -44,9 +44,9 @@ Visit [https://peacock.redvars.com](https://peacock.redvars.com) to view the doc
|
|
|
44
44
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
45
45
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Mono:wght@100..900&family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">
|
|
46
46
|
|
|
47
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@redvars/peacock@3.6.
|
|
47
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@redvars/peacock@3.6.1/dist/assets/styles.css"></link>
|
|
48
48
|
<script type='module'
|
|
49
|
-
src='https://cdn.jsdelivr.net/npm/@redvars/peacock@3.6.
|
|
49
|
+
src='https://cdn.jsdelivr.net/npm/@redvars/peacock@3.6.1/dist/peacock-loader.js'></script>
|
|
50
50
|
</head>
|
|
51
51
|
|
|
52
52
|
<wc-button>Button</wc-button>
|
|
@@ -74,7 +74,7 @@ menus, checkboxes, and radio buttons.
|
|
|
74
74
|
| Date time picker | datetime-picker | 🔴 |
|
|
75
75
|
| [Field / Form control](https://peacock.redvars.com/components/field) | wc-field | 🟡 |
|
|
76
76
|
| File picker | file-picker | 🔴 |
|
|
77
|
-
| HTML editor
|
|
77
|
+
| [HTML editor](https://peacock.redvars.com/components/html-editor) | wc-html-editor | 🟡 |
|
|
78
78
|
| [Input](https://peacock.redvars.com/components/input) | wc-input | 🟢 |
|
|
79
79
|
| Input URL | input-url | 🔴 |
|
|
80
80
|
| Month picker | month-picker | 🔴 |
|
|
@@ -171,7 +171,7 @@ export class CodeHighlighter extends LitElement {
|
|
|
171
171
|
<div class="header-title">${this.language}</div>
|
|
172
172
|
<div class="header-actions">
|
|
173
173
|
<wc-icon-button
|
|
174
|
-
color="
|
|
174
|
+
color="surface"
|
|
175
175
|
variant="text"
|
|
176
176
|
size="xs"
|
|
177
177
|
aria-label=${locale.copyToClipboard}
|
|
@@ -143,7 +143,7 @@ export class FlowDesignerNode extends LitElement {
|
|
|
143
143
|
|
|
144
144
|
return html`
|
|
145
145
|
<div class="node-header">
|
|
146
|
-
<wc-icon name=${icon} class="node-icon"></wc-icon>
|
|
146
|
+
<wc-icon provider="carbon" name=${icon} class="node-icon"></wc-icon>
|
|
147
147
|
<span class="node-title">${node.label}</span>
|
|
148
148
|
</div>
|
|
149
149
|
`;
|
|
@@ -13,10 +13,18 @@
|
|
|
13
13
|
// Let the field expand to fit content vertically
|
|
14
14
|
--field-height: auto;
|
|
15
15
|
--field-padding-block: 0;
|
|
16
|
+
--code-editor-height: var(--html-editor-min-height, 8rem);
|
|
16
17
|
|
|
17
18
|
width: 100%;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
.mode-switcher {
|
|
22
|
+
display: flex;
|
|
23
|
+
justify-content: flex-end;
|
|
24
|
+
padding: var(--spacing-075, 0.375rem) var(--spacing-100, 0.5rem)
|
|
25
|
+
var(--spacing-050, 0.25rem);
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
// ── Toolbar ─────────────────────────────────────────────────────────────────
|
|
21
29
|
|
|
22
30
|
.html-editor-toolbar {
|
|
@@ -76,6 +84,15 @@
|
|
|
76
84
|
cursor: not-allowed;
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
&.active {
|
|
88
|
+
background: color-mix(
|
|
89
|
+
in srgb,
|
|
90
|
+
var(--color-primary, #6750a4) 16%,
|
|
91
|
+
transparent
|
|
92
|
+
);
|
|
93
|
+
color: var(--color-primary, #6750a4);
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
wc-icon {
|
|
80
97
|
pointer-events: none;
|
|
81
98
|
}
|
|
@@ -104,13 +121,27 @@
|
|
|
104
121
|
word-break: break-word;
|
|
105
122
|
overflow-wrap: break-word;
|
|
106
123
|
|
|
124
|
+
&.hidden {
|
|
125
|
+
display: none;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.tiptap-root {
|
|
129
|
+
min-height: calc(var(--html-editor-min-height, 8rem) - 2rem);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ProseMirror {
|
|
133
|
+
outline: none;
|
|
134
|
+
min-height: calc(var(--html-editor-min-height, 8rem) - 2rem);
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
// Placeholder
|
|
108
|
-
|
|
138
|
+
.ProseMirror p.is-editor-empty:first-child::before {
|
|
109
139
|
content: attr(data-placeholder);
|
|
110
140
|
color: var(--color-on-surface-variant, #49454f);
|
|
111
141
|
opacity: 0.6;
|
|
112
142
|
pointer-events: none;
|
|
113
|
-
|
|
143
|
+
float: left;
|
|
144
|
+
height: 0;
|
|
114
145
|
}
|
|
115
146
|
|
|
116
147
|
// Sensible defaults for user-generated rich content
|
|
@@ -131,6 +162,12 @@
|
|
|
131
162
|
}
|
|
132
163
|
}
|
|
133
164
|
|
|
165
|
+
.html-source {
|
|
166
|
+
&.hidden {
|
|
167
|
+
display: none;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
134
171
|
// ── Read-only tag ───────────────────────────────────────────────────────────
|
|
135
172
|
|
|
136
173
|
.read-only-tag {
|
|
@@ -144,3 +181,8 @@
|
|
|
144
181
|
cursor: not-allowed;
|
|
145
182
|
opacity: 0.6;
|
|
146
183
|
}
|
|
184
|
+
|
|
185
|
+
:host([disabled]) .html-source,
|
|
186
|
+
:host([readonly]) .html-source {
|
|
187
|
+
opacity: 0.7;
|
|
188
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { html, nothing } from 'lit';
|
|
2
2
|
import { property, query, state } from 'lit/decorators.js';
|
|
3
3
|
import { classMap } from 'lit/directives/class-map.js';
|
|
4
|
+
import { Editor, mergeAttributes } from '@tiptap/core';
|
|
5
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
6
|
+
import Underline from '@tiptap/extension-underline';
|
|
7
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
8
|
+
import Mention from '@tiptap/extension-mention';
|
|
9
|
+
import { html as beautifyHtml } from 'js-beautify';
|
|
4
10
|
|
|
5
11
|
import IndividualComponent from '@/IndividualComponent.js';
|
|
6
12
|
import BaseInput from '../input/BaseInput.js';
|
|
@@ -13,24 +19,32 @@ import styles from './html-editor.scss';
|
|
|
13
19
|
* @tag wc-html-editor
|
|
14
20
|
* @rawTag html-editor
|
|
15
21
|
*
|
|
16
|
-
* @summary A
|
|
22
|
+
* @summary A Tiptap-powered HTML editor with visual and source editing modes.
|
|
17
23
|
* @overview
|
|
18
|
-
* <p>The HTML Editor provides a rich-text editing experience
|
|
19
|
-
*
|
|
20
|
-
*
|
|
24
|
+
* <p>The HTML Editor provides a rich-text editing experience built on Tiptap.
|
|
25
|
+
* It wraps the editable area in a Material 3 styled <code>wc-field</code>,
|
|
26
|
+
* exposes common formatting actions, and includes a segmented switch between
|
|
27
|
+
* <strong>Visual</strong> and <strong>HTML</strong> source modes.</p>
|
|
21
28
|
*
|
|
22
29
|
* <p>Get and set the HTML content via the <code>value</code> property. The component
|
|
23
|
-
* dispatches a <code>change</code> event whenever the content is modified
|
|
30
|
+
* dispatches a <code>change</code> event whenever the content is modified.
|
|
31
|
+
* Mention suggestions are supported through the <code>mentions</code> property,
|
|
32
|
+
* with optional externally managed lookup via the <code>search</code> event.</p>
|
|
24
33
|
*
|
|
25
34
|
* @cssprop --html-editor-min-height - Minimum height of the editable area. Defaults to 8rem.
|
|
26
35
|
* @cssprop --html-editor-toolbar-background - Background color of the toolbar.
|
|
27
36
|
* @cssprop --html-editor-toolbar-border-color - Border color between toolbar and editing area.
|
|
28
37
|
*
|
|
29
38
|
* @fires {Event} change - Fired whenever the editable content changes.
|
|
39
|
+
* @fires {CustomEvent} search - Fired in managed mention mode with { query, callback } detail.
|
|
30
40
|
*
|
|
31
41
|
* @example
|
|
32
42
|
* ```html
|
|
33
|
-
* <wc-html-editor
|
|
43
|
+
* <wc-html-editor
|
|
44
|
+
* label="Description"
|
|
45
|
+
* value="<p>Hello <strong>world</strong></p>"
|
|
46
|
+
* .mentions="[{ label: 'Alex', value: 'alex' }]"
|
|
47
|
+
* ></wc-html-editor>
|
|
34
48
|
* ```
|
|
35
49
|
* @tags input editor
|
|
36
50
|
*/
|
|
@@ -38,6 +52,10 @@ import styles from './html-editor.scss';
|
|
|
38
52
|
export class HtmlEditor extends BaseInput {
|
|
39
53
|
static styles = [styles];
|
|
40
54
|
|
|
55
|
+
private _editor?: Editor;
|
|
56
|
+
|
|
57
|
+
private _changeTimeout?: number;
|
|
58
|
+
|
|
41
59
|
/** Current HTML value of the editor. */
|
|
42
60
|
@property({ type: String })
|
|
43
61
|
value = '';
|
|
@@ -66,64 +84,230 @@ export class HtmlEditor extends BaseInput {
|
|
|
66
84
|
@property({ type: String, attribute: 'error-text' })
|
|
67
85
|
errorText = '';
|
|
68
86
|
|
|
87
|
+
/** Whether toolbar controls should be displayed in visual mode. */
|
|
88
|
+
@property({ type: Boolean, attribute: 'show-toolbar' })
|
|
89
|
+
showToolbar = true;
|
|
90
|
+
|
|
91
|
+
/** Mention suggestions used by the mention extension. */
|
|
92
|
+
@property({ type: Array })
|
|
93
|
+
mentions: Array<{ label: string; value: string }> = [];
|
|
94
|
+
|
|
95
|
+
/** Mention filtering mode. */
|
|
96
|
+
@property({ type: String, attribute: 'mentions-search' })
|
|
97
|
+
mentionsSearch: 'contains' | 'managed' = 'contains';
|
|
98
|
+
|
|
99
|
+
/** Character that triggers mention suggestions. */
|
|
100
|
+
@property({ type: String, attribute: 'suggestion-character' })
|
|
101
|
+
suggestionCharacter = '@';
|
|
102
|
+
|
|
103
|
+
/** Whether to include the suggestion character in rendered mention text. */
|
|
104
|
+
@property({ type: Boolean, attribute: 'show-suggestion-character' })
|
|
105
|
+
showSuggestionCharacter = true;
|
|
106
|
+
|
|
107
|
+
/** Debounce in milliseconds for dispatching `change`. */
|
|
108
|
+
@property({ type: Number })
|
|
109
|
+
debounce = 250;
|
|
110
|
+
|
|
69
111
|
@state() private _focused = false;
|
|
70
112
|
|
|
71
|
-
@
|
|
113
|
+
@state() private _mode: 'visual' | 'html' = 'visual';
|
|
114
|
+
|
|
115
|
+
@query('.tiptap-root')
|
|
72
116
|
private _editorEl!: HTMLDivElement;
|
|
73
117
|
|
|
74
118
|
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
|
75
119
|
|
|
76
120
|
protected firstUpdated() {
|
|
77
|
-
|
|
78
|
-
|
|
121
|
+
this._initializeEditor();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
disconnectedCallback() {
|
|
125
|
+
super.disconnectedCallback();
|
|
126
|
+
if (this._changeTimeout) {
|
|
127
|
+
window.clearTimeout(this._changeTimeout);
|
|
128
|
+
this._changeTimeout = undefined;
|
|
79
129
|
}
|
|
130
|
+
this._destroyEditor();
|
|
80
131
|
}
|
|
81
132
|
|
|
82
133
|
protected updated(changed: Map<string, unknown>) {
|
|
83
|
-
if (changed.has('value') && this.
|
|
84
|
-
|
|
85
|
-
|
|
134
|
+
if (changed.has('value') && this._editor) {
|
|
135
|
+
const editorHtml = HtmlEditor._normalizeHtml(this._editor.getHTML());
|
|
136
|
+
const nextHtml = HtmlEditor._normalizeHtml(this.value);
|
|
137
|
+
if (editorHtml !== nextHtml) {
|
|
138
|
+
this._editor.commands.setContent(this.value ?? '', false);
|
|
86
139
|
}
|
|
87
140
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
141
|
+
|
|
142
|
+
if ((changed.has('disabled') || changed.has('readonly')) && this._editor) {
|
|
143
|
+
this._editor.setEditable(!(this.disabled || this.readonly));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (changed.has('placeholder') && this._editor) {
|
|
147
|
+
this._destroyEditor();
|
|
148
|
+
this._initializeEditor();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (changed.has('mentions') && this._editor) {
|
|
152
|
+
const oldMentions = changed.get('mentions') as Array<{
|
|
153
|
+
label: string;
|
|
154
|
+
value: string;
|
|
155
|
+
}>;
|
|
156
|
+
if (oldMentions !== this.mentions) {
|
|
157
|
+
this._destroyEditor();
|
|
158
|
+
this._initializeEditor();
|
|
92
159
|
}
|
|
93
160
|
}
|
|
94
161
|
}
|
|
95
162
|
|
|
96
163
|
// ─── Private helpers ───────────────────────────────────────────────────────
|
|
97
164
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// execCommand is deprecated but remains broadly supported for rich-text editing
|
|
103
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
-
(document as any).execCommand(command, false, value ?? null);
|
|
165
|
+
private static _normalizeHtml(value: string) {
|
|
166
|
+
return beautifyHtml(value ?? '', {
|
|
167
|
+
wrap_line_length: 120,
|
|
168
|
+
});
|
|
105
169
|
}
|
|
106
170
|
|
|
107
|
-
private
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
);
|
|
171
|
+
private _destroyEditor() {
|
|
172
|
+
if (!this._editor) return;
|
|
173
|
+
this._editorEl?.removeEventListener('click', this._focusEditorOnContainerClick);
|
|
174
|
+
this._editor.destroy();
|
|
175
|
+
this._editor = undefined;
|
|
113
176
|
}
|
|
114
177
|
|
|
115
|
-
private
|
|
116
|
-
this.
|
|
178
|
+
private _initializeEditor() {
|
|
179
|
+
if (!this._editorEl || this._editor) return;
|
|
180
|
+
|
|
181
|
+
this._editor = new Editor({
|
|
182
|
+
element: this._editorEl,
|
|
183
|
+
extensions: [
|
|
184
|
+
StarterKit,
|
|
185
|
+
Underline,
|
|
186
|
+
Placeholder.configure({
|
|
187
|
+
placeholder: this.placeholder,
|
|
188
|
+
}),
|
|
189
|
+
Mention.configure({
|
|
190
|
+
HTMLAttributes: {
|
|
191
|
+
class: 'mention',
|
|
192
|
+
},
|
|
193
|
+
renderHTML: ({ options, node }) => {
|
|
194
|
+
const item = this._getMentionItem(node.attrs.id);
|
|
195
|
+
return [
|
|
196
|
+
'a',
|
|
197
|
+
mergeAttributes({ contenteditable: false }, options.HTMLAttributes),
|
|
198
|
+
`${this.showSuggestionCharacter ? options.suggestion.char : ''}${
|
|
199
|
+
item ? item.label : node.attrs.id
|
|
200
|
+
}`,
|
|
201
|
+
];
|
|
202
|
+
},
|
|
203
|
+
suggestion: {
|
|
204
|
+
allowSpaces: true,
|
|
205
|
+
char: this.suggestionCharacter,
|
|
206
|
+
items: async ({ query: mentionQuery }) => {
|
|
207
|
+
if (this.mentionsSearch === 'managed') {
|
|
208
|
+
return this._requestManagedMentions(mentionQuery);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return this.mentions
|
|
212
|
+
.filter(item =>
|
|
213
|
+
item.label.toLowerCase().startsWith(mentionQuery.toLowerCase()),
|
|
214
|
+
)
|
|
215
|
+
.map(item => item.value)
|
|
216
|
+
.slice(0, 5);
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
],
|
|
221
|
+
content: this.value,
|
|
222
|
+
editable: !(this.disabled || this.readonly),
|
|
223
|
+
onFocus: () => {
|
|
224
|
+
this._focused = true;
|
|
225
|
+
},
|
|
226
|
+
onBlur: () => {
|
|
227
|
+
this._focused = false;
|
|
228
|
+
},
|
|
229
|
+
onUpdate: () => {
|
|
230
|
+
if (!this._editor) return;
|
|
231
|
+
|
|
232
|
+
const nextHtml = HtmlEditor._normalizeHtml(this._editor.getHTML());
|
|
233
|
+
if (nextHtml !== this.value) {
|
|
234
|
+
this.value = nextHtml;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this._dispatchDebouncedChange();
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
this._editorEl.addEventListener('click', this._focusEditorOnContainerClick);
|
|
117
242
|
}
|
|
118
243
|
|
|
119
|
-
private
|
|
120
|
-
this.
|
|
244
|
+
private _focusEditorOnContainerClick = (event: Event) => {
|
|
245
|
+
if (!this._editor) return;
|
|
246
|
+
if (event.target === this._editorEl) {
|
|
247
|
+
this._editor.commands.focus('end');
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
private _dispatchDebouncedChange() {
|
|
252
|
+
if (this._changeTimeout) {
|
|
253
|
+
window.clearTimeout(this._changeTimeout);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this._changeTimeout = window.setTimeout(() => {
|
|
257
|
+
redispatchEvent(this, new Event('change', { bubbles: true, composed: true }));
|
|
258
|
+
}, this.debounce);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private _requestManagedMentions(mentionQuery: string): Promise<string[]> {
|
|
262
|
+
return new Promise(resolve => {
|
|
263
|
+
this.dispatchEvent(
|
|
264
|
+
new CustomEvent('search', {
|
|
265
|
+
detail: {
|
|
266
|
+
query: mentionQuery,
|
|
267
|
+
callback: (mentions: Array<{ label: string; value: string }>) => {
|
|
268
|
+
this.mentions = mentions;
|
|
269
|
+
resolve(this.mentions.map(item => item.value));
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
bubbles: true,
|
|
273
|
+
composed: true,
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private _getMentionItem(value: string) {
|
|
280
|
+
return this.mentions.find(item => item.value === value);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private _execCommand(command: () => void) {
|
|
284
|
+
if (this.disabled || this.readonly || !this._editor) return;
|
|
285
|
+
command();
|
|
286
|
+
this._editor.commands.focus();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private _switchMode(event: CustomEvent<{ value: string }>) {
|
|
290
|
+
event.stopPropagation();
|
|
291
|
+
const nextMode = event.detail?.value === 'html' ? 'html' : 'visual';
|
|
292
|
+
|
|
293
|
+
if (nextMode === this._mode) return;
|
|
294
|
+
|
|
295
|
+
if (nextMode === 'html' && this._editor) {
|
|
296
|
+
this.value = HtmlEditor._normalizeHtml(this._editor.getHTML());
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this._mode = nextMode;
|
|
121
300
|
}
|
|
122
301
|
|
|
123
|
-
private
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
302
|
+
private _handleSourceChange(event: Event) {
|
|
303
|
+
event.stopPropagation();
|
|
304
|
+
const target = event.currentTarget as { value?: string };
|
|
305
|
+
const nextValue = target.value ?? '';
|
|
306
|
+
|
|
307
|
+
if (nextValue === this.value) return;
|
|
308
|
+
|
|
309
|
+
this.value = nextValue;
|
|
310
|
+
this._dispatchDebouncedChange();
|
|
127
311
|
}
|
|
128
312
|
|
|
129
313
|
// ─── Toolbar button ────────────────────────────────────────────────────────
|
|
@@ -131,19 +315,22 @@ export class HtmlEditor extends BaseInput {
|
|
|
131
315
|
private _toolbarButton(
|
|
132
316
|
icon: string,
|
|
133
317
|
title: string,
|
|
134
|
-
|
|
135
|
-
|
|
318
|
+
action: () => void,
|
|
319
|
+
active = false,
|
|
136
320
|
) {
|
|
137
321
|
return html`
|
|
138
322
|
<button
|
|
139
|
-
class
|
|
323
|
+
class=${classMap({
|
|
324
|
+
'toolbar-btn': true,
|
|
325
|
+
active,
|
|
326
|
+
})}
|
|
140
327
|
title=${title}
|
|
141
328
|
aria-label=${title}
|
|
142
329
|
?disabled=${this.disabled || this.readonly}
|
|
143
330
|
@mousedown=${(e: Event) => e.preventDefault()}
|
|
144
331
|
@click=${(e: Event) => {
|
|
145
332
|
e.preventDefault();
|
|
146
|
-
this.
|
|
333
|
+
this._execCommand(action);
|
|
147
334
|
}}
|
|
148
335
|
>
|
|
149
336
|
<wc-icon name=${icon} size="sm"></wc-icon>
|
|
@@ -154,33 +341,52 @@ export class HtmlEditor extends BaseInput {
|
|
|
154
341
|
// ─── Toolbar ───────────────────────────────────────────────────────────────
|
|
155
342
|
|
|
156
343
|
private _renderToolbar() {
|
|
344
|
+
if (!this._editor || !this.showToolbar || this._mode !== 'visual') {
|
|
345
|
+
return nothing;
|
|
346
|
+
}
|
|
347
|
+
|
|
157
348
|
return html`
|
|
158
349
|
<div
|
|
159
350
|
class="html-editor-toolbar"
|
|
160
351
|
role="toolbar"
|
|
161
352
|
aria-label="Formatting toolbar"
|
|
162
353
|
>
|
|
163
|
-
${this._toolbarButton('format_bold', 'Bold', 'bold')}
|
|
164
|
-
${this._toolbarButton('format_italic', 'Italic', 'italic')}
|
|
165
|
-
${this._toolbarButton('format_underlined', 'Underline', 'underline')}
|
|
166
354
|
${this._toolbarButton(
|
|
167
|
-
'
|
|
168
|
-
'
|
|
169
|
-
|
|
355
|
+
'undo',
|
|
356
|
+
'Undo',
|
|
357
|
+
() => this._editor?.commands.undo(),
|
|
358
|
+
)}
|
|
359
|
+
${this._toolbarButton(
|
|
360
|
+
'redo',
|
|
361
|
+
'Redo',
|
|
362
|
+
() => this._editor?.commands.redo(),
|
|
170
363
|
)}
|
|
171
364
|
|
|
172
365
|
<span class="toolbar-divider"></span>
|
|
173
366
|
|
|
174
|
-
${this._toolbarButton('format_align_left', 'Align left', 'justifyLeft')}
|
|
175
367
|
${this._toolbarButton(
|
|
176
|
-
'
|
|
177
|
-
'
|
|
178
|
-
|
|
368
|
+
'format_bold',
|
|
369
|
+
'Bold',
|
|
370
|
+
() => this._editor?.chain().focus().toggleBold().run(),
|
|
371
|
+
this._editor.isActive('bold'),
|
|
179
372
|
)}
|
|
180
373
|
${this._toolbarButton(
|
|
181
|
-
'
|
|
182
|
-
'
|
|
183
|
-
|
|
374
|
+
'format_italic',
|
|
375
|
+
'Italic',
|
|
376
|
+
() => this._editor?.chain().focus().toggleItalic().run(),
|
|
377
|
+
this._editor.isActive('italic'),
|
|
378
|
+
)}
|
|
379
|
+
${this._toolbarButton(
|
|
380
|
+
'format_underlined',
|
|
381
|
+
'Underline',
|
|
382
|
+
() => this._editor?.chain().focus().toggleUnderline().run(),
|
|
383
|
+
this._editor.isActive('underline'),
|
|
384
|
+
)}
|
|
385
|
+
${this._toolbarButton(
|
|
386
|
+
'format_strikethrough',
|
|
387
|
+
'Strikethrough',
|
|
388
|
+
() => this._editor?.chain().focus().toggleStrike().run(),
|
|
389
|
+
this._editor.isActive('strike'),
|
|
184
390
|
)}
|
|
185
391
|
|
|
186
392
|
<span class="toolbar-divider"></span>
|
|
@@ -188,44 +394,29 @@ export class HtmlEditor extends BaseInput {
|
|
|
188
394
|
${this._toolbarButton(
|
|
189
395
|
'format_list_bulleted',
|
|
190
396
|
'Unordered list',
|
|
191
|
-
|
|
397
|
+
() => this._editor?.chain().focus().toggleBulletList().run(),
|
|
398
|
+
this._editor.isActive('bulletList'),
|
|
192
399
|
)}
|
|
193
400
|
${this._toolbarButton(
|
|
194
401
|
'format_list_numbered',
|
|
195
402
|
'Ordered list',
|
|
196
|
-
|
|
403
|
+
() => this._editor?.chain().focus().toggleOrderedList().run(),
|
|
404
|
+
this._editor.isActive('orderedList'),
|
|
197
405
|
)}
|
|
198
|
-
|
|
199
|
-
<span class="toolbar-divider"></span>
|
|
200
|
-
|
|
201
|
-
${this._toolbarButton(
|
|
202
|
-
'format_indent_increase',
|
|
203
|
-
'Indent',
|
|
204
|
-
'indent',
|
|
205
|
-
)}
|
|
206
|
-
${this._toolbarButton('format_indent_decrease', 'Outdent', 'outdent')}
|
|
207
|
-
|
|
208
|
-
<span class="toolbar-divider"></span>
|
|
209
|
-
|
|
210
|
-
<button
|
|
211
|
-
class="toolbar-btn"
|
|
212
|
-
title="Insert link"
|
|
213
|
-
aria-label="Insert link"
|
|
214
|
-
?disabled=${this.disabled || this.readonly}
|
|
215
|
-
@mousedown=${(e: Event) => e.preventDefault()}
|
|
216
|
-
@click=${() => this._insertLink()}
|
|
217
|
-
>
|
|
218
|
-
<wc-icon name="link" size="sm"></wc-icon>
|
|
219
|
-
</button>
|
|
220
|
-
|
|
221
|
-
<span class="toolbar-divider"></span>
|
|
222
|
-
|
|
223
|
-
${this._toolbarButton('undo', 'Undo', 'undo')}
|
|
224
|
-
${this._toolbarButton('redo', 'Redo', 'redo')}
|
|
225
406
|
</div>
|
|
226
407
|
`;
|
|
227
408
|
}
|
|
228
409
|
|
|
410
|
+
private _renderReadonlyTag() {
|
|
411
|
+
if (this.disabled) {
|
|
412
|
+
return html`<wc-tag class="read-only-tag" color="red">Disabled</wc-tag>`;
|
|
413
|
+
}
|
|
414
|
+
if (this.readonly) {
|
|
415
|
+
return html`<wc-tag class="read-only-tag" color="red">Read Only</wc-tag>`;
|
|
416
|
+
}
|
|
417
|
+
return nothing;
|
|
418
|
+
}
|
|
419
|
+
|
|
229
420
|
// ─── Render ────────────────────────────────────────────────────────────────
|
|
230
421
|
|
|
231
422
|
render() {
|
|
@@ -251,25 +442,49 @@ export class HtmlEditor extends BaseInput {
|
|
|
251
442
|
readonly: this.readonly,
|
|
252
443
|
})}
|
|
253
444
|
>
|
|
445
|
+
<div class="mode-switcher">
|
|
446
|
+
<wc-segmented-button-group @change=${this._switchMode}>
|
|
447
|
+
<wc-segmented-button
|
|
448
|
+
value="visual"
|
|
449
|
+
?selected=${this._mode === 'visual'}
|
|
450
|
+
?disabled=${this.disabled}
|
|
451
|
+
>
|
|
452
|
+
Visual
|
|
453
|
+
</wc-segmented-button>
|
|
454
|
+
<wc-segmented-button
|
|
455
|
+
value="html"
|
|
456
|
+
?selected=${this._mode === 'html'}
|
|
457
|
+
?disabled=${this.disabled}
|
|
458
|
+
>
|
|
459
|
+
HTML
|
|
460
|
+
</wc-segmented-button>
|
|
461
|
+
</wc-segmented-button-group>
|
|
462
|
+
</div>
|
|
463
|
+
|
|
254
464
|
${this._renderToolbar()}
|
|
255
465
|
|
|
256
466
|
<div
|
|
257
467
|
class=${classMap({
|
|
258
468
|
'html-editor-content': true,
|
|
259
469
|
'is-empty': isEmpty,
|
|
470
|
+
hidden: this._mode !== 'visual',
|
|
260
471
|
})}
|
|
261
|
-
contenteditable=${this.disabled || this.readonly ? 'false' : 'true'}
|
|
262
472
|
data-placeholder=${this.placeholder}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
473
|
+
>
|
|
474
|
+
<div class="tiptap-root"></div>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
<div class=${classMap({ 'html-source': true, hidden: this._mode !== 'html' })}>
|
|
478
|
+
<wc-code-editor
|
|
479
|
+
language="html"
|
|
480
|
+
.value=${this.value}
|
|
481
|
+
?readonly=${this.readonly}
|
|
482
|
+
?disabled=${this.disabled}
|
|
483
|
+
@change=${this._handleSourceChange}
|
|
484
|
+
></wc-code-editor>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
${this._renderReadonlyTag()}
|
|
273
488
|
</wc-field>
|
|
274
489
|
`;
|
|
275
490
|
}
|