diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 00dc1f6d..29c3748b 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -7,6 +7,7 @@ import path from 'path' import copyImage from 'browser/main/lib/dataApi/copyImage' import { findStorage } from 'browser/lib/findStorage' import fs from 'fs' +import eventEmitter from 'browser/main/lib/eventEmitter' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' @@ -47,6 +48,40 @@ export default class CodeEditor extends React.Component { this.loadStyleHandler = (e) => { this.editor.refresh() } + this.searchHandler = (e, msg) => this.handleSearch(msg) + this.searchState = null + } + + handleSearch (msg) { + const cm = this.editor + const component = this + + if (component.searchState) cm.removeOverlay(component.searchState) + if (msg.length < 3) return + + cm.operation(function () { + component.searchState = makeOverlay(msg, 'searching') + cm.addOverlay(component.searchState) + + function makeOverlay (query, style) { + query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi') + return { + token: function (stream) { + query.lastIndex = stream.pos + var match = query.exec(stream.string) + if (match && match.index === stream.pos) { + stream.pos += match[0].length || 1 + return style + } else if (match) { + stream.pos = match.index + } else { + stream.skipToEnd() + } + } + } + } + }) + this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) } componentDidMount () { @@ -107,6 +142,10 @@ export default class CodeEditor extends React.Component { this.editor.on('blur', this.blurHandler) this.editor.on('change', this.changeHandler) this.editor.on('paste', this.pasteHandler) + eventEmitter.on('top:search', this.searchHandler) + + eventEmitter.emit('code:init') + this.editor.on('scroll', this.scrollHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.addEventListener('load', this.loadStyleHandler) @@ -126,6 +165,8 @@ export default class CodeEditor extends React.Component { this.editor.off('blur', this.blurHandler) this.editor.off('change', this.changeHandler) this.editor.off('paste', this.pasteHandler) + eventEmitter.off('top:search', this.searchHandler) + this.editor.off('scroll', this.scrollHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.removeEventListener('load', this.loadStyleHandler) } @@ -231,27 +272,67 @@ export default class CodeEditor extends React.Component { } handlePaste (editor, e) { - const dataTransferItem = e.clipboardData.items[0] - if (!dataTransferItem.type.match('image')) return - - const blob = dataTransferItem.getAsFile() - const reader = new window.FileReader() - let base64data - - reader.readAsDataURL(blob) - reader.onloadend = () => { - base64data = reader.result.replace(/^data:image\/png;base64,/, '') - base64data += base64data.replace('+', ' ') - const binaryData = new Buffer(base64data, 'base64').toString('binary') - const imageName = Math.random().toString(36).slice(-16) - const storagePath = findStorage(this.props.storageKey).path - const imageDir = path.join(storagePath, 'images') - if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir) - const imagePath = path.join(imageDir, `${imageName}.png`) - fs.writeFile(imagePath, binaryData, 'binary') - const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})` - this.insertImageMd(imageMd) + const clipboardData = e.clipboardData + const dataTransferItem = clipboardData.items[0] + const pastedTxt = clipboardData.getData('text') + const isURL = (str) => { + const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/ + return matcher.test(str) } + if (dataTransferItem.type.match('image')) { + const blob = dataTransferItem.getAsFile() + const reader = new FileReader() + let base64data + + reader.readAsDataURL(blob) + reader.onloadend = () => { + base64data = reader.result.replace(/^data:image\/png;base64,/, '') + base64data += base64data.replace('+', ' ') + const binaryData = new Buffer(base64data, 'base64').toString('binary') + const imageName = Math.random().toString(36).slice(-16) + const storagePath = findStorage(this.props.storageKey).path + const imageDir = path.join(storagePath, 'images') + if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir) + const imagePath = path.join(imageDir, `${imageName}.png`) + fs.writeFile(imagePath, binaryData, 'binary') + const imageMd = `![${imageName}](${path.join('/:storage', `${imageName}.png`)})` + this.insertImageMd(imageMd) + } + } else if (this.props.fetchUrlTitle && isURL(pastedTxt)) { + this.handlePasteUrl(e, editor, pastedTxt) + } + } + + handleScroll (e) { + if (this.props.onScroll) { + this.props.onScroll(e) + } + } + + handlePasteUrl (e, editor, pastedTxt) { + e.preventDefault() + const taggedUrl = `<${pastedTxt}>` + editor.replaceSelection(taggedUrl) + + fetch(pastedTxt, { + method: 'get' + }).then((response) => { + return (response.text()) + }).then((response) => { + const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html') + const value = editor.getValue() + const cursor = editor.getCursor() + const LinkWithTitle = `[${parsedResponse.title}](${pastedTxt})` + const newValue = value.replace(taggedUrl, LinkWithTitle) + editor.setValue(newValue) + editor.setCursor(cursor) + }).catch((e) => { + const value = editor.getValue() + const newValue = value.replace(taggedUrl, pastedTxt) + const cursor = editor.getCursor() + editor.setValue(newValue) + editor.setCursor(cursor) + }) } render () { diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index 8d79629d..f02a146a 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -261,6 +261,7 @@ class MarkdownEditor extends React.Component { displayLineNumbers={config.editor.displayLineNumbers} scrollPastEnd={config.editor.scrollPastEnd} storageKey={storageKey} + fetchUrlTitle={config.editor.fetchUrlTitle} onChange={(e) => this.handleChange(e)} onBlur={(e) => this.handleBlur(e)} /> diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 1d0f7513..c5b0355d 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -121,6 +121,7 @@ export default class MarkdownPreview extends React.Component { this.mouseDownHandler = (e) => this.handleMouseDown(e) this.mouseUpHandler = (e) => this.handleMouseUp(e) this.DoubleClickHandler = (e) => this.handleDoubleClick(e) + this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) this.saveAsTextHandler = () => this.handleSaveAsText() @@ -151,6 +152,12 @@ export default class MarkdownPreview extends React.Component { this.props.onCheckboxClick(e) } + handleScroll (e) { + if (this.props.onScroll) { + this.props.onScroll(e) + } + } + handleContextMenu (e) { this.props.onContextMenu(e) } @@ -279,6 +286,7 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler) this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler) + this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler) eventEmitter.on('export:save-text', this.saveAsTextHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler) @@ -292,6 +300,7 @@ export default class MarkdownPreview extends React.Component { this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler) this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler) this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler) + this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler) eventEmitter.off('export:save-text', this.saveAsTextHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler) diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 2cf8e322..505fbaf4 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -2,6 +2,7 @@ import React from 'react' import CodeEditor from 'browser/components/CodeEditor' import MarkdownPreview from 'browser/components/MarkdownPreview' import { findStorage } from 'browser/lib/findStorage' +import _ from 'lodash' import styles from './MarkdownSplitEditor.styl' import CSSModules from 'browser/lib/CSSModules' @@ -12,6 +13,7 @@ class MarkdownSplitEditor extends React.Component { this.value = props.value this.focus = () => this.refs.code.focus() this.reload = () => this.refs.code.reload() + this.userScroll = true } handleOnChange () { @@ -19,6 +21,49 @@ class MarkdownSplitEditor extends React.Component { this.props.onChange() } + handleScroll (e) { + const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document') + const codeDoc = _.get(this, 'refs.code.editor.doc') + let srcTop, srcHeight, targetTop, targetHeight + + if (this.userScroll) { + if (e.doc) { + srcTop = _.get(e, 'doc.scrollTop') + srcHeight = _.get(e, 'doc.height') + targetTop = _.get(previewDoc, 'body.scrollTop') + targetHeight = _.get(previewDoc, 'body.scrollHeight') + } else { + srcTop = _.get(previewDoc, 'body.scrollTop') + srcHeight = _.get(previewDoc, 'body.scrollHeight') + targetTop = _.get(codeDoc, 'scrollTop') + targetHeight = _.get(codeDoc, 'height') + } + + const distance = (targetHeight * srcTop / srcHeight) - targetTop + const framerate = 1000 / 60 + const frames = 20 + const refractory = frames * framerate + + this.userScroll = false + + let frame = 0 + let scrollPos, time + const timer = setInterval(() => { + time = frame / frames + scrollPos = time < 0.5 + ? 2 * time * time // ease in + : -1 + (4 - 2 * time) * time // ease out + if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance) + else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance) + if (frame >= frames) { + clearInterval(timer) + setTimeout(() => { this.userScroll = true }, refractory) + } + frame++ + }, framerate) + } + } + handleCheckboxClick (e) { e.preventDefault() e.stopPropagation() @@ -66,8 +111,10 @@ class MarkdownSplitEditor extends React.Component { indentType={config.editor.indentType} indentSize={editorIndentSize} scrollPastEnd={config.editor.scrollPastEnd} + fetchUrlTitle={config.editor.fetchUrlTitle} storageKey={storageKey} onChange={this.handleOnChange.bind(this)} + onScroll={this.handleScroll.bind(this)} /> this.handleCheckboxClick(e)} + onScroll={this.handleScroll.bind(this)} showCopyNotification={config.ui.showCopyNotification} storagePath={storage.path} /> diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index d1d306e9..b64af56c 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -220,6 +220,7 @@ pre background-color white &.CodeMirror height initial + flex-wrap wrap &>code flex 1 overflow-x auto @@ -229,6 +230,13 @@ pre padding 0 border none border-radius 0 + &>span.filename + width 100% + border-radius: 5px 0px 0px 0px + margin -8px 100% 8px -8px + padding 0px 6px + background-color #777; + color white &>span.lineNumber display none font-size 1em diff --git a/browser/finder/NoteDetail.js b/browser/finder/NoteDetail.js new file mode 100644 index 00000000..e69de29b diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index c3510f89..d0801a1b 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -9,10 +9,11 @@ import {lastFindInArray} from './utils' const katex = window.katex const config = ConfigManager.get() -function createGutter (str) { - const lc = (str.match(/\n/g) || []).length +function createGutter (str, firstLineNumber) { + if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 + const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1 const lines = [] - for (let i = 1; i <= lc; i++) { + for (let i = firstLineNumber; i <= lastLineNumber; i++) { lines.push('' + i + '') } return '' + lines.join('') + '' @@ -25,15 +26,22 @@ var md = markdownit({ xhtmlOut: true, breaks: true, highlight: function (str, lang) { - if (lang === 'flowchart') { + const delimiter = ':' + const langInfo = lang.split(delimiter) + const langType = langInfo[0] + const fileName = langInfo[1] || '' + const firstLineNumber = parseInt(langInfo[2], 10) + + if (langType === 'flowchart') { return `
${str}
` } - if (lang === 'sequence') { + if (langType === 'sequence') { return `
${str}
` } return '
' +
-      createGutter(str) +
-      '' +
+      '' + fileName + '' +
+      createGutter(str, firstLineNumber) +
+      '' +
       str +
       '
' } diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index bb76b8f3..7694f1e7 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -68,11 +68,8 @@ class MarkdownNoteDetail extends React.Component { } componentWillUnmount () { - if (this.saveQueue != null) this.saveNow() - } - - componentDidUnmount () { ee.off('topbar:togglelockbutton', this.toggleLockButton) + if (this.saveQueue != null) this.saveNow() } handleUpdateTag () { diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 366c785b..791b490e 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -568,6 +568,7 @@ class SnippetNoteDetail extends React.Component { displayLineNumbers={config.editor.displayLineNumbers} keyMap={config.editor.keyMap} scrollPastEnd={config.editor.scrollPastEnd} + fetchUrlTitle={config.editor.fetchUrlTitle} onChange={(e) => this.handleCodeChange(index)(e)} ref={'code-' + index} /> diff --git a/browser/main/TopBar/index.js b/browser/main/TopBar/index.js index 11d31abd..6b4a118e 100644 --- a/browser/main/TopBar/index.js +++ b/browser/main/TopBar/index.js @@ -22,14 +22,18 @@ class TopBar extends React.Component { this.focusSearchHandler = () => { this.handleOnSearchFocus() } + + this.codeInitHandler = this.handleCodeInit.bind(this) } componentDidMount () { ee.on('top:focus-search', this.focusSearchHandler) + ee.on('code:init', this.codeInitHandler) } componentWillUnmount () { ee.off('top:focus-search', this.focusSearchHandler) + ee.off('code:init', this.codeInitHandler) } handleKeyDown (e) { @@ -73,14 +77,16 @@ class TopBar extends React.Component { handleSearchChange (e) { const { router } = this.context + const keyword = this.refs.searchInput.value if (this.state.isAlphabet || this.state.isConfirmTranslation) { router.push('/searched') } else { e.preventDefault() } this.setState({ - search: this.refs.searchInput.value + search: keyword }) + ee.emit('top:search', keyword) } handleSearchFocus (e) { @@ -115,6 +121,10 @@ class TopBar extends React.Component { } } + handleCodeInit () { + ee.emit('top:search', this.refs.searchInput.value) + } + render () { const { config, style, location } = this.props return ( diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index fde7dafd..0c8d6ee9 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -36,7 +36,8 @@ export const DEFAULT_CONFIG = { displayLineNumbers: true, switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR scrollPastEnd: false, - type: 'SPLIT' + type: 'SPLIT', + fetchUrlTitle: true }, preview: { fontSize: '14', diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 654e1dfd..50e13f6c 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -76,7 +76,8 @@ class UiTab extends React.Component { displayLineNumbers: this.refs.editorDisplayLineNumbers.checked, switchPreview: this.refs.editorSwitchPreview.value, keyMap: this.refs.editorKeyMap.value, - scrollPastEnd: this.refs.scrollPastEnd.checked + scrollPastEnd: this.refs.scrollPastEnd.checked, + fetchUrlTitle: this.refs.editorFetchUrlTitle.checked }, preview: { fontSize: this.refs.previewFontSize.value, @@ -328,6 +329,17 @@ class UiTab extends React.Component { +
+ +
+
Preview