@antscorp/antsomi-ui 1.3.5-beta.830 → 1.3.5-beta.832

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.
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /* eslint-disable no-param-reassign */
3
3
  // Libraries
4
- import { memo, useCallback, useEffect, useMemo } from 'react';
4
+ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
5
5
  import { useImmer } from 'use-immer';
6
6
  import _ from 'lodash';
7
7
  // Components & Styled
@@ -18,6 +18,7 @@ const { Text } = Typography;
18
18
  const emojiListParsed = JSON.parse(JSON.stringify(ICON_EMOJI_COMMON));
19
19
  const CommonCollection = ({ onEmojiClick }) => {
20
20
  // States
21
+ const [visibleCount, setVisibleCount] = useState(40);
21
22
  const [state, setState] = useImmer({
22
23
  collectionActive: SMILEYS_BODY,
23
24
  txtSearch: '',
@@ -47,10 +48,22 @@ const CommonCollection = ({ onEmojiClick }) => {
47
48
  });
48
49
  }
49
50
  }, [setState]);
51
+ const onScroll = useCallback((e, collectionLength) => {
52
+ const { scrollTop, scrollHeight, clientHeight } = e.target;
53
+ if (scrollHeight - scrollTop <= clientHeight * 1.5) {
54
+ setVisibleCount(prev => {
55
+ const newCount = prev + 30;
56
+ if (newCount >= collectionLength)
57
+ return collectionLength;
58
+ return newCount;
59
+ });
60
+ }
61
+ }, []);
50
62
  const handleChangeSelect = useCallback((newCollection) => {
51
63
  setState(draft => {
52
64
  draft.collectionActive = newCollection;
53
65
  });
66
+ setVisibleCount(prev => prev + 10);
54
67
  }, [setState]);
