feat: remove overflowed tags feature
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { SNTag } from '@standardnotes/snjs';
|
import { SNTag } from '@standardnotes/snjs';
|
||||||
import { FunctionalComponent } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
||||||
import { useCloseOnBlur } from './utils';
|
import { useCloseOnBlur } from './utils';
|
||||||
@@ -16,7 +16,7 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
application,
|
application,
|
||||||
appState,
|
appState,
|
||||||
}) => {
|
}) => {
|
||||||
const { tagElements, tags, tagsContainerMaxWidth, tagsOverflowed } = appState.activeNote;
|
const { tagElements } = appState.activeNote;
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||||
@@ -82,19 +82,6 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
await createAndAddNewTag();
|
await createAndAddNewTag();
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadInputOverflowed = useCallback(() => {
|
|
||||||
const overflowed = !tagsOverflowed && appState.activeNote.isElementOverflowed(inputRef.current);
|
|
||||||
appState.activeNote.setInputOverflowed(overflowed);
|
|
||||||
}, [appState.activeNote, tagsOverflowed]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reloadInputOverflowed();
|
|
||||||
}, [
|
|
||||||
reloadInputOverflowed,
|
|
||||||
tagsContainerMaxWidth,
|
|
||||||
tags,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHintVisible(
|
setHintVisible(
|
||||||
searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)
|
searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)
|
||||||
@@ -111,7 +98,6 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
onChange={onSearchQueryChange}
|
onChange={onSearchQueryChange}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Add tag"
|
placeholder="Add tag"
|
||||||
tabIndex={tagsOverflowed ? -1 : 0}
|
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
onFocus={showDropdown}
|
onFocus={showDropdown}
|
||||||
onKeyUp={(event) => {
|
onKeyUp={(event) => {
|
||||||
@@ -139,7 +125,6 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
className="sn-dropdown-item"
|
className="sn-dropdown-item"
|
||||||
onClick={() => onTagOptionClick(tag)}
|
onClick={() => onTagOptionClick(tag)}
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
tabIndex={tagsOverflowed ? -1 : 0}
|
|
||||||
>
|
>
|
||||||
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
||||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||||
@@ -177,7 +162,6 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
className="sn-dropdown-item"
|
className="sn-dropdown-item"
|
||||||
onClick={onTagHintClick}
|
onClick={onTagHintClick}
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
tabIndex={tagsOverflowed ? -1 : 0}
|
|
||||||
>
|
>
|
||||||
<span>Create new tag:</span>
|
<span>Create new tag:</span>
|
||||||
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
|
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { FunctionalComponent } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
import { useCallback, useRef, useState } from 'preact/hooks';
|
import { 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';
|
||||||
@@ -13,12 +13,9 @@ type Props = {
|
|||||||
|
|
||||||
export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
|
export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
|
||||||
const {
|
const {
|
||||||
tags,
|
|
||||||
tagsContainerExpanded,
|
|
||||||
tagsContainerMaxWidth,
|
tagsContainerMaxWidth,
|
||||||
} = appState.activeNote;
|
} = appState.activeNote;
|
||||||
|
|
||||||
const [overflowed, setOverflowed] = useState(false);
|
|
||||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState({ top: 0, left: 0 });
|
const [contextMenuPosition, setContextMenuPosition] = useState({ top: 0, left: 0 });
|
||||||
|
|
||||||
@@ -36,15 +33,6 @@ export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
|
|||||||
appState.setSelectedTag(tag);
|
appState.setSelectedTag(tag);
|
||||||
};
|
};
|
||||||
|
|
||||||
const reloadOverflowed = useCallback(() => {
|
|
||||||
const overflowed = appState.activeNote.isTagOverflowed(tag);
|
|
||||||
setOverflowed(overflowed);
|
|
||||||
}, [appState.activeNote, tag]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reloadOverflowed();
|
|
||||||
}, [reloadOverflowed, tags, tagsContainerExpanded, tagsContainerMaxWidth]);
|
|
||||||
|
|
||||||
const contextMenuListener = (event: MouseEvent) => {
|
const contextMenuListener = (event: MouseEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setContextMenuPosition({
|
setContextMenuPosition({
|
||||||
@@ -78,7 +66,6 @@ export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
|
|||||||
deleteTag();
|
deleteTag();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tabIndex={overflowed ? -1 : 0}
|
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
>
|
>
|
||||||
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
|
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
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, useCloseOnClickOutside } from './utils';
|
import { toDirective } from './utils';
|
||||||
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
import { WebApplication } from '@/ui_models/application';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
|
||||||
import { NoteTag } from './NoteTag';
|
import { NoteTag } from './NoteTag';
|
||||||
|
import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
@@ -13,72 +13,17 @@ type Props = {
|
|||||||
|
|
||||||
const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
||||||
const {
|
const {
|
||||||
inputOverflowed,
|
|
||||||
overflowCountPosition,
|
|
||||||
overflowedTagsCount,
|
|
||||||
tagElements,
|
|
||||||
tags,
|
tags,
|
||||||
tagsContainerMaxWidth,
|
tagsContainerMaxWidth,
|
||||||
tagsContainerExpanded,
|
|
||||||
tagsOverflowed,
|
|
||||||
} = appState.activeNote;
|
} = appState.activeNote;
|
||||||
|
|
||||||
const [expandedContainerHeight, setExpandedContainerHeight] = useState(0);
|
|
||||||
|
|
||||||
const tagsContainerRef = useRef<HTMLDivElement>();
|
|
||||||
const overflowButtonRef = useRef<HTMLButtonElement>();
|
|
||||||
|
|
||||||
useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => {
|
|
||||||
if (tagsContainerExpanded) {
|
|
||||||
appState.activeNote.setTagsContainerExpanded(expanded);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const reloadExpandedContainerHeight = useCallback(() => {
|
|
||||||
setExpandedContainerHeight(tagsContainerRef.current.scrollHeight);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
appState.activeNote.reloadTagsContainerLayout();
|
appState.activeNote.reloadTagsContainerMaxWidth();
|
||||||
reloadExpandedContainerHeight();
|
}, [appState.activeNote]);
|
||||||
}, [
|
|
||||||
appState.activeNote,
|
|
||||||
reloadExpandedContainerHeight,
|
|
||||||
tags,
|
|
||||||
tagsContainerMaxWidth,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let tagResizeObserver: ResizeObserver;
|
|
||||||
if (ResizeObserver) {
|
|
||||||
tagResizeObserver = new ResizeObserver(() => {
|
|
||||||
appState.activeNote.reloadTagsContainerLayout();
|
|
||||||
reloadExpandedContainerHeight();
|
|
||||||
});
|
|
||||||
tagElements.forEach(
|
|
||||||
(tagElement) => tagElement && tagResizeObserver.observe(tagElement)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (tagResizeObserver) {
|
|
||||||
tagResizeObserver.disconnect();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [appState.activeNote, reloadExpandedContainerHeight, tagElements]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
|
||||||
className={`flex transition-height duration-150 relative ${
|
|
||||||
inputOverflowed ? 'h-18' : 'h-9'
|
|
||||||
}`}
|
|
||||||
style={tagsContainerExpanded ? { height: expandedContainerHeight } : {}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref={tagsContainerRef}
|
className="bg-default flex flex-wrap pl-1 -ml-1"
|
||||||
className={`absolute bg-default flex flex-wrap pl-1 -ml-1 ${
|
|
||||||
inputOverflowed ? 'h-18' : 'h-9'
|
|
||||||
} ${tagsContainerExpanded || !tagsOverflowed ? '' : 'overflow-hidden'}`}
|
|
||||||
style={{
|
style={{
|
||||||
maxWidth: tagsContainerMaxWidth,
|
maxWidth: tagsContainerMaxWidth,
|
||||||
}}
|
}}
|
||||||
@@ -92,18 +37,6 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
|||||||
))}
|
))}
|
||||||
<AutocompleteTagInput application={application} appState={appState} />
|
<AutocompleteTagInput application={application} appState={appState} />
|
||||||
</div>
|
</div>
|
||||||
{tagsOverflowed && (
|
|
||||||
<button
|
|
||||||
ref={overflowButtonRef}
|
|
||||||
type="button"
|
|
||||||
className="sn-tag ml-1 absolute"
|
|
||||||
onClick={() => appState.activeNote.setTagsContainerExpanded(true)}
|
|
||||||
style={{ left: overflowCountPosition }}
|
|
||||||
>
|
|
||||||
+{overflowedTagsCount}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
} from '@standardnotes/snjs';
|
} from '@standardnotes/snjs';
|
||||||
import {
|
import {
|
||||||
action,
|
action,
|
||||||
computed,
|
|
||||||
makeObservable,
|
makeObservable,
|
||||||
observable,
|
observable,
|
||||||
} from 'mobx';
|
} from 'mobx';
|
||||||
@@ -13,14 +12,9 @@ import { WebApplication } from '../application';
|
|||||||
import { AppState } from './app_state';
|
import { AppState } from './app_state';
|
||||||
|
|
||||||
export class ActiveNoteState {
|
export class ActiveNoteState {
|
||||||
inputOverflowed = false;
|
|
||||||
overflowCountPosition = 0;
|
|
||||||
overflowedTagsCount = 0;
|
|
||||||
tagElements: (HTMLButtonElement | undefined)[] = [];
|
tagElements: (HTMLButtonElement | undefined)[] = [];
|
||||||
tagFocused = false;
|
|
||||||
tags: SNTag[] = [];
|
tags: SNTag[] = [];
|
||||||
tagsContainerMaxWidth: number | 'auto' = 0;
|
tagsContainerMaxWidth: number | 'auto' = 0;
|
||||||
tagsContainerExpanded = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private application: WebApplication,
|
private application: WebApplication,
|
||||||
@@ -28,24 +22,12 @@ export class ActiveNoteState {
|
|||||||
appEventListeners: (() => void)[]
|
appEventListeners: (() => void)[]
|
||||||
) {
|
) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
inputOverflowed: observable,
|
|
||||||
overflowCountPosition: observable,
|
|
||||||
overflowedTagsCount: observable,
|
|
||||||
tagElements: observable,
|
tagElements: observable,
|
||||||
tagFocused: observable,
|
|
||||||
tags: observable,
|
tags: observable,
|
||||||
tagsContainerExpanded: observable,
|
|
||||||
tagsContainerMaxWidth: observable,
|
tagsContainerMaxWidth: observable,
|
||||||
|
|
||||||
tagsOverflowed: computed,
|
|
||||||
|
|
||||||
setInputOverflowed: action,
|
|
||||||
setOverflowCountPosition: action,
|
|
||||||
setOverflowedTagsCount: action,
|
|
||||||
setTagElement: action,
|
setTagElement: action,
|
||||||
setTagFocused: action,
|
|
||||||
setTags: action,
|
setTags: action,
|
||||||
setTagsContainerExpanded: action,
|
|
||||||
setTagsContainerMaxWidth: action,
|
setTagsContainerMaxWidth: action,
|
||||||
reloadTags: action,
|
reloadTags: action,
|
||||||
});
|
});
|
||||||
@@ -60,23 +42,6 @@ export class ActiveNoteState {
|
|||||||
get activeNote(): SNNote | undefined {
|
get activeNote(): SNNote | undefined {
|
||||||
return this.appState.notes.activeEditor?.note;
|
return this.appState.notes.activeEditor?.note;
|
||||||
}
|
}
|
||||||
|
|
||||||
get tagsOverflowed(): boolean {
|
|
||||||
return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInputOverflowed(overflowed: boolean): void {
|
|
||||||
this.inputOverflowed = overflowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverflowCountPosition(position: number): void {
|
|
||||||
this.overflowCountPosition = position;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverflowedTagsCount(count: number): void {
|
|
||||||
this.overflowedTagsCount = count;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTagElement(tag: SNTag, element: HTMLButtonElement): void {
|
setTagElement(tag: SNTag, element: HTMLButtonElement): void {
|
||||||
const tagIndex = this.getTagIndex(tag);
|
const tagIndex = this.getTagIndex(tag);
|
||||||
if (tagIndex > -1) {
|
if (tagIndex > -1) {
|
||||||
@@ -84,10 +49,6 @@ export class ActiveNoteState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTagFocused(focused: boolean): void {
|
|
||||||
this.tagFocused = focused;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTagElements(elements: (HTMLButtonElement | undefined)[]): void {
|
setTagElements(elements: (HTMLButtonElement | undefined)[]): void {
|
||||||
this.tagElements = elements;
|
this.tagElements = elements;
|
||||||
}
|
}
|
||||||
@@ -96,10 +57,6 @@ export class ActiveNoteState {
|
|||||||
this.tags = tags;
|
this.tags = tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTagsContainerExpanded(expanded: boolean): void {
|
|
||||||
this.tagsContainerExpanded = expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTagsContainerMaxWidth(width: number): void {
|
setTagsContainerMaxWidth(width: number): void {
|
||||||
this.tagsContainerMaxWidth = width;
|
this.tagsContainerMaxWidth = width;
|
||||||
}
|
}
|
||||||
@@ -122,25 +79,6 @@ export class ActiveNoteState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isElementOverflowed(element: HTMLElement): boolean {
|
|
||||||
if (
|
|
||||||
this.tagElements.length === 0 ||
|
|
||||||
!this.tagElements[0]
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const firstTagTop = this.tagElements[0].offsetTop;
|
|
||||||
return element.offsetTop > firstTagTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
isTagOverflowed(tag: SNTag): boolean {
|
|
||||||
if (this.tagsContainerExpanded) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const tagElement = this.getTagElement(tag);
|
|
||||||
return tagElement ? this.isElementOverflowed(tagElement) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadTags(): void {
|
reloadTags(): void {
|
||||||
const { activeNote } = this;
|
const { activeNote } = this;
|
||||||
if (activeNote) {
|
if (activeNote) {
|
||||||
@@ -151,24 +89,6 @@ export class ActiveNoteState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadOverflowCountPosition(): void {
|
|
||||||
const lastVisibleTagIndex = this.tagElements.findIndex(tagElement => tagElement && this.isElementOverflowed(tagElement)) - 1;
|
|
||||||
if (lastVisibleTagIndex > -1 && this.tagElements.length > lastVisibleTagIndex) {
|
|
||||||
const lastVisibleTagElement = this.tagElements[lastVisibleTagIndex];
|
|
||||||
if (lastVisibleTagElement) {
|
|
||||||
const { offsetLeft, offsetWidth } = lastVisibleTagElement;
|
|
||||||
this.setOverflowCountPosition(offsetLeft + offsetWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadOverflowedTagsCount(): void {
|
|
||||||
const count = this.tagElements.filter((tagElement) =>
|
|
||||||
tagElement && this.isElementOverflowed(tagElement)
|
|
||||||
).length;
|
|
||||||
this.setOverflowedTagsCount(count);
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadTagsContainerMaxWidth(): void {
|
reloadTagsContainerMaxWidth(): void {
|
||||||
const EDITOR_ELEMENT_ID = 'editor-column';
|
const EDITOR_ELEMENT_ID = 'editor-column';
|
||||||
const defaultFontSize = parseFloat(window.getComputedStyle(
|
const defaultFontSize = parseFloat(window.getComputedStyle(
|
||||||
@@ -183,12 +103,6 @@ export class ActiveNoteState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadTagsContainerLayout(): void {
|
|
||||||
this.reloadTagsContainerMaxWidth();
|
|
||||||
this.reloadOverflowedTagsCount();
|
|
||||||
this.reloadOverflowCountPosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
async addTagToActiveNote(tag: SNTag): Promise<void> {
|
async addTagToActiveNote(tag: SNTag): Promise<void> {
|
||||||
const { activeNote } = this;
|
const { activeNote } = this;
|
||||||
if (activeNote) {
|
if (activeNote) {
|
||||||
|
|||||||
Reference in New Issue
Block a user