mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Electron: Resolves #144, Resolves #311: Highlight search results and search in real time. Associated Ctrl+F with searching.
This commit is contained in:
parent
d5c2982093
commit
67608e29c8
@ -368,11 +368,11 @@ class Application extends BaseApplication {
|
|||||||
}, {
|
}, {
|
||||||
label: _('Search in all the notes'),
|
label: _('Search in all the notes'),
|
||||||
screens: ['Main'],
|
screens: ['Main'],
|
||||||
accelerator: 'F6',
|
accelerator: 'CommandOrControl+F',
|
||||||
click: () => {
|
click: () => {
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
type: 'WINDOW_COMMAND',
|
type: 'WINDOW_COMMAND',
|
||||||
name: 'search',
|
name: 'focus_search',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
|
@ -6,6 +6,63 @@ const { _ } = require('lib/locale.js');
|
|||||||
|
|
||||||
class HeaderComponent extends React.Component {
|
class HeaderComponent extends React.Component {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
searchQuery: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.scheduleSearchChangeEventIid_ = null;
|
||||||
|
this.searchOnQuery_ = null;
|
||||||
|
this.searchElement_ = null;
|
||||||
|
|
||||||
|
const triggerOnQuery = (query) => {
|
||||||
|
clearTimeout(this.scheduleSearchChangeEventIid_);
|
||||||
|
if (this.searchOnQuery_) this.searchOnQuery_(query);
|
||||||
|
this.scheduleSearchChangeEventIid_ = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.search_onChange = (event) => {
|
||||||
|
this.setState({ searchQuery: event.target.value });
|
||||||
|
|
||||||
|
if (this.scheduleSearchChangeEventIid_) clearTimeout(this.scheduleSearchChangeEventIid_);
|
||||||
|
|
||||||
|
this.scheduleSearchChangeEventIid_ = setTimeout(() => {
|
||||||
|
triggerOnQuery(this.state.searchQuery);
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.search_onClear = (event) => {
|
||||||
|
this.setState({ searchQuery: '' });
|
||||||
|
triggerOnQuery('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentWillReceiveProps(nextProps) {
|
||||||
|
if (nextProps.windowCommand) {
|
||||||
|
this.doCommand(nextProps.windowCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async doCommand(command) {
|
||||||
|
if (!command) return;
|
||||||
|
|
||||||
|
let commandProcessed = true;
|
||||||
|
|
||||||
|
if (command.name === 'focus_search' && this.searchElement_) {
|
||||||
|
this.searchElement_.focus();
|
||||||
|
} else {
|
||||||
|
commandProcessed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandProcessed) {
|
||||||
|
this.props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
back_click() {
|
back_click() {
|
||||||
this.props.dispatch({ type: 'NAV_BACK' });
|
this.props.dispatch({ type: 'NAV_BACK' });
|
||||||
}
|
}
|
||||||
@ -40,6 +97,59 @@ class HeaderComponent extends React.Component {
|
|||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeSearch(key, style, options, state) {
|
||||||
|
const inputStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
paddingLeft: 4,
|
||||||
|
paddingRight: 4,
|
||||||
|
color: style.color,
|
||||||
|
fontSize: style.fontSize,
|
||||||
|
fontFamily: style.fontFamily,
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchButton = {
|
||||||
|
paddingLeft: 4,
|
||||||
|
paddingRight: 4,
|
||||||
|
paddingTop: 2,
|
||||||
|
paddingBottom: 2,
|
||||||
|
textDecoration: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
fontSize: Math.round(style.fontSize) * 1.2,
|
||||||
|
color: style.color,
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconName = state.searchQuery ? 'fa-times' : 'fa-search';
|
||||||
|
const icon = <i style={iconStyle} className={"fa " + iconName}></i>
|
||||||
|
if (options.onQuery) this.searchOnQuery_ = options.onQuery;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={key} style={containerStyle}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
style={inputStyle}
|
||||||
|
placeholder={options.title}
|
||||||
|
value={state.searchQuery}
|
||||||
|
onChange={this.search_onChange}
|
||||||
|
ref={elem => this.searchElement_ = elem}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
style={searchButton}
|
||||||
|
onClick={this.search_onClear}
|
||||||
|
>{icon}</a>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const style = Object.assign({}, this.props.style);
|
const style = Object.assign({}, this.props.style);
|
||||||
const theme = themeStyle(this.props.theme);
|
const theme = themeStyle(this.props.theme);
|
||||||
@ -50,9 +160,9 @@ class HeaderComponent extends React.Component {
|
|||||||
style.borderBottom = '1px solid ' + theme.dividerColor;
|
style.borderBottom = '1px solid ' + theme.dividerColor;
|
||||||
style.boxSizing = 'border-box';
|
style.boxSizing = 'border-box';
|
||||||
|
|
||||||
const buttons = [];
|
const items = [];
|
||||||
|
|
||||||
const buttonStyle = {
|
const itemStyle = {
|
||||||
height: theme.headerHeight,
|
height: theme.headerHeight,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -67,19 +177,24 @@ class HeaderComponent extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (showBackButton) {
|
if (showBackButton) {
|
||||||
buttons.push(this.makeButton('back', buttonStyle, { title: _('Back'), onClick: () => this.back_click(), iconName: 'fa-chevron-left ' }));
|
items.push(this.makeButton('back', itemStyle, { title: _('Back'), onClick: () => this.back_click(), iconName: 'fa-chevron-left ' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.buttons) {
|
if (this.props.items) {
|
||||||
for (let i = 0; i < this.props.buttons.length; i++) {
|
for (let i = 0; i < this.props.items.length; i++) {
|
||||||
const o = this.props.buttons[i];
|
const item = this.props.items[i];
|
||||||
buttons.push(this.makeButton('btn_' + i + '_' + o.title, buttonStyle, o));
|
|
||||||
|
if (item.type === 'search') {
|
||||||
|
items.push(this.makeSearch('item_' + i + '_search', itemStyle, item, this.state));
|
||||||
|
} else {
|
||||||
|
items.push(this.makeButton('item_' + i + '_' + item.title, itemStyle, item));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header" style={style}>
|
<div className="header" style={style}>
|
||||||
{ buttons }
|
{ items }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -87,7 +202,10 @@ class HeaderComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
return { theme: state.settings.theme };
|
return {
|
||||||
|
theme: state.settings.theme,
|
||||||
|
windowCommand: state.windowCommand,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header = connect(mapStateToProps)(HeaderComponent);
|
const Header = connect(mapStateToProps)(HeaderComponent);
|
||||||
|
@ -140,33 +140,27 @@ class MainScreenComponent extends React.Component {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (command.name === 'search') {
|
} else if (command.name === 'search') {
|
||||||
this.setState({
|
|
||||||
promptOptions: {
|
|
||||||
label: _('Search:'),
|
|
||||||
onClose: async (answer) => {
|
|
||||||
if (answer !== null) {
|
|
||||||
const searchId = uuid.create();
|
|
||||||
|
|
||||||
this.props.dispatch({
|
if (!this.searchId_) this.searchId_ = uuid.create();
|
||||||
type: 'SEARCH_ADD',
|
|
||||||
search: {
|
|
||||||
id: searchId,
|
|
||||||
title: answer,
|
|
||||||
query_pattern: answer,
|
|
||||||
query_folder_id: null,
|
|
||||||
type_: BaseModel.TYPE_SEARCH,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.dispatch({
|
this.props.dispatch({
|
||||||
type: 'SEARCH_SELECT',
|
type: 'SEARCH_UPDATE',
|
||||||
id: searchId,
|
search: {
|
||||||
});
|
id: this.searchId_,
|
||||||
}
|
title: command.query,
|
||||||
this.setState({ promptOptions: null });
|
query_pattern: command.query,
|
||||||
}
|
query_folder_id: null,
|
||||||
|
type_: BaseModel.TYPE_SEARCH,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (command.query) {
|
||||||
|
this.props.dispatch({
|
||||||
|
type: 'SEARCH_SELECT',
|
||||||
|
id: this.searchId_,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} else if (command.name === 'toggleVisiblePanes') {
|
} else if (command.name === 'toggleVisiblePanes') {
|
||||||
this.toggleVisiblePanes();
|
this.toggleVisiblePanes();
|
||||||
} else if (command.name === 'showModalMessage') {
|
} else if (command.name === 'showModalMessage') {
|
||||||
@ -298,41 +292,42 @@ class MainScreenComponent extends React.Component {
|
|||||||
const selectedFolderId = this.props.selectedFolderId;
|
const selectedFolderId = this.props.selectedFolderId;
|
||||||
const onConflictFolder = this.props.selectedFolderId === Folder.conflictFolderId();
|
const onConflictFolder = this.props.selectedFolderId === Folder.conflictFolderId();
|
||||||
|
|
||||||
const headerButtons = [];
|
const headerItems = [];
|
||||||
|
|
||||||
headerButtons.push({
|
headerItems.push({
|
||||||
title: _('New note'),
|
title: _('New note'),
|
||||||
iconName: 'fa-file-o',
|
iconName: 'fa-file-o',
|
||||||
enabled: !!folders.length && !onConflictFolder,
|
enabled: !!folders.length && !onConflictFolder,
|
||||||
onClick: () => { this.doCommand({ name: 'newNote' }) },
|
onClick: () => { this.doCommand({ name: 'newNote' }) },
|
||||||
});
|
});
|
||||||
|
|
||||||
headerButtons.push({
|
headerItems.push({
|
||||||
title: _('New to-do'),
|
title: _('New to-do'),
|
||||||
iconName: 'fa-check-square-o',
|
iconName: 'fa-check-square-o',
|
||||||
enabled: !!folders.length && !onConflictFolder,
|
enabled: !!folders.length && !onConflictFolder,
|
||||||
onClick: () => { this.doCommand({ name: 'newTodo' }) },
|
onClick: () => { this.doCommand({ name: 'newTodo' }) },
|
||||||
});
|
});
|
||||||
|
|
||||||
headerButtons.push({
|
headerItems.push({
|
||||||
title: _('New notebook'),
|
title: _('New notebook'),
|
||||||
iconName: 'fa-folder-o',
|
iconName: 'fa-folder-o',
|
||||||
onClick: () => { this.doCommand({ name: 'newNotebook' }) },
|
onClick: () => { this.doCommand({ name: 'newNotebook' }) },
|
||||||
});
|
});
|
||||||
|
|
||||||
headerButtons.push({
|
headerItems.push({
|
||||||
title: _('Search'),
|
|
||||||
iconName: 'fa-search',
|
|
||||||
onClick: () => { this.doCommand({ name: 'search' }) },
|
|
||||||
});
|
|
||||||
|
|
||||||
headerButtons.push({
|
|
||||||
title: _('Layout'),
|
title: _('Layout'),
|
||||||
iconName: 'fa-columns',
|
iconName: 'fa-columns',
|
||||||
enabled: !!notes.length,
|
enabled: !!notes.length,
|
||||||
onClick: () => { this.doCommand({ name: 'toggleVisiblePanes' }) },
|
onClick: () => { this.doCommand({ name: 'toggleVisiblePanes' }) },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
headerItems.push({
|
||||||
|
title: _('Search...'),
|
||||||
|
iconName: 'fa-search',
|
||||||
|
onQuery: (query) => { this.doCommand({ name: 'search', query: query }) },
|
||||||
|
type: 'search',
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.promptOnClose_) {
|
if (!this.promptOnClose_) {
|
||||||
this.promptOnClose_ = (answer, buttonType) => {
|
this.promptOnClose_ = (answer, buttonType) => {
|
||||||
return this.state.promptOptions.onClose(answer, buttonType);
|
return this.state.promptOptions.onClose(answer, buttonType);
|
||||||
@ -389,7 +384,7 @@ class MainScreenComponent extends React.Component {
|
|||||||
visible={!!this.state.promptOptions}
|
visible={!!this.state.promptOptions}
|
||||||
buttons={promptOptions && ('buttons' in promptOptions) ? promptOptions.buttons : null}
|
buttons={promptOptions && ('buttons' in promptOptions) ? promptOptions.buttons : null}
|
||||||
inputType={promptOptions && ('inputType' in promptOptions) ? promptOptions.inputType : null} />
|
inputType={promptOptions && ('inputType' in promptOptions) ? promptOptions.inputType : null} />
|
||||||
<Header style={styles.header} showBackButton={false} buttons={headerButtons} />
|
<Header style={styles.header} showBackButton={false} items={headerItems} />
|
||||||
{messageComp}
|
{messageComp}
|
||||||
<SideBar style={styles.sideBar} />
|
<SideBar style={styles.sideBar} />
|
||||||
<NoteList style={styles.noteList} />
|
<NoteList style={styles.noteList} />
|
||||||
|
@ -11,6 +11,8 @@ const MenuItem = bridge().MenuItem;
|
|||||||
const eventManager = require('../eventManager');
|
const eventManager = require('../eventManager');
|
||||||
const InteropService = require('lib/services/InteropService');
|
const InteropService = require('lib/services/InteropService');
|
||||||
const InteropServiceHelper = require('../InteropServiceHelper.js');
|
const InteropServiceHelper = require('../InteropServiceHelper.js');
|
||||||
|
const Search = require('lib/models/Search');
|
||||||
|
const Mark = require('mark.js/dist/mark.min.js');
|
||||||
|
|
||||||
class NoteListComponent extends React.Component {
|
class NoteListComponent extends React.Component {
|
||||||
|
|
||||||
@ -164,6 +166,12 @@ class NoteListComponent extends React.Component {
|
|||||||
|
|
||||||
const hPadding = 10;
|
const hPadding = 10;
|
||||||
|
|
||||||
|
let highlightedWords = [];
|
||||||
|
if (this.props.notesParentType === 'Search') {
|
||||||
|
const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||||
|
highlightedWords = search ? Search.keywords(search.query_pattern) : [];
|
||||||
|
}
|
||||||
|
|
||||||
let style = Object.assign({ width: width }, this.style().listItem);
|
let style = Object.assign({ width: width }, this.style().listItem);
|
||||||
|
|
||||||
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) {
|
if (this.props.selectedNoteIds.indexOf(item.id) >= 0) {
|
||||||
@ -182,8 +190,30 @@ class NoteListComponent extends React.Component {
|
|||||||
listItemTitleStyle.paddingLeft = !checkbox ? hPadding : 4;
|
listItemTitleStyle.paddingLeft = !checkbox ? hPadding : 4;
|
||||||
if (item.is_todo && !!item.todo_completed) listItemTitleStyle = Object.assign(listItemTitleStyle, this.style().listItemTitleCompleted);
|
if (item.is_todo && !!item.todo_completed) listItemTitleStyle = Object.assign(listItemTitleStyle, this.style().listItemTitleCompleted);
|
||||||
|
|
||||||
|
let displayTitle = Note.displayTitle(item);
|
||||||
|
let titleComp = null;
|
||||||
|
|
||||||
|
if (highlightedWords.length) {
|
||||||
|
const titleElement = document.createElement('span');
|
||||||
|
titleElement.textContent = displayTitle;
|
||||||
|
const mark = new Mark(titleElement, {
|
||||||
|
exclude: ['img'],
|
||||||
|
acrossElements: true,
|
||||||
|
});
|
||||||
|
mark.mark(highlightedWords);
|
||||||
|
|
||||||
|
// Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement
|
||||||
|
// is a span tag that we created and that contains data that's been inserted as plain text
|
||||||
|
// with `textContent` so it cannot contain any XSS attacks. We use this feature because
|
||||||
|
// mark.js can only deal with DOM elements.
|
||||||
|
// https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
|
||||||
|
titleComp = <span dangerouslySetInnerHTML={{ __html: titleElement.outerHTML }}></span>
|
||||||
|
} else {
|
||||||
|
titleComp = <span>{displayTitle}</span>
|
||||||
|
}
|
||||||
|
|
||||||
// Need to include "todo_completed" in key so that checkbox is updated when
|
// Need to include "todo_completed" in key so that checkbox is updated when
|
||||||
// item is changed via sync.
|
// item is changed via sync.
|
||||||
return <div key={item.id + '_' + item.todo_completed} style={style}>
|
return <div key={item.id + '_' + item.todo_completed} style={style}>
|
||||||
{checkbox}
|
{checkbox}
|
||||||
<a
|
<a
|
||||||
@ -196,7 +226,7 @@ class NoteListComponent extends React.Component {
|
|||||||
onDragStart={(event) => onDragStart(event) }
|
onDragStart={(event) => onDragStart(event) }
|
||||||
data-id={item.id}
|
data-id={item.id}
|
||||||
>
|
>
|
||||||
{Note.displayTitle(item)}
|
{titleComp}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -239,7 +269,9 @@ const mapStateToProps = (state) => {
|
|||||||
folders: state.folders,
|
folders: state.folders,
|
||||||
selectedNoteIds: state.selectedNoteIds,
|
selectedNoteIds: state.selectedNoteIds,
|
||||||
theme: state.settings.theme,
|
theme: state.settings.theme,
|
||||||
// uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
notesParentType: state.notesParentType,
|
||||||
|
searches: state.searches,
|
||||||
|
selectedSearchId: state.selectedSearchId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const Note = require('lib/models/Note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
|
const Search = require('lib/models/Search.js');
|
||||||
const { time } = require('lib/time-utils.js');
|
const { time } = require('lib/time-utils.js');
|
||||||
const Setting = require('lib/models/Setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
const { IconButton } = require('./IconButton.min.js');
|
const { IconButton } = require('./IconButton.min.js');
|
||||||
@ -195,7 +197,9 @@ class NoteTextComponent extends React.Component {
|
|||||||
this.editorSetScrollTop(1);
|
this.editorSetScrollTop(1);
|
||||||
this.restoreScrollTop_ = 0;
|
this.restoreScrollTop_ = 0;
|
||||||
|
|
||||||
if (note) {
|
// If a search is in progress we don't focus any field otherwise it will
|
||||||
|
// take the focus out of the search box.
|
||||||
|
if (note && this.props.notesParentType !== 'Search') {
|
||||||
const focusSettingName = !!note.is_todo ? 'newTodoFocus' : 'newNoteFocus';
|
const focusSettingName = !!note.is_todo ? 'newTodoFocus' : 'newNoteFocus';
|
||||||
|
|
||||||
if (Setting.value(focusSettingName) === 'title') {
|
if (Setting.value(focusSettingName) === 'title') {
|
||||||
@ -366,7 +370,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
webviewReady: true,
|
webviewReady: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
webview_ref(element) {
|
webview_ref(element) {
|
||||||
@ -393,6 +397,18 @@ class NoteTextComponent extends React.Component {
|
|||||||
|
|
||||||
if (this.editor_) {
|
if (this.editor_) {
|
||||||
this.editor_.editor.renderer.on('afterRender', this.onAfterEditorRender_);
|
this.editor_.editor.renderer.on('afterRender', this.onAfterEditorRender_);
|
||||||
|
|
||||||
|
const cancelledKeys = ['Ctrl+F', 'Ctrl+T', 'Ctrl+P', 'Ctrl+Q', 'Ctrl+L', 'Ctrl+,'];
|
||||||
|
for (let i = 0; i < cancelledKeys.length; i++) {
|
||||||
|
const k = cancelledKeys[i];
|
||||||
|
this.editor_.editor.commands.bindKey(k, () => {
|
||||||
|
// HACK: Ace doesn't seem to provide a way to override its shortcuts, but throwing
|
||||||
|
// an exception from this undocumented function seems to cancel it without any
|
||||||
|
// side effect.
|
||||||
|
// https://stackoverflow.com/questions/36075846
|
||||||
|
throw new Error('HACK: Overriding Ace Editor shortcut: ' + k);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -643,6 +659,10 @@ class NoteTextComponent extends React.Component {
|
|||||||
|
|
||||||
const html = this.mdToHtml().render(bodyToRender, theme, mdOptions);
|
const html = this.mdToHtml().render(bodyToRender, theme, mdOptions);
|
||||||
this.webview_.send('setHtml', html);
|
this.webview_.send('setHtml', html);
|
||||||
|
|
||||||
|
const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||||
|
const keywords = search ? Search.keywords(search.query_pattern) : [];
|
||||||
|
this.webview_.send('setMarkers', keywords);
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolbarItems = [];
|
const toolbarItems = [];
|
||||||
@ -695,6 +715,24 @@ class NoteTextComponent extends React.Component {
|
|||||||
ref={(elem) => { this.webview_ref(elem); } }
|
ref={(elem) => { this.webview_ref(elem); } }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
// const markers = [{
|
||||||
|
// startRow: 2,
|
||||||
|
// startCol: 3,
|
||||||
|
// endRow: 2,
|
||||||
|
// endCol: 6,
|
||||||
|
// type: 'text',
|
||||||
|
// className: 'test-marker'
|
||||||
|
// }];
|
||||||
|
|
||||||
|
// markers={markers}
|
||||||
|
// editorProps={{$useWorker: false}}
|
||||||
|
|
||||||
|
// #note-editor .test-marker {
|
||||||
|
// background-color: red;
|
||||||
|
// color: yellow;
|
||||||
|
// position: absolute;
|
||||||
|
// }
|
||||||
|
|
||||||
const editorRootStyle = Object.assign({}, editorStyle);
|
const editorRootStyle = Object.assign({}, editorStyle);
|
||||||
delete editorRootStyle.width;
|
delete editorRootStyle.width;
|
||||||
delete editorRootStyle.height;
|
delete editorRootStyle.height;
|
||||||
@ -721,7 +759,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
editorProps={{$blockScrolling: true}}
|
editorProps={{$blockScrolling: true}}
|
||||||
|
|
||||||
// This is buggy (gets outside the container)
|
// This is buggy (gets outside the container)
|
||||||
highlightActiveLine={false}
|
highlightActiveLine={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -751,6 +789,9 @@ const mapStateToProps = (state) => {
|
|||||||
syncStarted: state.syncStarted,
|
syncStarted: state.syncStarted,
|
||||||
newNote: state.newNote,
|
newNote: state.newNote,
|
||||||
windowCommand: state.windowCommand,
|
windowCommand: state.windowCommand,
|
||||||
|
notesParentType: state.notesParentType,
|
||||||
|
searches: state.searches,
|
||||||
|
selectedSearchId: state.selectedSearchId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -345,17 +345,17 @@ class SideBarComponent extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.searches.length) {
|
// if (this.props.searches.length) {
|
||||||
items.push(this.makeHeader("searchHeader", _("Searches"), "fa-search"));
|
// items.push(this.makeHeader("searchHeader", _("Searches"), "fa-search"));
|
||||||
|
|
||||||
const searchItems = shared.renderSearches(this.props, this.searchItem.bind(this));
|
// const searchItems = shared.renderSearches(this.props, this.searchItem.bind(this));
|
||||||
|
|
||||||
items.push(
|
// items.push(
|
||||||
<div className="searches" key="search_items">
|
// <div className="searches" key="search_items">
|
||||||
{searchItems}
|
// {searchItems}
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
let lines = Synchronizer.reportToLines(this.props.syncReport);
|
let lines = Synchronizer.reportToLines(this.props.syncReport);
|
||||||
const syncReportText = [];
|
const syncReportText = [];
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -13,12 +15,18 @@
|
|||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background: #CF3F00;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.katex { font-size: 1.3em; } /* This controls the global Katex font size*/
|
.katex { font-size: 1.3em; } /* This controls the global Katex font size*/
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="body">
|
<body id="body">
|
||||||
<div id="hlScriptContainer"></div>
|
<div id="hlScriptContainer"></div>
|
||||||
|
<div id="markScriptContainer"></div>
|
||||||
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
|
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -38,7 +46,7 @@
|
|||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.onload = function () {
|
script.onload = function () {
|
||||||
hljsLoaded = true;
|
hljsLoaded = true;
|
||||||
applyHljs();
|
applyHljs();
|
||||||
};
|
};
|
||||||
script.src = 'highlight/highlight.pack.js';
|
script.src = 'highlight/highlight.pack.js';
|
||||||
document.getElementById('hlScriptContainer').appendChild(script);
|
document.getElementById('hlScriptContainer').appendChild(script);
|
||||||
@ -160,6 +168,36 @@
|
|||||||
setPercentScroll(percent);
|
setPercentScroll(percent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mark_ = null;
|
||||||
|
function setMarkers(keywords) {
|
||||||
|
if (!mark_) {
|
||||||
|
mark_ = new Mark(document.getElementById('content'), {
|
||||||
|
exclude: ['img'],
|
||||||
|
acrossElements: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_.mark(keywords);
|
||||||
|
}
|
||||||
|
|
||||||
|
let markLoaded_ = false;
|
||||||
|
ipcRenderer.on('setMarkers', (event, keywords) => {
|
||||||
|
if (!keywords.length && !markLoaded_) return;
|
||||||
|
|
||||||
|
if (!markLoaded_) {
|
||||||
|
markLoaded_ = true;
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.onload = function() {
|
||||||
|
setMarkers(keywords);
|
||||||
|
};
|
||||||
|
|
||||||
|
script.src = '../../node_modules/mark.js/dist/mark.min.js';
|
||||||
|
document.getElementById('markScriptContainer').appendChild(script);
|
||||||
|
} else {
|
||||||
|
setMarkers(keywords);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function maxScrollTop() {
|
function maxScrollTop() {
|
||||||
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
|
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,10 @@
|
|||||||
.smalltalk .page {
|
.smalltalk .page {
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
}
|
}
|
||||||
|
mark {
|
||||||
|
background: #CF3F00;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
5
ElectronClient/app/package-lock.json
generated
5
ElectronClient/app/package-lock.json
generated
@ -3767,6 +3767,11 @@
|
|||||||
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
|
"integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"mark.js": {
|
||||||
|
"version": "8.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz",
|
||||||
|
"integrity": "sha1-GA8fnr74sOY45BZq1S24eb6y/8U="
|
||||||
|
},
|
||||||
"markdown-it": {
|
"markdown-it": {
|
||||||
"version": "8.4.0",
|
"version": "8.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.0.tgz",
|
||||||
|
@ -71,6 +71,7 @@
|
|||||||
"katex": "^0.9.0-beta1",
|
"katex": "^0.9.0-beta1",
|
||||||
"levenshtein": "^1.0.5",
|
"levenshtein": "^1.0.5",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
|
"mark.js": "^8.11.1",
|
||||||
"markdown-it": "^8.4.0",
|
"markdown-it": "^8.4.0",
|
||||||
"markdown-it-katex": "^2.0.3",
|
"markdown-it-katex": "^2.0.3",
|
||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
|
@ -260,7 +260,7 @@ class BaseApplication {
|
|||||||
const newState = store.getState();
|
const newState = store.getState();
|
||||||
let refreshNotes = false;
|
let refreshNotes = false;
|
||||||
|
|
||||||
if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE') {
|
if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
|
||||||
Setting.setValue('activeFolderId', newState.selectedFolderId);
|
Setting.setValue('activeFolderId', newState.selectedFolderId);
|
||||||
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
|
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
|
||||||
refreshNotes = true;
|
refreshNotes = true;
|
||||||
|
@ -3,6 +3,7 @@ const Entities = require('html-entities').AllHtmlEntities;
|
|||||||
const htmlentities = (new Entities()).encode;
|
const htmlentities = (new Entities()).encode;
|
||||||
const Resource = require('lib/models/Resource.js');
|
const Resource = require('lib/models/Resource.js');
|
||||||
const ModelCache = require('lib/ModelCache');
|
const ModelCache = require('lib/ModelCache');
|
||||||
|
const ObjectUtils = require('lib/ObjectUtils');
|
||||||
const { shim } = require('lib/shim.js');
|
const { shim } = require('lib/shim.js');
|
||||||
const md5 = require('md5');
|
const md5 = require('md5');
|
||||||
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
|
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
|
||||||
@ -336,14 +337,16 @@ class MdToHtml {
|
|||||||
|
|
||||||
// Insert the extra CSS at the top of the HTML
|
// Insert the extra CSS at the top of the HTML
|
||||||
|
|
||||||
const temp = ['<style>'];
|
if (!ObjectUtils.isEmpty(extraCssBlocks)) {
|
||||||
for (let n in extraCssBlocks) {
|
const temp = ['<style>'];
|
||||||
if (!extraCssBlocks.hasOwnProperty(n)) continue;
|
for (let n in extraCssBlocks) {
|
||||||
temp.push(extraCssBlocks[n]);
|
if (!extraCssBlocks.hasOwnProperty(n)) continue;
|
||||||
}
|
temp.push(extraCssBlocks[n]);
|
||||||
temp.push('</style>');
|
}
|
||||||
|
temp.push('</style>');
|
||||||
|
|
||||||
output = temp.concat(output);
|
output = temp.concat(output);
|
||||||
|
}
|
||||||
|
|
||||||
return output.join('');
|
return output.join('');
|
||||||
}
|
}
|
||||||
|
@ -53,4 +53,9 @@ ObjectUtils.convertValuesToFunctions = function(o) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ObjectUtils.isEmpty = function(o) {
|
||||||
|
if (!o) return true;
|
||||||
|
return Object.keys(o).length === 0 && o.constructor === Object;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = ObjectUtils;
|
module.exports = ObjectUtils;
|
23
ReactNativeClient/lib/models/Search.js
Normal file
23
ReactNativeClient/lib/models/Search.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
|
const Note = require('lib/models/Note.js');
|
||||||
|
|
||||||
|
class Search extends BaseModel {
|
||||||
|
|
||||||
|
static tableName() {
|
||||||
|
throw new Error('Not using database');
|
||||||
|
}
|
||||||
|
|
||||||
|
static modelType() {
|
||||||
|
return BaseModel.TYPE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
static keywords(query) {
|
||||||
|
let output = query.trim();
|
||||||
|
output = output.split(/[\s\t\n]+/);
|
||||||
|
output = output.filter(o => !!o);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Search;
|
@ -426,11 +426,35 @@ const reducer = (state = defaultState, action) => {
|
|||||||
case 'SEARCH_ADD':
|
case 'SEARCH_ADD':
|
||||||
|
|
||||||
newState = Object.assign({}, state);
|
newState = Object.assign({}, state);
|
||||||
let searches = newState.searches.slice();
|
var searches = newState.searches.slice();
|
||||||
searches.push(action.search);
|
searches.push(action.search);
|
||||||
newState.searches = searches;
|
newState.searches = searches;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'SEARCH_UPDATE':
|
||||||
|
|
||||||
|
newState = Object.assign({}, state);
|
||||||
|
var searches = newState.searches.slice();
|
||||||
|
var found = false;
|
||||||
|
for (let i = 0; i < searches.length; i++) {
|
||||||
|
if (searches[i].id === action.search.id) {
|
||||||
|
searches[i] = Object.assign({}, action.search);
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) searches.push(action.search);
|
||||||
|
|
||||||
|
if (!action.search.query_pattern) {
|
||||||
|
newState.notesParentType = defaultNotesParentType(state, 'Search');
|
||||||
|
} else {
|
||||||
|
newState.notesParentType = 'Search';
|
||||||
|
}
|
||||||
|
|
||||||
|
newState.searches = searches;
|
||||||
|
break;
|
||||||
|
|
||||||
case 'SEARCH_DELETE':
|
case 'SEARCH_DELETE':
|
||||||
|
|
||||||
newState = handleItemDelete(state, action);
|
newState = handleItemDelete(state, action);
|
||||||
|
Loading…
Reference in New Issue
Block a user