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 6d5183ae..c36c67b6 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -5,11 +5,6 @@ import hljsTheme from 'browser/lib/hljsThemes' 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' @@ -27,6 +22,27 @@ 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.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) + this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) + } + + handlePreviewAnchorClick (e) { + e.preventDefault() + e.stopPropagation() + + let href = e.target.getAttribute('href') + if (_.isString(href) && href.match(/^#/)) { + let targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length)) + if (targetElement != null) { + this.getWindow().scrollTo(0, targetElement.offsetTop) + } + } else { + shell.openExternal(e.target.href) + } + } + + handleCheckboxClick (e) { + this.props.onCheckboxClick(e) } handleContextMenu (e) { @@ -34,10 +50,20 @@ export default class MarkdownPreview extends React.Component { } handleMouseDown (e) { + if (e.target != null) { + switch (e.target.tagName) { + case 'A': + case 'INPUT': + return null + } + } if (this.props.onMouseDown != null) this.props.onMouseDown(e) } handleMouseUp (e) { + if (e.target != null && e.target.tagName === 'A') { + return null + } if (this.props.onMouseUp != null) this.props.onMouseUp(e) } @@ -68,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', goExternal) + 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 @@ -111,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('mousedown', goExternal) + el.addEventListener('click', this.anchorClickHandler) + }) + Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { + el.addEventListener('click', this.checkboxClickHandler) }) } @@ -124,14 +156,14 @@ export default class MarkdownPreview extends React.Component { } scrollTo (targetRow) { - let lineAnchors = this.getWindow().document.querySelectorAll('a.lineAnchor') + 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-key')) + 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] - this.getWindow().scrollTo(0, targetAnchor.offsetTop) + let targetAnchor = blocks[index - 1] + targetAnchor != null && this.getWindow().scrollTo(0, targetAnchor.offsetTop) break } } @@ -147,6 +179,7 @@ export default class MarkdownPreview extends React.Component { style={style} tabIndex={tabIndex} ref='root' + onClick={(e) => this.handleClick(e)} /> ) } diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 2cd0f3e2..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 @@ -102,12 +106,6 @@ a background-color alpha(#FFC95C, 0.3) &:visited color brandColor - &.lineAnchor - padding 0 - margin 0 - display block - font-size 0 - height 0 hr border-top none border-bottom solid 1px borderColor @@ -147,9 +145,6 @@ h6 line-height 1.4em margin 1em 0 1em color #777 - -*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre - margin-top 1em p line-height 1.6em margin 0 0 1em @@ -195,8 +190,6 @@ code font-size 0.85em text-decoration none margin-right 2px -*:not(a.lineAnchor) + code - margin-left 2px pre padding 0.5em !important border solid 1px alpha(borderColor, 0.5) diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 9667bf80..56d4070e 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -2,6 +2,7 @@ import markdownit from 'markdown-it' import emoji from 'markdown-it-emoji' import math from '@rokt33r/markdown-it-math' import hljs from 'highlight.js' +import _ from 'lodash' const katex = window.katex @@ -59,21 +60,77 @@ md.use(math, { return output } }) -md.use(require('markdown-it-checkbox')) +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 -let originalRenderToken = md.renderer.renderToken -md.renderer.renderToken = function renderToken (tokens, idx, options) { - let token = tokens[idx] + // 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 } - let result = originalRenderToken.call(md.renderer, tokens, idx, options) - if (token.map != null) { - return result + '' + // 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) => { + switch (token.type) { + case 'heading_open': + case 'paragraph_open': + case 'blockquote_open': + case 'table_open': + token.attrPush(['data-line', token.map[0]]) + } + }) + let result = originalRender.call(md.renderer, tokens, options, env) return result } +window.md = md export default function markdown (content) { - if (content == null) content = '' + if (!_.isString(content)) content = '' - return md.render(content.toString()) + return md.render(content) } diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index b31054fe..4ef6b806 100644 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -89,15 +89,17 @@ class MarkdownNoteDetail extends React.Component { } save () { - let { note, dispatch } = this.props + clearTimeout(this.saveQueue) + this.saveQueue = setTimeout(() => { + let { note, dispatch } = this.props + dispatch({ + type: 'UPDATE_NOTE', + note: this.state.note + }) - dispatch({ - type: 'UPDATE_NOTE', - note: this.state.note - }) - - dataApi - .updateNote(note.storage, note.folder, note.key, this.state.note) + dataApi + .updateNote(note.storage, note.folder, note.key, this.state.note) + }, 1000) } handleFolderChange (e) { diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index fba354ff..dc71a045 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -99,15 +99,17 @@ class SnippetNoteDetail extends React.Component { } save () { - let { note, dispatch } = this.props + clearTimeout(this.saveQueue) + this.saveQueue = setTimeout(() => { + let { note, dispatch } = this.props + dispatch({ + type: 'UPDATE_NOTE', + note: this.state.note + }) - dispatch({ - type: 'UPDATE_NOTE', - note: this.state.note - }) - - dataApi - .updateNote(note.storage, note.folder, note.key, this.state.note) + dataApi + .updateNote(note.storage, note.folder, note.key, this.state.note) + }, 1000) } handleFolderChange (e) { diff --git a/package.json b/package.json index a01b7d5c..ec1c6da1 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "markdown-it": "^6.0.1", "markdown-it-checkbox": "^1.1.0", "markdown-it-emoji": "^1.1.1", + "markdown-it-footnote": "^3.0.0", "md5": "^2.0.0", "moment": "^2.10.3", "sander": "^0.5.1",