@datarobot/design-system 28.4.0 → 28.5.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.
Files changed (38) hide show
  1. package/cjs/chat/chat-message-body.js +9 -4
  2. package/cjs/chat/hocs.d.ts +1 -1
  3. package/cjs/chat/hocs.js +21 -22
  4. package/cjs/chat/index.d.ts +2 -0
  5. package/cjs/chat/index.js +14 -0
  6. package/cjs/text-editor/inline-text-editor.d.ts +3 -1
  7. package/cjs/text-editor/inline-text-editor.js +6 -2
  8. package/cjs/text-editor/text-editor-constants.d.ts +0 -2
  9. package/cjs/text-editor/text-editor-constants.js +0 -8
  10. package/cjs/text-editor/text-editor-content.d.ts +2 -1
  11. package/cjs/text-editor/text-editor-content.js +22 -2
  12. package/cjs/text-editor/text-editor-header.js +2 -9
  13. package/cjs/text-editor/text-editor-helpers.d.ts +1 -0
  14. package/cjs/text-editor/text-editor-helpers.js +22 -17
  15. package/cjs/text-editor/text-editor.d.ts +5 -1
  16. package/cjs/text-editor/text-editor.js +31 -6
  17. package/esm/chat/chat-message-body.js +9 -4
  18. package/esm/chat/hocs.d.ts +1 -1
  19. package/esm/chat/hocs.js +21 -22
  20. package/esm/chat/index.d.ts +2 -0
  21. package/esm/chat/index.js +2 -0
  22. package/esm/text-editor/inline-text-editor.d.ts +3 -1
  23. package/esm/text-editor/inline-text-editor.js +6 -2
  24. package/esm/text-editor/text-editor-constants.d.ts +0 -2
  25. package/esm/text-editor/text-editor-constants.js +0 -6
  26. package/esm/text-editor/text-editor-content.d.ts +2 -1
  27. package/esm/text-editor/text-editor-content.js +22 -2
  28. package/esm/text-editor/text-editor-header.js +3 -10
  29. package/esm/text-editor/text-editor-helpers.d.ts +1 -0
  30. package/esm/text-editor/text-editor-helpers.js +22 -18
  31. package/esm/text-editor/text-editor.d.ts +5 -1
  32. package/esm/text-editor/text-editor.js +33 -8
  33. package/js/bundle/bundle.js +175 -127
  34. package/js/bundle/bundle.min.js +1 -1
  35. package/js/bundle/index.d.ts +22 -5
  36. package/package.json +1 -1
  37. package/styles/index.css +10 -0
  38. package/styles/index.min.css +1 -1
@@ -152,10 +152,15 @@ function ChatMessageBodyBase({
152
152
  // but there we prevent it only from updating slate inner state, html input still
153
153
  // adds entered text to the DOM input field causing buggy behavior: https://datarobot.atlassian.net/browse/TESTLIO-2963
154
154
  // preventing default from the onDOMBeforeInput won't add new character to the DOM input field.
155
- // we still won't to leave logic in hocs.ts as a fallback + it also covers copy+paste logic which doesn't trigger onDOMBeforeInput
156
- const existingMentionsCount = (0, _utils.getMentionsCount)(editor.children);
157
- if (_slate.Editor.string(editor, []).length + existingMentionsCount + (event?.data?.length ?? 0) > maxLength) {
158
- event.preventDefault();
155
+ // we still want to leave logic in hocs.ts because it covers copy+paste case, as for some reason
156
+ // under certain circumstances onDOMBeforeInput stops being triggering for copy+paste so having there is safer
157
+
158
+ // we want to always allow deleting text
159
+ if (!event.inputType.startsWith('delete')) {
160
+ const existingMentionsCount = (0, _utils.getMentionsCount)(editor.children);
161
+ if (!!event.data && _slate.Editor.string(editor, []).length + existingMentionsCount + (event?.data?.length ?? 0) > maxLength) {
162
+ event.preventDefault();
163
+ }
159
164
  }
160
165
  }
161
166
  })
@@ -1,3 +1,3 @@
1
1
  import { ReactEditor } from 'slate-react';
2
- export declare const withMaxLength: (maxLength: number) => (editor: ReactEditor) => ReactEditor;
2
+ export declare const withMaxLength: (maxLength: number | undefined) => (editor: ReactEditor) => ReactEditor;
3
3
  export declare const withMentions: (editor: any) => any;
package/cjs/chat/hocs.js CHANGED
@@ -8,31 +8,30 @@ var _slate = require("slate");
8
8
  var _utils = require("./utils");
