import { themeStyle } from '@joplin/lib/theme'; import * as React from 'react'; import { useMemo, useState, useEffect, useRef, useCallback } from 'react'; import NoteList2 from '../NoteList/NoteList2'; import NoteListControls from '../NoteListControls/NoteListControls'; import { Size } from '../ResizableLayout/utils/types'; import styled from 'styled-components'; import { getDefaultListRenderer, getListRendererById } from '@joplin/lib/services/noteList/renderers'; import Logger from '@joplin/utils/Logger'; import NoteListHeader from '../NoteListHeader/NoteListHeader'; import { _ } from '@joplin/lib/locale'; import { BaseBreakpoint, Breakpoints } from '../NoteList/utils/types'; import { ButtonSize, buttonSizePx } from '../Button/Button'; import Setting from '@joplin/lib/models/Setting'; import { OnItemClickHander } from '../NoteListHeader/types'; import { NoteListColumns } from '@joplin/lib/services/plugins/api/noteListType'; import depNameToNoteProp from '@joplin/lib/services/noteList/depNameToNoteProp'; import { getTrashFolderId } from '@joplin/lib/services/trash'; const logger = Logger.create('NoteListWrapper'); interface Props { resizableLayoutEventEmitter: any; size: Size; visible: boolean; themeId: number; listRendererId: string; startupPluginsLoaded: boolean; notesSortOrderField: string; notesSortOrderReverse: boolean; columns: NoteListColumns; selectedFolderId: string; } const StyledRoot = styled.div` display: flex; flex-direction: column; overflow: hidden; width: 100%; `; // Even though these calculations mostly concern the NoteListControls component, we do them here // because we need to know the height of that control to calculate the note list height. const useNoteListControlsBreakpoints = (width: number, newNoteRef: React.MutableRefObject, selectedFolderId: string) => { const [dynamicBreakpoints, setDynamicBreakpoints] = useState({ Sm: BaseBreakpoint.Sm, Md: BaseBreakpoint.Md, Lg: BaseBreakpoint.Lg, Xl: BaseBreakpoint.Xl }); const getTextWidth = useCallback((text: string): number => { const canvas = document.createElement('canvas'); if (!canvas) throw new Error('Failed to create canvas element'); const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('Failed to get context'); const fontWeight = getComputedStyle(newNoteRef.current).getPropertyValue('font-weight'); const fontSize = getComputedStyle(newNoteRef.current).getPropertyValue('font-size'); const fontFamily = getComputedStyle(newNoteRef.current).getPropertyValue('font-family'); ctx.font = `${fontWeight} ${fontSize} ${fontFamily}`; return ctx.measureText(text).width; }, [newNoteRef]); const showNewNoteButton = selectedFolderId !== getTrashFolderId(); // Initialize language-specific breakpoints useEffect(() => { if (!newNoteRef.current) return; if (showNewNoteButton) return; // Use the longest string to calculate the amount of extra width needed const smAdditional = getTextWidth(_('note')) > getTextWidth(_('to-do')) ? getTextWidth(_('note')) : getTextWidth(_('to-do')); const mdAdditional = getTextWidth(_('New note')) > getTextWidth(_('New to-do')) ? getTextWidth(_('New note')) : getTextWidth(_('New to-do')); const Sm = BaseBreakpoint.Sm + smAdditional * 2; const Md = BaseBreakpoint.Md + mdAdditional * 2; const Lg = BaseBreakpoint.Lg + Md; const Xl = BaseBreakpoint.Xl; setDynamicBreakpoints({ Sm, Md, Lg, Xl }); }, [newNoteRef, getTextWidth, showNewNoteButton]); const breakpoint: number = useMemo(() => { // Find largest breakpoint that width is less than const index = Object.values(dynamicBreakpoints).findIndex(x => width < x); return index === -1 ? dynamicBreakpoints.Xl : Object.values(dynamicBreakpoints)[index]; }, [width, dynamicBreakpoints]); const lineCount = breakpoint !== dynamicBreakpoints.Xl ? 2 : 1; return { breakpoint, dynamicBreakpoints, lineCount }; }; // If the renderer ID that was saved to settings is already registered, we // return it. If not, we need to wait for all plugins to be loaded, because one // of them will most likely register the renderer we need. If none of them do, // we use a default renderer instead of throwing an error. const useListRenderer = (listRendererId: string, startupPluginsLoaded: boolean) => { const listRenderer = getListRendererById(listRendererId); if (listRenderer) return listRenderer; if (startupPluginsLoaded) { logger.error(`Tried to load renderer "${listRendererId}" but none of the registered renderers match this ID - using default renderer instead`); return getDefaultListRenderer(); } return null; }; export default function NoteListWrapper(props: Props) { const theme = themeStyle(props.themeId); const [controlHeight] = useState(theme.topRowHeight); const listRenderer = useListRenderer(props.listRendererId, props.startupPluginsLoaded); const newNoteButtonRef = useRef(null); const isMultiColumns = listRenderer ? listRenderer.multiColumns : false; const columns = isMultiColumns ? props.columns : null; const { breakpoint, dynamicBreakpoints, lineCount } = useNoteListControlsBreakpoints(props.size.width, newNoteButtonRef, props.selectedFolderId); const noteListControlsButtonSize = ButtonSize.Small; const noteListControlsPadding = theme.mainPadding; const noteListControlsButtonVerticalGap = 5; const noteListControlsHeight = useMemo(() => { if (lineCount === 1) { return buttonSizePx(noteListControlsButtonSize) + noteListControlsPadding * 2; } else { return buttonSizePx(noteListControlsButtonSize) * 2 + noteListControlsPadding * 2 + noteListControlsButtonVerticalGap; } }, [lineCount, noteListControlsButtonSize, noteListControlsPadding, noteListControlsButtonVerticalGap]); const noteListSize = useMemo(() => { return { width: props.size.width, height: props.size.height - noteListControlsHeight - (isMultiColumns ? theme.noteListHeaderHeight : 0), }; }, [props.size, noteListControlsHeight, theme.noteListHeaderHeight, isMultiColumns]); const onHeaderItemClick: OnItemClickHander = useCallback(event => { const field = depNameToNoteProp(event.name as any).split('.')[1]; if (!Setting.isAllowedEnumOption('notes.sortOrder.field', field)) { logger.warn(`Unsupported sorting option: ${field}`); return; } if (Setting.value('notes.sortOrder.field') === field) { Setting.toggle('notes.sortOrder.reverse'); } else { Setting.setValue('notes.sortOrder.field', field); } }, []); const renderHeader = () => { if (!listRenderer || !isMultiColumns) return null; return ; }; const renderNoteList = () => { if (!listRenderer) return null; return ; }; return ( {renderHeader()} {renderNoteList()} ); }