@rh-support/components 2.5.222 → 2.5.225

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 (49) hide show
  1. package/lib/esm/ConfirmationModal/ConfirmationDialog.js +1 -1
  2. package/lib/esm/CustomDatePicker/CustomDatePicker.d.ts +18 -0
  3. package/lib/esm/CustomDatePicker/CustomDatePicker.d.ts.map +1 -0
  4. package/lib/esm/CustomDatePicker/CustomDatePicker.js +27 -0
  5. package/lib/esm/CustomDatePicker/customDatePicker.css +52 -0
  6. package/lib/esm/CustomDatePicker/index.d.ts +2 -0
  7. package/lib/esm/CustomDatePicker/index.d.ts.map +1 -0
  8. package/lib/esm/CustomDatePicker/index.js +1 -0
  9. package/lib/esm/Functional/CaseContactsSelectorExternal.d.ts +2 -0
  10. package/lib/esm/Functional/CaseContactsSelectorExternal.d.ts.map +1 -1
  11. package/lib/esm/Functional/CaseContactsSelectorExternal.js +156 -82
  12. package/lib/esm/InlineEdit/NewInlineEdit.d.ts +2 -0
  13. package/lib/esm/InlineEdit/NewInlineEdit.d.ts.map +1 -1
  14. package/lib/esm/InlineEdit/NewInlineEdit.js +7 -2
  15. package/lib/esm/OwnerTypeaheadDropdown/OwnerTypeaheadDropdown.d.ts +2 -0
  16. package/lib/esm/OwnerTypeaheadDropdown/OwnerTypeaheadDropdown.d.ts.map +1 -1
  17. package/lib/esm/OwnerTypeaheadDropdown/OwnerTypeaheadDropdown.js +8 -3
  18. package/lib/esm/PhoneInput/PhoneInput.d.ts.map +1 -1
  19. package/lib/esm/PhoneInput/PhoneInput.js +38 -1
  20. package/lib/esm/RichTextEditor/RichTextEditor.d.ts +45 -0
  21. package/lib/esm/RichTextEditor/RichTextEditor.d.ts.map +1 -0
  22. package/lib/esm/RichTextEditor/RichTextEditor.js +463 -0
  23. package/lib/esm/RichTextEditor/index.d.ts +4 -0
  24. package/lib/esm/RichTextEditor/index.d.ts.map +1 -0
  25. package/lib/esm/RichTextEditor/index.js +2 -0
  26. package/lib/esm/RichTextEditor/richTextEditor.css +153 -0
  27. package/lib/esm/RichTextEditor/useQuillEditor.d.ts +20 -0
  28. package/lib/esm/RichTextEditor/useQuillEditor.d.ts.map +1 -0
  29. package/lib/esm/RichTextEditor/useQuillEditor.js +81 -0
  30. package/lib/esm/Table/TablePagination.d.ts +1 -0
  31. package/lib/esm/Table/TablePagination.d.ts.map +1 -1
  32. package/lib/esm/Table/TablePagination.js +1 -1
  33. package/lib/esm/helperFunctions/errorMessage.d.ts +2 -1
  34. package/lib/esm/helperFunctions/errorMessage.d.ts.map +1 -1
  35. package/lib/esm/helperFunctions/errorMessage.js +19 -5
  36. package/lib/esm/hooks/useCopyToClipboard.d.ts +1 -1
  37. package/lib/esm/hooks/useCopyToClipboard.d.ts.map +1 -1
  38. package/lib/esm/hooks/useCopyToClipboard.js +3 -1
  39. package/lib/esm/index.d.ts +2 -1
  40. package/lib/esm/index.d.ts.map +1 -1
  41. package/lib/esm/index.js +3 -1
  42. package/package.json +9 -7
  43. package/lib/esm/MarkdownEditor/MarkdownEditor.d.ts +0 -49
  44. package/lib/esm/MarkdownEditor/MarkdownEditor.d.ts.map +0 -1
  45. package/lib/esm/MarkdownEditor/MarkdownEditor.js +0 -601
  46. package/lib/esm/MarkdownEditor/index.d.ts +0 -2
  47. package/lib/esm/MarkdownEditor/index.d.ts.map +0 -1
  48. package/lib/esm/MarkdownEditor/index.js +0 -1
  49. package/lib/esm/MarkdownEditor/markdownEditor.css +0 -223
@@ -107,7 +107,11 @@ const formatNumber = (fullPhoneNumber, localFormat) => {
107
107
  }
108
108
  }
109
109
  const isPatternWithPrefix = (_a = pattern === null || pattern === void 0 ? void 0 : pattern.replace(/\s+/g, '')) === null || _a === void 0 ? void 0 : _a.startsWith(PREFIX);
110
+ // TODO: Temporary — return pattern without forcing + prefix for portal phone without country code.
111
+ pattern = isPatternWithPrefix ? pattern : pattern;
112
+ /* TODO: Revert to previous logic after phone format handling is finalized.
110
113
  pattern = isPatternWithPrefix ? pattern : !isEmpty(isPatternWithPrefix) ? PREFIX + pattern : '';
114
+ */
111
115
  return pattern;
112
116
  };
113
117
  // return phoneline and countrycode for any given phone
