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 efgh', { _: ['efgh'], title: ['abcd'] }],
|
||||||
['title:abcd', { title: ['abcd'] }],
|
['title:abcd', { title: ['abcd'] }],
|
||||||
['"abcd efgh"', { _: ['abcd efgh'] }],
|
['"abcd efgh"', { _: ['abcd efgh'] }],
|
||||||
|
['title:abcd title:efgh', { title: ['abcd', 'efgh'] }],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < testCases.length; i++) {
|
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 () => {
|
it('should handle queries with special characters', asyncTest(async () => {
|
||||||
let rows;
|
let rows;
|
||||||
|
|
||||||
|
@ -28,6 +28,11 @@ const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
|||||||
const { bridge } = require('electron').remote.require('./bridge');
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
const MenuItem = bridge().MenuItem;
|
const MenuItem = bridge().MenuItem;
|
||||||
|
const PluginManager = require('lib/services/PluginManager');
|
||||||
|
|
||||||
|
const pluginClasses = [
|
||||||
|
require('./plugins/GotoAnything.min'),
|
||||||
|
];
|
||||||
|
|
||||||
const appDefaultState = Object.assign({}, defaultState, {
|
const appDefaultState = Object.assign({}, defaultState, {
|
||||||
route: {
|
route: {
|
||||||
@ -441,7 +446,7 @@ class Application extends BaseApplication {
|
|||||||
|
|
||||||
const printItem = {
|
const printItem = {
|
||||||
label: _('Print'),
|
label: _('Print'),
|
||||||
accelerator: 'CommandOrControl+P',
|
// accelerator: 'CommandOrControl+P',
|
||||||
screens: ['Main'],
|
screens: ['Main'],
|
||||||
click: () => {
|
click: () => {
|
||||||
this.dispatch({
|
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
|
/* Using a dummy entry for macOS here, because first menu
|
||||||
* becomes 'Joplin' and we need a nenu called 'File' later. */
|
* becomes 'Joplin' and we need a nenu called 'File' later. */
|
||||||
label: shim.isMac() ? '&JoplinMainMenu' : _('&File'),
|
label: shim.isMac() ? '&JoplinMainMenu' : _('&File'),
|
||||||
@ -572,7 +577,8 @@ class Application extends BaseApplication {
|
|||||||
accelerator: 'CommandOrControl+Q',
|
accelerator: 'CommandOrControl+Q',
|
||||||
click: () => { bridge().electronApp().quit() }
|
click: () => { bridge().electronApp().quit() }
|
||||||
}]
|
}]
|
||||||
}, {
|
},
|
||||||
|
file: {
|
||||||
label: _('&File'),
|
label: _('&File'),
|
||||||
visible: shim.isMac() ? true : false,
|
visible: shim.isMac() ? true : false,
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -591,7 +597,8 @@ class Application extends BaseApplication {
|
|||||||
},
|
},
|
||||||
printItem
|
printItem
|
||||||
]
|
]
|
||||||
}, {
|
},
|
||||||
|
edit: {
|
||||||
label: _('&Edit'),
|
label: _('&Edit'),
|
||||||
submenu: [{
|
submenu: [{
|
||||||
label: _('Copy'),
|
label: _('Copy'),
|
||||||
@ -692,7 +699,8 @@ class Application extends BaseApplication {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
}, {
|
},
|
||||||
|
view: {
|
||||||
label: _('&View'),
|
label: _('&View'),
|
||||||
submenu: [{
|
submenu: [{
|
||||||
label: _('Toggle sidebar'),
|
label: _('Toggle sidebar'),
|
||||||
@ -749,11 +757,12 @@ class Application extends BaseApplication {
|
|||||||
screens: ['Main'],
|
screens: ['Main'],
|
||||||
submenu: focusItems,
|
submenu: focusItems,
|
||||||
}],
|
}],
|
||||||
}, {
|
},
|
||||||
|
tools: {
|
||||||
label: _('&Tools'),
|
label: _('&Tools'),
|
||||||
visible: shim.isMac() ? false : true,
|
submenu: shim.isMac() ? [] : toolsItems,
|
||||||
submenu: toolsItems
|
},
|
||||||
}, {
|
help: {
|
||||||
label: _('&Help'),
|
label: _('&Help'),
|
||||||
submenu: [{
|
submenu: [{
|
||||||
label: _('Website and documentation'),
|
label: _('Website and documentation'),
|
||||||
@ -775,7 +784,22 @@ class Application extends BaseApplication {
|
|||||||
visible: shim.isMac() ? false : true,
|
visible: shim.isMac() ? false : true,
|
||||||
click: () => _showAbout()
|
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) {
|
function isEmptyMenu(template) {
|
||||||
@ -891,6 +915,10 @@ class Application extends BaseApplication {
|
|||||||
bridge().window().webContents.openDevTools();
|
bridge().window().webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PluginManager.instance().dispatch_ = this.dispatch.bind(this);
|
||||||
|
PluginManager.instance().setLogger(reg.logger());
|
||||||
|
PluginManager.instance().register(pluginClasses);
|
||||||
|
|
||||||
this.updateMenu('Main');
|
this.updateMenu('Main');
|
||||||
|
|
||||||
this.initRedux();
|
this.initRedux();
|
||||||
|
@ -3,7 +3,6 @@ const spawnSync = require('child_process').spawnSync;
|
|||||||
|
|
||||||
const babelPath = __dirname + '/node_modules/.bin/babel' + (process.platform === 'win32' ? '.cmd' : '');
|
const babelPath = __dirname + '/node_modules/.bin/babel' + (process.platform === 'win32' ? '.cmd' : '');
|
||||||
const basePath = __dirname + '/../..';
|
const basePath = __dirname + '/../..';
|
||||||
const guiPath = __dirname + '/gui';
|
|
||||||
|
|
||||||
function fileIsNewerThan(path1, path2) {
|
function fileIsNewerThan(path1, path2) {
|
||||||
if (!fs.existsSync(path2)) return true;
|
if (!fs.existsSync(path2)) return true;
|
||||||
@ -14,31 +13,36 @@ function fileIsNewerThan(path1, path2) {
|
|||||||
return stat1.mtime > stat2.mtime;
|
return stat1.mtime > stat2.mtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.readdirSync(guiPath).forEach((filename) => {
|
function convertJsx(path) {
|
||||||
const jsxPath = guiPath + '/' + filename;
|
fs.readdirSync(path).forEach((filename) => {
|
||||||
const p = jsxPath.split('.');
|
const jsxPath = path + '/' + filename;
|
||||||
if (p.length <= 1) return;
|
const p = jsxPath.split('.');
|
||||||
const ext = p[p.length - 1];
|
if (p.length <= 1) return;
|
||||||
if (ext !== 'jsx') return;
|
const ext = p[p.length - 1];
|
||||||
p.pop();
|
if (ext !== 'jsx') return;
|
||||||
|
p.pop();
|
||||||
|
|
||||||
const basePath = p.join('.');
|
const basePath = p.join('.');
|
||||||
|
|
||||||
const jsPath = basePath + '.min.js';
|
const jsPath = basePath + '.min.js';
|
||||||
|
|
||||||
if (fileIsNewerThan(jsxPath, jsPath)) {
|
if (fileIsNewerThan(jsxPath, jsPath)) {
|
||||||
console.info('Compiling ' + jsxPath + '...');
|
console.info('Compiling ' + jsxPath + '...');
|
||||||
const result = spawnSync(babelPath, ['--presets', 'react', '--out-file', jsPath, jsxPath]);
|
const result = spawnSync(babelPath, ['--presets', 'react', '--out-file', jsPath, jsxPath]);
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
const msg = [];
|
const msg = [];
|
||||||
if (result.stdout) msg.push(result.stdout.toString());
|
if (result.stdout) msg.push(result.stdout.toString());
|
||||||
if (result.stderr) msg.push(result.stderr.toString());
|
if (result.stderr) msg.push(result.stderr.toString());
|
||||||
console.error(msg.join('\n'));
|
console.error(msg.join('\n'));
|
||||||
if (result.error) console.error(result.error);
|
if (result.error) console.error(result.error);
|
||||||
process.exit(result.status);
|
process.exit(result.status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
convertJsx(__dirname + '/gui');
|
||||||
|
convertJsx(__dirname + '/plugins');
|
||||||
|
|
||||||
const libContent = [
|
const libContent = [
|
||||||
fs.readFileSync(basePath + '/ReactNativeClient/lib/string-utils-common.js', 'utf8'),
|
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 { bridge } = require('electron').remote.require('./bridge');
|
||||||
const eventManager = require('../eventManager');
|
const eventManager = require('../eventManager');
|
||||||
const VerticalResizer = require('./VerticalResizer.min');
|
const VerticalResizer = require('./VerticalResizer.min');
|
||||||
|
const PluginManager = require('lib/services/PluginManager');
|
||||||
|
|
||||||
class MainScreenComponent extends React.Component {
|
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 modalLayerStyle = Object.assign({}, styles.modalLayer, { display: this.state.modalLayer.visible ? 'block' : 'none' });
|
||||||
|
|
||||||
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
|
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
|
||||||
@ -491,6 +495,8 @@ class MainScreenComponent extends React.Component {
|
|||||||
<NoteList style={styles.noteList} />
|
<NoteList style={styles.noteList} />
|
||||||
<VerticalResizer style={styles.verticalResizer} onDrag={this.noteList_onDrag}/>
|
<VerticalResizer style={styles.verticalResizer} onDrag={this.noteList_onDrag}/>
|
||||||
<NoteText style={styles.noteText} visiblePanes={this.props.noteVisiblePanes} />
|
<NoteText style={styles.noteText} visiblePanes={this.props.noteVisiblePanes} />
|
||||||
|
|
||||||
|
{pluginDialog}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -512,6 +518,7 @@ const mapStateToProps = (state) => {
|
|||||||
sidebarWidth: state.settings['style.sidebar.width'],
|
sidebarWidth: state.settings['style.sidebar.width'],
|
||||||
noteListWidth: state.settings['style.noteList.width'],
|
noteListWidth: state.settings['style.noteList.width'],
|
||||||
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
selectedNoteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||||
|
plugins: state.plugins,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,25 +102,25 @@ class NotePropertiesDialog extends React.Component {
|
|||||||
this.styles_ = {};
|
this.styles_ = {};
|
||||||
this.styleKey_ = styleKey;
|
this.styleKey_ = styleKey;
|
||||||
|
|
||||||
this.styles_.modalLayer = {
|
// this.styles_.modalLayer = {
|
||||||
zIndex: 9999,
|
// zIndex: 9999,
|
||||||
display: 'flex',
|
// display: 'flex',
|
||||||
position: 'absolute',
|
// position: 'absolute',
|
||||||
top: 0,
|
// top: 0,
|
||||||
left: 0,
|
// left: 0,
|
||||||
width: '100%',
|
// width: '100%',
|
||||||
height: '100%',
|
// height: '100%',
|
||||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
// backgroundColor: 'rgba(0,0,0,0.6)',
|
||||||
alignItems: 'flex-start',
|
// alignItems: 'flex-start',
|
||||||
justifyContent: 'center',
|
// justifyContent: 'center',
|
||||||
};
|
// };
|
||||||
|
|
||||||
this.styles_.dialogBox = {
|
// this.styles_.dialogBox = {
|
||||||
backgroundColor: theme.backgroundColor,
|
// backgroundColor: theme.backgroundColor,
|
||||||
padding: 16,
|
// padding: 16,
|
||||||
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
|
// boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
|
||||||
marginTop: 20,
|
// marginTop: 20,
|
||||||
}
|
// }
|
||||||
|
|
||||||
this.styles_.controlBox = {
|
this.styles_.controlBox = {
|
||||||
marginBottom: '1em',
|
marginBottom: '1em',
|
||||||
@ -153,7 +153,7 @@ class NotePropertiesDialog extends React.Component {
|
|||||||
borderColor: theme.dividerColor,
|
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_;
|
return this.styles_;
|
||||||
}
|
}
|
||||||
@ -368,8 +368,6 @@ class NotePropertiesDialog extends React.Component {
|
|||||||
|
|
||||||
const noteComps = [];
|
const noteComps = [];
|
||||||
|
|
||||||
const modalLayerStyle = Object.assign({}, styles.modalLayer);
|
|
||||||
|
|
||||||
if (formNote) {
|
if (formNote) {
|
||||||
for (let key in formNote) {
|
for (let key in formNote) {
|
||||||
if (!formNote.hasOwnProperty(key)) continue;
|
if (!formNote.hasOwnProperty(key)) continue;
|
||||||
@ -379,9 +377,9 @@ class NotePropertiesDialog extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={modalLayerStyle}>
|
<div style={theme.dialogModalLayer}>
|
||||||
<div style={styles.dialogBox}>
|
<div style={theme.dialogBox}>
|
||||||
<div style={styles.dialogTitle}>{_('Note properties')}</div>
|
<div style={theme.dialogTitle}>{_('Note properties')}</div>
|
||||||
<div>{noteComps}</div>
|
<div>{noteComps}</div>
|
||||||
<div style={{ textAlign: 'right', marginTop: 10 }}>
|
<div style={{ textAlign: 'right', marginTop: 10 }}>
|
||||||
{buttonComps}
|
{buttonComps}
|
||||||
|
@ -800,7 +800,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (Setting.value('env') === 'dev') {
|
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 = {
|
globalStyle.inputStyle = {
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
|
height: 24,
|
||||||
|
paddingLeft: 5,
|
||||||
|
paddingRight: 5,
|
||||||
|
boxSizing: 'border-box',
|
||||||
};
|
};
|
||||||
|
|
||||||
globalStyle.containerStyle = {
|
globalStyle.containerStyle = {
|
||||||
@ -67,6 +71,7 @@ const lightStyle = {
|
|||||||
colorError: "red",
|
colorError: "red",
|
||||||
colorWarn: "#9A5B00",
|
colorWarn: "#9A5B00",
|
||||||
colorFaded: "#777777", // For less important text
|
colorFaded: "#777777", // For less important text
|
||||||
|
colorBright: "#000000", // For important text
|
||||||
dividerColor: "#dddddd",
|
dividerColor: "#dddddd",
|
||||||
selectedColor: '#e5e5e5',
|
selectedColor: '#e5e5e5',
|
||||||
urlColor: '#155BDA',
|
urlColor: '#155BDA',
|
||||||
@ -103,6 +108,7 @@ const darkStyle = {
|
|||||||
colorError: "red",
|
colorError: "red",
|
||||||
colorWarn: "#9A5B00",
|
colorWarn: "#9A5B00",
|
||||||
colorFaded: "#777777", // For less important text
|
colorFaded: "#777777", // For less important text
|
||||||
|
colorBright: "#ffffff", // For important text
|
||||||
dividerColor: '#555555',
|
dividerColor: '#555555',
|
||||||
selectedColor: '#333333',
|
selectedColor: '#333333',
|
||||||
urlColor: '#4E87EE',
|
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;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ const { Database } = require('lib/database.js');
|
|||||||
const { _ } = require('lib/locale.js');
|
const { _ } = require('lib/locale.js');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const BaseItem = require('lib/models/BaseItem.js');
|
const BaseItem = require('lib/models/BaseItem.js');
|
||||||
const lodash = require('lodash');
|
const { substrWithEllipsis } = require('lib/string-utils.js');
|
||||||
|
|
||||||
class Folder extends BaseItem {
|
class Folder extends BaseItem {
|
||||||
|
|
||||||
@ -217,6 +217,34 @@ class Folder extends BaseItem {
|
|||||||
return getNestedChildren(all, '');
|
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) {
|
static buildTree(folders) {
|
||||||
const idToFolders = {};
|
const idToFolders = {};
|
||||||
for (let i = 0; i < folders.length; i++) {
|
for (let i = 0; i < folders.length; i++) {
|
||||||
|
@ -90,9 +90,19 @@ class Tag extends BaseItem {
|
|||||||
return !!r;
|
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() {
|
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 (' + this.tagsWithNotesSql_() + ')');
|
||||||
return await Tag.modelSelectAll('SELECT * FROM tags WHERE id IN (' + tagIdSql + ')');
|
}
|
||||||
|
|
||||||
|
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) {
|
static async tagsByNoteId(noteId) {
|
||||||
|
@ -48,6 +48,7 @@ const defaultState = {
|
|||||||
toFetchCount: 0,
|
toFetchCount: 0,
|
||||||
},
|
},
|
||||||
historyNotes: [],
|
historyNotes: [],
|
||||||
|
plugins: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateUtils = {};
|
const stateUtils = {};
|
||||||
@ -691,6 +692,17 @@ const reducer = (state = defaultState, action) => {
|
|||||||
newState.selectedNoteTags = action.items;
|
newState.selectedNoteTags = action.items;
|
||||||
break;
|
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) {
|
} catch (error) {
|
||||||
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
|
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) {
|
if (c === ':' && !inQuote) {
|
||||||
currentCol = currentTerm;
|
currentCol = currentTerm;
|
||||||
terms[currentCol] = [];
|
if (!terms[currentCol]) terms[currentCol] = [];
|
||||||
currentTerm = '';
|
currentTerm = '';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -368,7 +368,7 @@ class SearchEngine {
|
|||||||
return this.basicSearch(query);
|
return this.basicSearch(query);
|
||||||
} else {
|
} else {
|
||||||
const parsedQuery = this.parseQuery(query);
|
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 {
|
try {
|
||||||
const rows = await this.db().selectAll(sql, [query]);
|
const rows = await this.db().selectAll(sql, [query]);
|
||||||
this.orderResults_(rows, parsedQuery);
|
this.orderResults_(rows, parsedQuery);
|
||||||
|
Loading…
Reference in New Issue
Block a user