You've already forked joplin
							
							
				mirror of
				https://github.com/laurent22/joplin.git
				synced 2025-10-31 00:07:48 +02:00 
			
		
		
		
	Show editor and rendering side by side
This commit is contained in:
		| @@ -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', | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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> | ||||
							
								
								
									
										23
									
								
								ElectronClient/app/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										23
									
								
								ElectronClient/app/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| body { | ||||
| body, textarea { | ||||
| 	margin: 0; | ||||
| 	padding: 0; | ||||
| 	border: none; | ||||
| } | ||||
|  | ||||
| #react-root { | ||||
|   | ||||
| @@ -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; | ||||
| 		} | ||||
|   | ||||
							
								
								
									
										9
									
								
								ReactNativeClient/lib/layout-utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ReactNativeClient/lib/layout-utils.js
									
									
									
									
									
										Normal 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; | ||||
		Reference in New Issue
	
	Block a user