@nectary/components 5.41.2 → 5.42.0

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/bundle.js CHANGED
@@ -291,40 +291,19 @@ class Icon extends NectaryElement {
291
291
  static get observedAttributes() {
292
292
  return ["name", "icons-version"];
293
293
  }
294
- /* Font class is now set before the text content. Covered by new regression tests.
295
-
296
- Before:
297
- 1. textContent = "fa-clone" Safari renders the text using the default font
298
- (Material Icons)
299
- 2. _matchNameToFont() → switches font-family to Sinch Icons Zero To D via
300
- class
301
-
302
- After:
303
- 1. _matchNameToFont() → sets the correct font-family class first
304
- 2. textContent = "fa-clone" → Safari renders the text using the correct font
305
- from the start
306
-
307
- Reasoning:
308
- * Safari caches the ligature layout from the first paint. When
309
- the text was set in Material Icons (which doesn't have these ligatures),
310
- Safari calculated glyph positions for individual characters. When the font
311
- then switched to Sinch Icons, Safari reused some of that cached layout
312
- instead of fully recalculating — shifting certain glyphs to the right.
313
- * _matchNameToFont() reads this.name via getAttribute(this,
314
- 'name'), which reads the HTML attribute — not textContent. The attribute is
315
- already set when the callback fires (that's what triggers the callback), so
316
- the method gets the correct name regardless of when textContent is set.
317
- */
294
+ // Font class is set before textContent to avoid a WebKit ligature caching
295
+ // bug: Safari caches glyph layout from the first paint, so if textContent
296
+ // lands while the default font (Material Icons) is active, the cached
297
+ // positions stick even after the font switches. Setting the class first
298
+ // ensures the correct font-family is active before any text renders.
318
299
  attributeChangedCallback(name, _, newVal) {
319
300
  switch (name) {
320
301
  case "name": {
321
- {
322
- this.#$icon.textContent = newVal;
323
- updateAttribute(this.#$icon, "aria-label", newVal);
324
- if (getAttribute(this, "icons-version", "1") !== "1") {
325
- this._matchNameToFont();
326
- }
302
+ if (getAttribute(this, "icons-version", "1") !== "1") {
303
+ this._matchNameToFont();
327
304
  }
305
+ this.#$icon.textContent = newVal;
306
+ updateAttribute(this.#$icon, "aria-label", newVal);
328
307
  break;
329
308
  }
330
309
  case "icons-version": {
@@ -12237,11 +12216,18 @@ const assertListItem = ($n) => {
12237
12216
  throw new Error(`Node is not a ListItem: ${$n?.nodeName}`);
12238
12217
  }
12239
12218
  };
12219
+ const BLOCK_CLASSNAME = "block";
12240
12220
  const markListItemAsBlock = ($li) => {
12241
- $li.classList.add("block");
12221
+ $li.classList.add(BLOCK_CLASSNAME);
12242
12222
  };
12243
12223
  const isListItemMarkedArBlock = ($li) => {
12244
- return $li.classList.contains("block");
12224
+ return $li.classList.contains(BLOCK_CLASSNAME);
12225
+ };
12226
+ const markParagraphAsBlock = ($p) => {
12227
+ $p.classList.add(BLOCK_CLASSNAME);
12228
+ };
12229
+ const isParagraphMarkedAsBlock = ($p) => {
12230
+ return $p.classList.contains(BLOCK_CLASSNAME);
12245
12231
  };
12246
12232
  const MAX_LISTITEM_LEVEL = 4;
12247
12233
  const removeListItemLevel = ($li) => {
@@ -13758,6 +13744,7 @@ const MD_ULISTITEM_TOKEN = "*";
13758
13744
  const MD_OLISTITEM_TOKEN = "1.";
13759
13745
  const MD_LISTITEM_JOIN = "\n";
13760
13746
  const MD_PARAGRAPH_JOIN = "\n\n";
13747
+ const MD_SOFT_LINEBREAK = "\n";
13761
13748
  const serializeTextReducer = (state, desc, i, descArray) => {
13762
13749
  const { chunks } = state;
13763
13750
  if (desc.isLink === true) {
@@ -13879,7 +13866,7 @@ const serializeRoot = ($root, range) => {
13879
13866
  };
13880
13867
  const flushParagraphChunks = () => {
13881
13868
  if (paragraphChunks.length > 0) {
13882
- chunks.push(...paragraphChunks);
13869
+ chunks.push(paragraphChunks.join(MD_SOFT_LINEBREAK));
13883
13870
  paragraphChunks.length = 0;
13884
13871
  }
13885
13872
  };
@@ -13897,6 +13884,9 @@ const serializeRoot = ($root, range) => {
13897
13884
  } else {
13898
13885
  assertTextBlock($child);
13899
13886
  flushListChunks();
13887
+ if (isParagraph($child) && isParagraphMarkedAsBlock($child) && paragraphChunks.length > 0) {
13888
+ flushParagraphChunks();
13889
+ }
13900
13890
  paragraphChunks.push(
13901
13891
  serializeTextBlock($child, range)
13902
13892
  );
@@ -14013,8 +14003,10 @@ const createParseVisitor = (doc) => {
14013
14003
  },
14014
14004
  paragraph() {
14015
14005
  if (listsStack.length === 0) {
14016
- $currentBlock = createActuallyEmptyParagraph(doc);
14017
- $root.appendChild($currentBlock);
14006
+ const $p = createActuallyEmptyParagraph(doc);
14007
+ markParagraphAsBlock($p);
14008
+ $currentBlock = $p;
14009
+ $root.appendChild($p);
14018
14010
  }
14019
14011
  },
14020
14012
  end() {
@@ -14041,7 +14033,7 @@ const setBrowserCaret = ({ startContainer, startOffset, endContainer, endOffset
14041
14033
  range.setEnd(endContainer, endOffset);
14042
14034
  selection.addRange(range);
14043
14035
  };
14044
- const templateHTML$p = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;position:relative;width:100%;box-sizing:border-box;background-color:var(--sinch-comp-textarea-color-default-background-initial);border-radius:var(--sinch-local-shape-radius);overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input-wrapper{position:relative;padding:8px 10px 8px 12px;box-sizing:border-box}#input{font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);white-space:pre-wrap;overflow-wrap:break-word;border:none;outline:0}#placeholder{display:none;position:absolute;left:0;top:0;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-placeholder);padding:8px 10px 8px 12px;pointer-events:none;user-select:none}#input.empty+#placeholder{display:block}#border{position:absolute;border:1px solid var(--sinch-comp-textarea-color-default-border-initial);border-radius:var(--sinch-local-shape-radius);inset:0;pointer-events:none}:host([invalid]) #border{border-color:var(--sinch-comp-textarea-color-invalid-border-initial)}:host([disabled]){color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}:host([disabled]) #border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}:host(:not([disabled])) #input-wrapper:focus-within~#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}.oli,.p,.uli{margin:0}.oli.l0,.uli.l0{margin-left:6px}.oli.l1,.uli.l1{margin-left:36px}.oli.l2,.uli.l2{margin-left:64px}.oli.l3,.uli.l3{margin-left:92px}.oli.l4,.uli.l4{margin-left:120px}.uli.l0{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli.l1{counter-reset:list-1 list-2 list-3 list-4}.uli.l2{counter-reset:list-2 list-3 list-4}.uli.l3{counter-reset:list-3 list-4}.uli.l4{counter-reset:list-4}.oli.l0{counter-reset:list-1 list-2 list-3 list-4}.oli.l1{counter-reset:list-2 list-3 list-4}.oli.l2{counter-reset:list-3 list-4}.oli.l3{counter-reset:list-4}.oli.l0::before{counter-increment:list-0;content:counter(list-0,decimal) ". "}.oli.l1::before{counter-increment:list-1;content:counter(list-1,lower-alpha) ". "}.oli.l2::before{counter-increment:list-2;content:counter(list-2,lower-roman) ". "}.oli.l3::before{counter-increment:list-3;content:counter(list-3,decimal) ". "}.oli.l4::before{counter-increment:list-4;content:counter(list-4,lower-alpha) ". "}.oli.block,.oli:first-of-type,.p+.oli{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli::before{content:"\\25CF";display:inline-block;width:16px}.oli+.p,.oli.block,.p+.oli,.p+.uli,.uli+.p,.uli.block{margin-top:.5em}.c{font:var(--sinch-comp-code-tag-font-text);font-size:inherit;line-height:inherit;color:var(--sinch-comp-code-tag-color-default-text-initial);border:1px solid var(--sinch-comp-code-tag-color-default-border-initial);background-color:var(--sinch-comp-code-tag-color-default-background-initial);padding:0 .25em;border-radius:var(--sinch-comp-code-tag-shape-radius)}.l{font:var(--sinch-comp-link-default-font-initial);color:var(--sinch-comp-link-color-default-text-initial);text-decoration:underline}sinch-rich-textarea-chip{display:inline-flex;vertical-align:middle;user-select:none}.i{font-style:italic}.b{font-weight:700}.s{text-decoration:line-through}.e{background-repeat:no-repeat;background-position:50% 50%;background-size:contain;width:1em;height:1em;vertical-align:-.2em}#top-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:4px 4px 0}#top-wrapper.empty{display:none}#bottom-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:0 4px 4px}#bottom-wrapper.empty{display:none}</style><div id="wrapper"><div id="top-wrapper"><slot id="top" name="top"></slot></div><div id="input-wrapper"><div id="input" contenteditable="true" role="textbox" aria-multiline="true" suppresscontenteditablewarning autocapitalize="false" autocorrect="false" autosave="false" spellcheck="false"></div><div id="placeholder"></div></div><div id="border"></div><div id="bottom-wrapper"><slot id="bottom" name="bottom"></slot></div></div>';
14036
+ const templateHTML$p = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;position:relative;width:100%;box-sizing:border-box;background-color:var(--sinch-comp-textarea-color-default-background-initial);border-radius:var(--sinch-local-shape-radius);overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input-wrapper{position:relative;padding:8px 10px 8px 12px;box-sizing:border-box}#input{font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);caret-color:var(--sinch-comp-textarea-color-default-text-initial);white-space:pre-wrap;overflow-wrap:break-word;border:none;outline:0}#placeholder{display:none;position:absolute;left:0;top:0;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-placeholder);padding:8px 10px 8px 12px;pointer-events:none;user-select:none}#input.empty+#placeholder{display:block}#border{position:absolute;border:1px solid var(--sinch-comp-textarea-color-default-border-initial);border-radius:var(--sinch-local-shape-radius);inset:0;pointer-events:none}:host([invalid]) #border{border-color:var(--sinch-comp-textarea-color-invalid-border-initial)}:host([disabled]){color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}:host([disabled]) #border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}:host(:not([disabled])) #input-wrapper:focus-within~#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}.oli,.p,.uli{margin:0}.oli.l0,.uli.l0{margin-left:6px}.oli.l1,.uli.l1{margin-left:36px}.oli.l2,.uli.l2{margin-left:64px}.oli.l3,.uli.l3{margin-left:92px}.oli.l4,.uli.l4{margin-left:120px}.uli.l0{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli.l1{counter-reset:list-1 list-2 list-3 list-4}.uli.l2{counter-reset:list-2 list-3 list-4}.uli.l3{counter-reset:list-3 list-4}.uli.l4{counter-reset:list-4}.oli.l0{counter-reset:list-1 list-2 list-3 list-4}.oli.l1{counter-reset:list-2 list-3 list-4}.oli.l2{counter-reset:list-3 list-4}.oli.l3{counter-reset:list-4}.oli.l0::before{counter-increment:list-0;content:counter(list-0,decimal) ". "}.oli.l1::before{counter-increment:list-1;content:counter(list-1,lower-alpha) ". "}.oli.l2::before{counter-increment:list-2;content:counter(list-2,lower-roman) ". "}.oli.l3::before{counter-increment:list-3;content:counter(list-3,decimal) ". "}.oli.l4::before{counter-increment:list-4;content:counter(list-4,lower-alpha) ". "}.oli.block,.oli:first-of-type,.p+.oli{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli::before{content:"\\25CF";display:inline-block;width:16px}.oli+.p,.oli.block,.p+.oli,.p+.uli,.uli+.p,.uli.block{margin-top:.5em}.c{font:var(--sinch-comp-code-tag-font-text);font-size:inherit;line-height:inherit;color:var(--sinch-comp-code-tag-color-default-text-initial);border:1px solid var(--sinch-comp-code-tag-color-default-border-initial);background-color:var(--sinch-comp-code-tag-color-default-background-initial);padding:0 .25em;border-radius:var(--sinch-comp-code-tag-shape-radius)}.l{font:var(--sinch-comp-link-default-font-initial);color:var(--sinch-comp-link-color-default-text-initial);text-decoration:underline}sinch-rich-textarea-chip{display:inline-flex;vertical-align:middle;user-select:none}.i{font-style:italic}.b{font-weight:700}.s{text-decoration:line-through}.e{background-repeat:no-repeat;background-position:50% 50%;background-size:contain;width:1em;height:1em;vertical-align:-.2em}#top-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:4px 4px 0}#top-wrapper.empty{display:none}#bottom-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:0 4px 4px}#bottom-wrapper.empty{display:none}</style><div id="wrapper"><div id="top-wrapper"><slot id="top" name="top"></slot></div><div id="input-wrapper"><div id="input" contenteditable="true" role="textbox" aria-multiline="true" suppresscontenteditablewarning autocapitalize="false" autocorrect="false" autosave="false" spellcheck="false"></div><div id="placeholder"></div></div><div id="border"></div><div id="bottom-wrapper"><slot id="bottom" name="bottom"></slot></div></div>';
14045
14037
  const template$p = document.createElement("template");
14046
14038
  template$p.innerHTML = templateHTML$p;
14047
14039
  const SUPPORTS_SHADOW_SELECTION = typeof window.ShadowRoot.prototype.getSelection === "function";
@@ -14059,7 +14051,13 @@ class RichTextarea extends NectaryElement {
14059
14051
  #cachedRange = null;
14060
14052
  #lastSelectionInfo = null;
14061
14053
  #prevDispatchedValue = null;
14054
+ #changeDebounce = null;
14062
14055
  #parseVisitor;
14056
+ /* @internal
14057
+ * The chip resolver is used to resolve the color and icon of a chip.
14058
+ * It is used to resolve the color and icon of a chip.
14059
+ * It is used to resolve the color and icon of a chip.
14060
+ */
14063
14061
  #chipResolver = null;
14064
14062
  constructor() {
14065
14063
  super();
@@ -14123,6 +14121,7 @@ class RichTextarea extends NectaryElement {
14123
14121
  }
14124
14122
  disconnectedCallback() {
14125
14123
  super.disconnectedCallback();
14124
+ this.#clearChangeDebounce();
14126
14125
  this.#controller.abort();
14127
14126
  this.#controller = null;
14128
14127
  }
@@ -14448,9 +14447,23 @@ class RichTextarea extends NectaryElement {
14448
14447
  if (result.range !== null) {
14449
14448
  setBrowserCaret(result.range);
14450
14449
  }
14450
+ this.#scheduleChangeDispatch();
14451
14451
  }
14452
14452
  this.#updateEditorEmptyClass();
14453
14453
  }
14454
+ #clearChangeDebounce() {
14455
+ if (this.#changeDebounce !== null) {
14456
+ clearTimeout(this.#changeDebounce);
14457
+ this.#changeDebounce = null;
14458
+ }
14459
+ }
14460
+ #scheduleChangeDispatch() {
14461
+ this.#clearChangeDebounce();
14462
+ this.#changeDebounce = setTimeout(() => {
14463
+ this.#changeDebounce = null;
14464
+ this.#dispatchChangeEvent();
14465
+ }, 100);
14466
+ }
14454
14467
  #onInputFocus = () => {
14455
14468
  if (this.#cachedRange !== null) {
14456
14469
  setBrowserCaret(this.#cachedRange);
@@ -14459,6 +14472,7 @@ class RichTextarea extends NectaryElement {
14459
14472
  };
14460
14473
  #onInputBlur = () => {
14461
14474
  this.dispatchEvent(new CustomEvent("-blur"));
14475
+ this.#clearChangeDebounce();
14462
14476
  this.#dispatchChangeEvent();
14463
14477
  };
14464
14478
  #dispatchChangeEvent() {
package/icon/index.js CHANGED
@@ -14,40 +14,19 @@ class Icon extends NectaryElement {
14
14
  static get observedAttributes() {
15
15
  return ["name", "icons-version"];
16
16
  }
17
- /* Font class is now set before the text content. Covered by new regression tests.
18
-
19
- Before:
20
- 1. textContent = "fa-clone" Safari renders the text using the default font
21
- (Material Icons)
22
- 2. _matchNameToFont() → switches font-family to Sinch Icons Zero To D via
23
- class
24
-
25
- After:
26
- 1. _matchNameToFont() → sets the correct font-family class first
27
- 2. textContent = "fa-clone" → Safari renders the text using the correct font
28
- from the start
29
-
30
- Reasoning:
31
- * Safari caches the ligature layout from the first paint. When
32
- the text was set in Material Icons (which doesn't have these ligatures),
33
- Safari calculated glyph positions for individual characters. When the font
34
- then switched to Sinch Icons, Safari reused some of that cached layout
35
- instead of fully recalculating — shifting certain glyphs to the right.
36
- * _matchNameToFont() reads this.name via getAttribute(this,
37
- 'name'), which reads the HTML attribute — not textContent. The attribute is
38
- already set when the callback fires (that's what triggers the callback), so
39
- the method gets the correct name regardless of when textContent is set.
40
- */
17
+ // Font class is set before textContent to avoid a WebKit ligature caching
18
+ // bug: Safari caches glyph layout from the first paint, so if textContent
19
+ // lands while the default font (Material Icons) is active, the cached
20
+ // positions stick even after the font switches. Setting the class first
21
+ // ensures the correct font-family is active before any text renders.
41
22
  attributeChangedCallback(name, _, newVal) {
42
23
  switch (name) {
43
24
  case "name": {
44
- {
45
- this.#$icon.textContent = newVal;
46
- updateAttribute(this.#$icon, "aria-label", newVal);
47
- if (getAttribute(this, "icons-version", "1") !== "1") {
48
- this._matchNameToFont();
49
- }
25
+ if (getAttribute(this, "icons-version", "1") !== "1") {
26
+ this._matchNameToFont();
50
27
  }
28
+ this.#$icon.textContent = newVal;
29
+ updateAttribute(this.#$icon, "aria-label", newVal);
51
30
  break;
52
31
  }
53
32
  case "icons-version": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "5.41.2",
3
+ "version": "5.42.0",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
@@ -6,7 +6,7 @@ import { isElementFocused } from "../utils/slot.js";
6
6
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
7
7
  import { parseMarkdown } from "../utils/markdown.js";
8
8
  import { createParseVisitor, getEndRange, formatList, handleEmojiMousedown, removeChip, setBrowserCaret, formatOutdent, formatIndent, formatInline, insertChip, insertLink, insertText, insertLineBreak, deleteContentBackward, serializeMarkdown, getSelectionInfo, isSelectionEqual, insertFromPaste, isEditorEmpty } from "./utils.js";
9
- const templateHTML = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;position:relative;width:100%;box-sizing:border-box;background-color:var(--sinch-comp-textarea-color-default-background-initial);border-radius:var(--sinch-local-shape-radius);overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input-wrapper{position:relative;padding:8px 10px 8px 12px;box-sizing:border-box}#input{font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);white-space:pre-wrap;overflow-wrap:break-word;border:none;outline:0}#placeholder{display:none;position:absolute;left:0;top:0;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-placeholder);padding:8px 10px 8px 12px;pointer-events:none;user-select:none}#input.empty+#placeholder{display:block}#border{position:absolute;border:1px solid var(--sinch-comp-textarea-color-default-border-initial);border-radius:var(--sinch-local-shape-radius);inset:0;pointer-events:none}:host([invalid]) #border{border-color:var(--sinch-comp-textarea-color-invalid-border-initial)}:host([disabled]){color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}:host([disabled]) #border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}:host(:not([disabled])) #input-wrapper:focus-within~#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}.oli,.p,.uli{margin:0}.oli.l0,.uli.l0{margin-left:6px}.oli.l1,.uli.l1{margin-left:36px}.oli.l2,.uli.l2{margin-left:64px}.oli.l3,.uli.l3{margin-left:92px}.oli.l4,.uli.l4{margin-left:120px}.uli.l0{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli.l1{counter-reset:list-1 list-2 list-3 list-4}.uli.l2{counter-reset:list-2 list-3 list-4}.uli.l3{counter-reset:list-3 list-4}.uli.l4{counter-reset:list-4}.oli.l0{counter-reset:list-1 list-2 list-3 list-4}.oli.l1{counter-reset:list-2 list-3 list-4}.oli.l2{counter-reset:list-3 list-4}.oli.l3{counter-reset:list-4}.oli.l0::before{counter-increment:list-0;content:counter(list-0,decimal) ". "}.oli.l1::before{counter-increment:list-1;content:counter(list-1,lower-alpha) ". "}.oli.l2::before{counter-increment:list-2;content:counter(list-2,lower-roman) ". "}.oli.l3::before{counter-increment:list-3;content:counter(list-3,decimal) ". "}.oli.l4::before{counter-increment:list-4;content:counter(list-4,lower-alpha) ". "}.oli.block,.oli:first-of-type,.p+.oli{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli::before{content:"\\25CF";display:inline-block;width:16px}.oli+.p,.oli.block,.p+.oli,.p+.uli,.uli+.p,.uli.block{margin-top:.5em}.c{font:var(--sinch-comp-code-tag-font-text);font-size:inherit;line-height:inherit;color:var(--sinch-comp-code-tag-color-default-text-initial);border:1px solid var(--sinch-comp-code-tag-color-default-border-initial);background-color:var(--sinch-comp-code-tag-color-default-background-initial);padding:0 .25em;border-radius:var(--sinch-comp-code-tag-shape-radius)}.l{font:var(--sinch-comp-link-default-font-initial);color:var(--sinch-comp-link-color-default-text-initial);text-decoration:underline}sinch-rich-textarea-chip{display:inline-flex;vertical-align:middle;user-select:none}.i{font-style:italic}.b{font-weight:700}.s{text-decoration:line-through}.e{background-repeat:no-repeat;background-position:50% 50%;background-size:contain;width:1em;height:1em;vertical-align:-.2em}#top-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:4px 4px 0}#top-wrapper.empty{display:none}#bottom-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:0 4px 4px}#bottom-wrapper.empty{display:none}</style><div id="wrapper"><div id="top-wrapper"><slot id="top" name="top"></slot></div><div id="input-wrapper"><div id="input" contenteditable="true" role="textbox" aria-multiline="true" suppresscontenteditablewarning autocapitalize="false" autocorrect="false" autosave="false" spellcheck="false"></div><div id="placeholder"></div></div><div id="border"></div><div id="bottom-wrapper"><slot id="bottom" name="bottom"></slot></div></div>';
9
+ const templateHTML = '<style>:host{display:block}#wrapper{display:flex;flex-direction:column;position:relative;width:100%;box-sizing:border-box;background-color:var(--sinch-comp-textarea-color-default-background-initial);border-radius:var(--sinch-local-shape-radius);overflow:hidden;--sinch-local-shape-radius:var(--sinch-comp-textarea-shape-radius)}#input-wrapper{position:relative;padding:8px 10px 8px 12px;box-sizing:border-box}#input{font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-initial);caret-color:var(--sinch-comp-textarea-color-default-text-initial);white-space:pre-wrap;overflow-wrap:break-word;border:none;outline:0}#placeholder{display:none;position:absolute;left:0;top:0;font:var(--sinch-comp-textarea-font-input);color:var(--sinch-comp-textarea-color-default-text-placeholder);padding:8px 10px 8px 12px;pointer-events:none;user-select:none}#input.empty+#placeholder{display:block}#border{position:absolute;border:1px solid var(--sinch-comp-textarea-color-default-border-initial);border-radius:var(--sinch-local-shape-radius);inset:0;pointer-events:none}:host([invalid]) #border{border-color:var(--sinch-comp-textarea-color-invalid-border-initial)}:host([disabled]){color:var(--sinch-comp-textarea-color-disabled-text-initial);-webkit-text-fill-color:var(--sinch-comp-textarea-color-disabled-text-initial)}:host([disabled]) #border{border-color:var(--sinch-comp-textarea-color-disabled-border-initial)}:host(:not([disabled])) #input-wrapper:focus-within~#border{border-color:var(--sinch-comp-textarea-color-default-border-focus);border-width:2px}.oli,.p,.uli{margin:0}.oli.l0,.uli.l0{margin-left:6px}.oli.l1,.uli.l1{margin-left:36px}.oli.l2,.uli.l2{margin-left:64px}.oli.l3,.uli.l3{margin-left:92px}.oli.l4,.uli.l4{margin-left:120px}.uli.l0{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli.l1{counter-reset:list-1 list-2 list-3 list-4}.uli.l2{counter-reset:list-2 list-3 list-4}.uli.l3{counter-reset:list-3 list-4}.uli.l4{counter-reset:list-4}.oli.l0{counter-reset:list-1 list-2 list-3 list-4}.oli.l1{counter-reset:list-2 list-3 list-4}.oli.l2{counter-reset:list-3 list-4}.oli.l3{counter-reset:list-4}.oli.l0::before{counter-increment:list-0;content:counter(list-0,decimal) ". "}.oli.l1::before{counter-increment:list-1;content:counter(list-1,lower-alpha) ". "}.oli.l2::before{counter-increment:list-2;content:counter(list-2,lower-roman) ". "}.oli.l3::before{counter-increment:list-3;content:counter(list-3,decimal) ". "}.oli.l4::before{counter-increment:list-4;content:counter(list-4,lower-alpha) ". "}.oli.block,.oli:first-of-type,.p+.oli{counter-reset:list-0 list-1 list-2 list-3 list-4}.uli::before{content:"\\25CF";display:inline-block;width:16px}.oli+.p,.oli.block,.p+.oli,.p+.uli,.uli+.p,.uli.block{margin-top:.5em}.c{font:var(--sinch-comp-code-tag-font-text);font-size:inherit;line-height:inherit;color:var(--sinch-comp-code-tag-color-default-text-initial);border:1px solid var(--sinch-comp-code-tag-color-default-border-initial);background-color:var(--sinch-comp-code-tag-color-default-background-initial);padding:0 .25em;border-radius:var(--sinch-comp-code-tag-shape-radius)}.l{font:var(--sinch-comp-link-default-font-initial);color:var(--sinch-comp-link-color-default-text-initial);text-decoration:underline}sinch-rich-textarea-chip{display:inline-flex;vertical-align:middle;user-select:none}.i{font-style:italic}.b{font-weight:700}.s{text-decoration:line-through}.e{background-repeat:no-repeat;background-position:50% 50%;background-size:contain;width:1em;height:1em;vertical-align:-.2em}#top-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:4px 4px 0}#top-wrapper.empty{display:none}#bottom-wrapper{display:flex;flex-direction:row;align-items:center;gap:8px;padding:0 4px 4px}#bottom-wrapper.empty{display:none}</style><div id="wrapper"><div id="top-wrapper"><slot id="top" name="top"></slot></div><div id="input-wrapper"><div id="input" contenteditable="true" role="textbox" aria-multiline="true" suppresscontenteditablewarning autocapitalize="false" autocorrect="false" autosave="false" spellcheck="false"></div><div id="placeholder"></div></div><div id="border"></div><div id="bottom-wrapper"><slot id="bottom" name="bottom"></slot></div></div>';
10
10
  const template = document.createElement("template");
11
11
  template.innerHTML = templateHTML;
12
12
  const SUPPORTS_SHADOW_SELECTION = typeof window.ShadowRoot.prototype.getSelection === "function";
@@ -24,7 +24,13 @@ class RichTextarea extends NectaryElement {
24
24
  #cachedRange = null;
25
25
  #lastSelectionInfo = null;
26
26
  #prevDispatchedValue = null;
27
+ #changeDebounce = null;
27
28
  #parseVisitor;
29
+ /* @internal
30
+ * The chip resolver is used to resolve the color and icon of a chip.
31
+ * It is used to resolve the color and icon of a chip.
32
+ * It is used to resolve the color and icon of a chip.
33
+ */
28
34
  #chipResolver = null;
29
35
  constructor() {
30
36
  super();
@@ -88,6 +94,7 @@ class RichTextarea extends NectaryElement {
88
94
  }
89
95
  disconnectedCallback() {
90
96
  super.disconnectedCallback();
97
+ this.#clearChangeDebounce();
91
98
  this.#controller.abort();
92
99
  this.#controller = null;
93
100
  }
@@ -413,9 +420,23 @@ class RichTextarea extends NectaryElement {
413
420
  if (result.range !== null) {
414
421
  setBrowserCaret(result.range);
415
422
  }
423
+ this.#scheduleChangeDispatch();
416
424
  }
417
425
  this.#updateEditorEmptyClass();
418
426
  }
427
+ #clearChangeDebounce() {
428
+ if (this.#changeDebounce !== null) {
429
+ clearTimeout(this.#changeDebounce);
430
+ this.#changeDebounce = null;
431
+ }
432
+ }
433
+ #scheduleChangeDispatch() {
434
+ this.#clearChangeDebounce();
435
+ this.#changeDebounce = setTimeout(() => {
436
+ this.#changeDebounce = null;
437
+ this.#dispatchChangeEvent();
438
+ }, 100);
439
+ }
419
440
  #onInputFocus = () => {
420
441
  if (this.#cachedRange !== null) {
421
442
  setBrowserCaret(this.#cachedRange);
@@ -424,6 +445,7 @@ class RichTextarea extends NectaryElement {
424
445
  };
425
446
  #onInputBlur = () => {
426
447
  this.dispatchEvent(new CustomEvent("-blur"));
448
+ this.#clearChangeDebounce();
427
449
  this.#dispatchChangeEvent();
428
450
  };
429
451
  #dispatchChangeEvent() {
@@ -24,6 +24,7 @@ export type TSinchRichTextareaProps = {
24
24
  'chip-color'?: TSinchTagColor;
25
25
  /** Default icon for chips/tags */
26
26
  'chip-icon'?: string;
27
+ /** Accessible label for the rich textarea */
27
28
  'aria-label': string;
28
29
  };
29
30
  export type TSinchRichTextareaMethods = {
@@ -44,7 +45,7 @@ export type TSinchRichTextareaMethods = {
44
45
  chipResolver: TChipResolver | null;
45
46
  };
46
47
  export type TSinchRichTextareaEvents = {
47
- /** Change value handler */
48
+ /** Change value handler — fires while editing (debounced) and on blur */
48
49
  '-change'?: (e: CustomEvent<string>) => void;
49
50
  /** Focus handler */
50
51
  '-focus'?: (e: CustomEvent<void>) => void;
@@ -55,11 +55,18 @@ const assertListItem = ($n) => {
55
55
  throw new Error(`Node is not a ListItem: ${$n?.nodeName}`);
56
56
  }
57
57
  };
58
+ const BLOCK_CLASSNAME = "block";
58
59
  const markListItemAsBlock = ($li) => {
59
- $li.classList.add("block");
60
+ $li.classList.add(BLOCK_CLASSNAME);
60
61
  };
61
62
  const isListItemMarkedArBlock = ($li) => {
62
- return $li.classList.contains("block");
63
+ return $li.classList.contains(BLOCK_CLASSNAME);
64
+ };
65
+ const markParagraphAsBlock = ($p) => {
66
+ $p.classList.add(BLOCK_CLASSNAME);
67
+ };
68
+ const isParagraphMarkedAsBlock = ($p) => {
69
+ return $p.classList.contains(BLOCK_CLASSNAME);
63
70
  };
64
71
  const MAX_LISTITEM_LEVEL = 4;
65
72
  const removeListItemLevel = ($li) => {
@@ -1576,6 +1583,7 @@ const MD_ULISTITEM_TOKEN = "*";
1576
1583
  const MD_OLISTITEM_TOKEN = "1.";
1577
1584
  const MD_LISTITEM_JOIN = "\n";
1578
1585
  const MD_PARAGRAPH_JOIN = "\n\n";
1586
+ const MD_SOFT_LINEBREAK = "\n";
1579
1587
  const serializeTextReducer = (state, desc, i, descArray) => {
1580
1588
  const { chunks } = state;
1581
1589
  if (desc.isLink === true) {
@@ -1697,7 +1705,7 @@ const serializeRoot = ($root, range) => {
1697
1705
  };
1698
1706
  const flushParagraphChunks = () => {
1699
1707
  if (paragraphChunks.length > 0) {
1700
- chunks.push(...paragraphChunks);
1708
+ chunks.push(paragraphChunks.join(MD_SOFT_LINEBREAK));
1701
1709
  paragraphChunks.length = 0;
1702
1710
  }
1703
1711
  };
@@ -1715,6 +1723,9 @@ const serializeRoot = ($root, range) => {
1715
1723
  } else {
1716
1724
  assertTextBlock($child);
1717
1725
  flushListChunks();
1726
+ if (isParagraph($child) && isParagraphMarkedAsBlock($child) && paragraphChunks.length > 0) {
1727
+ flushParagraphChunks();
1728
+ }
1718
1729
  paragraphChunks.push(
1719
1730
  serializeTextBlock($child, range)
1720
1731
  );
@@ -1831,8 +1842,10 @@ const createParseVisitor = (doc) => {
1831
1842
  },
1832
1843
  paragraph() {
1833
1844
  if (listsStack.length === 0) {
1834
- $currentBlock = createActuallyEmptyParagraph(doc);
1835
- $root.appendChild($currentBlock);
1845
+ const $p = createActuallyEmptyParagraph(doc);
1846
+ markParagraphAsBlock($p);
1847
+ $currentBlock = $p;
1848
+ $root.appendChild($p);
1836
1849
  }
1837
1850
  },
1838
1851
  end() {