@rh-support/components 2.5.230 → 2.5.231
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/lib/esm/ConfirmationModal/ConfirmationDialog.js +1 -1
- package/lib/esm/Functional/CaseContactsSelectorExternal.d.ts +0 -2
- package/lib/esm/Functional/CaseContactsSelectorExternal.d.ts.map +1 -1
- package/lib/esm/Functional/CaseContactsSelectorExternal.js +82 -176
- package/lib/esm/InlineEdit/NewInlineEdit.d.ts +0 -2
- package/lib/esm/InlineEdit/NewInlineEdit.d.ts.map +1 -1
- package/lib/esm/InlineEdit/NewInlineEdit.js +2 -7
- package/lib/esm/MarkdownEditor/MarkdownEditor.d.ts +49 -0
- package/lib/esm/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/lib/esm/MarkdownEditor/MarkdownEditor.js +601 -0
- package/lib/esm/MarkdownEditor/index.d.ts +2 -0
- package/lib/esm/MarkdownEditor/index.d.ts.map +1 -0
- package/lib/esm/MarkdownEditor/index.js +1 -0
- package/lib/esm/MarkdownEditor/markdownEditor.css +223 -0
- package/lib/esm/OwnerTypeaheadDropdown/OwnerTypeaheadDropdown.d.ts +0 -2
- package/lib/esm/OwnerTypeaheadDropdown/OwnerTypeaheadDropdown.d.ts.map +1 -1
- package/lib/esm/OwnerTypeaheadDropdown/OwnerTypeaheadDropdown.js +3 -8
- package/lib/esm/PhoneInput/PhoneInput.d.ts.map +1 -1
- package/lib/esm/PhoneInput/PhoneInput.js +1 -38
- package/lib/esm/SingleSelectDropdown/SingleSelectDropdown.d.ts +0 -2
- package/lib/esm/SingleSelectDropdown/SingleSelectDropdown.d.ts.map +1 -1
- package/lib/esm/SingleSelectDropdown/SingleSelectDropdown.js +6 -7
- package/lib/esm/SupportFeedbackForm/SupportFeedbackModal.d.ts.map +1 -1
- package/lib/esm/SupportFeedbackForm/SupportFeedbackModal.js +80 -62
- package/lib/esm/Table/TablePagination.d.ts +0 -1
- package/lib/esm/Table/TablePagination.d.ts.map +1 -1
- package/lib/esm/Table/TablePagination.js +1 -1
- package/lib/esm/ToastNotification/ToastNotification.d.ts +1 -5
- package/lib/esm/ToastNotification/ToastNotification.d.ts.map +1 -1
- package/lib/esm/ToastNotification/ToastNotification.js +3 -16
- package/lib/esm/ToastNotification/toastNotification.css +0 -7
- package/lib/esm/helperFunctions/errorMessage.d.ts +1 -2
- package/lib/esm/helperFunctions/errorMessage.d.ts.map +1 -1
- package/lib/esm/helperFunctions/errorMessage.js +5 -19
- package/lib/esm/hooks/useCopyToClipboard.d.ts +1 -1
- package/lib/esm/hooks/useCopyToClipboard.d.ts.map +1 -1
- package/lib/esm/hooks/useCopyToClipboard.js +1 -3
- package/lib/esm/index.d.ts +1 -3
- package/lib/esm/index.d.ts.map +1 -1
- package/lib/esm/index.js +1 -4
- package/package.json +6 -9
- package/lib/esm/CaseLabelLoadErrorModal/CaseLabelLoadErrorModal.d.ts +0 -10
- package/lib/esm/CaseLabelLoadErrorModal/CaseLabelLoadErrorModal.d.ts.map +0 -1
- package/lib/esm/CaseLabelLoadErrorModal/CaseLabelLoadErrorModal.js +0 -17
- package/lib/esm/CaseLabelLoadErrorModal/CaseLabelLoadErrorModal.scss +0 -13
- package/lib/esm/CaseLabelLoadErrorModal/index.d.ts +0 -2
- package/lib/esm/CaseLabelLoadErrorModal/index.d.ts.map +0 -1
- package/lib/esm/CaseLabelLoadErrorModal/index.js +0 -1
- package/lib/esm/CustomDatePicker/CustomDatePicker.d.ts +0 -18
- package/lib/esm/CustomDatePicker/CustomDatePicker.d.ts.map +0 -1
- package/lib/esm/CustomDatePicker/CustomDatePicker.js +0 -27
- package/lib/esm/CustomDatePicker/customDatePicker.css +0 -52
- package/lib/esm/CustomDatePicker/index.d.ts +0 -2
- package/lib/esm/CustomDatePicker/index.d.ts.map +0 -1
- package/lib/esm/CustomDatePicker/index.js +0 -1
- package/lib/esm/RichTextEditor/RichTextEditor.d.ts +0 -45
- package/lib/esm/RichTextEditor/RichTextEditor.d.ts.map +0 -1
- package/lib/esm/RichTextEditor/RichTextEditor.js +0 -464
- package/lib/esm/RichTextEditor/autoLinkModule.d.ts +0 -18
- package/lib/esm/RichTextEditor/autoLinkModule.d.ts.map +0 -1
- package/lib/esm/RichTextEditor/autoLinkModule.js +0 -156
- package/lib/esm/RichTextEditor/index.d.ts +0 -4
- package/lib/esm/RichTextEditor/index.d.ts.map +0 -1
- package/lib/esm/RichTextEditor/index.js +0 -2
- package/lib/esm/RichTextEditor/linkUrl.d.ts +0 -7
- package/lib/esm/RichTextEditor/linkUrl.d.ts.map +0 -1
- package/lib/esm/RichTextEditor/linkUrl.js +0 -26
- package/lib/esm/RichTextEditor/richTextEditor.css +0 -182
- package/lib/esm/RichTextEditor/useQuillEditor.d.ts +0 -20
- package/lib/esm/RichTextEditor/useQuillEditor.d.ts.map +0 -1
- package/lib/esm/RichTextEditor/useQuillEditor.js +0 -180
|
@@ -1,464 +0,0 @@
|
|
|
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 { isValidUrl, normalizeLinkUrl } from './linkUrl';
|
|
28
|
-
import { useQuillEditor } from './useQuillEditor';
|
|
29
|
-
const defaultCharLimit = 32000;
|
|
30
|
-
const defaultMinCharsForCounter = 31800;
|
|
31
|
-
const ALLOWED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']);
|
|
32
|
-
const IMAGE_SIZE_LIMIT = 2 * 1024 * 1024; // 2 MB
|
|
33
|
-
export function RichTextEditor(props) {
|
|
34
|
-
const { t } = useTranslation();
|
|
35
|
-
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;
|
|
36
|
-
const [isHeadingDropdownOpen, setIsHeadingDropdownOpen] = useState(false);
|
|
37
|
-
const [charCount, setCharCount] = useState(0);
|
|
38
|
-
const charLimitExceededRef = useRef(false);
|
|
39
|
-
const [isLinkModalOpen, setIsLinkModalOpen] = useState(false);
|
|
40
|
-
const [linkUrl, setLinkUrl] = useState('');
|
|
41
|
-
const [linkText, setLinkText] = useState('');
|
|
42
|
-
const savedSelectionRef = useRef(null);
|
|
43
|
-
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
|
|
44
|
-
const [imageUrl, setImageUrl] = useState('');
|
|
45
|
-
const [imageUrlError, setImageUrlError] = useState('');
|
|
46
|
-
const savedImageSelectionRef = useRef(null);
|
|
47
|
-
const [, setIsImageUploading] = useState(false);
|
|
48
|
-
const fileInputRef = useRef(null);
|
|
49
|
-
const { editorRef, quillRef, activeFormat, setActiveFormat, setContents, setReadOnly } = useQuillEditor({
|
|
50
|
-
placeholder,
|
|
51
|
-
readOnly: disabled,
|
|
52
|
-
onChange: (html) => {
|
|
53
|
-
onChange(html);
|
|
54
|
-
},
|
|
55
|
-
onChangePlainText: (text) => {
|
|
56
|
-
onChangePlainText === null || onChangePlainText === void 0 ? void 0 : onChangePlainText(text);
|
|
57
|
-
setCharCount(text.length);
|
|
58
|
-
},
|
|
59
|
-
onFocus,
|
|
60
|
-
onBlur,
|
|
61
|
-
});
|
|
62
|
-
// Sync external value into Quill when it changes externally
|
|
63
|
-
useEffect(() => {
|
|
64
|
-
setContents(value);
|
|
65
|
-
}, [value, setContents]);
|
|
66
|
-
// Sync disabled state
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
setReadOnly(disabled);
|
|
69
|
-
}, [disabled, setReadOnly]);
|
|
70
|
-
// Bind editor ref externally
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
if (quillRef.current && bindEditor) {
|
|
73
|
-
bindEditor(quillRef.current);
|
|
74
|
-
}
|
|
75
|
-
}, [bindEditor, quillRef]);
|
|
76
|
-
// Character limit exceeded callback
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
const exceeded = charCount > charLimit;
|
|
79
|
-
if (exceeded !== charLimitExceededRef.current) {
|
|
80
|
-
charLimitExceededRef.current = exceeded;
|
|
81
|
-
onCharLimitExceeded === null || onCharLimitExceeded === void 0 ? void 0 : onCharLimitExceeded(exceeded);
|
|
82
|
-
}
|
|
83
|
-
}, [charCount, charLimit, onCharLimitExceeded]);
|
|
84
|
-
const onImageUploadRef = useRef(onImageUpload);
|
|
85
|
-
onImageUploadRef.current = onImageUpload;
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
const editorEl = editorRef.current;
|
|
88
|
-
if (!editorEl || !onImageUploadRef.current)
|
|
89
|
-
return;
|
|
90
|
-
const handlePaste = (e) => __awaiter(this, void 0, void 0, function* () {
|
|
91
|
-
var _a;
|
|
92
|
-
if (!((_a = e.clipboardData) === null || _a === void 0 ? void 0 : _a.items))
|
|
93
|
-
return;
|
|
94
|
-
const imageItem = Array.from(e.clipboardData.items).find((item) => item.type.startsWith('image/') && ALLOWED_IMAGE_TYPES.has(item.type));
|
|
95
|
-
if (!imageItem)
|
|
96
|
-
return;
|
|
97
|
-
const file = imageItem.getAsFile();
|
|
98
|
-
if (!file)
|
|
99
|
-
return;
|
|
100
|
-
if (file.size > IMAGE_SIZE_LIMIT) {
|
|
101
|
-
e.preventDefault();
|
|
102
|
-
e.stopPropagation();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
e.preventDefault();
|
|
106
|
-
e.stopPropagation();
|
|
107
|
-
const quill = quillRef.current;
|
|
108
|
-
if (!quill)
|
|
109
|
-
return;
|
|
110
|
-
const range = quill.getSelection() || { index: quill.getLength() - 1, length: 0 };
|
|
111
|
-
setIsImageUploading(true);
|
|
112
|
-
try {
|
|
113
|
-
const url = yield onImageUploadRef.current(file);
|
|
114
|
-
if (url) {
|
|
115
|
-
quill.insertEmbed(range.index, 'image', url);
|
|
116
|
-
quill.setSelection(range.index + 1, 0, 'silent');
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
catch (_b) {
|
|
120
|
-
// Error handled by the consumer's upload implementation
|
|
121
|
-
}
|
|
122
|
-
finally {
|
|
123
|
-
setIsImageUploading(false);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
editorEl.addEventListener('paste', handlePaste);
|
|
127
|
-
return () => {
|
|
128
|
-
editorEl.removeEventListener('paste', handlePaste);
|
|
129
|
-
};
|
|
130
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
131
|
-
}, [editorRef, quillRef]);
|
|
132
|
-
const formatToggle = useCallback((format, value) => {
|
|
133
|
-
const quill = quillRef.current;
|
|
134
|
-
if (!quill)
|
|
135
|
-
return;
|
|
136
|
-
const selection = quill.getSelection();
|
|
137
|
-
if (!selection)
|
|
138
|
-
return;
|
|
139
|
-
const newValue = value !== null && value !== void 0 ? value : !activeFormat[format];
|
|
140
|
-
quill.format(format, newValue);
|
|
141
|
-
setActiveFormat((prev) => {
|
|
142
|
-
const next = Object.assign({}, prev);
|
|
143
|
-
if (newValue) {
|
|
144
|
-
next[format] = newValue;
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
delete next[format];
|
|
148
|
-
}
|
|
149
|
-
return next;
|
|
150
|
-
});
|
|
151
|
-
}, [activeFormat, quillRef, setActiveFormat]);
|
|
152
|
-
const onLinkClick = useCallback(() => {
|
|
153
|
-
const quill = quillRef.current;
|
|
154
|
-
if (!quill)
|
|
155
|
-
return;
|
|
156
|
-
const range = quill.getSelection();
|
|
157
|
-
if (!range)
|
|
158
|
-
return;
|
|
159
|
-
if (activeFormat.link) {
|
|
160
|
-
quill.format('link', false);
|
|
161
|
-
setActiveFormat((prev) => {
|
|
162
|
-
const next = Object.assign({}, prev);
|
|
163
|
-
delete next.link;
|
|
164
|
-
return next;
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
savedSelectionRef.current = { index: range.index, length: range.length };
|
|
169
|
-
setLinkText(range.length > 0 ? quill.getText(range.index, range.length).trim() : '');
|
|
170
|
-
setLinkUrl('');
|
|
171
|
-
setIsLinkModalOpen(true);
|
|
172
|
-
}
|
|
173
|
-
}, [activeFormat, quillRef, setActiveFormat]);
|
|
174
|
-
const onLinkInsert = useCallback(() => {
|
|
175
|
-
const quill = quillRef.current;
|
|
176
|
-
const saved = savedSelectionRef.current;
|
|
177
|
-
const url = normalizeLinkUrl(linkUrl);
|
|
178
|
-
if (!quill || !saved || !url)
|
|
179
|
-
return;
|
|
180
|
-
const text = linkText.trim() || url;
|
|
181
|
-
if (saved.length > 0) {
|
|
182
|
-
quill.setSelection(saved.index, saved.length, 'silent');
|
|
183
|
-
quill.format('link', url);
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
quill.insertText(saved.index, text, 'link', url, 'user');
|
|
187
|
-
quill.setSelection(saved.index + text.length, 0, 'silent');
|
|
188
|
-
}
|
|
189
|
-
setActiveFormat((prev) => (Object.assign(Object.assign({}, prev), { link: url })));
|
|
190
|
-
savedSelectionRef.current = null;
|
|
191
|
-
setIsLinkModalOpen(false);
|
|
192
|
-
setLinkUrl('');
|
|
193
|
-
setLinkText('');
|
|
194
|
-
}, [linkUrl, linkText, quillRef, setActiveFormat]);
|
|
195
|
-
const onLinkCancel = useCallback(() => {
|
|
196
|
-
savedSelectionRef.current = null;
|
|
197
|
-
setIsLinkModalOpen(false);
|
|
198
|
-
setLinkText('');
|
|
199
|
-
setLinkUrl('');
|
|
200
|
-
}, []);
|
|
201
|
-
const handleFileInputChange = useCallback((e) => __awaiter(this, void 0, void 0, function* () {
|
|
202
|
-
var _a;
|
|
203
|
-
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
204
|
-
if (fileInputRef.current)
|
|
205
|
-
fileInputRef.current.value = '';
|
|
206
|
-
if (!file || !onImageUpload)
|
|
207
|
-
return;
|
|
208
|
-
if (!ALLOWED_IMAGE_TYPES.has(file.type)) {
|
|
209
|
-
ToastNotification.addDangerMessage(t('Invalid file type. Only JPEG, PNG, GIF, and SVG images are allowed.'));
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (file.size > IMAGE_SIZE_LIMIT) {
|
|
213
|
-
ToastNotification.addDangerMessage(t('File is too large. Maximum size is 2 MB.'));
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const quill = quillRef.current;
|
|
217
|
-
const saved = savedImageSelectionRef.current;
|
|
218
|
-
if (!quill || !saved)
|
|
219
|
-
return;
|
|
220
|
-
setIsImageUploading(true);
|
|
221
|
-
try {
|
|
222
|
-
const url = yield onImageUpload(file);
|
|
223
|
-
if (url) {
|
|
224
|
-
quill.focus();
|
|
225
|
-
quill.setSelection(saved.index, saved.length, 'silent');
|
|
226
|
-
quill.insertEmbed(saved.index, 'image', url);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch (_b) {
|
|
230
|
-
ToastNotification.addDangerMessage(t('Failed to upload image.'));
|
|
231
|
-
}
|
|
232
|
-
finally {
|
|
233
|
-
setIsImageUploading(false);
|
|
234
|
-
savedImageSelectionRef.current = null;
|
|
235
|
-
}
|
|
236
|
-
}), [onImageUpload, quillRef, t]);
|
|
237
|
-
const onImageInsert = useCallback(() => {
|
|
238
|
-
const quill = quillRef.current;
|
|
239
|
-
const saved = savedImageSelectionRef.current;
|
|
240
|
-
if (!quill || !saved || !imageUrl.trim())
|
|
241
|
-
return;
|
|
242
|
-
const url = imageUrl.trim();
|
|
243
|
-
try {
|
|
244
|
-
const parsed = new URL(url);
|
|
245
|
-
if (parsed.hostname !== 'redhat.com' && !parsed.hostname.endsWith('.redhat.com')) {
|
|
246
|
-
setImageUrlError(t('Only *.redhat.com URLs are allowed'));
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
catch (_a) {
|
|
251
|
-
setImageUrlError(t('Only *.redhat.com URLs are allowed'));
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
quill.setSelection(saved.index, saved.length, 'silent');
|
|
255
|
-
quill.insertEmbed(saved.index, 'image', url);
|
|
256
|
-
savedImageSelectionRef.current = null;
|
|
257
|
-
setIsImageModalOpen(false);
|
|
258
|
-
setImageUrl('');
|
|
259
|
-
setImageUrlError('');
|
|
260
|
-
}, [imageUrl, quillRef, t]);
|
|
261
|
-
const onImageCancel = useCallback(() => {
|
|
262
|
-
savedImageSelectionRef.current = null;
|
|
263
|
-
setIsImageModalOpen(false);
|
|
264
|
-
setImageUrl('');
|
|
265
|
-
setImageUrlError('');
|
|
266
|
-
}, []);
|
|
267
|
-
const onImageUrlChange = useCallback((_e, val) => {
|
|
268
|
-
setImageUrl(val);
|
|
269
|
-
if (val.trim()) {
|
|
270
|
-
try {
|
|
271
|
-
const parsed = new URL(val.trim());
|
|
272
|
-
if (parsed.hostname !== 'redhat.com' && !parsed.hostname.endsWith('.redhat.com')) {
|
|
273
|
-
setImageUrlError(t('Only *.redhat.com URLs are allowed'));
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
setImageUrlError('');
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
catch (_a) {
|
|
280
|
-
setImageUrlError(t('Only *.redhat.com URLs are allowed'));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
setImageUrlError('');
|
|
285
|
-
}
|
|
286
|
-
}, [t]);
|
|
287
|
-
const onImageUrlKeyDown = useCallback((e) => {
|
|
288
|
-
if (e.key === 'Enter') {
|
|
289
|
-
e.preventDefault();
|
|
290
|
-
onImageInsert();
|
|
291
|
-
}
|
|
292
|
-
}, [onImageInsert]);
|
|
293
|
-
const onLinkTextChange = useCallback((_e, val) => {
|
|
294
|
-
setLinkText(val);
|
|
295
|
-
}, []);
|
|
296
|
-
const onLinkUrlChange = useCallback((_e, val) => {
|
|
297
|
-
setLinkUrl(val);
|
|
298
|
-
}, []);
|
|
299
|
-
const onLinkUrlKeyDown = useCallback((e) => {
|
|
300
|
-
if (e.key === 'Enter') {
|
|
301
|
-
e.preventDefault();
|
|
302
|
-
onLinkInsert();
|
|
303
|
-
}
|
|
304
|
-
}, [onLinkInsert]);
|
|
305
|
-
const onToolbarMouseDown = useCallback((e) => {
|
|
306
|
-
e.preventDefault();
|
|
307
|
-
}, []);
|
|
308
|
-
const onBoldClick = useCallback(() => formatToggle('bold'), [formatToggle]);
|
|
309
|
-
const onItalicClick = useCallback(() => formatToggle('italic'), [formatToggle]);
|
|
310
|
-
const onUnderlineClick = useCallback(() => formatToggle('underline'), [formatToggle]);
|
|
311
|
-
const onStrikethroughClick = useCallback(() => formatToggle('strike'), [formatToggle]);
|
|
312
|
-
const onIndentClick = useCallback(() => {
|
|
313
|
-
const quill = quillRef.current;
|
|
314
|
-
if (!quill)
|
|
315
|
-
return;
|
|
316
|
-
const range = quill.getSelection();
|
|
317
|
-
if (!range)
|
|
318
|
-
return;
|
|
319
|
-
const current = quill.getFormat(range).indent || 0;
|
|
320
|
-
quill.format('indent', current + 1);
|
|
321
|
-
setActiveFormat((prev) => (Object.assign(Object.assign({}, prev), { indent: current + 1 })));
|
|
322
|
-
}, [quillRef, setActiveFormat]);
|
|
323
|
-
const onOutdentClick = useCallback(() => {
|
|
324
|
-
const quill = quillRef.current;
|
|
325
|
-
if (!quill)
|
|
326
|
-
return;
|
|
327
|
-
const range = quill.getSelection();
|
|
328
|
-
if (!range)
|
|
329
|
-
return;
|
|
330
|
-
const current = quill.getFormat(range).indent || 0;
|
|
331
|
-
if (current > 0) {
|
|
332
|
-
quill.format('indent', current - 1 || false);
|
|
333
|
-
setActiveFormat((prev) => (Object.assign(Object.assign({}, prev), { indent: current - 1 || false })));
|
|
334
|
-
}
|
|
335
|
-
}, [quillRef, setActiveFormat]);
|
|
336
|
-
const onAlignClick = useCallback((alignment) => {
|
|
337
|
-
const quill = quillRef.current;
|
|
338
|
-
if (!quill)
|
|
339
|
-
return;
|
|
340
|
-
const range = quill.getSelection();
|
|
341
|
-
if (!range)
|
|
342
|
-
return;
|
|
343
|
-
const currentAlign = activeFormat.align || false;
|
|
344
|
-
const newValue = currentAlign === alignment ? false : alignment;
|
|
345
|
-
quill.format('align', newValue);
|
|
346
|
-
setActiveFormat((prev) => {
|
|
347
|
-
const next = Object.assign({}, prev);
|
|
348
|
-
if (newValue) {
|
|
349
|
-
next.align = newValue;
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
delete next.align;
|
|
353
|
-
}
|
|
354
|
-
return next;
|
|
355
|
-
});
|
|
356
|
-
}, [activeFormat.align, quillRef, setActiveFormat]);
|
|
357
|
-
const onAlignLeftClick = useCallback(() => onAlignClick(false), [onAlignClick]);
|
|
358
|
-
const onAlignCenterClick = useCallback(() => onAlignClick('center'), [onAlignClick]);
|
|
359
|
-
const onAlignRightClick = useCallback(() => onAlignClick('right'), [onAlignClick]);
|
|
360
|
-
const onAlignJustifyClick = useCallback(() => onAlignClick('justify'), [onAlignClick]);
|
|
361
|
-
const onRemoveFormatClick = useCallback(() => {
|
|
362
|
-
const quill = quillRef.current;
|
|
363
|
-
if (!quill)
|
|
364
|
-
return;
|
|
365
|
-
const range = quill.getSelection();
|
|
366
|
-
if (!range)
|
|
367
|
-
return;
|
|
368
|
-
if (range.length === 0) {
|
|
369
|
-
const [line, offset] = quill.getLine(range.index);
|
|
370
|
-
if (line) {
|
|
371
|
-
quill.removeFormat(range.index - offset, line.length());
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
else {
|
|
375
|
-
quill.removeFormat(range.index, range.length);
|
|
376
|
-
}
|
|
377
|
-
setActiveFormat(quill.getFormat(range));
|
|
378
|
-
}, [quillRef, setActiveFormat]);
|
|
379
|
-
const onHeadingSelect = useCallback(() => setIsHeadingDropdownOpen(false), []);
|
|
380
|
-
const onHeadingToggleClick = useCallback(() => setIsHeadingDropdownOpen((prev) => !prev), []);
|
|
381
|
-
const onHeading1Click = useCallback(() => formatToggle('header', 1), [formatToggle]);
|
|
382
|
-
const onHeading2Click = useCallback(() => formatToggle('header', 2), [formatToggle]);
|
|
383
|
-
const onHeading3Click = useCallback(() => formatToggle('header', 3), [formatToggle]);
|
|
384
|
-
const onHeadingNormalClick = useCallback(() => formatToggle('header', false), [formatToggle]);
|
|
385
|
-
const onBulletListClick = useCallback(() => formatToggle('list', activeFormat.list === 'bullet' ? false : 'bullet'), [formatToggle, activeFormat.list]);
|
|
386
|
-
const onOrderedListClick = useCallback(() => formatToggle('list', activeFormat.list === 'ordered' ? false : 'ordered'), [formatToggle, activeFormat.list]);
|
|
387
|
-
const headingToggleRenderer = useCallback((toggleRef) => {
|
|
388
|
-
const headingLevel = activeFormat.header;
|
|
389
|
-
const label = headingLevel ? `Heading ${headingLevel}` : 'Normal';
|
|
390
|
-
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' : ''}` },
|
|
391
|
-
React.createElement("span", { className: "toggle-content" },
|
|
392
|
-
label,
|
|
393
|
-
" ",
|
|
394
|
-
React.createElement(CaretDownIcon, { className: "dropdown-caret" }))));
|
|
395
|
-
}, [activeFormat.header, disabled, onHeadingToggleClick]);
|
|
396
|
-
const editorStyle = {};
|
|
397
|
-
if (height) {
|
|
398
|
-
editorStyle.height = typeof height === 'number' ? `${height}px` : height;
|
|
399
|
-
}
|
|
400
|
-
const showCounter = charCount >= minCharsForCounter;
|
|
401
|
-
const isOverLimit = charCount > charLimit;
|
|
402
|
-
return (React.createElement("div", { id: id, className: `richtext-editor-container ${disabled ? 'richtext-editor-container--disabled' : ''} ${className}` },
|
|
403
|
-
!hideToolbar && (React.createElement(Toolbar, { className: "richtext-toolbar", role: "toolbar", "aria-label": "Formatting options", onMouseDown: onToolbarMouseDown },
|
|
404
|
-
React.createElement(ToolbarGroup, { className: "richtext-toolbar-left-section" },
|
|
405
|
-
React.createElement(Button, { variant: ButtonVariant.plain, isClicked: !!activeFormat.bold, isDisabled: disabled, onClick: onBoldClick, "aria-label": "Bold", "aria-pressed": !!activeFormat.bold, className: "richtext-toolbar-btn" },
|
|
406
|
-
React.createElement("strong", null, "B")),
|
|
407
|
-
React.createElement(Button, { variant: ButtonVariant.plain, isClicked: !!activeFormat.italic, isDisabled: disabled, onClick: onItalicClick, "aria-label": "Italic", "aria-pressed": !!activeFormat.italic, className: "richtext-toolbar-btn" },
|
|
408
|
-
React.createElement("em", null, "I")),
|
|
409
|
-
React.createElement(Button, { variant: ButtonVariant.plain, isClicked: !!activeFormat.underline, isDisabled: disabled, onClick: onUnderlineClick, "aria-label": "Underline", "aria-pressed": !!activeFormat.underline, className: "richtext-toolbar-btn" },
|
|
410
|
-
React.createElement("u", null, "U")),
|
|
411
|
-
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" }),
|
|
412
|
-
React.createElement("div", { className: "btn-group-separator" }),
|
|
413
|
-
React.createElement(Dropdown, { isOpen: isHeadingDropdownOpen, onSelect: onHeadingSelect, onOpenChange: setIsHeadingDropdownOpen, toggle: headingToggleRenderer },
|
|
414
|
-
React.createElement(DropdownList, null,
|
|
415
|
-
React.createElement(DropdownItem, { key: "h1", onClick: onHeading1Click }, "Heading 1"),
|
|
416
|
-
React.createElement(DropdownItem, { key: "h2", onClick: onHeading2Click }, "Heading 2"),
|
|
417
|
-
React.createElement(DropdownItem, { key: "h3", onClick: onHeading3Click }, "Heading 3"),
|
|
418
|
-
React.createElement(DropdownItem, { key: "normal", onClick: onHeadingNormalClick }, "Normal"))),
|
|
419
|
-
React.createElement("div", { className: "btn-group-separator" }),
|
|
420
|
-
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" }),
|
|
421
|
-
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" }),
|
|
422
|
-
React.createElement(Button, { icon: React.createElement(IndentIcon, null), variant: ButtonVariant.plain, isDisabled: disabled, onClick: onIndentClick, "aria-label": "Indent", className: "richtext-toolbar-btn" }),
|
|
423
|
-
React.createElement(Button, { icon: React.createElement(OutdentIcon, null), variant: ButtonVariant.plain, isDisabled: disabled, onClick: onOutdentClick, "aria-label": "Outdent", className: "richtext-toolbar-btn" }),
|
|
424
|
-
React.createElement("div", { className: "btn-group-separator" }),
|
|
425
|
-
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" }),
|
|
426
|
-
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" }),
|
|
427
|
-
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" }),
|
|
428
|
-
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" }),
|
|
429
|
-
React.createElement("div", { className: "btn-group-separator" }),
|
|
430
|
-
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" }),
|
|
431
|
-
React.createElement(Button, { icon: React.createElement(RemoveFormatIcon, null), variant: ButtonVariant.plain, isDisabled: disabled, onClick: onRemoveFormatClick, "aria-label": "Remove Format", className: "richtext-toolbar-btn" })))),
|
|
432
|
-
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) => {
|
|
433
|
-
var _a, _b;
|
|
434
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
435
|
-
(_b = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('[contenteditable="true"]')) === null || _b === void 0 ? void 0 : _b.focus();
|
|
436
|
-
}
|
|
437
|
-
} }),
|
|
438
|
-
showCounter && (React.createElement("div", { className: `richtext-char-counter ${isOverLimit ? 'richtext-char-counter--exceeded' : ''}` },
|
|
439
|
-
charCount,
|
|
440
|
-
"/",
|
|
441
|
-
charLimit)),
|
|
442
|
-
React.createElement(Modal, { variant: ModalVariant.small, isOpen: isLinkModalOpen, onClose: onLinkCancel, "aria-labelledby": "insert-link-title" },
|
|
443
|
-
React.createElement(ModalHeader, { title: "Insert link", labelId: "insert-link-title" }),
|
|
444
|
-
React.createElement(ModalBody, null,
|
|
445
|
-
React.createElement(FormGroup, { label: "Display text", fieldId: "link-text-input" },
|
|
446
|
-
React.createElement(TextInput, { id: "link-text-input", value: linkText, onChange: onLinkTextChange, placeholder: "Link text (defaults to URL)" })),
|
|
447
|
-
React.createElement(FormGroup, { label: "URL", isRequired: true, fieldId: "link-url-input" },
|
|
448
|
-
React.createElement(TextInput, { id: "link-url-input", type: "url", value: linkUrl, onChange: onLinkUrlChange, placeholder: "https://", onKeyDown: onLinkUrlKeyDown }))),
|
|
449
|
-
React.createElement(ModalFooter, null,
|
|
450
|
-
React.createElement(Button, { variant: ButtonVariant.primary, onClick: onLinkInsert, isDisabled: !linkUrl.trim() || !isValidUrl(linkUrl.trim()) }, "Insert"),
|
|
451
|
-
React.createElement(Button, { variant: ButtonVariant.link, onClick: onLinkCancel }, "Cancel"))),
|
|
452
|
-
React.createElement("input", { ref: fileInputRef, type: "file", accept: "image/jpeg,image/png,image/gif,image/svg+xml", style: { display: 'none' }, onChange: handleFileInputChange, tabIndex: -1 }),
|
|
453
|
-
!onImageUpload && (React.createElement(Modal, { variant: ModalVariant.small, isOpen: isImageModalOpen, onClose: onImageCancel, "aria-labelledby": "insert-image-title" },
|
|
454
|
-
React.createElement(ModalHeader, { title: "Insert image", labelId: "insert-image-title" }),
|
|
455
|
-
React.createElement(ModalBody, null,
|
|
456
|
-
React.createElement(FormGroup, { label: "Image URL", isRequired: true, fieldId: "image-url-input" },
|
|
457
|
-
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 }),
|
|
458
|
-
imageUrlError && (React.createElement(FormGroup, null,
|
|
459
|
-
React.createElement("span", { className: "pf-v6-u-danger-color-100 pf-v6-u-font-size-sm" }, imageUrlError))))),
|
|
460
|
-
React.createElement(ModalFooter, null,
|
|
461
|
-
React.createElement(Button, { variant: ButtonVariant.primary, onClick: onImageInsert, isDisabled: !imageUrl.trim() || !!imageUrlError }, "Insert"),
|
|
462
|
-
React.createElement(Button, { variant: ButtonVariant.link, onClick: onImageCancel }, "Cancel"))))));
|
|
463
|
-
}
|
|
464
|
-
export default RichTextEditor;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { default as Quill } from 'quill';
|
|
2
|
-
/**
|
|
3
|
-
* Registers auto-link behaviour on a Quill instance:
|
|
4
|
-
*
|
|
5
|
-
* 1. **Type** — after a delimiter character, checks the preceding token
|
|
6
|
-
* for URL / case-ID / BZ patterns.
|
|
7
|
-
* 2. **Paste** — when the insert is longer than one character, scans the
|
|
8
|
-
* full inserted range.
|
|
9
|
-
* 3. **Blur** — a full-document scan so trailing tokens without a
|
|
10
|
-
* delimiter still get linked before submit.
|
|
11
|
-
*/
|
|
12
|
-
export declare function registerAutoLinkModule(quill: Quill): void;
|
|
13
|
-
/**
|
|
14
|
-
* Scans the entire document for unlinkified tokens.
|
|
15
|
-
* Call this on blur so trailing URLs / IDs without a delimiter are caught.
|
|
16
|
-
*/
|
|
17
|
-
export declare function linkifyFullDocument(quill: Quill): void;
|
|
18
|
-
//# sourceMappingURL=autoLinkModule.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"autoLinkModule.d.ts","sourceRoot":"","sources":["../../../src/RichTextEditor/autoLinkModule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,IAAI,KAAK,EAA8B,MAAM,OAAO,CAAC;AAyE1E;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CA0EzD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAItD"}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
function getCaseIdHref(caseId) {
|
|
2
|
-
const host = typeof window !== 'undefined' ? window.location.host : 'access.redhat.com';
|
|
3
|
-
return `https://${host}/support/cases/#/case/${caseId}`;
|
|
4
|
-
}
|
|
5
|
-
function getBugzillaHref(bugId) {
|
|
6
|
-
return `https://bugzilla.redhat.com/show_bug.cgi?id=${bugId}`;
|
|
7
|
-
}
|
|
8
|
-
const DELIMITER_RE = /[\s.,;:!?)}\]>]/;
|
|
9
|
-
const CASE_ID_RE = /(?:^|[\s(])(\d{8})$/;
|
|
10
|
-
const BZ_RE = /(?:^|[\s(])(?:BZ[\s-]*|Bug[#\s]?)(\d{7,})$/i;
|
|
11
|
-
const URL_RE = /(?:^|[\s(])(https?:\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi;
|
|
12
|
-
const URL_AT_END_RE = /(https?:\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])$/i;
|
|
13
|
-
const WORD_CASE_ID_RE = /^(\d{8})$/;
|
|
14
|
-
const WORD_BZ_RE = /^(?:BZ[\s-]*|Bug[#\s]?)(\d{7,})$/i;
|
|
15
|
-
/**
|
|
16
|
-
* Scans a text range inside the Quill document and applies link formatting
|
|
17
|
-
* to all recognised URL, case-ID, and BZ patterns that aren't already linked.
|
|
18
|
-
*
|
|
19
|
-
* Used by the blur scan and paste scan to cover text that was not preceded
|
|
20
|
-
* by a delimiter character.
|
|
21
|
-
*/
|
|
22
|
-
function linkifyRange(quill, rangeStart, rangeLength) {
|
|
23
|
-
const text = quill.getText(rangeStart, rangeLength);
|
|
24
|
-
URL_RE.lastIndex = 0;
|
|
25
|
-
let urlMatch;
|
|
26
|
-
while ((urlMatch = URL_RE.exec(text)) !== null) {
|
|
27
|
-
const url = urlMatch[1];
|
|
28
|
-
const matchOffset = urlMatch.index + urlMatch[0].length - url.length;
|
|
29
|
-
const absStart = rangeStart + matchOffset;
|
|
30
|
-
const format = quill.getFormat(absStart, url.length);
|
|
31
|
-
if (!format.link) {
|
|
32
|
-
quill.formatText(absStart, url.length, 'link', url, 'api');
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const lines = text.split('\n');
|
|
36
|
-
let offset = 0;
|
|
37
|
-
for (const line of lines) {
|
|
38
|
-
const words = line.split(/(\s+)/);
|
|
39
|
-
let wordOffset = offset;
|
|
40
|
-
for (const word of words) {
|
|
41
|
-
if (/\S/.test(word)) {
|
|
42
|
-
const caseMatch = WORD_CASE_ID_RE.exec(word);
|
|
43
|
-
if (caseMatch) {
|
|
44
|
-
const absStart = rangeStart + wordOffset;
|
|
45
|
-
const fmt = quill.getFormat(absStart, 8);
|
|
46
|
-
if (!fmt.link) {
|
|
47
|
-
quill.formatText(absStart, 8, 'link', getCaseIdHref(caseMatch[1]), 'api');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
const bzMatch = WORD_BZ_RE.exec(word);
|
|
52
|
-
if (bzMatch) {
|
|
53
|
-
const absStart = rangeStart + wordOffset;
|
|
54
|
-
const fmt = quill.getFormat(absStart, word.length);
|
|
55
|
-
if (!fmt.link) {
|
|
56
|
-
quill.formatText(absStart, word.length, 'link', getBugzillaHref(bzMatch[1]), 'api');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
wordOffset += word.length;
|
|
62
|
-
}
|
|
63
|
-
offset += line.length + 1;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Registers auto-link behaviour on a Quill instance:
|
|
68
|
-
*
|
|
69
|
-
* 1. **Type** — after a delimiter character, checks the preceding token
|
|
70
|
-
* for URL / case-ID / BZ patterns.
|
|
71
|
-
* 2. **Paste** — when the insert is longer than one character, scans the
|
|
72
|
-
* full inserted range.
|
|
73
|
-
* 3. **Blur** — a full-document scan so trailing tokens without a
|
|
74
|
-
* delimiter still get linked before submit.
|
|
75
|
-
*/
|
|
76
|
-
export function registerAutoLinkModule(quill) {
|
|
77
|
-
quill.on('text-change', (delta, _oldDelta, source) => {
|
|
78
|
-
if (source !== 'user')
|
|
79
|
-
return;
|
|
80
|
-
const ops = delta.ops;
|
|
81
|
-
if (!ops || ops.length === 0)
|
|
82
|
-
return;
|
|
83
|
-
const lastOp = ops.at(-1);
|
|
84
|
-
if (!lastOp || typeof lastOp.insert !== 'string')
|
|
85
|
-
return;
|
|
86
|
-
const insertedText = lastOp.insert;
|
|
87
|
-
// Paste: multi-char insert — scan the full inserted range
|
|
88
|
-
if (insertedText.length > 1) {
|
|
89
|
-
let insertOffset = 0;
|
|
90
|
-
for (const op of ops) {
|
|
91
|
-
if (!op.retain)
|
|
92
|
-
break;
|
|
93
|
-
insertOffset += op.retain;
|
|
94
|
-
}
|
|
95
|
-
setTimeout(() => linkifyRange(quill, insertOffset, insertedText.length), 0);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
// Type: single-char delimiter
|
|
99
|
-
if (!DELIMITER_RE.test(insertedText))
|
|
100
|
-
return;
|
|
101
|
-
const selection = quill.getSelection();
|
|
102
|
-
if (!selection)
|
|
103
|
-
return;
|
|
104
|
-
const cursorIndex = selection.index;
|
|
105
|
-
const text = quill.getText(0, cursorIndex);
|
|
106
|
-
let lineStart = text.lastIndexOf('\n', cursorIndex - 2);
|
|
107
|
-
if (lineStart === -1)
|
|
108
|
-
lineStart = 0;
|
|
109
|
-
else
|
|
110
|
-
lineStart += 1;
|
|
111
|
-
const lineText = text.slice(lineStart, cursorIndex - 1);
|
|
112
|
-
// URL — try first so 8-digit IDs inside URLs are not separately linked
|
|
113
|
-
const urlMatch = URL_AT_END_RE.exec(lineText);
|
|
114
|
-
if (urlMatch) {
|
|
115
|
-
const url = urlMatch[1];
|
|
116
|
-
const matchStart = lineStart + lineText.length - url.length;
|
|
117
|
-
const format = quill.getFormat(matchStart, url.length);
|
|
118
|
-
if (!format.link) {
|
|
119
|
-
quill.formatText(matchStart, url.length, 'link', url, 'api');
|
|
120
|
-
}
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
// Case ID (8-digit number)
|
|
124
|
-
const caseMatch = CASE_ID_RE.exec(lineText);
|
|
125
|
-
if (caseMatch) {
|
|
126
|
-
const caseId = caseMatch[1];
|
|
127
|
-
const matchStart = lineStart + lineText.length - caseId.length;
|
|
128
|
-
const format = quill.getFormat(matchStart, caseId.length);
|
|
129
|
-
if (!format.link) {
|
|
130
|
-
quill.formatText(matchStart, caseId.length, 'link', getCaseIdHref(caseId), 'api');
|
|
131
|
-
}
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
// Bugzilla
|
|
135
|
-
const bzMatch = BZ_RE.exec(lineText);
|
|
136
|
-
if (bzMatch) {
|
|
137
|
-
const bugId = bzMatch[1];
|
|
138
|
-
const fullMatch = bzMatch[0].trimStart();
|
|
139
|
-
const matchStart = lineStart + lineText.length - fullMatch.length;
|
|
140
|
-
const format = quill.getFormat(matchStart, fullMatch.length);
|
|
141
|
-
if (!format.link) {
|
|
142
|
-
quill.formatText(matchStart, fullMatch.length, 'link', getBugzillaHref(bugId), 'api');
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Scans the entire document for unlinkified tokens.
|
|
149
|
-
* Call this on blur so trailing URLs / IDs without a delimiter are caught.
|
|
150
|
-
*/
|
|
151
|
-
export function linkifyFullDocument(quill) {
|
|
152
|
-
const length = quill.getLength();
|
|
153
|
-
if (length <= 1)
|
|
154
|
-
return;
|
|
155
|
-
linkifyRange(quill, 0, length - 1);
|
|
156
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|