@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.
- package/es/components/molecules/EmojiCollections/CommonCollection/index.js +18 -4
- package/es/components/molecules/EmojiCollections/LineCollection/index.js +18 -4
- package/es/components/molecules/EmojiCollections/ViberCollection/index.js +17 -4
- package/es/components/molecules/EmojiPopover/EmojiPopover.js +1 -1
- package/es/components/molecules/TagifyInput/TagifyInput.js +175 -132
- package/es/components/molecules/TagifyInput/constants.js +1 -1
- package/es/components/molecules/TagifyInput/utils.d.ts +66 -18
- package/es/components/molecules/TagifyInput/utils.js +89 -23
- package/es/components/organism/ActivityTimeline/components/ProductCard/styled.js +9 -0
- package/es/locales/en/google-sheet.json +1 -1
- package/es/locales/ja/google-sheet.json +1577 -1577
- package/es/locales/vi/google-sheet.json +56 -56
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
620
|
+
if (!tagifyRef.current)
|
|
621
|
+
return;
|
|
622
|
+
if (typeof readonly !== 'undefined') {
|
|
574
623
|
tagifyRef.current.setReadonly(!!readonly);
|
|
575
624
|
}
|
|
576
|
-
|
|
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': '
|
|
16
|
+
'--tag--max-width': '170px',
|
|
17
17
|
'--tag-border-radius': '999px',
|
|
18
18
|
'--tag-bg': 'transparent',
|
|
19
19
|
'--tag-hover': 'transparent',
|