mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-20 22:42:53 -05:00
Updated form to only close when another form opens and scroll better
fixes https://github.com/TryGhost/Team/issues/1753 refs https://github.com/TryGhost/Team/issues/1758 - Scroll only if form is not in viewport + scroll to center - Fixes: You can’t tap reply if another reply box is open (or at least you have to tap a bunch of times)
This commit is contained in:
parent
4a0ad8c6bc
commit
ceaecf43e6
8 changed files with 131 additions and 26 deletions
|
@ -27,6 +27,9 @@ const Comment = ({updateIsEditing = null, isEditing, ...props}) => {
|
|||
let comment = props.comment;
|
||||
|
||||
useEffect(() => {
|
||||
// This doesn't work, and should receive an update.
|
||||
// When one Comment shows reply, while a different Form hides the reply form at the same time, the global
|
||||
// 'isEditing' is unreliable. We should use a counter of total open forms instead of a boolean.
|
||||
updateIsEditing?.(isInReplyMode || isInEditMode);
|
||||
}, [updateIsEditing, isInReplyMode, isInEditMode]);
|
||||
const toggleEditMode = () => {
|
||||
|
@ -99,7 +102,7 @@ const Comment = ({updateIsEditing = null, isEditing, ...props}) => {
|
|||
<div className="flex flex-row items-center gap-4 pb-[8px] pr-4 h-12">
|
||||
<p className="font-sans text-[16px] leading-normal text-neutral-300 italic mt-[4px]">{notPublishedMessage}</p>
|
||||
<div className="mt-[4px]">
|
||||
<More comment={comment} toggleEdit={toggleEditMode} disableEditing={isEditing} />
|
||||
<More comment={comment} toggleEdit={toggleEditMode} />
|
||||
</div>
|
||||
</div> :
|
||||
<div>
|
||||
|
@ -122,8 +125,8 @@ const Comment = ({updateIsEditing = null, isEditing, ...props}) => {
|
|||
{!isNotPublished && (
|
||||
<div className="flex gap-5 items-center">
|
||||
{!isNotPublished && <Like comment={comment} />}
|
||||
{!isNotPublished && (canReply && (isNotPublished || !props.parent) && <Reply disabled={!!isEditing} comment={comment} toggleReply={toggleReplyMode} isReplying={isInReplyMode} />)}
|
||||
{!isNotPublished && <More comment={comment} toggleEdit={toggleEditMode} disableEditing={isEditing} />}
|
||||
{!isNotPublished && (canReply && (isNotPublished || !props.parent) && <Reply comment={comment} toggleReply={toggleReplyMode} isReplying={isInReplyMode} />)}
|
||||
{!isNotPublished && <More comment={comment} toggleEdit={toggleEditMode} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ const CommentsBoxTitle = ({title, showCount, count}) => {
|
|||
};
|
||||
|
||||
const CommentsBoxContent = (props) => {
|
||||
// @todo: This doesn't work and should get replaced with a counter of total open forms. If total open forms > 0, don't show the main form.
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const {pagination, member, comments, commentCount, commentsEnabled, title, showCount} = useContext(AppContext);
|
||||
|
|
|
@ -8,6 +8,9 @@ import {isMobile} from '../utils/helpers';
|
|||
// import {formatRelativeTime} from '../utils/helpers';
|
||||
import {ReactComponent as SpinnerIcon} from '../images/icons/spinner.svg';
|
||||
import {ReactComponent as EditIcon} from '../images/icons/edit.svg';
|
||||
import {GlobalEventBus} from '../utils/event-bus';
|
||||
|
||||
let formId = 0;
|
||||
|
||||
const Form = (props) => {
|
||||
const {member, postId, dispatchAction, avatarSaturation} = useContext(AppContext);
|
||||
|
@ -55,7 +58,7 @@ const Form = (props) => {
|
|||
});
|
||||
|
||||
const getScrollToPosition = () => {
|
||||
let yOffset = -100;
|
||||
let yOffset = 0;
|
||||
const element = formEl.current;
|
||||
|
||||
// Because we are working in an iframe, we need to resolve the position inside this iframe to the position in the top window
|
||||
|
@ -83,7 +86,45 @@ const Form = (props) => {
|
|||
return y;
|
||||
};
|
||||
|
||||
// Generate an unique ID so we can exclude events that we send ourselve
|
||||
const [uniqueId] = useState(() => {
|
||||
formId += 1;
|
||||
return 'form-' + formId;
|
||||
});
|
||||
|
||||
const onOtherFormFocus = useCallback(() => {
|
||||
// A different form got focus. Should we close this form now?
|
||||
if ((props.isReply && editor?.isEmpty) || (props.isEdit && editor?.getHTML() === props.comment.html)) {
|
||||
if (props.close) {
|
||||
props.close();
|
||||
}
|
||||
}
|
||||
}, [editor, props]);
|
||||
|
||||
useEffect(() => {
|
||||
// Send event before attaching the listeer
|
||||
GlobalEventBus.addListener(uniqueId, 'form-focus', onOtherFormFocus);
|
||||
|
||||
return () => {
|
||||
GlobalEventBus.removeListener(uniqueId);
|
||||
};
|
||||
}, [onOtherFormFocus, uniqueId]);
|
||||
|
||||
useEffect(() => {
|
||||
// When opening a reply or edit form, try to close other forms that are open and can get closed
|
||||
if (props.isReply || props.isEdit) {
|
||||
// Send a form-focus event, but exclude ourself (uniqueId)
|
||||
GlobalEventBus.sendEvent('form-focus', {}, uniqueId);
|
||||
}
|
||||
}, [props.isReply, props.isEdit, uniqueId]);
|
||||
|
||||
const onFormFocus = useCallback(() => {
|
||||
// When focusing the main form, try to close other forms that are open and can get closed
|
||||
if (!props.isReply && !props.isEdit) {
|
||||
// Send a form-focus event, but exclude ourself (uniqueId)
|
||||
GlobalEventBus.sendEvent('form-focus', {}, uniqueId);
|
||||
}
|
||||
// Send an event around and try to close other forms that are open and can get closed
|
||||
if (!memberName && !props.isEdit) {
|
||||
setPreventClosing(true);
|
||||
editor.commands.blur();
|
||||
|
@ -103,7 +144,7 @@ const Form = (props) => {
|
|||
} else {
|
||||
setFormOpen(true);
|
||||
}
|
||||
}, [editor, dispatchAction, memberName, props]);
|
||||
}, [editor, dispatchAction, memberName, props, uniqueId]);
|
||||
|
||||
// Set the cursor position at the end of the form, instead of the beginning (= when using autofocus)
|
||||
useEffect(() => {
|
||||
|
@ -116,12 +157,26 @@ const Form = (props) => {
|
|||
// Scroll to view if it's a reply
|
||||
if (props.isReply) {
|
||||
timer = setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: getScrollToPosition(),
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}, 100);
|
||||
// Is the form already in view?
|
||||
const formHeight = 100;
|
||||
const yMin = getScrollToPosition();
|
||||
const yMax = yMin + formHeight;
|
||||
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewPortYMin = window.scrollY;
|
||||
const viewPortYMax = viewPortYMin + viewportHeight;
|
||||
|
||||
if (yMin < viewPortYMin || yMax > viewPortYMax) {
|
||||
// Center the form in the viewport
|
||||
const yCenter = (yMin + yMax) / 2;
|
||||
|
||||
window.scrollTo({
|
||||
top: yCenter - viewportHeight / 2,
|
||||
left: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// Focus editor + jump to end
|
||||
|
@ -150,7 +205,7 @@ const Form = (props) => {
|
|||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [editor, props]);
|
||||
}, [editor, props.isReply, props.isEdit]);
|
||||
|
||||
const submitForm = useCallback(async () => {
|
||||
if (editor.isEmpty) {
|
||||
|
@ -226,11 +281,6 @@ const Form = (props) => {
|
|||
editor.on('blur', () => {
|
||||
if (editor?.isEmpty) {
|
||||
setFormOpen(false);
|
||||
if (props.isReply && props.close && !preventClosing) {
|
||||
// TODO: we cannot toggle the form when this happens, because when the member doesn't have a name we'll always loose focus to input the name...
|
||||
// Need to find a different way for this behaviour
|
||||
props.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ const More = (props) => {
|
|||
return (
|
||||
<div className="relative">
|
||||
{show ? <button onClick={toggleContextMenu} className="outline-0"><MoreIcon className='transition duration-50 ease-linear gh-comments-icon gh-comments-icon-more outline-0 fill-neutral-400 dark:fill-rgba(255,255,255,0.5) hover:fill-neutral-600' /></button> : null}
|
||||
{isContextMenuOpen ? <CommentContextMenu comment={comment} close={closeContextMenu} toggleEdit={props.toggleEdit} disableEditing={props.disableEditing} /> : null}
|
||||
{isContextMenuOpen ? <CommentContextMenu comment={comment} close={closeContextMenu} toggleEdit={props.toggleEdit} /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,13 +5,8 @@ import {ReactComponent as ReplyIcon} from '../images/icons/reply.svg';
|
|||
function Reply(props) {
|
||||
const {member} = useContext(AppContext);
|
||||
|
||||
const preventDefault = (event) => {
|
||||
// We need to prevent blurring the input field when clicking the reply button (that could cause blur + focus again because mousedown is causing the input blur, then onclick focusses again)
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return member ?
|
||||
(<button disabled={!!props.disabled} className={`group transition-all duration-50 ease-linear flex font-sans items-center text-sm outline-0 ${props.isReplying ? 'text-neutral-900 dark:text-[rgba(255,255,255,0.9)]' : 'text-neutral-400 dark:text-[rgba(255,255,255,0.5)] hover:text-neutral-600'}`} onMouseDown={preventDefault} onClick={props.toggleReply}>
|
||||
(<button disabled={!!props.disabled} className={`group transition-all duration-50 ease-linear flex font-sans items-center text-sm outline-0 ${props.isReplying ? 'text-neutral-900 dark:text-[rgba(255,255,255,0.9)]' : 'text-neutral-400 dark:text-[rgba(255,255,255,0.5)] hover:text-neutral-600'}`} onClick={props.toggleReply}>
|
||||
<ReplyIcon className={`mr-[6px] ${props.isReplying ? 'fill-neutral-900 stroke-neutral-900 dark:fill-white dark:stroke-white' : 'stroke-neutral-400 dark:stroke-[rgba(255,255,255,0.5)] group-hover:stroke-neutral-600'} transition duration-50 ease-linear`} />Reply
|
||||
</button>) : null;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ const AuthorContextMenu = (props) => {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<button className="w-full mb-3 text-left text-[14px]" onClick={props.toggleEdit} disabled={props.disableEditing}>
|
||||
<button className="w-full mb-3 text-left text-[14px]" onClick={props.toggleEdit}>
|
||||
Edit
|
||||
</button>
|
||||
<button className="w-full text-left text-[14px] text-red-600" onClick={deleteComment}>
|
||||
|
|
|
@ -55,7 +55,7 @@ const CommentContextMenu = (props) => {
|
|||
let contextMenu = null;
|
||||
if (comment.status === 'published') {
|
||||
if (isAuthor) {
|
||||
contextMenu = <AuthorContextMenu comment={comment} close={props.close} toggleEdit={props.toggleEdit} disableEditing={props.disableEditing} />;
|
||||
contextMenu = <AuthorContextMenu comment={comment} close={props.close} toggleEdit={props.toggleEdit} />;
|
||||
} else {
|
||||
if (isAdmin) {
|
||||
contextMenu = <AdminContextMenu comment={comment} close={props.close}/>;
|
||||
|
|
56
apps/comments-ui/src/utils/event-bus.js
Normal file
56
apps/comments-ui/src/utils/event-bus.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
class EventBus {
|
||||
listeners = new Map()
|
||||
|
||||
addListener(owner, type, listener) {
|
||||
const existing = this.listeners.get(owner);
|
||||
if (existing) {
|
||||
existing.push({type, listener});
|
||||
} else {
|
||||
this.listeners.set(owner, [{type, listener}]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} owner
|
||||
* @param {*} [type] Leave out if you want to remove all types
|
||||
*/
|
||||
removeListener(owner, type) {
|
||||
if (type) {
|
||||
const existing = this.listeners.get(owner);
|
||||
if (existing) {
|
||||
this.listeners.set(
|
||||
owner,
|
||||
existing.filter(t => t.type !== type)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.listeners.delete(owner);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} type
|
||||
* @param {*} value
|
||||
* @param {*} [excludeOwner] Don't send the event to this owner (e.g. the sender of the message doesn't want to receive it when it is also listening for it)
|
||||
* @returns
|
||||
*/
|
||||
sendEvent(type, value, excludeOwner) {
|
||||
const values = [];
|
||||
for (const owner of this.listeners.values()) {
|
||||
if (excludeOwner !== undefined && owner === excludeOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const listener of owner) {
|
||||
if (listener.type === type) {
|
||||
values.push(listener.listener(value, type));
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
export const GlobalEventBus = new EventBus();
|
Loading…
Add table
Reference in a new issue