9
9
  const withMaxLength = maxLength => function Plugin(editor) {
10
10
  const {
11
- insertText,
12
11
  insertData
13
12
  } = editor;
14
13
 
15
- // we are preventing inserting text if it exceeds maxLength in onDOMBeforeInput event handler in Editor component
16
- // but I'd leave it here as well for safety in case onDOMBeforeInput stops working for some reason + we still need
17
- // insertData to handle copy+paste logic which doesn't trigger onDOMBeforeInput
18
- editor.insertText = text => {
19
- const existingMentionsCount = (0, _utils.getMentionsCount)(editor.children);
20
- if (_slate.Editor.string(editor, []).length + existingMentionsCount < maxLength) {
21
- insertText(text);
22
- }
23
- };
24
- editor.insertData = data => {
25
- const existingMentionsCount = (0, _utils.getMentionsCount)(editor.children);
26
- const text = data.getData('text/plain');
27
- const html = data.getData('text/html');
28
- const parsedHtml = new DOMParser().parseFromString(html, 'text/html');
29
- const mentionNodes = parsedHtml.querySelectorAll('.mention');
30
- const mentionsText = Array.from(mentionNodes).map(node => node.innerText).join('');
31
- const itemsLengthToBeInserted = mentionNodes.length ? text.length - mentionsText.length : text.length;
32
- if (_slate.Editor.string(editor, []).length + itemsLengthToBeInserted + existingMentionsCount <= maxLength) {
33
- insertData(data);
34
- }
35
- };
14
+ // usual text insertion prevention is handled in onDOMBeforeInput event on the Editable component
15
+ // this case prevents copy-paste of text that would exceed the maxLength
16
+ // copy-paste could have been also handled in onDOMBeforeInput, but for
17
+ // some reason sometimes Slate stops triggering onDOMBeforeInput for copy-paste ¯\_(ツ)_/¯
18
+ if (maxLength) {
19
+ editor.insertData = data => {
20
+ const existingMentionsCount = (0, _utils.getMentionsCount)(editor.children);
21
+ // other methods for inserting text does not take into account new lines, so when copy-pasting
22
+ // we should also remove them, otherwise there is a bug when entered text cannot be cut and pasted
23
+ // because on paste text length is considered longer due to \n characters
24
+ const text = data.getData('text/plain')?.replace(/\n/g, '');
25
+ const html = data.getData('text/html');
26
+ const parsedHtml = new DOMParser().parseFromString(html, 'text/html');
27
+ const mentionNodes = parsedHtml.querySelectorAll('.mention');
28
+ const mentionsText = Array.from(mentionNodes).map(node => node.innerText).join('');
29
+ const itemsLengthToBeInserted = mentionNodes.length ? text.length - mentionsText.length : text.length;
30
+ if (_slate.Editor.string(editor, []).length + itemsLengthToBeInserted + existingMentionsCount <= maxLength) {
31
+ insertData(data);
32
+ }
33
+ };
34
+ }
36
35
  return editor;
37
36
  };
38
37
  exports.withMaxLength = withMaxLength;
@@ -1,5 +1,7 @@
1
1
  export { Chat } from './chat';
2
+ export { CharactersCounter } from './characters-counter';
2
3
  export { isMentionElement, resetNodes } from './utils';
4
+ export { withMaxLength } from './hocs';
3
5
  export { DEFAULT_VALUE, NEW_ITEM_KEY } from './constants';
4
6
  export type { ChatMessageItem, ActiveItems } from './types';
5
7
  export type { ChatProps } from './chat';
