diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index e7d8c6238..1edf7e28e 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -12,28 +12,25 @@ type Props = { appState: AppState; }; -const TAGS_ROW_HEIGHT = 36; -const MIN_OVERFLOW_TOP = 76; -const TAG_RIGHT_MARGIN = 8; - const NoteTags = observer(({ application, appState }: Props) => { const { overflowedTagsCount, tags, - tagsContainerPosition, tagsContainerMaxWidth, tagsContainerExpanded, tagsOverflowed, } = appState.activeNote; - - const [containerHeight, setContainerHeight] = - useState(TAGS_ROW_HEIGHT); + + const [expandedContainerHeight, setExpandedContainerHeight] = useState(0); + const [lastVisibleTagIndex, setLastVisibleTagIndex] = + useState(null); const [overflowCountPosition, setOverflowCountPosition] = useState(0); const containerRef = useRef(); const tagsContainerRef = useRef(); const tagsRef = useRef([]); const overflowButtonRef = useRef(); + tagsRef.current = []; useCloseOnClickOutside(tagsContainerRef, (expanded: boolean) => { @@ -42,16 +39,16 @@ const NoteTags = observer(({ application, appState }: Props) => { } }); - const onTagBackspacePress = async (tag: SNTag) => { + const onTagBackspacePress = async (tag: SNTag, index: number) => { await appState.activeNote.removeTagFromActiveNote(tag); - if (tagsRef.current.length > 1) { - tagsRef.current[tagsRef.current.length - 1].focus(); + if (index > 0) { + tagsRef.current[index - 1].focus(); } }; const onTagClick = (clickedTag: SNTag) => { - const tagIndex = tags.findIndex(tag => tag.uuid === clickedTag.uuid); + const tagIndex = tags.findIndex((tag) => tag.uuid === clickedTag.uuid); if (tagsRef.current[tagIndex] === document.activeElement) { appState.setSelectedTag(clickedTag); } @@ -65,31 +62,29 @@ const NoteTags = observer(({ application, appState }: Props) => { if (tagsContainerExpanded) { return false; } - return tagElement.getBoundingClientRect().top >= MIN_OVERFLOW_TOP; + const firstTagTop = tagsRef.current[0].offsetTop; + return tagElement.offsetTop > firstTagTop; }, [tagsContainerExpanded] ); - const reloadOverflowCountPosition = useCallback(() => { + const reloadLastVisibleTagIndex = useCallback(() => { + if (tagsContainerExpanded) { + return tags.length - 1; + } const firstOverflowedTagIndex = tagsRef.current.findIndex((tagElement) => isTagOverflowed(tagElement) ); - if (tagsContainerExpanded || firstOverflowedTagIndex < 1) { - return; + if (firstOverflowedTagIndex > -1) { + setLastVisibleTagIndex(firstOverflowedTagIndex - 1); + } else { + setLastVisibleTagIndex(null); } - const previousTagRect = - tagsRef.current[firstOverflowedTagIndex - 1].getBoundingClientRect(); - const position = - previousTagRect.right - (tagsContainerPosition ?? 0) + TAG_RIGHT_MARGIN; - setOverflowCountPosition(position); - }, [isTagOverflowed, tagsContainerExpanded, tagsContainerPosition]); + }, [isTagOverflowed, tags, tagsContainerExpanded]); - const reloadContainersHeight = useCallback(() => { - const containerHeight = tagsContainerExpanded - ? tagsContainerRef.current.scrollHeight - : TAGS_ROW_HEIGHT; - setContainerHeight(containerHeight); - }, [tagsContainerExpanded]); + const reloadExpandedContainersHeight = useCallback(() => { + setExpandedContainerHeight(tagsContainerRef.current.scrollHeight); + }, []); const reloadOverflowCount = useCallback(() => { const count = tagsRef.current.filter((tagElement) => @@ -98,72 +93,108 @@ const NoteTags = observer(({ application, appState }: Props) => { appState.activeNote.setOverflowedTagsCount(count); }, [appState.activeNote, isTagOverflowed]); + const reloadOverflowCountPosition = useCallback(() => { + if (tagsContainerExpanded || !lastVisibleTagIndex) { + return; + } + const { offsetLeft: lastVisibleTagLeft, clientWidth: lastVisibleTagWidth } = + tagsRef.current[lastVisibleTagIndex]; + console.log(tagsRef.current[0].offsetLeft); + setOverflowCountPosition(lastVisibleTagLeft + lastVisibleTagWidth); + }, [lastVisibleTagIndex, tagsContainerExpanded]); + const expandTags = () => { appState.activeNote.setTagsContainerExpanded(true); }; - useEffect(() => { + const reloadTagsContainerLayout = useCallback(() => { appState.activeNote.reloadTagsContainerLayout(); - reloadOverflowCountPosition(); - reloadContainersHeight(); + reloadLastVisibleTagIndex(); + reloadExpandedContainersHeight(); reloadOverflowCount(); + reloadOverflowCountPosition(); }, [ appState.activeNote, - reloadOverflowCountPosition, - reloadContainersHeight, + reloadLastVisibleTagIndex, + reloadExpandedContainersHeight, reloadOverflowCount, - tags, + reloadOverflowCountPosition, ]); + useEffect(() => { + reloadTagsContainerLayout; + }, [ + reloadTagsContainerLayout, + tags, + tagsContainerMaxWidth, + ]); + + useEffect(() => { + let tagResizeObserver: ResizeObserver; + if (ResizeObserver) { + tagResizeObserver = new ResizeObserver(() => { + reloadTagsContainerLayout(); + }); + tagsRef.current.forEach((tagElement) => tagResizeObserver.observe(tagElement)); + } + + return () => { + if (tagResizeObserver) { + tagResizeObserver.disconnect(); + } + }; + }, [reloadTagsContainerLayout]); + const tagClass = `h-6 bg-contrast border-0 rounded text-xs color-text py-1 pr-2 flex items-center mt-2 cursor-pointer hover:bg-secondary-contrast focus:bg-secondary-contrast`; return (
- {tags.map((tag: SNTag, index: number) => ( - - ))} + {tags.map((tag: SNTag, index: number) => { + const overflowed = + !tagsContainerExpanded && + lastVisibleTagIndex && + index > lastVisibleTagIndex; + return ( + + ); + })} { diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts index 4fd33550c..2b8c10db8 100644 --- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts +++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts @@ -14,7 +14,6 @@ import { AppState } from './app_state'; export class ActiveNoteState { tags: SNTag[] = []; - tagsContainerPosition? = 0; tagsContainerMaxWidth: number | 'auto' = 'auto'; tagsContainerExpanded = false; overflowedTagsCount = 0; @@ -26,24 +25,18 @@ export class ActiveNoteState { ) { makeObservable(this, { tags: observable, - tagsContainerPosition: observable, tagsContainerMaxWidth: observable, tagsContainerExpanded: observable, overflowedTagsCount: observable, tagsOverflowed: computed, - setTagsContainerPosition: action, setTagsContainerMaxWidth: action, setTagsContainerExpanded: action, setOverflowedTagsCount: action, reloadTags: action, }); - this.tagsContainerPosition = document - .getElementById('editor-column') - ?.getBoundingClientRect().left; - appEventListeners.push( application.streamItems( ContentType.Tag, @@ -62,10 +55,6 @@ export class ActiveNoteState { return this.overflowedTagsCount > 0 && !this.tagsContainerExpanded; } - setTagsContainerPosition(position: number): void { - this.tagsContainerPosition = position; - } - setTagsContainerMaxWidth(width: number): void { this.tagsContainerMaxWidth = width; } @@ -86,16 +75,19 @@ export class ActiveNoteState { } reloadTagsContainerLayout(): void { - const MARGIN = 72; const EDITOR_ELEMENT_ID = 'editor-column'; - const { clientWidth } = document.documentElement; - const editorPosition = - document.getElementById(EDITOR_ELEMENT_ID)?.getBoundingClientRect() - .left ?? 0; - this.appState.activeNote.setTagsContainerPosition(editorPosition); - this.appState.activeNote.setTagsContainerMaxWidth( - clientWidth - editorPosition - MARGIN - ); + const defaultFontSize = window.getComputedStyle( + document.documentElement + ).fontSize; + const containerMargins = parseFloat(defaultFontSize) * 4; + const editorWidth = + document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth; + + if (editorWidth) { + this.appState.activeNote.setTagsContainerMaxWidth( + editorWidth - containerMargins + ); + } } async addTagToActiveNote(tag: SNTag): Promise { diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 32091c59a..c3e7e0f4f 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -57,6 +57,10 @@ margin-bottom: 0.5rem; } +.ml-1 { + margin-left: 0.25rem; +} + .mr-1 { margin-right: 0.25rem; } @@ -192,6 +196,10 @@ height: 2rem; } +.h-9 { + height: 2.25rem; +} + .h-10 { height: 2.5rem; } @@ -212,8 +220,8 @@ overflow: auto; } -.overflow-hidden { - overflow: hidden; +.overflow-y-hidden { + overflow-y: hidden; } .overflow-ellipsis { @@ -260,6 +268,10 @@ width: 20rem; } +.relative { + position: relative; +} + /** * A button that is just an icon. Separated from .sn-button because there * is almost no style overlap.