kempo-ui 0.4.15 → 0.4.16
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/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.16] - 2026-05-03
|
|
10
|
+
### Added
|
|
11
|
+
- **Markdown Editor component**: Debounced change event during input
|
|
12
|
+
- `change` event now fires with a 300ms debounce when text is being typed
|
|
13
|
+
- Previously, `change` events only fired on blur or native change events
|
|
14
|
+
- Enables apps to use the `change` event for enable/disable logic during long typing sessions
|
|
15
|
+
- Useful for "Save" button state management without waiting for blur
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Markdown Editor component**: Code style improvements
|
|
19
|
+
- Converted from hash private members (`#field`) to Symbol-keyed properties for full browser compatibility (Safari support)
|
|
20
|
+
- Reorganized comments to follow component-code skill pattern
|
|
21
|
+
- Removed unnecessary multi-line comments, keeping only documentation for properties and complex logic
|
|
22
|
+
|
|
9
23
|
## [0.4.13] - 2026-05-03
|
|
10
24
|
### Added
|
|
11
25
|
- **Markdown Editor component**: spacing but
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{html as t,css as e,nothing as i}from"../lit-all.min.js";import s from"./ShadowComponent.js";import
|
|
1
|
+
import{html as t,css as e,nothing as i}from"../lit-all.min.js";import s from"./ShadowComponent.js";import o from"../utils/renderMarkdown.js";import l,{STRIP_COMPLETELY as a}from"../utils/sanitizeHtml.js";import n from"../utils/debounce.js";import"./Resize.js";import"./Tabs.js";const r=Symbol(),d=Symbol(),m=Symbol(),h=Symbol(),c=Symbol();export default class u extends s{static formAssociated=!0;static properties={value:{type:String},name:{type:String,reflect:!0},placeholder:{type:String,reflect:!0},disabled:{type:Boolean,reflect:!0},required:{type:Boolean,reflect:!0},readonly:{type:Boolean,reflect:!0},mode:{type:String,reflect:!0},allowedTags:{type:String,reflect:!0,attribute:"allowed-tags"},disallowedTags:{type:String,reflect:!0,attribute:"disallowed-tags"},scriptsEnabled:{type:Boolean,reflect:!0,attribute:"scripts-enabled"},controls:{type:String,reflect:!0}};constructor(){super(),this.internals=this.attachInternals(),this.value="",this.name="",this.placeholder="",this.disabled=!1,this.required=!1,this.readonly=!1,this.mode="write",this.allowedTags="",this.disallowedTags="",this.scriptsEnabled=!1,this.controls="",this[r]="",this[m]=!1,this[d]=n(()=>this.handleChange(),300)}async loadControls(){if(this[m])return;this[m]=!0;const t=new URL("./markdownEditorControls/",import.meta.url).href;await Promise.all([import(`${t}Bold.js`),import(`${t}Italic.js`),import(`${t}Strikethrough.js`),import(`${t}Heading.js`),import(`${t}Code.js`),import(`${t}Link.js`),import(`${t}Image.js`),import(`${t}Table.js`),import(`${t}BulletList.js`),import(`${t}NumberedList.js`),import(`${t}Quote.js`),import(`${t}Menu.js`),import(`${t}FormatBlock.js`),import(`${t}SpeechToText.js`)]),this.requestUpdate()}connectedCallback(){super.connectedCallback(),this.hasAttribute("value")&&(this[r]=this.getAttribute("value")),!this.value&&this[r]&&(this.value=this[r])}updated(t){super.updated(t),t.has("value")&&this.internals.setFormValue(this.value),t.has("controls")&&this.controls&&"none"!==this.controls&&this.loadControls(),this[c]()}formResetCallback(){this.value=this[r],this.mode="write"}formStateRestoreCallback(t){"string"==typeof t&&(this.value=t)}formDisabledCallback(t){this.disabled=t}focus(){"write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{this.shadowRoot?.querySelector("textarea")?.focus()})}blur(){this.shadowRoot?.querySelector("textarea")?.blur()}clear(){this.value=""}setMode(t){"write"!==t&&"preview"!==t||(this.mode=t)}togglePreview(){this.mode="write"===this.mode?"preview":"write"}get textarea(){return this.shadowRoot?.querySelector("textarea")||null}getSelection(){const t=this.textarea;return t?{start:t.selectionStart,end:t.selectionEnd,text:t.value.substring(t.selectionStart,t.selectionEnd)}:{start:0,end:0,text:""}}replaceSelection(t,{selectInserted:e=!0}={}){const i=this.textarea;i&&("write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{i.focus();const{selectionStart:s,selectionEnd:o}=i,l=i.value.substring(0,s),a=i.value.substring(o),n=l+t+a;i.value=n;const r=s+t.length;i.selectionStart=e?s:r,i.selectionEnd=r,this.value=n,i.dispatchEvent(new Event("input",{bubbles:!0}))}))}wrapSelection(t,e=t,i=""){const s=this.textarea;s?("write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{s.focus();const{selectionStart:o,selectionEnd:l,value:a}=s,n=a.substring(o,l);if(n.length>=t.length+e.length&&n.startsWith(t)&&n.endsWith(e)){const i=n.slice(t.length,n.length-e.length),r=a.substring(0,o)+i+a.substring(l);return s.value=r,s.selectionStart=o,s.selectionEnd=o+i.length,this.value=r,void s.dispatchEvent(new Event("input",{bubbles:!0}))}const r=a.substring(Math.max(0,o-t.length),o),d=a.substring(l,Math.min(a.length,l+e.length));if(r===t&&d===e){const i=a.substring(0,o-t.length)+n+a.substring(l+e.length);return s.value=i,s.selectionStart=o-t.length,s.selectionEnd=s.selectionStart+n.length,this.value=i,void s.dispatchEvent(new Event("input",{bubbles:!0}))}const m=n||i,h=a.substring(0,o)+t+m+e+a.substring(l);s.value=h;const c=o+t.length;s.selectionStart=c,s.selectionEnd=c+m.length,this.value=h,s.dispatchEvent(new Event("input",{bubbles:!0}))})):this.replaceSelection(t+i+e)}insertAtCursor(t){this.replaceSelection(t,{selectInserted:!1})}replaceInSelectedLines(t,e=""){const i=this.textarea;i&&("write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{i.focus();const{selectionStart:s,selectionEnd:o,value:l}=i,a=l.lastIndexOf("\n",s-1)+1,n=l.indexOf("\n",o),r=-1===n?l.length:n,d=l.substring(a,r),m=d.split("\n").map(i=>i.replace(t,e)).join("\n");if(m===d)return;const h=l.substring(0,a)+m+l.substring(r);i.value=h,i.selectionStart=a,i.selectionEnd=a+m.length,this.value=h,i.dispatchEvent(new Event("input",{bubbles:!0}))}))}insertLinePrefix(t,e=null){const i=this.textarea;i&&("write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{i.focus();const{selectionStart:s,selectionEnd:o,value:l}=i,a=l.lastIndexOf("\n",s-1)+1,n=l.indexOf("\n",o),r=-1===n?l.length:n,d=l.substring(a,r).split("\n"),m=d.filter(t=>t.length>0),h=m.length>0&&m.every(e=>e.startsWith(t)),c=d.map(i=>0===i.length?i:h?i.startsWith(t)?i.slice(t.length):i:i.startsWith(t)?i:e&&e.test(i)?i.replace(e,t):t+i).join("\n"),u=l.substring(0,a)+c+l.substring(r);i.value=u,i.selectionStart=a,i.selectionEnd=a+c.length,this.value=u,i.dispatchEvent(new Event("input",{bubbles:!0}))}))}get isEmpty(){return!(this.value||"").trim()}get renderedHtml(){const t={},e=this[h];if(e&&(t.allowedTags=e),this.scriptsEnabled){const e=new Set(a);e.delete("SCRIPT"),t.stripCompletely=e}return l(o(this.value||""),t)}get[h](){const t=(this.allowedTags||"").trim(),e=(this.disallowedTags||"").trim();if(t&&e&&console.warn("[k-markdown-editor] `allowed-tags` and `disallowed-tags` are mutually exclusive; using `allowed-tags`."),t)return"*"===t?{has:()=>!0}:new Set(t.split(",").map(t=>t.trim().toUpperCase()).filter(Boolean));if(e){const t=new Set(e.split(",").map(t=>t.trim().toUpperCase()).filter(Boolean));return{has:e=>!t.has(e)}}return null}[c]=()=>{const t=this.shadowRoot?.querySelector("textarea");this.required&&this.isEmpty?this.internals.setValidity({valueMissing:!0},"Please fill out this field.",t):this.internals.setValidity({})};handleInput=t=>{this.value=t.target.value,this.dispatchEvent(new CustomEvent("input",{detail:{value:this.value},bubbles:!0})),this[d]()};handleChange=()=>{this.dispatchEvent(new CustomEvent("change",{detail:{value:this.value},bubbles:!0}))};handleTabChange=t=>{const e=t.detail?.tab;e&&e!==this.mode&&(this.mode=e,this.dispatchEvent(new CustomEvent("mode-change",{detail:{mode:e},bubbles:!0})))};render(){const e=this.constructor.controlSets[this.controls]??this.constructor.controlSets[""];return t`
|
|
2
2
|
<k-resize dimension="height" ?disabled=${this.disabled}>
|
|
3
3
|
<div class="frame">
|
|
4
4
|
<k-tabs fixed-height active=${this.mode} @tab=${this.handleTabChange}>
|
|
@@ -18,6 +18,7 @@ import{html as t,css as e,nothing as i}from"../lit-all.min.js";import s from"./S
|
|
|
18
18
|
aria-label=${this.name||this.placeholder}
|
|
19
19
|
@input=${this.handleInput}
|
|
20
20
|
@blur=${this.handleChange}
|
|
21
|
+
@change=${this.handleChange}
|
|
21
22
|
></textarea>
|
|
22
23
|
</k-tab-content>
|
|
23
24
|
<k-tab-content name="preview">
|
|
@@ -165,4 +166,4 @@ import{html as t,css as e,nothing as i}from"../lit-all.min.js";import s from"./S
|
|
|
165
166
|
<k-md-bullet-list></k-md-bullet-list>
|
|
166
167
|
<k-md-numbered-list></k-md-numbered-list>
|
|
167
168
|
<k-md-speech-to-text></k-md-speech-to-text>
|
|
168
|
-
`,bottom:null}}}customElements.define("k-markdown-editor",
|
|
169
|
+
`,bottom:null}}}customElements.define("k-markdown-editor",u);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{html as t,css as e,nothing as i}from"../lit-all.min.js";import s from"./ShadowComponent.js";import
|
|
1
|
+
import{html as t,css as e,nothing as i}from"../lit-all.min.js";import s from"./ShadowComponent.js";import o from"../utils/renderMarkdown.js";import l,{STRIP_COMPLETELY as a}from"../utils/sanitizeHtml.js";import n from"../utils/debounce.js";import"./Resize.js";import"./Tabs.js";const r=Symbol(),d=Symbol(),m=Symbol(),h=Symbol(),c=Symbol();export default class u extends s{static formAssociated=!0;static properties={value:{type:String},name:{type:String,reflect:!0},placeholder:{type:String,reflect:!0},disabled:{type:Boolean,reflect:!0},required:{type:Boolean,reflect:!0},readonly:{type:Boolean,reflect:!0},mode:{type:String,reflect:!0},allowedTags:{type:String,reflect:!0,attribute:"allowed-tags"},disallowedTags:{type:String,reflect:!0,attribute:"disallowed-tags"},scriptsEnabled:{type:Boolean,reflect:!0,attribute:"scripts-enabled"},controls:{type:String,reflect:!0}};constructor(){super(),this.internals=this.attachInternals(),this.value="",this.name="",this.placeholder="",this.disabled=!1,this.required=!1,this.readonly=!1,this.mode="write",this.allowedTags="",this.disallowedTags="",this.scriptsEnabled=!1,this.controls="",this[r]="",this[m]=!1,this[d]=n(()=>this.handleChange(),300)}async loadControls(){if(this[m])return;this[m]=!0;const t=new URL("./markdownEditorControls/",import.meta.url).href;await Promise.all([import(`${t}Bold.js`),import(`${t}Italic.js`),import(`${t}Strikethrough.js`),import(`${t}Heading.js`),import(`${t}Code.js`),import(`${t}Link.js`),import(`${t}Image.js`),import(`${t}Table.js`),import(`${t}BulletList.js`),import(`${t}NumberedList.js`),import(`${t}Quote.js`),import(`${t}Menu.js`),import(`${t}FormatBlock.js`),import(`${t}SpeechToText.js`)]),this.requestUpdate()}connectedCallback(){super.connectedCallback(),this.hasAttribute("value")&&(this[r]=this.getAttribute("value")),!this.value&&this[r]&&(this.value=this[r])}updated(t){super.updated(t),t.has("value")&&this.internals.setFormValue(this.value),t.has("controls")&&this.controls&&"none"!==this.controls&&this.loadControls(),this[c]()}formResetCallback(){this.value=this[r],this.mode="write"}formStateRestoreCallback(t){"string"==typeof t&&(this.value=t)}formDisabledCallback(t){this.disabled=t}focus(){"write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{this.shadowRoot?.querySelector("textarea")?.focus()})}blur(){this.shadowRoot?.querySelector("textarea")?.blur()}clear(){this.value=""}setMode(t){"write"!==t&&"preview"!==t||(this.mode=t)}togglePreview(){this.mode="write"===this.mode?"preview":"write"}get textarea(){return this.shadowRoot?.querySelector("textarea")||null}getSelection(){const t=this.textarea;return t?{start:t.selectionStart,end:t.selectionEnd,text:t.value.substring(t.selectionStart,t.selectionEnd)}:{start:0,end:0,text:""}}replaceSelection(t,{selectInserted:e=!0}={}){const i=this.textarea;i&&("write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{i.focus();const{selectionStart:s,selectionEnd:o}=i,l=i.value.substring(0,s),a=i.value.substring(o),n=l+t+a;i.value=n;const r=s+t.length;i.selectionStart=e?s:r,i.selectionEnd=r,this.value=n,i.dispatchEvent(new Event("input",{bubbles:!0}))}))}wrapSelection(t,e=t,i=""){const s=this.textarea;s?("write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{s.focus();const{selectionStart:o,selectionEnd:l,value:a}=s,n=a.substring(o,l);if(n.length>=t.length+e.length&&n.startsWith(t)&&n.endsWith(e)){const i=n.slice(t.length,n.length-e.length),r=a.substring(0,o)+i+a.substring(l);return s.value=r,s.selectionStart=o,s.selectionEnd=o+i.length,this.value=r,void s.dispatchEvent(new Event("input",{bubbles:!0}))}const r=a.substring(Math.max(0,o-t.length),o),d=a.substring(l,Math.min(a.length,l+e.length));if(r===t&&d===e){const i=a.substring(0,o-t.length)+n+a.substring(l+e.length);return s.value=i,s.selectionStart=o-t.length,s.selectionEnd=s.selectionStart+n.length,this.value=i,void s.dispatchEvent(new Event("input",{bubbles:!0}))}const m=n||i,h=a.substring(0,o)+t+m+e+a.substring(l);s.value=h;const c=o+t.length;s.selectionStart=c,s.selectionEnd=c+m.length,this.value=h,s.dispatchEvent(new Event("input",{bubbles:!0}))})):this.replaceSelection(t+i+e)}insertAtCursor(t){this.replaceSelection(t,{selectInserted:!1})}replaceInSelectedLines(t,e=""){const i=this.textarea;i&&("write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{i.focus();const{selectionStart:s,selectionEnd:o,value:l}=i,a=l.lastIndexOf("\n",s-1)+1,n=l.indexOf("\n",o),r=-1===n?l.length:n,d=l.substring(a,r),m=d.split("\n").map(i=>i.replace(t,e)).join("\n");if(m===d)return;const h=l.substring(0,a)+m+l.substring(r);i.value=h,i.selectionStart=a,i.selectionEnd=a+m.length,this.value=h,i.dispatchEvent(new Event("input",{bubbles:!0}))}))}insertLinePrefix(t,e=null){const i=this.textarea;i&&("write"!==this.mode&&(this.mode="write"),this.updateComplete.then(()=>{i.focus();const{selectionStart:s,selectionEnd:o,value:l}=i,a=l.lastIndexOf("\n",s-1)+1,n=l.indexOf("\n",o),r=-1===n?l.length:n,d=l.substring(a,r).split("\n"),m=d.filter(t=>t.length>0),h=m.length>0&&m.every(e=>e.startsWith(t)),c=d.map(i=>0===i.length?i:h?i.startsWith(t)?i.slice(t.length):i:i.startsWith(t)?i:e&&e.test(i)?i.replace(e,t):t+i).join("\n"),u=l.substring(0,a)+c+l.substring(r);i.value=u,i.selectionStart=a,i.selectionEnd=a+c.length,this.value=u,i.dispatchEvent(new Event("input",{bubbles:!0}))}))}get isEmpty(){return!(this.value||"").trim()}get renderedHtml(){const t={},e=this[h];if(e&&(t.allowedTags=e),this.scriptsEnabled){const e=new Set(a);e.delete("SCRIPT"),t.stripCompletely=e}return l(o(this.value||""),t)}get[h](){const t=(this.allowedTags||"").trim(),e=(this.disallowedTags||"").trim();if(t&&e&&console.warn("[k-markdown-editor] `allowed-tags` and `disallowed-tags` are mutually exclusive; using `allowed-tags`."),t)return"*"===t?{has:()=>!0}:new Set(t.split(",").map(t=>t.trim().toUpperCase()).filter(Boolean));if(e){const t=new Set(e.split(",").map(t=>t.trim().toUpperCase()).filter(Boolean));return{has:e=>!t.has(e)}}return null}[c]=()=>{const t=this.shadowRoot?.querySelector("textarea");this.required&&this.isEmpty?this.internals.setValidity({valueMissing:!0},"Please fill out this field.",t):this.internals.setValidity({})};handleInput=t=>{this.value=t.target.value,this.dispatchEvent(new CustomEvent("input",{detail:{value:this.value},bubbles:!0})),this[d]()};handleChange=()=>{this.dispatchEvent(new CustomEvent("change",{detail:{value:this.value},bubbles:!0}))};handleTabChange=t=>{const e=t.detail?.tab;e&&e!==this.mode&&(this.mode=e,this.dispatchEvent(new CustomEvent("mode-change",{detail:{mode:e},bubbles:!0})))};render(){const e=this.constructor.controlSets[this.controls]??this.constructor.controlSets[""];return t`
|
|
2
2
|
<k-resize dimension="height" ?disabled=${this.disabled}>
|
|
3
3
|
<div class="frame">
|
|
4
4
|
<k-tabs fixed-height active=${this.mode} @tab=${this.handleTabChange}>
|
|
@@ -18,6 +18,7 @@ import{html as t,css as e,nothing as i}from"../lit-all.min.js";import s from"./S
|
|
|
18
18
|
aria-label=${this.name||this.placeholder}
|
|
19
19
|
@input=${this.handleInput}
|
|
20
20
|
@blur=${this.handleChange}
|
|
21
|
+
@change=${this.handleChange}
|
|
21
22
|
></textarea>
|
|
22
23
|
</k-tab-content>
|
|
23
24
|
<k-tab-content name="preview">
|
|
@@ -165,4 +166,4 @@ import{html as t,css as e,nothing as i}from"../lit-all.min.js";import s from"./S
|
|
|
165
166
|
<k-md-bullet-list></k-md-bullet-list>
|
|
166
167
|
<k-md-numbered-list></k-md-numbered-list>
|
|
167
168
|
<k-md-speech-to-text></k-md-speech-to-text>
|
|
168
|
-
`,bottom:null}}}customElements.define("k-markdown-editor",
|
|
169
|
+
`,bottom:null}}}customElements.define("k-markdown-editor",u);
|
package/package.json
CHANGED
|
@@ -2,12 +2,17 @@ import { html, css, nothing } from '../lit-all.min.js';
|
|
|
2
2
|
import ShadowComponent from './ShadowComponent.js';
|
|
3
3
|
import renderMarkdown from '../utils/renderMarkdown.js';
|
|
4
4
|
import sanitizeHtml, { STRIP_COMPLETELY } from '../utils/sanitizeHtml.js';
|
|
5
|
+
import debounce from '../utils/debounce.js';
|
|
5
6
|
import './Resize.js';
|
|
6
7
|
import './Tabs.js';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const defaultValue = Symbol();
|
|
10
|
+
const debouncedChange = Symbol();
|
|
11
|
+
const controlsLoaded = Symbol();
|
|
12
|
+
const resolvedAllowedTags = Symbol();
|
|
13
|
+
const updateValidity = Symbol();
|
|
10
14
|
|
|
15
|
+
/*
|
|
11
16
|
Textarea that writes markdown, with a live "Preview" tab. Designed to mimic
|
|
12
17
|
GitHub's comment editor — write tab + preview tab + control slots above and
|
|
13
18
|
below the textarea. Markdown is parsed by snarkdown, which intentionally
|
|
@@ -17,10 +22,12 @@ import './Tabs.js';
|
|
|
17
22
|
Controls go in the `controls-top` and `controls-bottom` slots; subclass
|
|
18
23
|
MarkdownEditorControl (./MarkdownEditorControl.js) to write your own.
|
|
19
24
|
*/
|
|
20
|
-
|
|
21
25
|
export default class MarkdownEditor extends ShadowComponent {
|
|
22
26
|
static formAssociated = true;
|
|
23
27
|
|
|
28
|
+
/*
|
|
29
|
+
Reactive Properties / Attributes
|
|
30
|
+
*/
|
|
24
31
|
static properties = {
|
|
25
32
|
value: { type: String },
|
|
26
33
|
name: { type: String, reflect: true },
|
|
@@ -62,10 +69,8 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
62
69
|
controls: { type: String, reflect: true }
|
|
63
70
|
};
|
|
64
71
|
|
|
65
|
-
#defaultValue = '';
|
|
66
|
-
|
|
67
72
|
/*
|
|
68
|
-
|
|
73
|
+
Constructor
|
|
69
74
|
*/
|
|
70
75
|
constructor() {
|
|
71
76
|
super();
|
|
@@ -81,20 +86,17 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
81
86
|
this.disallowedTags = '';
|
|
82
87
|
this.scriptsEnabled = false;
|
|
83
88
|
this.controls = '';
|
|
84
|
-
this
|
|
89
|
+
this[defaultValue] = '';
|
|
90
|
+
this[controlsLoaded] = false;
|
|
91
|
+
this[debouncedChange] = debounce(() => this.handleChange(), 300);
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
#controlsLoaded = false;
|
|
88
|
-
|
|
89
94
|
/*
|
|
90
|
-
|
|
91
|
-
non-empty `controls` preset is set. Mirrors the HtmlEditor pattern so
|
|
92
|
-
consumers don't have to remember which control imports each preset
|
|
93
|
-
needs — they just set `controls="full"` and everything is loaded.
|
|
95
|
+
Lifecycle
|
|
94
96
|
*/
|
|
95
97
|
async loadControls() {
|
|
96
|
-
if(this
|
|
97
|
-
this
|
|
98
|
+
if(this[controlsLoaded]) return;
|
|
99
|
+
this[controlsLoaded] = true;
|
|
98
100
|
const base = new URL('./markdownEditorControls/', import.meta.url).href;
|
|
99
101
|
await Promise.all([
|
|
100
102
|
import(/* @vite-ignore */ `${base}Bold.js`),
|
|
@@ -117,14 +119,11 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
117
119
|
|
|
118
120
|
connectedCallback() {
|
|
119
121
|
super.connectedCallback();
|
|
120
|
-
// Initial markdown comes from the `value` attribute only. Don't fall back
|
|
121
|
-
// to children/textContent — slotted controls' own text (icon labels,
|
|
122
|
-
// separators, etc.) would leak in and pollute the value.
|
|
123
122
|
if(this.hasAttribute('value')){
|
|
124
|
-
this
|
|
123
|
+
this[defaultValue] = this.getAttribute('value');
|
|
125
124
|
}
|
|
126
|
-
if(!this.value && this
|
|
127
|
-
this.value = this
|
|
125
|
+
if(!this.value && this[defaultValue]){
|
|
126
|
+
this.value = this[defaultValue];
|
|
128
127
|
}
|
|
129
128
|
}
|
|
130
129
|
|
|
@@ -136,14 +135,11 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
136
135
|
if(changedProperties.has('controls') && this.controls && this.controls !== 'none'){
|
|
137
136
|
this.loadControls();
|
|
138
137
|
}
|
|
139
|
-
this
|
|
138
|
+
this[updateValidity]();
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
/*
|
|
143
|
-
Form-associated callbacks
|
|
144
|
-
*/
|
|
145
141
|
formResetCallback() {
|
|
146
|
-
this.value = this
|
|
142
|
+
this.value = this[defaultValue];
|
|
147
143
|
this.mode = 'write';
|
|
148
144
|
}
|
|
149
145
|
|
|
@@ -156,7 +152,7 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
156
152
|
}
|
|
157
153
|
|
|
158
154
|
/*
|
|
159
|
-
Public
|
|
155
|
+
Public Methods
|
|
160
156
|
*/
|
|
161
157
|
focus() {
|
|
162
158
|
if(this.mode !== 'write') this.mode = 'write';
|
|
@@ -181,11 +177,6 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
181
177
|
this.mode = this.mode === 'write' ? 'preview' : 'write';
|
|
182
178
|
}
|
|
183
179
|
|
|
184
|
-
/*
|
|
185
|
-
Public API — selection / text manipulation, used by MarkdownEditorControl
|
|
186
|
-
subclasses. All of these operate on the textarea (write mode); they
|
|
187
|
-
silently no-op in preview mode.
|
|
188
|
-
*/
|
|
189
180
|
get textarea() {
|
|
190
181
|
return this.shadowRoot?.querySelector('textarea') || null;
|
|
191
182
|
}
|
|
@@ -219,12 +210,6 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
219
210
|
});
|
|
220
211
|
}
|
|
221
212
|
|
|
222
|
-
/*
|
|
223
|
-
Wrap (or unwrap) the selection. If the selection is already wrapped with
|
|
224
|
-
the given markers — either the selection itself starts/ends with them, or
|
|
225
|
-
the surrounding characters are the markers — the wrap is removed instead
|
|
226
|
-
of being added again. This makes formatting buttons toggle.
|
|
227
|
-
*/
|
|
228
213
|
wrapSelection(prefix, suffix = prefix, placeholder = '') {
|
|
229
214
|
const ta = this.textarea;
|
|
230
215
|
if(!ta){
|
|
@@ -282,19 +267,6 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
282
267
|
this.replaceSelection(text, { selectInserted: false });
|
|
283
268
|
}
|
|
284
269
|
|
|
285
|
-
/*
|
|
286
|
-
Toggle a line-prefix on the selected lines (or the current line). If every
|
|
287
|
-
non-empty line in range already starts with `prefix`, the prefix is
|
|
288
|
-
removed; otherwise it's added. When `replacePattern` is provided, lines
|
|
289
|
-
that match it but do NOT yet have `prefix` get their match swapped out
|
|
290
|
-
rather than stacked — this is what lets headings switch levels (e.g.
|
|
291
|
-
`### Hello` becomes `## Hello` instead of `## ### Hello`).
|
|
292
|
-
*/
|
|
293
|
-
/*
|
|
294
|
-
Run a regex `.replace(pattern, replacement)` on every selected line
|
|
295
|
-
(or the current line if no selection). Useful for "convert to plain
|
|
296
|
-
paragraph" — strip any heading prefix without toggling.
|
|
297
|
-
*/
|
|
298
270
|
replaceInSelectedLines(pattern, replacement = '') {
|
|
299
271
|
const ta = this.textarea;
|
|
300
272
|
if(!ta) return;
|
|
@@ -349,7 +321,7 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
349
321
|
}
|
|
350
322
|
|
|
351
323
|
/*
|
|
352
|
-
|
|
324
|
+
Protected Members
|
|
353
325
|
*/
|
|
354
326
|
get isEmpty() {
|
|
355
327
|
return !(this.value || '').trim();
|
|
@@ -357,12 +329,9 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
357
329
|
|
|
358
330
|
get renderedHtml() {
|
|
359
331
|
const opts = {};
|
|
360
|
-
const resolved = this
|
|
332
|
+
const resolved = this[resolvedAllowedTags];
|
|
361
333
|
if(resolved) opts.allowedTags = resolved;
|
|
362
334
|
if(this.scriptsEnabled){
|
|
363
|
-
// Remove SCRIPT from the always-strip set so the allow/deny check
|
|
364
|
-
// gets the final say. SCRIPT is the only tag scripts-enabled affects;
|
|
365
|
-
// iframe/style/etc. stay nuked unconditionally.
|
|
366
335
|
const stripCompletely = new Set(STRIP_COMPLETELY);
|
|
367
336
|
stripCompletely.delete('SCRIPT');
|
|
368
337
|
opts.stripCompletely = stripCompletely;
|
|
@@ -370,19 +339,7 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
370
339
|
return sanitizeHtml(renderMarkdown(this.value || ''), opts);
|
|
371
340
|
}
|
|
372
341
|
|
|
373
|
-
|
|
374
|
-
Resolve `allowed-tags` / `disallowed-tags` into the Set sanitizeHtml
|
|
375
|
-
expects. The two attributes are mutually exclusive — if both are set,
|
|
376
|
-
`allowed-tags` wins and a console warning fires.
|
|
377
|
-
|
|
378
|
-
Returns:
|
|
379
|
-
- For `allowed-tags="*"` → proxy-Set that says yes to everything.
|
|
380
|
-
- For `allowed-tags="a,b,c"` → real Set of those uppercase names.
|
|
381
|
-
- For `disallowed-tags="x,y"` → proxy-Set that returns true for any
|
|
382
|
-
tag NOT in the deny list.
|
|
383
|
-
- For neither set → null (sanitizeHtml uses DEFAULT_TAGS).
|
|
384
|
-
*/
|
|
385
|
-
get #resolvedAllowedTags() {
|
|
342
|
+
get [resolvedAllowedTags]() {
|
|
386
343
|
const allow = (this.allowedTags || '').trim();
|
|
387
344
|
const deny = (this.disallowedTags || '').trim();
|
|
388
345
|
if(allow && deny){
|
|
@@ -399,7 +356,7 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
399
356
|
return null;
|
|
400
357
|
}
|
|
401
358
|
|
|
402
|
-
|
|
359
|
+
[updateValidity] = () => {
|
|
403
360
|
const ta = this.shadowRoot?.querySelector('textarea');
|
|
404
361
|
if(this.required && this.isEmpty){
|
|
405
362
|
this.internals.setValidity(
|
|
@@ -421,6 +378,7 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
421
378
|
detail: { value: this.value },
|
|
422
379
|
bubbles: true
|
|
423
380
|
}));
|
|
381
|
+
this[debouncedChange]();
|
|
424
382
|
};
|
|
425
383
|
|
|
426
384
|
handleChange = () => {
|
|
@@ -465,6 +423,7 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
465
423
|
aria-label=${this.name || this.placeholder}
|
|
466
424
|
@input=${this.handleInput}
|
|
467
425
|
@blur=${this.handleChange}
|
|
426
|
+
@change=${this.handleChange}
|
|
468
427
|
></textarea>
|
|
469
428
|
</k-tab-content>
|
|
470
429
|
<k-tab-content name="preview">
|
|
@@ -483,9 +442,6 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
483
442
|
`;
|
|
484
443
|
}
|
|
485
444
|
|
|
486
|
-
/*
|
|
487
|
-
Styles
|
|
488
|
-
*/
|
|
489
445
|
static styles = css`
|
|
490
446
|
:host {
|
|
491
447
|
--padding: 0.5rem 0.75rem;
|
|
@@ -576,8 +532,7 @@ export default class MarkdownEditor extends ShadowComponent {
|
|
|
576
532
|
attribute is set. Tags reference custom elements that are loaded by
|
|
577
533
|
`loadControls()` — listing them here doesn't require those modules to
|
|
578
534
|
be imported eagerly. Lit creates the elements as plain HTMLElements
|
|
579
|
-
until their definitions arrive, then the browser upgrades them in
|
|
580
|
-
place.
|
|
535
|
+
until their definitions arrive, then the browser upgrades them in place.
|
|
581
536
|
*/
|
|
582
537
|
static controlSets = {
|
|
583
538
|
'': { top: null, bottom: null },
|