kempo-ui 0.4.5 → 0.4.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/components/Chat.js +201 -0
- package/dist/components/CodeEditor.js +21 -2
- package/dist/components/Dropdown.js +1 -1
- package/dist/components/HtmlEditor.js +42 -3
- package/dist/components/MarkdownEditor.js +173 -0
- package/dist/components/Resize.js +12 -2
- package/dist/components/markdownEditorControls/Bold.js +11 -0
- package/dist/components/markdownEditorControls/BulletList.js +11 -0
- package/dist/components/markdownEditorControls/Code.js +11 -0
- package/dist/components/markdownEditorControls/FormatBlock.js +30 -0
- package/dist/components/markdownEditorControls/Heading.js +11 -0
- package/dist/components/markdownEditorControls/Image.js +93 -0
- package/dist/components/markdownEditorControls/Italic.js +11 -0
- package/dist/components/markdownEditorControls/Link.js +11 -0
- package/dist/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
- package/dist/components/markdownEditorControls/Menu.js +30 -0
- package/dist/components/markdownEditorControls/NumberedList.js +11 -0
- package/dist/components/markdownEditorControls/Quote.js +11 -0
- package/dist/components/markdownEditorControls/SpeechToText.js +16 -0
- package/dist/components/markdownEditorControls/Strikethrough.js +11 -0
- package/dist/components/markdownEditorControls/Table.js +80 -0
- package/dist/utils/marked.esm.js +1 -0
- package/dist/utils/renderMarkdown.js +1 -0
- package/dist/utils/sanitizeHtml.js +1 -0
- package/docs/components/accordion.html +12 -0
- package/docs/components/aside.html +12 -0
- package/docs/components/calendar.html +12 -0
- package/docs/components/card.html +12 -0
- package/docs/components/chat.html +715 -0
- package/docs/components/code-editor.html +48 -0
- package/docs/components/color-picker.html +12 -0
- package/docs/components/combobox.html +12 -0
- package/docs/components/content-slider.html +12 -0
- package/docs/components/context.html +12 -0
- package/docs/components/dialog.html +12 -0
- package/docs/components/dropdown.html +12 -0
- package/docs/components/filter-list.html +12 -0
- package/docs/components/focus-capture.html +12 -0
- package/docs/components/html-editor.html +53 -0
- package/docs/components/hybrid-component.html +12 -0
- package/docs/components/icon.html +12 -0
- package/docs/components/import.html +12 -0
- package/docs/components/light-component.html +12 -0
- package/docs/components/markdown-editor.html +924 -0
- package/docs/components/nav-spacer.html +12 -0
- package/docs/components/nav.html +12 -0
- package/docs/components/photo-viewer.html +12 -0
- package/docs/components/progress.html +12 -0
- package/docs/components/resize.html +15 -0
- package/docs/components/rich-textarea.html +650 -0
- package/docs/components/shadow-component.html +12 -0
- package/docs/components/show-more.html +12 -0
- package/docs/components/slider.html +12 -0
- package/docs/components/sortable.html +12 -0
- package/docs/components/speech-to-text.html +12 -0
- package/docs/components/spinner.html +12 -0
- package/docs/components/split.html +12 -0
- package/docs/components/table.html +12 -0
- package/docs/components/tableControls.html +12 -0
- package/docs/components/tableCustomFields.html +12 -0
- package/docs/components/tableFetchRecords.html +12 -0
- package/docs/components/tableFieldSortHide.html +12 -0
- package/docs/components/tablePagination.html +12 -0
- package/docs/components/tablePlaceholder.html +12 -0
- package/docs/components/tableRecordEditing.html +12 -0
- package/docs/components/tableRecordFiltering.html +12 -0
- package/docs/components/tableRecordHiding.html +12 -0
- package/docs/components/tableRecordSearching.html +12 -0
- package/docs/components/tableRecordSelection.html +12 -0
- package/docs/components/tableRowControls.html +12 -0
- package/docs/components/tableServerSync.html +12 -0
- package/docs/components/tableSorting.html +12 -0
- package/docs/components/tabs.html +12 -0
- package/docs/components/tags.html +12 -0
- package/docs/components/text-to-speech.html +12 -0
- package/docs/components/theme-select.html +12 -0
- package/docs/components/theme-switcher.html +12 -0
- package/docs/components/time.html +12 -0
- package/docs/components/timestamp.html +12 -0
- package/docs/components/toast.html +12 -0
- package/docs/components/toggle.html +12 -0
- package/docs/components/tree.html +12 -0
- package/docs/components/voice-selector.html +12 -0
- package/docs/icons/done_all.svg +1 -0
- package/docs/icons/format_quote.svg +1 -0
- package/docs/icons/format_strikethrough.svg +1 -0
- package/docs/icons/send.svg +1 -0
- package/docs/icons/text_fields.svg +1 -0
- package/docs/index.html +30 -0
- package/docs/src/components/Chat.js +201 -0
- package/docs/src/components/CodeEditor.js +21 -2
- package/docs/src/components/Dropdown.js +1 -1
- package/docs/src/components/HtmlEditor.js +42 -3
- package/docs/src/components/MarkdownEditor.js +173 -0
- package/docs/src/components/Resize.js +12 -2
- package/docs/src/components/markdownEditorControls/Bold.js +11 -0
- package/docs/src/components/markdownEditorControls/BulletList.js +11 -0
- package/docs/src/components/markdownEditorControls/Code.js +11 -0
- package/docs/src/components/markdownEditorControls/FormatBlock.js +30 -0
- package/docs/src/components/markdownEditorControls/Heading.js +11 -0
- package/docs/src/components/markdownEditorControls/Image.js +93 -0
- package/docs/src/components/markdownEditorControls/Italic.js +11 -0
- package/docs/src/components/markdownEditorControls/Link.js +11 -0
- package/docs/src/components/markdownEditorControls/MarkdownEditorControl.js +39 -0
- package/docs/src/components/markdownEditorControls/Menu.js +30 -0
- package/docs/src/components/markdownEditorControls/NumberedList.js +11 -0
- package/docs/src/components/markdownEditorControls/Quote.js +11 -0
- package/docs/src/components/markdownEditorControls/SpeechToText.js +16 -0
- package/docs/src/components/markdownEditorControls/Strikethrough.js +11 -0
- package/docs/src/components/markdownEditorControls/Table.js +80 -0
- package/docs/src/utils/marked.esm.js +1 -0
- package/docs/src/utils/renderMarkdown.js +1 -0
- package/docs/src/utils/sanitizeHtml.js +1 -0
- package/docs/utils/context.html +12 -0
- package/docs/utils/cookie.html +12 -0
- package/docs/utils/debounce.html +12 -0
- package/docs/utils/drag.html +12 -0
- package/docs/utils/elevation.html +16 -10
- package/docs/utils/formatTimestamp.html +12 -0
- package/docs/utils/object.html +12 -0
- package/docs/utils/propConverters.html +12 -0
- package/docs/utils/sanitizeHtml.html +555 -0
- package/docs/utils/string.html +12 -0
- package/docs/utils/theme.html +12 -0
- package/docs/utils/toTitleCase.html +12 -0
- package/docs/utils/type.html +12 -0
- package/docs/utils/voice.html +12 -0
- package/docs/utils/wait.html +12 -0
- package/docs-src/components/chat.page.html +230 -0
- package/docs-src/components/code-editor.page.html +36 -0
- package/docs-src/components/html-editor.page.html +41 -0
- package/docs-src/components/markdown-editor.page.html +439 -0
- package/docs-src/components/resize.page.html +3 -0
- package/docs-src/index.page.html +18 -0
- package/docs-src/nav.fragment.html +12 -0
- package/docs-src/utils/elevation.page.html +4 -10
- package/docs-src/utils/sanitizeHtml.page.html +73 -0
- package/icons/done_all.svg +1 -0
- package/icons/format_quote.svg +1 -0
- package/icons/format_strikethrough.svg +1 -0
- package/icons/send.svg +1 -0
- package/icons/text_fields.svg +1 -0
- package/llms.txt +5 -2
- package/package.json +3 -2
- package/src/components/Chat.js +435 -0
- package/src/components/CodeEditor.js +58 -2
- package/src/components/Dropdown.js +1 -1
- package/src/components/HtmlEditor.js +84 -4
- package/src/components/MarkdownEditor.js +654 -0
- package/src/components/Resize.js +17 -1
- package/src/components/markdownEditorControls/Bold.js +30 -0
- package/src/components/markdownEditorControls/BulletList.js +30 -0
- package/src/components/markdownEditorControls/Code.js +39 -0
- package/src/components/markdownEditorControls/FormatBlock.js +102 -0
- package/src/components/markdownEditorControls/Heading.js +45 -0
- package/src/components/markdownEditorControls/Image.js +250 -0
- package/src/components/markdownEditorControls/Italic.js +30 -0
- package/src/components/markdownEditorControls/Link.js +74 -0
- package/src/components/markdownEditorControls/MarkdownEditorControl.js +158 -0
- package/src/components/markdownEditorControls/Menu.js +86 -0
- package/src/components/markdownEditorControls/NumberedList.js +69 -0
- package/src/components/markdownEditorControls/Quote.js +30 -0
- package/src/components/markdownEditorControls/SpeechToText.js +65 -0
- package/src/components/markdownEditorControls/Strikethrough.js +35 -0
- package/src/components/markdownEditorControls/Table.js +164 -0
- package/src/utils/marked.esm.js +77 -0
- package/src/utils/renderMarkdown.js +29 -0
- package/src/utils/sanitizeHtml.js +137 -0
- package/tests/components/Chat.browser-test.js +540 -0
- package/tests/utils/sanitizeHtml.browser-test.js +179 -0
package/src/components/Resize.js
CHANGED
|
@@ -7,7 +7,8 @@ export default class Resize extends ShadowComponent {
|
|
|
7
7
|
/* Properties */
|
|
8
8
|
static properties = {
|
|
9
9
|
resizing: { type: String, reflect: true },
|
|
10
|
-
dimention: { type: String, reflect: true }
|
|
10
|
+
dimention: { type: String, reflect: true },
|
|
11
|
+
disabled: { type: Boolean, reflect: true }
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
/*
|
|
@@ -17,6 +18,7 @@ export default class Resize extends ShadowComponent {
|
|
|
17
18
|
super();
|
|
18
19
|
this.resizing = '';
|
|
19
20
|
this.dimension = '';
|
|
21
|
+
this.disabled = false;
|
|
20
22
|
this.startSize = { width: 0, height: 0 };
|
|
21
23
|
this.cleanupFuncs = [];
|
|
22
24
|
}
|
|
@@ -35,6 +37,7 @@ export default class Resize extends ShadowComponent {
|
|
|
35
37
|
Event Handlers
|
|
36
38
|
*/
|
|
37
39
|
dragStartHandler = ({ element }) => {
|
|
40
|
+
if(this.disabled) return;
|
|
38
41
|
const { width, height } = this.getBoundingClientRect();
|
|
39
42
|
this.startSize.width = width;
|
|
40
43
|
this.startSize.height = height;
|
|
@@ -46,14 +49,17 @@ export default class Resize extends ShadowComponent {
|
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
dragSideHandler = ({ x }) => {
|
|
52
|
+
if(this.disabled) return;
|
|
49
53
|
this.style.width = this.startSize.width + x + 'px';
|
|
50
54
|
};
|
|
51
55
|
|
|
52
56
|
dragBottomHandler = ({ y }) => {
|
|
57
|
+
if(this.disabled) return;
|
|
53
58
|
this.style.height = this.startSize.height + y + 'px';
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
dragCornerHandler = ({ x, y }) => {
|
|
62
|
+
if(this.disabled) return;
|
|
57
63
|
this.style.width = this.startSize.width + x + 'px';
|
|
58
64
|
this.style.height = this.startSize.height + y + 'px';
|
|
59
65
|
};
|
|
@@ -177,6 +183,16 @@ export default class Resize extends ShadowComponent {
|
|
|
177
183
|
:host([dimension="width"]) #corner {
|
|
178
184
|
display: none;
|
|
179
185
|
}
|
|
186
|
+
/* Disabled: handles can't capture pointer events, so a drag can't
|
|
187
|
+
start in the first place; visual cue: handles fade out and the
|
|
188
|
+
cursor reverts to default. */
|
|
189
|
+
:host([disabled]) #side,
|
|
190
|
+
:host([disabled]) #bottom,
|
|
191
|
+
:host([disabled]) #corner {
|
|
192
|
+
pointer-events: none;
|
|
193
|
+
cursor: default;
|
|
194
|
+
opacity: 0.4;
|
|
195
|
+
}
|
|
180
196
|
`;
|
|
181
197
|
|
|
182
198
|
/*
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import MarkdownEditorControl from './MarkdownEditorControl.js';
|
|
2
|
+
import { html } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
|
|
5
|
+
export default class MarkdownBold extends MarkdownEditorControl {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.label = 'Bold (Cmd/Ctrl+B)';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
command() {
|
|
12
|
+
this.wrapSelection('**', '**', 'bold text');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
render() {
|
|
16
|
+
return html`
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
class=${this.btnClass}
|
|
20
|
+
title=${this.label}
|
|
21
|
+
aria-label=${this.label}
|
|
22
|
+
@click=${this.handleClick}
|
|
23
|
+
>
|
|
24
|
+
<k-icon name="format_bold"></k-icon>
|
|
25
|
+
</button>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
customElements.define('k-md-bold', MarkdownBold);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import MarkdownEditorControl from './MarkdownEditorControl.js';
|
|
2
|
+
import { html } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
|
|
5
|
+
export default class MarkdownBulletList extends MarkdownEditorControl {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.label = 'Bulleted list';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
command() {
|
|
12
|
+
this.insertLinePrefix('- ');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
render() {
|
|
16
|
+
return html`
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
class=${this.btnClass}
|
|
20
|
+
title=${this.label}
|
|
21
|
+
aria-label=${this.label}
|
|
22
|
+
@click=${this.handleClick}
|
|
23
|
+
>
|
|
24
|
+
<k-icon name="format_list_bulleted"></k-icon>
|
|
25
|
+
</button>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
customElements.define('k-md-bullet-list', MarkdownBulletList);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import MarkdownEditorControl from './MarkdownEditorControl.js';
|
|
2
|
+
import { html } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Toggle inline code (backticks) for the selection. If the selection spans
|
|
7
|
+
multiple lines, a triple-backtick fenced block is inserted instead.
|
|
8
|
+
*/
|
|
9
|
+
export default class MarkdownCode extends MarkdownEditorControl {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.label = 'Code';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
command() {
|
|
16
|
+
const sel = this.getSelection();
|
|
17
|
+
if(sel.text.includes('\n')){
|
|
18
|
+
this.wrapSelection('```\n', '\n```', 'code');
|
|
19
|
+
} else {
|
|
20
|
+
this.wrapSelection('`', '`', 'code');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render() {
|
|
25
|
+
return html`
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
class=${this.btnClass}
|
|
29
|
+
title=${this.label}
|
|
30
|
+
aria-label=${this.label}
|
|
31
|
+
@click=${this.handleClick}
|
|
32
|
+
>
|
|
33
|
+
<k-icon name="code"></k-icon>
|
|
34
|
+
</button>
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
customElements.define('k-md-code', MarkdownCode);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import MarkdownEditorControl from './MarkdownEditorControl.js';
|
|
2
|
+
import { html, css } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Heading-block picker designed to go inside a `<k-md-menu>` (or any
|
|
7
|
+
container that wants menu-item-styled controls). The `tag` attribute
|
|
8
|
+
selects which heading level to switch the current line(s) to:
|
|
9
|
+
|
|
10
|
+
tag="h1" .. "h6" — Heading 1-6 (sets the `#` prefix, swapping any
|
|
11
|
+
existing heading level for the new one)
|
|
12
|
+
|
|
13
|
+
Renders as a full-width button with an icon and a text label so it reads
|
|
14
|
+
cleanly as a menu item.
|
|
15
|
+
*/
|
|
16
|
+
const TAG_LABELS = {
|
|
17
|
+
h1: 'Heading 1',
|
|
18
|
+
h2: 'Heading 2',
|
|
19
|
+
h3: 'Heading 3',
|
|
20
|
+
h4: 'Heading 4',
|
|
21
|
+
h5: 'Heading 5',
|
|
22
|
+
h6: 'Heading 6'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const TAG_ICONS = {
|
|
26
|
+
h1: 'format_h1',
|
|
27
|
+
h2: 'format_h2',
|
|
28
|
+
h3: 'format_h3',
|
|
29
|
+
h4: 'format_h4',
|
|
30
|
+
h5: 'format_h5',
|
|
31
|
+
h6: 'format_h6'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default class MarkdownFormatBlock extends MarkdownEditorControl {
|
|
35
|
+
static properties = {
|
|
36
|
+
...MarkdownEditorControl.properties,
|
|
37
|
+
tag: { type: String, reflect: true }
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super();
|
|
42
|
+
this.tag = 'h2';
|
|
43
|
+
this.label = '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
command() {
|
|
47
|
+
const tag = (this.tag || 'h2').toLowerCase();
|
|
48
|
+
if(/^h[1-6]$/.test(tag)){
|
|
49
|
+
const lvl = parseInt(tag.slice(1), 10);
|
|
50
|
+
this.insertLinePrefix('#'.repeat(lvl) + ' ', /^#{1,6} /);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get #displayLabel() {
|
|
55
|
+
if(this.label) return this.label;
|
|
56
|
+
return TAG_LABELS[(this.tag || 'h2').toLowerCase()] || this.tag;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get #iconName() {
|
|
60
|
+
return TAG_ICONS[(this.tag || 'h2').toLowerCase()] || 'format_h2';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render() {
|
|
64
|
+
return html`
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
class=${this.btnClass}
|
|
68
|
+
title=${this.#displayLabel}
|
|
69
|
+
aria-label=${this.#displayLabel}
|
|
70
|
+
@click=${this.handleClick}
|
|
71
|
+
>
|
|
72
|
+
<k-icon name=${this.#iconName}></k-icon>
|
|
73
|
+
<span class="fb-label">${this.#displayLabel}</span>
|
|
74
|
+
</button>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static styles = [
|
|
79
|
+
MarkdownEditorControl.styles,
|
|
80
|
+
css`
|
|
81
|
+
:host {
|
|
82
|
+
display: inline-flex;
|
|
83
|
+
}
|
|
84
|
+
.ctrl {
|
|
85
|
+
gap: 0.5rem;
|
|
86
|
+
justify-content: flex-start;
|
|
87
|
+
white-space: nowrap;
|
|
88
|
+
}
|
|
89
|
+
/* When this is slotted into something that stretches it (a menu),
|
|
90
|
+
the button fills the row instead of staying compact. */
|
|
91
|
+
:host([slot]) .ctrl,
|
|
92
|
+
:host(*) .ctrl {
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
.fb-label {
|
|
96
|
+
font-size: 0.875rem;
|
|
97
|
+
}
|
|
98
|
+
`
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
customElements.define('k-md-format-block', MarkdownFormatBlock);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import MarkdownEditorControl from './MarkdownEditorControl.js';
|
|
2
|
+
import { html } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Heading control. Defaults to H2 (the typical "section title" level inside
|
|
7
|
+
a comment / chat / form). Override the level with the `level` attribute
|
|
8
|
+
(1–6) — e.g. <k-md-heading level="3"></k-md-heading>.
|
|
9
|
+
*/
|
|
10
|
+
export default class MarkdownHeading extends MarkdownEditorControl {
|
|
11
|
+
static properties = {
|
|
12
|
+
...MarkdownEditorControl.properties,
|
|
13
|
+
level: { type: Number, reflect: true }
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
this.level = 2;
|
|
19
|
+
this.label = 'Heading';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
command() {
|
|
23
|
+
const lvl = Math.min(6, Math.max(1, Number(this.level) || 2));
|
|
24
|
+
// Pass a replacePattern so clicking a different heading level swaps the
|
|
25
|
+
// existing one out instead of stacking another `#` group on top.
|
|
26
|
+
this.insertLinePrefix('#'.repeat(lvl) + ' ', /^#{1,6} /);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render() {
|
|
30
|
+
const iconLevel = Math.min(6, Math.max(1, Number(this.level) || 2));
|
|
31
|
+
return html`
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
class=${this.btnClass}
|
|
35
|
+
title="${this.label} ${iconLevel}"
|
|
36
|
+
aria-label="${this.label} ${iconLevel}"
|
|
37
|
+
@click=${this.handleClick}
|
|
38
|
+
>
|
|
39
|
+
<k-icon name="format_h${iconLevel}"></k-icon>
|
|
40
|
+
</button>
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
customElements.define('k-md-heading', MarkdownHeading);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import MarkdownEditorControl from './MarkdownEditorControl.js';
|
|
2
|
+
import { html, css } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
import '../Dropdown.js';
|
|
5
|
+
import Dialog from '../Dialog.js';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
Image control. Asks the user for a URL and alt text, then inserts a
|
|
9
|
+
markdown image (``) at the cursor.
|
|
10
|
+
|
|
11
|
+
The popup type is configurable via the `popup` attribute:
|
|
12
|
+
popup="dropdown" (default) — anchored popover next to the button
|
|
13
|
+
popup="dialog" — modal Dialog centered on the page
|
|
14
|
+
|
|
15
|
+
If the user has text selected when they click the button, it's pre-filled
|
|
16
|
+
into the alt text field.
|
|
17
|
+
*/
|
|
18
|
+
export default class MarkdownImage extends MarkdownEditorControl {
|
|
19
|
+
static properties = {
|
|
20
|
+
...MarkdownEditorControl.properties,
|
|
21
|
+
popup: { type: String, reflect: true }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
this.label = 'Image';
|
|
27
|
+
this.popup = 'dropdown';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
For dropdown mode the trigger button toggles the dropdown via Dropdown's
|
|
32
|
+
own click handling — we don't want our base-class handleClick (which
|
|
33
|
+
calls command()) to fire on top, or it would create a feedback loop.
|
|
34
|
+
Override command() to do nothing in dropdown mode; the Insert button
|
|
35
|
+
inside the dropdown drives insertion instead.
|
|
36
|
+
*/
|
|
37
|
+
command() {
|
|
38
|
+
if(this.popup === 'dialog') this.#openDialog();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#insert(url, alt) {
|
|
42
|
+
if(!url) return false;
|
|
43
|
+
this.editor?.replaceSelection(``, { selectInserted: false });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
Dropdown flow — inline popover form.
|
|
49
|
+
*/
|
|
50
|
+
handleDropdownOpened = () => {
|
|
51
|
+
requestAnimationFrame(() => {
|
|
52
|
+
this.#urlInput?.focus();
|
|
53
|
+
// Pre-fill alt text with the current selection (if any).
|
|
54
|
+
const sel = this.getSelection();
|
|
55
|
+
if(this.#altInput) this.#altInput.value = sel.text || '';
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
handleDropdownClosed = () => {
|
|
60
|
+
if(this.#urlInput) this.#urlInput.value = '';
|
|
61
|
+
if(this.#altInput) this.#altInput.value = '';
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
handleFormKeydown = (e) => {
|
|
65
|
+
if(e.key === 'Enter'){
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
this.#submitFromDropdown();
|
|
68
|
+
} else if(e.key === 'Escape'){
|
|
69
|
+
this.shadowRoot?.querySelector('k-dropdown')?.close();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
handleInsertClick = () => this.#submitFromDropdown();
|
|
74
|
+
|
|
75
|
+
handleCancelClick = () => {
|
|
76
|
+
this.shadowRoot?.querySelector('k-dropdown')?.close();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
#submitFromDropdown() {
|
|
80
|
+
const url = this.#urlInput?.value.trim() || '';
|
|
81
|
+
const alt = this.#altInput?.value.trim() || '';
|
|
82
|
+
if(!url){
|
|
83
|
+
this.#urlInput?.focus();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.#insert(url, alt);
|
|
87
|
+
this.shadowRoot?.querySelector('k-dropdown')?.close();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get #urlInput() { return this.shadowRoot?.querySelector('.image-url'); }
|
|
91
|
+
get #altInput() { return this.shadowRoot?.querySelector('.image-alt'); }
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
Dialog flow — modal version, kept for consumers who want it.
|
|
95
|
+
*/
|
|
96
|
+
#openDialog() {
|
|
97
|
+
const editor = this.editor;
|
|
98
|
+
if(!editor) return;
|
|
99
|
+
const sel = this.getSelection();
|
|
100
|
+
|
|
101
|
+
const urlInput = document.createElement('input');
|
|
102
|
+
urlInput.type = 'url';
|
|
103
|
+
urlInput.placeholder = 'https://example.com/image.png';
|
|
104
|
+
urlInput.required = true;
|
|
105
|
+
urlInput.style.cssText = 'padding: 0.5rem; border: 1px solid var(--c_border); border-radius: var(--radius); font: inherit;';
|
|
106
|
+
|
|
107
|
+
const altInput = document.createElement('input');
|
|
108
|
+
altInput.type = 'text';
|
|
109
|
+
altInput.placeholder = 'Description for screen readers';
|
|
110
|
+
altInput.value = sel.text || '';
|
|
111
|
+
altInput.style.cssText = 'padding: 0.5rem; border: 1px solid var(--c_border); border-radius: var(--radius); font: inherit;';
|
|
112
|
+
|
|
113
|
+
const content = document.createElement('div');
|
|
114
|
+
content.className = 'p';
|
|
115
|
+
content.style.cssText = 'display: flex; flex-direction: column; gap: 1rem;';
|
|
116
|
+
content.innerHTML = `
|
|
117
|
+
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
|
118
|
+
<label style="font-weight: bold;">Image URL</label>
|
|
119
|
+
</div>
|
|
120
|
+
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
|
121
|
+
<label style="font-weight: bold;">Alt text</label>
|
|
122
|
+
</div>
|
|
123
|
+
`;
|
|
124
|
+
content.children[0].appendChild(urlInput);
|
|
125
|
+
content.children[1].appendChild(altInput);
|
|
126
|
+
|
|
127
|
+
Dialog.create(content, {
|
|
128
|
+
title: 'Insert Image',
|
|
129
|
+
cancelText: 'Cancel',
|
|
130
|
+
confirmText: 'Insert',
|
|
131
|
+
confirmClasses: 'success ml',
|
|
132
|
+
confirmAction: (event) => {
|
|
133
|
+
const url = urlInput.value.trim();
|
|
134
|
+
if(!url){
|
|
135
|
+
event.keepDialogOpen = true;
|
|
136
|
+
urlInput.focus();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
this.#insert(url, altInput.value.trim());
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
queueMicrotask(() => urlInput.focus());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
render() {
|
|
146
|
+
if(this.popup === 'dialog'){
|
|
147
|
+
return html`
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
class=${this.btnClass}
|
|
151
|
+
title=${this.label}
|
|
152
|
+
aria-label=${this.label}
|
|
153
|
+
@click=${this.handleClick}
|
|
154
|
+
>
|
|
155
|
+
<k-icon name="image"></k-icon>
|
|
156
|
+
</button>
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
return html`
|
|
160
|
+
<k-dropdown
|
|
161
|
+
@opened=${this.handleDropdownOpened}
|
|
162
|
+
@closed=${this.handleDropdownClosed}
|
|
163
|
+
>
|
|
164
|
+
<button
|
|
165
|
+
slot="trigger"
|
|
166
|
+
type="button"
|
|
167
|
+
class=${this.btnClass}
|
|
168
|
+
title=${this.label}
|
|
169
|
+
aria-label=${this.label}
|
|
170
|
+
>
|
|
171
|
+
<k-icon name="image"></k-icon>
|
|
172
|
+
</button>
|
|
173
|
+
<div class="image-form" @keydown=${this.handleFormKeydown}>
|
|
174
|
+
<label>Image URL</label>
|
|
175
|
+
<input
|
|
176
|
+
class="image-url"
|
|
177
|
+
type="url"
|
|
178
|
+
required
|
|
179
|
+
placeholder="https://example.com/image.png"
|
|
180
|
+
/>
|
|
181
|
+
<label>Alt text</label>
|
|
182
|
+
<input
|
|
183
|
+
class="image-alt"
|
|
184
|
+
type="text"
|
|
185
|
+
placeholder="Description for screen readers"
|
|
186
|
+
/>
|
|
187
|
+
<div class="image-actions">
|
|
188
|
+
<button type="button" class="no-btn" @click=${this.handleCancelClick}>Cancel</button>
|
|
189
|
+
<button type="button" class="no-btn primary" @click=${this.handleInsertClick}>Insert</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</k-dropdown>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
static styles = [
|
|
197
|
+
MarkdownEditorControl.styles,
|
|
198
|
+
css`
|
|
199
|
+
.image-form {
|
|
200
|
+
display: flex;
|
|
201
|
+
flex-direction: column;
|
|
202
|
+
gap: 0.5rem;
|
|
203
|
+
padding: 0.75rem;
|
|
204
|
+
min-width: 16rem;
|
|
205
|
+
}
|
|
206
|
+
.image-form label {
|
|
207
|
+
font-weight: bold;
|
|
208
|
+
font-size: 0.875rem;
|
|
209
|
+
}
|
|
210
|
+
.image-form input {
|
|
211
|
+
padding: 0.5rem;
|
|
212
|
+
border: 1px solid var(--c_border);
|
|
213
|
+
border-radius: var(--radius);
|
|
214
|
+
font: inherit;
|
|
215
|
+
background: var(--c_bg);
|
|
216
|
+
color: var(--tc);
|
|
217
|
+
}
|
|
218
|
+
.image-form input:focus-visible {
|
|
219
|
+
outline: none;
|
|
220
|
+
box-shadow: var(--focus_shadow);
|
|
221
|
+
}
|
|
222
|
+
.image-actions {
|
|
223
|
+
display: flex;
|
|
224
|
+
justify-content: flex-end;
|
|
225
|
+
gap: 0.5rem;
|
|
226
|
+
margin-top: 0.25rem;
|
|
227
|
+
}
|
|
228
|
+
.image-actions button {
|
|
229
|
+
padding: 0.4rem 0.8rem;
|
|
230
|
+
border: 1px solid var(--c_border);
|
|
231
|
+
border-radius: var(--radius);
|
|
232
|
+
background: var(--c_bg);
|
|
233
|
+
color: var(--tc);
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
font: inherit;
|
|
236
|
+
}
|
|
237
|
+
.image-actions button.primary {
|
|
238
|
+
background: var(--c_primary);
|
|
239
|
+
color: white;
|
|
240
|
+
border-color: var(--c_primary);
|
|
241
|
+
}
|
|
242
|
+
.image-actions button:focus-visible {
|
|
243
|
+
outline: none;
|
|
244
|
+
box-shadow: var(--focus_shadow);
|
|
245
|
+
}
|
|
246
|
+
`
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
customElements.define('k-md-image', MarkdownImage);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import MarkdownEditorControl from './MarkdownEditorControl.js';
|
|
2
|
+
import { html } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
|
|
5
|
+
export default class MarkdownItalic extends MarkdownEditorControl {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.label = 'Italic (Cmd/Ctrl+I)';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
command() {
|
|
12
|
+
this.wrapSelection('_', '_', 'italic text');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
render() {
|
|
16
|
+
return html`
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
class=${this.btnClass}
|
|
20
|
+
title=${this.label}
|
|
21
|
+
aria-label=${this.label}
|
|
22
|
+
@click=${this.handleClick}
|
|
23
|
+
>
|
|
24
|
+
<k-icon name="format_italic"></k-icon>
|
|
25
|
+
</button>
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
customElements.define('k-md-italic', MarkdownItalic);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import MarkdownEditorControl from './MarkdownEditorControl.js';
|
|
2
|
+
import { html } from '../../lit-all.min.js';
|
|
3
|
+
import '../Icon.js';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Link control. Wraps the selection in `[text](url)`. If the user has selected
|
|
7
|
+
text it becomes the link text and the URL placeholder is selected for them
|
|
8
|
+
to overtype. If nothing is selected, a `link text` placeholder is used and
|
|
9
|
+
the user can tab through to the URL.
|
|
10
|
+
*/
|
|
11
|
+
export default class MarkdownLink extends MarkdownEditorControl {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.label = 'Link';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
command() {
|
|
18
|
+
const sel = this.getSelection();
|
|
19
|
+
const editor = this.editor;
|
|
20
|
+
if(!editor) return;
|
|
21
|
+
const ta = editor.textarea;
|
|
22
|
+
if(!ta) return;
|
|
23
|
+
|
|
24
|
+
const fireInput = (next, selStart, selEnd) => {
|
|
25
|
+
ta.value = next;
|
|
26
|
+
ta.selectionStart = selStart;
|
|
27
|
+
ta.selectionEnd = selEnd;
|
|
28
|
+
ta.focus();
|
|
29
|
+
editor.value = next;
|
|
30
|
+
ta.dispatchEvent(new Event('input', { bubbles: true }));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Toggle: if the selection (or just the cursor) sits anywhere inside an
|
|
34
|
+
// existing `[label](url)`, unwrap that whole link to just its label. This
|
|
35
|
+
// works whether the user has the label, the URL, or nothing selected —
|
|
36
|
+
// they just have to be somewhere inside the link.
|
|
37
|
+
const re = /\[([^\]]*)\]\([^)]*\)/g;
|
|
38
|
+
let m;
|
|
39
|
+
while((m = re.exec(ta.value)) !== null){
|
|
40
|
+
const matchStart = m.index;
|
|
41
|
+
const matchEnd = matchStart + m[0].length;
|
|
42
|
+
if(sel.start >= matchStart && sel.end <= matchEnd){
|
|
43
|
+
const label = m[1];
|
|
44
|
+
const next = ta.value.substring(0, matchStart) + label + ta.value.substring(matchEnd);
|
|
45
|
+
fireInput(next, matchStart, matchStart + label.length);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Otherwise insert a new link with the URL portion pre-selected so the
|
|
51
|
+
// user can immediately overtype it.
|
|
52
|
+
const text = sel.text || 'link text';
|
|
53
|
+
const inserted = `[${text}](url)`;
|
|
54
|
+
const next = ta.value.substring(0, sel.start) + inserted + ta.value.substring(sel.end);
|
|
55
|
+
const urlStart = sel.start + 1 + text.length + 2; // past `[text](`
|
|
56
|
+
fireInput(next, urlStart, urlStart + 'url'.length);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render() {
|
|
60
|
+
return html`
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
class=${this.btnClass}
|
|
64
|
+
title=${this.label}
|
|
65
|
+
aria-label=${this.label}
|
|
66
|
+
@click=${this.handleClick}
|
|
67
|
+
>
|
|
68
|
+
<k-icon name="link"></k-icon>
|
|
69
|
+
</button>
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
customElements.define('k-md-link', MarkdownLink);
|