diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 8ecf1851..6ca06ab3 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -2,43 +2,34 @@ 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 -}) +const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] 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 + e.stopPropagation() + let el = e.relatedTarget + let isStillFocused = false + while (el != null) { + if (el === this.refs.root) { + isStillFocused = true + break + } + el = el.parentNode } - 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) { - return - } - - if (this.props.onBlur) this.props.onBlur(e) + if (!isStillFocused && this.props.onBlur != null) this.props.onBlur(e) } 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 +75,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 +84,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 +96,16 @@ 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, theme, fontSize } = this.props + this.value = value + 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/' + theme) editor.moveCursorTo(0, 0) editor.setReadOnly(!!this.props.readOnly) - editor.setFontSize(this.state.fontSize) + editor.setFontSize(fontSize) editor.on('blur', this.blurHandler) @@ -132,49 +119,34 @@ 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' session.setMode('ace/mode/' + syntaxMode) - session.setUseSoftTabs(this.state.indentType === 'space') - session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4) - session.setOption('useWorker', false) + session.setUseSoftTabs(this.props.indentType === 'space') + session.setTabSize(this.props.indentSize) + session.setOption('useWorker', true) 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) @@ -182,42 +154,36 @@ 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}) + let { value } = this.props + this.value = value + let editor = this.editor + let session = this.editor.getSession() + + 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) } + if (prevProps.theme !== this.props.theme) { + editor.setTheme('ace/theme/' + this.props.theme) + } + if (prevProps.fontSize !== this.props.fontSize) { + editor.setFontSize(this.props.fontSize) + } + if (prevProps.indentSize !== this.props.indentSize) { + session.setTabSize(this.props.indentSize) + } + if (prevProps.indentType !== this.props.indentType) { + session.setUseSoftTabs(this.props.indentType === 'space') + } } - 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) - - 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 +203,37 @@ 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, fontFamily } = this.props + fontFamily = _.isString(fontFamily) && fontFamily.length > 0 + ? [fontFamily].concat(defaultEditorFontFamily) + : defaultEditorFontFamily return (
) @@ -251,11 +241,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, @@ -263,7 +250,12 @@ CodeEditor.propTypes = { } CodeEditor.defaultProps = { - readOnly: false + readOnly: false, + theme: 'xcode', + fontSize: 14, + fontFamily: 'Monaco, Consolas', + indentSize: 4, + indentType: 'space' } export default CodeEditor diff --git a/browser/components/ExternalLink.js b/browser/components/ExternalLink.js deleted file mode 100644 index ec190ecc..00000000 --- a/browser/components/ExternalLink.js +++ /dev/null @@ -1,20 +0,0 @@ -import React, { PropTypes } from 'react' -const electron = require('electron') -const shell = electron.shell - -export default class ExternalLink extends React.Component { - handleClick (e) { - shell.openExternal(this.props.href) - e.preventDefault() - } - - render () { - return ( - this.handleClick(e)} {...this.props}/> - ) - } -} - -ExternalLink.propTypes = { - href: PropTypes.string -} diff --git a/browser/components/FolderMark.js b/browser/components/FolderMark.js deleted file mode 100644 index dd6708b9..00000000 --- a/browser/components/FolderMark.js +++ /dev/null @@ -1,52 +0,0 @@ -import React, { PropTypes } from 'react' - -const BLUE = '#3460C7' -const LIGHTBLUE = '#2BA5F7' -const ORANGE = '#FF8E00' -const YELLOW = '#E8D252' -const GREEN = '#3FD941' -const DARKGREEN = '#1FAD85' -const RED = '#E10051' -const PURPLE = '#B013A4' - -function getColorByIndex (index) { - switch (index % 8) { - case 0: - return RED - case 1: - return ORANGE - case 2: - return YELLOW - case 3: - return GREEN - case 4: - return DARKGREEN - case 5: - return LIGHTBLUE - case 6: - return BLUE - case 7: - return PURPLE - default: - return DARKGREEN - } -} - -export default class FolderMark extends React.Component { - render () { - let color = getColorByIndex(this.props.color) - let className = 'FolderMark fa fa-square fa-fw' - if (this.props.className != null) { - className += ' active' - } - - return ( - - ) - } -} - -FolderMark.propTypes = { - color: PropTypes.number, - className: PropTypes.string -} diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js new file mode 100644 index 00000000..2e07bc39 --- /dev/null +++ b/browser/components/MarkdownEditor.js @@ -0,0 +1,151 @@ +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: 'PREVIEW' + } + } + + componentDidMount () { + this.value = this.refs.code.value + } + + componentDidUpdate () { + this.value = this.refs.code.value + } + + handleChange (e) { + this.value = this.refs.code.value + this.props.onChange(e) + } + + handleContextMenu (e) { + let { config } = this.props + if (config.editor.switchPreview === 'RIGHTCLICK') { + 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() + } + }) + } + } + + handleBlur (e) { + let { config } = this.props + if (config.editor.switchPreview === 'BLUR') { + let cursorPosition = this.refs.code.getCursorPosition() + this.setState({ + status: 'PREVIEW' + }, () => { + this.refs.preview.focus() + this.refs.preview.scrollTo(cursorPosition.row) + }) + } + } + + handlePreviewMouseDown (e) { + this.previewMouseDownedAt = new Date() + } + + handlePreviewMouseUp (e) { + let { config } = this.props + if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) { + this.setState({ + status: 'CODE' + }, () => { + this.refs.code.focus() + }) + } + } + + focus () { + if (this.state.status === 'PREVIEW') { + this.setState({ + status: 'CODE' + }, () => { + this.refs.code.focus() + }) + } else { + this.refs.code.focus() + } + } + + reload () { + this.refs.code.reload() + } + + render () { + let { className, value, config } = this.props + let editorFontSize = parseInt(config.editor.fontSize, 10) + if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 + let editorIndentSize = parseInt(config.editor.indentSize, 10) + if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 + + let previewStyle = {} + if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none' + + return ( +
this.handleContextMenu(e)} + tabIndex='-1' + > + this.handleChange(e)} + onBlur={(e) => this.handleBlur(e)} + /> + this.handleContextMenu(e)} + tabIndex='0' + value={value} + onMouseUp={(e) => this.handlePreviewMouseUp(e)} + onMouseDown={(e) => this.handlePreviewMouseDown(e)} + /> +
+ ) + } +} + +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..6d5183ae 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -1,214 +1,152 @@ import React, { PropTypes } from 'react' -import markdown from '../lib/markdown' -import ReactDOM from 'react-dom' -import sanitizeHtml from '@rokt33r/sanitize-html' +import markdown from 'browser/lib/markdown' import _ from 'lodash' -import fetchConfig from '../lib/fetchConfig' +import hljsTheme from 'browser/lib/hljsThemes' -const electron = require('electron') -const shell = electron.shell -const ipc = electron.ipcRenderer - -const katex = window.katex +const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] +const { shell } = require('electron') +const goExternal = function (e) { + e.preventDefault() + e.stopPropagation() + shell.openExternal(e.target.href) +} 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 - } - } - } +const defaultFontFamily = ['helvetica', 'arial', 'sans-serif'] +if (!OSX) { + defaultFontFamily.unshift('\'Microsoft YaHei\'') + defaultFontFamily.unshift('meiryo') } - -function handleAnchorClick (e) { - if (this.attributes.href && this.attributes.href.nodeValue.match(/^#.+/)) { - return - } - e.preventDefault() - e.stopPropagation() - let href = this.href - if (href && href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) { - shell.openExternal(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 -}) +const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] 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'] - } - } - componentDidMount () { - this.addListener() - this.renderMath() - ipc.on('config-apply', this.configApplyHandler) + this.contextMenuHandler = (e) => this.handleContextMenu(e) + this.mouseDownHandler = (e) => this.handleMouseDown(e) + this.mouseUpHandler = (e) => this.handleMouseUp(e) } - componentDidUpdate () { - this.addListener() - this.renderMath() - } - - componentWillUnmount () { - this.removeListener() - ipc.removeListener('config-apply', this.configApplyHandler) - } - - componentWillUpdate () { - this.removeListener() - } - - 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) - }) - Array.prototype.forEach.call(inputs, input => { - input.addEventListener('click', stopPropagation) - }) - } - - 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) - } + handleContextMenu (e) { + this.props.onContextMenu(e) } handleMouseDown (e) { - if (this.props.onMouseDown) { - this.props.onMouseDown(e) - } + if (this.props.onMouseDown != null) this.props.onMouseDown(e) } handleMouseUp (e) { - if (this.props.onMouseUp) { - this.props.onMouseUp(e) - } + if (this.props.onMouseUp != null) this.props.onMouseUp(e) } - handleMouseMove (e) { - if (this.props.onMouseMove) { - this.props.onMouseMove(e) - } + componentDidMount () { + this.refs.root.setAttribute('sandbox', 'allow-same-origin') + this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler) + this.rewriteIframe() + + this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler) + this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler) } - handleConfigApply (e, config) { - this.setState({ - fontSize: config['preview-font-size'], - fontFamily: config['preview-font-family'], - lineNumber: config['preview-line-number'] + componentWillUnmount () { + this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler) + this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler) + this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler) + } + + componentDidUpdate (prevProps) { + if (prevProps.value !== this.props.value || + prevProps.fontFamily !== this.props.fontFamily || + prevProps.fontSize !== this.props.fontSize || + prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || + prevProps.codeBlockTheme !== this.props.codeBlockTheme || + prevProps.lineNumber !== this.props.lineNumber + ) this.rewriteIframe() + } + + rewriteIframe () { + Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { + el.removeEventListener('click', goExternal) + }) + + let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props + fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 + ? [fontFamily].concat(defaultFontFamily) + : defaultFontFamily + codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0 + ? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily) + : defaultCodeBlockFontFamily + codeBlockTheme = hljsTheme().some((theme) => theme.name === codeBlockTheme) ? codeBlockTheme : 'xcode' + + 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('mousedown', goExternal) }) } - render () { - let isEmpty = this.props.content.trim().length === 0 - let content = isEmpty - ? '(Empty content)' - : this.props.content - content = markdown(content) - content = sanitizeHtml(content, sanitizeOpts) + focus () { + this.refs.root.focus() + } + getWindow () { + return this.refs.root.contentWindow + } + + scrollTo (targetRow) { + let lineAnchors = this.getWindow().document.querySelectorAll('a.lineAnchor') + + for (let index = 0; index < lineAnchors.length; index++) { + let lineAnchor = lineAnchors[index] + let row = parseInt(lineAnchor.getAttribute('data-key')) + if (row > targetRow) { + let targetAnchor = lineAnchors[index - 1] + this.getWindow().scrollTo(0, targetAnchor.offsetTop) + break + } + } + } + + render () { + 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' - }} +