1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-04-11 11:12:03 +02:00

Show editor and rendering side by side

This commit is contained in:
Laurent Cozic 2017-11-07 21:11:14 +00:00
parent 9e3af21e66
commit 8bf85b6b88
8 changed files with 244 additions and 40 deletions

View File

@ -5,6 +5,7 @@ const { SideBar } = require('./SideBar.min.js');
const { NoteList } = require('./NoteList.min.js');
const { NoteText } = require('./NoteText.min.js');
const { themeStyle } = require('../theme.js');
const layoutUtils = require('lib/layout-utils.js');
class MainScreenComponent extends React.Component {
@ -18,22 +19,22 @@ class MainScreenComponent extends React.Component {
const rowHeight = style.height - theme.headerHeight;
const sideBarStyle = {
width: layoutUtils.size(style.width * .2, 100, 300),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const noteListStyle = {
width: Math.floor(style.width / 3),
width: layoutUtils.size(style.width * .2, 100, 300),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const noteTextStyle = {
width: noteListStyle.width,
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',
};
const sideBarStyle = {
width: style.width - (noteTextStyle.width + noteListStyle.width),
width: layoutUtils.size(style.width - sideBarStyle.width - noteListStyle.width, 0),
height: rowHeight,
display: 'inline-block',
verticalAlign: 'top',

View File

@ -1,8 +1,11 @@
const React = require('react');
const { Note } = require('lib/models/note.js');
const { connect } = require('react-redux');
const { _ } = require('lib/locale.js');
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');
class NoteTextComponent extends React.Component {
@ -18,34 +21,45 @@ class NoteTextComponent extends React.Component {
lastSavedNote: null,
isLoading: true,
webviewReady: false,
scrollHeight: null,
};
this.lastLoadedNoteId_ = null;
this.webviewListeners_ = null;
this.ignoreNextEditorScroll_ = false;
}
mdToHtml() {
if (this.mdToHtml_) return this.mdToHtml_;
this.mdToHtml_ = new MdToHtml();
return this.mdToHtml_;
}
async componentWillMount() {
this.mdToHtml_ = new MdToHtml();
await shared.initState(this);
}
componentDidMount() {
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
}
componentWillUnmount() {
this.mdToHtml_ = null;
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
this.destroyWebview();
}
async componentWillReceiveProps(nextProps) {
if ('noteId' in nextProps) {
this.mdToHtml_ = null;
const noteId = nextProps.noteId;
this.lastLoadedNoteId_ = noteId;
const note = noteId ? await Note.load(noteId) : null;
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
this.setState({ note: note });
this.setState({
note: note,
mode: 'view',
});
}
}
@ -63,6 +77,7 @@ class NoteTextComponent extends React.Component {
body_changeText(text) {
shared.noteComponent_change(this, 'body', text);
//this.updateScrollHeight();
}
async saveNoteButton_press() {
@ -81,25 +96,137 @@ class NoteTextComponent extends React.Component {
shared.showMetadata_onPress(this);
}
webview_ipcMessage(event) {
const msg = event.channel ? event.channel : '';
const args = event.args;
const arg0 = args && args.length >= 1 ? args[0] : null;
const arg1 = args && args.length >= 2 ? args[1] : null;
reg.logger().info('Got ipc-message: ' + msg, args);
if (msg.indexOf('checkboxclick:') === 0) {
const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.state.note.body);
this.saveOneProperty('body', newBody);
} else if (msg.toLowerCase().indexOf('http') === 0) {
require('electron').shell.openExternal(msg);
} else if (msg === 'editNote') {
const lineIndex = arg0 && arg0.length ? arg0[0] : 0;
this.webview_ref(null);
this.setState({
mode: 'edit',
webviewReady: false,
});
} else if (msg === 'percentScroll') {
this.ignoreNextEditorScroll_ = true;
this.setEditorPercentScroll(arg0);
} else {
bridge().showMessageBox({
type: 'error',
message: _('Unsupported link or message: %s', msg),
});
}
}
editorMaxScroll() {
return Math.max(0, this.editor_.scrollHeight - this.editor_.clientHeight);
}
setEditorPercentScroll(p) {
this.editor_.scrollTop = p * this.editorMaxScroll();
}
setViewerPercentScroll(p) {
this.webview_.send('setPercentScroll', p);
}
editor_scroll() {
if (this.ignoreNextEditorScroll_) {
this.ignoreNextEditorScroll_ = false;
return;
}
const m = this.editorMaxScroll();
this.setViewerPercentScroll(m ? this.editor_.scrollTop / m : 0);
}
webview_domReady() {
if (!this.webview_) return;
this.setState({
webviewReady: true,
});
this.webview_.openDevTools();
}
this.webview_.addEventListener('ipc-message', (event) => {
const msg = event.channel;
webview_ref(element) {
if (this.webview_) {
if (this.webview_ === element) return;
this.destroyWebview();
}
if (msg.indexOf('checkboxclick:') === 0) {
const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.state.note.body);
this.saveOneProperty('body', newBody);
}
})
if (!element) {
this.destroyWebview();
} else {
this.initWebview(element);
}
}
editor_ref(element) {
if (this.editor_ === element) return;
this.editor_ = element;
}
initWebview(wv) {
if (!this.webviewListeners_) {
this.webviewListeners_ = {
'dom-ready': this.webview_domReady.bind(this),
'ipc-message': this.webview_ipcMessage.bind(this),
};
}
for (let n in this.webviewListeners_) {
if (!this.webviewListeners_.hasOwnProperty(n)) continue;
const fn = this.webviewListeners_[n];
wv.addEventListener(n, fn);
}
this.webview_ = wv;
}
destroyWebview() {
if (!this.webview_) return;
for (let n in this.webviewListeners_) {
if (!this.webviewListeners_.hasOwnProperty(n)) continue;
const fn = this.webviewListeners_[n];
this.webview_.removeEventListener(n, fn);
}
this.webview_ = null;
}
render() {
const style = this.props.style;
const note = this.state.note;
const body = note ? note.body : '';
console.info(this.state.scrollHeight);
const viewerStyle = {
width: Math.floor(style.width / 2),
height: style.height,
overflow: 'hidden',
float: 'left',
verticalAlign: 'top',
};
const editorStyle = {
width: style.width - viewerStyle.width,
height: style.height,
overflowY: 'scroll',
float: 'left',
verticalAlign: 'top',
};
if (this.state.webviewReady) {
const mdOptions = {
@ -108,21 +235,17 @@ class NoteTextComponent extends React.Component {
},
postMessageSyntax: 'ipcRenderer.sendToHost',
};
const html = this.mdToHtml_.render(note ? note.body : '', {}, mdOptions);
const html = this.mdToHtml().render(body, {}, mdOptions);
this.webview_.send('setHtml', html);
}
const webviewStyle = {
width: this.props.style.width,
height: this.props.style.height,
overflow: 'hidden',
};
const viewer = <webview style={viewerStyle} nodeintegration="1" src="note-content.html" ref={(elem) => { this.webview_ref(elem); } } />
const editor = <textarea style={editorStyle} value={body} onScroll={() => { this.editor_scroll(); }} onChange={(text) => { this.body_changeText(text) }} ref={(elem) => { this.editor_ref(elem); } }></textarea>
return (
<div style={this.props.style}>
<webview style={webviewStyle} nodeintegration="1" src="note-content.html" ref={elem => this.webview_ = elem} />
<div style={style}>
{ editor }
{ viewer }
</div>
);
}
@ -132,7 +255,6 @@ class NoteTextComponent extends React.Component {
const mapStateToProps = (state) => {
return {
noteId: state.selectedNoteId,
//notes: state.notes,
folderId: state.selectedFolderId,
itemType: state.selectedItemType,
folders: state.folders,

View File

@ -1,8 +1,60 @@
<style>
body {
overflow: hidden;
}
#content {
overflow-y: scroll;
height: 100%;
}
</style>
<div id="content"></div>
<script>
const {ipcRenderer} = require('electron')
const { ipcRenderer } = require('electron');
const contentElement = document.getElementById('content');
ipcRenderer.on('setHtml', (event, html) => {
document.getElementById('content').innerHTML = html;
contentElement.innerHTML = html;
});
let ignoreNextScroll = false;
ipcRenderer.on('setPercentScroll', (event, percent) => {
ignoreNextScroll = true;
contentElement.scrollTop = percent * maxScrollTop();
});
function elementMapCoordinates(element) {
while (true) {
if (!element) break;
const m = element.getAttribute('data-map');
if (m) return m.split(':');
element = element.parentElement;
}
return null;
}
function maxScrollTop() {
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
}
contentElement.addEventListener('scroll', function(e) {
if (ignoreNextScroll) {
ignoreNextScroll = false;
return;
}
const m = maxScrollTop();
ipcRenderer.sendToHost('percentScroll', m ? contentElement.scrollTop / m : 0);
});
// document.addEventListener('click', function(e) {
// e = e || window.event;
// const target = e.target || e.srcElement;
// const coords = elementMapCoordinates(target);
// ipcRenderer.sendToHost('editNote', coords);
// }, false);
</script>

View File

@ -883,6 +883,11 @@
"supports-color": "4.5.0"
}
},
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
},
"chokidar": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
@ -1078,6 +1083,11 @@
"which": "1.3.0"
}
},
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
},
"cryptiles": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
@ -2147,8 +2157,7 @@
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"dev": true
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"is-builtin-module": {
"version": "1.0.0",
@ -2637,6 +2646,16 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz",
"integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc="
},
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
"requires": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "1.1.6"
}
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",

View File

@ -31,6 +31,7 @@
"lodash": "^4.17.4",
"markdown-it": "^8.4.0",
"marked": "^0.3.6",
"md5": "^2.2.1",
"moment": "^2.19.1",
"node-fetch": "^1.7.3",
"promise": "^8.0.1",

View File

@ -1,6 +1,7 @@
body {
body, textarea {
margin: 0;
padding: 0;
border: none;
}
#react-root {

View File

@ -122,7 +122,6 @@ class MdToHtml {
return '[Resource not yet supported: ' + htmlentities(text) + ']';
} else {
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
//let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + htmlentities(text) + '</a>';
let output = "<a title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>";
return output;
}

View File

@ -0,0 +1,9 @@
const layoutUtils = {};
layoutUtils.size = function(prefered, min, max) {
if (prefered < min) return min;
if (typeof max !== 'undefined' && prefered > max) return max;
return prefered;
}
module.exports = layoutUtils;