@nectary/components 5.14.6 → 5.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectary/components",
3
- "version": "5.14.6",
3
+ "version": "5.15.0",
4
4
  "files": [
5
5
  "**/*/*.css",
6
6
  "**/*/*.json",
@@ -1,8 +1,9 @@
1
1
  import '../emoji';
2
2
  import '../code-tag';
3
3
  import '../link';
4
- import '../tag';
4
+ import '../rich-textarea-chip';
5
5
  import { NectaryElement } from '../utils';
6
+ import type { TChipResolver } from '../rich-textarea/types';
6
7
  import type { TSinchTextType } from '../text/types';
7
8
  export * from './types';
8
9
  export declare class RichText extends NectaryElement {
@@ -16,4 +17,10 @@ export declare class RichText extends NectaryElement {
16
17
  set size(value: TSinchTextType);
17
18
  get text(): string;
18
19
  set text(value: string);
20
+ get chipColor(): string | null;
21
+ set chipColor(value: string | null);
22
+ get chipIcon(): string | null;
23
+ set chipIcon(value: string | null);
24
+ get chipResolver(): TChipResolver | null;
25
+ set chipResolver(value: TChipResolver | null);
19
26
  }
@@ -2,7 +2,7 @@ import { getEmojiBaseUrl } from "../emoji/utils.js";
2
2
  import "../emoji/index.js";
3
3
  import "../code-tag/index.js";
4
4
  import "../link/index.js";
5
- import "../tag/index.js";
5
+ import "../rich-textarea-chip/index.js";
6
6
  import { getLiteralAttribute, updateLiteralAttribute, getAttribute, updateAttribute } from "../utils/dom.js";
7
7
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
8
8
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
@@ -15,6 +15,7 @@ class RichText extends NectaryElement {
15
15
  #wrapper;
16
16
  #parseVisitor;
17
17
  #controller = null;
18
+ #chipResolver = null;
18
19
  constructor() {
19
20
  super();
20
21
  const shadowRoot = this.attachShadow();
@@ -39,6 +40,9 @@ class RichText extends NectaryElement {
39
40
  super.connectedCallback();
40
41
  this.setAttribute("role", "paragraph");
41
42
  this.#parseVisitor.updateEmojiBaseUrl(getEmojiBaseUrl(this));
43
+ this.#parseVisitor.updateChipColor(this.chipColor);
44
+ this.#parseVisitor.updateChipIcon(this.chipIcon);
45
+ this.#parseVisitor.updateChipResolver(this.#chipResolver);
42
46
  this.#updateText();
43
47
  this.#wrapper.addEventListener("click", this.#handleElementClick, { signal });
44
48
  this.addEventListener("-element-click", this.#onClickReactHandler, { signal });
@@ -51,7 +55,7 @@ class RichText extends NectaryElement {
51
55
  super.disconnectedCallback();
52
56
  }
53
57
  static get observedAttributes() {
54
- return ["text"];
58
+ return ["text", "chip-color", "chip-icon"];
55
59
  }
56
60
  attributeChangedCallback(name, _oldVal, _newVal) {
57
61
  switch (name) {
@@ -59,6 +63,16 @@ class RichText extends NectaryElement {
59
63
  this.#updateText();
60
64
  break;
61
65
  }
66
+ case "chip-color": {
67
+ this.#parseVisitor.updateChipColor(this.chipColor);
68
+ this.#updateText();
69
+ break;
70
+ }
71
+ case "chip-icon": {
72
+ this.#parseVisitor.updateChipIcon(this.chipIcon);
73
+ this.#updateText();
74
+ break;
75
+ }
62
76
  }
63
77
  }
64
78
  get size() {
@@ -73,6 +87,26 @@ class RichText extends NectaryElement {
73
87
  set text(value) {
74
88
  updateAttribute(this, "text", value);
75
89
  }
90
+ get chipColor() {
91
+ return getAttribute(this, "chip-color");
92
+ }
93
+ set chipColor(value) {
94
+ updateAttribute(this, "chip-color", value);
95
+ }
96
+ get chipIcon() {
97
+ return getAttribute(this, "chip-icon");
98
+ }
99
+ set chipIcon(value) {
100
+ updateAttribute(this, "chip-icon", value);
101
+ }
102
+ get chipResolver() {
103
+ return this.#chipResolver;
104
+ }
105
+ set chipResolver(value) {
106
+ this.#chipResolver = value;
107
+ this.#parseVisitor.updateChipResolver(value);
108
+ this.#updateText();
109
+ }
76
110
  #handleElementClick = (e) => {
77
111
  const eventTarget = e.target;
78
112
  const elementClickEvent = new CustomEvent("-element-click");
@@ -1,3 +1,5 @@
1
+ import type { TChipResolver } from '../rich-textarea/types';
2
+ import type { TSinchTagColor } from '../tag/colors';
1
3
  import type { TSinchTextType } from '../text/types';
2
4
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType, NectaryComponentReact, NectaryComponentVanilla } from '../types';
3
5
  export type ElementClickedEvent = CustomEvent & {
@@ -6,6 +8,14 @@ export type ElementClickedEvent = CustomEvent & {
6
8
  export type TSinchRichTextProps = {
7
9
  size?: TSinchTextType;
8
10
  text: string;
11
+ /** Default color for chips using the tag color system */
12
+ 'chip-color'?: TSinchTagColor;
13
+ /** Default icon for chips */
14
+ 'chip-icon'?: string;
15
+ };
16
+ export type TSinchRichTextMethods = {
17
+ /** Resolver callback for chip icon and color based on tag name */
18
+ chipResolver: TChipResolver | null;
9
19
  };
10
20
  export type TSinchRichTextEvents = {
11
21
  /** Click event handler */
@@ -32,6 +42,7 @@ export type TSinchRichTextStyle = {
32
42
  };
33
43
  export type TSinchRichText = {
34
44
  props: TSinchRichTextProps;
45
+ methods: TSinchRichTextMethods;
35
46
  events: TSinchRichTextEvents;
36
47
  style: TSinchRichTextStyle;
37
48
  };
@@ -1,7 +1,11 @@
1
+ import type { TChipResolver } from '../rich-textarea/types';
1
2
  import type { TSinchTextType } from '../text/types';
2
3
  import type { TMarkdownParseVisitor } from '../utils';
3
4
  export declare const sizeValues: readonly TSinchTextType[];
4
5
  export declare const createParseVisitor: (doc: Document) => {
5
6
  updateEmojiBaseUrl(url: string | null): void;
7
+ updateChipColor(color: string | null): void;
8
+ updateChipIcon(icon: string | null): void;
9
+ updateChipResolver(resolver: TChipResolver | null): void;
6
10
  createVisitor(): TMarkdownParseVisitor;
7
11
  };
@@ -2,10 +2,22 @@ import { setEmojiBaseUrl } from "../emoji/utils.js";
2
2
  const sizeValues = ["m", "s", "xs", "xxs"];
3
3
  const createParseVisitor = (doc) => {
4
4
  let emojiBaseUrl = null;
5
+ let chipColor = null;
6
+ let chipIcon = null;
7
+ let chipResolver = null;
5
8
  return {
6
9
  updateEmojiBaseUrl(url) {
7
10
  emojiBaseUrl = url;
8
11
  },
12
+ updateChipColor(color) {
13
+ chipColor = color;
14
+ },
15
+ updateChipIcon(icon) {
16
+ chipIcon = icon;
17
+ },
18
+ updateChipResolver(resolver) {
19
+ chipResolver = resolver;
20
+ },
9
21
  createVisitor() {
10
22
  const $root = doc.createDocumentFragment();
11
23
  let $p = null;
@@ -35,9 +47,19 @@ const createParseVisitor = (doc) => {
35
47
  $p.appendChild($codeTag);
36
48
  },
37
49
  tag(text) {
38
- const $tag = doc.createElement("sinch-tag");
39
- $tag.text = text;
40
- $p.appendChild($tag);
50
+ const $chip = doc.createElement("sinch-rich-textarea-chip");
51
+ const resolved = chipResolver?.(text);
52
+ $chip.text = text;
53
+ $chip.setAttribute("readonly", "");
54
+ const resolvedColor = resolved?.color ?? chipColor;
55
+ const resolvedIcon = resolved?.icon ?? chipIcon;
56
+ if (resolvedColor !== null && resolvedColor !== void 0) {
57
+ $chip.setAttribute("color", resolvedColor);
58
+ }
59
+ if (resolvedIcon !== null && resolvedIcon !== void 0) {
60
+ $chip.setAttribute("icon", resolvedIcon);
61
+ }
62
+ $p.appendChild($chip);
41
63
  },
42
64
  inline(text, { isBold, isItalic, isStrikethrough }) {
43
65
  const $inline = doc.createElement("SPAN");
@@ -1,4 +1,6 @@
1
+ import '../rich-textarea-chip';
1
2
  import { NectaryElement } from '../utils';
3
+ import type { TChipResolver } from './types';
2
4
  export * from './types';
3
5
  export declare class RichTextarea extends NectaryElement {
4
6
  #private;
@@ -15,16 +17,23 @@ export declare class RichTextarea extends NectaryElement {
15
17
  get disabled(): boolean;
16
18
  set rows(value: HTMLTextAreaElement['rows']);
17
19
  get rows(): HTMLTextAreaElement['rows'];
20
+ set chipColor(value: string | null);
21
+ get chipColor(): string | null;
22
+ set chipIcon(value: string | null);
23
+ get chipIcon(): string | null;
24
+ set chipResolver(resolver: TChipResolver | null);
25
+ get chipResolver(): TChipResolver | null;
18
26
  get focusable(): boolean;
19
27
  focus(): void;
20
28
  blur(): void;
21
29
  insertText(value: string): void;
22
30
  insertLink(text: string, href: string): void;
23
- insertMention(username: string): void;
31
+ insertChip(text: string): void;
24
32
  formatItalic(): void;
25
33
  formatBold(): void;
26
34
  formatStrikethrough(): void;
27
35
  formatCodeTag(): void;
28
36
  formatOrderedList(): void;
29
37
  formatUnorderedList(): void;
38
+ getCaretRect(): DOMRect | null;
30
39
  }
@@ -1,11 +1,12 @@
1
1
  import { getEmojiBaseUrl } from "../emoji/utils.js";
2
+ import "../rich-textarea-chip/index.js";
2
3
  import { updateAttribute, getAttribute, updateBooleanAttribute, getBooleanAttribute, getIntegerAttribute, setClass } from "../utils/dom.js";
3
4
  import { defineCustomElement, NectaryElement } from "../utils/element.js";
4
5
  import { isElementFocused } from "../utils/slot.js";
5
6
  import { getReactEventHandler } from "../utils/get-react-event-handler.js";
6
7
  import { parseMarkdown } from "../utils/markdown.js";
7
- import { createParseVisitor, getEndRange, formatList, handleEmojiMousedown, formatOutdent, formatIndent, formatInline, insertTag, insertLink, insertText, insertLineBreak, deleteContentBackward, setBrowserCaret, serializeMarkdown, getSelectionInfo, isSelectionEqual, insertFromPaste, isEditorEmpty } from "./utils.js";
8
- 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}.m{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:var(--sinch-comp-tag-shape-radius,4px);background-color:var(--sinch-comp-tag-color-default-background,#e3e8f0);color:var(--sinch-comp-tag-color-default-foreground,#1e2433);font-size:12px;font-weight:500;line-height:20px;white-space:nowrap;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 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>';
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
10
  const template = document.createElement("template");
10
11
  template.innerHTML = templateHTML;
11
12
  const SUPPORTS_SHADOW_SELECTION = typeof window.ShadowRoot.prototype.getSelection === "function";
@@ -24,6 +25,7 @@ class RichTextarea extends NectaryElement {
24
25
  #lastSelectionInfo = null;
25
26
  #prevDispatchedValue = null;
26
27
  #parseVisitor;
28
+ #chipResolver = null;
27
29
  constructor() {
28
30
  super();
29
31
  const shadowRoot = this.attachShadow({ delegatesFocus: true });
@@ -58,8 +60,6 @@ class RichTextarea extends NectaryElement {
58
60
  const options = {
59
61
  signal: this.#controller.signal
60
62
  };
61
- this.setAttribute("role", "textbox");
62
- this.ariaMultiLine = "true";
63
63
  this.#$input.addEventListener("beforeinput", this.#onBeforeInput, options);
64
64
  this.#$input.addEventListener("keydown", this.#onKeydown, options);
65
65
  this.#$input.addEventListener("mousedown", this.#onMouseDown, options);
@@ -69,6 +69,7 @@ class RichTextarea extends NectaryElement {
69
69
  this.#$input.addEventListener("cut", this.#onCut, options);
70
70
  this.#$input.addEventListener("copy", this.#onCopy, options);
71
71
  this.#$input.addEventListener("paste", this.#onPaste, options);
72
+ this.#$input.addEventListener("-right-icon-click", this.#onChipCloseClick, options);
72
73
  this.#$bottomSlot.addEventListener("slotchange", this.#onBottomSlotChange, options);
73
74
  this.#$topSlot.addEventListener("slotchange", this.#onTopSlotChange, options);
74
75
  this.addEventListener("-change", this.#onChangeReactHandler, options);
@@ -77,6 +78,8 @@ class RichTextarea extends NectaryElement {
77
78
  this.addEventListener("-selection", this.#onSelectionReactHandler, options);
78
79
  document.addEventListener("selectionchange", this.#onSelectionChange, options);
79
80
  this.#parseVisitor.updateEmojiBaseUrl(getEmojiBaseUrl(this));
81
+ this.#parseVisitor.updateChipColor(this.chipColor);
82
+ this.#parseVisitor.updateChipIcon(this.chipIcon);
80
83
  this.#onTopSlotChange();
81
84
  this.#onBottomSlotChange();
82
85
  this.#onValueChange(this.value);
@@ -91,7 +94,10 @@ class RichTextarea extends NectaryElement {
91
94
  static get observedAttributes() {
92
95
  return [
93
96
  "value",
94
- "placeholder"
97
+ "placeholder",
98
+ "aria-label",
99
+ "chip-color",
100
+ "chip-icon"
95
101
  ];
96
102
  }
97
103
  attributeChangedCallback(name, oldVal, newVal) {
@@ -105,6 +111,19 @@ class RichTextarea extends NectaryElement {
105
111
  case "placeholder": {
106
112
  this.#$placeholder.textContent = newVal;
107
113
  updateAttribute(this, "aria-placeholder", newVal);
114
+ updateAttribute(this.#$input, "aria-placeholder", newVal);
115
+ break;
116
+ }
117
+ case "aria-label": {
118
+ updateAttribute(this.#$input, "aria-label", newVal);
119
+ break;
120
+ }
121
+ case "chip-color": {
122
+ this.#updateChipColors(newVal);
123
+ break;
124
+ }
125
+ case "chip-icon": {
126
+ this.#updateChipIcons(newVal);
108
127
  break;
109
128
  }
110
129
  }
@@ -133,6 +152,26 @@ class RichTextarea extends NectaryElement {
133
152
  get rows() {
134
153
  return getIntegerAttribute(this, "rows", 0);
135
154
  }
155
+ set chipColor(value) {
156
+ updateAttribute(this, "chip-color", value);
157
+ }
158
+ get chipColor() {
159
+ return getAttribute(this, "chip-color");
160
+ }
161
+ set chipIcon(value) {
162
+ updateAttribute(this, "chip-icon", value);
163
+ }
164
+ get chipIcon() {
165
+ return getAttribute(this, "chip-icon");
166
+ }
167
+ set chipResolver(resolver) {
168
+ this.#chipResolver = resolver;
169
+ this.#parseVisitor.updateChipResolver(resolver);
170
+ this.#applyChipResolver();
171
+ }
172
+ get chipResolver() {
173
+ return this.#chipResolver;
174
+ }
136
175
  get focusable() {
137
176
  return true;
138
177
  }
@@ -150,8 +189,8 @@ class RichTextarea extends NectaryElement {
150
189
  const res = this.#handleInput("insertLink", this.#getCurrentRange(), text, href);
151
190
  this.#handleActionResult(res);
152
191
  }
153
- insertMention(username) {
154
- const res = this.#handleInput("insertMention", this.#getCurrentRange(), username);
192
+ insertChip(text) {
193
+ const res = this.#handleInput("insertChip", this.#getCurrentRange(), text);
155
194
  this.#handleActionResult(res);
156
195
  }
157
196
  formatItalic() {
@@ -178,6 +217,16 @@ class RichTextarea extends NectaryElement {
178
217
  const res = formatList(false, this.#getCurrentRange());
179
218
  this.#handleActionResult(res);
180
219
  }
220
+ getCaretRect() {
221
+ const tRange = this.#getSelectionRange();
222
+ if (tRange === null) {
223
+ return null;
224
+ }
225
+ const range = document.createRange();
226
+ range.setStart(tRange.startContainer, tRange.startOffset);
227
+ range.setEnd(tRange.endContainer, tRange.endOffset);
228
+ return range.getBoundingClientRect();
229
+ }
181
230
  /**
182
231
  * Input must be in focus
183
232
  */
@@ -210,6 +259,15 @@ class RichTextarea extends NectaryElement {
210
259
  handleEmojiMousedown(e.target)
211
260
  );
212
261
  };
262
+ #onChipCloseClick = (e) => {
263
+ const $chip = e.target;
264
+ const range = removeChip($chip, this.#$input);
265
+ if (range !== null) {
266
+ setBrowserCaret(range);
267
+ this.#cachedRange = range;
268
+ this.#dispatchChangeEvent();
269
+ }
270
+ };
213
271
  #onKeydown = (e) => {
214
272
  if (e.shiftKey) {
215
273
  switch (e.key) {
@@ -320,8 +378,12 @@ class RichTextarea extends NectaryElement {
320
378
  case "insertLink": {
321
379
  return insertLink(this.#$input, text, href, range);
322
380
  }
323
- case "insertMention": {
324
- return insertTag(this.#$input, text, range);
381
+ case "insertMention":
382
+ case "insertChip": {
383
+ const resolved = this.#chipResolver?.(text);
384
+ const color = resolved?.color ?? this.getAttribute("chip-color") ?? void 0;
385
+ const icon = resolved?.icon ?? this.getAttribute("chip-icon") ?? void 0;
386
+ return insertChip(this.#$input, text, range, { color, icon });
325
387
  }
326
388
  // case 'formatUnderline':
327
389
  case "formatItalic":
@@ -449,6 +511,58 @@ class RichTextarea extends NectaryElement {
449
511
  #updateEditorEmptyClass() {
450
512
  setClass(this.#$input, "empty", isEditorEmpty(this.#$input));
451
513
  }
514
+ #updateChipColors(color) {
515
+ this.#parseVisitor.updateChipColor(color);
516
+ if (this.#chipResolver === null) {
517
+ const chips = this.#$input.querySelectorAll("sinch-rich-textarea-chip");
518
+ chips.forEach((chip) => {
519
+ if (color !== null && color !== "") {
520
+ chip.setAttribute("color", color);
521
+ } else {
522
+ chip.removeAttribute("color");
523
+ }
524
+ });
525
+ }
526
+ }
527
+ #updateChipIcons(icon) {
528
+ this.#parseVisitor.updateChipIcon(icon);
529
+ if (this.#chipResolver === null) {
530
+ const chips = this.#$input.querySelectorAll("sinch-rich-textarea-chip");
531
+ chips.forEach((chip) => {
532
+ if (icon !== null && icon !== "") {
533
+ chip.setAttribute("icon", icon);
534
+ } else {
535
+ chip.removeAttribute("icon");
536
+ }
537
+ });
538
+ }
539
+ }
540
+ #applyChipResolver() {
541
+ if (this.#chipResolver === null) {
542
+ return;
543
+ }
544
+ const chips = this.#$input.querySelectorAll("sinch-rich-textarea-chip");
545
+ chips.forEach((chip) => {
546
+ const text = chip.getAttribute("text");
547
+ if (text !== null && text !== "") {
548
+ const resolved = this.#chipResolver(text);
549
+ if (resolved?.icon !== void 0) {
550
+ chip.setAttribute("icon", resolved.icon);
551
+ } else if (this.chipIcon !== null) {
552
+ chip.setAttribute("icon", this.chipIcon);
553
+ } else {
554
+ chip.removeAttribute("icon");
555
+ }
556
+ if (resolved?.color !== void 0) {
557
+ chip.setAttribute("color", resolved.color);
558
+ } else if (this.chipColor !== null) {
559
+ chip.setAttribute("color", this.chipColor);
560
+ } else {
561
+ chip.removeAttribute("color");
562
+ }
563
+ }
564
+ });
565
+ }
452
566
  #onBottomSlotChange = () => {
453
567
  const isEmpty = this.#$bottomSlot.assignedElements().length === 0;
454
568
  setClass(this.#$bottomWrapper, "empty", isEmpty);
@@ -1,3 +1,4 @@
1
+ import type { TSinchTagColor } from '../tag/colors';
1
2
  import type { NectaryComponentReactByType, NectaryComponentVanillaByType, NectaryComponentReact, NectaryComponentVanilla } from '../types';
2
3
  export type TRichTextareaSelection = {
3
4
  italic: boolean;
@@ -9,23 +10,38 @@ export type TRichTextareaSelection = {
9
10
  ulist: boolean;
10
11
  olist: boolean;
11
12
  };
13
+ /** Resolver callback for chip properties based on tag name */
14
+ export type TChipResolver = (tagName: string) => {
15
+ icon?: string;
16
+ color?: string;
17
+ } | undefined;
12
18
  export type TSinchRichTextareaProps = {
13
19
  /** Value */
14
20
  value: string;
15
21
  /** Text that appears in the text field when it has no value set */
16
22
  placeholder?: string;
23
+ /** Default Color for chips/tags using the tag color system */
24
+ 'chip-color'?: TSinchTagColor;
25
+ /** Default icon for chips/tags */
26
+ 'chip-icon'?: string;
17
27
  'aria-label': string;
18
28
  };
19
29
  export type TSinchRichTextareaMethods = {
20
30
  insertText(value: string): void;
21
31
  insertLink(text: string, href: string): void;
32
+ /** @deprecated — use insertChip instead */
22
33
  insertMention(username: string): void;
34
+ insertChip(name: string): void;
23
35
  formatItalic(): void;
24
36
  formatBold(): void;
25
37
  formatStrikethrough(): void;
26
38
  formatCodeTag(): void;
27
39
  formatOrderedList(): void;
28
40
  formatUnorderedList(): void;
41
+ /** Returns the bounding rectangle of the current caret/selection */
42
+ getCaretRect(): DOMRect | null;
43
+ /** Resolver callback for chip icon and color based on tag name */
44
+ chipResolver: TChipResolver | null;
29
45
  };
30
46
  export type TSinchRichTextareaEvents = {
31
47
  /** Change value handler */
@@ -1,4 +1,4 @@
1
- import type { TRichTextareaSelection } from './types';
1
+ import type { TRichTextareaSelection, TChipResolver } from './types';
2
2
  import type { TMarkdownParseVisitor } from '../utils';
3
3
  export interface TRichTextareaRoot extends HTMLElement {
4
4
  nodeName: 'DIV';
@@ -10,6 +10,7 @@ export type TRange = {
10
10
  startOffset: number;
11
11
  };
12
12
  export type TRichTextareaFormatInputType = 'formatItalic' | 'formatBold' | 'formatStrikeThrough' | 'formatCodeTag';
13
+ export declare const removeChip: ($chip: Node, $root: TRichTextareaRoot) => TRange | null;
13
14
  export declare const formatInline: (formatType: TRichTextareaFormatInputType, range: TRange) => TActionResult;
14
15
  export declare const formatIndent: (range: TRange) => TActionResult;
15
16
  export declare const formatOutdent: (range: TRange) => TActionResult;
@@ -22,7 +23,10 @@ export type TActionResult = {
22
23
  };
23
24
  export declare const deleteContentBackward: ($root: TRichTextareaRoot, range: TRange) => TActionResult;
24
25
  export declare const insertLink: ($root: TRichTextareaRoot, text: string, href: string, range: TRange) => TActionResult;
25
- export declare const insertTag: ($root: TRichTextareaRoot, text: string, range: TRange) => TActionResult;
26
+ export declare const insertChip: ($root: TRichTextareaRoot, text: string, range: TRange, options?: {
27
+ color?: string;
28
+ icon?: string;
29
+ }) => TActionResult;
26
30
  export declare const insertLineBreak: (range: TRange) => TActionResult;
27
31
  export declare const insertText: ($root: TRichTextareaRoot, data: string | null, range: TRange) => TActionResult;
28
32
  export declare const insertFromPaste: (data: string, range: Readonly<TRange>, visitor: TMarkdownParseVisitor) => TActionResult;
@@ -35,6 +39,9 @@ export declare const isEditorEmpty: ($root: TRichTextareaRoot) => boolean;
35
39
  export declare const serializeMarkdown: ($root: TRichTextareaRoot, range: Readonly<TRange> | null) => string;
36
40
  export declare const createParseVisitor: (doc: Document) => {
37
41
  updateEmojiBaseUrl(url: string | null): void;
42
+ updateChipColor(color: string | null): void;
43
+ updateChipIcon(icon: string | null): void;
44
+ updateChipResolver(resolver: TChipResolver | null): void;
38
45
  createVisitor(): TMarkdownParseVisitor;
39
46
  };
40
47
  export declare const setBrowserCaret: ({ startContainer, startOffset, endContainer, endOffset }: TRange) => void;