1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-12 08:54:00 +02:00
joplin/packages/app-desktop/gui/NoteListItem.tsx
2023-06-01 12:02:36 +01:00

202 lines
6.1 KiB
TypeScript

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<HTMLAnchorElement, MouseEvent>): void;
}
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 (
<div style={{ display: 'flex', height: props.height, alignItems: 'center', paddingLeft: hPadding }}>
<input
style={{ margin: 0, marginBottom: 1, marginRight: 5 }}
type="checkbox"
checked={!!item.todo_completed}
onChange={onCheckboxClick}
/>
</div>
);
}
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 = <span dangerouslySetInnerHTML={{ __html: titleElement.outerHTML }}></span>;
} else {
titleComp = <span>{displayTitle}</span>;
}
const watchedIconStyle = {
paddingRight: 4,
color: theme.color,
};
const watchedIcon = props.isWatched ? null : <i style={watchedIconStyle} className={'fa fa-share-square'}></i>;
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 (
<StyledRoot
className={classNames}
onDragOver={props.onNoteDragOver}
width={props.width}
height={props.height}
isProvisional={props.isProvisional}
selected={props.isSelected}
dragItemPosition={dragItemPosition}
>
{renderCheckbox()}
<a
ref={anchorRef}
onContextMenu={props.onContextMenu}
href="#"
draggable={true}
style={listItemTitleStyle}
onClick={onTitleClick}
onDragStart={props.onDragStart}
data-id={item.id}
>
{watchedIcon}
{titleComp}
</a>
</StyledRoot>
);
}
export default forwardRef(NoteListItem);