55
68
  useEffect(() => () => {
56
69
  setState(() => ({
@@ -72,13 +85,14 @@ const CommonCollection = ({ onEmojiClick }) => {
72
85
  (_.isArray(emojiCollection) && emojiCollection.length === 0)) {
73
86
  return (_jsx(EmptyEmoji, { image: _jsx(Button, { type: "text", shape: "round", icon: _jsx(EmojiSmileIcon, { color: globalToken?.bw5, style: { width: 30, height: 30 } }), style: { width: 60, height: 60, background: globalToken?.bw2 } }), imageStyle: { height: 60, marginBottom: 15 }, description: _jsx("span", { style: { color: globalToken?.bw8 }, children: "No emoji matches your keyword" }) }));
74
87
  }
75
- const content = emojiCollection.map((item) => {
88
+ const list = emojiCollection.slice(0, Math.min(visibleCount, emojiCollection.length));
89
+ const content = list.map((item) => {
76
90
  const { emoji, slug = '', unicode_version: unicodeVersion = '' } = item;
77
91
  const key = `${slug}-${unicodeVersion}`;
78
92
  return (_jsx(Emoji, { type: "text", onClick: () => onEmojiClick(emoji), children: _jsx("span", { style: { fontSize: '30px' }, children: emoji }) }, key));
79
93
  });
80
- return (_jsx(Scrollbars, { autoHeight: true, autoHeightMax: 150, children: _jsx(Flex, { wrap: "wrap", children: content }) }));
81
- }, [emojiCollection, onEmojiClick]);
94
+ return (_jsx(Scrollbars, { autoHeight: true, autoHeightMax: 150, onScroll: e => onScroll(e, emojiCollection.length), children: _jsx(Flex, { wrap: "wrap", children: content }) }));
95
+ }, [emojiCollection, visibleCount, onScroll, onEmojiClick]);
82
96
  return (_jsxs(WrapperCollection, { children: [_jsxs(Flex, { gap: 20, justify: "space-between", align: "flex-end", children: [_jsx(Input, { placeholder: "Search...", suffix: _jsx(SearchIcon, { color: globalToken?.bw8 }), styles: { affixWrapper: { minWidth: 280 } }, onChange: handleChangeInput }), _jsx(Select, { placeholder: "Select collection", value: state.collectionActive, style: { width: 200 }, options: collectionOptions, optionRender: renderOption, onChange: handleChangeSelect })] }), _jsx(EmojiList, { children: renderCommonCollection() })] }));
83
97
  };
84
98
  CommonCollection.defaultProps = {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /* eslint-disable no-param-reassign */
3
3
  // Libraries
4
- import { memo, useCallback, useEffect, useMemo } from 'react';
4
+ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
5
5
  import _ from 'lodash';
6
6
  import { useImmer } from 'use-immer';
7
7
  // Components & Styled
@@ -14,6 +14,7 @@ import { LINE_MESSAGE_EMOJIS, TAB_LINE_MESSAGE_EMOJIS } from './constants';
14
14
  import { globalToken } from '@antscorp/antsomi-ui/es/constants';
15
15
  const LineCollection = ({ onEmojiClick }) => {
16
16
  // States
17
+ const [visibleCount, setVisibleCount] = useState(40);
17
18
  const [state, setState] = useImmer({
18
19
  collectionActive: TAB_LINE_MESSAGE_EMOJIS[0]?.key || '',
19
20
  });
@@ -23,7 +24,19 @@ const LineCollection = ({ onEmojiClick }) => {
23
24
  setState(draft => {
24
25
  draft.collectionActive = newCollection;
25
26
  });
27
+ setVisibleCount(prev => prev + 10);
26
28
  }, [setState]);
29
+ const onScroll = useCallback((e, collectionLength) => {
30
+ const { scrollTop, scrollHeight, clientHeight } = e.target;
31
+ if (scrollHeight - scrollTop <= clientHeight * 1.5) {
32
+ setVisibleCount(prev => {
33
+ const newCount = prev + 30;
34
+ if (newCount >= collectionLength)
35
+ return collectionLength;
36
+ return newCount;
37
+ });
38
+ }
39
+ }, []);
27
40
  useEffect(() => () => {
28
41
  setState(draft => {
29
42
  draft.collectionActive = TAB_LINE_MESSAGE_EMOJIS[0]?.key || '';
@@ -40,13 +53,14 @@ const LineCollection = ({ onEmojiClick }) => {
40
53
  if (_.isArray(emojiList) && !emojiList.length) {
41
54
  return (_jsx(EmptyEmoji, { image: _jsx(Button, { type: "text", shape: "round", icon: _jsx(EmojiSmileIcon, { color: globalToken?.bw5, style: { width: 30, height: 30 } }), style: { width: 60, height: 60, background: globalToken?.bw2 } }), imageStyle: { height: 60, marginBottom: 15 }, description: _jsx("span", { style: { color: globalToken?.bw8 }, children: "No emoji matches your keyword" }) }));
42
55
  }
43
- const content = emojiList.map((emojiId) => {
56
+ const list = emojiList.slice(0, Math.min(visibleCount, emojiList.length));
57
+ const content = list.map((emojiId) => {
44
58
  const src = getLinkLineURLImage(state.collectionActive, emojiId);
45
59
  const code = `$((${PREFIX_PATTERN_LINE_MESSAGE}:${state.collectionActive}:${emojiId}))`;
46
60
  return (_jsx(Emoji, { type: "text", icon: _jsx(EmojiImage, { src: src, alt: emojiId }), onClick: () => onEmojiClick(code) }, emojiId));
47
61
  });
48
- return (_jsx(Scrollbars, { autoHeight: true, autoHeightMax: 150, children: _jsx(Flex, { wrap: "wrap", children: content }) }));
49
- }, [emojiList, state.collectionActive, onEmojiClick]);
62
+ return (_jsx(Scrollbars, { autoHeight: true, autoHeightMax: 150, onScroll: e => onScroll(e, emojiList.length), children: _jsx(Flex, { wrap: "wrap", children: content }) }));
63
+ }, [emojiList, visibleCount, state.collectionActive, onEmojiClick, onScroll]);
50
64
  return (_jsx(WrapperCollection, { children: _jsxs(Flex, { vertical: true, children: [renderLineCategory(), _jsx(EmojiList, { children: renderLineEmoji() })] }) }));
51
65
  };
52
66
  LineCollection.defaultProps = {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /* eslint-disable no-param-reassign */
3
3
  // Libraries
4
- import { memo, useCallback, useEffect, useMemo } from 'react';
4
+ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
5
5
  import { useImmer } from 'use-immer';
6
6
  import _ from 'lodash';
7
7
  // Hooks
@@ -16,6 +16,7 @@ import { globalToken } from '@antscorp/antsomi-ui/es/constants';
16
16
  const listEmoji = Object.values(iconsViber);
17
17
  const ViberCollection = ({ onEmojiClick }) => {
18
18
  // States
19
+ const [visibleCount, setVisibleCount] = useState(40);
19
20
  const [state, setState] = useImmer({
20
21
  txtSearch: '',
21
22
  });
@@ -36,6 +37,17 @@ const ViberCollection = ({ onEmojiClick }) => {
36
37
  });
37
38
  }
38
39
  }, [setState]);
40
+ const onScroll = useCallback((e, collectionLength) => {
41
+ const { scrollTop, scrollHeight, clientHeight } = e.target;
42
+ if (scrollHeight - scrollTop <= clientHeight * 1.5) {
43
+ setVisibleCount(prev => {
44
+ const newCount = prev + 30;
45
+ if (newCount >= collectionLength)
46
+ return collectionLength;
47
+ return newCount;
48
+ });
49
+ }
50
+ }, []);
39
51
  useEffect(() => () => {
40
52
  setState(draft => {
41
53
  draft.txtSearch = '';
@@ -45,14 +57,15 @@ const ViberCollection = ({ onEmojiClick }) => {
45
57
  if (_.isArray(listEmojiMemoized) && !listEmojiMemoized.length) {
46
58
  return (_jsx(EmptyEmoji, { image: _jsx(Button, { type: "text", shape: "round", icon: _jsx(EmojiSmileIcon, { color: globalToken?.bw5, style: { width: 30, height: 30 } }), style: { width: 60, height: 60, background: globalToken?.bw2 } }), imageStyle: { height: 60, marginBottom: 15 }, description: _jsx("span", { style: { color: globalToken?.bw8 }, children: "No emoji matches your keyword" }) }));
47
59
  }
48
- const content = listEmojiMemoized.map(emojiFileName => {
60
+ const list = listEmojiMemoized.slice(0, Math.min(visibleCount, listEmojiMemoized.length));
61
+ const content = list.map(emojiFileName => {
49
62
  const src = getImageSourceViberEmoji(emojiFileName);
50
63
  const [emoji] = emojiFileName.split('.');
51
64
  const code = `(${emoji})`;
52
65
  return (_jsx(Emoji, { type: "text", icon: _jsx(EmojiImage, { src: src, alt: emojiFileName }), onClick: () => onEmojiClick(code) }, emojiFileName));
53
66
  });
54
- return (_jsx(Scrollbars, { autoHeight: true, autoHeightMax: 150, children: _jsx(Flex, { wrap: "wrap", children: content }) }));
55
- }, [listEmojiMemoized, onEmojiClick]);
67
+ return (_jsx(Scrollbars, { autoHeight: true, autoHeightMax: 150, onScroll: e => onScroll(e, listEmojiMemoized.length), children: _jsx(Flex, { wrap: "wrap", children: content }) }));
68
+ }, [listEmojiMemoized, visibleCount, onEmojiClick, onScroll]);
56
69
  return (_jsx(WrapperCollection, { children: _jsxs(Flex, { vertical: true, children: [_jsx(Input, { placeholder: "Search...", suffix: _jsx(SearchIcon, { color: globalToken?.bw8 }), onChange: handleChangeInput }), _jsx(EmojiList, { children: renderViberCollection() })] }) }));
57
70
  };
58
71
  ViberCollection.defaultProps = {
@@ -66,7 +66,7 @@ const EmojiPopover = ({ disabled, collections, isForceHide, children, onEmojiCli
66
66
  children: renderCollection(collection.key),
67
67
  }));
68
68
  }, [collections, renderCollection]);
69
- return (_jsx(EmojiPopoverStyled, { content: _jsx(EmojiTabs, { activeKey: state.collectionActive, onTabClick: handleClickCollection, items: emojiCollections }), placement: "topLeft", trigger: "click", style: { padding: 15 }, arrow: false, rootClassName: "antsomi-emoji-popover", open: state.isOpen, onOpenChange: handleOpenChange, children: children || (_jsx(Button, { type: "link", icon: _jsx(EmojiSmileIcon, {}), disabled: disabled, onClick: () => handleOpenChange(true) })) }));
69
+ return (_jsx(EmojiPopoverStyled, { content: _jsx(EmojiTabs, { activeKey: state.collectionActive, onTabClick: handleClickCollection, items: emojiCollections }), placement: "topLeft", trigger: "click", fresh: false, style: { padding: 15 }, arrow: false, rootClassName: "antsomi-emoji-popover", open: state.isOpen, onOpenChange: handleOpenChange, children: children || (_jsx(Button, { type: "link", icon: _jsx(EmojiSmileIcon, {}), disabled: disabled, onClick: () => handleOpenChange(true) })) }));
70
70
  };
71
71
  EmojiPopover.defaultProps = {
72
72
  disabled: false,
@@ -17,7 +17,7 @@ import '@yaireo/tagify/dist/tagify.css';
17
17
  // Styled
18
18
  import { TagTextArea, TagifyWrapper, WrapperPlaceHolder } from './styled';
19
19
  // Utils
20
- import { parseTagStringToTagify, convertInputStringToOriginal, emojiManufacturer, getEmojiTag, isPersonalizeTagType, generateTagContent, unescapeString, isValidTagType, hasLineBreak, selectTextRange, } from './utils';
20
+ import { parseTagStringToTagify, convertInputStringToOriginal, emojiManufacturer, getEmojiTag, isPersonalizeTagType, generateTagContent, unescapeString, hasLineBreak, selectRange, isTagClickable, findURLInTextNodes, } from './utils';
21
21
  import { acceptablePatternChecking, detectURLRegex, getCachedRegex, getPersonalizeTagInfo, patternHandlers, } from './patternHandlers';
22
22
  // Constants
23
23
  import { DETECT_LINK, EMOJI, INVALID_TAG, PERSONALIZE_PTN, PROMOTION_CODE, READONLY_TAG, SHORT_LINK, SHORT_LINK_PTN, defaultCssVariables, tagifyDefaultProps, } from './constants';
@@ -48,6 +48,25 @@ const TagifyInput = forwardRef((props, ref) => {
48
48
  return content;
49
49
  // eslint-disable-next-line react-hooks/exhaustive-deps
50
50
  }, []);
51
+ const onSyncSelectionStateTagify = useCallback(() => {
52
+ try {
53
+ if (!tagifyRef.current) {
54
+ throw new Error('Tagify instance is not initialized');
55
+ }
56
+ const instancePrototype = Object.getPrototypeOf(tagifyRef.current);
57
+ if (!_.has(instancePrototype, 'setStateSelection')) {
58
+ throw new Error('Tagify instance does not support setStateSelection');
59
+ }
60
+ // Update the selection of the Tagify instance
61
+ tagifyRef.current.setStateSelection();
62
+ return true;
63
+ }
64
+ catch (error) {
65
+ // eslint-disable-next-line no-console
66
+ console.error(error);
67
+ return false;
68
+ }
69
+ }, []);
51
70
  const onSaveLastRange = useCallback((newRange) => {
52
71
  if (newRange) {
53
72
  lastRange.current = newRange;
@@ -60,16 +79,38 @@ const TagifyInput = forwardRef((props, ref) => {
60
79
  }
61
80
  }
62
81
  }, []);
63
- const onInjectTagAtCaret = useCallback((newTag, addRangeAfterInjected) => {
82
+ const onSelectionAfterInjection = useCallback((addRangeAfterInjected) => {
83
+ try {
84
+ const selection = window.getSelection();
85
+ if (selection?.rangeCount) {
86
+ const range = selection.getRangeAt(0);
87
+ if (addRangeAfterInjected) {
88
+ // Need to re-select to keep the caret position
89
+ selection.removeAllRanges();
90
+ selection.addRange(range);
91
+ }
92
+ // Save of the last range to restore it in case lost the selection
93
+ // E.g user blur of the input
94
+ // and then click direct to the popup without focus input to add new tag
95
+ onSaveLastRange(range);
96
+ }
97
+ }
98
+ catch (error) {
99
+ // eslint-disable-next-line no-console
100
+ console.error('Error while restoring selection after injection', error);
101
+ }
102
+ }, [onSaveLastRange]);
103
+ const onAdjustSelectionIfNeeded = useCallback(() => {
64
104
  try {
65
105
  if (!tagifyRef.current) {
66
106
  throw new Error('Tagify instance is not initialized');
67
107
  }
68
108
  const { settings, DOM } = tagifyRef.current;
109
+ const rangeInstance = _.get(tagifyRef.current, 'state.selection.range', null);
69
110
  const { empty } = settings.classNames;
70
111
  const selection = window.getSelection();
71
112
  // In case not have the selection yet or lost the selection,
72
- if (!selection?.rangeCount) {
113
+ if (!selection?.rangeCount || !rangeInstance) {
73
114
  // need to restore the last range before inject a new tag if the last range exists
74
115
  if (lastRange.current) {
75
116
  selection?.removeAllRanges();
@@ -79,57 +120,65 @@ const TagifyInput = forwardRef((props, ref) => {
79
120
  // If the last range is lost, need to select the last text node of the input
80
121
  const { input: inputElement } = tagifyRef.current.DOM;
81
122
  if (inputElement) {
82
- // Get all child nodes that are of type TEXT_NODE
83
- const textNodes = Array.from(inputElement.childNodes).filter((node) => node.nodeType === Node.TEXT_NODE);
84
- // In case have text nodes
85
- // -> need to place the caret at the end of the last text
86
- if (textNodes.length) {
87
- const lastTextNode = textNodes[textNodes.length - 1];
88
- if (lastTextNode?.textContent) {
89
- const textNodeLength = lastTextNode.textContent.length;
123
+ // Get all child nodes that are of type TEXT_NODE or ELEMENT_NODE with nodeName TAG
124
+ const nodeList = Array.from(inputElement.childNodes).filter((node) => {
125
+ if (node.nodeType === Node.ELEMENT_NODE) {
126
+ return node.nodeName === 'TAG';
127
+ }
128
+ return node.nodeType === Node.TEXT_NODE;
129
+ });
130
+ if (nodeList.length && selection) {
131
+ const lastNode = nodeList[nodeList.length - 1];
132
+ // In case have the last text nodes
133
+ // -> need to place the caret at the end of the last text
134
+ if (lastNode.nodeType === Node.TEXT_NODE && lastNode?.textContent) {
135
+ const textNodeLength = lastNode.textContent.length;
90
136
  // Place the caret at the end of the last text
91
- selectTextRange(lastTextNode, textNodeLength, textNodeLength);
92
- const instancePrototype = Object.getPrototypeOf(tagifyRef.current);
93
- // Update the selection of the Tagify instance
94
- if (_.has(instancePrototype, 'setStateSelection')) {
95
- tagifyRef.current.setStateSelection();
96
- }
137
+ selectRange(lastNode, textNodeLength, textNodeLength);
138
+ onSyncSelectionStateTagify();
139
+ }
140
+ else if (lastNode.nodeType === Node.ELEMENT_NODE && lastNode?.nodeName === 'TAG') {
141
+ // In case no text nodes but have an element node
142
+ const lastNodeExceptBrLength = inputElement.childNodes.length - 1; // Exclude the last <br>
143
+ // Place the caret at the end of the last element node
144
+ selectRange(inputElement, lastNodeExceptBrLength, lastNodeExceptBrLength);
145
+ onSyncSelectionStateTagify();
97
146
  }
98
147
  }
99
148
  else if (DOM?.scope?.classList?.contains(empty)) {
100
149
  // In case no text nodes and the input is empty
101
- selectTextRange(inputElement, 0, 0);
102
- const instancePrototype = Object.getPrototypeOf(tagifyRef.current);
103
- // Update the selection of the Tagify instance
104
- if (_.has(instancePrototype, 'setStateSelection')) {
105
- tagifyRef.current.setStateSelection();
106
- }
150
+ selectRange(inputElement, 0, 0);
151
+ onSyncSelectionStateTagify();
107
152
  }
108
153
  }
109
154
  }
110
155
  }
156
+ }
157
+ catch (error) {
158
+ // eslint-disable-next-line no-console
159
+ console.error('Error while adjusting selection', error);
160
+ }
161
+ }, [onSyncSelectionStateTagify]);
162
+ const onInjectTagAtCaret = useCallback((newTag, addRangeAfterInjected) => {
163
+ try {
164
+ if (!tagifyRef.current) {
165
+ throw new Error('Tagify instance is not initialized');
166
+ }
167
+ const { settings } = tagifyRef.current;
168
+ const { empty } = settings.classNames;
169
+ // Adjust the selection before injecting the new tag
170
+ onAdjustSelectionIfNeeded();
111
171
  // Inject the new tag
112
172
  tagifyRef.current.injectAtCaret(newTag);
113
173
  // Need to remove the empty class immediately after tag added to make the valid the DOM
114
174
  tagifyRef.current.toggleClass(empty, false);
115
- if (selection?.rangeCount) {
116
- const range = selection.getRangeAt(0);
117
- if (addRangeAfterInjected) {
118
- // Need to re-select to keep the caret position
119
- selection.removeAllRanges();
120
- selection.addRange(range);
121
- }
122
- // Save of the last range to restore it in case lost the selection
123
- // E.g user blur of the input
124
- // and then click direct to the popup without focus input to add new tag
125
- onSaveLastRange(range);
126
- }
175
+ onSelectionAfterInjection(addRangeAfterInjected);
127
176
  }
128
177
  catch (error) {
129
178
  // eslint-disable-next-line no-console
130
- console.warn(error);
179
+ console.warn('Error while injecting tag at caret', error);
131
180
  }
132
- }, [onSaveLastRange]);
181
+ }, [onAdjustSelectionIfNeeded, onSelectionAfterInjection]);
133
182
  const placeCaretAfterNode = useCallback((node) => {
134
183
  if (!tagifyRef.current) {
135
184
  throw new Error('Tagify instance is not initialized');
@@ -230,12 +279,9 @@ const TagifyInput = forwardRef((props, ref) => {
230
279
  event.preventDefault();
231
280
  if (event.detail && onTagClick) {
232
281
  const { tagify, tag, data } = event.detail;
233
- const { type } = data;
234
- const isValidType = isValidTagType(type);
235
- const readonlyTag = tag.getAttribute(READONLY_TAG);
236
- const { readonly } = tagify.settings;
282
+ const clickable = isTagClickable(tagify, tag, data);
237
283
  // Prevent to click on tag if readonly
238
- if (readonly || (readonlyTag && readonlyTag === 'true') || !isValidType)
284
+ if (!clickable)
239
285
  return;
240
286
  onTagClick(event.detail);
241
287
  }
@@ -435,7 +481,7 @@ const TagifyInput = forwardRef((props, ref) => {
435
481
  * Updates the window selection to highlight a specified range of text within a given node.
436
482
  * */
437
483
  const updateWindowSelection = useCallback((node, start, end) => {
438
- if (!node || !start || !end)
484
+ if (!node || !_.isNumber(start) || !_.isNumber(end))
439
485
  return false;
440
486
  if (node) {
441
487
  try {
@@ -447,11 +493,7 @@ const TagifyInput = forwardRef((props, ref) => {
447
493
  const selection = window.getSelection();
448
494
  selection?.removeAllRanges();
449
495
  selection?.addRange(range);
450
- const instancePrototype = Object.getPrototypeOf(tagifyRef.current);
451
- if (_.has(instancePrototype, 'setStateSelection')) {
452
- tagifyRef.current.setStateSelection();
453
- return true;
454
- }
496
+ return onSyncSelectionStateTagify();
455
497
  }
456
498
  throw new Error('Invalid start/end position');
457
499
  }
@@ -462,7 +504,76 @@ const TagifyInput = forwardRef((props, ref) => {
462
504
  }
463
505
  }
464
506
  return false;
465
- }, []);
507
+ }, [onSyncSelectionStateTagify]);
508
+ const replaceURLsWithTags = useCallback((nodesURl, lastRange) => {
509
+ try {
510
+ if (!tagifyRef.current) {
511
+ throw new Error('Tagify instance is not initialized');
512
+ }
513
+ // Replace all URLs in the given nodes
514
+ nodesURl.forEach((textNode) => {
515
+ const newTags = [];
516
+ const textContent = textNode.textContent || '';
517
+ if (textContent) {
518
+ let match;
519
+ // Reset the regex's lastIndex before each execution
520
+ detectURLRegex.lastIndex = 0;
521
+ // eslint-disable-next-line no-cond-assign
522
+ while ((match = detectURLRegex.exec(textContent)) !== null) {
523
+ const url = match[0];
524
+ const start = match.index;
525
+ const end = start + url.length;
526
+ const newTag = {
527
+ url,
528
+ range: [start, end],
529
+ };
530
+ // add new valid URL to tags
531
+ newTags.push(newTag);
532
+ }
533
+ if (newTags.length > 0) {
534
+ // Sort tags by their position in reverse order
535
+ // starting from the back avoids messing up earlier parts of the document
536
+ newTags.sort((a, b) => b.range[0] - a.range[0]);
537
+ // Add tags one by one, adjusting the selection for each
538
+ newTags.forEach(tag => {
539
+ const [start, end] = tag.range;
540
+ if (tagifyRef.current) {
541
+ const isUpdated = updateWindowSelection(textNode, start, end);
542
+ if (isUpdated) {
543
+ const newTag = tagifyRef.current.createTagElem({
544
+ value: tag.url,
545
+ label: tag.url,
546
+ type: DETECT_LINK,
547
+ });
548
+ // Inject the new detect link tag
549
+ tagifyRef.current.injectAtCaret(newTag);
550
+ }
551
+ }
552
+ });
553
+ }
554
+ }
555
+ });
556
+ const selection = window.getSelection();
557
+ if (selection) {
558
+ if (lastRange instanceof Range) {
559
+ // Need to restore the selection after execution all process to replace URL to Tag
560
+ // to maintain valid caret
561
+ selection.removeAllRanges();
562
+ selection.addRange(lastRange);
563
+ }
564
+ else {
565
+ // In case lost the selection not need to restore
566
+ // Reset the selection to clear and prepare the valid caret to add the next tag at fn: "onAddNewTag"
567
+ selection?.removeAllRanges();
568
+ onSyncSelectionStateTagify();
569
+ }
570
+ }
571
+ }
572
+ catch (error) {
573
+ // eslint-disable-next-line no-console
574
+ console.error('Error while replacing URL with tag', error);
575
+ }
576
+ }, [onSyncSelectionStateTagify, updateWindowSelection]);
466
577
  /**
467
578
  * Detects URLs within the text nodes of a specified input element and replaces them with tags.
468
579
  *
@@ -480,76 +591,12 @@ const TagifyInput = forwardRef((props, ref) => {
480
591
  const cacheLastRange = selection?.getRangeAt && selection?.rangeCount && selection?.getRangeAt(0);
481
592
  const minLengthValidURL = 8; // -> Because of valid URL should be "https://"
482
593
  // Get all child nodes that are of type TEXT_NODE and contain an URL
483
- const nodesURL = Array.from(inputElement.childNodes).filter((node) => {
484
- // Exclude non-text nodes
485
- if (node.nodeType !== Node.TEXT_NODE)
486
- return false;
487
- // Only check nodes that contain at least 8 characters
488
- if (node.textContent && node.textContent.length >= minLengthValidURL) {
489
- // reset the regex's lastIndex before each execution
490
- detectURLRegex.lastIndex = 0;
491
- return detectURLRegex.test(node.textContent);
492
- }
493
- return false;
494
- });
594
+ const nodesURL = findURLInTextNodes(inputElement, minLengthValidURL);
495
595
  // Starting from the back to avoid messing up earlier parts of the DOM
496
596
  const reversedNodes = nodesURL?.toReversed() || [];
597
+ // Process URL to Tag
497
598
  if (reversedNodes && reversedNodes.length > 0) {
498
- reversedNodes.forEach((textNode) => {
499
- const newTags = [];
500
- const textContent = textNode.textContent || '';
501
- if (textContent) {
502
- let match;
503
- // Reset the regex's lastIndex before each execution
504
- detectURLRegex.lastIndex = 0;
505
- // eslint-disable-next-line no-cond-assign
506
- while ((match = detectURLRegex.exec(textContent)) !== null) {
507
- const url = match[0];
508
- const start = match.index;
509
- const end = start + url.length;
510
- const newTag = {
511
- url,
512
- range: [start, end],
513
- };
514
- // add new valid URL to tags
515
- newTags.push(newTag);
516
- }
517
- if (newTags.length > 0) {
518
- // Sort tags by their position in reverse order
519
- // starting from the back avoids messing up earlier parts of the document
520
- newTags.sort((a, b) => b.range[0] - a.range[0]);
521
- // Add tags one by one, adjusting the selection for each
522
- newTags.forEach(tag => {
523
- const [start, end] = tag.range;
524
- if (tagifyRef.current) {
525
- const isUpdated = updateWindowSelection(textNode, start, end);
526
- if (isUpdated) {
527
- const newTag = tagifyRef.current.createTagElem({
528
- value: tag.url,
529
- label: tag.url,
530
- type: DETECT_LINK,
531
- });
532
- // Inject the new detect link tag
533
- tagifyRef.current.injectAtCaret(newTag);
534
- }
535
- }
536
- });
537
- }
538
- }
539
- });
540
- if (selection) {
541
- if (cacheLastRange instanceof Range) {
542
- // Need to restore the selection after execution all process to replace URL to Tag
543
- // to maintain valid caret
544
- selection.removeAllRanges();
545
- selection.addRange(cacheLastRange);
546
- }
547
- else {
548
- // In case lost the selection not need to restore
549
- // Reset the selection to clear and prepare the valid caret to add the next tag at fn: "onAddNewTag"
550
- selection?.removeAllRanges();
551
- }
552
- }
599
+ replaceURLsWithTags(reversedNodes, cacheLastRange);
553
600
  }
554
601
  }
555
602
  }
@@ -557,7 +604,7 @@ const TagifyInput = forwardRef((props, ref) => {
557
604
  // eslint-disable-next-line no-console
558
605
  console.log(error);
559
606
  }
560
- }, [updateWindowSelection]);
607
+ }, [replaceURLsWithTags]);
561
608
  // Initialization tagify
562
609
  useLayoutEffect(() => {
563
610
  initializeTagify();
@@ -568,12 +615,20 @@ const TagifyInput = forwardRef((props, ref) => {
568
615
  };
569
616
  }, [initializeTagify]);
570
617
  // Settings some tagify attributes
571
- // Set readonly
618
+ // Set [readonly, disabled, placeholder]
572
619
  useEffect(() => {
573
- if (tagifyRef.current && typeof readonly !== 'undefined') {
620
+ if (!tagifyRef.current)
621
+ return;
622
+ if (typeof readonly !== 'undefined') {
574
623
  tagifyRef.current.setReadonly(!!readonly);
575
624
  }
576
- }, [readonly]);
625
+ if (typeof disabled !== 'undefined') {
626
+ tagifyRef.current.setDisabled(!!disabled);
627
+ }
628
+ if (_.isString(placeholder)) {
629
+ tagifyRef.current.setPlaceholder(placeholder);
630
+ }
631
+ }, [disabled, placeholder, readonly]);
577
632
  // Set readonly for each tag
578
633
  useEffect(() => {
579
634
  if (tagifyRef.current && typeof readonlyTag !== 'undefined') {
@@ -597,18 +652,6 @@ const TagifyInput = forwardRef((props, ref) => {
597
652
  });
598
653
  }
599
654
  }, [readonlyTag]);
600
- // Set disabled
601
- useEffect(() => {
602
- if (tagifyRef.current && typeof disabled !== 'undefined') {
603
- tagifyRef.current.setDisabled(!!disabled);
604
- }
605
- }, [disabled]);
606
- // Set placeholder
607
- useLayoutEffect(() => {
608
- if (tagifyRef.current && _.isString(placeholder)) {
609
- tagifyRef.current.setPlaceholder(placeholder);
610
- }
611
- }, [placeholder]);
612
655
  // Set max personalize tags
613
656
  useLayoutEffect(() => {
614
657
  if (tagifyRef.current && typeof maxPersonalizeTags !== 'undefined') {
@@ -13,7 +13,7 @@ export const { READONLY_TAG, INVALID_TAG } = TAG_CUSTOM_ATTRIBUTES;
13
13
  export const defaultCssVariables = {
14
14
  '--input-color': globalToken?.colorText,
15
15
  '--input-font-size': `${globalToken?.fontSize}px`,
16
- '--tag--max-width': '230px',
16
+ '--tag--max-width': '170px',
17
17
  '--tag-border-radius': '999px',
18
18
  '--tag-bg': 'transparent',
19
19
  '--tag-hover': 'transparent',