mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Desktop: Added Goto Anything dialog and implemented basic plugin system
This commit is contained in:
parent
db04906416
commit
6b2910c3c7
@ -265,6 +265,7 @@ describe('services_SearchEngine', function() {
|
||||
['title:abcd efgh', { _: ['efgh'], title: ['abcd'] }],
|
||||
['title:abcd', { title: ['abcd'] }],
|
||||
['"abcd efgh"', { _: ['abcd efgh'] }],
|
||||
['title:abcd title:efgh', { title: ['abcd', 'efgh'] }],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
@ -283,33 +284,6 @@ describe('services_SearchEngine', function() {
|
||||
}
|
||||
}));
|
||||
|
||||
// it('should parse query strings with wildcards', asyncTest(async () => {
|
||||
// let rows;
|
||||
|
||||
// const testCases = [
|
||||
// ['do*', ['do', 'dog', 'domino'], [] ],
|
||||
// // "*" is a wildcard only when used at the end (to search for documents with the specified prefix)
|
||||
// // If it's at the beginning, it's ignored, if it's in the middle, it's treated as a litteral "*".
|
||||
// ['*an*', ['an', 'anneau'], ['piano', 'plan'] ],
|
||||
// ['no*no', ['no*no'], ['nonono'] ],
|
||||
// ];
|
||||
|
||||
// for (let i = 0; i < testCases.length; i++) {
|
||||
// const t = testCases[i];
|
||||
// const input = t[0];
|
||||
// const shouldMatch = t[1];
|
||||
// const shouldNotMatch = t[2];
|
||||
// const regex = new RegExp(engine.parseQuery(input).terms._[0].value, 'gmi');
|
||||
|
||||
// for (let j = 0; j < shouldMatch.length; j++) {
|
||||
// const r = shouldMatch[j].match(regex);
|
||||
// expect(!!r).toBe(true, '"' + input + '" should match "' + shouldMatch[j] + '"');
|
||||
// }
|
||||
// }
|
||||
|
||||
// expect(engine.parseQuery('*').termCount).toBe(0);
|
||||
// }));
|
||||
|
||||
it('should handle queries with special characters', asyncTest(async () => {
|
||||
let rows;
|
||||
|
||||
|
@ -28,6 +28,11 @@ const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const PluginManager = require('lib/services/PluginManager');
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything.min'),
|
||||
];
|
||||
|
||||
const appDefaultState = Object.assign({}, defaultState, {
|
||||
route: {
|
||||
@ -441,7 +446,7 @@ class Application extends BaseApplication {
|
||||
|
||||
const printItem = {
|
||||
label: _('Print'),
|
||||
accelerator: 'CommandOrControl+P',
|
||||
// accelerator: 'CommandOrControl+P',
|
||||
screens: ['Main'],
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
@ -502,8 +507,8 @@ class Application extends BaseApplication {
|
||||
});
|
||||
}
|
||||
|
||||
const template = [
|
||||
{
|
||||
const rootMenus = {
|
||||
macOsApp: {
|
||||
/* Using a dummy entry for macOS here, because first menu
|
||||
* becomes 'Joplin' and we need a nenu called 'File' later. */
|
||||
label: shim.isMac() ? '&JoplinMainMenu' : _('&File'),
|
||||
@ -572,7 +577,8 @@ class Application extends BaseApplication {
|
||||
accelerator: 'CommandOrControl+Q',
|
||||
click: () => { bridge().electronApp().quit() }
|
||||
}]
|
||||
}, {
|
||||
},
|
||||
file: {
|
||||
label: _('&File'),
|
||||
visible: shim.isMac() ? true : false,
|
||||
submenu: [
|
||||
@ -591,7 +597,8 @@ class Application extends BaseApplication {
|
||||
},
|
||||
printItem
|
||||
]
|
||||
}, {
|
||||
},
|
||||
edit: {
|
||||
label: _('&Edit'),
|
||||
submenu: [{
|
||||
label: _('Copy'),
|
||||
@ -692,7 +699,8 @@ class Application extends BaseApplication {
|
||||
});
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
},
|
||||
view: {
|
||||
label: _('&View'),
|
||||
submenu: [{
|
||||
label: _('Toggle sidebar'),
|
||||
@ -749,11 +757,12 @@ class Application extends BaseApplication {
|
||||
screens: ['Main'],
|
||||
submenu: focusItems,
|
||||
}],
|
||||
}, {
|
||||
},
|
||||
tools: {
|
||||
label: _('&Tools'),
|
||||
visible: shim.isMac() ? false : true,
|
||||
submenu: toolsItems
|
||||
}, {
|
||||
submenu: shim.isMac() ? [] : toolsItems,
|
||||
},
|
||||
help: {
|
||||
label: _('&Help'),
|
||||
submenu: [{
|
||||
label: _('Website and documentation'),
|
||||
@ -776,6 +785,21 @@ class Application extends BaseApplication {
|
||||
click: () => _showAbout()
|
||||
}]
|
||||
},
|
||||
};
|
||||
|
||||
const pluginMenuItems = PluginManager.instance().menuItems();
|
||||
for (const item of pluginMenuItems) {
|
||||
let itemParent = rootMenus[item.parent] ? rootMenus[item.parent] : 'tools';
|
||||
itemParent.submenu.push(item);
|
||||
}
|
||||
|
||||
const template = [
|
||||
rootMenus.macOsApp,
|
||||
rootMenus.file,
|
||||
rootMenus.edit,
|
||||
rootMenus.view,
|
||||
rootMenus.tools,
|
||||
rootMenus.help,
|
||||
];
|
||||
|
||||
function isEmptyMenu(template) {
|
||||
@ -891,6 +915,10 @@ class Application extends BaseApplication {
|
||||
bridge().window().webContents.openDevTools();
|
||||
}
|
||||
|
||||
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
||||
PluginManager.instance().setLogger(reg.logger());
|
||||
PluginManager.instance().register(pluginClasses);
|
||||
|
||||
this.updateMenu('Main');
|
||||
|
||||
this.initRedux();
|
||||
|
@ -3,7 +3,6 @@ const spawnSync = require('child_process').spawnSync;
|
||||
|
||||
const babelPath = __dirname + '/node_modules/.bin/babel' + (process.platform === 'win32' ? '.cmd' : '');
|
||||
const basePath = __dirname + '/../..';
|
||||
const guiPath = __dirname + '/gui';
|
||||
|
||||
function fileIsNewerThan(path1, path2) {
|
||||
if (!fs.existsSync(path2)) return true;
|
||||
@ -14,8 +13,9 @@ function fileIsNewerThan(path1, path2) {
|
||||
return stat1.mtime > stat2.mtime;
|
||||
}
|
||||
|
||||
fs.readdirSync(guiPath).forEach((filename) => {
|
||||
const jsxPath = guiPath + '/' + filename;
|
||||
function convertJsx(path) {
|
||||
fs.readdirSync(path).forEach((filename) => {
|
||||
const jsxPath = path + '/' + filename;
|
||||
const p = jsxPath.split('.');
|
||||
if (p.length <= 1) return;
|
||||
const ext = p[p.length - 1];
|
||||
@ -38,7 +38,11 @@ fs.readdirSync(guiPath).forEach((filename) => {
|
||||
process.exit(result.status);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
convertJsx(__dirname + '/gui');
|
||||
convertJsx(__dirname + '/plugins');
|
||||
|
||||
const libContent = [
|
||||
fs.readFileSync(basePath + '/ReactNativeClient/lib/string-utils-common.js', 'utf8'),
|
||||
|
@ -18,6 +18,7 @@ const layoutUtils = require('lib/layout-utils.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const eventManager = require('../eventManager');
|
||||
const VerticalResizer = require('./VerticalResizer.min');
|
||||
const PluginManager = require('lib/services/PluginManager');
|
||||
|
||||
class MainScreenComponent extends React.Component {
|
||||
|
||||
@ -458,6 +459,9 @@ class MainScreenComponent extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const dialogInfo = PluginManager.instance().pluginDialogToShow(this.props.plugins);
|
||||
const pluginDialog = !dialogInfo ? null : <dialogInfo.Dialog {...dialogInfo.props}/>;
|
||||
|
||||
const modalLayerStyle = Object.assign({}, styles.modalLayer, { display: this.state.modalLayer.visible ? 'block' : 'none' });
|
||||
|
||||
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
|
||||
@ -491,6 +495,8 @@ class MainScreenComponent extends React.Component {
|
||||
<NoteList style={styles.noteList} />
|
||||
<VerticalResizer style={styles.verticalResizer} onDrag={this.noteList_onDrag}/>
|
||||
<NoteText style={styles.noteText} visiblePanes={this.props.noteVisiblePanes} />
|
||||
|
||||
{pluginDialog}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -512,6 +518,7 @@ const mapStateToProps = (state) => {
|
||||
sidebarWidth: state.settings['style.sidebar.width'],
|
||||
noteListWidth: state.settings['style.noteList.width'],
|
||||
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||
plugins: state.plugins,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -102,25 +102,25 @@ class NotePropertiesDialog extends React.Component {
|
||||
this.styles_ = {};
|
||||
this.styleKey_ = styleKey;
|
||||
|
||||
this.styles_.modalLayer = {
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
// this.styles_.modalLayer = {
|
||||
// zIndex: 9999,
|
||||
// display: 'flex',
|
||||
// position: 'absolute',
|
||||
// top: 0,
|
||||
// left: 0,
|
||||
// width: '100%',
|
||||
// height: '100%',
|
||||
// backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
// alignItems: 'flex-start',
|
||||
// justifyContent: 'center',
|
||||
// };
|
||||
|
||||
this.styles_.dialogBox = {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
padding: 16,
|
||||
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
|
||||
marginTop: 20,
|
||||
}
|
||||
// this.styles_.dialogBox = {
|
||||
// backgroundColor: theme.backgroundColor,
|
||||
// padding: 16,
|
||||
// boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
|
||||
// marginTop: 20,
|
||||
// }
|
||||
|
||||
this.styles_.controlBox = {
|
||||
marginBottom: '1em',
|
||||
@ -153,7 +153,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
borderColor: theme.dividerColor,
|
||||
};
|
||||
|
||||
this.styles_.dialogTitle = Object.assign({}, theme.h1Style, { marginBottom: '1.2em' });
|
||||
// this.styles_.dialogTitle = Object.assign({}, theme.h1Style, { marginBottom: '1.2em' });
|
||||
|
||||
return this.styles_;
|
||||
}
|
||||
@ -368,8 +368,6 @@ class NotePropertiesDialog extends React.Component {
|
||||
|
||||
const noteComps = [];
|
||||
|
||||
const modalLayerStyle = Object.assign({}, styles.modalLayer);
|
||||
|
||||
if (formNote) {
|
||||
for (let key in formNote) {
|
||||
if (!formNote.hasOwnProperty(key)) continue;
|
||||
@ -379,9 +377,9 @@ class NotePropertiesDialog extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={modalLayerStyle}>
|
||||
<div style={styles.dialogBox}>
|
||||
<div style={styles.dialogTitle}>{_('Note properties')}</div>
|
||||
<div style={theme.dialogModalLayer}>
|
||||
<div style={theme.dialogBox}>
|
||||
<div style={theme.dialogTitle}>{_('Note properties')}</div>
|
||||
<div>{noteComps}</div>
|
||||
<div style={{ textAlign: 'right', marginTop: 10 }}>
|
||||
{buttonComps}
|
||||
|
@ -800,7 +800,7 @@ class NoteTextComponent extends React.Component {
|
||||
});
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
this.webviewRef_.current.wrappedInstance.openDevTools();
|
||||
// this.webviewRef_.current.wrappedInstance.openDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
|
358
ElectronClient/app/plugins/GotoAnything.jsx
Normal file
358
ElectronClient/app/plugins/GotoAnything.jsx
Normal file
@ -0,0 +1,358 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const { ItemList } = require('../gui/ItemList.min');
|
||||
const { substrWithEllipsis, surroundKeywords } = require('lib/string-utils.js');
|
||||
|
||||
const PLUGIN_NAME = 'gotoAnything';
|
||||
const itemHeight = 60;
|
||||
|
||||
class GotoAnything {
|
||||
|
||||
onTrigger(event) {
|
||||
this.dispatch({
|
||||
type: 'PLUGIN_DIALOG_SET',
|
||||
open: true,
|
||||
pluginName: PLUGIN_NAME,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Dialog extends React.PureComponent {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
query: '',
|
||||
results: [],
|
||||
selectedItemId: null,
|
||||
keywords: [],
|
||||
listType: BaseModel.TYPE_NOTE,
|
||||
showHelp: false,
|
||||
};
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
this.inputRef = React.createRef();
|
||||
this.itemListRef = React.createRef();
|
||||
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.input_onChange = this.input_onChange.bind(this);
|
||||
this.input_onKeyDown = this.input_onKeyDown.bind(this);
|
||||
this.listItemRenderer = this.listItemRenderer.bind(this);
|
||||
this.listItem_onClick = this.listItem_onClick.bind(this);
|
||||
this.helpButton_onClick = this.helpButton_onClick.bind(this);
|
||||
}
|
||||
|
||||
style() {
|
||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
this.styles_[this.props.theme] = {
|
||||
dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }),
|
||||
input: Object.assign({}, theme.inputStyle, { flex: 1 }),
|
||||
row: {overflow: 'hidden', height:itemHeight, display: 'flex', justifyContent: 'center', flexDirection: 'column', paddingLeft: 10, paddingRight: 10},
|
||||
help: Object.assign({}, theme.textStyle, { marginBottom: 10 }),
|
||||
inputHelpWrapper: {display: 'flex', flexDirection: 'row', alignItems: 'center'},
|
||||
helpIcon: {flex:0, width: 16, height: 16, marginLeft: 10},
|
||||
helpButton: {color: theme.color, textDecoration: 'none'},
|
||||
};
|
||||
|
||||
const rowTextStyle = {
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
fontFamily: theme.fontFamily,
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: 0.7,
|
||||
userSelect: 'none',
|
||||
};
|
||||
|
||||
const rowTitleStyle = Object.assign({}, rowTextStyle, {
|
||||
fontSize: rowTextStyle.fontSize * 1.4,
|
||||
marginBottom: 5,
|
||||
color: theme.colorFaded,
|
||||
});
|
||||
|
||||
this.styles_[this.props.theme].rowSelected = Object.assign({}, this.styles_[this.props.theme].row, { backgroundColor: theme.selectedColor });
|
||||
this.styles_[this.props.theme].rowPath = rowTextStyle;
|
||||
this.styles_[this.props.theme].rowTitle = rowTitleStyle;
|
||||
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listUpdateIID_) clearTimeout(this.listUpdateIID_);
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.keyCode === 27) { // ESCAPE
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
type: 'PLUGIN_DIALOG_SET',
|
||||
open: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
helpButton_onClick(event) {
|
||||
this.setState({ showHelp: !this.state.showHelp });
|
||||
}
|
||||
|
||||
input_onChange(event) {
|
||||
this.setState({ query: event.target.value });
|
||||
|
||||
this.scheduleListUpdate();
|
||||
}
|
||||
|
||||
scheduleListUpdate() {
|
||||
if (this.listUpdateIID_) return;
|
||||
|
||||
this.listUpdateIID_ = setTimeout(async () => {
|
||||
await this.updateList();
|
||||
this.listUpdateIID_ = null;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
makeSearchQuery(query) {
|
||||
const splitted = query.split(' ');
|
||||
const output = [];
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
const s = splitted[i].trim();
|
||||
if (!s) continue;
|
||||
|
||||
output.push('title:' + s + '*');
|
||||
}
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
keywords(searchQuery) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(searchQuery);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
|
||||
async updateList() {
|
||||
if (!this.state.query) {
|
||||
this.setState({ results: [], keywords: [] });
|
||||
} else {
|
||||
let results = [];
|
||||
let listType = null;
|
||||
let searchQuery = '';
|
||||
|
||||
if (this.state.query.indexOf('#') === 0) { // TAGS
|
||||
listType = BaseModel.TYPE_TAG;
|
||||
searchQuery = '*' + this.state.query.split(' ')[0].substr(1).trim() + '*';
|
||||
results = await Tag.searchAllWithNotes({ titlePattern: searchQuery });
|
||||
} else if (this.state.query.indexOf('@') === 0) { // FOLDERS
|
||||
listType = BaseModel.TYPE_FOLDER;
|
||||
searchQuery = '*' + this.state.query.split(' ')[0].substr(1).trim() + '*';
|
||||
results = await Folder.search({ titlePattern: searchQuery });
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const row = results[i];
|
||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||
results[i] = Object.assign({}, row, { path: path ? path : '/' });
|
||||
}
|
||||
} else { // NOTES
|
||||
listType = BaseModel.TYPE_NOTE;
|
||||
searchQuery = this.makeSearchQuery(this.state.query);
|
||||
results = await SearchEngine.instance().search(searchQuery);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const row = results[i];
|
||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||
results[i] = Object.assign({}, row, { path: path });
|
||||
}
|
||||
}
|
||||
|
||||
let selectedItemId = null;
|
||||
const itemIndex = this.selectedItemIndex(results, this.state.selectedItemId);
|
||||
if (itemIndex > 0) {
|
||||
selectedItemId = this.state.selectedItemId;
|
||||
} else if (results.length > 0) {
|
||||
selectedItemId = results[0].id;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
listType: listType,
|
||||
results: results,
|
||||
keywords: this.keywords(searchQuery),
|
||||
selectedItemId: selectedItemId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
gotoItem(item) {
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
type: 'PLUGIN_DIALOG_SET',
|
||||
open: false,
|
||||
});
|
||||
|
||||
if (this.state.listType === BaseModel.TYPE_NOTE) {
|
||||
this.props.dispatch({
|
||||
type: "FOLDER_AND_NOTE_SELECT",
|
||||
folderId: item.parent_id,
|
||||
noteId: item.id,
|
||||
});
|
||||
} else if (this.state.listType === BaseModel.TYPE_TAG) {
|
||||
this.props.dispatch({
|
||||
type: "TAG_SELECT",
|
||||
id: item.id,
|
||||
});
|
||||
} else if (this.state.listType === BaseModel.TYPE_FOLDER) {
|
||||
this.props.dispatch({
|
||||
type: "FOLDER_SELECT",
|
||||
id: item.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
listItem_onClick(event) {
|
||||
const itemId = event.currentTarget.getAttribute('data-id');
|
||||
const parentId = event.currentTarget.getAttribute('data-parent-id');
|
||||
|
||||
this.gotoItem({
|
||||
id: itemId,
|
||||
parent_id: parentId,
|
||||
});
|
||||
}
|
||||
|
||||
listItemRenderer(item) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.style();
|
||||
const rowStyle = item.id === this.state.selectedItemId ? style.rowSelected : style.row;
|
||||
const titleHtml = surroundKeywords(this.state.keywords, item.title, '<span style="font-weight: bold; color: ' + theme.colorBright + ';">', '</span>');
|
||||
|
||||
const pathComp = !item.path ? null : <div style={style.rowPath}>{item.path}</div>
|
||||
|
||||
return (
|
||||
<div key={item.id} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id}>
|
||||
<div style={style.rowTitle} dangerouslySetInnerHTML={{__html: titleHtml}}></div>
|
||||
{pathComp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
selectedItemIndex(results, itemId) {
|
||||
if (typeof results === 'undefined') results = this.state.results;
|
||||
if (typeof itemId === 'undefined') itemId = this.state.selectedItemId;
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const r = results[i];
|
||||
if (r.id === itemId) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
selectedItem() {
|
||||
const index = this.selectedItemIndex();
|
||||
if (index < 0) return null;
|
||||
return this.state.results[index];
|
||||
}
|
||||
|
||||
input_onKeyDown(event) {
|
||||
const keyCode = event.keyCode;
|
||||
|
||||
if (this.state.results.length > 0 && (keyCode === 40 || keyCode === 38)) { // DOWN / UP
|
||||
event.preventDefault();
|
||||
|
||||
const inc = keyCode === 38 ? -1 : +1;
|
||||
let index = this.selectedItemIndex();
|
||||
if (index < 0) return; // Not possible, but who knows
|
||||
|
||||
index += inc;
|
||||
if (index < 0) index = 0;
|
||||
if (index >= this.state.results.length) index = this.state.results.length - 1;
|
||||
|
||||
const newId = this.state.results[index].id;
|
||||
|
||||
this.itemListRef.current.makeItemIndexVisible(index);
|
||||
|
||||
this.setState({ selectedItemId: newId });
|
||||
}
|
||||
|
||||
if (keyCode === 13) { // ENTER
|
||||
event.preventDefault();
|
||||
|
||||
const item = this.selectedItem();
|
||||
if (!item) return;
|
||||
|
||||
this.gotoItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const style = {
|
||||
marginTop: 5,
|
||||
height: Math.min(itemHeight * this.state.results.length, 7 * itemHeight),
|
||||
};
|
||||
|
||||
return (
|
||||
<ItemList
|
||||
ref={this.itemListRef}
|
||||
itemHeight={itemHeight}
|
||||
items={this.state.results}
|
||||
style={style}
|
||||
itemRenderer={this.listItemRenderer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.style();
|
||||
const helpComp = !this.state.showHelp ? null : <div style={style.help}>{_('Type a note title to jump to it. Or type # followed by a tag name, or @ followed by a notebook name.')}</div>
|
||||
|
||||
return (
|
||||
<div style={theme.dialogModalLayer}>
|
||||
<div style={style.dialogBox}>
|
||||
{helpComp}
|
||||
<div style={style.inputHelpWrapper}>
|
||||
<input autoFocus type="text" style={style.input} ref={this.inputRef} value={this.state.query} onChange={this.input_onChange} onKeyDown={this.input_onKeyDown}/>
|
||||
<a href="#" style={style.helpButton} onClick={this.helpButton_onClick}><i style={style.helpIcon} className={"fa fa-question-circle"}></i></a>
|
||||
</div>
|
||||
{this.renderList()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
};
|
||||
|
||||
GotoAnything.Dialog = connect(mapStateToProps)(Dialog);
|
||||
|
||||
GotoAnything.manifest = {
|
||||
|
||||
name: PLUGIN_NAME,
|
||||
menuItems: [
|
||||
{
|
||||
name: 'main',
|
||||
parent: 'tools',
|
||||
label: _('Goto Anything...'),
|
||||
accelerator: 'CommandOrControl+P',
|
||||
screens: ['Main'],
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
module.exports = GotoAnything;
|
376
ElectronClient/app/plugins/GotoAnything.min.js
vendored
Normal file
376
ElectronClient/app/plugins/GotoAnything.min.js
vendored
Normal file
@ -0,0 +1,376 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const { ItemList } = require('../gui/ItemList.min');
|
||||
const { substrWithEllipsis, surroundKeywords } = require('lib/string-utils.js');
|
||||
|
||||
const PLUGIN_NAME = 'gotoAnything';
|
||||
const itemHeight = 60;
|
||||
|
||||
class GotoAnything {
|
||||
|
||||
onTrigger(event) {
|
||||
this.dispatch({
|
||||
type: 'PLUGIN_DIALOG_SET',
|
||||
open: true,
|
||||
pluginName: PLUGIN_NAME
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Dialog extends React.PureComponent {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
query: '',
|
||||
results: [],
|
||||
selectedItemId: null,
|
||||
keywords: [],
|
||||
listType: BaseModel.TYPE_NOTE,
|
||||
showHelp: false
|
||||
};
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
this.inputRef = React.createRef();
|
||||
this.itemListRef = React.createRef();
|
||||
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.input_onChange = this.input_onChange.bind(this);
|
||||
this.input_onKeyDown = this.input_onKeyDown.bind(this);
|
||||
this.listItemRenderer = this.listItemRenderer.bind(this);
|
||||
this.listItem_onClick = this.listItem_onClick.bind(this);
|
||||
this.helpButton_onClick = this.helpButton_onClick.bind(this);
|
||||
}
|
||||
|
||||
style() {
|
||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
this.styles_[this.props.theme] = {
|
||||
dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }),
|
||||
input: Object.assign({}, theme.inputStyle, { flex: 1 }),
|
||||
row: { overflow: 'hidden', height: itemHeight, display: 'flex', justifyContent: 'center', flexDirection: 'column', paddingLeft: 10, paddingRight: 10 },
|
||||
help: Object.assign({}, theme.textStyle, { marginBottom: 10 }),
|
||||
inputHelpWrapper: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
|
||||
helpIcon: { flex: 0, width: 16, height: 16, marginLeft: 10 },
|
||||
helpButton: { color: theme.color, textDecoration: 'none' }
|
||||
};
|
||||
|
||||
const rowTextStyle = {
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
fontFamily: theme.fontFamily,
|
||||
whiteSpace: 'nowrap',
|
||||
opacity: 0.7,
|
||||
userSelect: 'none'
|
||||
};
|
||||
|
||||
const rowTitleStyle = Object.assign({}, rowTextStyle, {
|
||||
fontSize: rowTextStyle.fontSize * 1.4,
|
||||
marginBottom: 5,
|
||||
color: theme.colorFaded
|
||||
});
|
||||
|
||||
this.styles_[this.props.theme].rowSelected = Object.assign({}, this.styles_[this.props.theme].row, { backgroundColor: theme.selectedColor });
|
||||
this.styles_[this.props.theme].rowPath = rowTextStyle;
|
||||
this.styles_[this.props.theme].rowTitle = rowTitleStyle;
|
||||
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.listUpdateIID_) clearTimeout(this.listUpdateIID_);
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.keyCode === 27) {
|
||||
// ESCAPE
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
type: 'PLUGIN_DIALOG_SET',
|
||||
open: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
helpButton_onClick(event) {
|
||||
this.setState({ showHelp: !this.state.showHelp });
|
||||
}
|
||||
|
||||
input_onChange(event) {
|
||||
this.setState({ query: event.target.value });
|
||||
|
||||
this.scheduleListUpdate();
|
||||
}
|
||||
|
||||
scheduleListUpdate() {
|
||||
if (this.listUpdateIID_) return;
|
||||
|
||||
this.listUpdateIID_ = setTimeout(async () => {
|
||||
await this.updateList();
|
||||
this.listUpdateIID_ = null;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
makeSearchQuery(query) {
|
||||
const splitted = query.split(' ');
|
||||
const output = [];
|
||||
for (let i = 0; i < splitted.length; i++) {
|
||||
const s = splitted[i].trim();
|
||||
if (!s) continue;
|
||||
|
||||
output.push('title:' + s + '*');
|
||||
}
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
keywords(searchQuery) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(searchQuery);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
|
||||
async updateList() {
|
||||
if (!this.state.query) {
|
||||
this.setState({ results: [], keywords: [] });
|
||||
} else {
|
||||
let results = [];
|
||||
let listType = null;
|
||||
let searchQuery = '';
|
||||
|
||||
if (this.state.query.indexOf('#') === 0) {
|
||||
// TAGS
|
||||
listType = BaseModel.TYPE_TAG;
|
||||
searchQuery = '*' + this.state.query.split(' ')[0].substr(1).trim() + '*';
|
||||
results = await Tag.searchAllWithNotes({ titlePattern: searchQuery });
|
||||
} else if (this.state.query.indexOf('@') === 0) {
|
||||
// FOLDERS
|
||||
listType = BaseModel.TYPE_FOLDER;
|
||||
searchQuery = '*' + this.state.query.split(' ')[0].substr(1).trim() + '*';
|
||||
results = await Folder.search({ titlePattern: searchQuery });
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const row = results[i];
|
||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||
results[i] = Object.assign({}, row, { path: path ? path : '/' });
|
||||
}
|
||||
} else {
|
||||
// NOTES
|
||||
listType = BaseModel.TYPE_NOTE;
|
||||
searchQuery = this.makeSearchQuery(this.state.query);
|
||||
results = await SearchEngine.instance().search(searchQuery);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const row = results[i];
|
||||
const path = Folder.folderPathString(this.props.folders, row.parent_id);
|
||||
results[i] = Object.assign({}, row, { path: path });
|
||||
}
|
||||
}
|
||||
|
||||
let selectedItemId = null;
|
||||
const itemIndex = this.selectedItemIndex(results, this.state.selectedItemId);
|
||||
if (itemIndex > 0) {
|
||||
selectedItemId = this.state.selectedItemId;
|
||||
} else if (results.length > 0) {
|
||||
selectedItemId = results[0].id;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
listType: listType,
|
||||
results: results,
|
||||
keywords: this.keywords(searchQuery),
|
||||
selectedItemId: selectedItemId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
gotoItem(item) {
|
||||
this.props.dispatch({
|
||||
pluginName: PLUGIN_NAME,
|
||||
type: 'PLUGIN_DIALOG_SET',
|
||||
open: false
|
||||
});
|
||||
|
||||
if (this.state.listType === BaseModel.TYPE_NOTE) {
|
||||
this.props.dispatch({
|
||||
type: "FOLDER_AND_NOTE_SELECT",
|
||||
folderId: item.parent_id,
|
||||
noteId: item.id
|
||||
});
|
||||
} else if (this.state.listType === BaseModel.TYPE_TAG) {
|
||||
this.props.dispatch({
|
||||
type: "TAG_SELECT",
|
||||
id: item.id
|
||||
});
|
||||
} else if (this.state.listType === BaseModel.TYPE_FOLDER) {
|
||||
this.props.dispatch({
|
||||
type: "FOLDER_SELECT",
|
||||
id: item.id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
listItem_onClick(event) {
|
||||
const itemId = event.currentTarget.getAttribute('data-id');
|
||||
const parentId = event.currentTarget.getAttribute('data-parent-id');
|
||||
|
||||
this.gotoItem({
|
||||
id: itemId,
|
||||
parent_id: parentId
|
||||
});
|
||||
}
|
||||
|
||||
listItemRenderer(item) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.style();
|
||||
const rowStyle = item.id === this.state.selectedItemId ? style.rowSelected : style.row;
|
||||
const titleHtml = surroundKeywords(this.state.keywords, item.title, '<span style="font-weight: bold; color: ' + theme.colorBright + ';">', '</span>');
|
||||
|
||||
const pathComp = !item.path ? null : React.createElement(
|
||||
'div',
|
||||
{ style: style.rowPath },
|
||||
item.path
|
||||
);
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ key: item.id, style: rowStyle, onClick: this.listItem_onClick, 'data-id': item.id, 'data-parent-id': item.parent_id },
|
||||
React.createElement('div', { style: style.rowTitle, dangerouslySetInnerHTML: { __html: titleHtml } }),
|
||||
pathComp
|
||||
);
|
||||
}
|
||||
|
||||
selectedItemIndex(results, itemId) {
|
||||
if (typeof results === 'undefined') results = this.state.results;
|
||||
if (typeof itemId === 'undefined') itemId = this.state.selectedItemId;
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const r = results[i];
|
||||
if (r.id === itemId) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
selectedItem() {
|
||||
const index = this.selectedItemIndex();
|
||||
if (index < 0) return null;
|
||||
return this.state.results[index];
|
||||
}
|
||||
|
||||
input_onKeyDown(event) {
|
||||
const keyCode = event.keyCode;
|
||||
|
||||
if (this.state.results.length > 0 && (keyCode === 40 || keyCode === 38)) {
|
||||
// DOWN / UP
|
||||
event.preventDefault();
|
||||
|
||||
const inc = keyCode === 38 ? -1 : +1;
|
||||
let index = this.selectedItemIndex();
|
||||
if (index < 0) return; // Not possible, but who knows
|
||||
|
||||
index += inc;
|
||||
if (index < 0) index = 0;
|
||||
if (index >= this.state.results.length) index = this.state.results.length - 1;
|
||||
|
||||
const newId = this.state.results[index].id;
|
||||
|
||||
this.itemListRef.current.makeItemIndexVisible(index);
|
||||
|
||||
this.setState({ selectedItemId: newId });
|
||||
}
|
||||
|
||||
if (keyCode === 13) {
|
||||
// ENTER
|
||||
event.preventDefault();
|
||||
|
||||
const item = this.selectedItem();
|
||||
if (!item) return;
|
||||
|
||||
this.gotoItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
renderList() {
|
||||
const style = {
|
||||
marginTop: 5,
|
||||
height: Math.min(itemHeight * this.state.results.length, 7 * itemHeight)
|
||||
};
|
||||
|
||||
return React.createElement(ItemList, {
|
||||
ref: this.itemListRef,
|
||||
itemHeight: itemHeight,
|
||||
items: this.state.results,
|
||||
style: style,
|
||||
itemRenderer: this.listItemRenderer
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.style();
|
||||
const helpComp = !this.state.showHelp ? null : React.createElement(
|
||||
'div',
|
||||
{ style: style.help },
|
||||
_('Type a note title to jump to it. Or type # followed by a tag name, or @ followed by a notebook name.')
|
||||
);
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: theme.dialogModalLayer },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: style.dialogBox },
|
||||
helpComp,
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: style.inputHelpWrapper },
|
||||
React.createElement('input', { autoFocus: true, type: 'text', style: style.input, ref: this.inputRef, value: this.state.query, onChange: this.input_onChange, onKeyDown: this.input_onKeyDown }),
|
||||
React.createElement(
|
||||
'a',
|
||||
{ href: '#', style: style.helpButton, onClick: this.helpButton_onClick },
|
||||
React.createElement('i', { style: style.helpIcon, className: "fa fa-question-circle" })
|
||||
)
|
||||
),
|
||||
this.renderList()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme
|
||||
};
|
||||
};
|
||||
|
||||
GotoAnything.Dialog = connect(mapStateToProps)(Dialog);
|
||||
|
||||
GotoAnything.manifest = {
|
||||
|
||||
name: PLUGIN_NAME,
|
||||
menuItems: [{
|
||||
name: 'main',
|
||||
parent: 'tools',
|
||||
label: _('Goto Anything...'),
|
||||
accelerator: 'CommandOrControl+P',
|
||||
screens: ['Main']
|
||||
}]
|
||||
|
||||
};
|
||||
|
||||
module.exports = GotoAnything;
|
@ -42,6 +42,10 @@ globalStyle.headerStyle = {
|
||||
|
||||
globalStyle.inputStyle = {
|
||||
border: '1px solid',
|
||||
height: 24,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
globalStyle.containerStyle = {
|
||||
@ -67,6 +71,7 @@ const lightStyle = {
|
||||
colorError: "red",
|
||||
colorWarn: "#9A5B00",
|
||||
colorFaded: "#777777", // For less important text
|
||||
colorBright: "#000000", // For important text
|
||||
dividerColor: "#dddddd",
|
||||
selectedColor: '#e5e5e5',
|
||||
urlColor: '#155BDA',
|
||||
@ -103,6 +108,7 @@ const darkStyle = {
|
||||
colorError: "red",
|
||||
colorWarn: "#9A5B00",
|
||||
colorFaded: "#777777", // For less important text
|
||||
colorBright: "#ffffff", // For important text
|
||||
dividerColor: '#555555',
|
||||
selectedColor: '#333333',
|
||||
urlColor: '#4E87EE',
|
||||
@ -198,6 +204,28 @@ function addExtraStyles(style) {
|
||||
}
|
||||
);
|
||||
|
||||
style.dialogModalLayer = {
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
style.dialogBox = {
|
||||
backgroundColor: style.backgroundColor,
|
||||
padding: 16,
|
||||
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
|
||||
marginTop: 20,
|
||||
}
|
||||
|
||||
style.dialogTitle = Object.assign({}, style.h1Style, { marginBottom: '1.2em' });
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ const { Database } = require('lib/database.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const moment = require('moment');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const lodash = require('lodash');
|
||||
const { substrWithEllipsis } = require('lib/string-utils.js');
|
||||
|
||||
class Folder extends BaseItem {
|
||||
|
||||
@ -217,6 +217,34 @@ class Folder extends BaseItem {
|
||||
return getNestedChildren(all, '');
|
||||
}
|
||||
|
||||
static folderPath(folders, folderId) {
|
||||
const idToFolders = {};
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
idToFolders[folders[i].id] = folders[i];
|
||||
}
|
||||
|
||||
const path = [];
|
||||
while (folderId) {
|
||||
const folder = idToFolders[folderId];
|
||||
if (!folder) break; // Shouldn't happen
|
||||
path.push(folder);
|
||||
folderId = folder.parent_id;
|
||||
}
|
||||
|
||||
path.reverse();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
static folderPathString(folders, folderId) {
|
||||
const path = this.folderPath(folders, folderId);
|
||||
const output = [];
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
output.push(substrWithEllipsis(path[i].title, 0, 16));
|
||||
}
|
||||
return output.join(' / ');
|
||||
}
|
||||
|
||||
static buildTree(folders) {
|
||||
const idToFolders = {};
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
|
@ -90,9 +90,19 @@ class Tag extends BaseItem {
|
||||
return !!r;
|
||||
}
|
||||
|
||||
static tagsWithNotesSql_() {
|
||||
return 'select distinct tags.id from tags left join note_tags nt on nt.tag_id = tags.id left join notes on notes.id = nt.note_id where notes.id IS NOT NULL';
|
||||
}
|
||||
|
||||
static async allWithNotes() {
|
||||
const tagIdSql = 'select distinct tags.id from tags left join note_tags nt on nt.tag_id = tags.id left join notes on notes.id = nt.note_id where notes.id IS NOT NULL';
|
||||
return await Tag.modelSelectAll('SELECT * FROM tags WHERE id IN (' + tagIdSql + ')');
|
||||
return await Tag.modelSelectAll('SELECT * FROM tags WHERE id IN (' + this.tagsWithNotesSql_() + ')');
|
||||
}
|
||||
|
||||
static async searchAllWithNotes(options) {
|
||||
if (!options) options = {};
|
||||
if (!options.conditions) options.conditions = [];
|
||||
options.conditions.push('id IN (' + this.tagsWithNotesSql_() + ')');
|
||||
return this.search(options);
|
||||
}
|
||||
|
||||
static async tagsByNoteId(noteId) {
|
||||
|
@ -48,6 +48,7 @@ const defaultState = {
|
||||
toFetchCount: 0,
|
||||
},
|
||||
historyNotes: [],
|
||||
plugins: {},
|
||||
};
|
||||
|
||||
const stateUtils = {};
|
||||
@ -691,6 +692,17 @@ const reducer = (state = defaultState, action) => {
|
||||
newState.selectedNoteTags = action.items;
|
||||
break;
|
||||
|
||||
case 'PLUGIN_DIALOG_SET':
|
||||
|
||||
if (!action.pluginName) throw new Error('action.pluginName not specified');
|
||||
newState = Object.assign({}, state);
|
||||
const newPlugins = Object.assign({}, newState.plugins);
|
||||
const newPlugin = newState.plugins[action.pluginName] ? Object.assign({}, newState.plugins[action.pluginName]) : {};
|
||||
if ('open' in action) newPlugin.dialogOpen = action.open;
|
||||
newPlugins[action.pluginName] = newPlugin;
|
||||
newState.plugins = newPlugins;
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
|
||||
|
105
ReactNativeClient/lib/services/PluginManager.js
Normal file
105
ReactNativeClient/lib/services/PluginManager.js
Normal file
@ -0,0 +1,105 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
|
||||
class PluginManager {
|
||||
|
||||
constructor() {
|
||||
this.plugins_ = {};
|
||||
this.logger_ = new Logger();
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
static instance() {
|
||||
if (this.instance_) return this.instance_;
|
||||
this.instance_ = new PluginManager();
|
||||
return this.instance_;
|
||||
}
|
||||
|
||||
register(classes) {
|
||||
if (!Array.isArray(classes)) classes = [classes];
|
||||
|
||||
for (let i = 0; i < classes.length; i++) {
|
||||
const PluginClass = classes[i];
|
||||
|
||||
if (this.plugins_[PluginClass.manifest.name]) throw new Error('Already registered: ' + PluginClass.manifest.name);
|
||||
|
||||
this.plugins_[PluginClass.manifest.name] = {
|
||||
Class: PluginClass,
|
||||
instance: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pluginInstance_(name) {
|
||||
const p = this.plugins_[name];
|
||||
if (p.instance) return p.instance;
|
||||
p.instance = new p.Class();
|
||||
p.instance.dispatch = (action) => this.dispatch_(action);
|
||||
return p.instance;
|
||||
}
|
||||
|
||||
pluginClass_(name) {
|
||||
return this.plugins_[name].Class;
|
||||
}
|
||||
|
||||
onPluginMenuItemTrigger_(event) {
|
||||
const p = this.pluginInstance_(event.pluginName);
|
||||
p.onTrigger({
|
||||
itemName: event.itemName,
|
||||
});
|
||||
}
|
||||
|
||||
pluginDialogToShow(pluginStates) {
|
||||
for (const name in pluginStates) {
|
||||
const p = pluginStates[name];
|
||||
if (!p.dialogOpen) continue;
|
||||
|
||||
const Class = this.pluginClass_(name);
|
||||
if (!Class.Dialog) continue;
|
||||
|
||||
return {
|
||||
Dialog: Class.Dialog,
|
||||
props: this.dialogProps_(name),
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
dialogProps_(name) {
|
||||
return {
|
||||
dispatch: (action) => this.dispatch_(action),
|
||||
plugin: this.pluginInstance_(name),
|
||||
};
|
||||
}
|
||||
|
||||
menuItems() {
|
||||
let output = [];
|
||||
for (const name in this.plugins_) {
|
||||
const menuItems = this.plugins_[name].Class.manifest.menuItems;
|
||||
if (!menuItems) continue;
|
||||
|
||||
for (const item of menuItems) {
|
||||
item.click = () => {
|
||||
this.onPluginMenuItemTrigger_({
|
||||
pluginName: name,
|
||||
itemName: item.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
output = output.concat(menuItems);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = PluginManager;
|
@ -267,7 +267,7 @@ class SearchEngine {
|
||||
|
||||
if (c === ':' && !inQuote) {
|
||||
currentCol = currentTerm;
|
||||
terms[currentCol] = [];
|
||||
if (!terms[currentCol]) terms[currentCol] = [];
|
||||
currentTerm = '';
|
||||
continue;
|
||||
}
|
||||
@ -368,7 +368,7 @@ class SearchEngine {
|
||||
return this.basicSearch(query);
|
||||
} else {
|
||||
const parsedQuery = this.parseQuery(query);
|
||||
const sql = 'SELECT notes_fts.id, notes_fts.title, offsets(notes_fts) AS offsets, notes.user_updated_time, notes.is_todo, notes.todo_completed FROM notes_fts LEFT JOIN notes ON notes_fts.id = notes.id WHERE notes_fts MATCH ?'
|
||||
const sql = 'SELECT notes_fts.id, notes_fts.title, offsets(notes_fts) AS offsets, notes.user_updated_time, notes.is_todo, notes.todo_completed, notes.parent_id FROM notes_fts LEFT JOIN notes ON notes_fts.id = notes.id WHERE notes_fts MATCH ?'
|
||||
try {
|
||||
const rows = await this.db().selectAll(sql, [query]);
|
||||
this.orderResults_(rows, parsedQuery);
|
||||
|
Loading…
Reference in New Issue
Block a user