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 CommandService = require('lib/services/CommandService.js').default; 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('lib/theme'); 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'); const { ALL_NOTES_FILTER_ID } = require('lib/reserved-ids'); const commands = [ require('./commands/focusElementSideBar'), ]; class SideBarComponent extends React.Component { constructor() { super(); CommandService.instance().componentRegisterCommands(this, commands); 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.onAllNotesClick_ = this.onAllNotesClick_.bind(this); this.rootRef = React.createRef(); this.anchorItemRefs = {}; this.state = { tagHeaderIsExpanded: Setting.value('tagHeaderIsExpanded'), folderHeaderIsExpanded: Setting.value('folderHeaderIsExpanded'), }; } style() { const theme = themeStyle(this.props.theme); const itemHeight = 25; const style = { root: { backgroundColor: theme.backgroundColor2, }, listItemContainer: { boxSizing: 'border-box', height: itemHeight, display: 'flex', flexDirection: 'row', }, 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', userSelect: 'none', }, listItemSelected: { backgroundColor: theme.selectedColor2, }, listItemExpandIcon: { color: theme.color2, cursor: 'default', opacity: 0.8, fontSize: theme.fontSize, textDecoration: 'none', paddingRight: 5, display: 'flex', alignItems: 'center', width: 12, }, 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', userSelect: 'none', }, 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', userSelect: 'none', }, 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', }, noteCount: { paddingLeft: 5, opacity: 0.5, userSelect: 'none', }, }; 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; } } componentWillUnmount() { this.clearForceUpdateDuringSync(); CommandService.instance().componentUnregisterCommands(commands); } async itemContextMenu(event) { const itemId = event.currentTarget.getAttribute('data-id'); if (itemId === Folder.conflictFolderId()) return; const itemType = Number(event.currentTarget.getAttribute('data-type')); if (!itemId || !itemType) throw new Error('No data on element'); let deleteMessage = ''; let buttonLabel = _('Remove'); 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)); buttonLabel = _('Delete'); } 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); } if (itemType === BaseModel.TYPE_FOLDER && !item.encryption_applied) { menu.append( new MenuItem(CommandService.instance().commandToMenuItem('newNotebook', null, { parentId: itemId })), ); } menu.append( new MenuItem({ label: buttonLabel, click: async () => { const ok = bridge().showConfirmMessageBox(deleteMessage, { buttons: [buttonLabel, _('Cancel')], defaultId: 1, }); 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(CommandService.instance().commandToMenuItem('renameFolder', null, { folderId: 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( CommandService.instance().commandToMenuItem('renameTag', null, { tagId: 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, }); } // 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]; } firstAnchorItemRef(type) { const refs = this.anchorItemRefs[type]; if (!refs) return null; const n = `${type}s`; const item = this.props[n] && this.props[n].length ? this.props[n][0] : null; console.info('props', this.props[n], item); if (!item) return null; return refs[item.id]; } noteCountElement(count) { return