feat: make tags container expandable
This commit is contained in:
@@ -10,13 +10,13 @@ import { AppState } from '@/ui_models/app_state';
|
|||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
lastTagRef: RefObject<HTMLButtonElement>;
|
tagsRef: RefObject<HTMLButtonElement[]>
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
||||||
application,
|
application,
|
||||||
appState,
|
appState,
|
||||||
lastTagRef,
|
tagsRef,
|
||||||
}) => {
|
}) => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||||
@@ -100,8 +100,8 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
onFocus={showDropdown}
|
onFocus={showDropdown}
|
||||||
onKeyUp={(event) => {
|
onKeyUp={(event) => {
|
||||||
if (event.key === 'Backspace' && searchQuery === '') {
|
if (event.key === 'Backspace' && searchQuery === '' && tagsRef.current && tagsRef.current.length > 1) {
|
||||||
lastTagRef.current?.focus();
|
tagsRef.current[tagsRef.current.length - 1].focus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { toDirective } 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';
|
||||||
import { useRef } from 'preact/hooks';
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { SNTag } from '@standardnotes/snjs';
|
import { SNTag } from '@standardnotes/snjs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,35 +12,142 @@ type Props = {
|
|||||||
appState: AppState;
|
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 NoteTags = observer(({ application, appState }: Props) => {
|
||||||
const { activeNoteTags } = appState.notes;
|
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) => {
|
const onTagBackspacePress = async (tag: SNTag) => {
|
||||||
await appState.notes.removeTagFromActiveNote(tag);
|
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 (
|
return (
|
||||||
<div className="flex flex-wrap">
|
<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) => (
|
{activeNoteTags.map((tag, index) => (
|
||||||
<button
|
<button
|
||||||
className={`bg-contrast border-0 rounded text-xs color-text py-1 pl-1 pr-2 flex items-center
|
className={`${tagClass} pl-1`}
|
||||||
mt-2 mr-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`}
|
style={{ maxWidth: tagsContainerMaxWidth }}
|
||||||
ref={index === activeNoteTags.length - 1 ? lastTagRef : undefined}
|
ref={(element) => {
|
||||||
|
if (element) {
|
||||||
|
tagsRef.current[index] = element;
|
||||||
|
}
|
||||||
|
}}
|
||||||
onKeyUp={(event) => {
|
onKeyUp={(event) => {
|
||||||
if (event.key === 'Backspace') {
|
if (event.key === 'Backspace') {
|
||||||
onTagBackspacePress(tag);
|
onTagBackspacePress(tag);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="hashtag" className="sn-icon--small color-neutral mr-1" />
|
<Icon
|
||||||
<span className="max-w-xs whitespace-nowrap overflow-hidden overflow-ellipsis">
|
type="hashtag"
|
||||||
|
className="sn-icon--small color-neutral mr-1"
|
||||||
|
/>
|
||||||
|
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||||
{tag.title}
|
{tag.title}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<AutocompleteTagInput application={application} appState={appState} lastTagRef={lastTagRef} />
|
<AutocompleteTagInput
|
||||||
|
application={application}
|
||||||
|
appState={appState}
|
||||||
|
tagsRef={tagsRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{overflowedTagsCount > 1 && tagsContainerCollapsed && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${tagClass} pl-2 absolute`}
|
||||||
|
style={{ left: overflowCountPosition }}
|
||||||
|
onClick={expandTags}
|
||||||
|
>
|
||||||
|
+{overflowedTagsCount}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const NotesOptionsPanel = observer(({ appState }: Props) => {
|
|||||||
}}
|
}}
|
||||||
onBlur={closeOnBlur}
|
onBlur={closeOnBlur}
|
||||||
ref={buttonRef}
|
ref={buttonRef}
|
||||||
className="sn-icon-button"
|
className="sn-icon-button mt-2"
|
||||||
>
|
>
|
||||||
<VisuallyHidden>Actions</VisuallyHidden>
|
<VisuallyHidden>Actions</VisuallyHidden>
|
||||||
<Icon type="more" className="block" />
|
<Icon type="more" className="block" />
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
ng-if="self.showLockedIcon"
|
ng-if="self.showLockedIcon"
|
||||||
)
|
)
|
||||||
| {{self.lockText}}
|
| {{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'
|
ng-show='self.note && !self.note.errorDecrypting'
|
||||||
)
|
)
|
||||||
div.flex-grow(
|
div.flex-grow(
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
select-on-focus='true',
|
select-on-focus='true',
|
||||||
spellcheck='false'
|
spellcheck='false'
|
||||||
)
|
)
|
||||||
.editor-tags
|
|
||||||
note-tags(
|
note-tags(
|
||||||
application='self.application'
|
application='self.application'
|
||||||
app-state='self.appState'
|
app-state='self.appState'
|
||||||
|
|||||||
Reference in New Issue
Block a user