From 3e313399c269598d7ad1e18d7f03ec3795827cb8 Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Sun, 9 Dec 2018 01:18:10 +0100 Subject: [PATCH] Desktop: Search within current note --- ElectronClient/app/app.js | 15 ++- ElectronClient/app/gui/NoteSearchBar.jsx | 122 ++++++++++++++++++ ElectronClient/app/gui/NoteText.jsx | 107 +++++++++++++-- ElectronClient/app/gui/note-viewer/index.html | 42 +++++- ElectronClient/app/gui/note-viewer/preload.js | 4 +- 5 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 ElectronClient/app/gui/NoteSearchBar.jsx diff --git a/ElectronClient/app/app.js b/ElectronClient/app/app.js index 7833721dd..960ec7c95 100644 --- a/ElectronClient/app/app.js +++ b/ElectronClient/app/app.js @@ -459,14 +459,27 @@ class Application extends BaseApplication { name: 'commandStartExternalEditing', }); }, + }, { + type: 'separator', + screens: ['Main'], }, { label: _('Search in all the notes'), screens: ['Main'], + accelerator: 'F6', + click: () => { + this.dispatch({ + type: 'WINDOW_COMMAND', + name: 'focus_search', + }); + }, + }, { + label: _('Search in current note'), + screens: ['Main'], accelerator: 'CommandOrControl+F', click: () => { this.dispatch({ type: 'WINDOW_COMMAND', - name: 'focus_search', + name: 'showLocalSearch', }); }, }], diff --git a/ElectronClient/app/gui/NoteSearchBar.jsx b/ElectronClient/app/gui/NoteSearchBar.jsx new file mode 100644 index 000000000..654b066b9 --- /dev/null +++ b/ElectronClient/app/gui/NoteSearchBar.jsx @@ -0,0 +1,122 @@ +const React = require('react'); +const { connect } = require('react-redux'); +const { themeStyle } = require('../theme.js'); +const { _ } = require('lib/locale.js'); + +class NoteSearchBarComponent extends React.Component { + + constructor() { + super(); + + this.state = { + query: '', + }; + + this.searchInput_change = this.searchInput_change.bind(this); + this.previousButton_click = this.previousButton_click.bind(this); + this.nextButton_click = this.nextButton_click.bind(this); + this.closeButton_click = this.closeButton_click.bind(this); + } + + style() { + const theme = themeStyle(this.props.theme); + + let style = { + root: Object.assign({}, theme.textStyle, { + backgroundColor: theme.backgroundColor, + color: theme.colorFaded, + }), + }; + + return style; + } + + componentDidMount() { + this.refs.searchInput.focus(); + } + + buttonIconComponent(iconName, clickHandler) { + const theme = themeStyle(this.props.theme); + + const searchButton = { + paddingLeft: 4, + paddingRight: 4, + paddingTop: 2, + paddingBottom: 2, + textDecoration: 'none', + marginRight: 5, + }; + + const iconStyle = { + display: 'flex', + fontSize: Math.round(theme.fontSize) * 1.2, + color: theme.color, + }; + + const icon = + + return ( + {icon} + ); + } + + searchInput_change(event) { + const query = event.currentTarget.value; + this.setState({ query: query }); + this.triggerOnChange(query); + } + + previousButton_click(event) { + if (this.props.onPrevious) this.props.onPrevious(); + } + + nextButton_click(event) { + if (this.props.onNext) this.props.onNext(); + } + + closeButton_click(event) { + if (this.props.onClose) this.props.onClose(); + } + + triggerOnChange(query) { + if (this.props.onChange) this.props.onChange(query); + } + + focus() { + this.refs.searchInput.focus(); + } + + render() { + const theme = themeStyle(this.props.theme); + + const closeButton = this.buttonIconComponent('fa-times', this.closeButton_click); + const previousButton = this.buttonIconComponent('fa-chevron-up', this.previousButton_click); + const nextButton = this.buttonIconComponent('fa-chevron-down', this.nextButton_click); + + return ( +
+
+ { closeButton } + + { nextButton } + { previousButton } +
+
+ ); + } + +} + +const mapStateToProps = (state) => { + return { + theme: state.settings.theme, + }; +}; + +const NoteSearchBar = connect(mapStateToProps, null, null, { withRef: true })(NoteSearchBarComponent); + +module.exports = NoteSearchBar; \ No newline at end of file diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index b2a8056d3..bae3cd2ec 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -25,8 +25,10 @@ const fs = require('fs-extra'); const md5 = require('md5'); const mimeUtils = require('lib/mime-utils.js').mime; const ArrayUtils = require('lib/ArrayUtils'); +const ObjectUtils = require('lib/ObjectUtils'); const urlUtils = require('lib/urlUtils'); const dialogs = require('./dialogs'); +const NoteSearchBar = require('./NoteSearchBar.min.js'); const markdownUtils = require('lib/markdownUtils'); const ExternalEditWatcher = require('lib/services/ExternalEditWatcher'); const ResourceFetcher = require('lib/services/ResourceFetcher'); @@ -46,6 +48,12 @@ class NoteTextComponent extends React.Component { constructor() { super(); + this.localSearchDefaultState = { + query: '', + selectedIndex: 0, + resultCount: 0, + }; + this.state = { note: null, noteMetadata: '', @@ -65,6 +73,8 @@ class NoteTextComponent extends React.Component { newAndNoTitleChangeNoteId: null, bodyHtml: '', lastKeys: [], + showLocalSearch: false, + localSearch: Object.assign({}, this.localSearchDefaultState), }; this.lastLoadedNoteId_ = null; @@ -75,7 +85,9 @@ class NoteTextComponent extends React.Component { this.restoreScrollTop_ = null; this.lastSetHtml_ = ''; this.lastSetMarkers_ = []; + this.lastSetMarkersOptions_ = {}; this.selectionRange_ = null; + this.noteSearchBar_ = React.createRef(); // Complicated but reliable method to get editor content height // https://github.com/ajaxorg/ace/issues/2046 @@ -214,6 +226,36 @@ class NoteTextComponent extends React.Component { this.updateHtml(this.state.note.body); } } + + this.noteSearchBar_change = (query) => { + this.setState({ localSearch: { + query: query, + selectedIndex: 0, + }}); + } + + const noteSearchBarNextPrevious = (inc) => { + const ls = Object.assign({}, this.state.localSearch); + ls.selectedIndex += inc; + if (ls.selectedIndex < 0) ls.selectedIndex = ls.resultCount - 1; + if (ls.selectedIndex >= ls.resultCount) ls.selectedIndex = 0; + + this.setState({ localSearch: ls }); + } + + this.noteSearchBar_next = () => { + noteSearchBarNextPrevious(+1); + } + + this.noteSearchBar_previous = () => { + noteSearchBarNextPrevious(-1); + } + + this.noteSearchBar_close = () => { + this.setState({ + showLocalSearch: false, + }); + } } // Note: @@ -441,8 +483,7 @@ class NoteTextComponent extends React.Component { } } - if (note) - { + if (note) { parentFolder = Folder.byId(props.folders, note.parent_id); } @@ -461,8 +502,14 @@ class NoteTextComponent extends React.Component { newState.newAndNoTitleChangeNoteId = null; } + if (!note || loadingNewNote) { + newState.showLocalSearch = false; + newState.localSearch = Object.assign({}, this.localSearchDefaultState); + } + this.lastSetHtml_ = ''; this.lastSetMarkers_ = []; + this.lastSetMarkersOptions_ = {}; this.setState(newState); @@ -565,6 +612,10 @@ class NoteTextComponent extends React.Component { const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.state.note.body); this.saveOneProperty('body', newBody); + } else if (msg === 'setMarkerCount') { + const ls = Object.assign({}, this.state.localSearch); + ls.resultCount = arg0; + this.setState({ localSearch: ls }); } else if (msg === 'percentScroll') { this.ignoreNextEditorScroll_ = true; this.setEditorPercentScroll(arg0); @@ -873,6 +924,8 @@ class NoteTextComponent extends React.Component { this.commandDateTime(); } else if (command.name === 'commandStartExternalEditing') { this.commandStartExternalEditing(); + } else if (command.name === 'showLocalSearch') { + this.commandShowLocalSearch(); } else { commandProcessed = false; } @@ -885,6 +938,19 @@ class NoteTextComponent extends React.Component { } } + commandShowLocalSearch() { + if (this.state.showLocalSearch) { + this.noteSearchBar_.current.wrappedInstance.focus(); + } else { + this.setState({ showLocalSearch: true }); + } + + this.props.dispatch({ + type: 'NOTE_VISIBLE_PANES_SET', + panes: ['editor', 'viewer'], + }); + } + async commandAttachFile(filePaths = null) { if (!filePaths) { filePaths = bridge().showOpenDialog({ @@ -1414,6 +1480,8 @@ class NoteTextComponent extends React.Component { height: 30 }; + const searchBarHeight = this.state.showLocalSearch ? 35 : 0; + let bottomRowHeight = 0; if (NOTE_TAG_BAR_FEATURE_ENABLED) { bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - tagStyle.height - tagStyle.marginBottom; @@ -1421,8 +1489,9 @@ class NoteTextComponent extends React.Component { toolbarStyle.marginBottom = 10; bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginBottom; } - + bottomRowHeight -= searchBarHeight; + const viewerStyle = { width: Math.floor(innerWidth / 2), height: bottomRowHeight, @@ -1481,12 +1550,21 @@ class NoteTextComponent extends React.Component { this.lastSetHtml_ = html; } - const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId); - const keywords = search ? Search.keywords(search.query_pattern) : []; + let keywords = []; + const markerOptions = {}; - if (htmlHasChanged || !ArrayUtils.contentEquals(this.lastSetMarkers_, keywords)) { - this.lastSetMarkers_ = []; - this.webview_.send('setMarkers', keywords); + if (this.state.showLocalSearch) { + keywords = [this.state.localSearch.query]; + markerOptions.selectedIndex = this.state.localSearch.selectedIndex; + } else { + const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId); + if (search) keywords = Search.keywords(search.query_pattern); + } + + if (htmlHasChanged || !ArrayUtils.contentEquals(this.lastSetMarkers_, keywords) || !ObjectUtils.fieldsEqual(this.lastSetMarkersOptions_, markerOptions)) { + this.lastSetMarkers_ = keywords.slice(); + this.lastSetMarkersOptions_ = Object.assign({}, markerOptions); + this.webview_.send('setMarkers', keywords, markerOptions); } } @@ -1574,6 +1652,17 @@ class NoteTextComponent extends React.Component { highlightActiveLine={false} /> + const noteSearchBarComp = !this.state.showLocalSearch ? null : ( + + ); + return (
@@ -1585,6 +1674,8 @@ class NoteTextComponent extends React.Component { { tagList } { editor } { viewer } +
+ { noteSearchBarComp }
); } diff --git a/ElectronClient/app/gui/note-viewer/index.html b/ElectronClient/app/gui/note-viewer/index.html index e7a801c41..906ea9204 100644 --- a/ElectronClient/app/gui/note-viewer/index.html +++ b/ElectronClient/app/gui/note-viewer/index.html @@ -16,6 +16,11 @@ } mark { + background: #F3B717; + color: black; + } + + .mark-selected { background: #CF3F00; color: white; } @@ -192,7 +197,10 @@ } let mark_ = null; - function setMarkers(keywords) { + let markSelectedElement_ = null; + function setMarkers(keywords, options = null) { + if (!options) options = {}; + if (!mark_) { mark_ = new Mark(document.getElementById('content'), { exclude: ['img'], @@ -200,26 +208,52 @@ }); } - mark_.mark(keywords); + mark_.unmark() + + if (markSelectedElement_) markSelectedElement_.classList.remove('mark-selected'); + + let selectedElement = null; + let elementIndex = 0; + + if (keywords.length) { + mark_.mark(keywords, { + each: (element) => { + if (!('selectedIndex' in options)) return; + + if (('selectedIndex' in options) && elementIndex === options.selectedIndex) { + markSelectedElement_ = element; + element.classList.add('mark-selected'); + selectedElement = element; + } + + elementIndex++; + } + }); + } + + ipcProxySendToHost('setMarkerCount', elementIndex); + + if (selectedElement) selectedElement.scrollIntoView(); } let markLoaded_ = false; ipc.setMarkers = (event) => { const keywords = event.keywords; + const options = event.options; if (!keywords.length && !markLoaded_) return; if (!markLoaded_) { const script = document.createElement('script'); script.onload = function() { - setMarkers(keywords); + setMarkers(keywords, options); }; script.src = '../../node_modules/mark.js/dist/mark.min.js'; document.getElementById('markScriptContainer').appendChild(script); markLoaded_ = true; } else { - setMarkers(keywords); + setMarkers(keywords, options); } } diff --git a/ElectronClient/app/gui/note-viewer/preload.js b/ElectronClient/app/gui/note-viewer/preload.js index 466008593..deceda30f 100644 --- a/ElectronClient/app/gui/note-viewer/preload.js +++ b/ElectronClient/app/gui/note-viewer/preload.js @@ -13,8 +13,8 @@ ipcRenderer.on('setPercentScroll', (event, percent) => { window.postMessage({ target: 'webview', name: 'setPercentScroll', data: { percent: percent } }, '*'); }); -ipcRenderer.on('setMarkers', (event, keywords) => { - window.postMessage({ target: 'webview', name: 'setMarkers', data: { keywords: keywords } }, '*'); +ipcRenderer.on('setMarkers', (event, keywords, options) => { + window.postMessage({ target: 'webview', name: 'setMarkers', data: { keywords: keywords, options: options } }, '*'); }); window.addEventListener('message', (event) => {