1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-20 23:30:05 +02:00
Files
joplin/packages/app-desktop/gui/NoteList/NoteList2.tsx

282 lines
8.2 KiB
TypeScript
Raw Normal View History

2023-08-06 17:21:09 +01:00
import * as React from 'react';
2023-08-11 18:09:12 +01:00
import { useMemo, useState, useCallback, memo, useEffect } from 'react';
2023-08-06 17:21:09 +01:00
import { AppState } from '../../app.reducer';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
const { connect } = require('react-redux');
2023-08-11 18:09:12 +01:00
import { ItemFlow, ListRenderer, ListRendererDepependency, OnChangeHandler, Props } from './types';
2023-08-06 17:21:09 +01:00
import { itemIsReadOnlySync, ItemSlice } from '@joplin/lib/models/utils/readOnly';
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
import ItemChange from '@joplin/lib/models/ItemChange';
import { Size } from '@joplin/utils/types';
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
2023-08-11 11:12:13 +01:00
import defaultListRenderer from './defaultListRenderer';
2023-08-11 10:57:27 +01:00
import * as Mustache from 'mustache';
2023-08-11 18:09:12 +01:00
import { waitForElement } from '@joplin/lib/dom';
import { msleep } from '@joplin/utils/time';
2023-08-06 17:21:09 +01:00
interface RenderedNote {
id: string;
html: string;
}
2023-08-11 18:09:12 +01:00
const useRenderedNotes = (notes: NoteEntity[], selectedNoteIds: string[], itemSize: Size, listRenderer: ListRenderer) => {
2023-08-06 17:21:09 +01:00
const initialValue = notes.map(n => {
return {
id: n.id,
html: '',
};
});
const [renderedNotes, setRenderedNotes] = useState<RenderedNote[]>(initialValue);
2023-08-11 11:12:13 +01:00
const prepareViewProps = async (dependencies: ListRendererDepependency[], note: NoteEntity, itemSize: Size, selected: boolean) => {
2023-08-11 10:57:27 +01:00
const output: any = {};
for (const dep of dependencies) {
if (dep.startsWith('note.')) {
const splitted = dep.split('.');
if (splitted.length !== 2) throw new Error(`Invalid dependency name: ${dep}`);
const propName = splitted.pop();
if (!output.note) output.note = {};
if (!(propName in note)) throw new Error(`Invalid dependency name: ${dep}`);
output.note[propName] = (note as any)[propName];
}
if (dep.startsWith('item.size.')) {
const splitted = dep.split('.');
if (splitted.length !== 3) throw new Error(`Invalid dependency name: ${dep}`);
const propName = splitted.pop();
if (!output.item) output.item = {};
if (!output.item.size) output.item.size = {};
if (!(propName in itemSize)) throw new Error(`Invalid dependency name: ${dep}`);
output.item.size[propName] = (itemSize as any)[propName];
}
if (dep === 'item.selected') {
if (!output.item) output.item = {};
output.item.selected = selected;
}
}
2023-08-06 17:21:09 +01:00
2023-08-11 10:57:27 +01:00
return output;
};
2023-08-06 17:21:09 +01:00
2023-08-11 10:57:27 +01:00
useAsyncEffect(async (event) => {
const newRenderedNotes: RenderedNote[] = [];
2023-08-06 17:21:09 +01:00
for (const note of notes) {
2023-08-11 18:09:12 +01:00
const view = await listRenderer.onRenderNote(await prepareViewProps(
listRenderer.dependencies,
2023-08-11 10:57:27 +01:00
note,
itemSize,
selectedNoteIds.includes(note.id)
));
2023-08-06 17:21:09 +01:00
newRenderedNotes.push({
id: note.id,
2023-08-11 18:09:12 +01:00
html: Mustache.render(listRenderer.itemTemplate, view),
2023-08-06 17:21:09 +01:00
});
}
if (event.cancelled) return null;
setRenderedNotes(newRenderedNotes);
}, [notes, selectedNoteIds, itemSize]);
return renderedNotes;
};
interface NoteItemProps {
onClick: React.MouseEventHandler<HTMLDivElement>;
2023-08-11 18:09:12 +01:00
onChange: OnChangeHandler;
2023-08-06 17:21:09 +01:00
noteId: string;
noteHtml: string;
style: React.CSSProperties;
}
const NoteItem = memo((props: NoteItemProps) => {
2023-08-11 18:09:12 +01:00
const [rootElement, setRootElement] = useState<HTMLDivElement>(null);
const [itemElement, setItemElement] = useState<HTMLDivElement>(null);
const elementId = `list-note-${props.noteId}`;
const idPrefix = 'user-note-list-item-';
const onCheckboxChange = useCallback((event: any) => {
const internalId: string = event.currentTarget.getAttribute('id');
const userId = internalId.substring(idPrefix.length);
void props.onChange({ noteId: props.noteId }, userId, { value: event.currentTarget.checked });
}, [props.onChange, props.noteId]);
useAsyncEffect(async (event) => {
const element = await waitForElement(document, elementId);
if (event.cancelled) return;
setRootElement(element);
}, [document, elementId]);
useEffect(() => {
if (!rootElement) return () => {};
const element = document.createElement('div');
element.setAttribute('data-note-id', props.noteId);
element.className = 'note-list-item';
for (const [n, v] of Object.entries(props.style)) {
(element.style as any)[n] = v;
}
element.innerHTML = props.noteHtml;
element.addEventListener('click', props.onClick as any);
rootElement.appendChild(element);
setItemElement(element);
return () => {
// TODO: do event handlers need to be removed if the element is removed?
// element.removeEventListener('click', props.onClick as any);
element.remove();
};
}, [rootElement, props.noteHtml, props.noteId, props.style, props.onClick, onCheckboxChange]);
useAsyncEffect(async (event) => {
if (!itemElement) return;
await msleep(1);
if (event.cancelled) return;
const inputs = itemElement.getElementsByTagName('input');
const all = rootElement.getElementsByTagName('*');
for (let i = 0; i < all.length; i++) {
const e = all[i];
if (e.getAttribute('id')) {
e.setAttribute('id', idPrefix + e.getAttribute('id'));
}
}
for (const input of inputs) {
if (input.type === 'checkbox') {
input.addEventListener('change', onCheckboxChange);
}
}
}, [itemElement]);
return <div id={elementId}></div>;
2023-08-06 17:21:09 +01:00
});
2023-08-11 10:57:27 +01:00
const NoteList = (props: Props) => {
2023-08-11 18:09:12 +01:00
const listRenderer = defaultListRenderer;
if (listRenderer.flow !== ItemFlow.TopToBottom) throw new Error('Not implemented');
2023-08-06 17:21:09 +01:00
const itemSize: Size = useMemo(() => {
2023-08-11 18:09:12 +01:00
return listRenderer.itemSize;
}, [listRenderer.itemSize]);
2023-08-06 17:21:09 +01:00
2023-08-11 18:09:12 +01:00
const renderedNotes = useRenderedNotes(props.notes, props.selectedNoteIds, itemSize, listRenderer);
2023-08-06 17:21:09 +01:00
const noteItemStyle = useMemo(() => {
return {
width: 'auto',
height: itemSize.height,
};
}, [itemSize.height]);
const noteListStyle = useMemo(() => {
return {
width: props.size.width,
height: props.size.height,
};
}, [props.size]);
const onNoteClick = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
const noteId = event.currentTarget.getAttribute('data-note-id');
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
props.dispatch({
type: 'NOTE_SELECT_TOGGLE',
id: noteId,
});
} else if (event.shiftKey) {
event.preventDefault();
props.dispatch({
type: 'NOTE_SELECT_EXTEND',
id: noteId,
});
} else {
props.dispatch({
type: 'NOTE_SELECT',
id: noteId,
});
}
}, [props.dispatch]);
2023-08-11 18:09:12 +01:00
useEffect(() => {
const element = document.createElement('style');
element.setAttribute('type', 'text/css');
element.appendChild(document.createTextNode(`
.note-list-item {
${listRenderer.itemCss};
}
`));
document.head.appendChild(element);
return () => {
element.remove();
};
}, [listRenderer.itemCss]);
2023-08-06 17:21:09 +01:00
const renderNotes = () => {
const output: JSX.Element[] = [];
for (const renderedNote of renderedNotes) {
output.push(
<NoteItem
key={renderedNote.id}
onClick={onNoteClick}
2023-08-11 18:09:12 +01:00
onChange={listRenderer.onChange}
2023-08-06 17:21:09 +01:00
noteId={renderedNote.id}
noteHtml={renderedNote.html}
style={noteItemStyle}
/>
);
}
return output;
};
return (
<div className="note-list" style={noteListStyle}>
{renderNotes()}
</div>
);
};
const mapStateToProps = (state: AppState) => {
const selectedFolder: FolderEntity = state.notesParentType === 'Folder' ? BaseModel.byId(state.folders, state.selectedFolderId) : null;
const userId = state.settings['sync.userId'];
return {
notes: state.notes,
folders: state.folders,
selectedNoteIds: state.selectedNoteIds,
selectedFolderId: state.selectedFolderId,
themeId: state.settings.theme,
notesParentType: state.notesParentType,
searches: state.searches,
selectedSearchId: state.selectedSearchId,
watchedNoteFiles: state.watchedNoteFiles,
provisionalNoteIds: state.provisionalNoteIds,
isInsertingNotes: state.isInsertingNotes,
noteSortOrder: state.settings['notes.sortOrder.field'],
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
showCompletedTodos: state.settings.showCompletedTodos,
highlightedWords: state.highlightedWords,
plugins: state.pluginService.plugins,
customCss: state.customCss,
focusedField: state.focusedField,
parentFolderIsReadOnly: state.notesParentType === 'Folder' && selectedFolder ? itemIsReadOnlySync(ModelType.Folder, ItemChange.SOURCE_UNSPECIFIED, selectedFolder as ItemSlice, userId, state.shareService) : false,
};
};
2023-08-11 10:57:27 +01:00
export default connect(mapStateToProps)(NoteList);