From 9cd6d6d4c14d30a4a346b0fd56f031b6337f13c7 Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Tue, 26 Jul 2016 20:00:32 +0900 Subject: [PATCH] GFM checkbox --- browser/components/CodeEditor.js | 6 ++++ browser/components/MarkdownEditor.js | 24 +++++++++++++ browser/components/MarkdownPreview.js | 35 ++++++++++++------ browser/components/markdown.styl | 4 +++ browser/lib/markdown.js | 51 +++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 10 deletions(-) diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 6ca06ab3..47780c4b 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -219,6 +219,12 @@ export default class CodeEditor extends React.Component { session.on('change', this.changeHandler) } + setValue (value) { + let session = this.editor.getSession() + session.setValue(value) + this.value = value + } + render () { let { className, fontFamily } = this.props fontFamily = _.isString(fontFamily) && fontFamily.length > 0 diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 2e07bc39..978e28f2 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -73,6 +73,29 @@ class MarkdownEditor extends React.Component { } } + handleCheckboxClick (e) { + e.preventDefault() + e.stopPropagation() + let idMatch = /checkbox-([0-9]+)/ + let checkedMatch = /\[x\]/i + let uncheckedMatch = /\[ \]/ + if (idMatch.test(e.target.getAttribute('id'))) { + let lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 + let lines = this.refs.code.value + .split('\n') + + let targetLine = lines[lineIndex] + + if (targetLine.match(checkedMatch)) { + lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]') + } + if (targetLine.match(uncheckedMatch)) { + lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]') + } + this.refs.code.setValue(lines.join('\n')) + } + } + focus () { if (this.state.status === 'PREVIEW') { this.setState({ @@ -135,6 +158,7 @@ class MarkdownEditor extends React.Component { value={value} onMouseUp={(e) => this.handlePreviewMouseUp(e)} onMouseDown={(e) => this.handlePreviewMouseDown(e)} + onCheckboxClick={(e) => this.handleCheckboxClick(e)} /> ) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 9936f9fa..c36c67b6 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -22,7 +22,8 @@ export default class MarkdownPreview extends React.Component { this.contextMenuHandler = (e) => this.handleContextMenu(e) this.mouseDownHandler = (e) => this.handleMouseDown(e) this.mouseUpHandler = (e) => this.handleMouseUp(e) - this.goExternalHandler = (e) => this.handlePreviewAnchorClick(e) + this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) + this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) } handlePreviewAnchorClick (e) { @@ -40,13 +41,21 @@ export default class MarkdownPreview extends React.Component { } } + handleCheckboxClick (e) { + this.props.onCheckboxClick(e) + } + handleContextMenu (e) { this.props.onContextMenu(e) } handleMouseDown (e) { - if (e.target != null && e.target.tagName === 'A') { - return null + if (e.target != null) { + switch (e.target.tagName) { + case 'A': + case 'INPUT': + return null + } } if (this.props.onMouseDown != null) this.props.onMouseDown(e) } @@ -85,7 +94,10 @@ export default class MarkdownPreview extends React.Component { rewriteIframe () { Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { - el.removeEventListener('click', this.goExternalHandler) + el.removeEventListener('click', this.anchorClickHandler) + }) + Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { + el.removeEventListener('click', this.checkboxClickHandler) }) let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props @@ -128,7 +140,10 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.body.innerHTML = markdown(value) Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { - el.addEventListener('click', this.goExternalHandler) + el.addEventListener('click', this.anchorClickHandler) + }) + Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { + el.addEventListener('click', this.checkboxClickHandler) }) } @@ -141,13 +156,13 @@ export default class MarkdownPreview extends React.Component { } scrollTo (targetRow) { - let lineAnchors = this.getWindow().document.querySelectorAll('[data-line]') + let blocks = this.getWindow().document.querySelectorAll('body>[data-line]') - for (let index = 0; index < lineAnchors.length; index++) { - let lineAnchor = lineAnchors[index] - let row = parseInt(lineAnchor.getAttribute('data-line')) + for (let index = 0; index < blocks.length; index++) { + let block = blocks[index] + let row = parseInt(block.getAttribute('data-line')) if (row > targetRow) { - let targetAnchor = lineAnchors[index - 1] + let targetAnchor = blocks[index - 1] targetAnchor != null && this.getWindow().scrollTo(0, targetAnchor.offsetTop) break } diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index c563d9ef..38b839df 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -68,6 +68,10 @@ body padding 5px margin -5px border-radius 5px +li + label.taskListItem + margin-left -2em + background-color white div.math-rendered text-align center .math-failed diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index bd3d9a6b..56d4070e 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -61,7 +61,58 @@ md.use(math, { } }) md.use(require('markdown-it-footnote')) +// Override task item +md.block.ruler.at('paragraph', function (state, startLine/*, endLine*/) { + let content, terminate, i, l, token + let nextLine = startLine + 1 + let terminatorRules = state.md.block.ruler.getRules('paragraph') + let endLine = state.lineMax + // jump line-by-line until empty one or EOF + for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { + // this would be a code block normally, but after paragraph + // it's considered a lazy continuation regardless of what's there + if (state.sCount[nextLine] - state.blkIndent > 3) { continue } + + // quirk for blockquotes, this line should already be checked by that rule + if (state.sCount[nextLine] < 0) { continue } + + // Some tags can terminate paragraph without empty line. + terminate = false + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true + break + } + } + if (terminate) { break } + } + + content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() + + state.line = nextLine + + token = state.push('paragraph_open', 'p', 1) + token.map = [ startLine, state.line ] + + if (state.parentType === 'list') { + let match = content.match(/\[( |x)\] ?(.+)/i) + if (match) { + content = `` + } + } + + token = state.push('inline', '', 0) + token.content = content + token.map = [ startLine, state.line ] + token.children = [] + + token = state.push('paragraph_close', 'p', -1) + + return true +}) + +// Add line number attribute for scrolling let originalRender = md.renderer.render md.renderer.render = function render (tokens, options, env) { tokens.forEach((token) => {