@redvars/peacock 3.8.5 → 3.8.6
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/button.js +4 -0
- package/dist/button.js.map +1 -1
- package/dist/custom-elements-jsdocs.json +185 -15
- package/dist/custom-elements.json +886 -177
- package/dist/floating-controller-CZYwnRVu.js +185 -0
- package/dist/floating-controller-CZYwnRVu.js.map +1 -0
- package/dist/{floating-controller-CnUZnOhK.js → floating-ui.dom-oQieRCpS.js} +2 -183
- package/dist/floating-ui.dom-oQieRCpS.js.map +1 -0
- package/dist/html-editor.js +3 -21091
- package/dist/html-editor.js.map +1 -1
- package/dist/index-DcbiqHpf.js +21091 -0
- package/dist/index-DcbiqHpf.js.map +1 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/loader.js +7 -2
- package/dist/loader.js.map +1 -1
- package/dist/markdown-editor.js +4439 -0
- package/dist/markdown-editor.js.map +1 -0
- package/dist/menu.js +2 -1
- package/dist/menu.js.map +1 -1
- package/dist/popover.js +2 -1
- package/dist/popover.js.map +1 -1
- package/dist/{select-D85kpjUt.js → select-CnnPbvpQ.js} +68 -71
- package/dist/select-CnnPbvpQ.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/input/input.d.ts +5 -5
- package/dist/src/markdown-editor/index.d.ts +2 -0
- package/dist/src/markdown-editor/markdown-editor.d.ts +116 -0
- package/dist/sub-menu.js +2 -1
- package/dist/sub-menu.js.map +1 -1
- package/dist/tooltip.js +2 -1
- package/dist/tooltip.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -1
- package/readme.md +2 -2
- package/src/button/button/button-sizes.scss +4 -0
- package/src/index.ts +1 -0
- package/src/input/input.ts +85 -82
- package/src/loader.ts +4 -0
- package/src/markdown-editor/index.ts +3 -0
- package/src/markdown-editor/markdown-editor.scss +378 -0
- package/src/markdown-editor/markdown-editor.ts +994 -0
- package/dist/floating-controller-CnUZnOhK.js.map +0 -1
- package/dist/select-D85kpjUt.js.map +0 -1
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
import { html, nothing } from 'lit';
|
|
2
|
+
import { property, query, state } from 'lit/decorators.js';
|
|
3
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
4
|
+
import { Editor, Extension, Range } from '@tiptap/core';
|
|
5
|
+
import { Plugin, NodeSelection } from '@tiptap/pm/state';
|
|
6
|
+
import { DOMSerializer, Fragment, Slice } from '@tiptap/pm/model';
|
|
7
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
8
|
+
import Underline from '@tiptap/extension-underline';
|
|
9
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
10
|
+
import Suggestion from '@tiptap/suggestion';
|
|
11
|
+
import { computePosition, flip, offset, shift } from '@floating-ui/dom';
|
|
12
|
+
import { marked } from 'marked';
|
|
13
|
+
import TurndownService from 'turndown';
|
|
14
|
+
|
|
15
|
+
import IndividualComponent from '@/IndividualComponent.js';
|
|
16
|
+
import BaseInput from '../input/BaseInput.js';
|
|
17
|
+
import { redispatchEvent } from '../__internal/utils/dispatch-event-utils.js';
|
|
18
|
+
|
|
19
|
+
import styles from './markdown-editor.scss';
|
|
20
|
+
|
|
21
|
+
interface SlashItem {
|
|
22
|
+
title: string;
|
|
23
|
+
hint: string;
|
|
24
|
+
icon: string;
|
|
25
|
+
command: (args: { editor: Editor; range: Range }) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SLASH_ITEMS: SlashItem[] = [
|
|
29
|
+
{
|
|
30
|
+
title: 'Heading 1',
|
|
31
|
+
hint: '#',
|
|
32
|
+
icon: 'title',
|
|
33
|
+
command: ({ editor, range }) =>
|
|
34
|
+
editor
|
|
35
|
+
.chain()
|
|
36
|
+
.focus()
|
|
37
|
+
.deleteRange(range)
|
|
38
|
+
.setNode('heading', { level: 1 })
|
|
39
|
+
.run(),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: 'Heading 2',
|
|
43
|
+
hint: '##',
|
|
44
|
+
icon: 'title',
|
|
45
|
+
command: ({ editor, range }) =>
|
|
46
|
+
editor
|
|
47
|
+
.chain()
|
|
48
|
+
.focus()
|
|
49
|
+
.deleteRange(range)
|
|
50
|
+
.setNode('heading', { level: 2 })
|
|
51
|
+
.run(),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: 'Heading 3',
|
|
55
|
+
hint: '###',
|
|
56
|
+
icon: 'title',
|
|
57
|
+
command: ({ editor, range }) =>
|
|
58
|
+
editor
|
|
59
|
+
.chain()
|
|
60
|
+
.focus()
|
|
61
|
+
.deleteRange(range)
|
|
62
|
+
.setNode('heading', { level: 3 })
|
|
63
|
+
.run(),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: 'Bulleted list',
|
|
67
|
+
hint: '-',
|
|
68
|
+
icon: 'format_list_bulleted',
|
|
69
|
+
command: ({ editor, range }) =>
|
|
70
|
+
editor.chain().focus().deleteRange(range).toggleBulletList().run(),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: 'Numbered list',
|
|
74
|
+
hint: '1.',
|
|
75
|
+
icon: 'format_list_numbered',
|
|
76
|
+
command: ({ editor, range }) =>
|
|
77
|
+
editor.chain().focus().deleteRange(range).toggleOrderedList().run(),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
title: 'Quote',
|
|
81
|
+
hint: '>',
|
|
82
|
+
icon: 'format_quote',
|
|
83
|
+
command: ({ editor, range }) =>
|
|
84
|
+
editor.chain().focus().deleteRange(range).toggleBlockquote().run(),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
title: 'Code block',
|
|
88
|
+
hint: '```',
|
|
89
|
+
icon: 'code',
|
|
90
|
+
command: ({ editor, range }) =>
|
|
91
|
+
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
title: 'Divider',
|
|
95
|
+
hint: '---',
|
|
96
|
+
icon: 'horizontal_rule',
|
|
97
|
+
command: ({ editor, range }) =>
|
|
98
|
+
editor.chain().focus().deleteRange(range).setHorizontalRule().run(),
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @label Markdown Editor
|
|
104
|
+
* @tag wc-markdown-editor
|
|
105
|
+
* @rawTag markdown-editor
|
|
106
|
+
*
|
|
107
|
+
* @summary A Tiptap-powered Notion-like Markdown editor with a bottom toggle
|
|
108
|
+
* between rendered and raw markdown views.
|
|
109
|
+
* @overview
|
|
110
|
+
* <p>The Markdown Editor provides a block-based, Notion-like writing surface
|
|
111
|
+
* built on Tiptap. Type <code>/</code> to open a slash menu for inserting
|
|
112
|
+
* headings, lists, quotes, code blocks, and dividers. Select text to reveal
|
|
113
|
+
* a floating bubble toolbar with inline formatting actions.</p>
|
|
114
|
+
*
|
|
115
|
+
* <p>The canonical <code>value</code> is Markdown. Use the bottom segmented
|
|
116
|
+
* switch to toggle between the rendered editor and the raw markdown source.
|
|
117
|
+
* The component dispatches a <code>change</code> event whenever content
|
|
118
|
+
* changes.</p>
|
|
119
|
+
*
|
|
120
|
+
* @cssprop --markdown-editor-min-height - Minimum height of the editable area. Defaults to 12rem.
|
|
121
|
+
* @cssprop --markdown-editor-bubble-background - Background color of the selection bubble menu.
|
|
122
|
+
* @cssprop --markdown-editor-slash-background - Background color of the slash command menu.
|
|
123
|
+
*
|
|
124
|
+
* @fires {Event} change - Fired whenever the markdown content changes.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```html
|
|
128
|
+
* <wc-markdown-editor
|
|
129
|
+
* label="Notes"
|
|
130
|
+
* value="# Hello\n\nType a slash on a new line to insert blocks."
|
|
131
|
+
* ></wc-markdown-editor>
|
|
132
|
+
* ```
|
|
133
|
+
* @tags input editor
|
|
134
|
+
*/
|
|
135
|
+
@IndividualComponent
|
|
136
|
+
export class MarkdownEditor extends BaseInput {
|
|
137
|
+
// ── Static ────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
static styles = [styles];
|
|
140
|
+
|
|
141
|
+
// ── Properties ────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/** Current Markdown value of the editor. */
|
|
144
|
+
@property({ type: String })
|
|
145
|
+
value = '';
|
|
146
|
+
|
|
147
|
+
/** Label displayed above the editor. */
|
|
148
|
+
@property({ type: String })
|
|
149
|
+
label = '';
|
|
150
|
+
|
|
151
|
+
/** Placeholder text shown when the editor is empty. */
|
|
152
|
+
@property({ type: String })
|
|
153
|
+
placeholder = "Type '/' for commands…";
|
|
154
|
+
|
|
155
|
+
/** Visual style of the wrapping field. */
|
|
156
|
+
@property({ type: String })
|
|
157
|
+
variant: 'filled' | 'outlined' | 'default' = 'default';
|
|
158
|
+
|
|
159
|
+
/** Helper text displayed below the editor. */
|
|
160
|
+
@property({ type: String, attribute: 'helper-text' })
|
|
161
|
+
helperText = '';
|
|
162
|
+
|
|
163
|
+
/** Whether to show an error state. */
|
|
164
|
+
@property({ type: Boolean })
|
|
165
|
+
error = false;
|
|
166
|
+
|
|
167
|
+
/** Error message text. */
|
|
168
|
+
@property({ type: String, attribute: 'error-text' })
|
|
169
|
+
errorText = '';
|
|
170
|
+
|
|
171
|
+
/** Debounce in milliseconds for dispatching `change`. */
|
|
172
|
+
@property({ type: Number })
|
|
173
|
+
debounce = 250;
|
|
174
|
+
|
|
175
|
+
// ── State ─────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/** Whether the editor currently has focus. */
|
|
178
|
+
@state() private _focused = false;
|
|
179
|
+
|
|
180
|
+
/** Current view mode: rendered editor or raw markdown. */
|
|
181
|
+
@state() private _mode: 'editor' | 'markdown' = 'editor';
|
|
182
|
+
|
|
183
|
+
/** Whether the bubble toolbar is visible. */
|
|
184
|
+
@state() private _bubbleOpen = false;
|
|
185
|
+
|
|
186
|
+
/** Whether the slash command menu is visible. */
|
|
187
|
+
@state() private _slashOpen = false;
|
|
188
|
+
|
|
189
|
+
/** Filtered slash menu items. */
|
|
190
|
+
@state() private _slashItems: SlashItem[] = [];
|
|
191
|
+
|
|
192
|
+
/** Currently highlighted slash menu index. */
|
|
193
|
+
@state() private _slashIndex = 0;
|
|
194
|
+
|
|
195
|
+
/** Whether the block handle panel is visible. */
|
|
196
|
+
@state() private _blockHandleVisible = false;
|
|
197
|
+
|
|
198
|
+
// ── Queries ───────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
@query('.tiptap-root')
|
|
201
|
+
private _editorEl!: HTMLDivElement;
|
|
202
|
+
|
|
203
|
+
@query('.bubble-menu')
|
|
204
|
+
private _bubbleEl?: HTMLDivElement;
|
|
205
|
+
|
|
206
|
+
@query('.slash-menu')
|
|
207
|
+
private _slashEl?: HTMLDivElement;
|
|
208
|
+
|
|
209
|
+
@query('.block-handle')
|
|
210
|
+
private _blockHandleEl?: HTMLDivElement;
|
|
211
|
+
|
|
212
|
+
// ── Private fields ────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/** TipTap editor instance. */
|
|
215
|
+
private _editor?: Editor;
|
|
216
|
+
|
|
217
|
+
/** Debounce timer for the `change` event. */
|
|
218
|
+
private _changeTimeout?: number;
|
|
219
|
+
|
|
220
|
+
/** Most recent slash-suggestion bounding rect (for popup placement). */
|
|
221
|
+
private _slashClientRect: (() => DOMRect | null) | null = null;
|
|
222
|
+
|
|
223
|
+
/** Slash-suggestion command callback to invoke on selection. */
|
|
224
|
+
private _slashCommand?: (item: SlashItem) => void;
|
|
225
|
+
|
|
226
|
+
/** ProseMirror doc position (inside-block) of the block under the handle. */
|
|
227
|
+
private _blockHandleNodePos = -1;
|
|
228
|
+
|
|
229
|
+
/** Timer id for delayed hide of the block handle. */
|
|
230
|
+
private _blockHandleHideTimeout?: number;
|
|
231
|
+
|
|
232
|
+
/** Reusable Turndown serializer instance. */
|
|
233
|
+
private _turndown = new TurndownService({
|
|
234
|
+
headingStyle: 'atx',
|
|
235
|
+
codeBlockStyle: 'fenced',
|
|
236
|
+
bulletListMarker: '-',
|
|
237
|
+
emDelimiter: '_',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
protected firstUpdated() {
|
|
243
|
+
this._initializeEditor();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
disconnectedCallback() {
|
|
247
|
+
super.disconnectedCallback();
|
|
248
|
+
if (this._changeTimeout) {
|
|
249
|
+
window.clearTimeout(this._changeTimeout);
|
|
250
|
+
this._changeTimeout = undefined;
|
|
251
|
+
}
|
|
252
|
+
if (this._blockHandleHideTimeout) {
|
|
253
|
+
window.clearTimeout(this._blockHandleHideTimeout);
|
|
254
|
+
this._blockHandleHideTimeout = undefined;
|
|
255
|
+
}
|
|
256
|
+
this._destroyEditor();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
protected updated(changed: Map<string, unknown>) {
|
|
260
|
+
if (changed.has('value') && this._editor) {
|
|
261
|
+
const currentMd = MarkdownEditor._normalizeMd(
|
|
262
|
+
this._htmlToMarkdown(this._editor.getHTML()),
|
|
263
|
+
);
|
|
264
|
+
const nextMd = MarkdownEditor._normalizeMd(this.value);
|
|
265
|
+
if (currentMd !== nextMd) {
|
|
266
|
+
this._editor.commands.setContent(
|
|
267
|
+
this._markdownToHtml(this.value),
|
|
268
|
+
false,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if ((changed.has('disabled') || changed.has('readonly')) && this._editor) {
|
|
274
|
+
this._editor.setEditable(!(this.disabled || this.readonly));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (changed.has('placeholder') && this._editor) {
|
|
278
|
+
this._destroyEditor();
|
|
279
|
+
this._initializeEditor();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Private methods: conversion ───────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
private static _normalizeMd(value: string) {
|
|
286
|
+
return (value ?? '').replace(/\r\n/g, '\n').replace(/\s+$/g, '');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private _markdownToHtml(md: string): string {
|
|
290
|
+
return marked.parse(md ?? '', {
|
|
291
|
+
gfm: true,
|
|
292
|
+
breaks: true,
|
|
293
|
+
async: false,
|
|
294
|
+
}) as string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private _htmlToMarkdown(htmlStr: string): string {
|
|
298
|
+
return this._turndown.turndown(htmlStr ?? '');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Private methods: TipTap setup ─────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
private _destroyEditor() {
|
|
304
|
+
if (!this._editor) return;
|
|
305
|
+
this._editorEl?.removeEventListener(
|
|
306
|
+
'click',
|
|
307
|
+
this._focusEditorOnContainerClick,
|
|
308
|
+
);
|
|
309
|
+
this._editor.destroy();
|
|
310
|
+
this._editor = undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _initializeEditor() {
|
|
314
|
+
if (!this._editorEl || this._editor) return;
|
|
315
|
+
|
|
316
|
+
this._editor = new Editor({
|
|
317
|
+
element: this._editorEl,
|
|
318
|
+
extensions: [
|
|
319
|
+
StarterKit,
|
|
320
|
+
Underline,
|
|
321
|
+
Placeholder.configure({
|
|
322
|
+
placeholder: this.placeholder,
|
|
323
|
+
}),
|
|
324
|
+
this._buildSlashExtension(),
|
|
325
|
+
this._buildBlockHandleExtension(),
|
|
326
|
+
],
|
|
327
|
+
content: this._markdownToHtml(this.value),
|
|
328
|
+
editable: !(this.disabled || this.readonly),
|
|
329
|
+
onFocus: () => {
|
|
330
|
+
this._focused = true;
|
|
331
|
+
},
|
|
332
|
+
onBlur: () => {
|
|
333
|
+
this._focused = false;
|
|
334
|
+
window.setTimeout(() => {
|
|
335
|
+
this._bubbleOpen = false;
|
|
336
|
+
}, 150);
|
|
337
|
+
},
|
|
338
|
+
onUpdate: () => {
|
|
339
|
+
if (!this._editor) return;
|
|
340
|
+
|
|
341
|
+
const nextMd = MarkdownEditor._normalizeMd(
|
|
342
|
+
this._htmlToMarkdown(this._editor.getHTML()),
|
|
343
|
+
);
|
|
344
|
+
if (nextMd !== this.value) {
|
|
345
|
+
this.value = nextMd;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
this._dispatchDebouncedChange();
|
|
349
|
+
},
|
|
350
|
+
onSelectionUpdate: () => {
|
|
351
|
+
this._updateBubbleMenu();
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
this._editorEl.addEventListener('click', this._focusEditorOnContainerClick);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private _focusEditorOnContainerClick = (event: Event) => {
|
|
359
|
+
if (!this._editor) return;
|
|
360
|
+
if (event.target === this._editorEl) {
|
|
361
|
+
this._editor.commands.focus('end');
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// ── Private methods: slash command ────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
private _buildSlashExtension() {
|
|
368
|
+
const host = this;
|
|
369
|
+
|
|
370
|
+
return Extension.create({
|
|
371
|
+
name: 'slashCommand',
|
|
372
|
+
|
|
373
|
+
addProseMirrorPlugins() {
|
|
374
|
+
return [
|
|
375
|
+
Suggestion<SlashItem>({
|
|
376
|
+
editor: this.editor,
|
|
377
|
+
char: '/',
|
|
378
|
+
startOfLine: false,
|
|
379
|
+
allowSpaces: false,
|
|
380
|
+
items: ({ query: slashQuery }) => {
|
|
381
|
+
const q = slashQuery.toLowerCase();
|
|
382
|
+
return SLASH_ITEMS.filter(item =>
|
|
383
|
+
item.title.toLowerCase().includes(q),
|
|
384
|
+
).slice(0, 10);
|
|
385
|
+
},
|
|
386
|
+
command: ({ editor, range, props }) => {
|
|
387
|
+
props.command({ editor, range });
|
|
388
|
+
},
|
|
389
|
+
render: () => ({
|
|
390
|
+
onStart: props => {
|
|
391
|
+
host._slashItems = props.items;
|
|
392
|
+
host._slashIndex = 0;
|
|
393
|
+
host._slashClientRect =
|
|
394
|
+
props.clientRect as () => DOMRect | null;
|
|
395
|
+
host._slashCommand = item => {
|
|
396
|
+
props.command({
|
|
397
|
+
...item,
|
|
398
|
+
command: item.command,
|
|
399
|
+
} as unknown as SlashItem);
|
|
400
|
+
};
|
|
401
|
+
host._slashOpen = props.items.length > 0;
|
|
402
|
+
host._positionSlashMenu();
|
|
403
|
+
},
|
|
404
|
+
onUpdate: props => {
|
|
405
|
+
host._slashItems = props.items;
|
|
406
|
+
host._slashIndex = 0;
|
|
407
|
+
host._slashClientRect =
|
|
408
|
+
props.clientRect as () => DOMRect | null;
|
|
409
|
+
host._slashOpen = props.items.length > 0;
|
|
410
|
+
host._positionSlashMenu();
|
|
411
|
+
},
|
|
412
|
+
onKeyDown: ({ event }) => {
|
|
413
|
+
if (!host._slashOpen) return false;
|
|
414
|
+
|
|
415
|
+
if (event.key === 'ArrowDown') {
|
|
416
|
+
host._slashIndex =
|
|
417
|
+
(host._slashIndex + 1) % host._slashItems.length;
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
if (event.key === 'ArrowUp') {
|
|
421
|
+
host._slashIndex =
|
|
422
|
+
(host._slashIndex - 1 + host._slashItems.length) %
|
|
423
|
+
host._slashItems.length;
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
if (event.key === 'Enter') {
|
|
427
|
+
const item = host._slashItems[host._slashIndex];
|
|
428
|
+
if (item && host._slashCommand) {
|
|
429
|
+
host._slashCommand(item);
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (event.key === 'Escape') {
|
|
434
|
+
host._slashOpen = false;
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
},
|
|
439
|
+
onExit: () => {
|
|
440
|
+
host._slashOpen = false;
|
|
441
|
+
host._slashClientRect = null;
|
|
442
|
+
host._slashCommand = undefined;
|
|
443
|
+
},
|
|
444
|
+
}),
|
|
445
|
+
}),
|
|
446
|
+
];
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private async _positionSlashMenu() {
|
|
452
|
+
await this.updateComplete;
|
|
453
|
+
|
|
454
|
+
if (!this._slashEl || !this._slashClientRect) return;
|
|
455
|
+
const rect = this._slashClientRect();
|
|
456
|
+
if (!rect) return;
|
|
457
|
+
|
|
458
|
+
const virtual = {
|
|
459
|
+
getBoundingClientRect: () => rect,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const { x, y } = await computePosition(virtual, this._slashEl, {
|
|
463
|
+
placement: 'bottom-start',
|
|
464
|
+
strategy: 'fixed',
|
|
465
|
+
middleware: [offset(6), flip(), shift({ padding: 8 })],
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
this._slashEl.style.left = `${x}px`;
|
|
469
|
+
this._slashEl.style.top = `${y}px`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private _onSlashItemClick = (event: MouseEvent) => {
|
|
473
|
+
event.preventDefault();
|
|
474
|
+
const target = (event.currentTarget as HTMLElement) ?? null;
|
|
475
|
+
if (!target) return;
|
|
476
|
+
const index = Number(target.dataset.index ?? '-1');
|
|
477
|
+
const item = this._slashItems[index];
|
|
478
|
+
if (item && this._slashCommand) {
|
|
479
|
+
this._slashCommand(item);
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// ── Private methods: block handle ─────────────────────────────────────────
|
|
484
|
+
|
|
485
|
+
private _buildBlockHandleExtension() {
|
|
486
|
+
const host = this;
|
|
487
|
+
|
|
488
|
+
return Extension.create({
|
|
489
|
+
name: 'blockHandle',
|
|
490
|
+
|
|
491
|
+
addProseMirrorPlugins() {
|
|
492
|
+
return [
|
|
493
|
+
new Plugin({
|
|
494
|
+
props: {
|
|
495
|
+
handleDOMEvents: {
|
|
496
|
+
mousemove: (view, event) => {
|
|
497
|
+
if (host.disabled || host.readonly || host._mode !== 'editor') {
|
|
498
|
+
host._blockHandleVisible = false;
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
const coords = view.posAtCoords({
|
|
502
|
+
left: event.clientX,
|
|
503
|
+
top: event.clientY,
|
|
504
|
+
});
|
|
505
|
+
if (!coords) {
|
|
506
|
+
host._blockHandleVisible = false;
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
// Store the inside-block position (not before(1)) so that
|
|
511
|
+
// domAtPos() resolves to the block element, not its parent.
|
|
512
|
+
host._blockHandleNodePos = coords.pos;
|
|
513
|
+
window.clearTimeout(host._blockHandleHideTimeout);
|
|
514
|
+
host._blockHandleHideTimeout = undefined;
|
|
515
|
+
host._blockHandleVisible = true;
|
|
516
|
+
host._positionBlockHandle();
|
|
517
|
+
} catch {
|
|
518
|
+
host._blockHandleVisible = false;
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
},
|
|
522
|
+
mouseleave: (_view, event) => {
|
|
523
|
+
const rel = (event as MouseEvent).relatedTarget as Node | null;
|
|
524
|
+
if (
|
|
525
|
+
host._blockHandleEl?.contains(rel) ||
|
|
526
|
+
rel === host._blockHandleEl
|
|
527
|
+
)
|
|
528
|
+
return false;
|
|
529
|
+
// Delay so the mouse can travel to the handle without it vanishing.
|
|
530
|
+
window.clearTimeout(host._blockHandleHideTimeout);
|
|
531
|
+
host._blockHandleHideTimeout = window.setTimeout(() => {
|
|
532
|
+
host._blockHandleVisible = false;
|
|
533
|
+
}, 400);
|
|
534
|
+
return false;
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
}),
|
|
539
|
+
];
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private _positionBlockHandle() {
|
|
545
|
+
if (!this._editor || !this._blockHandleEl) return;
|
|
546
|
+
const { view } = this._editor;
|
|
547
|
+
const pos = this._blockHandleNodePos;
|
|
548
|
+
if (pos < 0) return;
|
|
549
|
+
try {
|
|
550
|
+
const domRef = view.domAtPos(pos);
|
|
551
|
+
let el: Element | null =
|
|
552
|
+
domRef.node.nodeType === Node.TEXT_NODE
|
|
553
|
+
? (domRef.node as Text).parentElement
|
|
554
|
+
: (domRef.node as Element);
|
|
555
|
+
while (el && el !== view.dom) {
|
|
556
|
+
const d = getComputedStyle(el).display;
|
|
557
|
+
if (d === 'block' || d === 'list-item') break;
|
|
558
|
+
el = el.parentElement;
|
|
559
|
+
}
|
|
560
|
+
if (!el || el === view.dom) {
|
|
561
|
+
this._blockHandleVisible = false;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const blockRect = el.getBoundingClientRect();
|
|
565
|
+
// Use .markdown-editor-content (grandparent of .ProseMirror) as the
|
|
566
|
+
// left anchor so the handle sits inside the left padding area and is
|
|
567
|
+
// never clipped by a sidebar or page margin.
|
|
568
|
+
const contentEl = view.dom.parentElement?.parentElement;
|
|
569
|
+
const contentLeft = contentEl
|
|
570
|
+
? contentEl.getBoundingClientRect().left
|
|
571
|
+
: view.dom.getBoundingClientRect().left - 16;
|
|
572
|
+
const handleH = this._blockHandleEl.offsetHeight || 60;
|
|
573
|
+
const top = blockRect.top + blockRect.height / 2 - handleH / 2;
|
|
574
|
+
const left = contentLeft + 4;
|
|
575
|
+
this._blockHandleEl.style.top = `${top}px`;
|
|
576
|
+
this._blockHandleEl.style.left = `${left}px`;
|
|
577
|
+
} catch {
|
|
578
|
+
this._blockHandleVisible = false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private _onBlockHandleMouseEnter = () => {
|
|
583
|
+
window.clearTimeout(this._blockHandleHideTimeout);
|
|
584
|
+
this._blockHandleHideTimeout = undefined;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
private _onBlockHandleMouseLeave = (event: MouseEvent) => {
|
|
588
|
+
const rel = event.relatedTarget as Node | null;
|
|
589
|
+
if (this._editorEl?.contains(rel)) return;
|
|
590
|
+
window.clearTimeout(this._blockHandleHideTimeout);
|
|
591
|
+
this._blockHandleHideTimeout = window.setTimeout(() => {
|
|
592
|
+
this._blockHandleVisible = false;
|
|
593
|
+
}, 150);
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
private _onInsertBlockClick = () => {
|
|
597
|
+
if (!this._editor || this.disabled || this.readonly) return;
|
|
598
|
+
const { view } = this._editor;
|
|
599
|
+
const { state } = view;
|
|
600
|
+
try {
|
|
601
|
+
const $pos = state.doc.resolve(this._blockHandleNodePos);
|
|
602
|
+
const nodeEnd = $pos.after(1);
|
|
603
|
+
const safePos = Math.min(nodeEnd - 1, state.doc.content.size - 1);
|
|
604
|
+
const coords = view.coordsAtPos(safePos);
|
|
605
|
+
const rect = new DOMRect(coords.left, coords.bottom, 0, 0);
|
|
606
|
+
|
|
607
|
+
this._slashItems = [...SLASH_ITEMS];
|
|
608
|
+
this._slashIndex = 0;
|
|
609
|
+
this._slashClientRect = () => rect;
|
|
610
|
+
this._slashCommand = item => {
|
|
611
|
+
if (!this._editor) return;
|
|
612
|
+
const end = nodeEnd;
|
|
613
|
+
this._editor
|
|
614
|
+
.chain()
|
|
615
|
+
.focus()
|
|
616
|
+
.insertContentAt(end, { type: 'paragraph' })
|
|
617
|
+
.setTextSelection(end + 1)
|
|
618
|
+
.run();
|
|
619
|
+
const { from } = this._editor.state.selection;
|
|
620
|
+
item.command({ editor: this._editor, range: { from, to: from } });
|
|
621
|
+
this._slashOpen = false;
|
|
622
|
+
this._slashClientRect = null;
|
|
623
|
+
this._slashCommand = undefined;
|
|
624
|
+
};
|
|
625
|
+
this._slashOpen = true;
|
|
626
|
+
this._positionSlashMenu();
|
|
627
|
+
} catch { /* ignore */ }
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
private _onDragHandleMouseDown = (event: MouseEvent) => {
|
|
631
|
+
event.preventDefault();
|
|
632
|
+
if (!this._editor) return;
|
|
633
|
+
const { view } = this._editor;
|
|
634
|
+
const { state } = view;
|
|
635
|
+
try {
|
|
636
|
+
const $pos = state.doc.resolve(this._blockHandleNodePos);
|
|
637
|
+
const nodePos = $pos.depth > 0 ? $pos.before(1) : this._blockHandleNodePos;
|
|
638
|
+
const sel = NodeSelection.create(state.doc, nodePos);
|
|
639
|
+
view.dispatch(state.tr.setSelection(sel));
|
|
640
|
+
} catch { /* ignore */ }
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
private _onDragHandleDragStart = (event: DragEvent) => {
|
|
644
|
+
if (!this._editor || !event.dataTransfer) return;
|
|
645
|
+
const { view } = this._editor;
|
|
646
|
+
const { state } = view;
|
|
647
|
+
try {
|
|
648
|
+
const $pos = state.doc.resolve(this._blockHandleNodePos);
|
|
649
|
+
const nodePos = $pos.depth > 0 ? $pos.before(1) : this._blockHandleNodePos;
|
|
650
|
+
const node = state.doc.nodeAt(nodePos);
|
|
651
|
+
if (!node) return;
|
|
652
|
+
const slice = new Slice(Fragment.from(node), 0, 0);
|
|
653
|
+
const container = document.createElement('div');
|
|
654
|
+
container.appendChild(
|
|
655
|
+
DOMSerializer.fromSchema(state.schema).serializeFragment(slice.content),
|
|
656
|
+
);
|
|
657
|
+
event.dataTransfer.clearData();
|
|
658
|
+
event.dataTransfer.setData('text/html', container.innerHTML);
|
|
659
|
+
event.dataTransfer.setData('text/plain', container.textContent ?? '');
|
|
660
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
661
|
+
view.dragging = { slice, move: true };
|
|
662
|
+
} catch { /* ignore */ }
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
private _onDragHandleDragEnd = () => {
|
|
666
|
+
if (this._editor) this._editor.view.dragging = null;
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// ── Private methods: bubble menu ──────────────────────────────────────────
|
|
670
|
+
|
|
671
|
+
private async _updateBubbleMenu() {
|
|
672
|
+
if (!this._editor || this.disabled || this.readonly) {
|
|
673
|
+
this._bubbleOpen = false;
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (this._mode !== 'editor') {
|
|
678
|
+
this._bubbleOpen = false;
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const { from, to, empty } = this._editor.state.selection;
|
|
683
|
+
if (empty || from === to) {
|
|
684
|
+
this._bubbleOpen = false;
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
this._bubbleOpen = true;
|
|
689
|
+
await this.updateComplete;
|
|
690
|
+
|
|
691
|
+
if (!this._bubbleEl) return;
|
|
692
|
+
|
|
693
|
+
const { view } = this._editor;
|
|
694
|
+
const start = view.coordsAtPos(from);
|
|
695
|
+
const end = view.coordsAtPos(to);
|
|
696
|
+
const rect: DOMRect = {
|
|
697
|
+
x: Math.min(start.left, end.left),
|
|
698
|
+
y: start.top,
|
|
699
|
+
left: Math.min(start.left, end.left),
|
|
700
|
+
top: start.top,
|
|
701
|
+
right: Math.max(start.right, end.right),
|
|
702
|
+
bottom: end.bottom,
|
|
703
|
+
width: Math.max(start.right, end.right) - Math.min(start.left, end.left),
|
|
704
|
+
height: end.bottom - start.top,
|
|
705
|
+
toJSON: () => ({}),
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const virtual = {
|
|
709
|
+
getBoundingClientRect: () => rect,
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
const { x, y } = await computePosition(virtual, this._bubbleEl, {
|
|
713
|
+
placement: 'top',
|
|
714
|
+
strategy: 'fixed',
|
|
715
|
+
middleware: [offset(8), flip(), shift({ padding: 8 })],
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
this._bubbleEl.style.left = `${x}px`;
|
|
719
|
+
this._bubbleEl.style.top = `${y}px`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ── Private methods: events / mode ────────────────────────────────────────
|
|
723
|
+
|
|
724
|
+
private _dispatchDebouncedChange() {
|
|
725
|
+
if (this._changeTimeout) {
|
|
726
|
+
window.clearTimeout(this._changeTimeout);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
this._changeTimeout = window.setTimeout(() => {
|
|
730
|
+
redispatchEvent(
|
|
731
|
+
this,
|
|
732
|
+
new Event('change', { bubbles: true, composed: true }),
|
|
733
|
+
);
|
|
734
|
+
}, this.debounce);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private _execCommand(command: () => void) {
|
|
738
|
+
if (this.disabled || this.readonly || !this._editor) return;
|
|
739
|
+
command();
|
|
740
|
+
this._editor.commands.focus();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
private _switchMode = (event: CustomEvent<{ value: string }>) => {
|
|
744
|
+
event.stopPropagation();
|
|
745
|
+
const nextMode = event.detail?.value === 'markdown' ? 'markdown' : 'editor';
|
|
746
|
+
if (nextMode === this._mode) return;
|
|
747
|
+
|
|
748
|
+
if (nextMode === 'markdown' && this._editor) {
|
|
749
|
+
const nextMd = MarkdownEditor._normalizeMd(
|
|
750
|
+
this._htmlToMarkdown(this._editor.getHTML()),
|
|
751
|
+
);
|
|
752
|
+
if (nextMd !== this.value) {
|
|
753
|
+
this.value = nextMd;
|
|
754
|
+
}
|
|
755
|
+
this._bubbleOpen = false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
this._mode = nextMode;
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
private _handleSourceChange = (event: Event) => {
|
|
762
|
+
event.stopPropagation();
|
|
763
|
+
const target = event.currentTarget as { value?: string };
|
|
764
|
+
const nextValue = target.value ?? '';
|
|
765
|
+
if (nextValue === this.value) return;
|
|
766
|
+
|
|
767
|
+
this.value = nextValue;
|
|
768
|
+
this._dispatchDebouncedChange();
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// ── Render helpers ────────────────────────────────────────────────────────
|
|
772
|
+
|
|
773
|
+
private _renderToolbarButton(
|
|
774
|
+
icon: string,
|
|
775
|
+
title: string,
|
|
776
|
+
action: () => void,
|
|
777
|
+
active = false,
|
|
778
|
+
) {
|
|
779
|
+
return html`
|
|
780
|
+
<button
|
|
781
|
+
class=${classMap({ 'toolbar-btn': true, active })}
|
|
782
|
+
title=${title}
|
|
783
|
+
aria-label=${title}
|
|
784
|
+
?disabled=${this.disabled || this.readonly}
|
|
785
|
+
@mousedown=${(e: Event) => e.preventDefault()}
|
|
786
|
+
@click=${(e: Event) => {
|
|
787
|
+
e.preventDefault();
|
|
788
|
+
this._execCommand(action);
|
|
789
|
+
}}
|
|
790
|
+
>
|
|
791
|
+
<wc-icon name=${icon} size="sm"></wc-icon>
|
|
792
|
+
</button>
|
|
793
|
+
`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private _renderBubbleMenu() {
|
|
797
|
+
if (!this._editor) return nothing;
|
|
798
|
+
|
|
799
|
+
return html`
|
|
800
|
+
<div
|
|
801
|
+
class=${classMap({ 'bubble-menu': true, open: this._bubbleOpen })}
|
|
802
|
+
role="toolbar"
|
|
803
|
+
aria-label="Inline formatting"
|
|
804
|
+
>
|
|
805
|
+
${this._renderToolbarButton(
|
|
806
|
+
'format_bold',
|
|
807
|
+
'Bold',
|
|
808
|
+
() => this._editor?.chain().focus().toggleBold().run(),
|
|
809
|
+
this._editor.isActive('bold'),
|
|
810
|
+
)}
|
|
811
|
+
${this._renderToolbarButton(
|
|
812
|
+
'format_italic',
|
|
813
|
+
'Italic',
|
|
814
|
+
() => this._editor?.chain().focus().toggleItalic().run(),
|
|
815
|
+
this._editor.isActive('italic'),
|
|
816
|
+
)}
|
|
817
|
+
${this._renderToolbarButton(
|
|
818
|
+
'format_underlined',
|
|
819
|
+
'Underline',
|
|
820
|
+
() => this._editor?.chain().focus().toggleUnderline().run(),
|
|
821
|
+
this._editor.isActive('underline'),
|
|
822
|
+
)}
|
|
823
|
+
${this._renderToolbarButton(
|
|
824
|
+
'format_strikethrough',
|
|
825
|
+
'Strikethrough',
|
|
826
|
+
() => this._editor?.chain().focus().toggleStrike().run(),
|
|
827
|
+
this._editor.isActive('strike'),
|
|
828
|
+
)}
|
|
829
|
+
${this._renderToolbarButton(
|
|
830
|
+
'code',
|
|
831
|
+
'Inline code',
|
|
832
|
+
() => this._editor?.chain().focus().toggleCode().run(),
|
|
833
|
+
this._editor.isActive('code'),
|
|
834
|
+
)}
|
|
835
|
+
</div>
|
|
836
|
+
`;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private _renderSlashMenu() {
|
|
840
|
+
return html`
|
|
841
|
+
<div
|
|
842
|
+
class=${classMap({ 'slash-menu': true, open: this._slashOpen })}
|
|
843
|
+
role="listbox"
|
|
844
|
+
aria-label="Insert block"
|
|
845
|
+
>
|
|
846
|
+
${this._slashItems.map(
|
|
847
|
+
(item, index) => html`
|
|
848
|
+
<button
|
|
849
|
+
type="button"
|
|
850
|
+
class=${classMap({
|
|
851
|
+
'slash-item': true,
|
|
852
|
+
active: index === this._slashIndex,
|
|
853
|
+
})}
|
|
854
|
+
role="option"
|
|
855
|
+
aria-selected=${index === this._slashIndex}
|
|
856
|
+
data-index=${index}
|
|
857
|
+
@mousedown=${(e: Event) => e.preventDefault()}
|
|
858
|
+
@click=${this._onSlashItemClick}
|
|
859
|
+
>
|
|
860
|
+
<wc-icon name=${item.icon} size="sm"></wc-icon>
|
|
861
|
+
<span class="slash-item-label">${item.title}</span>
|
|
862
|
+
<span class="slash-item-hint">${item.hint}</span>
|
|
863
|
+
</button>
|
|
864
|
+
`,
|
|
865
|
+
)}
|
|
866
|
+
</div>
|
|
867
|
+
`;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private _renderBlockHandle() {
|
|
871
|
+
if (this._mode !== 'editor') return nothing;
|
|
872
|
+
return html`
|
|
873
|
+
<div
|
|
874
|
+
class=${classMap({
|
|
875
|
+
'block-handle': true,
|
|
876
|
+
visible: this._blockHandleVisible,
|
|
877
|
+
})}
|
|
878
|
+
@mouseenter=${this._onBlockHandleMouseEnter}
|
|
879
|
+
@mouseleave=${this._onBlockHandleMouseLeave}
|
|
880
|
+
>
|
|
881
|
+
<button
|
|
882
|
+
class="block-handle-btn drag-btn"
|
|
883
|
+
title="Drag to reorder"
|
|
884
|
+
aria-label="Drag to reorder"
|
|
885
|
+
draggable="true"
|
|
886
|
+
?disabled=${this.disabled || this.readonly}
|
|
887
|
+
@mousedown=${this._onDragHandleMouseDown}
|
|
888
|
+
@dragstart=${this._onDragHandleDragStart}
|
|
889
|
+
@dragend=${this._onDragHandleDragEnd}
|
|
890
|
+
>
|
|
891
|
+
<wc-icon name="drag_indicator" size="sm"></wc-icon>
|
|
892
|
+
</button>
|
|
893
|
+
<button
|
|
894
|
+
class="block-handle-btn insert-btn"
|
|
895
|
+
title="Insert block below"
|
|
896
|
+
aria-label="Insert block below"
|
|
897
|
+
?disabled=${this.disabled || this.readonly}
|
|
898
|
+
@mousedown=${(e: Event) => e.preventDefault()}
|
|
899
|
+
@click=${this._onInsertBlockClick}
|
|
900
|
+
>
|
|
901
|
+
<wc-icon name="add" size="sm"></wc-icon>
|
|
902
|
+
</button>
|
|
903
|
+
</div>
|
|
904
|
+
`;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private _renderReadonlyTag() {
|
|
908
|
+
if (this.disabled) {
|
|
909
|
+
return html`<wc-tag class="read-only-tag" color="red">Disabled</wc-tag>`;
|
|
910
|
+
}
|
|
911
|
+
if (this.readonly) {
|
|
912
|
+
return html`<wc-tag class="read-only-tag" color="red">Read Only</wc-tag>`;
|
|
913
|
+
}
|
|
914
|
+
return nothing;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ── Render ────────────────────────────────────────────────────────────────
|
|
918
|
+
|
|
919
|
+
render() {
|
|
920
|
+
const isEmpty = !this.value || this.value.trim() === '';
|
|
921
|
+
|
|
922
|
+
return html`
|
|
923
|
+
<wc-field
|
|
924
|
+
label=${this.label}
|
|
925
|
+
?required=${this.required}
|
|
926
|
+
?disabled=${this.disabled}
|
|
927
|
+
?readonly=${this.readonly}
|
|
928
|
+
?skeleton=${this.skeleton}
|
|
929
|
+
?focused=${this._focused}
|
|
930
|
+
?error=${this.error}
|
|
931
|
+
error-text=${this.errorText}
|
|
932
|
+
helper-text=${this.helperText}
|
|
933
|
+
variant=${this.variant}
|
|
934
|
+
?populated=${!isEmpty}
|
|
935
|
+
.host=${this}
|
|
936
|
+
class=${classMap({
|
|
937
|
+
'markdown-editor-field': true,
|
|
938
|
+
disabled: this.disabled,
|
|
939
|
+
readonly: this.readonly,
|
|
940
|
+
})}
|
|
941
|
+
>
|
|
942
|
+
<div
|
|
943
|
+
class=${classMap({
|
|
944
|
+
'markdown-editor-content': true,
|
|
945
|
+
'is-empty': isEmpty,
|
|
946
|
+
hidden: this._mode !== 'editor',
|
|
947
|
+
})}
|
|
948
|
+
data-placeholder=${this.placeholder}
|
|
949
|
+
>
|
|
950
|
+
<div class="tiptap-root"></div>
|
|
951
|
+
</div>
|
|
952
|
+
|
|
953
|
+
<div
|
|
954
|
+
class=${classMap({
|
|
955
|
+
'markdown-source': true,
|
|
956
|
+
hidden: this._mode !== 'markdown',
|
|
957
|
+
})}
|
|
958
|
+
>
|
|
959
|
+
<wc-code-editor
|
|
960
|
+
language="markdown"
|
|
961
|
+
.value=${this.value}
|
|
962
|
+
?readonly=${this.readonly}
|
|
963
|
+
?disabled=${this.disabled}
|
|
964
|
+
@change=${this._handleSourceChange}
|
|
965
|
+
></wc-code-editor>
|
|
966
|
+
</div>
|
|
967
|
+
|
|
968
|
+
<div class="mode-switcher">
|
|
969
|
+
<wc-segmented-button-group @change=${this._switchMode}>
|
|
970
|
+
<wc-segmented-button
|
|
971
|
+
value="editor"
|
|
972
|
+
?selected=${this._mode === 'editor'}
|
|
973
|
+
?disabled=${this.disabled}
|
|
974
|
+
>
|
|
975
|
+
Editor
|
|
976
|
+
</wc-segmented-button>
|
|
977
|
+
<wc-segmented-button
|
|
978
|
+
value="markdown"
|
|
979
|
+
?selected=${this._mode === 'markdown'}
|
|
980
|
+
?disabled=${this.disabled}
|
|
981
|
+
>
|
|
982
|
+
Markdown
|
|
983
|
+
</wc-segmented-button>
|
|
984
|
+
</wc-segmented-button-group>
|
|
985
|
+
</div>
|
|
986
|
+
|
|
987
|
+
${this._renderReadonlyTag()}
|
|
988
|
+
</wc-field>
|
|
989
|
+
|
|
990
|
+
${this._renderBubbleMenu()} ${this._renderSlashMenu()}
|
|
991
|
+
${this._renderBlockHandle()}
|
|
992
|
+
`;
|
|
993
|
+
}
|
|
994
|
+
}
|