1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Prompt dialog and popup menu

This commit is contained in:
Laurent Cozic 2017-11-08 17:51:55 +00:00
parent 5a7fde7d21
commit 7d12da27ad
15 changed files with 364 additions and 55 deletions

View File

@ -2,6 +2,7 @@ const { _ } = require('lib/locale.js');
const { BrowserWindow } = require('electron');
const url = require('url')
const path = require('path')
const urlUtils = require('lib/urlUtils.js');
class ElectronAppWrapper {
@ -27,20 +28,6 @@ class ElectronAppWrapper {
return this.win_;
}
// store() {
// return this.store_;
// }
// dispatch(action) {
// return this.store().dispatch(action);
// }
// windowContentSize() {
// if (!this.win_) return { width: 0, height: 0 };
// const s = this.win_.getContentSize();
// return { width: s[0], height: s[1] };
// }
createWindow() {
this.win_ = new BrowserWindow({width: 800, height: 600})
@ -55,18 +42,6 @@ class ElectronAppWrapper {
this.win_.on('closed', () => {
this.win_ = null
})
this.win_.on('resize', () => {
// this.dispatch({
// type: 'WINDOW_CONTENT_SIZE_SET',
// size: this.windowContentSize(),
// });
});
// this.dispatch({
// type: 'WINDOW_CONTENT_SIZE_SET',
// size: this.windowContentSize(),
// });
}
async waitForElectronAppReady() {

View File

@ -1,3 +1,5 @@
const { _ } = require('lib/locale.js');
class Bridge {
constructor(electronWrapper) {
@ -23,6 +25,30 @@ class Bridge {
return dialog.showMessageBox(options);
}
showErrorMessageBox(message) {
return this.showMessageBox({
type: 'error',
message: message,
});
}
showConfirmMessageBox(message) {
const result = this.showMessageBox({
type: 'question',
message: message,
buttons: [_('OK'), _('Cancel')],
});
return result === 0;
}
get Menu() {
return require('electron').Menu;
}
get MenuItem() {
return require('electron').MenuItem;
}
}
let bridge_ = null;

View File

@ -11,7 +11,6 @@ class HeaderComponent extends React.Component {
}
makeButton(key, options) {
console.info(key, options);
return <a key={key} href="#" onClick={() => {options.onClick()}}>{options.title}</a>
}
@ -42,7 +41,7 @@ class HeaderComponent extends React.Component {
}
const mapStateToProps = (state) => {
return { theme: state.theme };
return { theme: state.settings.theme };
};
const Header = connect(mapStateToProps)(HeaderComponent);

View File

@ -4,11 +4,20 @@ const { Header } = require('./Header.min.js');
const { SideBar } = require('./SideBar.min.js');
const { NoteList } = require('./NoteList.min.js');
const { NoteText } = require('./NoteText.min.js');
const { PromptDialog } = require('./PromptDialog.min.js');
const { Setting } = require('lib/models/setting.js');
const { Note } = require('lib/models/note.js');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const layoutUtils = require('lib/layout-utils.js');
const { bridge } = require('electron').remote.require('./bridge');
class MainScreenComponent extends React.Component {
componentWillMount() {
this.setState({ newNotePromptVisible: false });
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
@ -40,9 +49,40 @@ class MainScreenComponent extends React.Component {
verticalAlign: 'top',
};
const promptStyle = {
width: style.width,
height: style.height,
};
const headerButtons = [
{
title: _('New note'),
onClick: () => {
this.setState({ newNotePromptVisible: true });
},
},
];
const newNotePromptOnAccept = async (answer) => {
const folderId = Setting.value('activeFolderId');
if (!folderId) return;
const note = await Note.save({
title: answer,
parent_id: folderId,
});
Note.updateGeolocation(note.id);
this.props.dispatch({
type: 'NOTES_SELECT',
noteId: note.id,
});
}
return (
<div style={style}>
<Header style={headerStyle} showBackButton={false} />
<PromptDialog style={promptStyle} onAccept={(answer) => newNotePromptOnAccept(answer)} message={_('Note title:')} visible={this.state.newNotePromptVisible}/>
<Header style={headerStyle} showBackButton={false} buttons={headerButtons} />
<SideBar style={sideBarStyle} />
<NoteList itemHeight={40} style={noteListStyle} />
<NoteText style={noteTextStyle} />

View File

@ -1,10 +1,28 @@
const { ItemList } = require('./ItemList.min.js');
const React = require('react');
const { connect } = require('react-redux');
const { themeStyle } = require('../theme.js');
const { _ } = require('lib/locale.js');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
class NoteListComponent extends React.Component {
itemRenderer(index, item) {
itemContextMenu(event) {
const noteId = event.target.getAttribute('data-id');
if (!noteId) throw new Error('No data-id on element');
const menu = new Menu()
menu.append(new MenuItem({label: _('Delete'), async click() {
const ok = bridge().showConfirmMessageBox(_('Delete note?'));
if (!ok) return;
await Note.delete(noteId);
}}))
menu.popup(bridge().window());
}
itemRenderer(index, item, theme) {
const onClick = (item) => {
this.props.dispatch({
type: 'NOTES_SELECT',
@ -12,20 +30,27 @@ class NoteListComponent extends React.Component {
});
}
let classes = ['item'];
classes.push(index % 2 === 0 ? 'even' : 'odd');
if (this.props.selectedNoteId === item.id) classes.push('selected');
return <div onClick={() => { onClick(item) }} className={classes.join(' ')} key={index}>{item.title + ' ' + item.id.substr(0,4)}</div>
const style = {
height: this.props.itemHeight,
display: 'block',
cursor: 'pointer',
backgroundColor: index % 2 === 0 ? theme.backgroundColor : theme.oddBackgroundColor,
fontWeight: this.props.selectedNoteId === item.id ? 'bold' : 'normal',
};
return <a data-id={item.id} onContextMenu={(event) => this.itemContextMenu(event)} href="#" style={style} onClick={() => { onClick(item) }} key={index}>{item.title}</a>
}
render() {
const theme = themeStyle(this.props.theme);
return (
<ItemList
itemHeight={this.props.itemHeight}
style={this.props.style}
className={"note-list"}
items={this.props.notes}
itemRenderer={ (index, item) => { return this.itemRenderer(index, item) } }
itemRenderer={ (index, item) => { return this.itemRenderer(index, item, theme) } }
></ItemList>
);
}
@ -36,6 +61,7 @@ const mapStateToProps = (state) => {
return {
notes: state.notes,
selectedNoteId: state.selectedNoteId,
theme: state.settings.theme,
};
};

View File

@ -6,6 +6,7 @@ const { reg } = require('lib/registry.js');
const MdToHtml = require('lib/MdToHtml');
const shared = require('lib/components/shared/note-screen-shared.js');
const { bridge } = require('electron').remote.require('./bridge');
const { themeStyle } = require('../theme.js');
class NoteTextComponent extends React.Component {
@ -55,6 +56,10 @@ class NoteTextComponent extends React.Component {
await shared.saveNoteButton_press(this);
}
async saveOneProperty(name, value) {
await shared.saveOneProperty(this, name, value);
}
scheduleSave() {
if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_);
this.scheduleSaveTimeout_ = setTimeout(() => {
@ -73,6 +78,7 @@ class NoteTextComponent extends React.Component {
this.setState({
note: note,
lastSavedNote: Object.assign({}, note),
mode: 'view',
});
}
@ -164,7 +170,7 @@ class NoteTextComponent extends React.Component {
webviewReady: true,
});
this.webview_.openDevTools();
//this.webview_.openDevTools();
}
webview_ref(element) {
@ -218,6 +224,7 @@ class NoteTextComponent extends React.Component {
const style = this.props.style;
const note = this.state.note;
const body = note ? note.body : '';
const theme = themeStyle(this.props.theme);
const viewerStyle = {
width: Math.floor(style.width / 2),
@ -227,12 +234,17 @@ class NoteTextComponent extends React.Component {
verticalAlign: 'top',
};
const paddingTop = 14;
const editorStyle = {
width: style.width - viewerStyle.width,
height: style.height,
height: style.height - paddingTop,
overflowY: 'scroll',
float: 'left',
verticalAlign: 'top',
paddingTop: paddingTop + 'px',
lineHeight: theme.textAreaLineHeight + 'px',
fontSize: theme.fontSize + 'px',
};
if (this.state.webviewReady) {
@ -242,7 +254,7 @@ class NoteTextComponent extends React.Component {
},
postMessageSyntax: 'ipcRenderer.sendToHost',
};
const html = this.mdToHtml().render(body, {}, mdOptions);
const html = this.mdToHtml().render(body, theme, mdOptions);
this.webview_.send('setHtml', html);
}

View File

@ -0,0 +1,107 @@
const React = require('react');
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
const { themeStyle } = require('../theme.js');
class PromptDialog extends React.Component {
componentWillMount() {
this.setState({
visible: false,
answer: '',
});
this.focusInput_ = true;
}
componentWillReceiveProps(newProps) {
if ('visible' in newProps) {
this.setState({ visible: newProps.visible });
if (newProps.visible) this.focusInput_ = true;
}
}
componentDidUpdate() {
if (this.focusInput_ && this.answerInput_) this.answerInput_.focus();
this.focusInput_ = false;
}
render() {
const style = this.props.style;
const theme = themeStyle(this.props.theme);
const modalLayerStyle = {
zIndex: 9999,
position: 'absolute',
top: 0,
left: 0,
width: style.width,
height: style.height,
backgroundColor: 'rgba(0,0,0,0.6)',
display: this.state.visible ? 'flex' : 'none',
alignItems: 'center',
justifyContent: 'center',
};
const promptDialogStyle = {
backgroundColor: 'white',
padding: 10,
display: 'inline-block',
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)',
};
const buttonStyle = {
minWidth: theme.buttonMinWidth,
minHeight: theme.buttonMinHeight,
marginLeft: 5,
};
const inputStyle = {
width: 0.5 * style.width,
maxWidth: 400,
};
const onAccept = () => {
if (this.props.onAccept) this.props.onAccept(this.state.answer);
this.setState({ visible: false, answer: '' });
}
const onReject = () => {
if (this.props.onReject) this.props.onReject();
this.setState({ visible: false, answer: '' });
}
const onChange = (event) => {
this.setState({ answer: event.target.value });
}
const onKeyDown = (event) => {
if (event.key === 'Enter') {
onAccept();
} else if (event.key === 'Escape') {
onReject();
}
}
return (
<div style={modalLayerStyle}>
<div style={promptDialogStyle}>
<label style={{ marginRight: 5 }}>{this.props.message ? this.props.message : ''}</label>
<input
style={inputStyle}
ref={input => this.answerInput_ = input}
value={this.state.answer}
type="text"
onChange={(event) => onChange(event)}
onKeyDown={(event) => onKeyDown(event)} />
<div style={{ textAlign: 'right', marginTop: 10 }}>
<button style={buttonStyle} onClick={() => onAccept()}>OK</button>
<button style={buttonStyle} onClick={() => onReject()}>Cancel</button>
</div>
</div>
</div>
);
}
}
module.exports = { PromptDialog };

View File

@ -2,9 +2,27 @@ const React = require('react');
const { connect } = require('react-redux');
const shared = require('lib/components/shared/side-menu-shared.js');
const { Synchronizer } = require('lib/synchronizer.js');
const { themeStyle } = require('../theme.js');
class SideBarComponent extends React.Component {
style() {
const theme = themeStyle(this.props.theme);
const itemHeight = 20;
let style = {
root: {},
listItem: {
display: 'block',
cursor: 'pointer',
height: itemHeight,
},
};
return style;
}
folderItem_click(folder) {
this.props.dispatch({
type: 'FOLDERS_SELECT',
@ -24,15 +42,17 @@ class SideBarComponent extends React.Component {
}
folderItem(folder, selected) {
let classes = [];
if (selected) classes.push('selected');
return <div key={folder.id} className={classes.join(' ')} onClick={() => {this.folderItem_click(folder)}}>{folder.title}</div>
const style = Object.assign({}, this.style().listItem, {
fontWeight: selected ? 'bold' : 'normal',
});
return <a href="#" key={folder.id} style={style} onClick={() => {this.folderItem_click(folder)}}>{folder.title}</a>
}
tagItem(tag, selected) {
let classes = [];
if (selected) classes.push('selected');
return <div key={tag.id} className={classes.join(' ')} onClick={() => {this.tagItem_click(tag)}}>Tag: {tag.title}</div>
const style = Object.assign({}, this.style().listItem, {
fontWeight: selected ? 'bold' : 'normal',
});
return <a href="#" key={tag.id} style={style} onClick={() => {this.tagItem_click(tag)}}>Tag: {tag.title}</a>
}
makeDivider(key) {
@ -44,6 +64,9 @@ class SideBarComponent extends React.Component {
}
render() {
const theme = themeStyle(this.props.theme);
const style = Object.assign({}, this.style().root, this.props.style);
let items = [];
if (this.props.folders.length) {
@ -69,7 +92,7 @@ class SideBarComponent extends React.Component {
items.push(<div key='sync_report'>{syncReportText}</div>);
return (
<div className="side-bar" style={this.props.style}>
<div className="side-bar" style={style}>
{items}
</div>
);

View File

@ -2,12 +2,11 @@
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<title>Joplin</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="react-root"></div>
<!-- <script src="gui/Root.min.js"></script> -->
<script src="main-html.js"></script>
</body>
</html>

View File

@ -1360,6 +1360,25 @@
"yargs": "10.0.3"
}
},
"electron-context-menu": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-0.9.1.tgz",
"integrity": "sha1-7U3yDAgEkcPJlqv8s2MVmUajgFg=",
"requires": {
"electron-dl": "1.10.0",
"electron-is-dev": "0.1.2"
}
},
"electron-dl": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.10.0.tgz",
"integrity": "sha1-+UQWBkBW/G8qhq5JhhTJNSaJCvk=",
"requires": {
"ext-name": "5.0.0",
"pupa": "1.0.0",
"unused-filename": "1.0.0"
}
},
"electron-download-tf": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/electron-download-tf/-/electron-download-tf-4.3.4.tgz",
@ -1377,6 +1396,11 @@
"sumchecker": "2.0.2"
}
},
"electron-is-dev": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-0.1.2.tgz",
"integrity": "sha1-ihBD4ys6HaHD9VPc4oznZCRhZ+M="
},
"electron-osx-sign": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.7.tgz",
@ -1617,6 +1641,23 @@
"integrity": "sha512-kkjwkMqj0h4w/sb32ERCDxCQkREMCAgS39DscDnSwDsbxnwwM1BTZySdC3Bn1lhY7vL08n9GoO/fVTynjDgRyQ==",
"dev": true
},
"ext-list": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
"integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
"requires": {
"mime-db": "1.30.0"
}
},
"ext-name": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
"integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
"requires": {
"ext-list": "2.2.2",
"sort-keys-length": "1.0.1"
}
},
"extend": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@ -2275,6 +2316,11 @@
"path-is-inside": "1.0.2"
}
},
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"is-posix-bracket": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
@ -2719,8 +2765,7 @@
"mime-db": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
"integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=",
"dev": true
"integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
},
"mime-types": {
"version": "2.1.17",
@ -2769,6 +2814,11 @@
}
}
},
"modify-filename": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
},
"moment": {
"version": "2.19.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.19.1.tgz",
@ -3134,8 +3184,7 @@
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
},
"path-is-absolute": {
"version": "1.0.1",
@ -3317,6 +3366,11 @@
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
"pupa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz",
"integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y="
},
"q": {
"version": "0.9.7",
"resolved": "https://registry.npmjs.org/q/-/q-0.9.7.tgz",
@ -3771,6 +3825,22 @@
"hoek": "4.2.0"
}
},
"sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
"requires": {
"is-plain-obj": "1.1.0"
}
},
"sort-keys-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
"requires": {
"sort-keys": "1.1.2"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -4890,6 +4960,15 @@
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
"integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc="
},
"unused-filename": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz",
"integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=",
"requires": {
"modify-filename": "1.1.0",
"path-exists": "3.0.0"
}
},
"unzip-response": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz",

View File

@ -26,6 +26,7 @@
},
"dependencies": {
"app-module-path": "^2.2.0",
"electron-context-menu": "^0.9.1",
"fs-extra": "^4.0.2",
"html-entities": "^1.2.1",
"lodash": "^4.17.4",

View File

@ -6,6 +6,7 @@ const globalStyle = {
itemMarginTop: 10,
itemMarginBottom: 10,
backgroundColor: "#ffffff",
oddBackgroundColor: "#dddddd",
color: "#555555", // For regular text
colorError: "red",
colorWarn: "#9A5B00",
@ -15,17 +16,21 @@ const globalStyle = {
selectedColor: '#e5e5e5',
disabledOpacity: 0.3,
headerHeight: 20,
buttonMinWidth: 50,
buttonMinHeight: 30,
textAreaLineHeight: 17,
raisedBackgroundColor: "#0080EF",
raisedColor: "#003363",
raisedHighlightedColor: "#ffffff",
// For WebView - must correspond to the properties above
htmlFontSize: '20x',
htmlFontSize: '16px',
htmlColor: 'black', // Note: CSS in WebView component only supports named colors or rgb() notation
htmlBackgroundColor: 'white',
htmlDividerColor: 'Gainsboro',
htmlLinkColor: 'blue',
htmlLineHeight: '20px',
};
globalStyle.marginRight = globalStyle.margin;

View File

@ -203,6 +203,8 @@ class MdToHtml {
if (!options) options = {};
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
console.info(style);
const cacheKey = this.makeContentKey(this.loadedResources_, body, style, options);
if (this.cachedContentKey_ === cacheKey) return this.cachedContent_;
@ -255,8 +257,13 @@ class MdToHtml {
body {
font-size: ` + style.htmlFontSize + `;
color: ` + style.htmlColor + `;
line-height: 1.5em;
line-height: ` + style.htmlLineHeight + `;
background-color: ` + style.htmlBackgroundColor + `;
font-family: sans-serif;
}
p, h1, h2, h3, h4, ul {
margin-top: 14px;
margin-bottom: 14px;
}
h1 {
font-size: 1.2em;

View File

@ -20,11 +20,12 @@ const globalStyle = {
raisedHighlightedColor: "#ffffff",
// For WebView - must correspond to the properties above
htmlFontSize: '20x',
htmlFontSize: '16px',
htmlColor: 'black', // Note: CSS in WebView component only supports named colors or rgb() notation
htmlBackgroundColor: 'white',
htmlDividerColor: 'Gainsboro',
htmlLinkColor: 'blue',
htmlLineHeight: '20px',
};
globalStyle.marginRight = globalStyle.margin;

View File

@ -0,0 +1,9 @@
const urlUtils = {};
urlUtils.hash = function(url) {
const s = url.split('#');
if (s.length <= 1) return '';
return s[s.length - 1];
}
module.exports = urlUtils;