mirror of
https://github.com/plankanban/planka.git
synced 2025-07-19 13:19:44 +02:00
fix: Save description when clicking outside
This commit is contained in:
parent
f75b0237d3
commit
d9e8c24c3f
5 changed files with 106 additions and 12 deletions
|
@ -1,8 +1,11 @@
|
||||||
import React, { useCallback, useImperativeHandle, useMemo, useState } from 'react';
|
import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form } from 'semantic-ui-react';
|
import { Button, Form } from 'semantic-ui-react';
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
|
import { useClickAwayListener } from '../../lib/hooks';
|
||||||
|
|
||||||
|
import { useNestedRef } from '../../hooks';
|
||||||
|
|
||||||
import styles from './DescriptionEdit.module.scss';
|
import styles from './DescriptionEdit.module.scss';
|
||||||
|
|
||||||
|
@ -11,6 +14,10 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
||||||
const [isOpened, setIsOpened] = useState(false);
|
const [isOpened, setIsOpened] = useState(false);
|
||||||
const [value, setValue] = useState(null);
|
const [value, setValue] = useState(null);
|
||||||
|
|
||||||
|
const editorWrapperRef = useRef(null);
|
||||||
|
const codemirrorRef = useRef(null);
|
||||||
|
const [buttonRef, handleButtonRef] = useNestedRef();
|
||||||
|
|
||||||
const open = useCallback(() => {
|
const open = useCallback(() => {
|
||||||
setIsOpened(true);
|
setIsOpened(true);
|
||||||
setValue(defaultValue || '');
|
setValue(defaultValue || '');
|
||||||
|
@ -55,6 +62,28 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
||||||
close();
|
close();
|
||||||
}, [close]);
|
}, [close]);
|
||||||
|
|
||||||
|
const handleAwayClick = useCallback(() => {
|
||||||
|
if (!isOpened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
close();
|
||||||
|
}, [isOpened, close]);
|
||||||
|
|
||||||
|
const handleClickAwayCancel = useCallback(() => {
|
||||||
|
codemirrorRef.current.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clickAwayProps = useClickAwayListener(
|
||||||
|
[editorWrapperRef, buttonRef],
|
||||||
|
handleAwayClick,
|
||||||
|
handleClickAwayCancel,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGetCodemirrorInstance = useCallback((codemirror) => {
|
||||||
|
codemirrorRef.current = codemirror;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const mdEditorOptions = useMemo(
|
const mdEditorOptions = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
autoDownloadFontAwesome: false,
|
autoDownloadFontAwesome: false,
|
||||||
|
@ -92,16 +121,20 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
|
<div {...clickAwayProps} ref={editorWrapperRef}>
|
||||||
<SimpleMDE
|
<SimpleMDE
|
||||||
value={value}
|
value={value}
|
||||||
options={mdEditorOptions}
|
options={mdEditorOptions}
|
||||||
placeholder={t('common.enterDescription')}
|
placeholder={t('common.enterDescription')}
|
||||||
className={styles.field}
|
className={styles.field}
|
||||||
|
getCodemirrorInstance={handleGetCodemirrorInstance}
|
||||||
onKeyDown={handleFieldKeyDown}
|
onKeyDown={handleFieldKeyDown}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
<Button positive content={t('action.save')} />
|
<Button positive ref={handleButtonRef} content={t('action.save')} />
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
import useNestedRef from './use-nested-ref';
|
||||||
import useField from './use-field';
|
import useField from './use-field';
|
||||||
import useForm from './use-form';
|
import useForm from './use-form';
|
||||||
import useSteps from './use-steps';
|
import useSteps from './use-steps';
|
||||||
import useModal from './use-modal';
|
import useModal from './use-modal';
|
||||||
import useClosableForm from './use-closable-form';
|
import useClosableForm from './use-closable-form';
|
||||||
|
|
||||||
export { useField, useForm, useSteps, useModal, useClosableForm };
|
export { useNestedRef, useField, useForm, useSteps, useModal, useClosableForm };
|
||||||
|
|
14
client/src/hooks/use-nested-ref.js
Normal file
14
client/src/hooks/use-nested-ref.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
export default (nestedRefName = 'ref') => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
const handleRef = useCallback(
|
||||||
|
(element) => {
|
||||||
|
ref.current = element?.[nestedRefName].current;
|
||||||
|
},
|
||||||
|
[nestedRefName],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [ref, handleRef];
|
||||||
|
};
|
|
@ -2,5 +2,6 @@ import usePrevious from './use-previous';
|
||||||
import useToggle from './use-toggle';
|
import useToggle from './use-toggle';
|
||||||
import useForceUpdate from './use-force-update';
|
import useForceUpdate from './use-force-update';
|
||||||
import useDidUpdate from './use-did-update';
|
import useDidUpdate from './use-did-update';
|
||||||
|
import useClickAwayListener from './use-click-away-listener';
|
||||||
|
|
||||||
export { usePrevious, useToggle, useForceUpdate, useDidUpdate };
|
export { usePrevious, useToggle, useForceUpdate, useDidUpdate, useClickAwayListener };
|
||||||
|
|
45
client/src/lib/hooks/use-click-away-listener.js
Normal file
45
client/src/lib/hooks/use-click-away-listener.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
export default (elementRefs, onAwayClick, onCancel) => {
|
||||||
|
const pressedElement = useRef(null);
|
||||||
|
|
||||||
|
const handlePress = useCallback((event) => {
|
||||||
|
pressedElement.current = event.target;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEvent = (event) => {
|
||||||
|
const element = elementRefs.find(({ current }) => current?.contains(event.target))?.current;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
if (!pressedElement.current || pressedElement.current !== element) {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
} else if (pressedElement.current) {
|
||||||
|
onCancel();
|
||||||
|
} else {
|
||||||
|
onAwayClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
pressedElement.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', handleEvent, true);
|
||||||
|
document.addEventListener('touchend', handleEvent, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mouseup', handleEvent, true);
|
||||||
|
document.removeEventListener('touchend', handleEvent, true);
|
||||||
|
};
|
||||||
|
}, [onAwayClick, onCancel]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const props = useMemo(
|
||||||
|
() => ({
|
||||||
|
onMouseDown: handlePress,
|
||||||
|
onTouchStart: handlePress,
|
||||||
|
}),
|
||||||
|
[handlePress],
|
||||||
|
);
|
||||||
|
|
||||||
|
return props;
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue