@nectary/components 5.41.2 → 5.42.1

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": {
@@ -1549,7 +1528,8 @@ const regStrikethrough = new RegExp("(?<!\\\\)~~(?<strike>.+?)(?<!\\\\)~~");
1549
1528
  const regButtonPlaceholder = new RegExp("(?<!\\\\)\\[\\[(?<button>[a-zA-Z0-9_-]+)\\]\\]");
1550
1529
  const regLink = new RegExp("(?<!\\\\)!?\\[(?<linktext>[^\\]]*?)\\]\\((?<linkhref>[^)]+?)\\)(\\{(?<linkattrs>[^)]+?)\\})?");
1551
1530
  const regChip = new RegExp("(?<!\\\\)\\{\\{(?<chip>[a-zA-Z0-9_-]+)\\}\\}");
1552
- const regEmoji = new RegExp("(?<emoji>(?![0-9*#])\\p{Emoji})", "u");
1531
+ const regEmojiSeq = "(?![0-9#*])\\p{Emoji}(?:\\uFE0F|\\p{Emoji_Modifier})?\\u20E3?";
1532
+ const regEmoji = new RegExp(`(?<emoji>\\p{RI}\\p{RI}|${regEmojiSeq}(?:\\u200D${regEmojiSeq})*)`, "u");
1553
1533
  const regUList = /^(?<indent>[\t ]*?)[*+-][\t ]+(?<ultext>.*?)[\t ]*?$/;
1554
1534
  const regOList = /^(?<indent>[\t ]*?)\d+\.[\t ]+(?<oltext>.*?)[\t ]*?$/;
