mirror of
https://github.com/plankanban/planka.git
synced 2025-08-10 16:05:35 +02:00
feat: improve mention
This commit is contained in:
parent
1c9a42781c
commit
5a270f51e6
3 changed files with 86 additions and 26 deletions
|
@ -8,7 +8,8 @@ import { useDidUpdate, useToggle } from '../../../lib/hooks';
|
|||
import { useClosableForm, useForm } from '../../../hooks';
|
||||
|
||||
import styles from './CommentAdd.module.scss';
|
||||
import Tag, { TagRegex } from '../../Tag/Tag';
|
||||
import Tag from '../../Tag/Tag';
|
||||
import useMention from '../../../hooks/use-mention';
|
||||
|
||||
const DEFAULT_DATA = {
|
||||
text: '',
|
||||
|
@ -21,12 +22,12 @@ const CommentAdd = React.memo(({ onCreate, boardMemberships }) => {
|
|||
const [selectTextFieldState, selectTextField] = useToggle();
|
||||
const [cursor, setCursor] = useState(0);
|
||||
const textField = useRef(null);
|
||||
const [showTag, setShowTag] = useState(false);
|
||||
const [mention, setMention] = useState('');
|
||||
const { mention, onChange, onSelectMention, isMentioning } = useMention();
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (isMentioning) return;
|
||||
setIsOpened(false);
|
||||
}, []);
|
||||
}, [isMentioning]);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
const cleanData = {
|
||||
|
@ -52,13 +53,14 @@ const CommentAdd = React.memo(({ onCreate, boardMemberships }) => {
|
|||
const calculatePosition = (position) => Math.min(position, 22) * 8;
|
||||
|
||||
const handleFieldKeyDown = useCallback(
|
||||
(event) => {
|
||||
if (event.ctrlKey && event.key === 'Enter') {
|
||||
(ev) => {
|
||||
if (ev.ctrlKey && ev.key === 'Enter') {
|
||||
submit();
|
||||
}
|
||||
setCursor(calculatePosition(event.target.selectionStart));
|
||||
setCursor(calculatePosition(ev.target.selectionStart));
|
||||
onChange(ev);
|
||||
},
|
||||
[submit],
|
||||
[submit, onChange],
|
||||
);
|
||||
|
||||
const [handleFieldBlur, handleControlMouseOver, handleControlMouseOut] = useClosableForm(close);
|
||||
|
@ -67,21 +69,15 @@ const CommentAdd = React.memo(({ onCreate, boardMemberships }) => {
|
|||
submit();
|
||||
}, [submit]);
|
||||
|
||||
const handleChange = (e, updatedData) => {
|
||||
handleFieldChange(e, updatedData);
|
||||
const text = e.target.value;
|
||||
const handleChange = (ev, updatedData) => {
|
||||
handleFieldChange(ev, updatedData);
|
||||
onChange(ev);
|
||||
};
|
||||
|
||||
TagRegex.lastIndex = 0;
|
||||
if (TagRegex.test(text)) {
|
||||
// Extract the mention after @ symbol
|
||||
const mentionSymbols = text.split('@');
|
||||
const mentionName = mentionSymbols[mentionSymbols.length - 1].split(' ')[0].toLowerCase();
|
||||
setMention(mentionName);
|
||||
setShowTag(true);
|
||||
} else {
|
||||
setMention('');
|
||||
setShowTag(false);
|
||||
}
|
||||
const handleMention = (user) => {
|
||||
setData({
|
||||
text: onSelectMention(data.text, user),
|
||||
});
|
||||
};
|
||||
|
||||
useDidUpdate(() => {
|
||||
|
@ -104,7 +100,7 @@ const CommentAdd = React.memo(({ onCreate, boardMemberships }) => {
|
|||
onChange={handleChange}
|
||||
onBlur={handleFieldBlur}
|
||||
/>
|
||||
{showTag && (
|
||||
{isMentioning && (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: `${cursor}px`,
|
||||
|
@ -114,7 +110,11 @@ const CommentAdd = React.memo(({ onCreate, boardMemberships }) => {
|
|||
}}
|
||||
>
|
||||
{' '}
|
||||
<Tag search={mention} boardMemberships={boardMemberships} />
|
||||
<Tag
|
||||
search={mention}
|
||||
boardMemberships={boardMemberships}
|
||||
handleUserSelect={handleMention}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isOpened && (
|
||||
|
|
|
@ -7,7 +7,7 @@ import UserItem from '../Memberships/AddStep/UserItem';
|
|||
|
||||
export const TagRegex = /(^|\s)@/gi;
|
||||
|
||||
const Tag = React.memo(({ search, boardMemberships }) => {
|
||||
const Tag = React.memo(({ search, boardMemberships, handleUserSelect }) => {
|
||||
const cleanSearch = useMemo(() => search.trim().toLowerCase(), [search]);
|
||||
|
||||
const filteredUsers = useMemo(
|
||||
|
@ -26,10 +26,11 @@ const Tag = React.memo(({ search, boardMemberships }) => {
|
|||
{filteredUsers.map((member) => {
|
||||
return (
|
||||
<UserItem
|
||||
isActive={false}
|
||||
key={member.user.id}
|
||||
name={member.user.name}
|
||||
avatarUrl={member.user.avatarUrl}
|
||||
onSelect={() => handleUserSelect(member.user.id)}
|
||||
onSelect={() => handleUserSelect(member.user)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -44,6 +45,7 @@ Tag.defaultProps = {
|
|||
Tag.propTypes = {
|
||||
search: PropTypes.string,
|
||||
boardMemberships: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
handleUserSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
|
|
58
client/src/hooks/use-mention.js
Normal file
58
client/src/hooks/use-mention.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { useState } from 'react';
|
||||
import { TagRegex } from '../components/Tag/Tag';
|
||||
|
||||
export default () => {
|
||||
const [mention, setMention] = useState('');
|
||||
const [isMentioning, setIsMentioning] = useState(false);
|
||||
const [currentPos, setCurrentPos] = useState(null);
|
||||
|
||||
const getWord = (s, pos) => {
|
||||
const n = s.substring(pos).match(/^[^ ]+/);
|
||||
const p = s.substring(0, pos).match(/[^ ]+$/);
|
||||
// if you really only want the word if you click at start or between
|
||||
// but not at end instead use if (!n) return
|
||||
if (!p && !n) return '';
|
||||
return (p || '') + (n || '');
|
||||
};
|
||||
|
||||
const onChange = (ev) => {
|
||||
TagRegex.lastIndex = 0;
|
||||
|
||||
const position = ev.target.selectionStart;
|
||||
const word = getWord(ev.target.value, position);
|
||||
setCurrentPos(position);
|
||||
|
||||
if (TagRegex.test(word)) {
|
||||
// Extract the mention after @ symbol
|
||||
const mentionSymbols = word.split('@');
|
||||
const mentionName = mentionSymbols[mentionSymbols.length - 1].split(' ')[0].toLowerCase();
|
||||
setMention(mentionName);
|
||||
setIsMentioning(true);
|
||||
} else {
|
||||
setMention('');
|
||||
setIsMentioning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectMention = (text, user) => {
|
||||
if (!isMentioning) return text;
|
||||
|
||||
const { username } = user;
|
||||
const wordLength = getWord(text, currentPos).length;
|
||||
|
||||
const n = text.substring(currentPos).match(/^[^ ]+/);
|
||||
const p = text.substring(0, currentPos).match(/[^ ]+$/);
|
||||
|
||||
const startIndex = p ? p.index : n.index;
|
||||
|
||||
setIsMentioning(false);
|
||||
return `${text.substring(0, startIndex)}@mention[${username}]${text.substring(startIndex + wordLength)}`;
|
||||
};
|
||||
|
||||
return {
|
||||
mention,
|
||||
isMentioning,
|
||||
onChange,
|
||||
onSelectMention,
|
||||
};
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue