const React = require('react'); const { connect } = require('react-redux'); const shared = require('lib/components/shared/side-menu-shared.js'); const { Synchronizer } = require('lib/synchronizer.js'); const BaseModel = require('lib/BaseModel.js'); const Setting = require('lib/models/Setting.js'); const Folder = require('lib/models/Folder.js'); const Note = require('lib/models/Note.js'); const Tag = require('lib/models/Tag.js'); const { _ } = require('lib/locale.js'); const { themeStyle } = require('../theme.js'); const { bridge } = require('electron').remote.require('./bridge'); const Menu = bridge().Menu; const MenuItem = bridge().MenuItem; const InteropServiceHelper = require('../InteropServiceHelper.js'); const { substrWithEllipsis } = require('lib/string-utils'); class SideBarComponent extends React.Component { constructor() { super(); this.onFolderDragStart_ = event => { const folderId = event.currentTarget.getAttribute('folderid'); if (!folderId) return; event.dataTransfer.setDragImage(new Image(), 1, 1); event.dataTransfer.clearData(); event.dataTransfer.setData('text/x-jop-folder-ids', JSON.stringify([folderId])); }; this.onFolderDragOver_ = event => { 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(); }; this.onFolderDrop_ = async event => { const folderId = event.currentTarget.getAttribute('folderid'); const dt = event.dataTransfer; if (!dt) return; // folderId can be NULL when dropping on the sidebar Notebook header. In that case, it's used // to put the dropped folder at the root. But for notes, folderId needs to always be defined // since there's no such thing as a root note. if (dt.types.indexOf('text/x-jop-note-ids') >= 0) { event.preventDefault(); if (!folderId) return; const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids')); for (let i = 0; i < noteIds.length; i++) { await Note.moveToFolder(noteIds[i], folderId); } } else if (dt.types.indexOf('text/x-jop-folder-ids') >= 0) { event.preventDefault(); const folderIds = JSON.parse(dt.getData('text/x-jop-folder-ids')); for (let i = 0; i < folderIds.length; i++) { await Folder.moveToFolder(folderIds[i], folderId); } } }; this.onTagDrop_ = async event => { const tagId = event.currentTarget.getAttribute('tagid'); const dt = event.dataTransfer; if (!dt) return; if (dt.types.indexOf('text/x-jop-note-ids') >= 0) { event.preventDefault(); const noteIds = JSON.parse(dt.getData('text/x-jop-note-ids')); for (let i = 0; i < noteIds.length; i++) { await Tag.addNote(tagId, noteIds[i]); } } }; this.onFolderToggleClick_ = async event => { const folderId = event.currentTarget.getAttribute('folderid'); this.props.dispatch({ type: 'FOLDER_TOGGLE', id: folderId, }); }; this.folderItemsOrder_ = []; this.tagItemsOrder_ = []; this.onKeyDown = this.onKeyDown.bind(this); this.rootRef = React.createRef(); this.anchorItemRefs = {}; this.state = { tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'), folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'), }; } style(depth) { const theme = themeStyle(this.props.theme); const itemHeight = 25; let style = { root: { backgroundColor: theme.backgroundColor2, }, listItemContainer: { boxSizing: 'border-box', height: itemHeight, // paddingLeft: 14, display: 'flex', alignItems: 'stretch', // Allow 3 levels of color depth backgroundColor: theme.depthColor.replace('OPACITY', Math.min(depth * 0.1, 0.3)), }, listItem: { fontFamily: theme.fontFamily, fontSize: theme.fontSize, textDecoration: 'none', color: theme.color2, cursor: 'default', opacity: 0.8, whiteSpace: 'nowrap', display: 'flex', flex: 1, alignItems: 'center', }, listItemSelected: { backgroundColor: theme.selectedColor2, }, listItemExpandIcon: { color: theme.color2, cursor: 'default', opacity: 0.8, // fontFamily: theme.fontFamily, fontSize: theme.fontSize, textDecoration: 'none', paddingRight: 5, display: 'flex', alignItems: 'center', }, conflictFolder: { color: theme.colorError2, fontWeight: 'bold', }, header: { height: itemHeight * 1.8, fontFamily: theme.fontFamily, fontSize: theme.fontSize * 1.16, textDecoration: 'none', boxSizing: 'border-box', color: theme.color2, paddingLeft: 8, display: 'flex', alignItems: 'center', }, button: { padding: 6, fontFamily: theme.fontFamily, fontSize: theme.fontSize, textDecoration: 'none', boxSizing: 'border-box', color: theme.color2, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(255,255,255,0.2)', marginTop: 10, marginLeft: 5, marginRight: 5, cursor: 'default', }, syncReport: { fontFamily: theme.fontFamily, fontSize: Math.round(theme.fontSize * 0.9), color: theme.color2, opacity: 0.5, display: 'flex', alignItems: 'left', justifyContent: 'top', flexDirection: 'column', marginTop: 10, marginLeft: 5, marginRight: 5, marginBottom: 10, wordWrap: 'break-word', }, }; style.tagItem = Object.assign({}, style.listItem); style.tagItem.paddingLeft = 23; style.tagItem.height = itemHeight; return style; } clearForceUpdateDuringSync() { if (this.forceUpdateDuringSyncIID_) { clearInterval(this.forceUpdateDuringSyncIID_); this.forceUpdateDuringSyncIID_ = null; } } doCommand(command) { if (!command) return; let commandProcessed = true; if (command.name === 'focusElement' && command.target === 'sideBar') { if (this.props.sidebarVisibility) { const item = this.selectedItem(); if (item) { const anchorRef = this.anchorItemRefs[item.type][item.id]; if (anchorRef) anchorRef.current.focus(); } } } else if (command.name === 'synchronize') { if (!this.props.syncStarted) this.sync_click(); } else { commandProcessed = false; } if (commandProcessed) { this.props.dispatch({ type: 'WINDOW_COMMAND', name: null, }); } } componentWillUnmount() { this.clearForceUpdateDuringSync(); } componentDidUpdate(prevProps) { if (prevProps.windowCommand !== this.props.windowCommand) { this.doCommand(this.props.windowCommand); } // if (shim.isLinux()) { // // For some reason, the UI seems to sleep in some Linux distro during // // sync. Cannot find the reason for it and cannot replicate, so here // // as a test force the update at regular intervals. // // https://github.com/laurent22/joplin/issues/312#issuecomment-429472193 // if (!prevProps.syncStarted && this.props.syncStarted) { // this.clearForceUpdateDuringSync(); // this.forceUpdateDuringSyncIID_ = setInterval(() => { // this.forceUpdate(); // }, 2000); // } // if (prevProps.syncStarted && !this.props.syncStarted) this.clearForceUpdateDuringSync(); // } } async itemContextMenu(event) { const itemId = event.target.getAttribute('data-id'); if (itemId === Folder.conflictFolderId()) return; const itemType = Number(event.target.getAttribute('data-type')); if (!itemId || !itemType) throw new Error('No data on element'); let deleteMessage = ''; if (itemType === BaseModel.TYPE_FOLDER) { const folder = await Folder.load(itemId); deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32)); } else if (itemType === BaseModel.TYPE_TAG) { const tag = await Tag.load(itemId); deleteMessage = _('Remove tag "%s" from all notes?', substrWithEllipsis(tag.title, 0, 32)); } else if (itemType === BaseModel.TYPE_SEARCH) { deleteMessage = _('Remove this search from the sidebar?'); } const menu = new Menu(); let item = null; if (itemType === BaseModel.TYPE_FOLDER) { item = BaseModel.byId(this.props.folders, itemId); } menu.append( new MenuItem({ label: _('Delete'), click: async () => { const ok = bridge().showConfirmMessageBox(deleteMessage); if (!ok) return; if (itemType === BaseModel.TYPE_FOLDER) { await Folder.delete(itemId); } else if (itemType === BaseModel.TYPE_TAG) { await Tag.untagAll(itemId); } else if (itemType === BaseModel.TYPE_SEARCH) { this.props.dispatch({ type: 'SEARCH_DELETE', id: itemId, }); } }, }) ); if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { menu.append( new MenuItem({ label: _('Rename'), click: async () => { this.props.dispatch({ type: 'WINDOW_COMMAND', name: 'renameFolder', id: itemId, }); }, }) ); // menu.append( // new MenuItem({ // label: _("Move"), // click: async () => { // this.props.dispatch({ // type: "WINDOW_COMMAND", // name: "renameFolder", // id: itemId, // }); // }, // }) // ); menu.append(new MenuItem({ type: 'separator' })); const InteropService = require('lib/services/InteropService.js'); const exportMenu = new Menu(); const ioService = new InteropService(); const ioModules = ioService.modules(); for (let i = 0; i < ioModules.length; i++) { const module = ioModules[i]; if (module.type !== 'exporter') continue; exportMenu.append( new MenuItem({ label: module.fullLabel(), click: async () => { await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceFolderIds: [itemId] }); }, }) ); } menu.append( new MenuItem({ label: _('Export'), submenu: exportMenu, }) ); } if (itemType === BaseModel.TYPE_TAG) { menu.append( new MenuItem({ label: _('Rename'), click: async () => { this.props.dispatch({ type: 'WINDOW_COMMAND', name: 'renameTag', id: itemId, }); }, }) ); } menu.popup(bridge().window()); } folderItem_click(folder) { this.props.dispatch({ type: 'FOLDER_SELECT', id: folder ? folder.id : null, }); } tagItem_click(tag) { this.props.dispatch({ type: 'TAG_SELECT', id: tag ? tag.id : null, }); } // searchItem_click(search) { // this.props.dispatch({ // type: "SEARCH_SELECT", // id: search ? search.id : null, // }); // } async sync_click() { await shared.synchronize_press(this); } anchorItemRef(type, id) { if (!this.anchorItemRefs[type]) this.anchorItemRefs[type] = {}; if (this.anchorItemRefs[type][id]) return this.anchorItemRefs[type][id]; this.anchorItemRefs[type][id] = React.createRef(); return this.anchorItemRefs[type][id]; } folderItem(folder, selected, hasChildren, depth) { let style = Object.assign({}, this.style().listItem); if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder); const itemTitle = Folder.displayTitle(folder); let containerStyle = Object.assign({}, this.style(depth).listItemContainer); if (selected) containerStyle = Object.assign(containerStyle, this.style().listItemSelected); let expandLinkStyle = Object.assign({}, this.style().listItemExpandIcon); let expandIconStyle = { visibility: hasChildren ? 'visible' : 'hidden', paddingLeft: 8 + depth * 10, }; const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'fa-plus-square' : 'fa-minus-square'; const expandIcon = ; const expandLink = hasChildren ? ( {expandIcon} ) : ( {expandIcon} ); const anchorRef = this.anchorItemRef('folder', folder.id); return (
); } tagItem(tag, selected) { let style = Object.assign({}, this.style().tagItem); if (selected) style = Object.assign(style, this.style().listItemSelected); const anchorRef = this.anchorItemRef('tag', tag.id); return ( this.itemContextMenu(event)} tagid={tag.id} key={tag.id} style={style} onDrop={this.onTagDrop_} onClick={() => { this.tagItem_click(tag); }} > {Tag.displayTitle(tag)} ); } // searchItem(search, selected) { // let style = Object.assign({}, this.style().listItem); // if (selected) style = Object.assign(style, this.style().listItemSelected); // return ( // this.itemContextMenu(event)} // key={search.id} // style={style} // onClick={() => { // this.searchItem_click(search); // }} // > // {search.title} // // ); // } makeDivider(key) { return ; } makeHeader(key, label, iconName, extraProps = {}) { const style = this.style().header; const icon = ; if (extraProps.toggleblock || extraProps.onClick) { style.cursor = 'pointer'; } let headerClick = extraProps.onClick || null; delete extraProps.onClick; // check if toggling option is set. let toggleIcon = null; const toggleKey = `${key}IsExpanded`; if (extraProps.toggleblock) { let isExpanded = this.state[toggleKey]; toggleIcon = ; } const ref = this.anchorItemRef('headers', key); return (