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