kempo-ui 0.4.4 → 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/TextToSpeech.js +4 -4
- package/dist/components/VoiceSelector.js +49 -0
- 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/dist/utils/voice.js +1 -0
- package/docs/components/accordion.html +19 -0
- package/docs/components/aside.html +19 -0
- package/docs/components/calendar.html +19 -0
- package/docs/components/card.html +19 -0
- package/docs/components/chat.html +715 -0
- package/docs/components/code-editor.html +55 -0
- package/docs/components/color-picker.html +19 -0
- package/docs/components/combobox.html +19 -0
- package/docs/components/content-slider.html +19 -0
- package/docs/components/context.html +19 -0
- package/docs/components/dialog.html +19 -0
- package/docs/components/dropdown.html +19 -0
- package/docs/components/filter-list.html +19 -0
- package/docs/components/focus-capture.html +19 -0
- package/docs/components/html-editor.html +60 -0
- package/docs/components/hybrid-component.html +19 -0
- package/docs/components/icon.html +19 -0
- package/docs/components/import.html +19 -0
- package/docs/components/light-component.html +19 -0
- package/docs/components/markdown-editor.html +924 -0
- package/docs/components/nav-spacer.html +19 -0
- package/docs/components/nav.html +19 -0
- package/docs/components/photo-viewer.html +19 -0
- package/docs/components/progress.html +19 -0
- package/docs/components/resize.html +22 -0
- package/docs/components/rich-textarea.html +650 -0
- package/docs/components/shadow-component.html +19 -0
- package/docs/components/show-more.html +19 -0
- package/docs/components/slider.html +19 -0
- package/docs/components/sortable.html +19 -0
- package/docs/components/speech-to-text.html +19 -0
- package/docs/components/spinner.html +19 -0
- package/docs/components/split.html +19 -0
- package/docs/components/table.html +19 -0
- package/docs/components/tableControls.html +19 -0
- package/docs/components/tableCustomFields.html +19 -0
- package/docs/components/tableFetchRecords.html +19 -0
- package/docs/components/tableFieldSortHide.html +19 -0
- package/docs/components/tablePagination.html +19 -0
- package/docs/components/tablePlaceholder.html +19 -0
- package/docs/components/tableRecordEditing.html +19 -0
- package/docs/components/tableRecordFiltering.html +19 -0
- package/docs/components/tableRecordHiding.html +19 -0
- package/docs/components/tableRecordSearching.html +19 -0
- package/docs/components/tableRecordSelection.html +19 -0
- package/docs/components/tableRowControls.html +19 -0
- package/docs/components/tableServerSync.html +19 -0
- package/docs/components/tableSorting.html +19 -0
- package/docs/components/tabs.html +19 -0
- package/docs/components/tags.html +19 -0
- package/docs/components/text-to-speech.html +63 -1
- package/docs/components/theme-select.html +19 -0
- package/docs/components/theme-switcher.html +19 -0
- package/docs/components/time.html +19 -0
- package/docs/components/timestamp.html +19 -0
- package/docs/components/toast.html +19 -0
- package/docs/components/toggle.html +19 -0
- package/docs/components/tree.html +19 -0
- package/docs/components/voice-selector.html +610 -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 +49 -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/TextToSpeech.js +4 -4
- package/docs/src/components/VoiceSelector.js +49 -0
- 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/src/utils/voice.js +1 -0
- package/docs/utils/context.html +19 -0
- package/docs/utils/cookie.html +19 -0
- package/docs/utils/debounce.html +19 -0
- package/docs/utils/drag.html +19 -0
- package/docs/utils/elevation.html +23 -10
- package/docs/utils/formatTimestamp.html +19 -0
- package/docs/utils/object.html +19 -0
- package/docs/utils/propConverters.html +19 -0
- package/docs/utils/sanitizeHtml.html +555 -0
- package/docs/utils/string.html +19 -0
- package/docs/utils/theme.html +19 -0
- package/docs/utils/toTitleCase.html +19 -0
- package/docs/utils/type.html +19 -0
- package/docs/utils/voice.html +548 -0
- package/docs/utils/wait.html +19 -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/components/text-to-speech.page.html +44 -1
- package/docs-src/components/voice-selector.page.html +125 -0
- package/docs-src/index.page.html +30 -0
- package/docs-src/nav.fragment.html +19 -0
- package/docs-src/utils/elevation.page.html +4 -10
- package/docs-src/utils/sanitizeHtml.page.html +73 -0
- package/docs-src/utils/voice.page.html +66 -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 +7 -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/TextToSpeech.js +13 -2
- package/src/components/VoiceSelector.js +271 -0
- 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/src/utils/voice.js +103 -0
- package/tests/components/Chat.browser-test.js +540 -0
- package/tests/components/TextToSpeech.browser-test.js +150 -1
- package/tests/components/VoiceSelector.browser-test.js +374 -0
- package/tests/utils/sanitizeHtml.browser-test.js +179 -0
- package/tests/utils/voice.browser-test.js +128 -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
|
/*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { html, css } from '../lit-all.min.js';
|
|
2
2
|
import ShadowComponent from './ShadowComponent.js';
|
|
3
|
+
import { getVoice as getPreferredVoice } from '../utils/voice.js';
|
|
3
4
|
import './Icon.js';
|
|
4
5
|
|
|
5
6
|
export default class TextToSpeech extends ShadowComponent {
|
|
@@ -53,9 +54,19 @@ export default class TextToSpeech extends ShadowComponent {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
resolveVoice = () => {
|
|
56
|
-
if(
|
|
57
|
+
if(this.#unsupported) return null;
|
|
58
|
+
// Use the explicit `voice` attribute first; if none is set, fall back to
|
|
59
|
+
// the user's saved preference from the voice utility (so a site-wide
|
|
60
|
+
// VoiceSelector can drive every TextToSpeech that hasn't opted out).
|
|
61
|
+
const source = this.voice || getPreferredVoice() || '';
|
|
62
|
+
if(!source) return null;
|
|
57
63
|
const voices = window.speechSynthesis.getVoices();
|
|
58
|
-
|
|
64
|
+
const candidates = source.split(',').map(s => s.trim()).filter(Boolean);
|
|
65
|
+
for(const name of candidates){
|
|
66
|
+
const match = voices.find(v => v.name === name);
|
|
67
|
+
if(match) return match;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
/*
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { html, css } from '../lit-all.min.js';
|
|
2
|
+
import ShadowComponent from './ShadowComponent.js';
|
|
3
|
+
import {
|
|
4
|
+
getVoice,
|
|
5
|
+
setVoice,
|
|
6
|
+
subscribeToVoice,
|
|
7
|
+
subscribeToAvailableVoices
|
|
8
|
+
} from '../utils/voice.js';
|
|
9
|
+
|
|
10
|
+
const primaryLang = (tag) => (tag || '').split('-')[0].toLowerCase();
|
|
11
|
+
|
|
12
|
+
const browserLanguage = () => {
|
|
13
|
+
if(typeof navigator === 'undefined') return 'en';
|
|
14
|
+
return primaryLang(navigator.language || 'en') || 'en';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let displayNames = null;
|
|
18
|
+
try {
|
|
19
|
+
if(typeof Intl !== 'undefined' && Intl.DisplayNames){
|
|
20
|
+
const locale = (typeof navigator !== 'undefined' && navigator.language) || 'en';
|
|
21
|
+
displayNames = new Intl.DisplayNames([locale], { type: 'language' });
|
|
22
|
+
}
|
|
23
|
+
} catch(e) {}
|
|
24
|
+
|
|
25
|
+
// Cache for native-name lookups (Intl.DisplayNames in the language's own locale)
|
|
26
|
+
// so we don't construct a new instance per option per render.
|
|
27
|
+
const nativeNameCache = new Map();
|
|
28
|
+
const getNativeName = (code) => {
|
|
29
|
+
if(nativeNameCache.has(code)) return nativeNameCache.get(code);
|
|
30
|
+
let native = '';
|
|
31
|
+
try {
|
|
32
|
+
if(typeof Intl !== 'undefined' && Intl.DisplayNames){
|
|
33
|
+
const dn = new Intl.DisplayNames([code], { type: 'language' });
|
|
34
|
+
native = dn.of(code) || '';
|
|
35
|
+
}
|
|
36
|
+
} catch(e) {}
|
|
37
|
+
// Capitalize the first character so dropdown labels look consistent —
|
|
38
|
+
// Intl.DisplayNames returns "español"/"français" lowercase, which looks
|
|
39
|
+
// off next to the capitalized localized name.
|
|
40
|
+
if(native){
|
|
41
|
+
native = native.charAt(0).toUpperCase() + native.slice(1);
|
|
42
|
+
}
|
|
43
|
+
nativeNameCache.set(code, native);
|
|
44
|
+
return native;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Render a language code as "Localized (Native)" — e.g. "Spanish (Español)",
|
|
48
|
+
// "Japanese (日本語)". The native name is a deliberate accessibility escape
|
|
49
|
+
// hatch: if the user accidentally has the page in a language they can't read,
|
|
50
|
+
// they can still recognize their own language by its native name. When the
|
|
51
|
+
// localized and native names match (user already in that language) we skip
|
|
52
|
+
// the duplicate.
|
|
53
|
+
const formatLanguage = (code) => {
|
|
54
|
+
let localized = code;
|
|
55
|
+
if(displayNames){
|
|
56
|
+
try {
|
|
57
|
+
localized = displayNames.of(code) || code;
|
|
58
|
+
} catch(e) {}
|
|
59
|
+
}
|
|
60
|
+
const native = getNativeName(code);
|
|
61
|
+
if(native && native.toLowerCase() !== localized.toLowerCase()){
|
|
62
|
+
return `${localized} (${native})`;
|
|
63
|
+
}
|
|
64
|
+
return localized;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Strip the parenthetical "(language (region))" that OS-level voices often
|
|
68
|
+
// append to their name (e.g. "Eddy (German (Germany))" → "Eddy"). The actual
|
|
69
|
+
// `value` we submit to SpeechSynthesis still uses the full name.
|
|
70
|
+
const displayVoiceName = (name) => {
|
|
71
|
+
const cleaned = name.split('(')[0].trim();
|
|
72
|
+
return cleaned || name;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default class VoiceSelector extends ShadowComponent {
|
|
76
|
+
static properties = {
|
|
77
|
+
language: { type: String, reflect: true },
|
|
78
|
+
placeholder: { type: String, reflect: true },
|
|
79
|
+
disabled: { type: Boolean, reflect: true },
|
|
80
|
+
voices: { state: true },
|
|
81
|
+
selected: { state: true },
|
|
82
|
+
currentLang: { state: true }
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
#unsubVoices = null;
|
|
86
|
+
#unsubSelected = null;
|
|
87
|
+
#unsupported = false;
|
|
88
|
+
#langInitialized = false;
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
Lifecycle Callbacks
|
|
92
|
+
*/
|
|
93
|
+
constructor() {
|
|
94
|
+
super();
|
|
95
|
+
this.language = '';
|
|
96
|
+
this.placeholder = 'Browser default';
|
|
97
|
+
this.disabled = false;
|
|
98
|
+
this.voices = [];
|
|
99
|
+
this.selected = '';
|
|
100
|
+
this.currentLang = '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
connectedCallback() {
|
|
104
|
+
super.connectedCallback();
|
|
105
|
+
if(typeof window === 'undefined' || !window.speechSynthesis){
|
|
106
|
+
this.#unsupported = true;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.#unsubVoices = subscribeToAvailableVoices(list => {
|
|
110
|
+
this.voices = list;
|
|
111
|
+
this.#initLanguageOnce();
|
|
112
|
+
});
|
|
113
|
+
this.#unsubSelected = subscribeToVoice(name => {
|
|
114
|
+
this.selected = name || '';
|
|
115
|
+
// Keep the language dropdown in sync with externally-set voices
|
|
116
|
+
if(name){
|
|
117
|
+
const match = this.voices.find(v => v.name === name);
|
|
118
|
+
if(match && match.lang){
|
|
119
|
+
this.currentLang = primaryLang(match.lang);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
disconnectedCallback() {
|
|
126
|
+
super.disconnectedCallback();
|
|
127
|
+
if(this.#unsubVoices) this.#unsubVoices();
|
|
128
|
+
if(this.#unsubSelected) this.#unsubSelected();
|
|
129
|
+
this.#unsubVoices = null;
|
|
130
|
+
this.#unsubSelected = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
Utility
|
|
135
|
+
*/
|
|
136
|
+
#initLanguageOnce = () => {
|
|
137
|
+
if(this.#langInitialized || this.voices.length === 0) return;
|
|
138
|
+
this.#langInitialized = true;
|
|
139
|
+
// Priority: existing voice's language > attribute > navigator > first available
|
|
140
|
+
const fromVoice = this.selected
|
|
141
|
+
? this.voices.find(v => v.name === this.selected)
|
|
142
|
+
: null;
|
|
143
|
+
if(fromVoice && fromVoice.lang){
|
|
144
|
+
this.currentLang = primaryLang(fromVoice.lang);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if(this.language){
|
|
148
|
+
this.currentLang = primaryLang(this.language);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const navLang = browserLanguage();
|
|
152
|
+
const langs = this.availableLanguages;
|
|
153
|
+
if(langs.includes(navLang)){
|
|
154
|
+
this.currentLang = navLang;
|
|
155
|
+
} else if(langs.length > 0){
|
|
156
|
+
this.currentLang = langs[0];
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
get availableLanguages() {
|
|
161
|
+
const langs = new Set();
|
|
162
|
+
for(const v of this.voices){
|
|
163
|
+
if(v.lang) langs.add(primaryLang(v.lang));
|
|
164
|
+
}
|
|
165
|
+
return [...langs].sort();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
get voicesForCurrentLanguage() {
|
|
169
|
+
if(!this.currentLang) return this.voices;
|
|
170
|
+
return this.voices.filter(v => primaryLang(v.lang) === this.currentLang);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/*
|
|
174
|
+
Event Handlers
|
|
175
|
+
*/
|
|
176
|
+
handleLanguageChange = (e) => {
|
|
177
|
+
if(this.disabled) return;
|
|
178
|
+
this.currentLang = e.target.value;
|
|
179
|
+
// If the saved voice no longer exists in this language, clear it
|
|
180
|
+
const stillValid = this.voicesForCurrentLanguage.some(v => v.name === this.selected);
|
|
181
|
+
if(!stillValid){
|
|
182
|
+
setVoice('');
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
handleVoiceChange = (e) => {
|
|
187
|
+
if(this.disabled) return;
|
|
188
|
+
const value = e.target.value;
|
|
189
|
+
setVoice(value);
|
|
190
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
191
|
+
detail: { voice: value, language: this.currentLang },
|
|
192
|
+
bubbles: true
|
|
193
|
+
}));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
Public Methods
|
|
198
|
+
*/
|
|
199
|
+
refresh = () => {
|
|
200
|
+
if(this.#unsupported) return;
|
|
201
|
+
this.voices = window.speechSynthesis.getVoices();
|
|
202
|
+
this.#initLanguageOnce();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
Rendering
|
|
207
|
+
*/
|
|
208
|
+
render() {
|
|
209
|
+
if(this.#unsupported){
|
|
210
|
+
return html`
|
|
211
|
+
<select disabled aria-label="Voice selector (unsupported)">
|
|
212
|
+
<option>Speech synthesis not supported</option>
|
|
213
|
+
</select>
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
const langs = this.availableLanguages;
|
|
217
|
+
const voicesInLang = this.voicesForCurrentLanguage;
|
|
218
|
+
return html`
|
|
219
|
+
<div class="row">
|
|
220
|
+
<div class="col" style="min-width: 10rem; flex: 0 0 auto">
|
|
221
|
+
<select
|
|
222
|
+
class="lang"
|
|
223
|
+
aria-label="Language"
|
|
224
|
+
?disabled=${this.disabled || langs.length === 0}
|
|
225
|
+
.value=${this.currentLang}
|
|
226
|
+
@change=${this.handleLanguageChange}
|
|
227
|
+
>
|
|
228
|
+
${langs.map(code => html`
|
|
229
|
+
<option value=${code} ?selected=${code === this.currentLang}>${formatLanguage(code)}</option>
|
|
230
|
+
`)}
|
|
231
|
+
</select>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="col">
|
|
234
|
+
<select
|
|
235
|
+
class="voice"
|
|
236
|
+
aria-label="Voice"
|
|
237
|
+
?disabled=${this.disabled}
|
|
238
|
+
.value=${this.selected}
|
|
239
|
+
@change=${this.handleVoiceChange}
|
|
240
|
+
>
|
|
241
|
+
<option value="">${this.placeholder}</option>
|
|
242
|
+
${voicesInLang.map(v => html`
|
|
243
|
+
<option
|
|
244
|
+
value=${v.name}
|
|
245
|
+
?selected=${v.name === this.selected}
|
|
246
|
+
>${displayVoiceName(v.name)}</option>
|
|
247
|
+
`)}
|
|
248
|
+
</select>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/*
|
|
255
|
+
Styles
|
|
256
|
+
*/
|
|
257
|
+
static styles = css`
|
|
258
|
+
:host {
|
|
259
|
+
display: block;
|
|
260
|
+
}
|
|
261
|
+
:host([disabled]) {
|
|
262
|
+
opacity: 0.5;
|
|
263
|
+
pointer-events: none;
|
|
264
|
+
}
|
|
265
|
+
select {
|
|
266
|
+
width: 100%;
|
|
267
|
+
}
|
|
268
|
+
`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
customElements.define('k-voice-selector', VoiceSelector);
|
|
@@ -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);
|