@nectary/components 5.41.1 → 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 +62 -43
- package/floating-panel/index.js +12 -7
- package/icon/index.js +9 -30
- package/package.json +1 -1
- package/rich-textarea/index.js +23 -1
- package/rich-textarea/types.d.ts +2 -1
- package/rich-textarea/utils.js +18 -5
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
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": {
|
|
@@ -9032,6 +9011,7 @@ const applyCountPlaceholder = (template2, count) => {
|
|
|
9032
9011
|
return template2.replaceAll("{count}", count);
|
|
9033
9012
|
};
|
|
9034
9013
|
const sanitizeAttr = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
9014
|
+
const toReactCamelCaseHandlerName = (eventSuffix) => `on${eventSuffix.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
|
|
9035
9015
|
const isValidAction = (value) => {
|
|
9036
9016
|
if (typeof value !== "object" || value === null) {
|
|
9037
9017
|
return false;
|
|
@@ -9102,9 +9082,9 @@ class FloatingPanel extends NectaryElement {
|
|
|
9102
9082
|
this.#resizeThrottle?.fn();
|
|
9103
9083
|
});
|
|
9104
9084
|
this.#resizeObserver.observe(this.#$dialog);
|
|
9105
|
-
this.addEventListener("-close", this.#
|
|
9106
|
-
this.addEventListener("-select-all", this.#
|
|
9107
|
-
this.addEventListener("-action", this.#
|
|
9085
|
+
this.addEventListener("-close", this.#onCloseReactHandler, { signal });
|
|
9086
|
+
this.addEventListener("-select-all", this.#onSelectAllReactHandler, { signal });
|
|
9087
|
+
this.addEventListener("-action", this.#onActionReactHandler, { signal });
|
|
9108
9088
|
if (!this.hasAttribute("actions")) {
|
|
9109
9089
|
this.#renderActions(null);
|
|
9110
9090
|
}
|
|
@@ -9305,7 +9285,7 @@ class FloatingPanel extends NectaryElement {
|
|
|
9305
9285
|
* `-${action}` convenience event. The per-action event lets consumers bind
|
|
9306
9286
|
* directly to a specific action (e.g. `on-archive`) without switching on
|
|
9307
9287
|
* `e.detail` — and it works for any user-defined action, not just the
|
|
9308
|
-
* built-in defaults. React handlers are forwarded by `#
|
|
9288
|
+
* built-in defaults. React handlers are forwarded by `#onActionReactHandler`.
|
|
9309
9289
|
*/
|
|
9310
9290
|
#dispatchAction(action) {
|
|
9311
9291
|
this.dispatchEvent(new CustomEvent("-action", { detail: action }));
|
|
@@ -9411,16 +9391,20 @@ class FloatingPanel extends NectaryElement {
|
|
|
9411
9391
|
#onActionsPopoverClose = () => {
|
|
9412
9392
|
this.#closeOverflowMenu();
|
|
9413
9393
|
};
|
|
9414
|
-
#
|
|
9394
|
+
#onCloseReactHandler = (e) => {
|
|
9415
9395
|
getReactEventHandler(this, "on-close")?.(e);
|
|
9396
|
+
getReactEventHandler(this, "onClose")?.(e);
|
|
9416
9397
|
};
|
|
9417
|
-
#
|
|
9398
|
+
#onSelectAllReactHandler = (e) => {
|
|
9418
9399
|
getReactEventHandler(this, "on-select-all")?.(e);
|
|
9400
|
+
getReactEventHandler(this, "onSelectAll")?.(e);
|
|
9419
9401
|
};
|
|
9420
|
-
#
|
|
9402
|
+
#onActionReactHandler = (e) => {
|
|
9421
9403
|
getReactEventHandler(this, "on-action")?.(e);
|
|
9404
|
+
getReactEventHandler(this, "onAction")?.(e);
|
|
9422
9405
|
if (e instanceof CustomEvent && typeof e.detail === "string") {
|
|
9423
9406
|
getReactEventHandler(this, `on-${e.detail}`)?.(e);
|
|
9407
|
+
getReactEventHandler(this, toReactCamelCaseHandlerName(e.detail))?.(e);
|
|
9424
9408
|
}
|
|
9425
9409
|
};
|
|
9426
9410
|
get panelRect() {
|
|
@@ -12232,11 +12216,18 @@ const assertListItem = ($n) => {
|
|
|
12232
12216
|
throw new Error(`Node is not a ListItem: ${$n?.nodeName}`);
|
|
12233
12217
|
}
|
|
12234
12218
|
};
|
|
12219
|
+
const BLOCK_CLASSNAME = "block";
|
|
12235
12220
|
const markListItemAsBlock = ($li) => {
|
|
12236
|
-
$li.classList.add(
|
|
12221
|
+
$li.classList.add(BLOCK_CLASSNAME);
|
|
12237
12222
|
};
|
|
12238
12223
|
const isListItemMarkedArBlock = ($li) => {
|
|
12239
|
-
return $li.classList.contains(
|
|
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);
|
|
12240
12231
|
};
|
|
12241
12232
|
const MAX_LISTITEM_LEVEL = 4;
|
|
12242
12233
|
const removeListItemLevel = ($li) => {
|
|
@@ -13753,6 +13744,7 @@ const MD_ULISTITEM_TOKEN = "*";
|
|
|
13753
13744
|
const MD_OLISTITEM_TOKEN = "1.";
|
|
13754
13745
|
const MD_LISTITEM_JOIN = "\n";
|
|
13755
13746
|
const MD_PARAGRAPH_JOIN = "\n\n";
|
|
13747
|
+
const MD_SOFT_LINEBREAK = "\n";
|
|
13756
13748
|
const serializeTextReducer = (state, desc, i, descArray) => {
|
|
13757
13749
|
const { chunks } = state;
|
|
13758
13750
|
if (desc.isLink === true) {
|
|
@@ -13874,7 +13866,7 @@ const serializeRoot = ($root, range) => {
|
|
|
13874
13866
|
};
|
|
13875
13867
|
const flushParagraphChunks = () => {
|
|
13876
13868
|
if (paragraphChunks.length > 0) {
|
|
13877
|
-
chunks.push(
|
|
13869
|
+
chunks.push(paragraphChunks.join(MD_SOFT_LINEBREAK));
|
|
13878
13870
|
paragraphChunks.length = 0;
|
|
13879
13871
|
}
|
|
13880
13872
|
};
|
|
@@ -13892,6 +13884,9 @@ const serializeRoot = ($root, range) => {
|
|
|
13892
13884
|
} else {
|
|
13893
13885
|
assertTextBlock($child);
|
|
13894
13886
|
flushListChunks();
|
|
13887
|
+
if (isParagraph($child) && isParagraphMarkedAsBlock($child) && paragraphChunks.length > 0) {
|
|
13888
|
+
flushParagraphChunks();
|
|
13889
|
+
}
|
|
13895
13890
|
paragraphChunks.push(
|
|
13896
13891
|
serializeTextBlock($child, range)
|
|
13897
13892
|
);
|
|
@@ -14008,8 +14003,10 @@ const createParseVisitor = (doc) => {
|
|
|
14008
14003
|
},
|
|
14009
14004
|
paragraph() {
|
|
14010
14005
|
if (listsStack.length === 0) {
|
|
14011
|
-
$
|
|
14012
|
-
|
|
14006
|
+
const $p = createActuallyEmptyParagraph(doc);
|
|
14007
|
+
markParagraphAsBlock($p);
|
|
14008
|
+
$currentBlock = $p;
|
|
14009
|
+
$root.appendChild($p);
|
|
14013
14010
|
}
|
|
14014
14011
|
},
|
|
14015
14012
|
end() {
|
|
@@ -14036,7 +14033,7 @@ const setBrowserCaret = ({ startContainer, startOffset, endContainer, endOffset
|
|
|
14036
14033
|
range.setEnd(endContainer, endOffset);
|
|
14037
14034
|
selection.addRange(range);
|
|
14038
14035
|
};
|
|
14039
|
-
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>';
|
|
14040
14037
|
const template$p = document.createElement("template");
|
|
14041
14038
|
template$p.innerHTML = templateHTML$p;
|
|
14042
14039
|
const SUPPORTS_SHADOW_SELECTION = typeof window.ShadowRoot.prototype.getSelection === "function";
|
|
@@ -14054,7 +14051,13 @@ class RichTextarea extends NectaryElement {
|
|
|
14054
14051
|
#cachedRange = null;
|
|
14055
14052
|
#lastSelectionInfo = null;
|
|
14056
14053
|
#prevDispatchedValue = null;
|
|
14054
|
+
#changeDebounce = null;
|
|
14057
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
|
+
*/
|
|
14058
14061
|
#chipResolver = null;
|
|
14059
14062
|
constructor() {
|
|
14060
14063
|
super();
|
|
@@ -14118,6 +14121,7 @@ class RichTextarea extends NectaryElement {
|
|
|
14118
14121
|
}
|
|
14119
14122
|
disconnectedCallback() {
|
|
14120
14123
|
super.disconnectedCallback();
|
|
14124
|
+
this.#clearChangeDebounce();
|
|
14121
14125
|
this.#controller.abort();
|
|
14122
14126
|
this.#controller = null;
|
|
14123
14127
|
}
|
|
@@ -14443,9 +14447,23 @@ class RichTextarea extends NectaryElement {
|
|
|
14443
14447
|
if (result.range !== null) {
|
|
14444
14448
|
setBrowserCaret(result.range);
|
|
14445
14449
|
}
|
|
14450
|
+
this.#scheduleChangeDispatch();
|
|
14446
14451
|
}
|
|
14447
14452
|
this.#updateEditorEmptyClass();
|
|
14448
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
|
+
}
|
|
14449
14467
|
#onInputFocus = () => {
|
|
14450
14468
|
if (this.#cachedRange !== null) {
|
|
14451
14469
|
setBrowserCaret(this.#cachedRange);
|
|
@@ -14454,6 +14472,7 @@ class RichTextarea extends NectaryElement {
|
|
|
14454
14472
|
};
|
|
14455
14473
|
#onInputBlur = () => {
|
|
14456
14474
|
this.dispatchEvent(new CustomEvent("-blur"));
|
|
14475
|
+
this.#clearChangeDebounce();
|
|
14457
14476
|
this.#dispatchChangeEvent();
|
|
14458
14477
|
};
|
|
14459
14478
|
#dispatchChangeEvent() {
|
package/floating-panel/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const applyCountPlaceholder = (template2, count) => {
|
|
|
29
29
|
return template2.replaceAll("{count}", count);
|
|
30
30
|
};
|
|
31
31
|
const sanitizeAttr = (value) => value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
32
|
+
const toReactCamelCaseHandlerName = (eventSuffix) => `on${eventSuffix.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}`;
|
|
32
33
|
const isValidAction = (value) => {
|
|
33
34
|
if (typeof value !== "object" || value === null) {
|
|
34
35
|
return false;
|
|
@@ -99,9 +100,9 @@ class FloatingPanel extends NectaryElement {
|
|
|
99
100
|
this.#resizeThrottle?.fn();
|
|
100
101
|
});
|
|
101
102
|
this.#resizeObserver.observe(this.#$dialog);
|
|
102
|
-
this.addEventListener("-close", this.#
|
|
103
|
-
this.addEventListener("-select-all", this.#
|
|
104
|
-
this.addEventListener("-action", this.#
|
|
103
|
+
this.addEventListener("-close", this.#onCloseReactHandler, { signal });
|
|
104
|
+
this.addEventListener("-select-all", this.#onSelectAllReactHandler, { signal });
|
|
105
|
+
this.addEventListener("-action", this.#onActionReactHandler, { signal });
|
|
105
106
|
if (!this.hasAttribute("actions")) {
|
|
106
107
|
this.#renderActions(null);
|
|
107
108
|
}
|
|
@@ -302,7 +303,7 @@ class FloatingPanel extends NectaryElement {
|
|
|
302
303
|
* `-${action}` convenience event. The per-action event lets consumers bind
|
|
303
304
|
* directly to a specific action (e.g. `on-archive`) without switching on
|
|
304
305
|
* `e.detail` — and it works for any user-defined action, not just the
|
|
305
|
-
* built-in defaults. React handlers are forwarded by `#
|
|
306
|
+
* built-in defaults. React handlers are forwarded by `#onActionReactHandler`.
|
|
306
307
|
*/
|
|
307
308
|
#dispatchAction(action) {
|
|
308
309
|
this.dispatchEvent(new CustomEvent("-action", { detail: action }));
|
|
@@ -408,16 +409,20 @@ class FloatingPanel extends NectaryElement {
|
|
|
408
409
|
#onActionsPopoverClose = () => {
|
|
409
410
|
this.#closeOverflowMenu();
|
|
410
411
|
};
|
|
411
|
-
#
|
|
412
|
+
#onCloseReactHandler = (e) => {
|
|
412
413
|
getReactEventHandler(this, "on-close")?.(e);
|
|
414
|
+
getReactEventHandler(this, "onClose")?.(e);
|
|
413
415
|
};
|
|
414
|
-
#
|
|
416
|
+
#onSelectAllReactHandler = (e) => {
|
|
415
417
|
getReactEventHandler(this, "on-select-all")?.(e);
|
|
418
|
+
getReactEventHandler(this, "onSelectAll")?.(e);
|
|
416
419
|
};
|
|
417
|
-
#
|
|
420
|
+
#onActionReactHandler = (e) => {
|
|
418
421
|
getReactEventHandler(this, "on-action")?.(e);
|
|
422
|
+
getReactEventHandler(this, "onAction")?.(e);
|
|
419
423
|
if (e instanceof CustomEvent && typeof e.detail === "string") {
|
|
420
424
|
getReactEventHandler(this, `on-${e.detail}`)?.(e);
|
|
425
|
+
getReactEventHandler(this, toReactCamelCaseHandlerName(e.detail))?.(e);
|
|
421
426
|
}
|
|
422
427
|
};
|
|
423
428
|
get panelRect() {
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
package/rich-textarea/index.js
CHANGED
|
@@ -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() {
|
package/rich-textarea/types.d.ts
CHANGED
|
@@ -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;
|
package/rich-textarea/utils.js
CHANGED
|
@@ -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(
|
|
60
|
+
$li.classList.add(BLOCK_CLASSNAME);
|
|
60
61
|
};
|
|
61
62
|
const isListItemMarkedArBlock = ($li) => {
|
|
62
|
-
return $li.classList.contains(
|
|
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(
|
|
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
|
-
$
|
|
1835
|
-
|
|
1845
|
+
const $p = createActuallyEmptyParagraph(doc);
|
|
1846
|
+
markParagraphAsBlock($p);
|
|
1847
|
+
$currentBlock = $p;
|
|
1848
|
+
$root.appendChild($p);
|
|
1836
1849
|
}
|
|
1837
1850
|
},
|
|
1838
1851
|
end() {
|