feat: context menu for deleting tags
This commit is contained in:
@@ -1,26 +1,31 @@
|
|||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { FunctionalComponent, RefObject } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
import { useCallback, useRef, useState } from 'preact/hooks';
|
import { useCallback, useRef, useState } from 'preact/hooks';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { SNTag } from '@standardnotes/snjs/dist/@types';
|
import { SNTag } from '@standardnotes/snjs/dist/@types';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { useCloseOnBlur, useCloseOnClickOutside } from './utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
tag: SNTag;
|
tag: SNTag;
|
||||||
overflowButtonRef: RefObject<HTMLButtonElement>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoteTag: FunctionalComponent<Props> = ({ appState, tag, overflowButtonRef }) => {
|
export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
|
||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
tagsContainerMaxWidth,
|
tagsContainerMaxWidth,
|
||||||
} = appState.activeNote;
|
} = appState.activeNote;
|
||||||
|
|
||||||
const [overflowed, setOverflowed] = useState(false);
|
const [overflowed, setOverflowed] = useState(false);
|
||||||
const [showDeleteButton, setShowDeleteButton] = useState(false);
|
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||||
|
const [contextMenuPosition, setContextMenuPosition] = useState({ top: 0, left: 0 });
|
||||||
|
|
||||||
const deleteTagRef = useRef<HTMLButtonElement>();
|
const contextMenuRef = useRef<HTMLDivElement>();
|
||||||
|
const tagRef = useRef<HTMLButtonElement>();
|
||||||
|
|
||||||
|
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, setContextMenuOpen);
|
||||||
|
useCloseOnClickOutside(contextMenuRef, setContextMenuOpen);
|
||||||
|
|
||||||
const deleteTag = async () => {
|
const deleteTag = async () => {
|
||||||
await appState.activeNote.removeTagFromActiveNote(tag);
|
await appState.activeNote.removeTagFromActiveNote(tag);
|
||||||
@@ -36,21 +41,6 @@ export const NoteTag: FunctionalComponent<Props> = ({ appState, tag, overflowBut
|
|||||||
appState.setSelectedTag(tag);
|
appState.setSelectedTag(tag);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFocus = () => {
|
|
||||||
appState.activeNote.setTagFocused(true);
|
|
||||||
setShowDeleteButton(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBlur = (event: FocusEvent) => {
|
|
||||||
const relatedTarget = event.relatedTarget as Node;
|
|
||||||
if (relatedTarget === overflowButtonRef.current) {
|
|
||||||
(event.target as HTMLButtonElement).focus();
|
|
||||||
} else if (relatedTarget !== deleteTagRef.current) {
|
|
||||||
appState.activeNote.setTagFocused(false);
|
|
||||||
setShowDeleteButton(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reloadOverflowed = useCallback(() => {
|
const reloadOverflowed = useCallback(() => {
|
||||||
const overflowed = appState.activeNote.isTagOverflowed(tag);
|
const overflowed = appState.activeNote.isTagOverflowed(tag);
|
||||||
setOverflowed(overflowed);
|
setOverflowed(overflowed);
|
||||||
@@ -60,44 +50,67 @@ export const NoteTag: FunctionalComponent<Props> = ({ appState, tag, overflowBut
|
|||||||
reloadOverflowed();
|
reloadOverflowed();
|
||||||
}, [reloadOverflowed, tags, tagsContainerMaxWidth]);
|
}, [reloadOverflowed, tags, tagsContainerMaxWidth]);
|
||||||
|
|
||||||
|
const contextMenuListener = (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setContextMenuPosition({
|
||||||
|
top: event.clientY,
|
||||||
|
left: event.clientX,
|
||||||
|
});
|
||||||
|
setContextMenuOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tagRef.current.addEventListener('contextmenu', contextMenuListener);
|
||||||
|
return () => {
|
||||||
|
tagRef.current.removeEventListener('contextmenu', contextMenuListener);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<>
|
||||||
ref={(element) => {
|
<button
|
||||||
if (element) {
|
ref={(element) => {
|
||||||
appState.activeNote.setTagElement(tag, element);
|
if (element) {
|
||||||
}
|
appState.activeNote.setTagElement(tag, element);
|
||||||
}}
|
tagRef.current = element;
|
||||||
className="sn-tag pl-1 pr-2 mr-2"
|
}
|
||||||
style={{ maxWidth: tagsContainerMaxWidth }}
|
}}
|
||||||
onClick={onTagClick}
|
className="sn-tag pl-1 pr-2 mr-2"
|
||||||
onKeyUp={(event) => {
|
style={{ maxWidth: tagsContainerMaxWidth }}
|
||||||
if (event.key === 'Backspace') {
|
onClick={onTagClick}
|
||||||
deleteTag();
|
onKeyUp={(event) => {
|
||||||
}
|
if (event.key === 'Backspace') {
|
||||||
}}
|
deleteTag();
|
||||||
tabIndex={overflowed ? -1 : 0}
|
}
|
||||||
onFocus={onFocus}
|
}}
|
||||||
onBlur={onBlur}
|
tabIndex={overflowed ? -1 : 0}
|
||||||
>
|
onBlur={closeOnBlur}
|
||||||
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
|
>
|
||||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
|
||||||
{tag.title}
|
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||||
</span>
|
{tag.title}
|
||||||
{showDeleteButton && (
|
</span>
|
||||||
<button
|
</button>
|
||||||
ref={deleteTagRef}
|
{contextMenuOpen && (
|
||||||
type="button"
|
<div
|
||||||
className="ml-2 -mr-1 border-0 p-0 bg-transparent cursor-pointer flex"
|
ref={contextMenuRef}
|
||||||
onFocus={onFocus}
|
className="sn-dropdown sn-dropdown--small max-h-120 max-w-xs flex flex-col py-2 overflow-y-scroll fixed"
|
||||||
onBlur={onBlur}
|
style={{
|
||||||
onClick={deleteTag}
|
...contextMenuPosition
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<button
|
||||||
type="close"
|
type="button"
|
||||||
className="sn-icon--small color-neutral hover:color-info"
|
className="sn-dropdown-item"
|
||||||
/>
|
onClick={deleteTag}
|
||||||
</button>
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Icon type="close" className="color-danger mr-2" />
|
||||||
|
<span className="color-danger">Remove tag</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
|||||||
key={tag.uuid}
|
key={tag.uuid}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
tag={tag}
|
tag={tag}
|
||||||
overflowButtonRef={overflowButtonRef}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<AutocompleteTagInput application={application} appState={appState} />
|
<AutocompleteTagInput application={application} appState={appState} />
|
||||||
|
|||||||
@@ -190,6 +190,10 @@
|
|||||||
min-width: 1.25rem;
|
min-width: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-w-40 {
|
||||||
|
min-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-1px {
|
.h-1px {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
@@ -366,6 +370,10 @@
|
|||||||
@extend .duration-150;
|
@extend .duration-150;
|
||||||
@extend .slide-down-animation;
|
@extend .slide-down-animation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.sn-dropdown--small {
|
||||||
|
@extend .min-w-40;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Lesser specificity will give priority to reach's styles */
|
/** Lesser specificity will give priority to reach's styles */
|
||||||
|
|||||||
Reference in New Issue
Block a user