1555
1535
  const regEscapedChars = /\\(?<escaped>[\\\*_\[\]`~\{\}])/;
@@ -12237,11 +12217,18 @@ const assertListItem = ($n) => {
12237
12217
  throw new Error(`Node is not a ListItem: ${$n?.nodeName}`);
12238
12218
  }
12239
12219
  };
12220
+ const BLOCK_CLASSNAME = "block";
12240
12221
  const markListItemAsBlock = ($li) => {
12241
- $li.classList.add("block");
12222
+ $li.classList.add(BLOCK_CLASSNAME);
12242
12223
  };
12243
12224
  const isListItemMarkedArBlock = ($li) => {
12244
- return $li.classList.contains("block");
12225
+ return $li.classList.contains(BLOCK_CLASSNAME);
12226
+ };
12227
+ const markParagraphAsBlock = ($p) => {
12228
+ $p.classList.add(BLOCK_CLASSNAME);
12229
+ };
12230
+ const isParagraphMarkedAsBlock = ($p) => {
12231
+ return $p.classList.contains(BLOCK_CLASSNAME);
12245
12232
  };
12246
12233
  const MAX_LISTITEM_LEVEL = 4;
12247
12234
  const removeListItemLevel = ($li) => {
@@ -13758,6 +13745,7 @@ const MD_ULISTITEM_TOKEN = "*";
13758
13745
  const MD_OLISTITEM_TOKEN = "1.";
13759
13746
  const MD_LISTITEM_JOIN = "\n";
13760
13747
  const MD_PARAGRAPH_JOIN = "\n\n";
13748
+ const MD_SOFT_LINEBREAK = "\n";
13761
13749
  const serializeTextReducer = (state, desc, i, descArray) => {
13762
13750
  const { chunks } = state;
13763
13751
  if (desc.isLink === true) {
@@ -13879,7 +13867,7 @@ const serializeRoot = ($root, range) => {
13879
13867
  };
13880
13868
  const flushParagraphChunks = () => {
13881
13869
  if (paragraphChunks.length > 0) {
13882
- chunks.push(...paragraphChunks);
13870
+ chunks.push(paragraphChunks.join(MD_SOFT_LINEBREAK));
13883
13871
  paragraphChunks.length = 0;
13884
13872
  }
13885
13873
  };
@@ -13897,6 +13885,9 @@ const serializeRoot = ($root, range) => {
13897
13885
  } else {
13898
13886
  assertTextBlock($child);
13899
13887
  flushListChunks();
13888
+ if (isParagraph($child) && isParagraphMarkedAsBlock($child) && paragraphChunks.length > 0) {
13889
+ flushParagraphChunks();
13890
+ }
13900
13891
  paragraphChunks.push(
13901
13892
  serializeTextBlock($child, range)
13902
13893
  );
@@ -14013,8 +14004,10 @@ const createParseVisitor = (doc) => {
14013
14004
  },
14014
14005
  paragraph() {
14015
14006
  if (listsStack.length === 0) {
14016
- $currentBlock = createActuallyEmptyParagraph(doc);
14017
- $root.appendChild($currentBlock);
14007
+ const $p = createActuallyEmptyParagraph(doc);
14008
+ markParagraphAsBlock($p);
14009
+ $currentBlock = $p;
14010
+ $root.appendChild($p);
14018
14011
  }
14019
14012
  },
14020
14013
  end() {
@@ -14041,7 +14034,7 @@ const setBrowserCaret = ({ startContainer, startOffset, endContainer, endOffset
14041
14034
  range.setEnd(endContainer, endOffset);
14042
14035
  selection.addRange(range);
14043
14036
  };
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>';
14037
+ 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
14038
  const template$p = document.createElement("template");
14046
14039
  template$p.innerHTML = templateHTML$p;
14047
14040
  const SUPPORTS_SHADOW_SELECTION = typeof window.ShadowRoot.prototype.getSelection === "function";
@@ -14059,7 +14052,13 @@ class RichTextarea extends NectaryElement {
14059
14052
  #cachedRange = null;
14060
14053
  #lastSelectionInfo = null;
14061
14054
  #prevDispatchedValue = null;
14055
+ #changeDebounce = null;
14062
14056
  #parseVisitor;
14057
+ /* @internal
14058
+ * The chip resolver 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
+ * It is used to resolve the color and icon of a chip.
14061
+ */
14063
14062
  #chipResolver = null;
14064
14063
  constructor() {
14065
14064
  super();
@@ -14123,6 +14122,7 @@ class RichTextarea extends NectaryElement {
14123
14122
  }
14124
14123
  disconnectedCallback() {
14125
14124
  super.disconnectedCallback();
14125
+ this.#clearChangeDebounce();
14126
14126
  this.#controller.abort();
14127
14127
  this.#controller = null;
14128
14128
  }
@@ -14448,9 +14448,23 @@ class RichTextarea extends NectaryElement {
14448
14448
  if (result.range !== null) {
14449
14449
  setBrowserCaret(result.range);
14450
14450
  }
14451
+ this.#scheduleChangeDispatch();
14451
14452
  }
14452
14453
  this.#updateEditorEmptyClass();
14453
14454
  }
14455
+ #clearChangeDebounce() {
14456
+ if (this.#changeDebounce !== null) {
14457
+ clearTimeout(this.#changeDebounce);
14458
+ this.#changeDebounce = null;
14459
+ }
14460
+ }
14461
+ #scheduleChangeDispatch() {
14462
+ this.#clearChangeDebounce();
14463
+ this.#changeDebounce = setTimeout(() => {
14464
+ this.#changeDebounce = null;
14465
+ this.#dispatchChangeEvent();
14466
+ }, 100);
14467
+ }
14454
14468
  #onInputFocus = () => {
14455
14469
  if (this.#cachedRange !== null) {
14456
14470
  setBrowserCaret(this.#cachedRange);
@@ -14459,6 +14473,7 @@ class RichTextarea extends NectaryElement {
14459
14473
  };
14460
14474
  #onInputBlur = () => {
14461
14475
  this.dispatchEvent(new CustomEvent("-blur"));
14476
+ this.#clearChangeDebounce();
14462
14477
  this.#dispatchChangeEvent();
14463
14478
  };
14464
14479
  #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.1",
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() {
package/utils/markdown.js CHANGED
@@ -11,7 +11,8 @@ const regStrikethrough = new RegExp("(?<!\\\\)~~(?<strike>.+?)(?<!\\\\)~~");
11
11
  const regButtonPlaceholder = new RegExp("(?<!\\\\)\\[\\[(?<button>[a-zA-Z0-9_-]+)\\]\\]");
12
12
  const regLink = new RegExp("(?<!\\\\)!?\\[(?<linktext>[^\\]]*?)\\]\\((?<linkhref>[^)]+?)\\)(\\{(?<linkattrs>[^)]+?)\\})?");
13
13
  const regChip = new RegExp("(?<!\\\\)\\{\\{(?<chip>[a-zA-Z0-9_-]+)\\}\\}");
14
- const regEmoji = new RegExp("(?<emoji>(?![0-9*#])\\p{Emoji})", "u");
14
+ const regEmojiSeq = "(?![0-9#*])\\p{Emoji}(?:\\uFE0F|\\p{Emoji_Modifier})?\\u20E3?";
15
+ const regEmoji = new RegExp(`(?<emoji>\\p{RI}\\p{RI}|${regEmojiSeq}(?:\\u200D${regEmojiSeq})*)`, "u");
15
16
  const regUList = /^(?<indent>[\t ]*?)[*+-][\t ]+(?<ultext>.*?)[\t ]*?$/;
16
17
  const regOList = /^(?<indent>[\t ]*?)\d+\.[\t ]+(?<oltext>.*?)[\t ]*?$/;
17
18
  const regEscapedChars = /\\(?<escaped>[\\\*_\[\]`~\{\}])/;