mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Search within current note
This commit is contained in:
parent
7947e14792
commit
3e313399c2
@ -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',
|
||||
});
|
||||
},
|
||||
}],
|
||||
|
122
ElectronClient/app/gui/NoteSearchBar.jsx
Normal file
122
ElectronClient/app/gui/NoteSearchBar.jsx
Normal file
@ -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 = <i style={iconStyle} className={"fa " + iconName}></i>
|
||||
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
style={searchButton}
|
||||
onClick={clickHandler}
|
||||
>{icon}</a>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div style={this.props.style}>
|
||||
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
{ closeButton }
|
||||
<input placeholder={_('Search...')} value={this.state.query} onChange={this.searchInput_change} ref="searchInput" type="text" style={{width: 200, marginRight: 5}}></input>
|
||||
{ nextButton }
|
||||
{ previousButton }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
const NoteSearchBar = connect(mapStateToProps, null, null, { withRef: true })(NoteSearchBarComponent);
|
||||
|
||||
module.exports = NoteSearchBar;
|
@ -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 : (
|
||||
<NoteSearchBar
|
||||
ref={this.noteSearchBar_}
|
||||
style={{display: 'flex', height:searchBarHeight,width:innerWidth, borderTop: '1px solid ' + theme.dividerColor}}
|
||||
onChange={this.noteSearchBar_change}
|
||||
onNext={this.noteSearchBar_next}
|
||||
onPrevious={this.noteSearchBar_previous}
|
||||
onClose={this.noteSearchBar_close}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={rootStyle} onDrop={this.onDrop_}>
|
||||
<div style={titleBarStyle}>
|
||||
@ -1585,6 +1674,8 @@ class NoteTextComponent extends React.Component {
|
||||
{ tagList }
|
||||
{ editor }
|
||||
{ viewer }
|
||||
<div style={{clear:'both'}}/>
|
||||
{ noteSearchBarComp }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) => {
|
||||
|
Loading…
Reference in New Issue
Block a user