diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 8ecf1851..b837c206 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -2,31 +2,20 @@ import React, { PropTypes } from 'react' import ReactDOM from 'react-dom' import modes from '../lib/modes' import _ from 'lodash' -import fetchConfig from '../lib/fetchConfig' - -const electron = require('electron') -const remote = electron.remote -const ipc = electron.ipcRenderer const ace = window.ace -let config = fetchConfig() -ipc.on('config-apply', function (e, newConfig) { - config = newConfig -}) - export default class CodeEditor extends React.Component { constructor (props) { super(props) - this.configApplyHandler = (e, config) => this.handleConfigApply(e, config) - this.changeHandler = e => this.handleChange(e) + this.changeHandler = (e) => this.handleChange(e) this.blurHandler = (e) => { if (e.relatedTarget === null) { return } - let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some(clss => { + let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some((clss) => { return clss === 'ace_search_field' || clss === 'ace_searchbtn' || clss === 'ace_replacebtn' || clss === 'ace_searchbtn_close' || clss === 'ace_text-input' }) if (isFocusingToSearch) { @@ -38,7 +27,7 @@ export default class CodeEditor extends React.Component { this.killedBuffer = '' this.execHandler = (e) => { - console.log(e.command.name) + console.info('ACE COMMAND >> %s', e.command.name) switch (e.command.name) { case 'gotolinestart': e.preventDefault() @@ -84,7 +73,7 @@ export default class CodeEditor extends React.Component { this.afterExecHandler = (e) => { switch (e.command.name) { case 'find': - Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), el => { + Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), (el) => { el.removeEventListener('blur', this.blurHandler) el.addEventListener('blur', this.blurHandler) }) @@ -93,11 +82,6 @@ export default class CodeEditor extends React.Component { } this.state = { - fontSize: config['editor-font-size'], - fontFamily: config['editor-font-family'], - indentType: config['editor-indent-type'], - indentSize: config['editor-indent-size'], - themeSyntax: config['theme-syntax'] } this.silentChange = false @@ -110,15 +94,15 @@ export default class CodeEditor extends React.Component { } componentDidMount () { - let { article } = this.props - var el = ReactDOM.findDOMNode(this) - var editor = this.editor = ace.edit(el) + let { mode, value } = this.props + let el = ReactDOM.findDOMNode(this) + let editor = this.editor = ace.edit(el) editor.$blockScrolling = Infinity editor.renderer.setShowGutter(true) - editor.setTheme('ace/theme/' + this.state.themeSyntax) + editor.setTheme('ace/theme/xcode') editor.moveCursorTo(0, 0) editor.setReadOnly(!!this.props.readOnly) - editor.setFontSize(this.state.fontSize) + editor.setFontSize('14') editor.on('blur', this.blurHandler) @@ -132,31 +116,19 @@ export default class CodeEditor extends React.Component { readOnly: true }) editor.commands.addCommand({ - name: 'Emacs cursor up', + name: 'Emacs kill buffer', bindKey: {mac: 'Ctrl-Y'}, exec: function (editor) { editor.insert(this.killedBuffer) }.bind(this), readOnly: true }) - editor.commands.addCommand({ - name: 'Focus title', - bindKey: {win: 'Esc', mac: 'Esc'}, - exec: function (editor, e) { - let currentWindow = remote.getCurrentWebContents() - if (config['switch-preview'] === 'rightclick') { - currentWindow.send('detail-preview') - } - currentWindow.send('list-focus') - }, - readOnly: true - }) editor.commands.on('exec', this.execHandler) editor.commands.on('afterExec', this.afterExecHandler) var session = editor.getSession() - let mode = _.findWhere(modes, {name: article.mode}) + mode = _.find(modes, {name: mode}) let syntaxMode = mode != null ? mode.mode : 'text' @@ -166,15 +138,12 @@ export default class CodeEditor extends React.Component { session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4) session.setOption('useWorker', false) session.setUseWrapMode(true) - session.setValue(this.props.article.content) + session.setValue(_.isString(value) ? value : '') session.on('change', this.changeHandler) - - ipc.on('config-apply', this.configApplyHandler) } componentWillUnmount () { - ipc.removeListener('config-apply', this.configApplyHandler) this.editor.getSession().removeListener('change', this.changeHandler) this.editor.removeListener('blur', this.blurHandler) this.editor.commands.removeListener('exec', this.execHandler) @@ -183,41 +152,37 @@ export default class CodeEditor extends React.Component { componentDidUpdate (prevProps, prevState) { var session = this.editor.getSession() - if (this.props.article.key !== prevProps.article.key) { - session.removeListener('change', this.changeHandler) - session.setValue(this.props.article.content) - session.getUndoManager().reset() - session.on('change', this.changeHandler) - } - if (prevProps.article.mode !== this.props.article.mode) { - let mode = _.findWhere(modes, {name: this.props.article.mode}) + + if (prevProps.mode !== this.props.mode) { + let mode = _.find(modes, {name: this.props.mode}) let syntaxMode = mode != null ? mode.mode : 'text' - session.setMode('ace/mode/' + syntaxMode) + session.setMode('ace/mode' + syntaxMode) } } handleConfigApply (e, config) { - this.setState({ - fontSize: config['editor-font-size'], - fontFamily: config['editor-font-family'], - indentType: config['editor-indent-type'], - indentSize: config['editor-indent-size'], - themeSyntax: config['theme-syntax'] - }, function () { - var editor = this.editor - editor.setTheme('ace/theme/' + this.state.themeSyntax) + // this.setState({ + // fontSize: config['editor-font-size'], + // fontFamily: config['editor-font-family'], + // indentType: config['editor-indent-type'], + // indentSize: config['editor-indent-size'], + // themeSyntax: config['theme-syntax'] + // }, function () { + // var editor = this.editor + // editor.setTheme('ace/theme/' + this.state.themeSyntax) - var session = editor.getSession() - session.setUseSoftTabs(this.state.indentType === 'space') - session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4) - }) + // var session = editor.getSession() + // session.setUseSoftTabs(this.state.indentType === 'space') + // session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4) + // }) } + handleChange (e) { if (this.props.onChange) { - var value = this.editor.getValue() - this.props.onChange(value) + this.value = this.editor.getValue() + this.props.onChange(e) } } @@ -237,13 +202,34 @@ export default class CodeEditor extends React.Component { this.editor.scrollToLine(num, false, false) } + focus () { + this.editor.focus() + } + + blur () { + this.editor.blur() + } + + reload () { + let session = this.editor.getSession() + session.removeListener('change', this.changeHandler) + session.setValue(this.props.value) + session.getUndoManager().reset() + session.on('change', this.changeHandler) + } + render () { + let { className } = this.props + return (
) @@ -251,11 +237,8 @@ export default class CodeEditor extends React.Component { } CodeEditor.propTypes = { - article: PropTypes.shape({ - content: PropTypes.string, - mode: PropTypes.string, - key: PropTypes.string - }), + value: PropTypes.string, + mode: PropTypes.string, className: PropTypes.string, onBlur: PropTypes.func, onChange: PropTypes.func, diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js new file mode 100644 index 00000000..c2e359d8 --- /dev/null +++ b/browser/components/MarkdownEditor.js @@ -0,0 +1,81 @@ +import React, { PropTypes } from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './MarkdownEditor.styl' +import CodeEditor from 'browser/components/CodeEditor' +import MarkdownPreview from 'browser/components/MarkdownPreview' + +class MarkdownEditor extends React.Component { + constructor (props) { + super(props) + + this.state = { + status: 'CODE' + } + } + + handleChange (e) { + this.value = this.refs.code.value + this.props.onChange(e) + } + + handleContextMenu (e) { + let newStatus = this.state.status === 'PREVIEW' + ? 'CODE' + : 'PREVIEW' + this.setState({ + status: newStatus + }, () => { + if (newStatus === 'CODE') { + this.refs.code.focus() + } else { + this.refs.code.blur() + this.refs.preview.focus() + } + }) + } + + reload () { + this.refs.code.reload() + } + + render () { + let { className, value } = this.props + + return ( +
this.handleContextMenu(e)} + > + this.handleChange(e)} + /> + this.handleContextMenu(e)} + tabIndex='0' + value={value} + /> +
+ ) + } +} + +MarkdownEditor.propTypes = { + className: PropTypes.string, + value: PropTypes.string, + onChange: PropTypes.func, + ignorePreviewPointerEvents: PropTypes.bool +} + +export default CSSModules(MarkdownEditor, styles) diff --git a/browser/components/MarkdownEditor.styl b/browser/components/MarkdownEditor.styl new file mode 100644 index 00000000..d62114ac --- /dev/null +++ b/browser/components/MarkdownEditor.styl @@ -0,0 +1,23 @@ +.root + position relative + +.codeEditor + absolute top bottom left right + +.codeEditor--hide + @extend .codeEditor + +.preview + display block + absolute top bottom left right + z-index 100 + background-color white + height 100% + width 100% + +.preview--hide + @extend .preview + z-index 0 + opacity 0 + pointer-events none + diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index f4c00971..edcbadce 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -1,214 +1,81 @@ import React, { PropTypes } from 'react' -import markdown from '../lib/markdown' -import ReactDOM from 'react-dom' -import sanitizeHtml from '@rokt33r/sanitize-html' -import _ from 'lodash' -import fetchConfig from '../lib/fetchConfig' +import markdown from 'browser/lib/markdown' -const electron = require('electron') -const shell = electron.shell -const ipc = electron.ipcRenderer - -const katex = window.katex - -const OSX = global.process.platform === 'darwin' - -const sanitizeOpts = { - allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', - 'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', - 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ], - allowedClasses: { - 'a': ['lineAnchor'], - 'div': ['math'], - 'pre': ['hljs'], - 'span': ['math', 'hljs-*', 'lineNumber'], - 'code': ['language-*'] - }, - allowedAttributes: { - a: ['href', 'data-key'], - img: [ 'src' ], - label: ['for'], - input: ['checked', 'type'], - '*': ['id', 'name'] - }, - transformTags: { - '*': function (tagName, attribs) { - let href = attribs.href - if (tagName === 'input' && attribs.type !== 'checkbox') { - return false - } - if (_.isString(href) && href.match(/^#.+$/)) attribs.href = href.replace(/^#/, '#md-anchor-') - if (attribs.id) attribs.id = 'md-anchor-' + attribs.id - if (attribs.name) attribs.name = 'md-anchor-' + attribs.name - if (attribs.for) attribs.for = 'md-anchor-' + attribs.for - return { - tagName: tagName, - attribs: attribs - } - } - } -} - -function handleAnchorClick (e) { - if (this.attributes.href && this.attributes.href.nodeValue.match(/^#.+/)) { - return - } +const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] +const { shell } = require('electron') +const goExternal = function (e) { e.preventDefault() - e.stopPropagation() - let href = this.href - if (href && href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) { - shell.openExternal(href) - } + shell.openExternal(e.target.href) } -function stopPropagation (e) { - e.preventDefault() - e.stopPropagation() -} - -function math2Katex (display) { - return function (el) { - try { - katex.render(el.innerHTML.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/&/g, '&'), el, {display: display}) - el.className = 'math-rendered' - } catch (e) { - el.innerHTML = e.message - el.className = 'math-failed' - } - } -} - -let config = fetchConfig() -ipc.on('config-apply', function (e, newConfig) { - config = newConfig -}) - export default class MarkdownPreview extends React.Component { constructor (props) { super(props) - this.configApplyHandler = (e, config) => this.handleConfigApply(e, config) - - this.state = { - fontSize: config['preview-font-size'], - fontFamily: config['preview-font-family'], - lineNumber: config['preview-line-number'] - } + this.contextMenuHandler = (e) => this.handleContextMenu(e) } + + handleContextMenu (e) { + this.props.onContextMenu(e) + } + componentDidMount () { - this.addListener() - this.renderMath() - ipc.on('config-apply', this.configApplyHandler) - } - - componentDidUpdate () { - this.addListener() - this.renderMath() + this.refs.root.setAttribute('sandbox', 'allow-same-origin') + this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) + this.rewriteIframe() } componentWillUnmount () { - this.removeListener() - ipc.removeListener('config-apply', this.configApplyHandler) + this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler) } - componentWillUpdate () { - this.removeListener() + componentDidUpdate (prevProps) { + if (prevProps.value !== this.props.value) this.rewriteIframe() } - renderMath () { - let inline = ReactDOM.findDOMNode(this).querySelectorAll('span.math') - Array.prototype.forEach.call(inline, math2Katex(false)) - let block = ReactDOM.findDOMNode(this).querySelectorAll('div.math') - Array.prototype.forEach.call(block, math2Katex(true)) - } - - addListener () { - var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)') - var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input') - - Array.prototype.forEach.call(anchors, anchor => { - anchor.addEventListener('click', handleAnchorClick) - anchor.addEventListener('mousedown', stopPropagation) - anchor.addEventListener('mouseup', stopPropagation) + rewriteIframe () { + Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { + el.removeEventListener('click', goExternal) }) - Array.prototype.forEach.call(inputs, input => { - input.addEventListener('click', stopPropagation) + + let { value } = this.props + this.refs.root.contentWindow.document.head.innerHTML = ` + + + + ` + this.refs.root.contentWindow.document.body.innerHTML = markdown(value) + + Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { + el.addEventListener('click', goExternal) }) } - removeListener () { - var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)') - var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input') - - Array.prototype.forEach.call(anchors, anchor => { - anchor.removeEventListener('click', handleAnchorClick) - anchor.removeEventListener('mousedown', stopPropagation) - anchor.removeEventListener('mouseup', stopPropagation) - }) - Array.prototype.forEach.call(inputs, input => { - input.removeEventListener('click', stopPropagation) - }) - } - - handleClick (e) { - if (this.props.onClick) { - this.props.onClick(e) - } - } - - handleDoubleClick (e) { - if (this.props.onDoubleClick) { - this.props.onDoubleClick(e) - } - } - - handleMouseDown (e) { - if (this.props.onMouseDown) { - this.props.onMouseDown(e) - } - } - - handleMouseUp (e) { - if (this.props.onMouseUp) { - this.props.onMouseUp(e) - } - } - - handleMouseMove (e) { - if (this.props.onMouseMove) { - this.props.onMouseMove(e) - } - } - - handleConfigApply (e, config) { - this.setState({ - fontSize: config['preview-font-size'], - fontFamily: config['preview-font-family'], - lineNumber: config['preview-line-number'] - }) + focus () { + this.refs.root.focus() } render () { - let isEmpty = this.props.content.trim().length === 0 - let content = isEmpty - ? '(Empty content)' - : this.props.content - content = markdown(content) - content = sanitizeHtml(content, sanitizeOpts) - + let { className, style, tabIndex } = this.props return ( -
this.handleClick(e)} - onDoubleClick={e => this.handleDoubleClick(e)} - onMouseDown={e => this.handleMouseDown(e)} - onMouseMove={e => this.handleMouseMove(e)} - onMouseUp={e => this.handleMouseUp(e)} - dangerouslySetInnerHTML={{__html: ' ' + content}} - style={{ - fontSize: this.state.fontSize, - fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif' - }} +