feat: collapse tags on click outside
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { toDirective } from './utils';
|
||||
import { toDirective, useCloseOnClickOutside } from './utils';
|
||||
import { Icon } from './Icon';
|
||||
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
@@ -22,7 +22,7 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
||||
tags,
|
||||
tagsContainerPosition,
|
||||
tagsContainerMaxWidth,
|
||||
tagsContainerCollapsed,
|
||||
tagsContainerExpanded,
|
||||
tagsOverflowed,
|
||||
} = appState.activeNote;
|
||||
|
||||
@@ -30,8 +30,19 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
||||
useState(TAGS_ROW_HEIGHT);
|
||||
const [overflowCountPosition, setOverflowCountPosition] = useState(0);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const tagsContainerRef = useRef<HTMLDivElement>();
|
||||
const tagsRef = useRef<HTMLButtonElement[]>([]);
|
||||
const overflowButtonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
useCloseOnClickOutside(
|
||||
tagsContainerRef,
|
||||
(expanded: boolean) => {
|
||||
if (overflowButtonRef.current || tagsContainerExpanded) {
|
||||
appState.activeNote.setTagsContainerExpanded(expanded);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const onTagBackspacePress = async (tag: SNTag) => {
|
||||
await appState.activeNote.removeTagFromActiveNote(tag);
|
||||
@@ -41,28 +52,24 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const expandTags = () => {
|
||||
appState.activeNote.setTagsContainerCollapsed(false);
|
||||
};
|
||||
|
||||
const isTagOverflowed = useCallback(
|
||||
(tagElement?: HTMLButtonElement): boolean | undefined => {
|
||||
if (!tagElement) {
|
||||
return;
|
||||
}
|
||||
if (!tagsContainerCollapsed) {
|
||||
if (tagsContainerExpanded) {
|
||||
return false;
|
||||
}
|
||||
return tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP;
|
||||
},
|
||||
[tagsContainerCollapsed]
|
||||
[tagsContainerExpanded]
|
||||
);
|
||||
|
||||
const reloadOverflowCountPosition = useCallback(() => {
|
||||
const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) =>
|
||||
isTagOverflowed(tagElement)
|
||||
);
|
||||
if (!tagsContainerCollapsed || firstOverflowedTagIndex < 1) {
|
||||
if (tagsContainerExpanded || firstOverflowedTagIndex < 1) {
|
||||
return;
|
||||
}
|
||||
const previousTagRect =
|
||||
@@ -70,14 +77,14 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
||||
const position =
|
||||
previousTagRect.right - (tagsContainerPosition ?? 0) + TAG_RIGHT_MARGIN;
|
||||
setOverflowCountPosition(position);
|
||||
}, [isTagOverflowed, tagsContainerCollapsed, tagsContainerPosition]);
|
||||
}, [isTagOverflowed, tagsContainerExpanded, tagsContainerPosition]);
|
||||
|
||||
const reloadTagsContainerHeight = useCallback(() => {
|
||||
const height = tagsContainerCollapsed
|
||||
? TAGS_ROW_HEIGHT
|
||||
: tagsContainerRef.current.scrollHeight;
|
||||
const height = tagsContainerExpanded
|
||||
? tagsContainerRef.current.scrollHeight
|
||||
: TAGS_ROW_HEIGHT;
|
||||
setTagsContainerHeight(height);
|
||||
}, [tagsContainerCollapsed]);
|
||||
}, [tagsContainerExpanded]);
|
||||
|
||||
const reloadOverflowCount = useCallback(() => {
|
||||
const count = tagsRef.current.filter((tagElement) =>
|
||||
@@ -86,6 +93,10 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
||||
appState.activeNote.setOverflowedTagsCount(count);
|
||||
}, [appState.activeNote, isTagOverflowed]);
|
||||
|
||||
const setTagsContainerExpanded = (expanded: boolean) => {
|
||||
appState.activeNote.setTagsContainerExpanded(expanded);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
appState.activeNote.reloadTagsContainerLayout();
|
||||
reloadOverflowCountPosition();
|
||||
@@ -103,11 +114,11 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
||||
mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`;
|
||||
|
||||
return (
|
||||
<div className="flex" style={{ height: tagsContainerHeight }}>
|
||||
<div className="flex" ref={containerRef} style={{ height: tagsContainerHeight }}>
|
||||
<div
|
||||
ref={tagsContainerRef}
|
||||
className={`absolute flex flex-wrap pl-1 -ml-1 ${
|
||||
tagsContainerCollapsed ? 'overflow-hidden' : ''
|
||||
tagsContainerExpanded ? '' : 'overflow-hidden'
|
||||
}`}
|
||||
style={{
|
||||
maxWidth: tagsContainerMaxWidth,
|
||||
@@ -146,12 +157,15 @@ const NoteTags = observer(({ application, appState }: Props) => {
|
||||
tabIndex={tagsOverflowed ? -1 : 0}
|
||||
/>
|
||||
</div>
|
||||
{overflowedTagsCount > 1 && tagsContainerCollapsed && (
|
||||
{tagsOverflowed && (
|
||||
<button
|
||||
ref={overflowButtonRef}
|
||||
type="button"
|
||||
className={`${tagClass} pl-2 absolute`}
|
||||
style={{ left: overflowCountPosition }}
|
||||
onClick={expandTags}
|
||||
onClick={() => {
|
||||
setTagsContainerExpanded(true);
|
||||
}}
|
||||
>
|
||||
+{overflowedTagsCount}
|
||||
</button>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { toDirective, useCloseOnBlur } from './utils';
|
||||
import { toDirective, useCloseOnBlur, useCloseOnClickOutside } from './utils';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { NotesOptions } from './NotesOptions';
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks';
|
||||
import { useRef } from 'preact/hooks';
|
||||
|
||||
type Props = {
|
||||
appState: AppState;
|
||||
@@ -15,18 +15,10 @@ const NotesContextMenu = observer(({ appState }: Props) => {
|
||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
||||
);
|
||||
|
||||
const closeOnClickOutside = useCallback((event: MouseEvent) => {
|
||||
if (!contextMenuRef.current?.contains(event.target as Node)) {
|
||||
appState.notes.setContextMenuOpen(false);
|
||||
}
|
||||
}, [appState]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', closeOnClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', closeOnClickOutside);
|
||||
};
|
||||
}, [closeOnClickOutside]);
|
||||
useCloseOnClickOutside(
|
||||
contextMenuRef,
|
||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
||||
);
|
||||
|
||||
return appState.notes.contextMenuOpen ? (
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FunctionComponent, h, render } from 'preact';
|
||||
import { StateUpdater, useCallback, useState } from 'preact/hooks';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* @returns a callback that will close a dropdown if none of its children has
|
||||
@@ -30,6 +31,26 @@ export function useCloseOnBlur(
|
||||
];
|
||||
}
|
||||
|
||||
export function useCloseOnClickOutside(
|
||||
container: { current: HTMLDivElement },
|
||||
setOpen: (open: boolean) => void
|
||||
): void {
|
||||
const closeOnClickOutside = useCallback((event: { target: EventTarget | null }) => {
|
||||
if (
|
||||
!container.current?.contains(event.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [container, setOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', closeOnClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', closeOnClickOutside);
|
||||
};
|
||||
}, [closeOnClickOutside]);
|
||||
}
|
||||
|
||||
export function toDirective<Props>(
|
||||
component: FunctionComponent<Props>,
|
||||
scope: Record<string, '=' | '&' | '@'> = {}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class ActiveNoteState {
|
||||
tags: SNTag[] = [];
|
||||
tagsContainerPosition? = 0;
|
||||
tagsContainerMaxWidth: number | 'auto' = 'auto';
|
||||
tagsContainerCollapsed = true;
|
||||
tagsContainerExpanded = false;
|
||||
overflowedTagsCount = 0;
|
||||
|
||||
constructor(
|
||||
@@ -28,14 +28,14 @@ export class ActiveNoteState {
|
||||
tags: observable,
|
||||
tagsContainerPosition: observable,
|
||||
tagsContainerMaxWidth: observable,
|
||||
tagsContainerCollapsed: observable,
|
||||
tagsContainerExpanded: observable,
|
||||
overflowedTagsCount: observable,
|
||||
|
||||
tagsOverflowed: computed,
|
||||
|
||||
setTagsContainerPosition: action,
|
||||
setTagsContainerMaxWidth: action,
|
||||
setTagsContainerCollapsed: action,
|
||||
setTagsContainerExpanded: action,
|
||||
setOverflowedTagsCount: action,
|
||||
reloadTags: action,
|
||||
});
|
||||
@@ -59,7 +59,7 @@ export class ActiveNoteState {
|
||||
}
|
||||
|
||||
get tagsOverflowed(): boolean {
|
||||
return this.overflowedTagsCount > 0 && this.tagsContainerCollapsed;
|
||||
return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded;
|
||||
}
|
||||
|
||||
setTagsContainerPosition(position: number): void {
|
||||
@@ -70,8 +70,8 @@ export class ActiveNoteState {
|
||||
this.tagsContainerMaxWidth = width;
|
||||
}
|
||||
|
||||
setTagsContainerCollapsed(collapsed: boolean): void {
|
||||
this.tagsContainerCollapsed = collapsed;
|
||||
setTagsContainerExpanded(expanded: boolean): void {
|
||||
this.tagsContainerExpanded = expanded;
|
||||
}
|
||||
|
||||
setOverflowedTagsCount(count: number): void {
|
||||
@@ -86,7 +86,7 @@ export class ActiveNoteState {
|
||||
}
|
||||
|
||||
reloadTagsContainerLayout(): void {
|
||||
const MARGIN = this.tagsContainerCollapsed ? 68 : 24;
|
||||
const MARGIN = this.tagsContainerExpanded ? 68 : 24;
|
||||
const EDITOR_ELEMENT_ID = 'editor-column';
|
||||
const { clientWidth } = document.documentElement;
|
||||
const editorPosition =
|
||||
|
||||
Reference in New Issue
Block a user