import * as React from 'react'; import { useRef, forwardRef, useImperativeHandle, useCallback } from 'react'; const { themeStyle } = require('@joplin/lib/theme'); const Mark = require('mark.js/dist/mark.min.js'); const markJsUtils = require('@joplin/lib/markJsUtils'); import Note from '@joplin/lib/models/Note'; const { replaceRegexDiacritics, pregQuote } = require('@joplin/lib/string-utils'); const styled = require('styled-components').default; const StyledRoot = styled.div` width: ${(props: any) => props.width}px; height: ${(props: any) => props.height}px; opacity: ${(props: any) => props.isProvisional ? '0.5' : '1'}; max-width: 100%; box-sizing: border-box; display: flex; align-items: stretch; position: relative; background-color: ${(props: any) => props.selected ? props.theme.selectedColor : 'none'}; border-style: solid; border-color: ${(props: any) => props.theme.color}; border-top-width: ${(props: any) => props.dragItemPosition === 'top' ? 2 : 0}px; border-bottom-width: ${(props: any) => props.dragItemPosition === 'bottom' ? 2 : 0}px; border-right: none; border-left: none; // https://stackoverflow.com/questions/50174448/css-how-to-add-white-space-before-elements-border &::before { content: ''; border-bottom: 1px solid ${(props: any) => props.theme.dividerColor}; width: ${(props: any) => props.width - 32}px; position: absolute; bottom: 0; left: 16px; } &:hover { background-color: ${(props: any) => props.theme.backgroundColorHover3}; } `; interface NoteListItemProps { themeId: number; width: number; height: number; style: any; dragItemIndex: number; highlightedWords: string[]; index: number; isProvisional: boolean; isSelected: boolean; isWatched: boolean; item: any; itemCount: number; onCheckboxClick: any; onDragStart: any; onNoteDragOver: any; onTitleClick: any; onContextMenu(event: React.MouseEvent): void; draggable: boolean; } function NoteListItem(props: NoteListItemProps, ref: any) { const item = props.item; const theme = themeStyle(props.themeId); const hPadding = 16; const anchorRef = useRef(null); useImperativeHandle(ref, () => { return { focus: function() { if (anchorRef.current) anchorRef.current.focus(); }, getHeight: () => anchorRef.current?.clientHeight, }; }); let dragItemPosition = ''; if (props.dragItemIndex === props.index) { dragItemPosition = 'top'; } else if (props.index === props.itemCount - 1 && props.dragItemIndex >= props.itemCount) { dragItemPosition = 'bottom'; } const onTitleClick = useCallback((event) => { props.onTitleClick(event, props.item); }, [props.onTitleClick, props.item]); const onCheckboxClick = useCallback((event) => { props.onCheckboxClick(event, props.item); }, [props.onCheckboxClick, props.item]); // Setting marginBottom = 1 because it makes the checkbox looks more centered, at least on Windows // but don't know how it will look in other OSes. function renderCheckbox() { if (!item.is_todo) return null; return (
); } let listItemTitleStyle = { ...props.style.listItemTitle }; listItemTitleStyle.paddingLeft = !item.is_todo ? hPadding : 4; if (item.is_shared) listItemTitleStyle.color = theme.colorWarn3; if (item.is_todo && !!item.todo_completed) listItemTitleStyle = { ...listItemTitleStyle, ...props.style.listItemTitleCompleted }; const displayTitle = Note.displayTitle(item); let titleComp = null; if (props.highlightedWords.length) { const titleElement = document.createElement('span'); titleElement.textContent = displayTitle; const mark = new Mark(titleElement, { exclude: ['img'], acrossElements: true, }); mark.unmark(); try { for (const wordToBeHighlighted of props.highlightedWords) { markJsUtils.markKeyword(mark, wordToBeHighlighted, { pregQuote: pregQuote, replaceRegexDiacritics: replaceRegexDiacritics, }); } } catch (error) { if (error.name !== 'SyntaxError') { throw error; } // An error of 'Regular expression too large' might occour in the markJs library // when the input is really big, this catch is here to avoid the application crashing // https://github.com/laurent22/joplin/issues/7634 console.error('Error while trying to highlight words from search: ', error); } // Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement // is a span tag that we created and that contains data that's been inserted as plain text // with `textContent` so it cannot contain any XSS attacks. We use this feature because // mark.js can only deal with DOM elements. // https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml titleComp = ; } else { titleComp = {displayTitle}; } const watchedIconStyle = { paddingRight: 4, color: theme.color, }; const watchedIcon = props.isWatched ? null : ; const classNames = [ 'list-item-container', props.isSelected && 'selected', item.todo_completed && 'todo-completed', item.is_todo ? 'todo-list-item' : 'note-list-item', (props.index + 1) % 2 === 0 ? 'even' : 'odd', ] .filter(e => !!e) .join(' '); // Need to include "todo_completed" in key so that checkbox is updated when // item is changed via sync. return ( {renderCheckbox()} {watchedIcon} {titleComp} ); } export default forwardRef(NoteListItem);