package/cjs/chat/index.js CHANGED
@@ -3,6 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "CharactersCounter", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _charactersCounter.CharactersCounter;
10
+ }
11
+ });
6
12
  Object.defineProperty(exports, "Chat", {
7
13
  enumerable: true,
8
14
  get: function () {
@@ -33,6 +39,14 @@ Object.defineProperty(exports, "resetNodes", {
33
39
  return _utils.resetNodes;
34
40
  }
35
41
  });
42
+ Object.defineProperty(exports, "withMaxLength", {
43
+ enumerable: true,
44
+ get: function () {
45
+ return _hocs.withMaxLength;
46
+ }
47
+ });
36
48
  var _chat = require("./chat");
49
+ var _charactersCounter = require("./characters-counter");
37
50
  var _utils = require("./utils");
51
+ var _hocs = require("./hocs");
38
52
  var _constants = require("./constants");
@@ -24,5 +24,7 @@ export type InlineTextEditorProps = {
24
24
  readOnly?: boolean;
25
25
  deleteEditorContentButtonAriaLabel?: string;
26
26
  autoFocus?: boolean;
27
+ maxLength?: number;
28
+ getCharactersCounterString?: (count: number) => string;
27
29
  };
28
- export default function InlineTextEditor({ name, type, readOnly, initialValue, placeholder, ariaLabel, className, enabledTools, header, toolbarPosition, height, onCancel, onSave, autoFocus, deleteEditorContentButtonAriaLabel, }: InlineTextEditorProps): import("react/jsx-runtime").JSX.Element;
30
+ export default function InlineTextEditor({ name, type, readOnly, initialValue, placeholder, ariaLabel, className, enabledTools, header, toolbarPosition, height, onCancel, onSave, autoFocus, deleteEditorContentButtonAriaLabel, maxLength, getCharactersCounterString, }: InlineTextEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -30,7 +30,9 @@ function InlineTextEditor({
30
30
  onCancel,
31
31
  onSave,
32
32
  autoFocus = false,
33
- deleteEditorContentButtonAriaLabel
33
+ deleteEditorContentButtonAriaLabel,
34
+ maxLength,
35
+ getCharactersCounterString
34
36
  }) {
35
37
  const {
36
38
  t
@@ -102,7 +104,9 @@ function InlineTextEditor({
102
104
  height: height,
103
105
  testId: "inline-text-editor-edit",
104
106
  contentAriaLabel: ariaLabel || titleValue,
105
- deleteEditorContentButtonAriaLabel: deleteEditorContentButtonAriaLabel ?? t('Delete Editor Content')
107
+ deleteEditorContentButtonAriaLabel: deleteEditorContentButtonAriaLabel ?? t('Delete Editor Content'),
108
+ maxLength: maxLength,
109
+ getCharactersCounterString: getCharactersCounterString
106
110
  }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)("div", {
107
111
  "test-id": "inline-text-editor-view-container",
108
112
  className: (0, _classnames.default)('inline-text-editor-view-container', isFlex && 'is-flex'),
@@ -74,8 +74,6 @@ export interface SlateReact {
74
74
  Slate: FC<SlateComponent>;
75
75
  withReact: typeof withReact;
76
76
  }
77
- export declare function instanceOfContentChild(obj: TextEditorValue | TextEditorValueChild): obj is TextEditorValueChild;
78
- export declare function instanceOfContent(obj: TextEditorValue | TextEditorValueChild): obj is TextEditorValue;
79
77
  export declare const TEXT_EDITOR_DEFAULT_VALUE: TextEditorValue[];
80
78
  export declare const TEXT_EDITOR_DEFAULT_TOOLS: TextEditorTool[];
81
79
  export interface TextEditorToolbarLabels {
@@ -4,8 +4,6 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.TOOLBAR_POSITION = exports.TEXT_EDITOR_TOOLS = exports.TEXT_EDITOR_DEFAULT_VALUE = exports.TEXT_EDITOR_DEFAULT_TOOLS = exports.MARK_TYPES = exports.INLINE_EDITOR_TYPES = exports.HEADER_TITLE_MAX_LENGTH = exports.HEADER_HEIGHT = exports.BLOCK_TYPES = void 0;
7
- exports.instanceOfContent = instanceOfContent;
8
- exports.instanceOfContentChild = instanceOfContentChild;
9
7
  const MARK_TYPES = exports.MARK_TYPES = {
10
8
  BOLD: 'bold',
11
9
  ITALIC: 'italic',
@@ -39,12 +37,6 @@ const TEXT_EDITOR_TOOLS = exports.TEXT_EDITOR_TOOLS = {
39
37
  CODE: 'code',
40
38
  CLEAR_FORMATTING: 'clear-formatting'
41
39
  };
42
- function instanceOfContentChild(obj) {
43
- return 'text' in obj;
44
- }
45
- function instanceOfContent(obj) {
46
- return !('text' in obj);
47
- }
48
40
  const TEXT_EDITOR_DEFAULT_VALUE = exports.TEXT_EDITOR_DEFAULT_VALUE = [{
49
41
  type: BLOCK_TYPES.REGULAR_TEXT,
50
42
  children: [{
@@ -35,5 +35,6 @@ export type TextEditorContentProps = {
35
35
  leafRender?: FC<LeafProps>;
36
36
  onBlur?: () => void;
37
37
  onFocus?: () => void;
38
+ maxLength?: number;
38
39
  };
39
- export default function TextEditorContent({ ariaAttrs, testId, placeholder, readOnly, autoFocus, isScrollable, elementRender, leafRender, onBlur, onFocus, }: TextEditorContentProps): import("react/jsx-runtime").JSX.Element;
40
+ export default function TextEditorContent({ ariaAttrs, testId, placeholder, readOnly, autoFocus, isScrollable, elementRender, leafRender, onBlur, onFocus, maxLength, }: TextEditorContentProps): import("react/jsx-runtime").JSX.Element;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.Placeholder = exports.Leaf = exports.Element = void 0;
7
7
  exports.default = TextEditorContent;
8
8
  var _react = _interopRequireWildcard(require("react"));
9
+ var _slate = require("slate");
9
10
  var _classnames = _interopRequireDefault(require("classnames"));
10
11
  var _textEditorContext = _interopRequireDefault(require("./text-editor-context"));
11
12
  var _textEditorConstants = require("./text-editor-constants");
@@ -158,14 +159,17 @@ function TextEditorContent({
158
159
  elementRender = Element,
159
160
  leafRender = Leaf,
160
161
  onBlur = () => {},
161
- onFocus = () => {}
162
+ onFocus = () => {},
163
+ maxLength
162
164
  }) {
163
165
  const {
164
166
  slateReact
165
167
  } = (0, _react.useContext)(_textEditorContext.default);
166
168
  const {
167
- Editable
169
+ Editable,
170
+ useSlate
168
171
  } = slateReact;
172
+ const editor = useSlate();
169
173
  /* eslint-disable-next-line testing-library/render-result-naming-convention */
170
174
  const ElementNode = elementRender;
171
175
  /* eslint-disable-next-line testing-library/render-result-naming-convention */
@@ -202,6 +206,22 @@ function TextEditorContent({
202
206
  renderLeaf: renderLeaf,
203
207
  onBlur: onBlur,
204
208
  onFocus: onFocus,
209
+ onDOMBeforeInput: event => {
210
+ // NOTE: copy-paste events handled in withMaxLength() plugin because Slate under some
211
+ // circumstances stops triggering onDOMBeforeInput event for copy-paste ¯\_(ツ)_/¯
212
+
213
+ // no need to do any calculations if maxLength is not set or if it's a delete event
214
+ if (maxLength && !event.inputType.startsWith('delete')) {
215
+ const currentTextLength = _slate.Editor.string(editor, []).length;
216
+ let newText = '';
217
+ if (!!event.data) {
218
+ newText = event.data;
219
+ }
220
+ if (currentTextLength + newText.length > maxLength) {
221
+ event.preventDefault();
222
+ }
223
+ }
224
+ },
205
225
  ...ariaAttrs
206
226
  })
207
227
  });
@@ -29,7 +29,6 @@ function TextEditorHeader({
29
29
  const {
30
30
  t
31
31
  } = (0, _useTranslation.useTranslation)();
32
- const [isDelete, setIsDelete] = (0, _react.useState)(false);
33
32
  const {
34
33
  slate,
35
34
  slateReact
@@ -41,13 +40,6 @@ function TextEditorHeader({
41
40
  const editor = slateReact.useSlate();
42
41
  const hasReadOnlyTitle = title && !isEditable;
43
42
  const hasEditableTitle = isEditable && onChangeTitle;
44
- (0, _react.useEffect)(() => {
45
- if (isDelete) {
46
- onChangeTitle('');
47
- onDelete();
48
- setIsDelete(false);
49
- }
50
- }, [isDelete]);
51
43
  return /*#__PURE__*/(0, _jsxRuntime.jsx)("header", {
52
44
  className: "text-editor-header",
53
45
  "test-id": "text-editor-header",
@@ -71,7 +63,8 @@ function TextEditorHeader({
71
63
  onClick: () => {
72
64
  textEditorService.deleteContent(editor);
73
65
  ReactEditor.focus(editor);
74
- setIsDelete(true);
66
+ onChangeTitle('');
67
+ onDelete();
75
68
  },
76
69
  accentType: _button.ACCENT_TYPES.ROUND_ICON,
77
70
  tooltipText: t('Delete'),
@@ -10,6 +10,7 @@ export declare function isEmptyTextEditorValue(value: TextEditorValue[]): boolea
10
10
  * @returns {boolean}
11
11
  */
12
12
  export declare function isLongerTextEditorValueThan(value: TextEditorValue[] | TextEditorValueChild[], maxLength?: number): boolean;
13
+ export declare function getTextEditorValueLength(value: TextEditorValue[] | TextEditorValueChild[]): number;
13
14
  /**
14
15
  * @param {Array} value - the type of value is defined with TextEditorValue
15
16
  * @param {number} maxTextLines
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.convertTextToTextEditorValue = convertTextToTextEditorValue;
7
+ exports.getTextEditorValueLength = getTextEditorValueLength;
7
8
  exports.hasGreaterTextEditorLinesThan = hasGreaterTextEditorLinesThan;
8
9
  exports.isEmptyTextEditorValue = isEmptyTextEditorValue;
9
10
  exports.isLongerTextEditorValueThan = isLongerTextEditorValueThan;
@@ -26,29 +27,33 @@ function isEmptyTextEditorValue(value) {
26
27
  function isLongerTextEditorValueThan(value, maxLength = 120) {
27
28
  let length = 0;
28
29
  for (const record of value) {
29
- if ((0, _textEditorConstants.instanceOfContentChild)(record) && !!record.text) {
30
- length += record.text.length;
31
- }
32
- if ((0, _textEditorConstants.instanceOfContent)(record) && !!record.children) {
33
- for (const childRecord of record.children) {
34
- if (!!childRecord.text) {
35
- length += childRecord.text.length;
36
- }
37
-
38
- // We already know the result
39
- if (length > maxLength) {
40
- break;
41
- }
42
- }
43
- }
44
-
45
- // We already know the result
30
+ // if we reached maxLength - no need to continue
46
31
  if (length > maxLength) {
47
32
  break;
48
33
  }
34
+ // if this is leaf node that has text - sum it up
35
+ if (record.hasOwnProperty('text') && !!record.text) {
36
+ length += record?.text?.length ?? 0;
37
+ } else if (record.hasOwnProperty('children') && !!record.children) {
38
+ // if this is a node that has children - calculate length of children
39
+ length += getTextEditorValueLength(record.children);
40
+ }
49
41
  }
50
42
  return length > maxLength;
51
43
  }
44
+ function getTextEditorValueLength(value) {
45
+ let length = 0;
46
+ for (const record of value) {
47
+ // if this is leaf node that has text - sum it up
48
+ if (record.hasOwnProperty('text') && !!record.text) {
49
+ length += record?.text?.length ?? 0;
50
+ } else if (record.hasOwnProperty('children') && !!record.children) {
51
+ // if this is a node that has children - iterate through them via recursion
52
+ length += getTextEditorValueLength(record.children);
53
+ }
54
+ }
55
+ return length;
56
+ }
52
57
 
53
58
  /**
54
59
  * @param {Array} value - the type of value is defined with TextEditorValue
@@ -38,6 +38,8 @@ export type TextEditorComponentProps = {
38
38
  onBlur?: () => void;
39
39
  deleteEditorContentButtonAriaLabel: string;
40
40
  id: string;
41
+ maxLength?: number;
42
+ getCharactersCounterString?: (count: number) => string;
41
43
  };
42
44
  export type TextEditorProps = {
43
45
  name: string;
@@ -94,9 +96,11 @@ export type TextEditorProps = {
94
96
  linkInputLabelText?: string;
95
97
  deleteEditorContentButtonAriaLabel: string;
96
98
  id?: string;
99
+ maxLength?: number;
100
+ getCharactersCounterString?: (count: number) => string;
97
101
  };
98
102
  /**
99
103
  * @midnight-gray-supported
100
104
  * @alpine-light-supported
101
105
  * */
102
- export default function TextEditor({ name, header, type, value, initialValue, validity, label, placeholder, autoFocus, readOnly, testId, contentTestId, contentAriaLabel, className, height, enabledTools, toolbarPosition, elementRender, leafRender, onChange, onCancel, onSave, tooltipTextBold, tooltipTextItalic, tooltipTextUnderline, tooltipTextSuperscript, tooltipTextLink, tooltipTextRemoveLink, tooltipTextBulletedList, tooltipTextNumberedList, tooltipTextClearFormatting, tooltipTextCode, urlIsRequiredMessage, enterValidUrlMessage, saveButtonText, headingLargeText, headingMediumText, headingSmallText, regularText, validationErrorWarningMessage, linkInputLabelText, deleteEditorContentButtonAriaLabel, id, }: TextEditorProps): import("react/jsx-runtime").JSX.Element;
106
+ export default function TextEditor({ name, header, type, value, initialValue, validity, label, placeholder, autoFocus, readOnly, testId, contentTestId, contentAriaLabel, className, height, enabledTools, toolbarPosition, elementRender, leafRender, onChange, onCancel, onSave, tooltipTextBold, tooltipTextItalic, tooltipTextUnderline, tooltipTextSuperscript, tooltipTextLink, tooltipTextRemoveLink, tooltipTextBulletedList, tooltipTextNumberedList, tooltipTextClearFormatting, tooltipTextCode, urlIsRequiredMessage, enterValidUrlMessage, saveButtonText, headingLargeText, headingMediumText, headingSmallText, regularText, validationErrorWarningMessage, linkInputLabelText, deleteEditorContentButtonAriaLabel, id, maxLength, getCharactersCounterString, }: TextEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -18,6 +18,8 @@ var _withLinks = _interopRequireDefault(require("./hooks/with-links"));
18
18
  var _textEditorLoader = _interopRequireDefault(require("./text-editor-loader"));
19
19
  var _textEditorConstants = require("./text-editor-constants");
20
20
  var _textEditorHooks = require("./text-editor-hooks");
21
+ var _textEditorHelpers = require("./text-editor-helpers");
22
+ var _useTranslation = require("../hooks/use-translation");
21
23
  var _jsxRuntime = require("react/jsx-runtime");
22
24
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
23
25
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
@@ -46,8 +48,13 @@ function TextEditorComponent({
46
48
  leafRender = _textEditorContent.Leaf,
47
49
  onChange = () => {},
48
50
  deleteEditorContentButtonAriaLabel,
49
- id = ''
51
+ id = '',
52
+ maxLength,
53
+ getCharactersCounterString
50
54
  }) {
55
+ const {
56
+ t
57
+ } = (0, _useTranslation.useTranslation)();
51
58
  const {
52
59
  slate,
53
60
  slateReact
@@ -62,7 +69,15 @@ function TextEditorComponent({
62
69
  withReact,
63
70
  Slate
64
71
  } = slateReact;
65
- const editor = (0, _react.useMemo)(() => (0, _withLinks.default)((0, _slateHistory.withHistory)(withReact(createEditor()))), []);
72
+ const getCharactersCounterStringText = (0, _react.useCallback)(count => {
73
+ if (getCharactersCounterString) {
74
+ return getCharactersCounterString(count);
75
+ }
76
+ return t('{{count}} characters remaining', {
77
+ count
78
+ });
79
+ }, [getCharactersCounterString]);
80
+ const editor = (0, _react.useMemo)(() => (0, _chat.withMaxLength)(maxLength)((0, _withLinks.default)((0, _slateHistory.withHistory)(withReact(createEditor())))), [maxLength]);
66
81
  const selection = (0, _react.useRef)(null);
67
82
  const slateValue = value || initialValue || _textEditorConstants.TEXT_EDITOR_DEFAULT_VALUE;
68
83
  (0, _react.useEffect)(() => {
@@ -127,7 +142,8 @@ function TextEditorComponent({
127
142
  hasDeleteButton: header.hasDeleteButton,
128
143
  onDelete: header.onDelete,
129
144
  deleteEditorContentButtonAriaLabel: deleteEditorContentButtonAriaLabel,
130
- inputAriaLabel: header.inputAriaLabel
145
+ inputAriaLabel: header.inputAriaLabel,
146
+ maxLength: header.maxLength
131
147
  }), toolbarPosition === _textEditorConstants.TOOLBAR_POSITION.TOP && toolbarMemo, /*#__PURE__*/(0, _jsxRuntime.jsx)(_textEditorContent.default, {
132
148
  placeholder: placeholder,
133
149
  readOnly: readOnly,
@@ -137,9 +153,14 @@ function TextEditorComponent({
137
153
  leafRender: leafRender,
138
154
  onBlur: saveSelection,
139
155
  onFocus: recoverSelection,
140
- ariaAttrs: ariaAttrs
156
+ ariaAttrs: ariaAttrs,
157
+ maxLength: maxLength
141
158
  }), toolbarPosition === _textEditorConstants.TOOLBAR_POSITION.BOTTOM && toolbarMemo]
142
159
  })
160
+ }), maxLength && /*#__PURE__*/(0, _jsxRuntime.jsx)(_chat.CharactersCounter, {
161
+ currLength: (0, _textEditorHelpers.getTextEditorValueLength)(slateValue),
162
+ maxLength: maxLength,
163
+ getCharactersCounterString: getCharactersCounterStringText
143
164
  }), validity && /*#__PURE__*/(0, _jsxRuntime.jsx)(_formField.ValidityMessages, {
144
165
  validity: validity,
145
166
  validationTestId: `${name}-validity`,
@@ -194,7 +215,9 @@ function TextEditor({
194
215
  validationErrorWarningMessage,
195
216
  linkInputLabelText,
196
217
  deleteEditorContentButtonAriaLabel,
197
- id = ''
218
+ id = '',
219
+ maxLength,
220
+ getCharactersCounterString
198
221
  }) {
199
222
  const [isFocused, setIsFocused] = (0, _react.useState)(false);
200
223
  const defaultTranslations = (0, _textEditorHooks.useTextEditorDefaultTranslations)();
@@ -252,7 +275,9 @@ function TextEditor({
252
275
  leafRender: leafRender,
253
276
  onChange: onChange,
254
277
  deleteEditorContentButtonAriaLabel: deleteEditorContentButtonAriaLabel,
255
- id: id
278
+ id: id,
279
+ maxLength: maxLength,
280
+ getCharactersCounterString: getCharactersCounterString
256
281
  })
257
282
  })
258
283
  });
@@ -144,10 +144,15 @@ function ChatMessageBodyBase({
144
144
  // but there we prevent it only from updating slate inner state, html input still
145
145
  // adds entered text to the DOM input field causing buggy behavior: https://datarobot.atlassian.net/browse/TESTLIO-2963
146
146
  // preventing default from the onDOMBeforeInput won't add new character to the DOM input field.
147
- // we still won't to leave logic in hocs.ts as a fallback + it also covers copy+paste logic which doesn't trigger onDOMBeforeInput
148
- const existingMentionsCount = getMentionsCount(editor.children);
149
- if (Editor.string(editor, []).length + existingMentionsCount + (event?.data?.length ?? 0) > maxLength) {
150
- event.preventDefault();
147
+ // we still want to leave logic in hocs.ts because it covers copy+paste case, as for some reason
148
+ // under certain circumstances onDOMBeforeInput stops being triggering for copy+paste so having there is safer
149
+
150
+ // we want to always allow deleting text
151
+ if (!event.inputType.startsWith('delete')) {
152
+ const existingMentionsCount = getMentionsCount(editor.children);
153
+ if (!!event.data && Editor.string(editor, []).length + existingMentionsCount + (event?.data?.length ?? 0) > maxLength) {
154
+ event.preventDefault();
155
+ }
151
156
  }
152
157
  }
153
158
  })
@@ -1,3 +1,3 @@
1
1
  import { ReactEditor } from 'slate-react';
2
- export declare const withMaxLength: (maxLength: number) => (editor: ReactEditor) => ReactEditor;
2
+ export declare const withMaxLength: (maxLength: number | undefined) => (editor: ReactEditor) => ReactEditor;
3
3
  export declare const withMentions: (editor: any) => any;
package/esm/chat/hocs.js CHANGED
@@ -2,31 +2,30 @@ import { Editor } from 'slate';
2
2
  import { getMentionsCount, isMentionElement } from './utils';
3
3
  export const withMaxLength = maxLength => function Plugin(editor) {
4
4
  const {
5
- insertText,
6
5
  insertData
7
6
  } = editor;
8
7
 
9
- // we are preventing inserting text if it exceeds maxLength in onDOMBeforeInput event handler in Editor component
10
- // but I'd leave it here as well for safety in case onDOMBeforeInput stops working for some reason + we still need
11
- // insertData to handle copy+paste logic which doesn't trigger onDOMBeforeInput
12
- editor.insertText = text => {
13
- const existingMentionsCount = getMentionsCount(editor.children);
14
- if (Editor.string(editor, []).length + existingMentionsCount < maxLength) {
15
- insertText(text);
16
- }
17
- };
18
- editor.insertData = data => {
19
- const existingMentionsCount = getMentionsCount(editor.children);
20
- const text = data.getData('text/plain');
21
- const html = data.getData('text/html');
22
- const parsedHtml = new DOMParser().parseFromString(html, 'text/html');
23
- const mentionNodes = parsedHtml.querySelectorAll('.mention');
24
- const mentionsText = Array.from(mentionNodes).map(node => node.innerText).join('');
25
- const itemsLengthToBeInserted = mentionNodes.length ? text.length - mentionsText.length : text.length;
26
- if (Editor.string(editor, []).length + itemsLengthToBeInserted + existingMentionsCount <= maxLength) {
27
- insertData(data);
28
- }
29
- };
8
+ // usual text insertion prevention is handled in onDOMBeforeInput event on the Editable component
9
+ // this case prevents copy-paste of text that would exceed the maxLength
10
+ // copy-paste could have been also handled in onDOMBeforeInput, but for
11
+ // some reason sometimes Slate stops triggering onDOMBeforeInput for copy-paste ¯\_(ツ)_/¯
12
+ if (maxLength) {
13
+ editor.insertData = data => {
14
+ const existingMentionsCount = getMentionsCount(editor.children);
15
+ // other methods for inserting text does not take into account new lines, so when copy-pasting
16
+ // we should also remove them, otherwise there is a bug when entered text cannot be cut and pasted
17
+ // because on paste text length is considered longer due to \n characters
18
+ const text = data.getData('text/plain')?.replace(/\n/g, '');
19
+ const html = data.getData('text/html');
20
+ const parsedHtml = new DOMParser().parseFromString(html, 'text/html');
21
+ const mentionNodes = parsedHtml.querySelectorAll('.mention');
22
+ const mentionsText = Array.from(mentionNodes).map(node => node.innerText).join('');
23
+ const itemsLengthToBeInserted = mentionNodes.length ? text.length - mentionsText.length : text.length;
24
+ if (Editor.string(editor, []).length + itemsLengthToBeInserted + existingMentionsCount <= maxLength) {
25
+ insertData(data);
26
+ }
27
+ };
28
+ }
30
29
  return editor;
31
30
  };
32
31
  export const withMentions = editor => {
@@ -1,5 +1,7 @@
1
1
  export { Chat } from './chat';
2
+ export { CharactersCounter } from './characters-counter';
2
3
  export { isMentionElement, resetNodes } from './utils';
4
+ export { withMaxLength } from './hocs';
3
5
  export { DEFAULT_VALUE, NEW_ITEM_KEY } from './constants';
4
6
  export type { ChatMessageItem, ActiveItems } from './types';
5
7
  export type { ChatProps } from './chat';
package/esm/chat/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { Chat } from './chat';
2
+ export { CharactersCounter } from './characters-counter';
2
3
  export { isMentionElement, resetNodes } from './utils';
4
+ export { withMaxLength } from './hocs';
3
5
  export { DEFAULT_VALUE, NEW_ITEM_KEY } from './constants';
@@ -24,5 +24,7 @@ export type InlineTextEditorProps = {
24
24
  readOnly?: boolean;
25
25
  deleteEditorContentButtonAriaLabel?: string;
26
26
  autoFocus?: boolean;
27
+ maxLength?: number;
28
+ getCharactersCounterString?: (count: number) => string;
27
29
  };
28
- export default function InlineTextEditor({ name, type, readOnly, initialValue, placeholder, ariaLabel, className, enabledTools, header, toolbarPosition, height, onCancel, onSave, autoFocus, deleteEditorContentButtonAriaLabel, }: InlineTextEditorProps): import("react/jsx-runtime").JSX.Element;
30
+ export default function InlineTextEditor({ name, type, readOnly, initialValue, placeholder, ariaLabel, className, enabledTools, header, toolbarPosition, height, onCancel, onSave, autoFocus, deleteEditorContentButtonAriaLabel, maxLength, getCharactersCounterString, }: InlineTextEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -22,7 +22,9 @@ export default function InlineTextEditor({
22
22
  onCancel,
23
23
  onSave,
24
24
  autoFocus = false,
25
- deleteEditorContentButtonAriaLabel
25
+ deleteEditorContentButtonAriaLabel,
26
+ maxLength,
27
+ getCharactersCounterString
26
28
  }) {
27
29
  const {
28
30
  t
@@ -94,7 +96,9 @@ export default function InlineTextEditor({
94
96
  height: height,
95
97
  testId: "inline-text-editor-edit",
96
98
  contentAriaLabel: ariaLabel || titleValue,
97
- deleteEditorContentButtonAriaLabel: deleteEditorContentButtonAriaLabel ?? t('Delete Editor Content')
99
+ deleteEditorContentButtonAriaLabel: deleteEditorContentButtonAriaLabel ?? t('Delete Editor Content'),
100
+ maxLength: maxLength,
101
+ getCharactersCounterString: getCharactersCounterString
98
102
  }) : /*#__PURE__*/_jsxs("div", {
99
103
  "test-id": "inline-text-editor-view-container",
100
104
  className: classnames('inline-text-editor-view-container', isFlex && 'is-flex'),
@@ -74,8 +74,6 @@ export interface SlateReact {
74
74
  Slate: FC<SlateComponent>;
75
75
  withReact: typeof withReact;
76
76
  }
77
- export declare function instanceOfContentChild(obj: TextEditorValue | TextEditorValueChild): obj is TextEditorValueChild;
78
- export declare function instanceOfContent(obj: TextEditorValue | TextEditorValueChild): obj is TextEditorValue;
79
77
  export declare const TEXT_EDITOR_DEFAULT_VALUE: TextEditorValue[];
80
78
  export declare const TEXT_EDITOR_DEFAULT_TOOLS: TextEditorTool[];
81
79
  export interface TextEditorToolbarLabels {
@@ -31,12 +31,6 @@ export const TEXT_EDITOR_TOOLS = {
31
31
  CODE: 'code',
32
32
  CLEAR_FORMATTING: 'clear-formatting'
33
33
  };
34
- export function instanceOfContentChild(obj) {
35
- return 'text' in obj;
36
- }
37
- export function instanceOfContent(obj) {
38
- return !('text' in obj);
39
- }
40
34
  export const TEXT_EDITOR_DEFAULT_VALUE = [{
41
35
  type: BLOCK_TYPES.REGULAR_TEXT,
42
36
  children: [{
@@ -35,5 +35,6 @@ export type TextEditorContentProps = {
35
35
  leafRender?: FC<LeafProps>;
36
36
  onBlur?: () => void;
37
37
  onFocus?: () => void;
38
+ maxLength?: number;
38
39
  };
39
- export default function TextEditorContent({ ariaAttrs, testId, placeholder, readOnly, autoFocus, isScrollable, elementRender, leafRender, onBlur, onFocus, }: TextEditorContentProps): import("react/jsx-runtime").JSX.Element;
40
+ export default function TextEditorContent({ ariaAttrs, testId, placeholder, readOnly, autoFocus, isScrollable, elementRender, leafRender, onBlur, onFocus, maxLength, }: TextEditorContentProps): import("react/jsx-runtime").JSX.Element;