@@ -140,7 +144,11 @@ export function PhoneInput(props) {
140
144
  const setLocalFormat = (phoneNumber) => {
141
145
  const localPhone = removeAllChars(phoneNumber);
142
146
  if (shouldNotSetCountryCode) {
147
+ // TODO: Temporary — portal phone without country code; use mask without + prefix.
148
+ return DEFAULT_MASK;
149
+ /* TODO: Revert to previous logic after phone format handling is finalized.
143
150
  return DEFAULT_FORMAT; // default format for portal phone without country code
151
+ */
144
152
  }
145
153
  let localFormat;
146
154
  const exactCountry = getExactCountry(localPhone);
@@ -169,12 +177,23 @@ export function PhoneInput(props) {
169
177
  const localFormat = setLocalFormat(phone);
170
178
  const _formattedNumber = formatNumber(phone, localFormat);
171
179
  setFormattedNumber(_formattedNumber);
172
- if (!(phone === null || phone === void 0 ? void 0 : phone.trim().startsWith(PREFIX)) && regex.test(phone) && !isEqual(phone, '-')) {
180
+ // TODO: Temporary — do not prepend + when shouldNotSetCountryCode is true.
181
+ if (!shouldNotSetCountryCode &&
182
+ !(phone === null || phone === void 0 ? void 0 : phone.trim().startsWith(PREFIX)) &&
183
+ regex.test(phone) &&
184
+ !isEqual(phone, '-')) {
173
185
  onPhoneValueChange(PREFIX + (phone === null || phone === void 0 ? void 0 : phone.trim()));
174
186
  }
175
187
  else {
176
188
  onPhoneValueChange(phone === null || phone === void 0 ? void 0 : phone.slice(0, localFormat === null || localFormat === void 0 ? void 0 : localFormat.length));
177
189
  }
190
+ /* TODO: Revert to previous logic after phone format handling is finalized.
191
+ if (!phone?.trim().startsWith(PREFIX) && regex.test(phone) && !isEqual(phone, '-')) {
192
+ onPhoneValueChange(PREFIX + phone?.trim());
193
+ } else {
194
+ onPhoneValueChange(phone?.slice(0, localFormat?.length));
195
+ }
196
+ */
178
197
  };
179
198
  const onCountrySelect = (_event, itemId) => {
180
199
  var _a, _b;
@@ -233,10 +252,18 @@ export function PhoneInput(props) {
233
252
  onPhoneValueChange('');
234
253
  }
235
254
  else {
255
+ // TODO: Temporary — strip leading + when shouldNotSetCountryCode is true.
256
+ const normalizedPhone = shouldNotSetCountryCode ? (phoneValue === null || phoneValue === void 0 ? void 0 : phoneValue.replace(/^\++/, '')) || '' : phoneValue;
257
+ onPhoneValueChange(normalizedPhone);
258
+ const localFormat = setLocalFormat(normalizedPhone);
259
+ const _formattedNumber = formatNumber(normalizedPhone, localFormat);
260
+ setFormattedNumber(_formattedNumber);
261
+ /* TODO: Revert to previous logic after phone format handling is finalized.
236
262
  onPhoneValueChange(phoneValue);
237
263
  const localFormat = setLocalFormat(phoneValue);
238
264
  const _formattedNumber = formatNumber(phoneValue, localFormat);
239
265
  setFormattedNumber(_formattedNumber);
266
+ */
240
267
  }
241
268
  // eslint-disable-next-line react-hooks/exhaustive-deps
242
269
  }, [phoneValue]);
@@ -251,10 +278,20 @@ export function PhoneInput(props) {
251
278
  else {
252
279
  onCountryCodeChange && onCountryCodeChange(PREFIX + countryCode);
253
280
  }
281
+ // TODO: Temporary — strip leading + when shouldNotSetCountryCode is true.
282
+ const normalizedPhone = shouldNotSetCountryCode
283
+ ? removeAllSpaces((phoneValue === null || phoneValue === void 0 ? void 0 : phoneValue.replace(/^\++/, '')) || '')
284
+ : removeAllSpaces(phoneValue);
285
+ onPhoneValueChange && onPhoneValueChange(normalizedPhone);
286
+ const localFormat = setLocalFormat(normalizedPhone);
287
+ const _formattedNumber = formatNumber(normalizedPhone, localFormat);
288
+ setFormattedNumber(_formattedNumber);
289
+ /* TODO: Revert to previous logic after phone format handling is finalized.
254
290
  onPhoneValueChange && onPhoneValueChange(removeAllSpaces(phoneValue)); // removing trailing and in between spaces
255
291
  const localFormat = setLocalFormat(phoneValue);
256
292
  const _formattedNumber = formatNumber(phoneValue, localFormat);
257
293
  setFormattedNumber(_formattedNumber);
294
+ */
258
295
  // eslint-disable-next-line react-hooks/exhaustive-deps
259
296
  }, [shouldNotSetCountryCode]);
260
297
  const toggle = (React.createElement(MenuToggle, { ref: toggleRef, onClick: () => setIsOpen(!isOpen), isExpanded: isOpen, className: "phone-number-select", "data-tracking-id": "case-phone-number-country-code", isDisabled: props.isDisabled, "aria-label": "Country Code" }, iso2 && phoneValue !== '' ? placeholder : t('Country Code')));
@@ -0,0 +1,45 @@
1
+ import './richTextEditor.css';
2
+ import Quill from 'quill';
3
+ import React from 'react';
4
+ export interface IRichTextEditorProps {
5
+ /** HTML string value */
6
+ value: string;
7
+ /** Called with HTML string on change */
8
+ onChange: (html: string) => void;
9
+ /** Emit plain text for char counting or previews */
10
+ onChangePlainText?: (text: string) => void;
11
+ /** Placeholder text when editor is empty */
12
+ placeholder?: string;
13
+ /** Disable the editor */
14
+ disabled?: boolean;
15
+ /** CSS class for the outer container */
16
+ className?: string;
17
+ /** Unique ID */
18
+ id?: string;
19
+ /** Hide the formatting toolbar */
20
+ hideToolbar?: boolean;
21
+ /** Character limit (counted against plain text) */
22
+ charLimit?: number;
23
+ /** Threshold to show character counter */
24
+ minCharsForCounter?: number;
25
+ /** Callback when character limit is exceeded */
26
+ onCharLimitExceeded?: (exceeded: boolean) => void;
27
+ /** Editor height */
28
+ height?: string | number;
29
+ /** Focus handler */
30
+ onFocus?: () => void;
31
+ /** Blur handler */
32
+ onBlur?: () => void;
33
+ /** Ref binding for programmatic access to the Quill instance */
34
+ bindEditor?: (editor: Quill) => void;
35
+ /** Accessible label */
36
+ 'aria-label'?: string;
37
+ /**
38
+ * Async callback invoked when the user selects an image file (via toolbar or paste).
39
+ * Must upload the file and return the image URL to embed.
40
+ */
41
+ onImageUpload?: (file: File) => Promise<string>;
42
+ }
43
+ export declare function RichTextEditor(props: Readonly<IRichTextEditorProps>): React.JSX.Element;
44
+ export default RichTextEditor;
45
+ //# sourceMappingURL=RichTextEditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../../src/RichTextEditor/RichTextEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+B9B,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAmD,MAAM,OAAO,CAAC;AAMxE,MAAM,WAAW,oBAAoB;IACjC,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,oDAAoD;IACpD,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,kCAAkC;IAClC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0CAA0C;IAC1C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,oBAAoB;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,mBAAmB;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,gEAAgE;IAChE,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,IAAI,CAAC;IACrC,uBAAuB;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACnD;AAQD,wBAAgB,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,oBAAoB,CAAC,qBAgvBnE;AAED,eAAe,cAAc,CAAC"}
@@ -0,0 +1,463 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import './richTextEditor.css';
11
+ import { Button, ButtonVariant, Dropdown, DropdownItem, DropdownList, FormGroup, MenuToggle, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant, TextInput, Toolbar, ToolbarGroup, } from '@patternfly/react-core';
12
+ import AlignCenterIcon from '@patternfly/react-icons/dist/js/icons/align-center-icon';
13
+ import AlignJustifyIcon from '@patternfly/react-icons/dist/js/icons/align-justify-icon';
14
+ import AlignLeftIcon from '@patternfly/react-icons/dist/js/icons/align-left-icon';
15
+ import AlignRightIcon from '@patternfly/react-icons/dist/js/icons/align-right-icon';
16
+ import CaretDownIcon from '@patternfly/react-icons/dist/js/icons/caret-down-icon';
17
+ import IndentIcon from '@patternfly/react-icons/dist/js/icons/indent-icon';
18
+ import LinkIcon from '@patternfly/react-icons/dist/js/icons/link-icon';
19
+ import ListOlIcon from '@patternfly/react-icons/dist/js/icons/list-ol-icon';
20
+ import ListUlIcon from '@patternfly/react-icons/dist/js/icons/list-ul-icon';
21
+ import OutdentIcon from '@patternfly/react-icons/dist/js/icons/outdent-icon';
22
+ import RemoveFormatIcon from '@patternfly/react-icons/dist/js/icons/remove-format-icon';
23
+ import StrikethroughIcon from '@patternfly/react-icons/dist/js/icons/strikethrough-icon';
24
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
25
+ import { useTranslation } from 'react-i18next';
26
+ import { ToastNotification } from '../ToastNotification';
27
+ import { useQuillEditor } from './useQuillEditor';
28
+ const defaultCharLimit = 32000;
29
+ const defaultMinCharsForCounter = 31800;
30
+ const ALLOWED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']);
31
+ const IMAGE_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB
32
+ export function RichTextEditor(props) {
33
+ const { t } = useTranslation();
34
+ const { value, onChange, onChangePlainText, placeholder = '', disabled = false, className = '', id = 'rich-text-editor', hideToolbar = false, charLimit = defaultCharLimit, minCharsForCounter = defaultMinCharsForCounter, onCharLimitExceeded, height, onFocus, onBlur, bindEditor, 'aria-label': ariaLabel, onImageUpload, } = props;
35
+ const [isHeadingDropdownOpen, setIsHeadingDropdownOpen] = useState(false);
36
+ const [charCount, setCharCount] = useState(0);
37
+ const charLimitExceededRef = useRef(false);
38
+ const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
39
+ const [linkUrl, setLinkUrl] = useState('');
40
+ const [linkText, setLinkText] = useState('');
41
+ const savedSelectionRef = useRef(null);
42
+ const [isImageModalOpen, setIsImageModalOpen] = useState(false);
43
+ const [imageUrl, setImageUrl] = useState('');
44
+ const [imageUrlError, setImageUrlError] = useState('');
45
+ const savedImageSelectionRef = useRef(null);
46
+ const [, setIsImageUploading] = useState(false);
47
+ const fileInputRef = useRef(null);
48
+ const { editorRef, quillRef, activeFormat, setActiveFormat, setContents, setReadOnly } = useQuillEditor({
49
+ placeholder,
50
+ readOnly: disabled,
51
+ onChange: (html) => {
52
+ onChange(html);
53
+ },
54
+ onChangePlainText: (text) => {
55
+ onChangePlainText === null || onChangePlainText === void 0 ? void 0 : onChangePlainText(text);
56
+ setCharCount(text.length);
57
+ },
58
+ onFocus,
59
+ onBlur,
60
+ });
61
+ // Sync external value into Quill when it changes externally
62
+ useEffect(() => {
63
+ setContents(value);
64
+ }, [value, setContents]);
65
+ // Sync disabled state
66
+ useEffect(() => {
67
+ setReadOnly(disabled);
68
+ }, [disabled, setReadOnly]);
69
+ // Bind editor ref externally
70
+ useEffect(() => {
71
+ if (quillRef.current && bindEditor) {
72
+ bindEditor(quillRef.current);
73
+ }
74
+ }, [bindEditor, quillRef]);
75
+ // Character limit exceeded callback
76
+ useEffect(() => {
77
+ const exceeded = charCount > charLimit;
78
+ if (exceeded !== charLimitExceededRef.current) {
79
+ charLimitExceededRef.current = exceeded;
80
+ onCharLimitExceeded === null || onCharLimitExceeded === void 0 ? void 0 : onCharLimitExceeded(exceeded);
81
+ }
82
+ }, [charCount, charLimit, onCharLimitExceeded]);
83
+ const onImageUploadRef = useRef(onImageUpload);
84
+ onImageUploadRef.current = onImageUpload;
85
+ useEffect(() => {
86
+ const editorEl = editorRef.current;
87
+ if (!editorEl || !onImageUploadRef.current)
88
+ return;
89
+ const handlePaste = (e) => __awaiter(this, void 0, void 0, function* () {
90
+ var _a;
91
+ if (!((_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.items))
92
+ return;
93
+ const imageItem = Array.from(e.clipboardData.items).find((item) => item.type.startsWith('image/') && ALLOWED_IMAGE_TYPES.has(item.type));
94
+ if (!imageItem)
95
+ return;
96
+ const file = imageItem.getAsFile();
97
+ if (!file)
98
+ return;
99
+ if (file.size > IMAGE_SIZE_LIMIT) {
100
+ e.preventDefault();
101
+ e.stopPropagation();
102
+ return;
103
+ }
104
+ e.preventDefault();
105
+ e.stopPropagation();
106
+ const quill = quillRef.current;
107
+ if (!quill)
108
+ return;
109
+ const range = quill.getSelection() || { index: quill.getLength() - 1, length: 0 };
110
+ setIsImageUploading(true);
111
+ try {
112
+ const url = yield onImageUploadRef.current(file);
113
+ if (url) {
114
+ quill.insertEmbed(range.index, 'image', url);
115
+ quill.setSelection(range.index + 1, 0, 'silent');
116
+ }
117
+ }
118
+ catch (_b) {
119
+ // Error handled by the consumer's upload implementation
120
+ }
121
+ finally {
122
+ setIsImageUploading(false);
123
+ }
124
+ });
125
+ editorEl.addEventListener('paste', handlePaste);
126
+ return () => {
127
+ editorEl.removeEventListener('paste', handlePaste);
128
+ };
129
+ // eslint-disable-next-line react-hooks/exhaustive-deps
130
+ }, [editorRef, quillRef]);
131
+ const formatToggle = useCallback((format, value) => {
132
+ const quill = quillRef.current;
133
+ if (!quill)
134
+ return;
135
+ const selection = quill.getSelection();
136
+ if (!selection)
137
+ return;
138
+ const newValue = value !== null && value !== void 0 ? value : !activeFormat[format];
139
+ quill.format(format, newValue);
140
+ setActiveFormat((prev) => {
141
+ const next = Object.assign({}, prev);
142
+ if (newValue) {
143
+ next[format] = newValue;
144
+ }
145
+ else {
146
+ delete next[format];
147
+ }
148
+ return next;
149
+ });
150
+ }, [activeFormat, quillRef, setActiveFormat]);
151
+ const onLinkClick = useCallback(() => {
152
+ const quill = quillRef.current;
153
+ if (!quill)
154
+ return;
155
+ const range = quill.getSelection();
156
+ if (!range)
157
+ return;
158
+ if (activeFormat.link) {
159
+ quill.format('link', false);
160
+ setActiveFormat((prev) => {
161
+ const next = Object.assign({}, prev);
162
+ delete next.link;
163
+ return next;
164
+ });
165
+ }
166
+ else {
167
+ savedSelectionRef.current = { index: range.index, length: range.length };
168
+ setLinkText(range.length > 0 ? quill.getText(range.index, range.length).trim() : '');
169
+ setLinkUrl('');
170
+ setIsLinkModalOpen(true);
171
+ }
172
+ }, [activeFormat, quillRef, setActiveFormat]);
173
+ const onLinkInsert = useCallback(() => {
174
+ const quill = quillRef.current;
175
+ const saved = savedSelectionRef.current;
176
+ const url = linkUrl.trim();
177
+ const text = linkText.trim() || url;
178
+ if (!quill || !saved || !url)
179
+ return;
180
+ if (saved.length > 0) {
181
+ quill.setSelection(saved.index, saved.length, 'silent');
182
+ quill.format('link', url);
183
+ }
184
+ else {
185
+ quill.insertText(saved.index, text, 'link', url, 'user');
186
+ quill.setSelection(saved.index + text.length, 0, 'silent');
187
+ }
188
+ setActiveFormat((prev) => (Object.assign(Object.assign({}, prev), { link: url })));
189
+ savedSelectionRef.current = null;
190
+ setIsLinkModalOpen(false);
191
+ setLinkUrl('');
192
+ setLinkText('');
193
+ }, [linkUrl, linkText, quillRef, setActiveFormat]);
194
+ const onLinkCancel = useCallback(() => {
195
+ savedSelectionRef.current = null;
196
+ setIsLinkModalOpen(false);
197
+ setLinkText('');
198
+ setLinkUrl('');
199
+ }, []);
200
+ const handleFileInputChange = useCallback((e) => __awaiter(this, void 0, void 0, function* () {
201
+ var _a;
202
+ const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
203
+ if (fileInputRef.current)
204
+ fileInputRef.current.value = '';
205
+ if (!file || !onImageUpload)
206
+ return;
207
+ if (!ALLOWED_IMAGE_TYPES.has(file.type)) {
208
+ ToastNotification.addDangerMessage(t('Invalid file type. Only JPEG, PNG, GIF, and SVG images are allowed.'));
209
+ return;
210
+ }
211
+ if (file.size > IMAGE_SIZE_LIMIT) {
212
+ ToastNotification.addDangerMessage(t('File is too large. Maximum size is 2 MB.'));
213
+ return;
214
+ }
215
+ const quill = quillRef.current;
216
+ const saved = savedImageSelectionRef.current;
217
+ if (!quill || !saved)
218
+ return;
219
+ setIsImageUploading(true);
220
+ try {
221
+ const url = yield onImageUpload(file);
222
+ if (url) {
223
+ quill.focus();
224
+ quill.setSelection(saved.index, saved.length, 'silent');
225
+ quill.insertEmbed(saved.index, 'image', url);
226
+ }
227
+ }
228
+ catch (_b) {
229
+ ToastNotification.addDangerMessage(t('Failed to upload image.'));
230
+ }
231
+ finally {
232
+ setIsImageUploading(false);
233
+ savedImageSelectionRef.current = null;
234
+ }
235
+ }), [onImageUpload, quillRef, t]);
236
+ const onImageInsert = useCallback(() => {
237
+ const quill = quillRef.current;
238
+ const saved = savedImageSelectionRef.current;
239
+ if (!quill || !saved || !imageUrl.trim())
240
+ return;
241
+ const url = imageUrl.trim();
242
+ try {
243
+ const parsed = new URL(url);
244
+ if (parsed.hostname !== 'redhat.com' && !parsed.hostname.endsWith('.redhat.com')) {
245
+ setImageUrlError(t('Only *.redhat.com URLs are allowed'));
246
+ return;
247
+ }
248
+ }
249
+ catch (_a) {
250
+ setImageUrlError(t('Only *.redhat.com URLs are allowed'));
251
+ return;
252
+ }
253
+ quill.setSelection(saved.index, saved.length, 'silent');
254
+ quill.insertEmbed(saved.index, 'image', url);
255
+ savedImageSelectionRef.current = null;
256
+ setIsImageModalOpen(false);
257
+ setImageUrl('');
258
+ setImageUrlError('');
259
+ }, [imageUrl, quillRef, t]);
260
+ const onImageCancel = useCallback(() => {
261
+ savedImageSelectionRef.current = null;
262
+ setIsImageModalOpen(false);
263
+ setImageUrl('');
264
+ setImageUrlError('');
265
+ }, []);
266
+ const onImageUrlChange = useCallback((_e, val) => {
267
+ setImageUrl(val);
268
+ if (val.trim()) {
269
+ try {
270
+ const parsed = new URL(val.trim());
271
+ if (parsed.hostname !== 'redhat.com' && !parsed.hostname.endsWith('.redhat.com')) {
272
+ setImageUrlError(t('Only *.redhat.com URLs are allowed'));
273
+ }
274
+ else {
275
+ setImageUrlError('');
276
+ }
277
+ }
278
+ catch (_a) {
279
+ setImageUrlError(t('Only *.redhat.com URLs are allowed'));
280
+ }
281
+ }
282
+ else {
283
+ setImageUrlError('');
284
+ }
285
+ }, [t]);
286
+ const onImageUrlKeyDown = useCallback((e) => {
287
+ if (e.key === 'Enter') {
288
+ e.preventDefault();
289
+ onImageInsert();
290
+ }
291
+ }, [onImageInsert]);
292
+ const onLinkTextChange = useCallback((_e, val) => {
293
+ setLinkText(val);
294
+ }, []);
295
+ const onLinkUrlChange = useCallback((_e, val) => {
296
+ setLinkUrl(val);
297
+ }, []);
298
+ const onLinkUrlKeyDown = useCallback((e) => {
299
+ if (e.key === 'Enter') {
300
+ e.preventDefault();
301
+ onLinkInsert();
302
+ }
303
+ }, [onLinkInsert]);
304
+ const onToolbarMouseDown = useCallback((e) => {
305
+ e.preventDefault();
306
+ }, []);
307
+ const onBoldClick = useCallback(() => formatToggle('bold'), [formatToggle]);
308
+ const onItalicClick = useCallback(() => formatToggle('italic'), [formatToggle]);
309
+ const onUnderlineClick = useCallback(() => formatToggle('underline'), [formatToggle]);
310
+ const onStrikethroughClick = useCallback(() => formatToggle('strike'), [formatToggle]);
311
+ const onIndentClick = useCallback(() => {
312
+ const quill = quillRef.current;
313
+ if (!quill)
314
+ return;
315
+ const range = quill.getSelection();
316
+ if (!range)
317
+ return;
318
+ const current = quill.getFormat(range).indent || 0;
319
+ quill.format('indent', current + 1);
320
+ setActiveFormat((prev) => (Object.assign(Object.assign({}, prev), { indent: current + 1 })));
321
+ }, [quillRef, setActiveFormat]);
322
+ const onOutdentClick = useCallback(() => {
323
+ const quill = quillRef.current;
324
+ if (!quill)
325
+ return;
326
+ const range = quill.getSelection();
327
+ if (!range)
328
+ return;
329
+ const current = quill.getFormat(range).indent || 0;
330
+ if (current > 0) {
331
+ quill.format('indent', current - 1 || false);
332
+ setActiveFormat((prev) => (Object.assign(Object.assign({}, prev), { indent: current - 1 || false })));
333
+ }
334
+ }, [quillRef, setActiveFormat]);
335
+ const onAlignClick = useCallback((alignment) => {
336
+ const quill = quillRef.current;
337
+ if (!quill)
338
+ return;
339
+ const range = quill.getSelection();
340
+ if (!range)
341
+ return;
342
+ const currentAlign = activeFormat.align || false;
343
+ const newValue = currentAlign === alignment ? false : alignment;
344
+ quill.format('align', newValue);
345
+ setActiveFormat((prev) => {
346
+ const next = Object.assign({}, prev);
347
+ if (newValue) {
348
+ next.align = newValue;
349
+ }
350
+ else {
351
+ delete next.align;
352
+ }
353
+ return next;
354
+ });
355
+ }, [activeFormat.align, quillRef, setActiveFormat]);
356
+ const onAlignLeftClick = useCallback(() => onAlignClick(false), [onAlignClick]);
357
+ const onAlignCenterClick = useCallback(() => onAlignClick('center'), [onAlignClick]);
358
+ const onAlignRightClick = useCallback(() => onAlignClick('right'), [onAlignClick]);
359
+ const onAlignJustifyClick = useCallback(() => onAlignClick('justify'), [onAlignClick]);
360
+ const onRemoveFormatClick = useCallback(() => {
361
+ const quill = quillRef.current;
362
+ if (!quill)
363
+ return;
364
+ const range = quill.getSelection();
365
+ if (!range)
366
+ return;
367
+ if (range.length === 0) {
368
+ const [line, offset] = quill.getLine(range.index);
369
+ if (line) {
370
+ quill.removeFormat(range.index - offset, line.length());
371
+ }
372
+ }
373
+ else {
374
+ quill.removeFormat(range.index, range.length);
375
+ }
376
+ setActiveFormat(quill.getFormat(range));
377
+ }, [quillRef, setActiveFormat]);
378
+ const onHeadingSelect = useCallback(() => setIsHeadingDropdownOpen(false), []);
379
+ const onHeadingToggleClick = useCallback(() => setIsHeadingDropdownOpen((prev) => !prev), []);
380
+ const onHeading1Click = useCallback(() => formatToggle('header', 1), [formatToggle]);
381
+ const onHeading2Click = useCallback(() => formatToggle('header', 2), [formatToggle]);
382
+ const onHeading3Click = useCallback(() => formatToggle('header', 3), [formatToggle]);
383
+ const onHeadingNormalClick = useCallback(() => formatToggle('header', false), [formatToggle]);
384
+ const onBulletListClick = useCallback(() => formatToggle('list', activeFormat.list === 'bullet' ? false : 'bullet'), [formatToggle, activeFormat.list]);
385
+ const onOrderedListClick = useCallback(() => formatToggle('list', activeFormat.list === 'ordered' ? false : 'ordered'), [formatToggle, activeFormat.list]);
386
+ const headingToggleRenderer = useCallback((toggleRef) => {
387
+ const headingLevel = activeFormat.header;
388
+ const label = headingLevel ? `Heading ${headingLevel}` : 'Normal';
389
+ return (React.createElement(MenuToggle, { ref: toggleRef, onClick: onHeadingToggleClick, isDisabled: disabled, variant: "plain", "aria-label": `Text style: ${label}`, className: `richtext-toolbar-btn ${headingLevel ? 'pf-m-active' : ''}` },
390
+ React.createElement("span", { className: "toggle-content" },
391
+ label,
392
+ " ",
393
+ React.createElement(CaretDownIcon, { className: "dropdown-caret" }))));
394
+ }, [activeFormat.header, disabled, onHeadingToggleClick]);
395
+ const editorStyle = {};
396
+ if (height) {
397
+ editorStyle.height = typeof height === 'number' ? `${height}px` : height;
398
+ }
399
+ const showCounter = charCount >= minCharsForCounter;
400
+ const isOverLimit = charCount > charLimit;
401
+ return (React.createElement("div", { id: id, className: `richtext-editor-container ${disabled ? 'richtext-editor-container--disabled' : ''} ${className}` },
402
+ !hideToolbar && (React.createElement(Toolbar, { className: "richtext-toolbar", role: "toolbar", "aria-label": "Formatting options", onMouseDown: onToolbarMouseDown },
403
+ React.createElement(ToolbarGroup, { className: "richtext-toolbar-left-section" },
404
+ React.createElement(Button, { variant: ButtonVariant.plain, isClicked: !!activeFormat.bold, isDisabled: disabled, onClick: onBoldClick, "aria-label": "Bold", "aria-pressed": !!activeFormat.bold, className: "richtext-toolbar-btn" },
405
+ React.createElement("strong", null, "B")),
406
+ React.createElement(Button, { variant: ButtonVariant.plain, isClicked: !!activeFormat.italic, isDisabled: disabled, onClick: onItalicClick, "aria-label": "Italic", "aria-pressed": !!activeFormat.italic, className: "richtext-toolbar-btn" },
407
+ React.createElement("em", null, "I")),
408
+ React.createElement(Button, { variant: ButtonVariant.plain, isClicked: !!activeFormat.underline, isDisabled: disabled, onClick: onUnderlineClick, "aria-label": "Underline", "aria-pressed": !!activeFormat.underline, className: "richtext-toolbar-btn" },
409
+ React.createElement("u", null, "U")),
410
+ React.createElement(Button, { icon: React.createElement(StrikethroughIcon, null), variant: ButtonVariant.plain, isClicked: !!activeFormat.strike, isDisabled: disabled, onClick: onStrikethroughClick, "aria-label": "Strikethrough", "aria-pressed": !!activeFormat.strike, className: "richtext-toolbar-btn" }),
411
+ React.createElement("div", { className: "btn-group-separator" }),
412
+ React.createElement(Dropdown, { isOpen: isHeadingDropdownOpen, onSelect: onHeadingSelect, onOpenChange: setIsHeadingDropdownOpen, toggle: headingToggleRenderer },
413
+ React.createElement(DropdownList, null,
414
+ React.createElement(DropdownItem, { key: "h1", onClick: onHeading1Click }, "Heading 1"),
415
+ React.createElement(DropdownItem, { key: "h2", onClick: onHeading2Click }, "Heading 2"),
416
+ React.createElement(DropdownItem, { key: "h3", onClick: onHeading3Click }, "Heading 3"),
417
+ React.createElement(DropdownItem, { key: "normal", onClick: onHeadingNormalClick }, "Normal"))),
418
+ React.createElement("div", { className: "btn-group-separator" }),
419
+ React.createElement(Button, { icon: React.createElement(ListUlIcon, null), variant: ButtonVariant.plain, isClicked: activeFormat.list === 'bullet', isDisabled: disabled, onClick: onBulletListClick, "aria-label": "Bullet List", "aria-pressed": activeFormat.list === 'bullet', className: "richtext-toolbar-btn" }),
420
+ React.createElement(Button, { icon: React.createElement(ListOlIcon, null), variant: ButtonVariant.plain, isClicked: activeFormat.list === 'ordered', isDisabled: disabled, onClick: onOrderedListClick, "aria-label": "Ordered List", "aria-pressed": activeFormat.list === 'ordered', className: "richtext-toolbar-btn" }),
421
+ React.createElement(Button, { icon: React.createElement(IndentIcon, null), variant: ButtonVariant.plain, isDisabled: disabled, onClick: onIndentClick, "aria-label": "Indent", className: "richtext-toolbar-btn" }),
422
+ React.createElement(Button, { icon: React.createElement(OutdentIcon, null), variant: ButtonVariant.plain, isDisabled: disabled, onClick: onOutdentClick, "aria-label": "Outdent", className: "richtext-toolbar-btn" }),
423
+ React.createElement("div", { className: "btn-group-separator" }),
424
+ React.createElement(Button, { icon: React.createElement(AlignLeftIcon, null), variant: ButtonVariant.plain, isClicked: !activeFormat.align, isDisabled: disabled, onClick: onAlignLeftClick, "aria-label": "Align Left", "aria-pressed": !activeFormat.align, className: "richtext-toolbar-btn" }),
425
+ React.createElement(Button, { icon: React.createElement(AlignCenterIcon, null), variant: ButtonVariant.plain, isClicked: activeFormat.align === 'center', isDisabled: disabled, onClick: onAlignCenterClick, "aria-label": "Align Center", "aria-pressed": activeFormat.align === 'center', className: "richtext-toolbar-btn" }),
426
+ React.createElement(Button, { icon: React.createElement(AlignRightIcon, null), variant: ButtonVariant.plain, isClicked: activeFormat.align === 'right', isDisabled: disabled, onClick: onAlignRightClick, "aria-label": "Align Right", "aria-pressed": activeFormat.align === 'right', className: "richtext-toolbar-btn" }),
427
+ React.createElement(Button, { icon: React.createElement(AlignJustifyIcon, null), variant: ButtonVariant.plain, isClicked: activeFormat.align === 'justify', isDisabled: disabled, onClick: onAlignJustifyClick, "aria-label": "Justify", "aria-pressed": activeFormat.align === 'justify', className: "richtext-toolbar-btn" }),
428
+ React.createElement("div", { className: "btn-group-separator" }),
429
+ React.createElement(Button, { icon: React.createElement(LinkIcon, null), variant: ButtonVariant.plain, isClicked: !!activeFormat.link, isDisabled: disabled, onClick: onLinkClick, "aria-label": "Link", "aria-pressed": !!activeFormat.link, className: "richtext-toolbar-btn" }),
430
+ React.createElement(Button, { icon: React.createElement(RemoveFormatIcon, null), variant: ButtonVariant.plain, isDisabled: disabled, onClick: onRemoveFormatClick, "aria-label": "Remove Format", className: "richtext-toolbar-btn" })))),
431
+ React.createElement("div", { ref: editorRef, role: "textbox", className: "richtext-editor-content", style: editorStyle, "aria-label": ariaLabel, tabIndex: -1, onClick: () => { var _a, _b; return (_b = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[contenteditable="true"]')) === null || _b === void 0 ? void 0 : _b.focus(); }, onKeyDown: (e) => {
432
+ var _a, _b;
433
+ if (e.key === 'Enter' || e.key === ' ') {
434
+ (_b = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[contenteditable="true"]')) === null || _b === void 0 ? void 0 : _b.focus();
435
+ }
436
+ } }),
437
+ showCounter && (React.createElement("div", { className: `richtext-char-counter ${isOverLimit ? 'richtext-char-counter--exceeded' : ''}` },
438
+ charCount,
439
+ "/",
440
+ charLimit)),
441
+ React.createElement(Modal, { variant: ModalVariant.small, isOpen: isLinkModalOpen, onClose: onLinkCancel, "aria-labelledby": "insert-link-title" },
442
+ React.createElement(ModalHeader, { title: "Insert link", labelId: "insert-link-title" }),
443
+ React.createElement(ModalBody, null,
444
+ React.createElement(FormGroup, { label: "Display text", fieldId: "link-text-input" },
445
+ React.createElement(TextInput, { id: "link-text-input", value: linkText, onChange: onLinkTextChange, placeholder: "Link text (defaults to URL)" })),
446
+ React.createElement(FormGroup, { label: "URL", isRequired: true, fieldId: "link-url-input" },
447
+ React.createElement(TextInput, { id: "link-url-input", type: "url", value: linkUrl, onChange: onLinkUrlChange, placeholder: "https://", onKeyDown: onLinkUrlKeyDown }))),
448
+ React.createElement(ModalFooter, null,
449
+ React.createElement(Button, { variant: ButtonVariant.primary, onClick: onLinkInsert, isDisabled: !linkUrl.trim() }, "Insert"),
450
+ React.createElement(Button, { variant: ButtonVariant.link, onClick: onLinkCancel }, "Cancel"))),
451
+ React.createElement("input", { ref: fileInputRef, type: "file", accept: "image/jpeg,image/png,image/gif,image/svg+xml", style: { display: 'none' }, onChange: handleFileInputChange, tabIndex: -1 }),
452
+ !onImageUpload && (React.createElement(Modal, { variant: ModalVariant.small, isOpen: isImageModalOpen, onClose: onImageCancel, "aria-labelledby": "insert-image-title" },
453
+ React.createElement(ModalHeader, { title: "Insert image", labelId: "insert-image-title" }),
454
+ React.createElement(ModalBody, null,
455
+ React.createElement(FormGroup, { label: "Image URL", isRequired: true, fieldId: "image-url-input" },
456
+ React.createElement(TextInput, { id: "image-url-input", type: "url", value: imageUrl, validated: imageUrlError ? 'error' : 'default', onChange: onImageUrlChange, placeholder: "https://example.redhat.com/image.png", onKeyDown: onImageUrlKeyDown }),
457
+ imageUrlError && (React.createElement(FormGroup, null,
458
+ React.createElement("span", { className: "pf-v6-u-danger-color-100 pf-v6-u-font-size-sm" }, imageUrlError))))),
459
+ React.createElement(ModalFooter, null,
460
+ React.createElement(Button, { variant: ButtonVariant.primary, onClick: onImageInsert, isDisabled: !imageUrl.trim() || !!imageUrlError }, "Insert"),
461
+ React.createElement(Button, { variant: ButtonVariant.link, onClick: onImageCancel }, "Cancel"))))));
462
+ }
463
+ export default RichTextEditor;
@@ -0,0 +1,4 @@
1
+ export * from './RichTextEditor';
2
+ export { useQuillEditor } from './useQuillEditor';
3
+ export type { UseQuillEditorOptions } from './useQuillEditor';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/RichTextEditor/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './RichTextEditor';
2
+ export { useQuillEditor } from './useQuillEditor';