mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 20:20:21 +01:00
perf: improve list rendering performance
fundamentally, we have done 3 things: 1. move out rendering of menu per item to a global component so it renders only once 2. ditch react-virtuoso and migrate to react-window 3. make all items of predictable size and give that size to react-window List Combining all three, the performance while scrolling and initial rendering shoul improve by almost 50%.
This commit is contained in:
@@ -24,11 +24,8 @@
|
||||
"react-app-polyfill": "^1.0.6",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-modal": "^3.11.2",
|
||||
"react-placeholder": "^4.0.3",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-tiny-virtual-list": "^2.2.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-virtuoso": "^0.15.0",
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"timeago-react": "^3.0.0",
|
||||
@@ -74,4 +71,4 @@
|
||||
"last 4 edge version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,19 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<!-- for calculating 1 em size -->
|
||||
<div
|
||||
id="div"
|
||||
style="
|
||||
height: 0;
|
||||
width: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
padding: none;
|
||||
margin: none;
|
||||
box-sizing: content-box;
|
||||
"
|
||||
></div>
|
||||
<script>
|
||||
(function (document, src, libName, config) {
|
||||
var script = document.createElement("script");
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useRoutes } from "hookrouter";
|
||||
import routes from "./navigation/routes";
|
||||
import Editor from "./components/editor";
|
||||
import useMobile from "./utils/use-mobile";
|
||||
import GlobalMenuWrapper from "./components/globalmenuwrapper";
|
||||
|
||||
function App() {
|
||||
const [show, setShow] = usePersistentState("isContainerVisible", true);
|
||||
@@ -89,7 +90,7 @@ function App() {
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box id="dialogContainer" />
|
||||
<Box id="snackbarContainer" />
|
||||
<GlobalMenuWrapper />
|
||||
</Flex>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
48
apps/web/src/common/height-calculator.js
Normal file
48
apps/web/src/common/height-calculator.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const SINGLE_LINE_HEIGHT = 1.4;
|
||||
const DEFAULT_FONT_SIZE = 16;
|
||||
|
||||
function getNoteHeight(item) {
|
||||
const { title, headline } = item;
|
||||
let height = SINGLE_LINE_HEIGHT * 3;
|
||||
if (title.length > 40) height += SINGLE_LINE_HEIGHT;
|
||||
if (headline.length > 0) height += SINGLE_LINE_HEIGHT;
|
||||
if (headline.length > 60) height += SINGLE_LINE_HEIGHT;
|
||||
return height * DEFAULT_FONT_SIZE;
|
||||
}
|
||||
|
||||
function getNotebookHeight(item) {
|
||||
const { topics, description, title } = item;
|
||||
// at the minimum we will have a title and the info text
|
||||
let height = SINGLE_LINE_HEIGHT * 3; // 2.8 = 2 lines
|
||||
|
||||
if (title.length > 40) {
|
||||
height += SINGLE_LINE_HEIGHT; // title has become multiline
|
||||
}
|
||||
|
||||
if (topics.length > 1) {
|
||||
height += SINGLE_LINE_HEIGHT;
|
||||
}
|
||||
|
||||
if (description.length > 0) {
|
||||
height += SINGLE_LINE_HEIGHT;
|
||||
}
|
||||
|
||||
if (description.length > 80) {
|
||||
height += SINGLE_LINE_HEIGHT;
|
||||
}
|
||||
return height * DEFAULT_FONT_SIZE;
|
||||
}
|
||||
|
||||
function getItemHeight(item) {
|
||||
const { title } = item;
|
||||
// at the minimum we will have a title and the info text
|
||||
let height = SINGLE_LINE_HEIGHT * 3; // 2.8 = 2 lines
|
||||
|
||||
if (title.length > 40) {
|
||||
height += SINGLE_LINE_HEIGHT; // title has become multiline
|
||||
}
|
||||
|
||||
return height * DEFAULT_FONT_SIZE;
|
||||
}
|
||||
|
||||
export { getNoteHeight, getNotebookHeight, getItemHeight };
|
||||
23
apps/web/src/components/global-menu-wrapper/index.js
Normal file
23
apps/web/src/components/global-menu-wrapper/index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import Menu from "../menu";
|
||||
import useContextMenu from "../../utils/useContextMenu";
|
||||
|
||||
function GlobalMenuWrapper() {
|
||||
const [items, data, title, closeMenu] = useContextMenu();
|
||||
|
||||
return (
|
||||
<Menu
|
||||
id="globalContextMenu"
|
||||
menuItems={items}
|
||||
data={data}
|
||||
title={title}
|
||||
style={{
|
||||
position: "absolute",
|
||||
display: "none",
|
||||
zIndex: 999,
|
||||
}}
|
||||
closeMenu={closeMenu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default GlobalMenuWrapper;
|
||||
15
apps/web/src/components/group-header/index.js
Normal file
15
apps/web/src/components/group-header/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { Box, Text } from "rebass";
|
||||
|
||||
function GroupHeader(props) {
|
||||
const { title } = props;
|
||||
if (title === "Pinned") return null;
|
||||
return (
|
||||
<Box height={22} mx={2} bg="background" py={0}>
|
||||
<Text variant="heading" color="primary" fontSize="subtitle">
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
export default GroupHeader;
|
||||
@@ -1,17 +1,20 @@
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { Flex } from "rebass";
|
||||
import Button from "../button";
|
||||
import Search from "../search";
|
||||
import * as Icon from "../icons";
|
||||
import { Virtuoso as List } from "react-virtuoso";
|
||||
import { VariableSizeList as List } from "react-window";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import { useStore as useSearchStore } from "../../stores/searchstore";
|
||||
import { useStore as useSelectionStore } from "../../stores/selection-store";
|
||||
import LoginBar from "../loginbar";
|
||||
import GroupHeader from "../group-header";
|
||||
|
||||
function ListContainer(props) {
|
||||
const setSearchContext = useSearchStore((store) => store.setSearchContext);
|
||||
const shouldSelectAll = useSelectionStore((store) => store.shouldSelectAll);
|
||||
const setSelectedItems = useSelectionStore((store) => store.setSelectedItems);
|
||||
const listRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldSelectAll) setSelectedItems(props.items);
|
||||
@@ -26,6 +29,15 @@ function ListContainer(props) {
|
||||
});
|
||||
}, [setSearchContext, props.item, props.items, props.type, props.noSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.static) return;
|
||||
// whenever there is a change in items array we have to reset the size cache
|
||||
// so it can be recalculated.
|
||||
if (listRef.current) {
|
||||
listRef.current.resetAfterIndex(0, true);
|
||||
}
|
||||
}, [props.items, listRef, props.static]);
|
||||
|
||||
return (
|
||||
<Flex variant="columnFill">
|
||||
{!props.items.length && props.placeholder ? (
|
||||
@@ -40,20 +52,44 @@ function ListContainer(props) {
|
||||
{props.children
|
||||
? props.children
|
||||
: props.items.length > 0 && (
|
||||
<List
|
||||
style={{
|
||||
width: "100%",
|
||||
flex: "1 1 auto",
|
||||
height: "auto",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
totalCount={props.items.length}
|
||||
item={(index) => {
|
||||
const item = props.items[index];
|
||||
if (!item) return null;
|
||||
return props.item(index, item);
|
||||
}}
|
||||
/>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<List
|
||||
ref={listRef}
|
||||
height={height}
|
||||
width={width}
|
||||
itemKey={(index) => {
|
||||
const item = props.items[index];
|
||||
return item.id || item.title;
|
||||
}}
|
||||
overscanCount={2}
|
||||
estimatedItemSize={props.estimatedItemHeight}
|
||||
itemSize={(index) => {
|
||||
const item = props.items[index];
|
||||
if (item.type === "header") {
|
||||
if (item.title === "Pinned") return 22;
|
||||
else return 22;
|
||||
} else {
|
||||
return props.itemHeight(item);
|
||||
}
|
||||
}}
|
||||
itemCount={props.items.length}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const item = props.items[index];
|
||||
return (
|
||||
<div key={item.id} style={style}>
|
||||
{item.type === "header" ? (
|
||||
<GroupHeader title={item.title} />
|
||||
) : (
|
||||
props.item(index, item)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</List>
|
||||
)}
|
||||
</AutoSizer>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import React, { useEffect, useCallback } from "react";
|
||||
import { Flex, Box, Text } from "rebass";
|
||||
import * as Icon from "../icons";
|
||||
import Menu from "../menu";
|
||||
import {
|
||||
store as selectionStore,
|
||||
useStore as useSelectionStore,
|
||||
} from "../../stores/selection-store";
|
||||
import useContextMenu from "../../utils/useContextMenu";
|
||||
|
||||
function selectMenuItem(isSelected, toggleSelection) {
|
||||
return {
|
||||
@@ -42,14 +39,11 @@ const ItemSelector = ({ isSelected, toggleSelection }) => {
|
||||
};
|
||||
|
||||
function ListItem(props) {
|
||||
const menuId = `contextMenu-${props.item.id}`;
|
||||
const [parentRef, closeMenu, openMenu] = useContextMenu(menuId);
|
||||
const isSelectionMode = useSelectionStore((store) => store.isSelectionMode);
|
||||
const selectedItems = useSelectionStore((store) => store.selectedItems);
|
||||
const isSelected =
|
||||
selectedItems.findIndex((item) => props.item.id === item.id) > -1;
|
||||
const selectItem = useSelectionStore((store) => store.selectItem);
|
||||
const [menuItems, setMenuItems] = useState(props.menuItems);
|
||||
|
||||
const toggleSelection = useCallback(
|
||||
function toggleSelection() {
|
||||
@@ -62,24 +56,44 @@ function ListItem(props) {
|
||||
if (!isSelectionMode && isSelected) toggleSelection();
|
||||
}, [isSelectionMode, toggleSelection, isSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.selectable) {
|
||||
setMenuItems([
|
||||
selectMenuItem(isSelected, toggleSelection),
|
||||
...props.menuItems,
|
||||
]);
|
||||
}
|
||||
}, [props.menuItems, isSelected, props.selectable, toggleSelection]);
|
||||
const openContextMenu = useCallback(
|
||||
(event, withClick) => {
|
||||
let items = props.menuItems;
|
||||
if (props.selectable)
|
||||
items = [selectMenuItem(isSelected, toggleSelection), ...items];
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("globalcontextmenu", {
|
||||
detail: {
|
||||
items,
|
||||
data: props.menuData,
|
||||
internalEvent: event,
|
||||
withClick,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[
|
||||
isSelected,
|
||||
props.menuData,
|
||||
props.menuItems,
|
||||
props.selectable,
|
||||
toggleSelection,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
ref={parentRef}
|
||||
bg={props.pinned ? "shade" : "background"}
|
||||
alignItems="center"
|
||||
onContextMenu={openContextMenu}
|
||||
p={2}
|
||||
justifyContent="space-between"
|
||||
sx={{
|
||||
height: "inherit",
|
||||
borderBottom: "1px solid",
|
||||
borderBottomColor: "border",
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
":hover": {
|
||||
borderBottomColor: "primary",
|
||||
},
|
||||
@@ -91,122 +105,99 @@ function ListItem(props) {
|
||||
toggleSelection={toggleSelection}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
flex="1 1 auto"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={2}
|
||||
sx={{
|
||||
width: "full",
|
||||
position: "relative",
|
||||
marginTop: props.pinned ? 4 : 0,
|
||||
paddingTop: props.pinned ? 0 : 2,
|
||||
paddingBottom: 2,
|
||||
|
||||
//TODO add onpressed reaction
|
||||
}}
|
||||
>
|
||||
{props.pinned && (
|
||||
<Flex
|
||||
variant="rowCenter"
|
||||
bg="primary"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: -15,
|
||||
left: 0,
|
||||
borderRadius: 35,
|
||||
width: 30,
|
||||
height: 30,
|
||||
boxShadow: "2px 1px 3px #00000066",
|
||||
}}
|
||||
mx={2}
|
||||
>
|
||||
<Box
|
||||
bg="static"
|
||||
sx={{
|
||||
borderRadius: 5,
|
||||
width: 5,
|
||||
height: 5,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
<Box
|
||||
{props.pinned && (
|
||||
<Flex
|
||||
variant="rowCenter"
|
||||
bg="primary"
|
||||
onClick={() => {
|
||||
//e.stopPropagation();
|
||||
if (isSelectionMode) {
|
||||
toggleSelection();
|
||||
} else if (props.onClick) {
|
||||
props.onClick();
|
||||
}
|
||||
//TODO unpin
|
||||
}}
|
||||
sx={{
|
||||
flex: "1 1 auto",
|
||||
paddingTop: props.pinned ? 4 : 0,
|
||||
width: "90%",
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
position: "absolute",
|
||||
top: -15,
|
||||
right: 0,
|
||||
borderRadius: 35,
|
||||
width: 30,
|
||||
height: 30,
|
||||
boxShadow: "2px 1px 3px #00000066",
|
||||
}}
|
||||
mx={2}
|
||||
>
|
||||
<Text
|
||||
color={props.focused ? "primary" : "text"}
|
||||
fontFamily={"heading"}
|
||||
fontSize="title"
|
||||
fontWeight={"bold"}
|
||||
>
|
||||
{props.title}
|
||||
</Text>
|
||||
<Text
|
||||
display={props.body ? "flex" : "none"}
|
||||
variant="body"
|
||||
<Box
|
||||
bg="static"
|
||||
sx={{
|
||||
marginBottom: 1,
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
{props.body}
|
||||
</Text>
|
||||
{props.subBody && props.subBody}
|
||||
<Text
|
||||
display={props.info ? "flex" : "none"}
|
||||
variant="body"
|
||||
fontSize={11}
|
||||
color="fontTertiary"
|
||||
sx={{ marginTop: 2 }}
|
||||
>
|
||||
{props.info}
|
||||
</Text>
|
||||
</Box>
|
||||
{props.menuItems && (
|
||||
<Icon.MoreVertical
|
||||
size={22}
|
||||
color="icon"
|
||||
sx={{ marginRight: -1 }}
|
||||
onClick={(e) => {
|
||||
openMenu(e.nativeEvent, menuId, true);
|
||||
borderRadius: 5,
|
||||
width: 5,
|
||||
height: 5,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
onClick={() => {
|
||||
//e.stopPropagation();
|
||||
if (isSelectionMode) {
|
||||
toggleSelection();
|
||||
} else if (props.onClick) {
|
||||
props.onClick();
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
width: "90%",
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
as="h3"
|
||||
color={props.focused ? "primary" : "text"}
|
||||
fontFamily={"heading"}
|
||||
fontSize="title"
|
||||
fontWeight={"bold"}
|
||||
sx={{
|
||||
lineHeight: "1.4em",
|
||||
maxHeight: "2.8em", // 2 lines, i hope
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Text>
|
||||
<Text
|
||||
as="p"
|
||||
display={props.body ? "box" : "none"}
|
||||
variant="body"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
lineHeight: "1.4em",
|
||||
maxHeight: "2.8em", // 2 lines, i hope
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{props.body}
|
||||
</Text>
|
||||
{props.subBody && props.subBody}
|
||||
<Text
|
||||
display={props.info ? "flex" : "none"}
|
||||
variant="body"
|
||||
fontSize={11}
|
||||
color="fontTertiary"
|
||||
sx={{ marginTop: 2 }}
|
||||
>
|
||||
{props.info}
|
||||
</Text>
|
||||
</Flex>
|
||||
{props.menuItems &&
|
||||
ReactDOM.createPortal(
|
||||
<Menu
|
||||
id={menuId}
|
||||
menuItems={menuItems}
|
||||
data={props.menuData}
|
||||
style={{
|
||||
position: "absolute",
|
||||
display: "none",
|
||||
zIndex: 999,
|
||||
}}
|
||||
closeMenu={() => closeMenu()}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
{props.menuItems && (
|
||||
<Icon.MoreVertical
|
||||
size={22}
|
||||
color="icon"
|
||||
onClick={(event) => openContextMenu(event, true)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ class Notebook extends React.Component {
|
||||
onTopicClick(notebook, index + 1);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
key={topic.title}
|
||||
bg="primary"
|
||||
px={1}
|
||||
sx={{
|
||||
@@ -80,7 +79,7 @@ class Notebook extends React.Component {
|
||||
}}
|
||||
>
|
||||
<Text variant="body" color="static" fontSize={11}>
|
||||
{topic.title}
|
||||
{"ghghgeh"}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&family=DM+Serif+Text:ital@0;1&display=swap");
|
||||
|
||||
html,
|
||||
body {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:root,
|
||||
body,
|
||||
#root {
|
||||
|
||||
@@ -12,6 +12,7 @@ class AppStore extends BaseStore {
|
||||
isSideMenuOpen = false;
|
||||
isFocusMode = false;
|
||||
colors = [];
|
||||
globalMenu = { items: [], data: {} };
|
||||
|
||||
refresh = async () => {
|
||||
noteStore.refresh();
|
||||
@@ -31,7 +32,15 @@ class AppStore extends BaseStore {
|
||||
};
|
||||
|
||||
toggleSideMenu = (toggleState) => {
|
||||
this.set((state) => (state.isSideMenuOpen = toggleState != null ? toggleState : !state.isSideMenuOpen));
|
||||
this.set(
|
||||
(state) =>
|
||||
(state.isSideMenuOpen =
|
||||
toggleState != null ? toggleState : !state.isSideMenuOpen)
|
||||
);
|
||||
};
|
||||
|
||||
setGlobalMenu = (items, data) => {
|
||||
this.set((state) => (state.globalMenu = { items, data }));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ import BaseStore from ".";
|
||||
import { navigate } from "hookrouter";
|
||||
|
||||
class NoteStore extends BaseStore {
|
||||
notes = {
|
||||
items: [],
|
||||
groupCounts: [],
|
||||
groups: [],
|
||||
};
|
||||
notes = [];
|
||||
context = undefined;
|
||||
selectedNote = 0;
|
||||
|
||||
@@ -41,7 +37,7 @@ class NoteStore extends BaseStore {
|
||||
|
||||
refresh = () => {
|
||||
this.refreshContext();
|
||||
this.set((state) => (state.notes = db.notes.group(undefined, true)));
|
||||
this.set((state) => (state.notes = db.notes.group()));
|
||||
};
|
||||
|
||||
refreshContext = () => {
|
||||
@@ -126,12 +122,12 @@ class NoteStore extends BaseStore {
|
||||
_setValue = (noteId, prop, value) => {
|
||||
this.set((state) => {
|
||||
const { context, notes } = state;
|
||||
const arr = !context ? notes.items : context.notes;
|
||||
const arr = !context ? notes : context.notes;
|
||||
let index = arr.findIndex((note) => note.id === noteId);
|
||||
if (index < 0) return;
|
||||
|
||||
arr[index][prop] = value;
|
||||
// this._syncEditor(noteId, prop, value);
|
||||
this._syncEditor(noteId, prop, value);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
var oldOpenedMenu;
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
var isOpening = false;
|
||||
|
||||
function isMouseInside(e, element) {
|
||||
if (!e || !element) return false;
|
||||
return element.contains(e.target);
|
||||
}
|
||||
|
||||
function contextMenuHandler(event, ref, menuId) {
|
||||
if (
|
||||
isMouseInside(event, ref.current) &&
|
||||
!isMouseInside(event, oldOpenedMenu)
|
||||
) {
|
||||
dismissMenu(oldOpenedMenu);
|
||||
event.preventDefault();
|
||||
openMenu(event, menuId);
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
if (event.keyCode === 27) dismissMenu(oldOpenedMenu);
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
function closeMenu() {
|
||||
if (isOpening) {
|
||||
isOpening = false;
|
||||
return;
|
||||
}
|
||||
dismissMenu(oldOpenedMenu);
|
||||
const menu = document.getElementById("globalContextMenu");
|
||||
menu.style.display = "none";
|
||||
window.removeEventListener("click", onWindowClick);
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
function dismissMenu(menu) {
|
||||
if (menu) {
|
||||
menu.style.display = "none";
|
||||
oldOpenedMenu = undefined;
|
||||
}
|
||||
function onKeyDown(event) {
|
||||
if (event.keyCode === 27) closeMenu();
|
||||
}
|
||||
|
||||
function openMenu(event, menuId, withOnClick = false) {
|
||||
if (withOnClick) {
|
||||
function onWindowClick() {
|
||||
closeMenu();
|
||||
}
|
||||
|
||||
// updated positionMenu function
|
||||
var lastTarget;
|
||||
function openMenu(e) {
|
||||
e.preventDefault();
|
||||
const menu = document.getElementById("globalContextMenu");
|
||||
if (e.type === "click") {
|
||||
isOpening = true;
|
||||
// make it work like a toggle button
|
||||
if (menu.style.display === "block" && e.target === lastTarget) {
|
||||
closeMenu();
|
||||
return;
|
||||
}
|
||||
lastTarget = e.target;
|
||||
}
|
||||
const menu = document.getElementById(menuId);
|
||||
if (!menu) return;
|
||||
menu.style.display = "block";
|
||||
positionMenu(event, menu);
|
||||
oldOpenedMenu = menu;
|
||||
}
|
||||
|
||||
function useContextMenu(menuId) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
const parent = ref.current;
|
||||
const handler = (e) => contextMenuHandler(e, ref, menuId);
|
||||
parent.addEventListener("contextmenu", handler);
|
||||
window.onkeydown = onKeyDown;
|
||||
window.onclick = onClick;
|
||||
return () => {
|
||||
parent.removeEventListener("contextmenu", handler);
|
||||
};
|
||||
});
|
||||
return [ref, onClick, openMenu];
|
||||
const clickCoords = getPosition(e);
|
||||
const clickCoordsX = clickCoords.x;
|
||||
const clickCoordsY = clickCoords.y;
|
||||
|
||||
const menuWidth = menu.offsetWidth + 4;
|
||||
const menuHeight = menu.offsetHeight + 4;
|
||||
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (windowWidth - clickCoordsX < menuWidth) {
|
||||
menu.style.left = windowWidth - menuWidth + "px";
|
||||
} else {
|
||||
menu.style.left = clickCoordsX + "px";
|
||||
}
|
||||
|
||||
if (windowHeight - clickCoordsY < menuHeight) {
|
||||
menu.style.top = windowHeight - menuHeight + "px";
|
||||
} else {
|
||||
menu.style.top = clickCoordsY + "px";
|
||||
}
|
||||
menu.style.display = "block";
|
||||
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
window.addEventListener("click", onWindowClick);
|
||||
}
|
||||
|
||||
function getPosition(e) {
|
||||
@@ -87,29 +87,24 @@ function getPosition(e) {
|
||||
};
|
||||
}
|
||||
|
||||
// updated positionMenu function
|
||||
function positionMenu(e, menu) {
|
||||
const clickCoords = getPosition(e);
|
||||
const clickCoordsX = clickCoords.x;
|
||||
const clickCoordsY = clickCoords.y;
|
||||
|
||||
const menuWidth = menu.offsetWidth + 4;
|
||||
const menuHeight = menu.offsetHeight + 4;
|
||||
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (windowWidth - clickCoordsX < menuWidth) {
|
||||
menu.style.left = windowWidth - menuWidth + "px";
|
||||
} else {
|
||||
menu.style.left = clickCoordsX + "px";
|
||||
}
|
||||
|
||||
if (windowHeight - clickCoordsY < menuHeight) {
|
||||
menu.style.top = windowHeight - menuHeight + "px";
|
||||
} else {
|
||||
menu.style.top = clickCoordsY + "px";
|
||||
}
|
||||
function useContextMenu() {
|
||||
const [items, setItems] = useState([]);
|
||||
const [data, setData] = useState({});
|
||||
const [title, setTitle] = useState();
|
||||
useEffect(() => {
|
||||
const onGlobalContextMenu = (e) => {
|
||||
const { items, data, title, internalEvent } = e.detail;
|
||||
setItems(items);
|
||||
setData(data);
|
||||
setTitle(title);
|
||||
openMenu(internalEvent);
|
||||
};
|
||||
window.addEventListener("globalcontextmenu", onGlobalContextMenu);
|
||||
return () => {
|
||||
window.removeEventListener("globalcontextmenu", onGlobalContextMenu);
|
||||
};
|
||||
}, []);
|
||||
return [items, data, title, closeMenu];
|
||||
}
|
||||
|
||||
export default useContextMenu;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Text, Box } from "rebass";
|
||||
import { GroupedVirtuoso as GroupList } from "react-virtuoso";
|
||||
import Note from "../components/note";
|
||||
import { useStore, store } from "../stores/note-store";
|
||||
import { useStore as useEditorStore } from "../stores/editor-store";
|
||||
import ListContainer from "../components/list-container";
|
||||
import NotesPlaceholder from "../components/placeholders/notesplacholder";
|
||||
import { getNoteHeight } from "../common/height-calculator";
|
||||
|
||||
function Home() {
|
||||
useEffect(() => store.refresh(), []);
|
||||
@@ -14,42 +13,14 @@ function Home() {
|
||||
|
||||
return (
|
||||
<ListContainer
|
||||
items={notes.items}
|
||||
type="notes"
|
||||
item={(index, item) => (
|
||||
<Note index={index} pinnable={false} item={item} />
|
||||
)}
|
||||
items={notes}
|
||||
estimatedItemHeight={100}
|
||||
itemHeight={getNoteHeight}
|
||||
item={(index, item) => <Note index={index} pinnable item={item} />}
|
||||
placeholder={NotesPlaceholder}
|
||||
button={{ content: "Make a new note", onClick: () => newSession() }}
|
||||
>
|
||||
<GroupList
|
||||
style={{
|
||||
width: "100%",
|
||||
flex: "1 1 auto",
|
||||
height: "auto",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
groupCounts={notes.groupCounts}
|
||||
group={(groupIndex) => {
|
||||
if (!notes.groups[groupIndex]) return;
|
||||
return notes.groups[groupIndex].title === "Pinned" ? (
|
||||
<Box px={2} bg="background" py={1} />
|
||||
) : (
|
||||
<Box mx={2} bg="background" py={0}>
|
||||
<Text variant="heading" color="primary" fontSize="subtitle">
|
||||
{notes.groups[groupIndex].title}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
item={(index, groupIndex) => {
|
||||
if (!notes.groupCounts[groupIndex] || !notes.items[index]) return;
|
||||
return (
|
||||
<Note index={index} pinnable={true} item={notes.items[index]} />
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListContainer>
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default Home;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export const Home = require("./home").default;
|
||||
export const Notebooks = require("./notebooks").default;
|
||||
export const Notes = require("./notes.js").default;
|
||||
export const Topics = require("./topics").default;
|
||||
export const Settings = require("./settings.js").default;
|
||||
export const Trash = require("./trash").default;
|
||||
export const Account = require("./Account").default;
|
||||
export const Tags = require("./tags").default;
|
||||
export const Search = require("./search").default;
|
||||
@@ -9,6 +9,7 @@ import Notes from "./notes.js";
|
||||
import { useRoutes, navigate } from "hookrouter";
|
||||
import RouteContainer from "../components/route-container";
|
||||
import { db } from "../common";
|
||||
import { getNotebookHeight } from "../common/height-calculator";
|
||||
|
||||
const routes = {
|
||||
"/": () => (
|
||||
@@ -51,7 +52,7 @@ function NotebooksContainer() {
|
||||
return routeResult;
|
||||
}
|
||||
|
||||
function Notebooks(props) {
|
||||
function Notebooks() {
|
||||
const [open, setOpen] = useState(false);
|
||||
useEffect(() => store.refresh(), []);
|
||||
const notebooks = useStore((state) => state.notebooks);
|
||||
@@ -62,6 +63,8 @@ function Notebooks(props) {
|
||||
<ListContainer
|
||||
type="notebooks"
|
||||
items={notebooks}
|
||||
estimatedItemHeight={120}
|
||||
itemHeight={getNotebookHeight}
|
||||
item={(index, item) => (
|
||||
<Notebook
|
||||
index={index}
|
||||
|
||||
@@ -4,6 +4,7 @@ import ListContainer from "../components/list-container";
|
||||
import { useStore } from "../stores/editor-store";
|
||||
import { useStore as useNotesStore } from "../stores/note-store";
|
||||
import NotesPlaceholder from "../components/placeholders/notesplacholder";
|
||||
import { getNoteHeight } from "../common/height-calculator";
|
||||
|
||||
function Notes(props) {
|
||||
const newSession = useStore((store) => store.newSession);
|
||||
@@ -20,6 +21,8 @@ function Notes(props) {
|
||||
return (
|
||||
<ListContainer
|
||||
type="notes"
|
||||
estimatedItemHeight={100}
|
||||
itemHeight={getNoteHeight}
|
||||
items={context.notes}
|
||||
placeholder={props.placeholder || NotesPlaceholder}
|
||||
item={(index, item) => (
|
||||
|
||||
@@ -9,6 +9,7 @@ window.addEventListener("load", () => {
|
||||
if (path === "/search") window.location = "/";
|
||||
});
|
||||
|
||||
// TODO this will break for now.
|
||||
function Search() {
|
||||
const results = useStore((store) => store.results);
|
||||
const item = useStore((store) => store.item);
|
||||
|
||||
@@ -7,6 +7,7 @@ import TagsPlaceholder from "../components/placeholders/tags-placeholder";
|
||||
import Notes from "./notes";
|
||||
import { useRoutes, navigate } from "hookrouter";
|
||||
import RouteContainer from "../components/route-container";
|
||||
import { getItemHeight } from "../common/height-calculator";
|
||||
|
||||
function TagNode({ title }) {
|
||||
return (
|
||||
@@ -46,6 +47,8 @@ function Tags() {
|
||||
<ListContainer
|
||||
type="tags"
|
||||
items={tags}
|
||||
itemHeight={getItemHeight}
|
||||
estimatedItemHeight={80}
|
||||
item={(index, item) => {
|
||||
const { title, noteIds } = item;
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import ListContainer from "../components/list-container";
|
||||
import { useStore as useNbStore } from "../stores/notebook-store";
|
||||
import { showTopicDialog } from "../components/dialogs/topicdialog";
|
||||
import { navigate } from "hookrouter";
|
||||
import { getItemHeight } from "../common/height-calculator";
|
||||
|
||||
function Topics(props) {
|
||||
const { notebookId } = props;
|
||||
@@ -30,22 +31,13 @@ function Topics(props) {
|
||||
<ListContainer
|
||||
type="topics"
|
||||
items={topics}
|
||||
itemHeight={getItemHeight}
|
||||
estimatedItemHeight={80}
|
||||
item={(index, item) => (
|
||||
<Topic
|
||||
index={index}
|
||||
item={item}
|
||||
onClick={() => {
|
||||
//let topic = item;
|
||||
navigate(`/notebooks/${notebookId}/${index}`);
|
||||
/* props.navigator.navigate("notes", {
|
||||
title: props.notebook.title,
|
||||
subtitle: topic.title,
|
||||
context: {
|
||||
type: "topic",
|
||||
value: { id: props.notebook.id, topic: topic.title },
|
||||
},
|
||||
}); */
|
||||
}}
|
||||
onClick={() => navigate(`/notebooks/${notebookId}/${item.id}`)}
|
||||
/>
|
||||
)}
|
||||
placeholder={Flex}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { toTitleCase } from "../utils/string";
|
||||
import TrashPlaceholder from "../components/placeholders/trash-placeholder";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { showPermanentDeleteToast } from "../common/toasts";
|
||||
import { getNotebookHeight, getNoteHeight } from "../common/height-calculator";
|
||||
|
||||
function menuItems(item, index) {
|
||||
return [
|
||||
@@ -49,13 +50,18 @@ function Trash() {
|
||||
<ListContainer
|
||||
type="trash"
|
||||
placeholder={TrashPlaceholder}
|
||||
estimatedItemHeight={120}
|
||||
itemHeight={(item) => {
|
||||
if (item.type === "note") return getNoteHeight(item);
|
||||
else if (item.type === "notebook") return getNotebookHeight(item);
|
||||
}}
|
||||
items={items}
|
||||
item={(index, item) => (
|
||||
<ListItem
|
||||
selectable
|
||||
item={item}
|
||||
title={item.title}
|
||||
body={item.headline}
|
||||
body={item.headline || item.description}
|
||||
index={index}
|
||||
info={
|
||||
<Flex variant="rowCenter">
|
||||
|
||||
@@ -4639,11 +4639,6 @@ etag@~1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
|
||||
|
||||
event-source-polyfill@1.0.18:
|
||||
version "1.0.18"
|
||||
resolved "https://registry.yarnpkg.com/event-source-polyfill/-/event-source-polyfill-1.0.18.tgz#186488ddb81b4efce3e22f59c2fa68bd0df27c56"
|
||||
integrity sha512-4ooLMUkUYFNXjYl96twwoMkXrhwmue5aI5ayU4ORswP2b8F6bYGNsdkYqutWV77DKJLI+ZRRLvZZTSuZcUCVTA==
|
||||
|
||||
eventemitter3@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
|
||||
@@ -9004,7 +8999,7 @@ prompts@^2.0.1:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.4"
|
||||
|
||||
prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
@@ -9281,11 +9276,6 @@ react-modal@^3.11.2:
|
||||
react-lifecycles-compat "^3.0.0"
|
||||
warning "^4.0.3"
|
||||
|
||||
react-placeholder@^4.0.3:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/react-placeholder/-/react-placeholder-4.0.3.tgz#098f8ee00122bfd73f1ce47d7d99de00384ab141"
|
||||
integrity sha512-yPyqFYbh/u72p0UHsKTu19KruK/aJFP0x3YENg8ZBBjf13PoTKBQckVxlpLCfn0Rm2MaMly7IxZuRsUIQV+mrw==
|
||||
|
||||
react-scripts@3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.4.1.tgz#f551298b5c71985cc491b9acf3c8e8c0ae3ada0a"
|
||||
@@ -9346,26 +9336,11 @@ react-scripts@3.4.1:
|
||||
optionalDependencies:
|
||||
fsevents "2.1.2"
|
||||
|
||||
react-tiny-virtual-list@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-tiny-virtual-list/-/react-tiny-virtual-list-2.2.0.tgz#eafb6fcf764e4ed41150ff9752cdaad8b35edf4a"
|
||||
integrity sha512-MDiy2xyqfvkWrRiQNdHFdm36lfxmcLLKuYnUqcf9xIubML85cmYCgzBJrDsLNZ3uJQ5LEHH9BnxGKKSm8+C0Bw==
|
||||
dependencies:
|
||||
prop-types "^15.5.7"
|
||||
|
||||
react-virtualized-auto-sizer@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
|
||||
integrity sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==
|
||||
|
||||
react-virtuoso@^0.15.0:
|
||||
version "0.15.0"
|
||||
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-0.15.0.tgz#e429abb0179c3d0ff12860e984b4d3b5ea9ee735"
|
||||
integrity sha512-hyGLs/Puxju73zThFD8JXpEbdF9b0p8FpovkA6gk83EqGoveh5FJsNwFshRzrJ8UAfaStoGMrJpt53Rzj4mp2A==
|
||||
dependencies:
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
tslib "^1.10.0"
|
||||
|
||||
react-window@^1.8.5:
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1"
|
||||
@@ -9664,11 +9639,6 @@ requires-port@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resolve-cwd@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
|
||||
|
||||
Reference in New Issue
Block a user