feat: make tags container expandable
This commit is contained in:
@@ -10,13 +10,13 @@ import { AppState } from '@/ui_models/app_state';
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
lastTagRef: RefObject<HTMLButtonElement>;
|
||||
tagsRef: RefObject<HTMLButtonElement[]>
|
||||
};
|
||||
|
||||
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
||||
application,
|
||||
appState,
|
||||
lastTagRef,
|
||||
tagsRef,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
@@ -100,8 +100,8 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
||||
onBlur={closeOnBlur}
|
||||
onFocus={showDropdown}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Backspace' && searchQuery === '') {
|
||||
lastTagRef.current?.focus();
|
||||
if (event.key === 'Backspace' && searchQuery === '' && tagsRef.current && tagsRef.current.length > 1) {
|
||||
tagsRef.current[tagsRef.current.length - 1].focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { toDirective } from './utils';
|
||||
import { Icon } from './Icon';
|
||||
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { useRef } from 'preact/hooks';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { SNTag } from '@standardnotes/snjs';
|
||||
|
||||
type Props = {
|
||||
@@ -12,35 +12,142 @@ type Props = {
|
||||
appState: AppState;
|
||||
};
|
||||
|
||||
const TAGS_ROW_RIGHT_MARGIN = 92;
|
||||
const TAGS_ROW_HEIGHT = 32;
|
||||
const MIN_OVERFLOW_TOP = 76;
|
||||
const TAG_RIGHT_MARGIN = 8;
|
||||
|
||||
const NoteTags = observer(({ application, appState }: Props) => {
|
||||
const { activeNoteTags } = appState.notes;
|
||||
const lastTagRef = useRef<HTMLButtonElement>();
|
||||
const [tagsContainerMaxWidth, setTagsContainerMaxWidth] =
|
||||
useState<number | 'auto'>('auto');
|
||||
const [overflowedTagsCount, setOverflowedTagsCount] = useState(0);
|
||||
const [overflowCountPosition, setOverflowCountPosition] = useState(0);
|
||||
const [tagsContainerCollapsed, setTagsContainerCollapsed] = useState(true);
|
||||
const [containerHeight, setContainerHeight] = useState(TAGS_ROW_HEIGHT);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const tagsContainerRef = useRef<HTMLDivElement>();
|
||||
const tagsRef = useRef<HTMLButtonElement[]>([]);
|
||||
tagsRef.current = [];
|
||||
|
||||
const onTagBackspacePress = async (tag: SNTag) => {
|
||||
await appState.notes.removeTagFromActiveNote(tag);
|
||||
lastTagRef.current?.focus();
|
||||
|
||||
if (tagsRef.current.length > 1) {
|
||||
tagsRef.current[tagsRef.current.length - 1].focus();
|
||||
}
|
||||
};
|
||||
|
||||
const reloadOverflowCount = useCallback(() => {
|
||||
const editorElement = document.getElementById('editor-column');
|
||||
let overflowCount = 0;
|
||||
for (const [index, tagElement] of tagsRef.current.entries()) {
|
||||
if (tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP) {
|
||||
if (overflowCount === 0) {
|
||||
setOverflowCountPosition(
|
||||
tagsRef.current[index - 1].getBoundingClientRect().right -
|
||||
(editorElement ? editorElement.getBoundingClientRect().left : 0) +
|
||||
TAG_RIGHT_MARGIN
|
||||
);
|
||||
}
|
||||
overflowCount += 1;
|
||||
}
|
||||
}
|
||||
setOverflowedTagsCount(overflowCount);
|
||||
|
||||
if (!tagsContainerCollapsed) {
|
||||
setContainerHeight(tagsContainerRef.current.scrollHeight);
|
||||
}
|
||||
}, [tagsContainerCollapsed]);
|
||||
|
||||
const expandTags = () => {
|
||||
setContainerHeight(tagsContainerRef.current.scrollHeight);
|
||||
setTagsContainerCollapsed(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const editorElement = document.getElementById('editor-column');
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
const { width } = entry.contentRect;
|
||||
setTagsContainerMaxWidth(width);
|
||||
reloadOverflowCount();
|
||||
});
|
||||
|
||||
if (editorElement) {
|
||||
resizeObserver.observe(editorElement);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [reloadOverflowCount]);
|
||||
|
||||
useEffect(() => {
|
||||
reloadOverflowCount();
|
||||
}, [activeNoteTags, reloadOverflowCount]);
|
||||
|
||||
const tagClass = `bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center
|
||||
mt-2 mr-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap">
|
||||
{activeNoteTags.map((tag, index) => (
|
||||
<div
|
||||
className="flex"
|
||||
ref={containerRef}
|
||||
style={{ height: containerHeight }}
|
||||
>
|
||||
<div
|
||||
ref={tagsContainerRef}
|
||||
className={`absolute flex flex-wrap ${
|
||||
tagsContainerCollapsed ? 'overflow-hidden' : ''
|
||||
}`}
|
||||
style={{
|
||||
maxWidth: tagsContainerMaxWidth,
|
||||
height: TAGS_ROW_HEIGHT,
|
||||
marginRight: TAGS_ROW_RIGHT_MARGIN,
|
||||
}}
|
||||
>
|
||||
{activeNoteTags.map((tag, index) => (
|
||||
<button
|
||||
className={`${tagClass} pl-1`}
|
||||
style={{ maxWidth: tagsContainerMaxWidth }}
|
||||
ref={(element) => {
|
||||
if (element) {
|
||||
tagsRef.current[index] = element;
|
||||
}
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
onTagBackspacePress(tag);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
type="hashtag"
|
||||
className="sn-icon--small color-neutral mr-1"
|
||||
/>
|
||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{tag.title}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
<AutocompleteTagInput
|
||||
application={application}
|
||||
appState={appState}
|
||||
tagsRef={tagsRef}
|
||||
/>
|
||||
</div>
|
||||
{overflowedTagsCount > 1 && tagsContainerCollapsed && (
|
||||
<button
|
||||
className={`bg-contrast border-0 rounded text-xs color-text py-1 pl-1 pr-2 flex items-center
|
||||
mt-2 mr-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`}
|
||||
ref={index === activeNoteTags.length - 1 ? lastTagRef : undefined}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Backspace') {
|
||||
onTagBackspacePress(tag);
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
className={`${tagClass} pl-2 absolute`}
|
||||
style={{ left: overflowCountPosition }}
|
||||
onClick={expandTags}
|
||||
>
|
||||
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
|
||||
<span className="max-w-xs whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{tag.title}
|
||||
</span>
|
||||
+{overflowedTagsCount}
|
||||
</button>
|
||||
))}
|
||||
<AutocompleteTagInput application={application} appState={appState} lastTagRef={lastTagRef} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
ref={buttonRef}
|
||||
className="sn-icon-button"
|
||||
className="sn-icon-button mt-2"
|
||||
>
|
||||
<VisuallyHidden>Actions</VisuallyHidden>
|
||||
<Icon type="more" className="block" />
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
ng-if="self.showLockedIcon"
|
||||
)
|
||||
| {{self.lockText}}
|
||||
#editor-title-bar.section-title-bar.flex.items-center.justify-between.w-full(
|
||||
#editor-title-bar.section-title-bar.flex.items-start.justify-between.w-full(
|
||||
ng-show='self.note && !self.note.errorDecrypting'
|
||||
)
|
||||
div.flex-grow(
|
||||
@@ -41,11 +41,10 @@
|
||||
select-on-focus='true',
|
||||
spellcheck='false'
|
||||
)
|
||||
.editor-tags
|
||||
note-tags(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
note-tags(
|
||||
application='self.application'
|
||||
app-state='self.appState'
|
||||
)
|
||||
div.flex.items-center
|
||||
#save-status
|
||||
.message(
|
||||
|
||||
Reference in New Issue
Block a user