2023-08-21 17:01:20 +02:00
|
|
|
import * as React from 'react';
|
|
|
|
import { useCallback, forwardRef, LegacyRef, ChangeEvent, CSSProperties, MouseEventHandler, DragEventHandler, useMemo, memo } from 'react';
|
2023-09-18 18:40:36 +02:00
|
|
|
import { ItemFlow, ListRenderer, OnChangeEvent, OnChangeHandler } from '@joplin/lib/services/plugins/api/noteListType';
|
2023-08-21 17:01:20 +02:00
|
|
|
import { Size } from '@joplin/utils/types';
|
|
|
|
import useRootElement from './utils/useRootElement';
|
|
|
|
import useItemElement from './utils/useItemElement';
|
|
|
|
import useItemEventHandlers from './utils/useItemEventHandlers';
|
2023-09-18 18:40:36 +02:00
|
|
|
import { OnInputChange } from './utils/types';
|
2023-08-21 17:01:20 +02:00
|
|
|
import Note from '@joplin/lib/models/Note';
|
2023-09-18 18:40:36 +02:00
|
|
|
import { NoteEntity } from '@joplin/lib/services/database/types';
|
|
|
|
import useRenderedNote from './utils/useRenderedNote';
|
2023-08-21 17:01:20 +02:00
|
|
|
|
|
|
|
interface NoteItemProps {
|
|
|
|
dragIndex: number;
|
|
|
|
flow: ItemFlow;
|
|
|
|
highlightedWords: string[];
|
|
|
|
index: number;
|
|
|
|
isProvisional: boolean;
|
|
|
|
itemSize: Size;
|
|
|
|
noteCount: number;
|
|
|
|
onChange: OnChangeHandler;
|
|
|
|
onClick: MouseEventHandler<HTMLDivElement>;
|
|
|
|
onContextMenu: MouseEventHandler;
|
|
|
|
onDragOver: DragEventHandler;
|
|
|
|
onDragStart: DragEventHandler;
|
|
|
|
style: CSSProperties;
|
2023-09-18 18:40:36 +02:00
|
|
|
note: NoteEntity;
|
|
|
|
isSelected: boolean;
|
|
|
|
isWatched: boolean;
|
|
|
|
listRenderer: ListRenderer;
|
2023-08-21 17:01:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const NoteListItem = (props: NoteItemProps, ref: LegacyRef<HTMLDivElement>) => {
|
2023-09-18 18:40:36 +02:00
|
|
|
const noteId = props.note.id;
|
|
|
|
const elementId = `list-note-${noteId}`;
|
|
|
|
|
|
|
|
const onInputChange: OnInputChange = useCallback(async (event: ChangeEvent<HTMLInputElement>) => {
|
|
|
|
const getValue = (element: HTMLInputElement) => {
|
|
|
|
if (element.type === 'checkbox') return element.checked;
|
|
|
|
if (element.type === 'text') return element.value;
|
|
|
|
throw new Error(`Unsupported element: ${element.type}`);
|
|
|
|
};
|
2023-08-21 17:01:20 +02:00
|
|
|
|
|
|
|
const changeEvent: OnChangeEvent = {
|
2023-09-18 18:40:36 +02:00
|
|
|
noteId: noteId,
|
2023-08-21 17:01:20 +02:00
|
|
|
elementId: event.currentTarget.getAttribute('data-id'),
|
2023-09-18 18:40:36 +02:00
|
|
|
value: getValue(event.currentTarget),
|
2023-08-21 17:01:20 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
if (changeEvent.elementId === 'todo-checkbox') {
|
|
|
|
await Note.save({
|
|
|
|
id: changeEvent.noteId,
|
|
|
|
todo_completed: changeEvent.value ? Date.now() : 0,
|
|
|
|
}, { userSideValidation: true });
|
|
|
|
} else {
|
|
|
|
if (props.onChange) await props.onChange(changeEvent);
|
|
|
|
}
|
2023-09-18 18:40:36 +02:00
|
|
|
}, [props.onChange, noteId]);
|
2023-08-21 17:01:20 +02:00
|
|
|
|
|
|
|
const rootElement = useRootElement(elementId);
|
|
|
|
|
2023-09-18 18:40:36 +02:00
|
|
|
const renderedNote = useRenderedNote(props.note, props.isSelected, props.isWatched, props.listRenderer, props.highlightedWords);
|
|
|
|
|
2023-08-21 17:01:20 +02:00
|
|
|
const itemElement = useItemElement(
|
|
|
|
rootElement,
|
2023-09-18 18:40:36 +02:00
|
|
|
noteId,
|
|
|
|
renderedNote ? renderedNote.html : '',
|
2023-08-21 17:01:20 +02:00
|
|
|
props.style,
|
|
|
|
props.itemSize,
|
|
|
|
props.onClick,
|
2023-08-22 12:58:53 +02:00
|
|
|
props.flow,
|
2023-08-21 17:01:20 +02:00
|
|
|
);
|
|
|
|
|
2023-09-18 18:40:36 +02:00
|
|
|
useItemEventHandlers(rootElement, itemElement, onInputChange);
|
2023-08-21 17:01:20 +02:00
|
|
|
|
|
|
|
const className = useMemo(() => {
|
|
|
|
return [
|
|
|
|
'note-list-item-wrapper',
|
|
|
|
|
|
|
|
// This is not used by the app, but kept here because it may be used
|
|
|
|
// by users for custom CSS.
|
|
|
|
(props.index + 1) % 2 === 0 ? 'even' : 'odd',
|
|
|
|
|
|
|
|
props.isProvisional && '-provisional',
|
|
|
|
].filter(e => !!e).join(' ');
|
|
|
|
}, [props.index, props.isProvisional]);
|
|
|
|
|
|
|
|
const isActiveDragItem = props.dragIndex === props.index;
|
|
|
|
const isLastActiveDragItem = props.index === props.noteCount - 1 && props.dragIndex >= props.noteCount;
|
|
|
|
|
|
|
|
const dragCursorStyle = useMemo(() => {
|
|
|
|
if (props.flow === ItemFlow.TopToBottom) {
|
|
|
|
let dragItemPosition = '';
|
|
|
|
if (isActiveDragItem) {
|
|
|
|
dragItemPosition = 'top';
|
|
|
|
} else if (isLastActiveDragItem) {
|
|
|
|
dragItemPosition = 'bottom';
|
|
|
|
}
|
|
|
|
|
|
|
|
const output: React.CSSProperties = {
|
|
|
|
width: props.itemSize.width,
|
|
|
|
display: dragItemPosition ? 'block' : 'none',
|
|
|
|
left: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (dragItemPosition === 'top') {
|
|
|
|
output.top = 0;
|
|
|
|
} else {
|
|
|
|
output.bottom = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (props.flow === ItemFlow.LeftToRight) {
|
|
|
|
let dragItemPosition = '';
|
|
|
|
if (isActiveDragItem) {
|
|
|
|
dragItemPosition = 'left';
|
|
|
|
} else if (isLastActiveDragItem) {
|
|
|
|
dragItemPosition = 'right';
|
|
|
|
}
|
|
|
|
|
|
|
|
const output: React.CSSProperties = {
|
|
|
|
height: props.itemSize.height,
|
|
|
|
display: dragItemPosition ? 'block' : 'none',
|
|
|
|
top: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (dragItemPosition === 'left') {
|
|
|
|
output.left = 0;
|
|
|
|
} else {
|
|
|
|
output.right = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new Error('Unreachable');
|
|
|
|
}, [isActiveDragItem, isLastActiveDragItem, props.flow, props.itemSize]);
|
|
|
|
|
|
|
|
return <div
|
|
|
|
id={elementId}
|
|
|
|
ref={ref}
|
|
|
|
draggable={true}
|
|
|
|
tabIndex={0}
|
|
|
|
className={className}
|
2023-09-18 18:40:36 +02:00
|
|
|
data-id={noteId}
|
2023-08-21 17:01:20 +02:00
|
|
|
onContextMenu={props.onContextMenu}
|
|
|
|
onDragStart={props.onDragStart}
|
|
|
|
onDragOver={props.onDragOver}
|
|
|
|
>
|
|
|
|
<div className="dragcursor" style={dragCursorStyle}></div>
|
|
|
|
</div>;
|
|
|
|
};
|
|
|
|
|
|
|
|
export default memo(forwardRef(NoteListItem));
|