You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-12-20 23:30:05 +02:00
scroll
This commit is contained in:
@@ -276,6 +276,7 @@ packages/app-desktop/gui/NoteList/utils/defaultListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/useItemCss.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRenderedNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -262,6 +262,7 @@ packages/app-desktop/gui/NoteList/utils/defaultListRenderer.js
|
||||
packages/app-desktop/gui/NoteList/utils/types.js
|
||||
packages/app-desktop/gui/NoteList/utils/useItemCss.js
|
||||
packages/app-desktop/gui/NoteList/utils/useRenderedNote.js
|
||||
packages/app-desktop/gui/NoteList/utils/useVisibleRange.js
|
||||
packages/app-desktop/gui/NoteListControls/NoteListControls.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/focusSearch.js
|
||||
packages/app-desktop/gui/NoteListControls/commands/index.js
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useMemo, useCallback, useState } from 'react';
|
||||
import { AppState } from '../../app.reducer';
|
||||
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
|
||||
const { connect } = require('react-redux');
|
||||
@@ -13,8 +14,11 @@ import NoteListItem from '../NoteListItem/NoteListItem';
|
||||
import useRenderedNotes from './utils/useRenderedNote';
|
||||
import useItemCss from './utils/useItemCss';
|
||||
import useOnContextMenu from '../NoteListItem/utils/useOnContextMenu';
|
||||
import useVisibleRange from './utils/useVisibleRange';
|
||||
|
||||
const NoteList = (props: Props) => {
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
|
||||
const listRenderer = defaultListRenderer;
|
||||
|
||||
if (listRenderer.flow !== ItemFlow.TopToBottom) throw new Error('Not implemented');
|
||||
@@ -23,7 +27,9 @@ const NoteList = (props: Props) => {
|
||||
return listRenderer.itemSize;
|
||||
}, [listRenderer.itemSize]);
|
||||
|
||||
const renderedNotes = useRenderedNotes(props.notes, props.selectedNoteIds, itemSize, listRenderer);
|
||||
const [startNoteIndex, endNoteIndex] = useVisibleRange(scrollTop, props.size, itemSize, props.notes.length);
|
||||
|
||||
const renderedNotes = useRenderedNotes(startNoteIndex, endNoteIndex, props.notes, props.selectedNoteIds, itemSize, listRenderer);
|
||||
|
||||
const noteItemStyle = useMemo(() => {
|
||||
return {
|
||||
@@ -74,17 +80,37 @@ const NoteList = (props: Props) => {
|
||||
props.customCss
|
||||
);
|
||||
|
||||
const onScroll = useCallback((event: any) => {
|
||||
setScrollTop(event.target.scrollTop);
|
||||
}, []);
|
||||
|
||||
const renderFiller = (key: string, height: number) => {
|
||||
return <div key={key} style={{ height: height }}></div>;
|
||||
};
|
||||
|
||||
const renderEmptyList = () => {
|
||||
if (props.notes.length) return null;
|
||||
return <div className="emptylist">{props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>;
|
||||
};
|
||||
|
||||
const renderNotes = () => {
|
||||
if (!props.notes.length) return null;
|
||||
|
||||
const output: JSX.Element[] = [];
|
||||
|
||||
for (const renderedNote of renderedNotes) {
|
||||
output.push(renderFiller('top', startNoteIndex * itemSize.height));
|
||||
|
||||
for (let i = startNoteIndex; i <= endNoteIndex; i++) {
|
||||
const note = props.notes[i];
|
||||
const renderedNote = renderedNotes[note.id];
|
||||
|
||||
output.push(
|
||||
<NoteListItem
|
||||
key={renderedNote.id}
|
||||
key={note.id}
|
||||
onClick={onNoteClick}
|
||||
onChange={listRenderer.onChange}
|
||||
noteId={renderedNote.id}
|
||||
noteHtml={renderedNote.html}
|
||||
noteId={note.id}
|
||||
noteHtml={renderedNote ? renderedNote.html : ''}
|
||||
itemSize={itemSize}
|
||||
style={noteItemStyle}
|
||||
onContextMenu={onItemContextMenu}
|
||||
@@ -92,11 +118,14 @@ const NoteList = (props: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
output.push(renderFiller('bottom', (props.notes.length - endNoteIndex - 1) * itemSize.height));
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="note-list" style={noteListStyle}>
|
||||
<div className="note-list" style={noteListStyle} onScroll={onScroll}>
|
||||
{renderEmptyList()}
|
||||
{renderNotes()}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,6 @@ import NoteListItem from '../NoteListItem';
|
||||
import CommandService from '@joplin/lib/services/CommandService';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import styled from 'styled-components';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import ItemList from '../ItemList';
|
||||
const { connect } = require('react-redux');
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
@@ -53,7 +52,7 @@ const NoteListComponent = (props: Props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [itemHeight, setItemHeight] = useState(34);
|
||||
const itemHeight = 34;
|
||||
|
||||
const focusItemIID_ = useRef<any>(null);
|
||||
const noteListRef = useRef(null);
|
||||
@@ -408,37 +407,6 @@ const NoteListComponent = (props: Props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line @seiyab/react-hooks/exhaustive-deps -- Old code before rule was applied
|
||||
useEffect(() => {
|
||||
// When a note list item is styled by userchrome.css, its height is reflected.
|
||||
// Ref. https://github.com/laurent22/joplin/pull/6542
|
||||
if (dragOverTargetNoteIndex !== null) {
|
||||
// When dragged, its height should not be considered.
|
||||
// Ref. https://github.com/laurent22/joplin/issues/6639
|
||||
return;
|
||||
}
|
||||
const noteItem = Object.values<any>(itemAnchorRefs_.current)[0]?.current;
|
||||
const actualItemHeight = noteItem?.getHeight() ?? 0;
|
||||
if (actualItemHeight >= 8) { // To avoid generating too many narrow items
|
||||
setItemHeight(actualItemHeight);
|
||||
}
|
||||
});
|
||||
|
||||
const renderEmptyList = () => {
|
||||
if (props.notes.length) return null;
|
||||
|
||||
const theme = themeStyle(props.themeId);
|
||||
const padding = 10;
|
||||
const emptyDivStyle = {
|
||||
padding: `${padding}px`,
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
fontFamily: theme.fontFamily,
|
||||
};
|
||||
return <div style={emptyDivStyle}>{props.folders.length ? _('No notes in here. Create one by clicking on "New note".') : _('There is currently no notebook. Create one by clicking on "New notebook".')}</div>;
|
||||
};
|
||||
|
||||
const renderItemList = () => {
|
||||
if (!props.notes.length) return null;
|
||||
|
||||
@@ -461,7 +429,6 @@ const NoteListComponent = (props: Props) => {
|
||||
|
||||
return (
|
||||
<StyledRoot ref={noteListRef}>
|
||||
{renderEmptyList()}
|
||||
{renderItemList()}
|
||||
</StyledRoot>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
height: 100%;
|
||||
background-color: var(--joplin-background-color3);
|
||||
border-right: 1px solid var(--joplin-divider-color);
|
||||
overflow-y: scroll;
|
||||
|
||||
> .emptylist {
|
||||
padding: 10px;
|
||||
font-size: var(--joplin-font-size);
|
||||
color: var(--joplin-color);
|
||||
background-color: var(--joplin-background-color);
|
||||
font-family: var(--joplin-font-family);
|
||||
}
|
||||
}
|
||||
|
||||
.note-list-item {
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface Props {
|
||||
resizableLayoutEventEmitter: any;
|
||||
isInsertingNotes: boolean;
|
||||
folders: FolderEntity[];
|
||||
size: any;
|
||||
size: Size;
|
||||
searches: any[];
|
||||
selectedSearchId: string;
|
||||
highlightedWords: string[];
|
||||
|
||||
@@ -4,24 +4,21 @@ import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||
import * as Mustache from 'mustache';
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
interface RenderedNote {
|
||||
id: string;
|
||||
hash: string;
|
||||
html: string;
|
||||
}
|
||||
|
||||
const useRenderedNotes = (notes: NoteEntity[], selectedNoteIds: string[], itemSize: Size, listRenderer: ListRenderer) => {
|
||||
const initialValue = notes.map(n => {
|
||||
return {
|
||||
id: n.id,
|
||||
html: '',
|
||||
};
|
||||
});
|
||||
const hashContent = (content: any) => {
|
||||
return createHash('sha1').update(JSON.stringify(content)).digest('hex');
|
||||
};
|
||||
|
||||
const [renderedNotes, setRenderedNotes] = useState<RenderedNote[]>(initialValue);
|
||||
|
||||
const prepareViewProps = async (dependencies: ListRendererDepependency[], note: NoteEntity, itemSize: Size, selected: boolean) => {
|
||||
const prepareViewProps = async (dependencies: ListRendererDepependency[], note: NoteEntity, itemSize: Size, selected: boolean) => {
|
||||
const output: any = {};
|
||||
|
||||
for (const dep of dependencies) {
|
||||
|
||||
if (dep.startsWith('note.')) {
|
||||
@@ -50,12 +47,13 @@ const useRenderedNotes = (notes: NoteEntity[], selectedNoteIds: string[], itemSi
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
};
|
||||
|
||||
const useRenderedNotes = (startNoteIndex: number, endNoteIndex: number, notes: NoteEntity[], selectedNoteIds: string[], itemSize: Size, listRenderer: ListRenderer) => {
|
||||
const [renderedNotes, setRenderedNotes] = useState<Record<string, RenderedNote>>({});
|
||||
|
||||
useAsyncEffect(async (event) => {
|
||||
const newRenderedNotes: RenderedNote[] = [];
|
||||
|
||||
for (const note of notes) {
|
||||
const renderNote = async (note: NoteEntity): Promise<void> => {
|
||||
const view = await listRenderer.onRenderNote(await prepareViewProps(
|
||||
listRenderer.dependencies,
|
||||
note,
|
||||
@@ -63,16 +61,33 @@ const useRenderedNotes = (notes: NoteEntity[], selectedNoteIds: string[], itemSi
|
||||
selectedNoteIds.includes(note.id)
|
||||
));
|
||||
|
||||
newRenderedNotes.push({
|
||||
id: note.id,
|
||||
html: Mustache.render(listRenderer.itemTemplate, view),
|
||||
});
|
||||
}
|
||||
|
||||
if (event.cancelled) return null;
|
||||
|
||||
setRenderedNotes(newRenderedNotes);
|
||||
}, [notes, selectedNoteIds, itemSize]);
|
||||
const viewHash = hashContent(view);
|
||||
|
||||
setRenderedNotes(prev => {
|
||||
if (prev[note.id] && prev[note.id].hash === viewHash) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[note.id]: {
|
||||
id: note.id,
|
||||
hash: viewHash,
|
||||
html: Mustache.render(listRenderer.itemTemplate, view),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (let i = startNoteIndex; i <= endNoteIndex; i++) {
|
||||
const note = notes[i];
|
||||
promises.push(renderNote(note));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}, [startNoteIndex, endNoteIndex, notes, selectedNoteIds, itemSize, listRenderer]);
|
||||
|
||||
return renderedNotes;
|
||||
};
|
||||
|
||||
22
packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts
Normal file
22
packages/app-desktop/gui/NoteList/utils/useVisibleRange.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Size } from '@joplin/utils/types';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const useVisibleRange = (scrollTop: number, listSize: Size, itemSize: Size, noteCount: number) => {
|
||||
const visibleItemCount = useMemo(() => {
|
||||
return Math.ceil(listSize.height / itemSize.height);
|
||||
}, [listSize.height, itemSize.height]);
|
||||
|
||||
const startNoteIndex = useMemo(() => {
|
||||
return Math.floor(scrollTop / itemSize.height);
|
||||
}, [scrollTop, itemSize.height]);
|
||||
|
||||
const endNoteIndex = useMemo(() => {
|
||||
let output = startNoteIndex + (visibleItemCount - 1);
|
||||
if (output >= noteCount) output = noteCount - 1;
|
||||
return output;
|
||||
}, [visibleItemCount, startNoteIndex, noteCount]);
|
||||
|
||||
return [startNoteIndex, endNoteIndex];
|
||||
};
|
||||
|
||||
export default useVisibleRange;
|
||||
Reference in New Issue
Block a user