@selfcommunity/react-ui 0.7.0-alpha.332 → 0.7.0-alpha.334
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/cjs/components/CommentObjectReply/CommentObjectReply.js +1 -1
- package/lib/cjs/components/Editor/plugins/MentionsPlugin.d.ts +15 -3
- package/lib/cjs/components/Editor/plugins/MentionsPlugin.js +149 -37
- package/lib/cjs/components/FeedObject/FeedObject.js +1 -1
- package/lib/esm/components/CommentObjectReply/CommentObjectReply.js +1 -1
- package/lib/esm/components/Editor/plugins/MentionsPlugin.d.ts +15 -3
- package/lib/esm/components/Editor/plugins/MentionsPlugin.js +146 -38
- package/lib/esm/components/FeedObject/FeedObject.js +1 -1
- package/lib/umd/react-ui.js +1 -1
- package/package.json +2 -2
|
@@ -118,7 +118,7 @@ function CommentObjectReply(inProps) {
|
|
|
118
118
|
return _html === '' || _html === '<p class="SCEditor-paragraph"></p>' || _html === '<p class="SCEditor-paragraph"><br></p>';
|
|
119
119
|
}, [html]);
|
|
120
120
|
// RENDER
|
|
121
|
-
return (react_1.default.createElement(Root, Object.assign({}, rest, { disableTypography: true, onClick: handleEditorFocus, elevation: elevation, className: (0, classnames_1.default)(classes.root, className), image: !scUserContext.user ? (react_1.default.createElement(material_1.Avatar, { variant: "circular", className: classes.avatar })) : (react_1.default.createElement(UserAvatar_1.default, { hide: !scUserContext.user.community_badge },
|
|
121
|
+
return (react_1.default.createElement(Root, Object.assign({ id: id }, rest, { disableTypography: true, onClick: handleEditorFocus, elevation: elevation, className: (0, classnames_1.default)(classes.root, className), image: !scUserContext.user ? (react_1.default.createElement(material_1.Avatar, { variant: "circular", className: classes.avatar })) : (react_1.default.createElement(UserAvatar_1.default, { hide: !scUserContext.user.community_badge },
|
|
122
122
|
react_1.default.createElement(material_1.Avatar, { alt: scUserContext.user.username, variant: "circular", src: scUserContext.user.avatar, classes: { root: classes.avatar } }))), secondary: react_1.default.createElement(Widget_1.default, Object.assign({ className: (0, classnames_1.default)(classes.comment, { [classes.hasValue]: !isEditorEmpty }) }, WidgetProps),
|
|
123
123
|
react_1.default.createElement(Editor_1.default, { ref: editor, onChange: handleChangeText, defaultValue: html, editable: editable, uploadImage: true }),
|
|
124
124
|
!isEditorEmpty && (react_1.default.createElement(material_1.Stack, { direction: "row", spacing: 2, className: classes.actions },
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { MutableRefObject } from 'react';
|
|
2
|
+
declare type MentionMatch = {
|
|
3
|
+
leadOffset: number;
|
|
4
|
+
matchingString: string;
|
|
5
|
+
replaceableString: string;
|
|
6
|
+
};
|
|
7
|
+
declare type Resolution = {
|
|
8
|
+
match: MentionMatch;
|
|
9
|
+
range: Range;
|
|
10
|
+
};
|
|
11
|
+
export declare function getScrollParent(element: HTMLElement, includeHidden: boolean): HTMLElement | HTMLBodyElement;
|
|
12
|
+
export declare function useDynamicPositioning(resolution: Resolution | null, targetElement: HTMLElement | null, onReposition: () => void, onVisibilityChange?: (isInView: boolean) => void): void;
|
|
13
|
+
export declare function useMenuAnchorRef(resolution: Resolution | null, setResolution: (r: Resolution | null) => void, className?: string): MutableRefObject<HTMLElement>;
|
|
14
|
+
export default function MentionsPlugin(): JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useMenuAnchorRef = exports.useDynamicPositioning = exports.getScrollParent = void 0;
|
|
3
4
|
const tslib_1 = require("tslib");
|
|
4
5
|
const react_1 = tslib_1.__importStar(require("react"));
|
|
5
6
|
const react_core_1 = require("@selfcommunity/react-core");
|
|
6
7
|
const lexical_1 = require("lexical");
|
|
7
8
|
const LexicalComposerContext_1 = require("@lexical/react/LexicalComposerContext");
|
|
8
9
|
const utils_1 = require("@lexical/utils");
|
|
9
|
-
const react_dom_1 = require("react-dom");
|
|
10
10
|
const MentionNode_1 = require("../nodes/MentionNode");
|
|
11
11
|
const api_services_1 = require("@selfcommunity/api-services");
|
|
12
12
|
const classnames_1 = tslib_1.__importDefault(require("classnames"));
|
|
13
13
|
const material_1 = require("@mui/material");
|
|
14
14
|
const styles_1 = require("@mui/material/styles");
|
|
15
|
+
const ClickAwayListener_1 = tslib_1.__importDefault(require("@mui/material/ClickAwayListener"));
|
|
15
16
|
const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;';
|
|
16
17
|
const NAME = '\\b[A-Z][^\\s' + PUNCTUATION + ']';
|
|
17
18
|
const DocumentMentionsRegex = {
|
|
@@ -40,6 +41,137 @@ const ALIAS_LENGTH_LIMIT = 50;
|
|
|
40
41
|
const AtSignMentionsRegexAliasRegex = new RegExp('(^|\\s|\\()(' + '[' + TRIGGERS + ']' + '((?:' + VALID_CHARS + '){0,' + ALIAS_LENGTH_LIMIT + '})' + ')$');
|
|
41
42
|
// At most, 5 suggestions are shown in the popup.
|
|
42
43
|
const SUGGESTION_LIST_LENGTH_LIMIT = 5;
|
|
44
|
+
function isTriggerVisibleInNearestScrollContainer(targetElement, containerElement) {
|
|
45
|
+
const tRect = targetElement.getBoundingClientRect();
|
|
46
|
+
const cRect = containerElement.getBoundingClientRect();
|
|
47
|
+
return tRect.top > cRect.top && tRect.top < cRect.bottom;
|
|
48
|
+
}
|
|
49
|
+
function getScrollParent(element, includeHidden) {
|
|
50
|
+
let style = getComputedStyle(element);
|
|
51
|
+
const excludeStaticParent = style.position === 'absolute';
|
|
52
|
+
const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
|
|
53
|
+
if (style.position === 'fixed') {
|
|
54
|
+
return document.body;
|
|
55
|
+
}
|
|
56
|
+
for (let parent = element; (parent = parent.parentElement);) {
|
|
57
|
+
style = getComputedStyle(parent);
|
|
58
|
+
if (excludeStaticParent && style.position === 'static') {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
|
|
62
|
+
return parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return document.body;
|
|
66
|
+
}
|
|
67
|
+
exports.getScrollParent = getScrollParent;
|
|
68
|
+
function useDynamicPositioning(resolution, targetElement, onReposition, onVisibilityChange) {
|
|
69
|
+
const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
|
|
70
|
+
(0, react_1.useEffect)(() => {
|
|
71
|
+
if (targetElement != null && resolution != null) {
|
|
72
|
+
const rootElement = editor.getRootElement();
|
|
73
|
+
const rootScrollParent = rootElement != null ? getScrollParent(rootElement, false) : document.body;
|
|
74
|
+
let ticking = false;
|
|
75
|
+
let previousIsInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
|
|
76
|
+
const handleScroll = function () {
|
|
77
|
+
if (!ticking) {
|
|
78
|
+
window.requestAnimationFrame(function () {
|
|
79
|
+
onReposition();
|
|
80
|
+
ticking = false;
|
|
81
|
+
});
|
|
82
|
+
ticking = true;
|
|
83
|
+
}
|
|
84
|
+
const isInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
|
|
85
|
+
if (isInView !== previousIsInView) {
|
|
86
|
+
previousIsInView = isInView;
|
|
87
|
+
if (onVisibilityChange != null) {
|
|
88
|
+
onVisibilityChange(isInView);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const resizeObserver = new ResizeObserver(onReposition);
|
|
93
|
+
window.addEventListener('resize', onReposition);
|
|
94
|
+
document.addEventListener('scroll', handleScroll, {
|
|
95
|
+
capture: true,
|
|
96
|
+
passive: true
|
|
97
|
+
});
|
|
98
|
+
resizeObserver.observe(targetElement);
|
|
99
|
+
return () => {
|
|
100
|
+
resizeObserver.unobserve(targetElement);
|
|
101
|
+
window.removeEventListener('resize', onReposition);
|
|
102
|
+
document.removeEventListener('scroll', handleScroll, true);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
|
|
106
|
+
}
|
|
107
|
+
exports.useDynamicPositioning = useDynamicPositioning;
|
|
108
|
+
function useMenuAnchorRef(resolution, setResolution, className) {
|
|
109
|
+
const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
|
|
110
|
+
const anchorElementRef = (0, react_1.useRef)(document.createElement('div'));
|
|
111
|
+
const positionMenu = (0, react_1.useCallback)(() => {
|
|
112
|
+
const rootElement = editor.getRootElement();
|
|
113
|
+
const containerDiv = anchorElementRef.current;
|
|
114
|
+
const menuEle = containerDiv.firstChild;
|
|
115
|
+
if (rootElement !== null && resolution !== null) {
|
|
116
|
+
const { left, top, width, height } = resolution.range.getBoundingClientRect();
|
|
117
|
+
containerDiv.style.top = `${top + window.pageYOffset}px`;
|
|
118
|
+
containerDiv.style.left = `${left + window.pageXOffset}px`;
|
|
119
|
+
containerDiv.style.height = `${height}px`;
|
|
120
|
+
containerDiv.style.width = `${width}px`;
|
|
121
|
+
if (menuEle !== null) {
|
|
122
|
+
const menuRect = menuEle.getBoundingClientRect();
|
|
123
|
+
const menuHeight = menuRect.height;
|
|
124
|
+
const menuWidth = menuRect.width;
|
|
125
|
+
const rootElementRect = rootElement.getBoundingClientRect();
|
|
126
|
+
if (left + menuWidth > rootElementRect.right) {
|
|
127
|
+
containerDiv.style.left = `${rootElementRect.right - menuWidth + window.pageXOffset}px`;
|
|
128
|
+
}
|
|
129
|
+
const margin = 10;
|
|
130
|
+
if ((top + menuHeight > window.innerHeight || top + menuHeight > rootElementRect.bottom) && top - rootElementRect.top > menuHeight) {
|
|
131
|
+
containerDiv.style.top = `${top - menuHeight + window.pageYOffset - (height + margin)}px`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!containerDiv.isConnected) {
|
|
135
|
+
if (className != null) {
|
|
136
|
+
containerDiv.className = className;
|
|
137
|
+
}
|
|
138
|
+
containerDiv.setAttribute('aria-label', 'Typeahead menu');
|
|
139
|
+
containerDiv.setAttribute('id', 'typeahead-menu');
|
|
140
|
+
containerDiv.setAttribute('role', 'listbox');
|
|
141
|
+
containerDiv.style.display = 'block';
|
|
142
|
+
containerDiv.style.position = 'absolute';
|
|
143
|
+
document.body.append(containerDiv);
|
|
144
|
+
}
|
|
145
|
+
anchorElementRef.current = containerDiv;
|
|
146
|
+
rootElement.setAttribute('aria-controls', 'typeahead-menu');
|
|
147
|
+
}
|
|
148
|
+
}, [editor, resolution, className]);
|
|
149
|
+
(0, react_1.useEffect)(() => {
|
|
150
|
+
const rootElement = editor.getRootElement();
|
|
151
|
+
if (resolution !== null) {
|
|
152
|
+
positionMenu();
|
|
153
|
+
return () => {
|
|
154
|
+
if (rootElement !== null) {
|
|
155
|
+
rootElement.removeAttribute('aria-controls');
|
|
156
|
+
}
|
|
157
|
+
const containerDiv = anchorElementRef.current;
|
|
158
|
+
if (containerDiv !== null && containerDiv.isConnected) {
|
|
159
|
+
containerDiv.remove();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}, [editor, positionMenu, resolution]);
|
|
164
|
+
const onVisibilityChange = (0, react_1.useCallback)((isInView) => {
|
|
165
|
+
if (resolution !== null) {
|
|
166
|
+
if (!isInView) {
|
|
167
|
+
setResolution(null);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, [resolution, setResolution]);
|
|
171
|
+
useDynamicPositioning(resolution, anchorElementRef.current, positionMenu, onVisibilityChange);
|
|
172
|
+
return anchorElementRef;
|
|
173
|
+
}
|
|
174
|
+
exports.useMenuAnchorRef = useMenuAnchorRef;
|
|
43
175
|
const mentionsCache = new Map();
|
|
44
176
|
function useMentionLookupService(mentionString) {
|
|
45
177
|
const [results, setResults] = (0, react_1.useState)(null);
|
|
@@ -73,34 +205,12 @@ function MentionsTypeaheadItem({ index, isHovered, isSelected, onClick, onMouseE
|
|
|
73
205
|
" ",
|
|
74
206
|
result.username));
|
|
75
207
|
}
|
|
76
|
-
function MentionsTypeahead({ close, editor, resolution, className = ''
|
|
208
|
+
function MentionsTypeahead({ close, editor, resolution, className = '' }) {
|
|
77
209
|
const divRef = (0, react_1.useRef)(null);
|
|
78
210
|
const match = resolution.match;
|
|
79
211
|
const results = useMentionLookupService(match.matchingString);
|
|
80
212
|
const [selectedIndex, setSelectedIndex] = (0, react_1.useState)(null);
|
|
81
213
|
const [hoveredIndex, setHoveredIndex] = (0, react_1.useState)(null);
|
|
82
|
-
(0, react_1.useEffect)(() => {
|
|
83
|
-
const div = divRef.current;
|
|
84
|
-
const rootElement = editor.getRootElement();
|
|
85
|
-
const parentContainerElement = containerEl ? containerEl : rootElement.parentElement;
|
|
86
|
-
if (results !== null && div !== null && rootElement !== null) {
|
|
87
|
-
const range = resolution.range;
|
|
88
|
-
// Re-calc, relative to the parent container, prevent scroll problems
|
|
89
|
-
const parentRootPos = parentContainerElement.getBoundingClientRect();
|
|
90
|
-
const { left, right, top, height } = range.getBoundingClientRect();
|
|
91
|
-
let relativePosTop = top - parentRootPos.top;
|
|
92
|
-
let relativePosLeft = right - parentRootPos.left;
|
|
93
|
-
div.style.position = 'absolute';
|
|
94
|
-
div.style.top = `${relativePosTop + height + 7}px`;
|
|
95
|
-
div.style.left = `${relativePosLeft - 14}px`;
|
|
96
|
-
div.style.display = 'block';
|
|
97
|
-
rootElement.setAttribute('aria-controls', 'mentions-typeahead');
|
|
98
|
-
return () => {
|
|
99
|
-
div.style.display = 'none';
|
|
100
|
-
rootElement.removeAttribute('aria-controls');
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
}, [editor, resolution, results]);
|
|
104
214
|
const applyCurrentSelected = (0, react_1.useCallback)((index) => {
|
|
105
215
|
index = index || selectedIndex;
|
|
106
216
|
if (results === null || index === null) {
|
|
@@ -139,9 +249,9 @@ function MentionsTypeahead({ close, editor, resolution, className = '', containe
|
|
|
139
249
|
if (results !== null && selectedIndex !== null) {
|
|
140
250
|
if (selectedIndex < SUGGESTION_LIST_LENGTH_LIMIT - 1 && selectedIndex !== results.length - 1) {
|
|
141
251
|
updateSelectedIndex(selectedIndex + 1);
|
|
252
|
+
event.preventDefault();
|
|
253
|
+
event.stopImmediatePropagation();
|
|
142
254
|
}
|
|
143
|
-
event.preventDefault();
|
|
144
|
-
event.stopImmediatePropagation();
|
|
145
255
|
}
|
|
146
256
|
return true;
|
|
147
257
|
}, lexical_1.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical_1.KEY_ARROW_UP_COMMAND, (payload) => {
|
|
@@ -149,9 +259,9 @@ function MentionsTypeahead({ close, editor, resolution, className = '', containe
|
|
|
149
259
|
if (results !== null && selectedIndex !== null) {
|
|
150
260
|
if (selectedIndex !== 0) {
|
|
151
261
|
updateSelectedIndex(selectedIndex - 1);
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
event.stopImmediatePropagation();
|
|
152
264
|
}
|
|
153
|
-
event.preventDefault();
|
|
154
|
-
event.stopImmediatePropagation();
|
|
155
265
|
}
|
|
156
266
|
return true;
|
|
157
267
|
}, lexical_1.COMMAND_PRIORITY_LOW), editor.registerCommand(lexical_1.KEY_ESCAPE_COMMAND, (payload) => {
|
|
@@ -260,6 +370,9 @@ function tryToPositionRange(match, range) {
|
|
|
260
370
|
const anchorNode = domSelection.anchorNode;
|
|
261
371
|
const startOffset = match.leadOffset;
|
|
262
372
|
const endOffset = domSelection.anchorOffset;
|
|
373
|
+
if (anchorNode == null || endOffset == null) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
263
376
|
try {
|
|
264
377
|
range.setStart(anchorNode, startOffset);
|
|
265
378
|
range.setEnd(anchorNode, endOffset);
|
|
@@ -366,8 +479,9 @@ const Root = (0, styles_1.styled)(MentionsTypeahead, {
|
|
|
366
479
|
slot: 'Root',
|
|
367
480
|
overridesResolver: (props, styles) => styles.root
|
|
368
481
|
})(({ theme }) => ({}));
|
|
369
|
-
function useMentions(editor,
|
|
482
|
+
function useMentions(editor, anchorClassName = null) {
|
|
370
483
|
const [resolution, setResolution] = (0, react_1.useState)(null);
|
|
484
|
+
const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName);
|
|
371
485
|
(0, react_1.useEffect)(() => {
|
|
372
486
|
if (!editor.hasNodes([MentionNode_1.MentionNode])) {
|
|
373
487
|
throw new Error('MentionsPlugin: MentionNode not registered on editor');
|
|
@@ -410,18 +524,16 @@ function useMentions(editor, containerSelector = null) {
|
|
|
410
524
|
}, [editor]);
|
|
411
525
|
const closeTypeahead = (0, react_1.useCallback)(() => {
|
|
412
526
|
setResolution(null);
|
|
413
|
-
}, []);
|
|
527
|
+
}, [resolution]);
|
|
414
528
|
if (resolution === null || editor === null) {
|
|
415
529
|
return null;
|
|
416
530
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
: editor.getRootElement().parentElement;
|
|
421
|
-
return (0, react_dom_1.createPortal)(react_1.default.createElement(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root, containerEl: portalContainer }), portalContainer);
|
|
531
|
+
return (react_1.default.createElement(ClickAwayListener_1.default, { onClickAway: closeTypeahead },
|
|
532
|
+
react_1.default.createElement(material_1.Portal, { container: anchorElementRef.current },
|
|
533
|
+
react_1.default.createElement(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root }))));
|
|
422
534
|
}
|
|
423
|
-
function MentionsPlugin(
|
|
535
|
+
function MentionsPlugin() {
|
|
424
536
|
const [editor] = (0, LexicalComposerContext_1.useLexicalComposerContext)();
|
|
425
|
-
return useMentions(editor
|
|
537
|
+
return useMentions(editor);
|
|
426
538
|
}
|
|
427
539
|
exports.default = MentionsPlugin;
|
|
@@ -457,7 +457,7 @@ function FeedObject(inProps) {
|
|
|
457
457
|
react_1.default.createElement(material_1.CardActions, { className: classes.actionsSection },
|
|
458
458
|
react_1.default.createElement(Actions_1.default, Object.assign({ feedObjectId: feedObjectId, feedObjectType: feedObjectType, feedObject: obj, hideCommentAction: template === feedObject_1.SCFeedObjectTemplateType.DETAIL, handleExpandActivities: template === feedObject_1.SCFeedObjectTemplateType.PREVIEW ? handleExpandActivities : null, VoteActionProps: { onVoteAction: handleVoteSuccess } }, ActionsProps)),
|
|
459
459
|
(template === feedObject_1.SCFeedObjectTemplateType.DETAIL || expandedActivities) && (react_1.default.createElement(material_1.Box, { className: classes.replyContent },
|
|
460
|
-
react_1.default.createElement(CommentObjectReplyComponent, Object.assign({ onReply: handleReply, editable: !isReplying || Boolean(obj), key: Number(isReplying) }, CommentObjectReplyComponentProps))))),
|
|
460
|
+
react_1.default.createElement(CommentObjectReplyComponent, Object.assign({ id: `reply-feedObject-${obj.id}`, onReply: handleReply, editable: !isReplying || Boolean(obj), key: Number(isReplying) }, CommentObjectReplyComponentProps))))),
|
|
461
461
|
template === feedObject_1.SCFeedObjectTemplateType.PREVIEW && (obj.comment_count > 0 || (feedObjectActivities && feedObjectActivities.length > 0)) && (react_1.default.createElement(material_1.Collapse, { in: expandedActivities, timeout: "auto", classes: { root: classes.activitiesSection } },
|
|
462
462
|
react_1.default.createElement(CardContent_1.default, { className: classes.activitiesContent },
|
|
463
463
|
react_1.default.createElement(Activities_1.default, Object.assign({ feedObject: obj, key: selectedActivities, feedObjectActivities: feedObjectActivities, activitiesType: selectedActivities, onSetSelectedActivities: handleSelectedActivities, comments: comments, CommentsObjectProps: {
|
|
@@ -116,7 +116,7 @@ export default function CommentObjectReply(inProps) {
|
|
|
116
116
|
return _html === '' || _html === '<p class="SCEditor-paragraph"></p>' || _html === '<p class="SCEditor-paragraph"><br></p>';
|
|
117
117
|
}, [html]);
|
|
118
118
|
// RENDER
|
|
119
|
-
return (React.createElement(Root, Object.assign({}, rest, { disableTypography: true, onClick: handleEditorFocus, elevation: elevation, className: classNames(classes.root, className), image: !scUserContext.user ? (React.createElement(Avatar, { variant: "circular", className: classes.avatar })) : (React.createElement(UserAvatar, { hide: !scUserContext.user.community_badge },
|
|
119
|
+
return (React.createElement(Root, Object.assign({ id: id }, rest, { disableTypography: true, onClick: handleEditorFocus, elevation: elevation, className: classNames(classes.root, className), image: !scUserContext.user ? (React.createElement(Avatar, { variant: "circular", className: classes.avatar })) : (React.createElement(UserAvatar, { hide: !scUserContext.user.community_badge },
|
|
120
120
|
React.createElement(Avatar, { alt: scUserContext.user.username, variant: "circular", src: scUserContext.user.avatar, classes: { root: classes.avatar } }))), secondary: React.createElement(Widget, Object.assign({ className: classNames(classes.comment, { [classes.hasValue]: !isEditorEmpty }) }, WidgetProps),
|
|
121
121
|
React.createElement(Editor, { ref: editor, onChange: handleChangeText, defaultValue: html, editable: editable, uploadImage: true }),
|
|
122
122
|
!isEditorEmpty && (React.createElement(Stack, { direction: "row", spacing: 2, className: classes.actions },
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { MutableRefObject } from 'react';
|
|
2
|
+
declare type MentionMatch = {
|
|
3
|
+
leadOffset: number;
|
|
4
|
+
matchingString: string;
|
|
5
|
+
replaceableString: string;
|
|
6
|
+
};
|
|
7
|
+
declare type Resolution = {
|
|
8
|
+
match: MentionMatch;
|
|
9
|
+
range: Range;
|
|
10
|
+
};
|
|
11
|
+
export declare function getScrollParent(element: HTMLElement, includeHidden: boolean): HTMLElement | HTMLBodyElement;
|
|
12
|
+
export declare function useDynamicPositioning(resolution: Resolution | null, targetElement: HTMLElement | null, onReposition: () => void, onVisibilityChange?: (isInView: boolean) => void): void;
|
|
13
|
+
export declare function useMenuAnchorRef(resolution: Resolution | null, setResolution: (r: Resolution | null) => void, className?: string): MutableRefObject<HTMLElement>;
|
|
14
|
+
export default function MentionsPlugin(): JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -3,12 +3,12 @@ import { useIsomorphicLayoutEffect } from '@selfcommunity/react-core';
|
|
|
3
3
|
import { $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND } from 'lexical';
|
|
4
4
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
5
5
|
import { mergeRegister } from '@lexical/utils';
|
|
6
|
-
import { createPortal } from 'react-dom';
|
|
7
6
|
import { createMentionNode, MentionNode } from '../nodes/MentionNode';
|
|
8
7
|
import { http, Endpoints } from '@selfcommunity/api-services';
|
|
9
8
|
import classNames from 'classnames';
|
|
10
|
-
import { Avatar } from '@mui/material';
|
|
9
|
+
import { Avatar, Portal } from '@mui/material';
|
|
11
10
|
import { styled } from '@mui/material/styles';
|
|
11
|
+
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
|
12
12
|
const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;';
|
|
13
13
|
const NAME = '\\b[A-Z][^\\s' + PUNCTUATION + ']';
|
|
14
14
|
const DocumentMentionsRegex = {
|
|
@@ -37,6 +37,134 @@ const ALIAS_LENGTH_LIMIT = 50;
|
|
|
37
37
|
const AtSignMentionsRegexAliasRegex = new RegExp('(^|\\s|\\()(' + '[' + TRIGGERS + ']' + '((?:' + VALID_CHARS + '){0,' + ALIAS_LENGTH_LIMIT + '})' + ')$');
|
|
38
38
|
// At most, 5 suggestions are shown in the popup.
|
|
39
39
|
const SUGGESTION_LIST_LENGTH_LIMIT = 5;
|
|
40
|
+
function isTriggerVisibleInNearestScrollContainer(targetElement, containerElement) {
|
|
41
|
+
const tRect = targetElement.getBoundingClientRect();
|
|
42
|
+
const cRect = containerElement.getBoundingClientRect();
|
|
43
|
+
return tRect.top > cRect.top && tRect.top < cRect.bottom;
|
|
44
|
+
}
|
|
45
|
+
export function getScrollParent(element, includeHidden) {
|
|
46
|
+
let style = getComputedStyle(element);
|
|
47
|
+
const excludeStaticParent = style.position === 'absolute';
|
|
48
|
+
const overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
|
|
49
|
+
if (style.position === 'fixed') {
|
|
50
|
+
return document.body;
|
|
51
|
+
}
|
|
52
|
+
for (let parent = element; (parent = parent.parentElement);) {
|
|
53
|
+
style = getComputedStyle(parent);
|
|
54
|
+
if (excludeStaticParent && style.position === 'static') {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) {
|
|
58
|
+
return parent;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return document.body;
|
|
62
|
+
}
|
|
63
|
+
export function useDynamicPositioning(resolution, targetElement, onReposition, onVisibilityChange) {
|
|
64
|
+
const [editor] = useLexicalComposerContext();
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (targetElement != null && resolution != null) {
|
|
67
|
+
const rootElement = editor.getRootElement();
|
|
68
|
+
const rootScrollParent = rootElement != null ? getScrollParent(rootElement, false) : document.body;
|
|
69
|
+
let ticking = false;
|
|
70
|
+
let previousIsInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
|
|
71
|
+
const handleScroll = function () {
|
|
72
|
+
if (!ticking) {
|
|
73
|
+
window.requestAnimationFrame(function () {
|
|
74
|
+
onReposition();
|
|
75
|
+
ticking = false;
|
|
76
|
+
});
|
|
77
|
+
ticking = true;
|
|
78
|
+
}
|
|
79
|
+
const isInView = isTriggerVisibleInNearestScrollContainer(targetElement, rootScrollParent);
|
|
80
|
+
if (isInView !== previousIsInView) {
|
|
81
|
+
previousIsInView = isInView;
|
|
82
|
+
if (onVisibilityChange != null) {
|
|
83
|
+
onVisibilityChange(isInView);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const resizeObserver = new ResizeObserver(onReposition);
|
|
88
|
+
window.addEventListener('resize', onReposition);
|
|
89
|
+
document.addEventListener('scroll', handleScroll, {
|
|
90
|
+
capture: true,
|
|
91
|
+
passive: true
|
|
92
|
+
});
|
|
93
|
+
resizeObserver.observe(targetElement);
|
|
94
|
+
return () => {
|
|
95
|
+
resizeObserver.unobserve(targetElement);
|
|
96
|
+
window.removeEventListener('resize', onReposition);
|
|
97
|
+
document.removeEventListener('scroll', handleScroll, true);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
|
|
101
|
+
}
|
|
102
|
+
export function useMenuAnchorRef(resolution, setResolution, className) {
|
|
103
|
+
const [editor] = useLexicalComposerContext();
|
|
104
|
+
const anchorElementRef = useRef(document.createElement('div'));
|
|
105
|
+
const positionMenu = useCallback(() => {
|
|
106
|
+
const rootElement = editor.getRootElement();
|
|
107
|
+
const containerDiv = anchorElementRef.current;
|
|
108
|
+
const menuEle = containerDiv.firstChild;
|
|
109
|
+
if (rootElement !== null && resolution !== null) {
|
|
110
|
+
const { left, top, width, height } = resolution.range.getBoundingClientRect();
|
|
111
|
+
containerDiv.style.top = `${top + window.pageYOffset}px`;
|
|
112
|
+
containerDiv.style.left = `${left + window.pageXOffset}px`;
|
|
113
|
+
containerDiv.style.height = `${height}px`;
|
|
114
|
+
containerDiv.style.width = `${width}px`;
|
|
115
|
+
if (menuEle !== null) {
|
|
116
|
+
const menuRect = menuEle.getBoundingClientRect();
|
|
117
|
+
const menuHeight = menuRect.height;
|
|
118
|
+
const menuWidth = menuRect.width;
|
|
119
|
+
const rootElementRect = rootElement.getBoundingClientRect();
|
|
120
|
+
if (left + menuWidth > rootElementRect.right) {
|
|
121
|
+
containerDiv.style.left = `${rootElementRect.right - menuWidth + window.pageXOffset}px`;
|
|
122
|
+
}
|
|
123
|
+
const margin = 10;
|
|
124
|
+
if ((top + menuHeight > window.innerHeight || top + menuHeight > rootElementRect.bottom) && top - rootElementRect.top > menuHeight) {
|
|
125
|
+
containerDiv.style.top = `${top - menuHeight + window.pageYOffset - (height + margin)}px`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!containerDiv.isConnected) {
|
|
129
|
+
if (className != null) {
|
|
130
|
+
containerDiv.className = className;
|
|
131
|
+
}
|
|
132
|
+
containerDiv.setAttribute('aria-label', 'Typeahead menu');
|
|
133
|
+
containerDiv.setAttribute('id', 'typeahead-menu');
|
|
134
|
+
containerDiv.setAttribute('role', 'listbox');
|
|
135
|
+
containerDiv.style.display = 'block';
|
|
136
|
+
containerDiv.style.position = 'absolute';
|
|
137
|
+
document.body.append(containerDiv);
|
|
138
|
+
}
|
|
139
|
+
anchorElementRef.current = containerDiv;
|
|
140
|
+
rootElement.setAttribute('aria-controls', 'typeahead-menu');
|
|
141
|
+
}
|
|
142
|
+
}, [editor, resolution, className]);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const rootElement = editor.getRootElement();
|
|
145
|
+
if (resolution !== null) {
|
|
146
|
+
positionMenu();
|
|
147
|
+
return () => {
|
|
148
|
+
if (rootElement !== null) {
|
|
149
|
+
rootElement.removeAttribute('aria-controls');
|
|
150
|
+
}
|
|
151
|
+
const containerDiv = anchorElementRef.current;
|
|
152
|
+
if (containerDiv !== null && containerDiv.isConnected) {
|
|
153
|
+
containerDiv.remove();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}, [editor, positionMenu, resolution]);
|
|
158
|
+
const onVisibilityChange = useCallback((isInView) => {
|
|
159
|
+
if (resolution !== null) {
|
|
160
|
+
if (!isInView) {
|
|
161
|
+
setResolution(null);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}, [resolution, setResolution]);
|
|
165
|
+
useDynamicPositioning(resolution, anchorElementRef.current, positionMenu, onVisibilityChange);
|
|
166
|
+
return anchorElementRef;
|
|
167
|
+
}
|
|
40
168
|
const mentionsCache = new Map();
|
|
41
169
|
function useMentionLookupService(mentionString) {
|
|
42
170
|
const [results, setResults] = useState(null);
|
|
@@ -70,34 +198,12 @@ function MentionsTypeaheadItem({ index, isHovered, isSelected, onClick, onMouseE
|
|
|
70
198
|
" ",
|
|
71
199
|
result.username));
|
|
72
200
|
}
|
|
73
|
-
function MentionsTypeahead({ close, editor, resolution, className = ''
|
|
201
|
+
function MentionsTypeahead({ close, editor, resolution, className = '' }) {
|
|
74
202
|
const divRef = useRef(null);
|
|
75
203
|
const match = resolution.match;
|
|
76
204
|
const results = useMentionLookupService(match.matchingString);
|
|
77
205
|
const [selectedIndex, setSelectedIndex] = useState(null);
|
|
78
206
|
const [hoveredIndex, setHoveredIndex] = useState(null);
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
const div = divRef.current;
|
|
81
|
-
const rootElement = editor.getRootElement();
|
|
82
|
-
const parentContainerElement = containerEl ? containerEl : rootElement.parentElement;
|
|
83
|
-
if (results !== null && div !== null && rootElement !== null) {
|
|
84
|
-
const range = resolution.range;
|
|
85
|
-
// Re-calc, relative to the parent container, prevent scroll problems
|
|
86
|
-
const parentRootPos = parentContainerElement.getBoundingClientRect();
|
|
87
|
-
const { left, right, top, height } = range.getBoundingClientRect();
|
|
88
|
-
let relativePosTop = top - parentRootPos.top;
|
|
89
|
-
let relativePosLeft = right - parentRootPos.left;
|
|
90
|
-
div.style.position = 'absolute';
|
|
91
|
-
div.style.top = `${relativePosTop + height + 7}px`;
|
|
92
|
-
div.style.left = `${relativePosLeft - 14}px`;
|
|
93
|
-
div.style.display = 'block';
|
|
94
|
-
rootElement.setAttribute('aria-controls', 'mentions-typeahead');
|
|
95
|
-
return () => {
|
|
96
|
-
div.style.display = 'none';
|
|
97
|
-
rootElement.removeAttribute('aria-controls');
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
}, [editor, resolution, results]);
|
|
101
207
|
const applyCurrentSelected = useCallback((index) => {
|
|
102
208
|
index = index || selectedIndex;
|
|
103
209
|
if (results === null || index === null) {
|
|
@@ -136,9 +242,9 @@ function MentionsTypeahead({ close, editor, resolution, className = '', containe
|
|
|
136
242
|
if (results !== null && selectedIndex !== null) {
|
|
137
243
|
if (selectedIndex < SUGGESTION_LIST_LENGTH_LIMIT - 1 && selectedIndex !== results.length - 1) {
|
|
138
244
|
updateSelectedIndex(selectedIndex + 1);
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
event.stopImmediatePropagation();
|
|
139
247
|
}
|
|
140
|
-
event.preventDefault();
|
|
141
|
-
event.stopImmediatePropagation();
|
|
142
248
|
}
|
|
143
249
|
return true;
|
|
144
250
|
}, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_UP_COMMAND, (payload) => {
|
|
@@ -146,9 +252,9 @@ function MentionsTypeahead({ close, editor, resolution, className = '', containe
|
|
|
146
252
|
if (results !== null && selectedIndex !== null) {
|
|
147
253
|
if (selectedIndex !== 0) {
|
|
148
254
|
updateSelectedIndex(selectedIndex - 1);
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
event.stopImmediatePropagation();
|
|
149
257
|
}
|
|
150
|
-
event.preventDefault();
|
|
151
|
-
event.stopImmediatePropagation();
|
|
152
258
|
}
|
|
153
259
|
return true;
|
|
154
260
|
}, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ESCAPE_COMMAND, (payload) => {
|
|
@@ -257,6 +363,9 @@ function tryToPositionRange(match, range) {
|
|
|
257
363
|
const anchorNode = domSelection.anchorNode;
|
|
258
364
|
const startOffset = match.leadOffset;
|
|
259
365
|
const endOffset = domSelection.anchorOffset;
|
|
366
|
+
if (anchorNode == null || endOffset == null) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
260
369
|
try {
|
|
261
370
|
range.setStart(anchorNode, startOffset);
|
|
262
371
|
range.setEnd(anchorNode, endOffset);
|
|
@@ -363,8 +472,9 @@ const Root = styled(MentionsTypeahead, {
|
|
|
363
472
|
slot: 'Root',
|
|
364
473
|
overridesResolver: (props, styles) => styles.root
|
|
365
474
|
})(({ theme }) => ({}));
|
|
366
|
-
function useMentions(editor,
|
|
475
|
+
function useMentions(editor, anchorClassName = null) {
|
|
367
476
|
const [resolution, setResolution] = useState(null);
|
|
477
|
+
const anchorElementRef = useMenuAnchorRef(resolution, setResolution, anchorClassName);
|
|
368
478
|
useEffect(() => {
|
|
369
479
|
if (!editor.hasNodes([MentionNode])) {
|
|
370
480
|
throw new Error('MentionsPlugin: MentionNode not registered on editor');
|
|
@@ -407,17 +517,15 @@ function useMentions(editor, containerSelector = null) {
|
|
|
407
517
|
}, [editor]);
|
|
408
518
|
const closeTypeahead = useCallback(() => {
|
|
409
519
|
setResolution(null);
|
|
410
|
-
}, []);
|
|
520
|
+
}, [resolution]);
|
|
411
521
|
if (resolution === null || editor === null) {
|
|
412
522
|
return null;
|
|
413
523
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
: editor.getRootElement().parentElement;
|
|
418
|
-
return createPortal(React.createElement(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root, containerEl: portalContainer }), portalContainer);
|
|
524
|
+
return (React.createElement(ClickAwayListener, { onClickAway: closeTypeahead },
|
|
525
|
+
React.createElement(Portal, { container: anchorElementRef.current },
|
|
526
|
+
React.createElement(Root, { close: closeTypeahead, resolution: resolution, editor: editor, className: classes.root }))));
|
|
419
527
|
}
|
|
420
|
-
export default function MentionsPlugin(
|
|
528
|
+
export default function MentionsPlugin() {
|
|
421
529
|
const [editor] = useLexicalComposerContext();
|
|
422
|
-
return useMentions(editor
|
|
530
|
+
return useMentions(editor);
|
|
423
531
|
}
|
|
@@ -455,7 +455,7 @@ export default function FeedObject(inProps) {
|
|
|
455
455
|
React.createElement(CardActions, { className: classes.actionsSection },
|
|
456
456
|
React.createElement(Actions, Object.assign({ feedObjectId: feedObjectId, feedObjectType: feedObjectType, feedObject: obj, hideCommentAction: template === SCFeedObjectTemplateType.DETAIL, handleExpandActivities: template === SCFeedObjectTemplateType.PREVIEW ? handleExpandActivities : null, VoteActionProps: { onVoteAction: handleVoteSuccess } }, ActionsProps)),
|
|
457
457
|
(template === SCFeedObjectTemplateType.DETAIL || expandedActivities) && (React.createElement(Box, { className: classes.replyContent },
|
|
458
|
-
React.createElement(CommentObjectReplyComponent, Object.assign({ onReply: handleReply, editable: !isReplying || Boolean(obj), key: Number(isReplying) }, CommentObjectReplyComponentProps))))),
|
|
458
|
+
React.createElement(CommentObjectReplyComponent, Object.assign({ id: `reply-feedObject-${obj.id}`, onReply: handleReply, editable: !isReplying || Boolean(obj), key: Number(isReplying) }, CommentObjectReplyComponentProps))))),
|
|
459
459
|
template === SCFeedObjectTemplateType.PREVIEW && (obj.comment_count > 0 || (feedObjectActivities && feedObjectActivities.length > 0)) && (React.createElement(Collapse, { in: expandedActivities, timeout: "auto", classes: { root: classes.activitiesSection } },
|
|
460
460
|
React.createElement(CardContent, { className: classes.activitiesContent },
|
|
461
461
|
React.createElement(Activities, Object.assign({ feedObject: obj, key: selectedActivities, feedObjectActivities: feedObjectActivities, activitiesType: selectedActivities, onSetSelectedActivities: handleSelectedActivities, comments: comments, CommentsObjectProps: {
|