mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-30 08:26:59 +02:00
Chore: Converted desktop Sidebar to React Hooks
This commit is contained in:
parent
597569745c
commit
e37d980453
@ -1,4 +1,5 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
import { StyledRoot, StyledAddButton, StyledShareIcon, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
|
import { StyledRoot, StyledAddButton, StyledShareIcon, StyledHeader, StyledHeaderIcon, StyledAllNotesIcon, StyledHeaderLabel, StyledListItem, StyledListItemAnchor, StyledExpandLink, StyledNoteCount, StyledSyncReportText, StyledSyncReport, StyledSynchronizeButton } from './styles';
|
||||||
import { ButtonLevel } from '../Button/Button';
|
import { ButtonLevel } from '../Button/Button';
|
||||||
import CommandService from '@joplin/lib/services/CommandService';
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
@ -23,6 +24,8 @@ import { store } from '@joplin/lib/reducer';
|
|||||||
import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService';
|
import PerFolderSortOrderService from '../../services/sortOrder/PerFolderSortOrderService';
|
||||||
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
import { getFolderCallbackUrl, getTagCallbackUrl } from '@joplin/lib/callbackUrlUtils';
|
||||||
import FolderIconBox from '../FolderIconBox';
|
import FolderIconBox from '../FolderIconBox';
|
||||||
|
import { Theme } from '@joplin/lib/themes/type';
|
||||||
|
import { RuntimeProps } from './commands/focusElementSideBar';
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
|
const shared = require('@joplin/lib/components/shared/side-menu-shared.js');
|
||||||
const { themeStyle } = require('@joplin/lib/theme');
|
const { themeStyle } = require('@joplin/lib/theme');
|
||||||
@ -50,11 +53,8 @@ interface Props {
|
|||||||
tags: any[];
|
tags: any[];
|
||||||
syncStarted: boolean;
|
syncStarted: boolean;
|
||||||
plugins: PluginStates;
|
plugins: PluginStates;
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
tagHeaderIsExpanded: boolean;
|
|
||||||
folderHeaderIsExpanded: boolean;
|
folderHeaderIsExpanded: boolean;
|
||||||
|
tagHeaderIsExpanded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commands = [
|
const commands = [
|
||||||
@ -119,56 +119,80 @@ function FolderItem(props: any) {
|
|||||||
|
|
||||||
const menuUtils = new MenuUtils(CommandService.instance());
|
const menuUtils = new MenuUtils(CommandService.instance());
|
||||||
|
|
||||||
class SidebarComponent extends React.Component<Props, State> {
|
const SidebarComponent = (props: Props) => {
|
||||||
|
|
||||||
private folderItemsOrder_: any[] = [];
|
const folderItemsOrder_ = useRef<any[]>();
|
||||||
private tagItemsOrder_: any[] = [];
|
folderItemsOrder_.current = [];
|
||||||
private rootRef: any = null;
|
const tagItemsOrder_ = useRef<any[]>();
|
||||||
private anchorItemRefs: any = {};
|
tagItemsOrder_.current = [];
|
||||||
private pluginsRef: any;
|
|
||||||
|
|
||||||
constructor(props: any) {
|
const rootRef = useRef(null);
|
||||||
super(props);
|
const anchorItemRefs = useRef<Record<string, any>>(null);
|
||||||
|
anchorItemRefs.current = {};
|
||||||
|
|
||||||
CommandService.instance().componentRegisterCommands(this, commands);
|
// This whole component is a bit of a mess and rather than passing
|
||||||
|
// a plugins prop around, not knowing how it's going to affect
|
||||||
|
// re-rendering, we just keep a ref to it. Currently that's enough
|
||||||
|
// as plugins are only accessed from context menus. However if want
|
||||||
|
// to do more complex things with plugins in the sidebar, it will
|
||||||
|
// probably have to be refactored using React Hooks first.
|
||||||
|
const pluginsRef = useRef<PluginStates>(null);
|
||||||
|
pluginsRef.current = props.plugins;
|
||||||
|
|
||||||
this.state = {
|
const getSelectedItem = useCallback(() => {
|
||||||
tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'),
|
if (props.notesParentType === 'Folder' && props.selectedFolderId) {
|
||||||
folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'),
|
return { type: 'folder', id: props.selectedFolderId };
|
||||||
|
} else if (props.notesParentType === 'Tag' && props.selectedTagId) {
|
||||||
|
return { type: 'tag', id: props.selectedTagId };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [props.notesParentType, props.selectedFolderId, props.selectedTagId]);
|
||||||
|
|
||||||
|
const getFirstAnchorItemRef = useCallback((type: string) => {
|
||||||
|
const refs = anchorItemRefs.current[type];
|
||||||
|
if (!refs) return null;
|
||||||
|
|
||||||
|
const p = type === 'folder' ? props.folders : props.tags;
|
||||||
|
const item = p && p.length ? p[0] : null;
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
return refs[item.id];
|
||||||
|
}, [anchorItemRefs, props.folders, props.tags]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const runtimeProps: RuntimeProps = {
|
||||||
|
getSelectedItem,
|
||||||
|
anchorItemRefs,
|
||||||
|
getFirstAnchorItemRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
// This whole component is a bit of a mess and rather than passing
|
CommandService.instance().componentRegisterCommands(runtimeProps, commands);
|
||||||
// a plugins prop around, not knowing how it's going to affect
|
|
||||||
// re-rendering, we just keep a ref to it. Currently that's enough
|
|
||||||
// as plugins are only accessed from context menus. However if want
|
|
||||||
// to do more complex things with plugins in the sidebar, it will
|
|
||||||
// probably have to be refactored using React Hooks first.
|
|
||||||
this.pluginsRef = React.createRef();
|
|
||||||
|
|
||||||
this.onFolderToggleClick_ = this.onFolderToggleClick_.bind(this);
|
return () => {
|
||||||
this.onKeyDown = this.onKeyDown.bind(this);
|
CommandService.instance().componentUnregisterCommands(commands);
|
||||||
this.onAllNotesClick_ = this.onAllNotesClick_.bind(this);
|
};
|
||||||
this.header_contextMenu = this.header_contextMenu.bind(this);
|
}, [
|
||||||
this.onAddFolderButtonClick = this.onAddFolderButtonClick.bind(this);
|
getSelectedItem,
|
||||||
this.folderItem_click = this.folderItem_click.bind(this);
|
anchorItemRefs,
|
||||||
this.itemContextMenu = this.itemContextMenu.bind(this);
|
getFirstAnchorItemRef,
|
||||||
}
|
]);
|
||||||
|
|
||||||
onFolderDragStart_(event: any) {
|
const onFolderDragStart_ = useCallback((event: any) => {
|
||||||
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
||||||
if (!folderId) return;
|
if (!folderId) return;
|
||||||
|
|
||||||
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
event.dataTransfer.setDragImage(new Image(), 1, 1);
|
||||||
event.dataTransfer.clearData();
|
event.dataTransfer.clearData();
|
||||||
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId]));
|
event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId]));
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
onFolderDragOver_(event: any) {
|
const onFolderDragOver_ = useCallback((event: any) => {
|
||||||
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
|
if (event.dataTransfer.types.indexOf('text/x-jop-note-ids') >= 0) event.preventDefault();
|
||||||
if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault();
|
if (event.dataTransfer.types.indexOf('text/x-jop-folder-ids') >= 0) event.preventDefault();
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
async onFolderDrop_(event: any) {
|
const onFolderDrop_ = useCallback(async (event: any) => {
|
||||||
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
||||||
const dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
if (!dt) return;
|
if (!dt) return;
|
||||||
@ -199,9 +223,9 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
logger.error(error);
|
logger.error(error);
|
||||||
alert(error.message);
|
alert(error.message);
|
||||||
}
|
}
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
async onTagDrop_(event: any) {
|
const onTagDrop_ = useCallback(async (event: any) => {
|
||||||
const tagId = event.currentTarget.getAttribute('data-tag-id');
|
const tagId = event.currentTarget.getAttribute('data-tag-id');
|
||||||
const dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
if (!dt) return;
|
if (!dt) return;
|
||||||
@ -214,22 +238,18 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
await Tag.addNote(tagId, noteIds[i]);
|
await Tag.addNote(tagId, noteIds[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
async onFolderToggleClick_(event: any) {
|
const onFolderToggleClick_ = useCallback((event: any) => {
|
||||||
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
const folderId = event.currentTarget.getAttribute('data-folder-id');
|
||||||
|
|
||||||
this.props.dispatch({
|
props.dispatch({
|
||||||
type: 'FOLDER_TOGGLE',
|
type: 'FOLDER_TOGGLE',
|
||||||
id: folderId,
|
id: folderId,
|
||||||
});
|
});
|
||||||
}
|
}, [props.dispatch]);
|
||||||
|
|
||||||
componentWillUnmount() {
|
const header_contextMenu = useCallback(async () => {
|
||||||
CommandService.instance().componentUnregisterCommands(commands);
|
|
||||||
}
|
|
||||||
|
|
||||||
async header_contextMenu() {
|
|
||||||
const menu = new Menu();
|
const menu = new Menu();
|
||||||
|
|
||||||
menu.append(
|
menu.append(
|
||||||
@ -237,9 +257,9 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
menu.popup(bridge().window());
|
menu.popup(bridge().window());
|
||||||
}
|
}, []);
|
||||||
|
|
||||||
async itemContextMenu(event: any) {
|
const itemContextMenu = useCallback(async (event: any) => {
|
||||||
const itemId = event.currentTarget.getAttribute('data-id');
|
const itemId = event.currentTarget.getAttribute('data-id');
|
||||||
if (itemId === Folder.conflictFolderId()) return;
|
if (itemId === Folder.conflictFolderId()) return;
|
||||||
|
|
||||||
@ -265,7 +285,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
|
|
||||||
let item = null;
|
let item = null;
|
||||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||||
item = BaseModel.byId(this.props.folders, itemId);
|
item = BaseModel.byId(props.folders, itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) {
|
||||||
@ -289,7 +309,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
} else if (itemType === BaseModel.TYPE_TAG) {
|
} else if (itemType === BaseModel.TYPE_TAG) {
|
||||||
await Tag.untagAll(itemId);
|
await Tag.untagAll(itemId);
|
||||||
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
} else if (itemType === BaseModel.TYPE_SEARCH) {
|
||||||
this.props.dispatch({
|
props.dispatch({
|
||||||
type: 'SEARCH_DELETE',
|
type: 'SEARCH_DELETE',
|
||||||
id: itemId,
|
id: itemId,
|
||||||
});
|
});
|
||||||
@ -314,7 +334,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
new MenuItem({
|
new MenuItem({
|
||||||
label: module.fullLabel(),
|
label: module.fullLabel(),
|
||||||
click: async () => {
|
click: async () => {
|
||||||
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId], plugins: this.pluginsRef.current });
|
await InteropServiceHelper.export(props.dispatch, module, { sourceFolderIds: [itemId], plugins: pluginsRef.current });
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -374,7 +394,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginViews = pluginUtils.viewsByType(this.pluginsRef.current, 'menuItem');
|
const pluginViews = pluginUtils.viewsByType(pluginsRef.current, 'menuItem');
|
||||||
|
|
||||||
for (const view of pluginViews) {
|
for (const view of pluginViews) {
|
||||||
const location = view.location;
|
const location = view.location;
|
||||||
@ -389,80 +409,79 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
menu.popup(bridge().window());
|
menu.popup(bridge().window());
|
||||||
}
|
}, [props.folders, props.dispatch, pluginsRef]);
|
||||||
|
|
||||||
folderItem_click(folderId: string) {
|
const folderItem_click = useCallback((folderId: string) => {
|
||||||
this.props.dispatch({
|
props.dispatch({
|
||||||
type: 'FOLDER_SELECT',
|
type: 'FOLDER_SELECT',
|
||||||
id: folderId ? folderId : null,
|
id: folderId ? folderId : null,
|
||||||
});
|
});
|
||||||
}
|
}, [props.dispatch]);
|
||||||
|
|
||||||
tagItem_click(tag: any) {
|
const tagItem_click = useCallback((tag: any) => {
|
||||||
this.props.dispatch({
|
props.dispatch({
|
||||||
type: 'TAG_SELECT',
|
type: 'TAG_SELECT',
|
||||||
id: tag ? tag.id : null,
|
id: tag ? tag.id : null,
|
||||||
});
|
});
|
||||||
}
|
}, [props.dispatch]);
|
||||||
|
|
||||||
anchorItemRef(type: string, id: string) {
|
const onHeaderClick_ = useCallback((key: string) => {
|
||||||
if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {};
|
const isExpanded = key === 'tag' ? props.tagHeaderIsExpanded : props.folderHeaderIsExpanded;
|
||||||
if (this.anchorItemRefs[type][id]) return this.anchorItemRefs[type][id];
|
Setting.setValue(key === 'tag' ? 'tagHeaderIsExpanded' : 'folderHeaderIsExpanded', !isExpanded);
|
||||||
this.anchorItemRefs[type][id] = React.createRef();
|
}, [props.folderHeaderIsExpanded, props.tagHeaderIsExpanded]);
|
||||||
return this.anchorItemRefs[type][id];
|
|
||||||
}
|
|
||||||
|
|
||||||
firstAnchorItemRef(type: string) {
|
const onAllNotesClick_ = () => {
|
||||||
const refs = this.anchorItemRefs[type];
|
props.dispatch({
|
||||||
if (!refs) return null;
|
type: 'SMART_FILTER_SELECT',
|
||||||
|
id: ALL_NOTES_FILTER_ID,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const n = `${type}s`;
|
const anchorItemRef = (type: string, id: string) => {
|
||||||
const p = this.props as any;
|
if (!anchorItemRefs.current[type]) anchorItemRefs.current[type] = {};
|
||||||
const item = p[n] && p[n].length ? p[n][0] : null;
|
if (anchorItemRefs.current[type][id]) return anchorItemRefs.current[type][id];
|
||||||
if (!item) return null;
|
anchorItemRefs.current[type][id] = React.createRef();
|
||||||
|
return anchorItemRefs.current[type][id];
|
||||||
|
};
|
||||||
|
|
||||||
return refs[item.id];
|
const renderNoteCount = (count: number) => {
|
||||||
}
|
|
||||||
|
|
||||||
renderNoteCount(count: number) {
|
|
||||||
return count ? <StyledNoteCount className="note-count-label">{count}</StyledNoteCount> : null;
|
return count ? <StyledNoteCount className="note-count-label">{count}</StyledNoteCount> : null;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderExpandIcon(isExpanded: boolean, isVisible: boolean = true) {
|
const renderExpandIcon = (theme: any, isExpanded: boolean, isVisible: boolean) => {
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
const style: any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' };
|
const style: any = { width: 16, maxWidth: 16, opacity: 0.5, fontSize: Math.round(theme.toolbarIconSize * 0.8), display: 'flex', justifyContent: 'center' };
|
||||||
if (!isVisible) style.visibility = 'hidden';
|
if (!isVisible) style.visibility = 'hidden';
|
||||||
return <i className={isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
|
return <i className={isExpanded ? 'fas fa-caret-down' : 'fas fa-caret-right'} style={style}></i>;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderAllNotesItem(selected: boolean) {
|
const renderAllNotesItem = (theme: Theme, selected: boolean) => {
|
||||||
return (
|
return (
|
||||||
<StyledListItem key="allNotesHeader" selected={selected} className={'list-item-container list-item-depth-0 all-notes'} isSpecialItem={true}>
|
<StyledListItem key="allNotesHeader" selected={selected} className={'list-item-container list-item-depth-0 all-notes'} isSpecialItem={true}>
|
||||||
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
|
<StyledExpandLink>{renderExpandIcon(theme, false, false)}</StyledExpandLink>
|
||||||
<StyledAllNotesIcon className="icon-notes"/>
|
<StyledAllNotesIcon className="icon-notes"/>
|
||||||
<StyledListItemAnchor
|
<StyledListItemAnchor
|
||||||
className="list-item"
|
className="list-item"
|
||||||
isSpecialItem={true}
|
isSpecialItem={true}
|
||||||
href="#"
|
href="#"
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onClick={this.onAllNotesClick_}
|
onClick={onAllNotesClick_}
|
||||||
>
|
>
|
||||||
{_('All notes')}
|
{_('All notes')}
|
||||||
</StyledListItemAnchor>
|
</StyledListItemAnchor>
|
||||||
</StyledListItem>
|
</StyledListItem>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderFolderItem(folder: FolderEntity, selected: boolean, hasChildren: boolean, depth: number) {
|
const renderFolderItem = (folder: FolderEntity, selected: boolean, hasChildren: boolean, depth: number) =>{
|
||||||
const anchorRef = this.anchorItemRef('folder', folder.id);
|
const anchorRef = anchorItemRef('folder', folder.id);
|
||||||
const isExpanded = this.props.collapsedFolderIds.indexOf(folder.id) < 0;
|
const isExpanded = props.collapsedFolderIds.indexOf(folder.id) < 0;
|
||||||
let noteCount = (folder as any).note_count;
|
let noteCount = (folder as any).note_count;
|
||||||
|
|
||||||
// Thunderbird count: Subtract children note_count from parent folder if it expanded.
|
// Thunderbird count: Subtract children note_count from parent folder if it expanded.
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
for (let i = 0; i < this.props.folders.length; i++) {
|
for (let i = 0; i < props.folders.length; i++) {
|
||||||
if (this.props.folders[i].parent_id === folder.id) {
|
if (props.folders[i].parent_id === folder.id) {
|
||||||
noteCount -= this.props.folders[i].note_count;
|
noteCount -= props.folders[i].note_count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -472,40 +491,40 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
folderId={folder.id}
|
folderId={folder.id}
|
||||||
folderTitle={Folder.displayTitle(folder)}
|
folderTitle={Folder.displayTitle(folder)}
|
||||||
folderIcon={Folder.unserializeIcon(folder.icon)}
|
folderIcon={Folder.unserializeIcon(folder.icon)}
|
||||||
themeId={this.props.themeId}
|
themeId={props.themeId}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
hasChildren={hasChildren}
|
hasChildren={hasChildren}
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
noteCount={noteCount}
|
noteCount={noteCount}
|
||||||
onFolderDragStart_={this.onFolderDragStart_}
|
onFolderDragStart_={onFolderDragStart_}
|
||||||
onFolderDragOver_={this.onFolderDragOver_}
|
onFolderDragOver_={onFolderDragOver_}
|
||||||
onFolderDrop_={this.onFolderDrop_}
|
onFolderDrop_={onFolderDrop_}
|
||||||
itemContextMenu={this.itemContextMenu}
|
itemContextMenu={itemContextMenu}
|
||||||
folderItem_click={this.folderItem_click}
|
folderItem_click={folderItem_click}
|
||||||
onFolderToggleClick_={this.onFolderToggleClick_}
|
onFolderToggleClick_={onFolderToggleClick_}
|
||||||
shareId={folder.share_id}
|
shareId={folder.share_id}
|
||||||
parentId={folder.parent_id}
|
parentId={folder.parent_id}
|
||||||
/>;
|
/>;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderTag(tag: any, selected: boolean) {
|
const renderTag = (tag: any, selected: boolean) => {
|
||||||
const anchorRef = this.anchorItemRef('tag', tag.id);
|
const anchorRef = anchorItemRef('tag', tag.id);
|
||||||
let noteCount = null;
|
let noteCount = null;
|
||||||
if (Setting.value('showNoteCounts')) {
|
if (Setting.value('showNoteCounts')) {
|
||||||
if (Setting.value('showCompletedTodos')) noteCount = this.renderNoteCount(tag.note_count);
|
if (Setting.value('showCompletedTodos')) noteCount = renderNoteCount(tag.note_count);
|
||||||
else noteCount = this.renderNoteCount(tag.note_count - tag.todo_completed_count);
|
else noteCount = renderNoteCount(tag.note_count - tag.todo_completed_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledListItem selected={selected}
|
<StyledListItem selected={selected}
|
||||||
className={`list-item-container ${selected ? 'selected' : ''}`}
|
className={`list-item-container ${selected ? 'selected' : ''}`}
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
onDrop={this.onTagDrop_}
|
onDrop={onTagDrop_}
|
||||||
data-tag-id={tag.id}
|
data-tag-id={tag.id}
|
||||||
>
|
>
|
||||||
<StyledExpandLink>{this.renderExpandIcon(false, false)}</StyledExpandLink>
|
<StyledExpandLink>{renderExpandIcon(theme, false, false)}</StyledExpandLink>
|
||||||
<StyledListItemAnchor
|
<StyledListItemAnchor
|
||||||
ref={anchorRef}
|
ref={anchorRef}
|
||||||
className="list-item"
|
className="list-item"
|
||||||
@ -513,9 +532,9 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
selected={selected}
|
selected={selected}
|
||||||
data-id={tag.id}
|
data-id={tag.id}
|
||||||
data-type={BaseModel.TYPE_TAG}
|
data-type={BaseModel.TYPE_TAG}
|
||||||
onContextMenu={this.itemContextMenu}
|
onContextMenu={itemContextMenu}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.tagItem_click(tag);
|
tagItem_click(tag);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="tag-label">{Tag.displayTitle(tag)}</span>
|
<span className="tag-label">{Tag.displayTitle(tag)}</span>
|
||||||
@ -523,16 +542,12 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
</StyledListItemAnchor>
|
</StyledListItemAnchor>
|
||||||
</StyledListItem>
|
</StyledListItem>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
makeDivider(key: string) {
|
const renderHeader = (key: string, label: string, iconName: string, contextMenuHandler: Function = null, onPlusButtonClick: Function = null, extraProps: any = {}) => {
|
||||||
return <div style={{ height: 2, backgroundColor: 'blue' }} key={key} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHeader(key: string, label: string, iconName: string, contextMenuHandler: Function = null, onPlusButtonClick: Function = null, extraProps: any = {}) {
|
|
||||||
const headerClick = extraProps.onClick || null;
|
const headerClick = extraProps.onClick || null;
|
||||||
delete extraProps.onClick;
|
delete extraProps.onClick;
|
||||||
const ref = this.anchorItemRef('headers', key);
|
const ref = anchorItemRef('headers', key);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
<div key={key} style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||||
@ -545,7 +560,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
if (headerClick) {
|
if (headerClick) {
|
||||||
headerClick(key, event);
|
headerClick(key, event);
|
||||||
}
|
}
|
||||||
this.onHeaderClick_(key);
|
onHeaderClick_(key);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StyledHeaderIcon className={iconName}/>
|
<StyledHeaderIcon className={iconName}/>
|
||||||
@ -554,21 +569,11 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
{ onPlusButtonClick && <StyledAddButton onClick={onPlusButtonClick} iconName="fas fa-plus" level={ButtonLevel.SidebarSecondary}/> }
|
{ onPlusButtonClick && <StyledAddButton onClick={onPlusButtonClick} iconName="fas fa-plus" level={ButtonLevel.SidebarSecondary}/> }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
selectedItem() {
|
const onKeyDown = useCallback((event: any) => {
|
||||||
if (this.props.notesParentType === 'Folder' && this.props.selectedFolderId) {
|
|
||||||
return { type: 'folder', id: this.props.selectedFolderId };
|
|
||||||
} else if (this.props.notesParentType === 'Tag' && this.props.selectedTagId) {
|
|
||||||
return { type: 'tag', id: this.props.selectedTagId };
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown(event: any) {
|
|
||||||
const keyCode = event.keyCode;
|
const keyCode = event.keyCode;
|
||||||
const selectedItem = this.selectedItem();
|
const selectedItem = getSelectedItem();
|
||||||
|
|
||||||
if (keyCode === 40 || keyCode === 38) {
|
if (keyCode === 40 || keyCode === 38) {
|
||||||
// DOWN / UP
|
// DOWN / UP
|
||||||
@ -576,14 +581,14 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
|
|
||||||
const focusItems = [];
|
const focusItems = [];
|
||||||
|
|
||||||
for (let i = 0; i < this.folderItemsOrder_.length; i++) {
|
for (let i = 0; i < folderItemsOrder_.current.length; i++) {
|
||||||
const id = this.folderItemsOrder_[i];
|
const id = folderItemsOrder_.current[i];
|
||||||
focusItems.push({ id: id, ref: this.anchorItemRefs['folder'][id], type: 'folder' });
|
focusItems.push({ id: id, ref: anchorItemRefs.current['folder'][id], type: 'folder' });
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.tagItemsOrder_.length; i++) {
|
for (let i = 0; i < tagItemsOrder_.current.length; i++) {
|
||||||
const id = this.tagItemsOrder_[i];
|
const id = tagItemsOrder_.current[i];
|
||||||
focusItems.push({ id: id, ref: this.anchorItemRefs['tag'][id], type: 'tag' });
|
focusItems.push({ id: id, ref: anchorItemRefs.current['tag'][id], type: 'tag' });
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
@ -604,7 +609,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
|
|
||||||
const actionName = `${focusItem.type.toUpperCase()}_SELECT`;
|
const actionName = `${focusItem.type.toUpperCase()}_SELECT`;
|
||||||
|
|
||||||
this.props.dispatch({
|
props.dispatch({
|
||||||
type: actionName,
|
type: actionName,
|
||||||
id: focusItem.id,
|
id: focusItem.id,
|
||||||
});
|
});
|
||||||
@ -627,7 +632,7 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
// SPACE
|
// SPACE
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.props.dispatch({
|
props.dispatch({
|
||||||
type: 'FOLDER_TOGGLE',
|
type: 'FOLDER_TOGGLE',
|
||||||
id: selectedItem.id,
|
id: selectedItem.id,
|
||||||
});
|
});
|
||||||
@ -637,24 +642,9 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
// Ctrl+A key
|
// Ctrl+A key
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}, [getSelectedItem, props.dispatch]);
|
||||||
|
|
||||||
onHeaderClick_(key: string) {
|
const renderSynchronizeButton = (type: string) => {
|
||||||
const toggleKey = `${key}IsExpanded`;
|
|
||||||
const isExpanded = (this.state as any)[toggleKey];
|
|
||||||
const newState: any = { [toggleKey]: !isExpanded };
|
|
||||||
this.setState(newState);
|
|
||||||
Setting.setValue(toggleKey, !isExpanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAllNotesClick_() {
|
|
||||||
this.props.dispatch({
|
|
||||||
type: 'SMART_FILTER_SELECT',
|
|
||||||
id: ALL_NOTES_FILTER_ID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSynchronizeButton(type: string) {
|
|
||||||
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
|
const label = type === 'sync' ? _('Synchronise') : _('Cancel');
|
||||||
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
|
const iconAnimation = type !== 'sync' ? 'icon-infinite-rotation 1s linear infinite' : '';
|
||||||
|
|
||||||
@ -670,116 +660,98 @@ class SidebarComponent extends React.Component<Props, State> {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
onAddFolderButtonClick() {
|
const onAddFolderButtonClick = useCallback(() => {
|
||||||
void CommandService.instance().execute('newFolder');
|
void CommandService.instance().execute('newFolder');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const theme = themeStyle(props.themeId);
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', header_contextMenu, onAddFolderButtonClick, {
|
||||||
|
onDrop: onFolderDrop_,
|
||||||
|
['data-folder-id']: '',
|
||||||
|
toggleblock: 1,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (props.folders.length) {
|
||||||
|
const allNotesSelected = props.notesParentType === 'SmartFilter' && props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
|
||||||
|
const result = shared.renderFolders(props, renderFolderItem);
|
||||||
|
const folderItems = [renderAllNotesItem(theme, allNotesSelected)].concat(result.items);
|
||||||
|
folderItemsOrder_.current = result.order;
|
||||||
|
items.push(
|
||||||
|
<div
|
||||||
|
className={`folders ${props.folderHeaderIsExpanded ? 'expanded' : ''}`}
|
||||||
|
key="folder_items"
|
||||||
|
style={{ display: props.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}
|
||||||
|
>
|
||||||
|
{folderItems}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// componentDidUpdate(prevProps:any, prevState:any) {
|
items.push(
|
||||||
// for (const n in prevProps) {
|
renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, {
|
||||||
// if (prevProps[n] !== (this.props as any)[n]) {
|
toggleblock: 1,
|
||||||
// console.info('CHANGED PROPS', n);
|
})
|
||||||
// }
|
);
|
||||||
// }
|
|
||||||
|
|
||||||
// for (const n in prevState) {
|
if (props.tags.length) {
|
||||||
// if (prevState[n] !== (this.state as any)[n]) {
|
const result = shared.renderTags(props, renderTag);
|
||||||
// console.info('CHANGED STATE', n);
|
const tagItems = result.items;
|
||||||
// }
|
tagItemsOrder_.current = result.order;
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.pluginsRef.current = this.props.plugins;
|
|
||||||
|
|
||||||
const theme = themeStyle(this.props.themeId);
|
|
||||||
|
|
||||||
const items = [];
|
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
this.renderHeader('folderHeader', _('Notebooks'), 'icon-notebooks', this.header_contextMenu, this.onAddFolderButtonClick, {
|
<div className="tags" key="tag_items" style={{ display: props.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
||||||
onDrop: this.onFolderDrop_,
|
{tagItems}
|
||||||
['data-folder-id']: '',
|
</div>
|
||||||
toggleblock: 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.props.folders.length) {
|
|
||||||
const allNotesSelected = this.props.notesParentType === 'SmartFilter' && this.props.selectedSmartFilterId === ALL_NOTES_FILTER_ID;
|
|
||||||
const result = shared.renderFolders(this.props, this.renderFolderItem.bind(this));
|
|
||||||
const folderItems = [this.renderAllNotesItem(allNotesSelected)].concat(result.items);
|
|
||||||
this.folderItemsOrder_ = result.order;
|
|
||||||
items.push(
|
|
||||||
<div
|
|
||||||
className={`folders ${this.state.folderHeaderIsExpanded ? 'expanded' : ''}`}
|
|
||||||
key="folder_items"
|
|
||||||
style={{ display: this.state.folderHeaderIsExpanded ? 'block' : 'none', paddingBottom: 10 }}
|
|
||||||
>
|
|
||||||
{folderItems}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
this.renderHeader('tagHeader', _('Tags'), 'icon-tags', null, null, {
|
|
||||||
toggleblock: 1,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.props.tags.length) {
|
|
||||||
const result = shared.renderTags(this.props, this.renderTag.bind(this));
|
|
||||||
const tagItems = result.items;
|
|
||||||
this.tagItemsOrder_ = result.order;
|
|
||||||
|
|
||||||
items.push(
|
|
||||||
<div className="tags" key="tag_items" style={{ display: this.state.tagHeaderIsExpanded ? 'block' : 'none' }}>
|
|
||||||
{tagItems}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let decryptionReportText = '';
|
|
||||||
if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) {
|
|
||||||
decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
let resourceFetcherText = '';
|
|
||||||
if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) {
|
|
||||||
resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = Synchronizer.reportToLines(this.props.syncReport);
|
|
||||||
if (resourceFetcherText) lines.push(resourceFetcherText);
|
|
||||||
if (decryptionReportText) lines.push(decryptionReportText);
|
|
||||||
const syncReportText = [];
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
syncReportText.push(
|
|
||||||
<StyledSyncReportText key={i}>
|
|
||||||
{lines[i]}
|
|
||||||
</StyledSyncReportText>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncButton = this.renderSynchronizeButton(this.props.syncStarted ? 'cancel' : 'sync');
|
|
||||||
|
|
||||||
const syncReportComp = !syncReportText.length ? null : (
|
|
||||||
<StyledSyncReport key="sync_report">
|
|
||||||
{syncReportText}
|
|
||||||
</StyledSyncReport>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledRoot ref={this.rootRef} onKeyDown={this.onKeyDown} className="sidebar">
|
|
||||||
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
|
|
||||||
<div style={{ flex: 0, padding: theme.mainPadding }}>
|
|
||||||
{syncReportComp}
|
|
||||||
{syncButton}
|
|
||||||
</div>
|
|
||||||
</StyledRoot>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
let decryptionReportText = '';
|
||||||
|
if (props.decryptionWorker && props.decryptionWorker.state !== 'idle' && props.decryptionWorker.itemCount) {
|
||||||
|
decryptionReportText = _('Decrypting items: %d/%d', props.decryptionWorker.itemIndex + 1, props.decryptionWorker.itemCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resourceFetcherText = '';
|
||||||
|
if (props.resourceFetcher && props.resourceFetcher.toFetchCount) {
|
||||||
|
resourceFetcherText = _('Fetching resources: %d/%d', props.resourceFetcher.fetchingCount, props.resourceFetcher.toFetchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = Synchronizer.reportToLines(props.syncReport);
|
||||||
|
if (resourceFetcherText) lines.push(resourceFetcherText);
|
||||||
|
if (decryptionReportText) lines.push(decryptionReportText);
|
||||||
|
const syncReportText = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
syncReportText.push(
|
||||||
|
<StyledSyncReportText key={i}>
|
||||||
|
{lines[i]}
|
||||||
|
</StyledSyncReportText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncButton = renderSynchronizeButton(props.syncStarted ? 'cancel' : 'sync');
|
||||||
|
|
||||||
|
const syncReportComp = !syncReportText.length ? null : (
|
||||||
|
<StyledSyncReport key="sync_report">
|
||||||
|
{syncReportText}
|
||||||
|
</StyledSyncReport>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledRoot ref={rootRef} onKeyDown={onKeyDown} className="sidebar">
|
||||||
|
<div style={{ flex: 1, overflowX: 'hidden', overflowY: 'auto' }}>{items}</div>
|
||||||
|
<div style={{ flex: 0, padding: theme.mainPadding }}>
|
||||||
|
{syncReportComp}
|
||||||
|
{syncButton}
|
||||||
|
</div>
|
||||||
|
</StyledRoot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: AppState) => {
|
const mapStateToProps = (state: AppState) => {
|
||||||
return {
|
return {
|
||||||
@ -799,6 +771,8 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
decryptionWorker: state.decryptionWorker,
|
decryptionWorker: state.decryptionWorker,
|
||||||
resourceFetcher: state.resourceFetcher,
|
resourceFetcher: state.resourceFetcher,
|
||||||
plugins: state.pluginService.plugins,
|
plugins: state.pluginService.plugins,
|
||||||
|
tagHeaderIsExpanded: state.settings.tagHeaderIsExpanded,
|
||||||
|
folderHeaderIsExpanded: state.settings.folderHeaderIsExpanded,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,18 +9,24 @@ export const declaration: CommandDeclaration = {
|
|||||||
parentLabel: () => _('Focus'),
|
parentLabel: () => _('Focus'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const runtime = (comp: any): CommandRuntime => {
|
export interface RuntimeProps {
|
||||||
|
getSelectedItem(): any;
|
||||||
|
getFirstAnchorItemRef(type: string): any;
|
||||||
|
anchorItemRefs: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runtime = (props: RuntimeProps): CommandRuntime => {
|
||||||
return {
|
return {
|
||||||
execute: async (context: CommandContext) => {
|
execute: async (context: CommandContext) => {
|
||||||
const sidebarVisible = layoutItemProp((context.state as AppState).mainLayout, 'sideBar', 'visible');
|
const sidebarVisible = layoutItemProp((context.state as AppState).mainLayout, 'sideBar', 'visible');
|
||||||
|
|
||||||
if (sidebarVisible) {
|
if (sidebarVisible) {
|
||||||
const item = comp.selectedItem();
|
const item = props.getSelectedItem();
|
||||||
if (item) {
|
if (item) {
|
||||||
const anchorRef = comp.anchorItemRefs[item.type][item.id];
|
const anchorRef = props.anchorItemRefs.current[item.type][item.id];
|
||||||
if (anchorRef) anchorRef.current.focus();
|
if (anchorRef) anchorRef.current.focus();
|
||||||
} else {
|
} else {
|
||||||
const anchorRef = comp.firstAnchorItemRef('folder');
|
const anchorRef = props.getFirstAnchorItemRef('folder');
|
||||||
if (anchorRef) anchorRef.current.focus();
|
if (anchorRef) anchorRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user