@carbon/ai-chat 1.5.0 → 1.5.1-rc.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.
@@ -12075,8 +12075,6 @@ const StopStreamingButton = createComponent({
12075
12075
  react: React
12076
12076
  });
12077
12077
 
12078
- const MAX_AUTO_RESIZE_HEIGHT = 180;
12079
-
12080
12078
  function normalizeTextValue(value) {
12081
12079
  return value.replace(/\r\n?/g, "\n").replace(/\u00a0/g, " ");
12082
12080
  }
@@ -12090,47 +12088,172 @@ function toDisplayHTML(value) {
12090
12088
  return escaped.replace(/\n/g, "<br>");
12091
12089
  }
12092
12090
 
12091
+ function getSelectionForElement(element) {
12092
+ const root = element.getRootNode();
12093
+ if (root) {
12094
+ const shadowRoot = root;
12095
+ if (typeof shadowRoot.getSelection === "function") {
12096
+ return shadowRoot.getSelection();
12097
+ }
12098
+ }
12099
+ return window.getSelection();
12100
+ }
12101
+
12102
+ function getSelectionRangeForElement(element, selection) {
12103
+ if (!selection || selection.rangeCount === 0) {
12104
+ return null;
12105
+ }
12106
+ const range = selection.getRangeAt(0);
12107
+ if (range.commonAncestorContainer !== element && !element.contains(range.commonAncestorContainer)) {
12108
+ return null;
12109
+ }
12110
+ return range;
12111
+ }
12112
+
12113
+ function createRangeAtEnd(element) {
12114
+ const range = document.createRange();
12115
+ range.selectNodeContents(element);
12116
+ range.collapse(false);
12117
+ return range;
12118
+ }
12119
+
12120
+ function placeCaretAtEnd(element, selection) {
12121
+ if (typeof window === "undefined" || typeof document === "undefined") {
12122
+ return;
12123
+ }
12124
+ const range = createRangeAtEnd(element);
12125
+ selection?.removeAllRanges();
12126
+ selection?.addRange(range);
12127
+ }
12128
+
12129
+ function calculateAvailableLength(currentText, selectedTextLength, maxLength) {
12130
+ if (maxLength === undefined || maxLength === null) {
12131
+ return undefined;
12132
+ }
12133
+ return Math.max(maxLength - (currentText.length - selectedTextLength), 0);
12134
+ }
12135
+
12136
+ function truncateToLength(text, maxLength) {
12137
+ if (maxLength === undefined) {
12138
+ return text;
12139
+ }
12140
+ return text.length > maxLength ? text.slice(0, maxLength) : text;
12141
+ }
12142
+
12143
+ function updateContentAttribute(element, value) {
12144
+ element.dataset.hasContent = value.trim() ? "true" : "false";
12145
+ }
12146
+
12147
+ function getSelectionRange(element) {
12148
+ const selection = getSelectionForElement(element);
12149
+ return getSelectionRangeForElement(element, selection);
12150
+ }
12151
+
12152
+ function extractNormalizedText(element, maxLength) {
12153
+ const textValue = normalizeTextValue(element.innerText || "");
12154
+ let nextValue = textValue;
12155
+ let wasTruncated = false;
12156
+ if (maxLength && nextValue.length > maxLength) {
12157
+ nextValue = nextValue.slice(0, maxLength);
12158
+ element.innerText = nextValue;
12159
+ wasTruncated = true;
12160
+ }
12161
+ return {
12162
+ rawValue: nextValue,
12163
+ displayValue: toDisplayHTML(nextValue),
12164
+ wasTruncated
12165
+ };
12166
+ }
12167
+
12168
+ const MAX_AUTO_RESIZE_HEIGHT = 180;
12169
+
12093
12170
  const ContentEditableInput = forwardRef(({ariaLabel, autoSize, disabled, displayValue, maxLength, rawValue, onBlur, onChange, onFocus, onKeyDown, placeholder, testId}, ref) => {
12094
12171
  const editorRef = useRef(null);
12095
12172
  const sizerRef = useRef(null);
12096
12173
  const skipNextDomSync = useRef(false);
12097
12174
  const lastDisplayValue = useRef("");
12098
- function setHasContentData(value) {
12099
- if (!editorRef.current) {
12100
- return;
12101
- }
12102
- editorRef.current.dataset.hasContent = value ? "true" : "false";
12103
- }
12104
- function emitChangeFromDom() {
12175
+ const lastRangeRef = useRef(null);
12176
+ const lastNonCollapsedRangeRef = useRef(null);
12177
+ const emitChangeFromDom = useCallback(() => {
12105
12178
  if (!editorRef.current) {
12106
12179
  return;
12107
12180
  }
12108
- const textValue = normalizeTextValue(editorRef.current.innerText || "");
12109
- let nextValue = textValue;
12110
- if (maxLength && nextValue.length > maxLength) {
12111
- nextValue = nextValue.slice(0, maxLength);
12112
- editorRef.current.innerText = nextValue;
12113
- placeCaretAtEnd(editorRef.current);
12181
+ const {rawValue, displayValue, wasTruncated} = extractNormalizedText(editorRef.current, maxLength);
12182
+ if (wasTruncated) {
12183
+ const selection = getSelectionForElement(editorRef.current);
12184
+ placeCaretAtEnd(editorRef.current, selection);
12114
12185
  }
12115
- setHasContentData(nextValue);
12186
+ updateContentAttribute(editorRef.current, rawValue);
12116
12187
  skipNextDomSync.current = true;
12117
12188
  onChange({
12118
- rawValue: nextValue,
12119
- displayValue: toDisplayHTML(nextValue)
12189
+ rawValue,
12190
+ displayValue
12120
12191
  });
12121
- }
12122
- function handleInput() {
12192
+ }, [ maxLength, onChange ]);
12193
+ const captureSelection = useCallback(() => {
12194
+ if (!editorRef.current) {
12195
+ return;
12196
+ }
12197
+ const range = getSelectionRange(editorRef.current);
12198
+ if (!range) {
12199
+ return;
12200
+ }
12201
+ lastRangeRef.current = range.cloneRange();
12202
+ if (!range.collapsed) {
12203
+ lastNonCollapsedRangeRef.current = range.cloneRange();
12204
+ }
12205
+ }, []);
12206
+ const handleNativePaste = useCallback(event => {
12207
+ const element = editorRef.current;
12208
+ if (!element) {
12209
+ return;
12210
+ }
12211
+ const clipboardText = event.clipboardData?.getData("text/plain") || "";
12212
+ if (!clipboardText) {
12213
+ return;
12214
+ }
12215
+ event.preventDefault();
12216
+ const normalizedText = normalizeTextValue(clipboardText);
12217
+ const currentText = normalizeTextValue(element.innerText || "");
12218
+ const selection = getSelectionForElement(element);
12219
+ const range = getSelectionRangeForElement(element, selection);
12220
+ const selectedTextLength = range ? normalizeTextValue(range.toString()).length : 0;
12221
+ const available = calculateAvailableLength(currentText, selectedTextLength, maxLength);
12222
+ const textToInsert = truncateToLength(normalizedText, available);
12223
+ if (!textToInsert.length) {
12224
+ return;
12225
+ }
12226
+ document.execCommand("insertText", false, textToInsert);
12227
+ captureSelection();
12123
12228
  emitChangeFromDom();
12124
- }
12125
- function handleKeyDown(event) {
12126
- onKeyDown?.(event);
12127
- }
12128
- function handleFocus(event) {
12129
- onFocus?.(event);
12130
- }
12131
- function handleBlur(event) {
12132
- onBlur?.(event);
12133
- }
12229
+ }, [ captureSelection, emitChangeFromDom, maxLength ]);
12230
+ useEffect(() => {
12231
+ const element = editorRef.current;
12232
+ if (!element) {
12233
+ return undefined;
12234
+ }
12235
+ const eventHandlers = [ {
12236
+ type: "keydown",
12237
+ handler: captureSelection
12238
+ }, {
12239
+ type: "keyup",
12240
+ handler: captureSelection
12241
+ }, {
12242
+ type: "mouseup",
12243
+ handler: captureSelection
12244
+ }, {
12245
+ type: "paste",
12246
+ handler: handleNativePaste
12247
+ } ];
12248
+ eventHandlers.forEach(({type, handler}) => {
12249
+ element.addEventListener(type, handler);
12250
+ });
12251
+ return () => {
12252
+ eventHandlers.forEach(({type, handler}) => {
12253
+ element.removeEventListener(type, handler);
12254
+ });
12255
+ };
12256
+ }, [ captureSelection, handleNativePaste ]);
12134
12257
  useImperativeHandle(ref, () => ({
12135
12258
  getHTMLElement: () => editorRef.current,
12136
12259
  takeFocus: () => {
@@ -12169,11 +12292,17 @@ const ContentEditableInput = forwardRef(({ariaLabel, autoSize, disabled, display
12169
12292
  } else {
12170
12293
  editorRef.current.innerHTML = "";
12171
12294
  }
12172
- placeCaretAtEnd(editorRef.current);
12295
+ const selection = getSelectionForElement(editorRef.current);
12296
+ placeCaretAtEnd(editorRef.current, selection);
12173
12297
  }, [ displayValue ]);
12174
12298
  useLayoutEffect(() => {
12175
- setHasContentData(rawValue);
12299
+ if (editorRef.current) {
12300
+ updateContentAttribute(editorRef.current, rawValue);
12301
+ }
12176
12302
  }, [ rawValue ]);
12303
+ const sizerInnerHTML = useMemo(() => ({
12304
+ __html: rawValue && rawValue.length ? displayValue || "&nbsp;" : escapeHTML(placeholder || " ") || "&nbsp;"
12305
+ }), [ rawValue, displayValue, placeholder ]);
12177
12306
  return React.createElement("div", {
12178
12307
  className: cx("cds-aichat--text-area", {
12179
12308
  "cds-aichat--text-area--auto-size": autoSize,
@@ -12188,10 +12317,10 @@ const ContentEditableInput = forwardRef(({ariaLabel, autoSize, disabled, display
12188
12317
  "data-placeholder": placeholder,
12189
12318
  "data-testid": testId,
12190
12319
  "aria-disabled": disabled,
12191
- onBlur: handleBlur,
12192
- onFocus: handleFocus,
12193
- onInput: handleInput,
12194
- onKeyDown: handleKeyDown,
12320
+ onBlur,
12321
+ onFocus,
12322
+ onInput: emitChangeFromDom,
12323
+ onKeyDown,
12195
12324
  role: "textbox",
12196
12325
  tabIndex: disabled ? -1 : 0,
12197
12326
  spellCheck: true,
@@ -12203,26 +12332,12 @@ const ContentEditableInput = forwardRef(({ariaLabel, autoSize, disabled, display
12203
12332
  ref: sizerRef,
12204
12333
  className: "cds-aichat--text-area-sizer",
12205
12334
  "aria-hidden": true,
12206
- dangerouslySetInnerHTML: {
12207
- __html: rawValue && rawValue.length ? displayValue || "&nbsp;" : escapeHTML(placeholder || " ") || "&nbsp;"
12208
- }
12335
+ dangerouslySetInnerHTML: sizerInnerHTML
12209
12336
  }));
12210
12337
  });
12211
12338
 
12212
12339
  ContentEditableInput.displayName = "ContentEditableInput";
12213
12340
 
12214
- function placeCaretAtEnd(element) {
12215
- if (typeof window === "undefined" || typeof document === "undefined") {
12216
- return;
12217
- }
12218
- const range = document.createRange();
12219
- range.selectNodeContents(element);
12220
- range.collapse(false);
12221
- const selection = window.getSelection();
12222
- selection?.removeAllRanges();
12223
- selection?.addRange(range);
12224
- }
12225
-
12226
12341
  const Send = carbonIconToReact(Send16);
12227
12342
 
12228
12343
  const SendFilled = carbonIconToReact(SendFilled16);