diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index d81ce39d..304171ba 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -5,7 +5,7 @@ import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import convertModeName from 'browser/lib/convertModeName'
-import { options, TableEditor } from '@susisu/mte-kernel'
+import { options, TableEditor, Alignment } from '@susisu/mte-kernel'
import TextEditorInterface from 'browser/lib/TextEditorInterface'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
@@ -59,6 +59,7 @@ export default class CodeEditor extends React.Component {
this.searchState = null
this.formatTable = () => this.handleFormatTable()
+ this.editorActivityHandler = () => this.handleEditorActivity()
}
handleSearch (msg) {
@@ -99,6 +100,28 @@ export default class CodeEditor extends React.Component {
this.tableEditor.formatAll(options({textWidthOptions: {}}))
}
+ handleEditorActivity () {
+ if (!this.textEditorInterface.transaction) {
+ this.updateTableEditorState()
+ }
+ }
+
+ updateTableEditorState () {
+ const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
+ if (active) {
+ if (this.extraKeysMode !== 'editor') {
+ this.extraKeysMode = 'editor'
+ this.editor.setOption('extraKeys', this.editorKeyMap)
+ }
+ } else {
+ if (this.extraKeysMode !== 'default') {
+ this.extraKeysMode = 'default'
+ this.editor.setOption('extraKeys', this.defaultKeyMap)
+ this.tableEditor.resetSmartCursor()
+ }
+ }
+ }
+
componentDidMount () {
const { rulers, enableRulers } = this.props
const expandSnippet = this.expandSnippet.bind(this)
@@ -119,6 +142,59 @@ export default class CodeEditor extends React.Component {
)
}
+ this.defaultKeyMap = CodeMirror.normalizeKeyMap({
+ Tab: function (cm) {
+ const cursor = cm.getCursor()
+ const line = cm.getLine(cursor.line)
+ const cursorPosition = cursor.ch
+ const charBeforeCursor = line.substr(cursorPosition - 1, 1)
+ if (cm.somethingSelected()) cm.indentSelection('add')
+ else {
+ const tabs = cm.getOption('indentWithTabs')
+ if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
+ cm.execCommand('goLineStart')
+ if (tabs) {
+ cm.execCommand('insertTab')
+ } else {
+ cm.execCommand('insertSoftTab')
+ }
+ cm.execCommand('goLineEnd')
+ } else if (
+ !charBeforeCursor.match(/\t|\s|\r|\n/) &&
+ cursor.ch > 1
+ ) {
+ // text expansion on tab key if the char before is alphabet
+ const snippets = JSON.parse(
+ fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
+ )
+ if (expandSnippet(line, cursor, cm, snippets) === false) {
+ if (tabs) {
+ cm.execCommand('insertTab')
+ } else {
+ cm.execCommand('insertSoftTab')
+ }
+ }
+ } else {
+ if (tabs) {
+ cm.execCommand('insertTab')
+ } else {
+ cm.execCommand('insertSoftTab')
+ }
+ }
+ }
+ },
+ 'Cmd-T': function (cm) {
+ // Do nothing
+ },
+ Enter: 'boostNewLineAndIndentContinueMarkdownList',
+ 'Ctrl-C': cm => {
+ if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
+ document.execCommand('copy')
+ }
+ return CodeMirror.Pass
+ }
+ })
+
this.value = this.props.value
this.editor = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers),
@@ -141,58 +217,7 @@ export default class CodeEditor extends React.Component {
explode: '[]{}``$$',
override: true
},
- extraKeys: {
- Tab: function (cm) {
- const cursor = cm.getCursor()
- const line = cm.getLine(cursor.line)
- const cursorPosition = cursor.ch
- const charBeforeCursor = line.substr(cursorPosition - 1, 1)
- if (cm.somethingSelected()) cm.indentSelection('add')
- else {
- const tabs = cm.getOption('indentWithTabs')
- if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
- cm.execCommand('goLineStart')
- if (tabs) {
- cm.execCommand('insertTab')
- } else {
- cm.execCommand('insertSoftTab')
- }
- cm.execCommand('goLineEnd')
- } else if (
- !charBeforeCursor.match(/\t|\s|\r|\n/) &&
- cursor.ch > 1
- ) {
- // text expansion on tab key if the char before is alphabet
- const snippets = JSON.parse(
- fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
- )
- if (expandSnippet(line, cursor, cm, snippets) === false) {
- if (tabs) {
- cm.execCommand('insertTab')
- } else {
- cm.execCommand('insertSoftTab')
- }
- }
- } else {
- if (tabs) {
- cm.execCommand('insertTab')
- } else {
- cm.execCommand('insertSoftTab')
- }
- }
- }
- },
- 'Cmd-T': function (cm) {
- // Do nothing
- },
- Enter: 'boostNewLineAndIndentContinueMarkdownList',
- 'Ctrl-C': cm => {
- if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
- document.execCommand('copy')
- }
- return CodeMirror.Pass
- }
- }
+ extraKeys: this.defaultKeyMap
})
this.setMode(this.props.mode)
@@ -215,8 +240,58 @@ export default class CodeEditor extends React.Component {
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
CodeMirror.Vim.map('ZZ', ':q', 'normal')
- this.tableEditor = new TableEditor(new TextEditorInterface(this.editor))
+ this.textEditorInterface = new TextEditorInterface(this.editor)
+ this.tableEditor = new TableEditor(this.textEditorInterface)
eventEmitter.on('code:format-table', this.formatTable)
+
+ this.tableEditorOptions = options({
+ smartCursor: true
+ })
+
+ this.editorKeyMap = CodeMirror.normalizeKeyMap({
+ 'Tab': () => { this.tableEditor.nextCell(this.tableEditorOptions) },
+ 'Shift-Tab': () => { this.tableEditor.previousCell(this.tableEditorOptions) },
+ 'Enter': () => { this.tableEditor.nextRow(this.tableEditorOptions) },
+ 'Ctrl-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
+ 'Cmd-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
+ 'Shift-Ctrl-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
+ 'Shift-Cmd-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
+ 'Shift-Ctrl-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
+ 'Shift-Cmd-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
+ 'Shift-Ctrl-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
+ 'Shift-Cmd-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
+ 'Shift-Ctrl-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
+ 'Shift-Cmd-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
+ 'Ctrl-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
+ 'Cmd-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
+ 'Ctrl-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
+ 'Cmd-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
+ 'Ctrl-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
+ 'Cmd-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
+ 'Ctrl-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
+ 'Cmd-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
+ 'Ctrl-K Ctrl-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
+ 'Cmd-K Cmd-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
+ 'Ctrl-L Ctrl-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
+ 'Cmd-L Cmd-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
+ 'Ctrl-K Ctrl-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
+ 'Cmd-K Cmd-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
+ 'Ctrl-L Ctrl-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
+ 'Cmd-L Cmd-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
+ 'Alt-Shift-Ctrl-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
+ 'Alt-Shift-Cmd-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
+ 'Alt-Shift-Ctrl-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
+ 'Alt-Shift-Cmd-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
+ 'Alt-Shift-Ctrl-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
+ 'Alt-Shift-Cmd-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
+ 'Alt-Shift-Ctrl-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) },
+ 'Alt-Shift-Cmd-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) }
+ })
+
+ if (this.props.enableTableEditor) {
+ this.editor.on('cursorActivity', this.editorActivityHandler)
+ this.editor.on('changes', this.editorActivityHandler)
+ }
}
expandSnippet (line, cursor, cm, snippets) {
@@ -353,6 +428,19 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
}
+ if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
+ if (this.props.enableTableEditor) {
+ this.editor.on('cursorActivity', this.editorActivityHandler)
+ this.editor.on('changes', this.editorActivityHandler)
+ } else {
+ this.editor.off('cursorActivity', this.editorActivityHandler)
+ this.editor.off('changes', this.editorActivityHandler)
+ }
+
+ this.extraKeysMode = 'default'
+ this.editor.setOption('extraKeys', this.defaultKeyMap)
+ }
+
if (needRefresh) {
this.editor.refresh()
}
@@ -516,7 +604,10 @@ export default class CodeEditor extends React.Component {
body,
'text/html'
)
- const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
+ const escapePipe = (str) => {
+ return str.replace('|', '\\|')
+ }
+ const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})`
resolve(linkWithTitle)
} catch (e) {
reject(e)
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index ee80c887..4c195797 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -265,6 +265,7 @@ class MarkdownEditor extends React.Component {
storageKey={storageKey}
noteKey={noteKey}
fetchUrlTitle={config.editor.fetchUrlTitle}
+ enableTableEditor={config.editor.enableTableEditor}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
/>
diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js
index 8fa3cc07..ddc9d7e0 100644
--- a/browser/components/MarkdownSplitEditor.js
+++ b/browser/components/MarkdownSplitEditor.js
@@ -158,6 +158,7 @@ class MarkdownSplitEditor extends React.Component {
rulers={config.editor.rulers}
scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
+ enableTableEditor={config.editor.enableTableEditor}
storageKey={storageKey}
noteKey={noteKey}
onChange={this.handleOnChange.bind(this)}
diff --git a/browser/lib/TextEditorInterface.js b/browser/lib/TextEditorInterface.js
index 53ae2337..55f2c29d 100644
--- a/browser/lib/TextEditorInterface.js
+++ b/browser/lib/TextEditorInterface.js
@@ -1,53 +1,113 @@
-import { Point } from '@susisu/mte-kernel'
-
-export default class TextEditorInterface {
- constructor (editor) {
- this.editor = editor
- }
-
- getCursorPosition () {
- const pos = this.editor.getCursor()
- return new Point(pos.line, pos.ch)
- }
-
- setCursorPosition (pos) {
- this.editor.setCursor({line: pos.row, ch: pos.column})
- }
-
- setSelectionRange (range) {
- this.editor.setSelection({
- anchor: {line: range.start.row, ch: range.start.column},
- head: {line: range.end.row, ch: range.end.column}
- })
- }
-
- getLastRow () {
- return this.editor.lastLine()
- }
-
- acceptsTableEdit (row) {
- return true
- }
-
- getLine (row) {
- return this.editor.getLine(row)
- }
-
- insertLine (row, line) {
- this.editor.replaceRange(line, {line: row, ch: 0})
- }
-
- deleteLine (row) {
- this.editor.replaceRange('', {line: row, ch: 0}, {line: row, ch: this.editor.getLine(row).length})
- }
-
- replaceLines (startRow, endRow, lines) {
- endRow-- // because endRow is a first line after a table.
- const endRowCh = this.editor.getLine(endRow).length
- this.editor.replaceRange(lines, {line: startRow, ch: 0}, {line: endRow, ch: endRowCh})
- }
-
- transact (func) {
- func()
- }
-}
+import { Point } from '@susisu/mte-kernel'
+
+export default class TextEditorInterface {
+ constructor (editor) {
+ this.editor = editor
+ this.doc = editor.getDoc()
+ this.transaction = false
+ }
+
+ getCursorPosition () {
+ const { line, ch } = this.doc.getCursor()
+ return new Point(line, ch)
+ }
+
+ setCursorPosition (pos) {
+ this.doc.setCursor({
+ line: pos.row,
+ ch: pos.column
+ })
+ }
+
+ setSelectionRange (range) {
+ this.doc.setSelection(
+ { line: range.start.row, ch: range.start.column },
+ { line: range.end.row, ch: range.end.column }
+ )
+ }
+
+ getLastRow () {
+ return this.doc.lineCount() - 1
+ }
+
+ acceptsTableEdit () {
+ return true
+ }
+
+ getLine (row) {
+ return this.doc.getLine(row)
+ }
+
+ insertLine (row, line) {
+ const lastRow = this.getLastRow()
+ if (row > lastRow) {
+ const lastLine = this.getLine(lastRow)
+ this.doc.replaceRange(
+ '\n' + line,
+ { line: lastRow, ch: lastLine.length },
+ { line: lastRow, ch: lastLine.length }
+ )
+ } else {
+ this.doc.replaceRange(
+ line + '\n',
+ { line: row, ch: 0 },
+ { line: row, ch: 0 }
+ )
+ }
+ }
+
+ deleteLine (row) {
+ const lastRow = this.getLastRow()
+ if (row >= lastRow) {
+ if (lastRow > 0) {
+ const preLastLine = this.getLine(lastRow - 1)
+ const lastLine = this.getLine(lastRow)
+ this.doc.replaceRange(
+ '',
+ { line: lastRow - 1, ch: preLastLine.length },
+ { line: lastRow, ch: lastLine.length }
+ )
+ } else {
+ const lastLine = this.getLine(lastRow)
+ this.doc.replaceRange(
+ '',
+ { line: lastRow, ch: 0 },
+ { line: lastRow, ch: lastLine.length }
+ )
+ }
+ } else {
+ this.doc.replaceRange(
+ '',
+ { line: row, ch: 0 },
+ { line: row + 1, ch: 0 }
+ )
+ }
+ }
+
+ replaceLines (startRow, endRow, lines) {
+ const lastRow = this.getLastRow()
+ if (endRow > lastRow) {
+ const lastLine = this.getLine(lastRow)
+ this.doc.replaceRange(
+ lines.join('\n'),
+ { line: startRow, ch: 0 },
+ { line: lastRow, ch: lastLine.length }
+ )
+ } else {
+ this.doc.replaceRange(
+ lines.join('\n') + '\n',
+ { line: startRow, ch: 0 },
+ { line: endRow, ch: 0 }
+ )
+ }
+ }
+
+ transact (func) {
+ this.transaction = true
+ func()
+ this.transaction = false
+ if (this.onDidFinishTransaction) {
+ this.onDidFinishTransaction.call(undefined)
+ }
+ }
+}
diff --git a/browser/lib/findNoteTitle.js b/browser/lib/findNoteTitle.js
index 81c9400f..b954f172 100644
--- a/browser/lib/findNoteTitle.js
+++ b/browser/lib/findNoteTitle.js
@@ -3,6 +3,17 @@ export function findNoteTitle (value) {
let title = null
let isInsideCodeBlock = false
+ if (splitted[0] === '---') {
+ let line = 0
+ while (++line < splitted.length) {
+ if (splitted[line] === '---') {
+ splitted.splice(0, line + 1)
+
+ break
+ }
+ }
+ }
+
splitted.some((line, index) => {
const trimmedLine = line.trim()
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
diff --git a/browser/lib/markdown-it-frontmatter.js b/browser/lib/markdown-it-frontmatter.js
new file mode 100644
index 00000000..66d8ce89
--- /dev/null
+++ b/browser/lib/markdown-it-frontmatter.js
@@ -0,0 +1,24 @@
+'use strict'
+
+module.exports = function frontMatterPlugin (md) {
+ function frontmatter (state, startLine, endLine, silent) {
+ if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') {
+ return false
+ }
+
+ let line = 0
+ while (++line < state.lineMax) {
+ if (state.src.substring(state.bMarks[line], state.eMarks[line]) === '---') {
+ state.line = line + 1
+
+ return true
+ }
+ }
+
+ return false
+ }
+
+ md.block.ruler.before('table', 'frontmatter', frontmatter, {
+ alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
+ })
+}
diff --git a/browser/lib/markdown-toc-generator.js b/browser/lib/markdown-toc-generator.js
new file mode 100644
index 00000000..716be83a
--- /dev/null
+++ b/browser/lib/markdown-toc-generator.js
@@ -0,0 +1,100 @@
+/**
+ * @fileoverview Markdown table of contents generator
+ */
+
+import toc from 'markdown-toc'
+import diacritics from 'diacritics-map'
+import stripColor from 'strip-color'
+
+const EOL = require('os').EOL
+
+/**
+ * @caseSensitiveSlugify Custom slugify function
+ * Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js),
+ * but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067
+ */
+function caseSensitiveSlugify (str) {
+ function replaceDiacritics (str) {
+ return str.replace(/[À-ž]/g, function (ch) {
+ return diacritics[ch] || ch
+ })
+ }
+
+ function getTitle (str) {
+ if (/^\[[^\]]+\]\(/.test(str)) {
+ var m = /^\[([^\]]+)\]/.exec(str)
+ if (m) return m[1]
+ }
+ return str
+ }
+
+ str = getTitle(str)
+ str = stripColor(str)
+ // str = str.toLowerCase() //let's be case sensitive
+
+ // `.split()` is often (but not always) faster than `.replace()`
+ str = str.split(' ').join('-')
+ str = str.split(/\t/).join('--')
+ str = str.split(/<\/?[^>]+>/).join('')
+ str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
+ str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
+ str = replaceDiacritics(str)
+ return str
+}
+
+const TOC_MARKER_START = ''
+const TOC_MARKER_END = ''
+
+/**
+ * Takes care of proper updating given editor with TOC.
+ * If TOC doesn't exit in the editor, it's inserted at current caret position.
+ * Otherwise,TOC is updated in place.
+ * @param editor CodeMirror editor to be updated with TOC
+ */
+export function generateInEditor (editor) {
+ const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
+
+ function tocExistsInEditor () {
+ return tocRegex.test(editor.getValue())
+ }
+
+ function updateExistingToc () {
+ const toc = generate(editor.getValue())
+ const search = editor.getSearchCursor(tocRegex)
+ while (search.findNext()) {
+ search.replace(toc)
+ }
+ }
+
+ function addTocAtCursorPosition () {
+ const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity}))
+ editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
+ }
+
+ if (tocExistsInEditor()) {
+ updateExistingToc()
+ } else {
+ addTocAtCursorPosition()
+ }
+}
+
+/**
+ * Generates MD TOC based on MD document passed as string.
+ * @param markdownText MD document
+ * @returns generatedTOC String containing generated TOC
+ */
+export function generate (markdownText) {
+ const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify})
+ return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
+}
+
+function wrapTocWithEol (toc, editor) {
+ const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
+ const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL
+ return leftWrap + toc + rightWrap
+}
+
+export default {
+ generate,
+ generateInEditor
+}
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index 49fd2f86..ba57ec6b 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -149,6 +149,7 @@ class Markdown {
})
this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'))
+ this.md.use(require('./markdown-it-frontmatter'))
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
diff --git a/browser/lib/newNote.js b/browser/lib/newNote.js
new file mode 100644
index 00000000..bed69735
--- /dev/null
+++ b/browser/lib/newNote.js
@@ -0,0 +1,62 @@
+import { hashHistory } from 'react-router'
+import dataApi from 'browser/main/lib/dataApi'
+import ee from 'browser/main/lib/eventEmitter'
+import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
+
+export function createMarkdownNote (storage, folder, dispatch, location) {
+ AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
+ AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
+ return dataApi
+ .createNote(storage, {
+ type: 'MARKDOWN_NOTE',
+ folder: folder,
+ title: '',
+ content: ''
+ })
+ .then(note => {
+ const noteHash = note.key
+ dispatch({
+ type: 'UPDATE_NOTE',
+ note: note
+ })
+
+ hashHistory.push({
+ pathname: location.pathname,
+ query: { key: noteHash }
+ })
+ ee.emit('list:jump', noteHash)
+ ee.emit('detail:focus')
+ })
+}
+
+export function createSnippetNote (storage, folder, dispatch, location, config) {
+ AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
+ AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
+ return dataApi
+ .createNote(storage, {
+ type: 'SNIPPET_NOTE',
+ folder: folder,
+ title: '',
+ description: '',
+ snippets: [
+ {
+ name: '',
+ mode: config.editor.snippetDefaultLanguage || 'text',
+ content: ''
+ }
+ ]
+ })
+ .then(note => {
+ const noteHash = note.key
+ dispatch({
+ type: 'UPDATE_NOTE',
+ note: note
+ })
+ hashHistory.push({
+ pathname: location.pathname,
+ query: { key: noteHash }
+ })
+ ee.emit('list:jump', noteHash)
+ ee.emit('detail:focus')
+ })
+}
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index 82073162..e4493a80 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -29,6 +29,7 @@ import { formatDate } from 'browser/lib/date-formatter'
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
+import markdownToc from 'browser/lib/markdown-toc-generator'
class MarkdownNoteDetail extends React.Component {
constructor (props) {
@@ -47,6 +48,7 @@ class MarkdownNoteDetail extends React.Component {
this.dispatchTimer = null
this.toggleLockButton = this.handleToggleLockButton.bind(this)
+ this.generateToc = () => this.handleGenerateToc()
}
focus () {
@@ -59,6 +61,7 @@ class MarkdownNoteDetail extends React.Component {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType)
})
+ ee.on('code:generate-toc', this.generateToc)
}
componentWillReceiveProps (nextProps) {
@@ -75,6 +78,7 @@ class MarkdownNoteDetail extends React.Component {
componentWillUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
+ ee.off('code:generate-toc', this.generateToc)
if (this.saveQueue != null) this.saveNow()
}
@@ -262,6 +266,11 @@ class MarkdownNoteDetail extends React.Component {
}
}
+ handleGenerateToc () {
+ const editor = this.refs.content.refs.code.editor
+ markdownToc.generateInEditor(editor)
+ }
+
handleFocus (e) {
this.focus()
}
@@ -363,6 +372,7 @@ class MarkdownNoteDetail extends React.Component {
diff --git a/browser/main/Detail/NoteDetailInfo.styl b/browser/main/Detail/NoteDetailInfo.styl
index 8d454203..7166a497 100644
--- a/browser/main/Detail/NoteDetailInfo.styl
+++ b/browser/main/Detail/NoteDetailInfo.styl
@@ -13,6 +13,7 @@ $info-margin-under-border = 30px
display flex
align-items center
padding 0 20px
+ z-index 99
.info-left
padding 0 10px
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index 6220b5a4..fefba2ce 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -29,6 +29,7 @@ import InfoPanelTrashed from './InfoPanelTrashed'
import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
+import markdownToc from 'browser/lib/markdown-toc-generator'
const electron = require('electron')
const { remote } = electron
@@ -52,6 +53,7 @@ class SnippetNoteDetail extends React.Component {
}
this.scrollToNextTabThreshold = 0.7
+ this.generateToc = () => this.handleGenerateToc()
}
componentDidMount () {
@@ -65,6 +67,7 @@ class SnippetNoteDetail extends React.Component {
enableLeftArrow: allTabs.offsetLeft !== 0
})
}
+ ee.on('code:generate-toc', this.generateToc)
}
componentWillReceiveProps (nextProps) {
@@ -91,6 +94,16 @@ class SnippetNoteDetail extends React.Component {
componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
+ ee.off('code:generate-toc', this.generateToc)
+ }
+
+ handleGenerateToc () {
+ const { note, snippetIndex } = this.state
+ const currentMode = note.snippets[snippetIndex].mode
+ if (currentMode.includes('Markdown')) {
+ const currentEditor = this.refs[`code-${snippetIndex}`].refs.code.editor
+ markdownToc.generateInEditor(currentEditor)
+ }
}
handleChange (e) {
@@ -441,7 +454,7 @@ class SnippetNoteDetail extends React.Component {
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
- if (isSuper && !e.shiftKey) {
+ if (isSuper && !e.shiftKey && !e.altKey) {
e.preventDefault()
this.addSnippet()
}
@@ -692,6 +705,7 @@ class SnippetNoteDetail extends React.Component {
keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
+ enableTableEditor={config.editor.enableTableEditor}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
/>
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js
index 553dc680..eb160e4c 100644
--- a/browser/main/Detail/TagSelect.js
+++ b/browser/main/Detail/TagSelect.js
@@ -6,71 +6,33 @@ import _ from 'lodash'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter'
+import Autosuggest from 'react-autosuggest'
class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
- newTag: ''
+ newTag: '',
+ suggestions: []
}
- this.addtagHandler = this.handleAddTag.bind(this)
+
+ this.handleAddTag = this.handleAddTag.bind(this)
+ this.onInputBlur = this.onInputBlur.bind(this)
+ this.onInputChange = this.onInputChange.bind(this)
+ this.onInputKeyDown = this.onInputKeyDown.bind(this)
+ this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
+ this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
+ this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
}
- componentDidMount () {
- this.value = this.props.value
- ee.on('editor:add-tag', this.addtagHandler)
- }
-
- componentDidUpdate () {
- this.value = this.props.value
- }
-
- componentWillUnmount () {
- ee.off('editor:add-tag', this.addtagHandler)
- }
-
- handleAddTag () {
- this.refs.newTag.focus()
- }
-
- handleNewTagInputKeyDown (e) {
- switch (e.keyCode) {
- case 9:
- e.preventDefault()
- this.submitTag()
- break
- case 13:
- this.submitTag()
- break
- case 8:
- if (this.refs.newTag.value.length === 0) {
- this.removeLastTag()
- }
- }
- }
-
- handleNewTagBlur (e) {
- this.submitTag()
- }
-
- removeLastTag () {
- this.removeTagByCallback((value) => {
- value.pop()
- })
- }
-
- reset () {
- this.setState({
- newTag: ''
- })
- }
-
- submitTag () {
+ addNewTag (newTag) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
- let { value } = this.props
- let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
- newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
+
+ newTag = newTag.trim().replace(/ +/g, '_')
+ if (newTag.charAt(0) === '#') {
+ newTag.substring(1)
+ }
if (newTag.length <= 0) {
this.setState({
@@ -79,6 +41,7 @@ class TagSelect extends React.Component {
return
}
+ let { value } = this.props
value = _.isArray(value)
? value.slice()
: []
@@ -93,10 +56,36 @@ class TagSelect extends React.Component {
})
}
- handleNewTagInputChange (e) {
- this.setState({
- newTag: this.refs.newTag.value
- })
+ buildSuggestions () {
+ this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
+ (tag, name) => ({
+ name,
+ nameLC: name.toLowerCase(),
+ size: tag.size
+ })
+ ).filter(
+ tag => tag.size > 0
+ ), ['name'])
+ }
+
+ componentDidMount () {
+ this.value = this.props.value
+
+ this.buildSuggestions()
+
+ ee.on('editor:add-tag', this.handleAddTag)
+ }
+
+ componentDidUpdate () {
+ this.value = this.props.value
+ }
+
+ componentWillUnmount () {
+ ee.off('editor:add-tag', this.handleAddTag)
+ }
+
+ handleAddTag () {
+ this.refs.newTag.input.focus()
}
handleTagLabelClick (tag) {
@@ -110,6 +99,60 @@ class TagSelect extends React.Component {
}, tag)
}
+ onInputBlur (e) {
+ this.submitNewTag()
+ }
+
+ onInputChange (e, { newValue, method }) {
+ this.setState({
+ newTag: newValue
+ })
+ }
+
+ onInputKeyDown (e) {
+ switch (e.keyCode) {
+ case 9:
+ e.preventDefault()
+ this.submitNewTag()
+ break
+ case 13:
+ this.submitNewTag()
+ break
+ case 8:
+ if (this.state.newTag.length === 0) {
+ this.removeLastTag()
+ }
+ }
+ }
+
+ onSuggestionsClearRequested () {
+ this.setState({
+ suggestions: []
+ })
+ }
+
+ onSuggestionsFetchRequested ({ value }) {
+ const valueLC = value.toLowerCase()
+ const suggestions = _.filter(
+ this.suggestions,
+ tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
+ )
+
+ this.setState({
+ suggestions
+ })
+ }
+
+ onSuggestionSelected (event, { suggestion, suggestionValue }) {
+ this.addNewTag(suggestionValue)
+ }
+
+ removeLastTag () {
+ this.removeTagByCallback((value) => {
+ value.pop()
+ })
+ }
+
removeTagByCallback (callback, tag = null) {
let { value } = this.props
@@ -123,6 +166,18 @@ class TagSelect extends React.Component {
this.props.onChange()
}
+ reset () {
+ this.buildSuggestions()
+
+ this.setState({
+ newTag: ''
+ })
+ }
+
+ submitNewTag () {
+ this.addNewTag(this.refs.newTag.input.value)
+ }
+
render () {
const { value, className } = this.props
@@ -143,6 +198,8 @@ class TagSelect extends React.Component {
})
: []
+ const { newTag, suggestions } = this.state
+
return (
{tagList}
-
this.handleNewTagInputChange(e)}
- onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
- onBlur={(e) => this.handleNewTagBlur(e)}
+ suggestions={suggestions}
+ onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
+ onSuggestionsClearRequested={this.onSuggestionsClearRequested}
+ onSuggestionSelected={this.onSuggestionSelected}
+ getSuggestionValue={suggestion => suggestion.name}
+ renderSuggestion={suggestion => (
+
+ {suggestion.name}
+
+ )}
+ inputProps={{
+ placeholder: i18n.__('Add tag...'),
+ value: newTag,
+ onChange: this.onInputChange,
+ onKeyDown: this.onInputKeyDown,
+ onBlur: this.onInputBlur
+ }}
/>
)
@@ -172,7 +241,6 @@ TagSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func
-
}
export default CSSModules(TagSelect, styles)
diff --git a/browser/main/Detail/TagSelect.styl b/browser/main/Detail/TagSelect.styl
index 31371397..e3912e39 100644
--- a/browser/main/Detail/TagSelect.styl
+++ b/browser/main/Detail/TagSelect.styl
@@ -43,14 +43,6 @@
padding 4px 16px 4px 8px
cursor pointer
-.newTag
- box-sizing border-box
- border none
- background-color transparent
- outline none
- padding 0 4px
- font-size 13px
-
body[data-theme="dark"]
.tag
background-color alpha($ui-dark-tag-backgroundColor, 60%)
@@ -63,11 +55,6 @@ body[data-theme="dark"]
.tag-label
color $ui-dark-text-color
- .newTag
- border-color none
- background-color transparent
- color $ui-dark-text-color
-
body[data-theme="solarized-dark"]
.tag
background-color $ui-solarized-dark-tag-backgroundColor
@@ -79,11 +66,6 @@ body[data-theme="solarized-dark"]
.tag-label
color $ui-solarized-dark-text-color
- .newTag
- border-color none
- background-color transparent
- color $ui-solarized-dark-text-color
-
body[data-theme="monokai"]
.tag
background-color $ui-monokai-button-backgroundColor
@@ -94,8 +76,3 @@ body[data-theme="monokai"]
.tag-label
color $ui-monokai-text-color
-
- .newTag
- border-color none
- background-color transparent
- color $ui-monokai-text-color
diff --git a/browser/main/NewNoteButton/index.js b/browser/main/NewNoteButton/index.js
index 85dc7f40..e739a550 100644
--- a/browser/main/NewNoteButton/index.js
+++ b/browser/main/NewNoteButton/index.js
@@ -7,6 +7,7 @@ import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import eventEmitter from 'browser/main/lib/eventEmitter'
import i18n from 'browser/lib/i18n'
+import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
const { remote } = require('electron')
const { dialog } = remote
@@ -37,13 +38,19 @@ class NewNoteButton extends React.Component {
const { location, dispatch, config } = this.props
const { storage, folder } = this.resolveTargetFolder()
- modal.open(NewNoteModal, {
- storage: storage.key,
- folder: folder.key,
- dispatch,
- location,
- config
- })
+ if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
+ createMarkdownNote(storage.key, folder.key, dispatch, location)
+ } else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
+ createSnippetNote(storage.key, folder.key, dispatch, location, config)
+ } else {
+ modal.open(NewNoteModal, {
+ storage: storage.key,
+ folder: folder.key,
+ dispatch,
+ location,
+ config
+ })
+ }
}
resolveTargetFolder () {
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index f7dd0764..880f8479 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -80,6 +80,7 @@ class NoteList extends React.Component {
this.getViewType = this.getViewType.bind(this)
this.restoreNote = this.restoreNote.bind(this)
this.copyNoteLink = this.copyNoteLink.bind(this)
+ this.navigate = this.navigate.bind(this)
// TODO: not Selected noteKeys but SelectedNote(for reusing)
this.state = {
@@ -98,6 +99,7 @@ class NoteList extends React.Component {
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.on('import:file', this.importFromFileHandler)
ee.on('list:jump', this.jumpNoteByHash)
+ ee.on('list:navigate', this.navigate)
}
componentWillReceiveProps (nextProps) {
@@ -687,6 +689,16 @@ class NoteList extends React.Component {
return copy(noteLink)
}
+ navigate (sender, pathname) {
+ const { router } = this.context
+ router.push({
+ pathname,
+ query: {
+ // key: noteKey
+ }
+ })
+ }
+
save (note) {
const { dispatch } = this.props
dataApi
diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js
index d72f0a8f..d17314b3 100644
--- a/browser/main/SideNav/StorageItem.js
+++ b/browser/main/SideNav/StorageItem.js
@@ -38,6 +38,22 @@ class StorageItem extends React.Component {
{
type: 'separator'
},
+ {
+ label: i18n.__('Export Storage'),
+ submenu: [
+ {
+ label: i18n.__('Export as txt'),
+ click: (e) => this.handleExportStorageClick(e, 'txt')
+ },
+ {
+ label: i18n.__('Export as md'),
+ click: (e) => this.handleExportStorageClick(e, 'md')
+ }
+ ]
+ },
+ {
+ type: 'separator'
+ },
{
label: i18n.__('Unlink Storage'),
click: (e) => this.handleUnlinkStorageClick(e)
@@ -68,6 +84,30 @@ class StorageItem extends React.Component {
}
}
+ handleExportStorageClick (e, fileType) {
+ const options = {
+ properties: ['openDirectory', 'createDirectory'],
+ buttonLabel: i18n.__('Select directory'),
+ title: i18n.__('Select a folder to export the files to'),
+ multiSelections: false
+ }
+ dialog.showOpenDialog(remote.getCurrentWindow(), options,
+ (paths) => {
+ if (paths && paths.length === 1) {
+ const { storage, dispatch } = this.props
+ dataApi
+ .exportStorage(storage.key, fileType, paths[0])
+ .then(data => {
+ dispatch({
+ type: 'EXPORT_STORAGE',
+ storage: data.storage,
+ fileType: data.fileType
+ })
+ })
+ }
+ })
+ }
+
handleToggleButtonClick (e) {
const { storage, dispatch } = this.props
const isOpen = !this.state.isOpen
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index c4fa417b..977a8fb5 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -198,12 +198,12 @@ class SideNav extends React.Component {
const tags = pathSegments[pathSegments.length - 1]
return (tags === 'alltags')
? []
- : tags.split(' ')
+ : tags.split(' ').map(tag => decodeURIComponent(tag))
}
handleClickTagListItem (name) {
const { router } = this.context
- router.push(`/tags/${name}`)
+ router.push(`/tags/${encodeURIComponent(name)}`)
}
handleSortTagsByChange (e) {
@@ -230,7 +230,7 @@ class SideNav extends React.Component {
} else {
listOfTags.push(tag)
}
- router.push(`/tags/${listOfTags.join(' ')}`)
+ router.push(`/tags/${listOfTags.map(tag => encodeURIComponent(tag)).join(' ')}`)
}
emptyTrash (entries) {
diff --git a/browser/main/global.styl b/browser/main/global.styl
index e4505a4e..815cff4e 100644
--- a/browser/main/global.styl
+++ b/browser/main/global.styl
@@ -132,6 +132,15 @@ body[data-theme="dark"]
.CodeMirror-foldgutter-folded:after
content: "\25B8"
+.CodeMirror-hover
+ padding 2px 4px 0 4px
+ position absolute
+ z-index 99
+
+.CodeMirror-hyperlink
+ cursor pointer
+
+
.sortableItemHelper
z-index modalZIndex + 5
@@ -156,3 +165,5 @@ body[data-theme="monokai"]
body[data-theme="default"]
.SideNav ::-webkit-scrollbar-thumb
background-color rgba(255, 255, 255, 0.3)
+
+@import '../styles/Detail/TagSelect.styl'
\ No newline at end of file
diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js
index 434b0d22..2c601b57 100644
--- a/browser/main/lib/ConfigManager.js
+++ b/browser/main/lib/ConfigManager.js
@@ -46,7 +46,8 @@ export const DEFAULT_CONFIG = {
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
scrollPastEnd: false,
type: 'SPLIT',
- fetchUrlTitle: true
+ fetchUrlTitle: true,
+ enableTableEditor: false
},
preview: {
fontSize: '14',
diff --git a/browser/main/lib/dataApi/exportStorage.js b/browser/main/lib/dataApi/exportStorage.js
new file mode 100644
index 00000000..ce2c4573
--- /dev/null
+++ b/browser/main/lib/dataApi/exportStorage.js
@@ -0,0 +1,63 @@
+import { findStorage } from 'browser/lib/findStorage'
+import resolveStorageData from './resolveStorageData'
+import resolveStorageNotes from './resolveStorageNotes'
+import filenamify from 'filenamify'
+import * as path from 'path'
+import * as fs from 'fs'
+
+/**
+ * @param {String} storageKey
+ * @param {String} fileType
+ * @param {String} exportDir
+ *
+ * @return {Object}
+ * ```
+ * {
+ * storage: Object,
+ * fileType: String,
+ * exportDir: String
+ * }
+ * ```
+ */
+
+function exportStorage (storageKey, fileType, exportDir) {
+ let targetStorage
+ try {
+ targetStorage = findStorage(storageKey)
+ } catch (e) {
+ return Promise.reject(e)
+ }
+
+ return resolveStorageData(targetStorage)
+ .then(storage => (
+ resolveStorageNotes(storage).then(notes => ({storage, notes}))
+ ))
+ .then(function exportNotes (data) {
+ const { storage, notes } = data
+ const folderNamesMapping = {}
+ storage.folders.forEach(folder => {
+ const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
+ folderNamesMapping[folder.key] = folderExportedDir
+ // make sure directory exists
+ try {
+ fs.mkdirSync(folderExportedDir)
+ } catch (e) {}
+ })
+ notes
+ .filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
+ .forEach(markdownNote => {
+ const folderExportedDir = folderNamesMapping[markdownNote.folder]
+ const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
+ const notePath = path.join(folderExportedDir, snippetName)
+ fs.writeFileSync(notePath, markdownNote.content)
+ })
+
+ return {
+ storage,
+ fileType,
+ exportDir
+ }
+ })
+}
+
+module.exports = exportStorage
diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js
index 4e2f0061..92be6b93 100644
--- a/browser/main/lib/dataApi/index.js
+++ b/browser/main/lib/dataApi/index.js
@@ -9,6 +9,7 @@ const dataApi = {
deleteFolder: require('./deleteFolder'),
reorderFolder: require('./reorderFolder'),
exportFolder: require('./exportFolder'),
+ exportStorage: require('./exportStorage'),
createNote: require('./createNote'),
updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'),
diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js
index f6aa2c67..8b16f2a2 100644
--- a/browser/main/modals/NewNoteModal.js
+++ b/browser/main/modals/NewNoteModal.js
@@ -1,12 +1,9 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NewNoteModal.styl'
-import dataApi from 'browser/main/lib/dataApi'
-import { hashHistory } from 'react-router'
-import ee from 'browser/main/lib/eventEmitter'
import ModalEscButton from 'browser/components/ModalEscButton'
-import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
+import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
class NewNoteModal extends React.Component {
constructor (props) {
@@ -24,31 +21,10 @@ class NewNoteModal extends React.Component {
}
handleMarkdownNoteButtonClick (e) {
- AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
- AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
const { storage, folder, dispatch, location } = this.props
- dataApi
- .createNote(storage, {
- type: 'MARKDOWN_NOTE',
- folder: folder,
- title: '',
- content: ''
- })
- .then(note => {
- const noteHash = note.key
- dispatch({
- type: 'UPDATE_NOTE',
- note: note
- })
-
- hashHistory.push({
- pathname: location.pathname,
- query: { key: noteHash }
- })
- ee.emit('list:jump', noteHash)
- ee.emit('detail:focus')
- setTimeout(this.props.close, 200)
- })
+ createMarkdownNote(storage, folder, dispatch, location).then(() => {
+ setTimeout(this.props.close, 200)
+ })
}
handleMarkdownNoteButtonKeyDown (e) {
@@ -59,38 +35,10 @@ class NewNoteModal extends React.Component {
}
handleSnippetNoteButtonClick (e) {
- AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
- AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
const { storage, folder, dispatch, location, config } = this.props
-
- dataApi
- .createNote(storage, {
- type: 'SNIPPET_NOTE',
- folder: folder,
- title: '',
- description: '',
- snippets: [
- {
- name: '',
- mode: config.editor.snippetDefaultLanguage || 'text',
- content: ''
- }
- ]
- })
- .then(note => {
- const noteHash = note.key
- dispatch({
- type: 'UPDATE_NOTE',
- note: note
- })
- hashHistory.push({
- pathname: location.pathname,
- query: { key: noteHash }
- })
- ee.emit('list:jump', noteHash)
- ee.emit('detail:focus')
- setTimeout(this.props.close, 200)
- })
+ createSnippetNote(storage, folder, dispatch, location, config).then(() => {
+ setTimeout(this.props.close, 200)
+ })
}
handleSnippetNoteButtonKeyDown (e) {
diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js
index 74047d44..1b7d3672 100644
--- a/browser/main/modals/PreferencesModal/UiTab.js
+++ b/browser/main/modals/PreferencesModal/UiTab.js
@@ -67,6 +67,7 @@ class UiTab extends React.Component {
ui: {
theme: this.refs.uiTheme.value,
language: this.refs.uiLanguage.value,
+ defaultNote: this.refs.defaultNote.value,
showCopyNotification: this.refs.showCopyNotification.checked,
confirmDeletion: this.refs.confirmDeletion.checked,
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
@@ -87,7 +88,8 @@ class UiTab extends React.Component {
keyMap: this.refs.editorKeyMap.value,
snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value,
scrollPastEnd: this.refs.scrollPastEnd.checked,
- fetchUrlTitle: this.refs.editorFetchUrlTitle.checked
+ fetchUrlTitle: this.refs.editorFetchUrlTitle.checked,
+ enableTableEditor: this.refs.enableTableEditor.checked
},
preview: {
fontSize: this.refs.previewFontSize.value,
@@ -173,7 +175,9 @@ class UiTab extends React.Component {
{i18n.__('Interface')}
- {i18n.__('Interface Theme')}
+
+ {i18n.__('Interface Theme')}
+
- {i18n.__('Language')}
+
+ {i18n.__('Language')}
+
+
+
+ {i18n.__('Default New Note')}
+
+
+
+
+
+
+
+
+
+
{i18n.__('Preview')}
diff --git a/browser/main/store.js b/browser/main/store.js
index a1b6b791..b8f13cc8 100644
--- a/browser/main/store.js
+++ b/browser/main/store.js
@@ -216,16 +216,10 @@ function data (state = defaultDataMap(), action) {
return state
}
case 'UPDATE_FOLDER':
- state = Object.assign({}, state)
- state.storageMap = new Map(state.storageMap)
- state.storageMap.set(action.storage.key, action.storage)
- return state
case 'REORDER_FOLDER':
- state = Object.assign({}, state)
- state.storageMap = new Map(state.storageMap)
- state.storageMap.set(action.storage.key, action.storage)
- return state
case 'EXPORT_FOLDER':
+ case 'RENAME_STORAGE':
+ case 'EXPORT_STORAGE':
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage)
@@ -355,11 +349,6 @@ function data (state = defaultDataMap(), action) {
})
}
return state
- case 'RENAME_STORAGE':
- state = Object.assign({}, state)
- state.storageMap = new Map(state.storageMap)
- state.storageMap.set(action.storage.key, action.storage)
- return state
case 'EXPAND_STORAGE':
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
diff --git a/browser/styles/Detail/TagSelect.styl b/browser/styles/Detail/TagSelect.styl
new file mode 100644
index 00000000..84fd74c2
--- /dev/null
+++ b/browser/styles/Detail/TagSelect.styl
@@ -0,0 +1,109 @@
+.TagSelect
+ .react-autosuggest__input
+ box-sizing border-box
+ border none
+ background-color transparent
+ outline none
+ padding 0 4px
+ font-size 13px
+
+ ul
+ position fixed
+ z-index 999
+ box-sizing border-box
+ list-style none
+ padding 0
+ margin 0
+
+ border-radius 4px
+ margin .2em 0 0
+ background-color $ui-noteList-backgroundColor
+ border 1px solid rgba(0,0,0,.3)
+ box-shadow .05em .2em .6em rgba(0,0,0,.2)
+ text-shadow none
+
+ &:empty,
+ &[hidden]
+ display none
+
+ &:before
+ content ""
+ position absolute
+ top -7px
+ left 14px
+ width 0 height 0
+ padding 6px
+ background-color $ui-noteList-backgroundColor
+ border inherit
+ border-right 0
+ border-bottom 0
+ -webkit-transform rotate(45deg)
+ transform rotate(45deg)
+
+ li
+ position relative
+ padding 6px 18px 6px 10px
+ cursor pointer
+
+ li[aria-selected="true"]
+ background-color alpha($ui-button--active-backgroundColor, 40%)
+ color $ui-text-color
+
+body[data-theme="dark"]
+ .TagSelect
+ .react-autosuggest__input
+ color $ui-dark-text-color
+
+ ul
+ border-color $ui-dark-borderColor
+ background-color $ui-dark-noteList-backgroundColor
+ color $ui-dark-text-color
+
+ &:before
+ background-color $ui-dark-noteList-backgroundColor
+
+ li[aria-selected="true"]
+ background-color $ui-dark-button--active-backgroundColor
+ color $ui-dark-text-color
+
+body[data-theme="monokai"]
+ .TagSelect
+ .react-autosuggest__input
+ color $ui-monokai-text-color
+
+ ul
+ border-color $ui-monokai-borderColor
+ background-color $ui-monokai-noteList-backgroundColor
+ color $ui-monokai-text-color
+
+ &:before
+ background-color $ui-dark-noteList-backgroundColor
+
+ li[aria-selected="true"]
+ background-color $ui-monokai-button-backgroundColor
+ color $ui-monokai-text-color
+
+body[data-theme="solarized-dark"]
+ .TagSelect
+ .react-autosuggest__input
+ color $ui-solarized-dark-text-color
+
+ ul
+ border-color $ui-solarized-dark-borderColor
+ background-color $ui-solarized-dark-noteList-backgroundColor
+ color $ui-solarized-dark-text-color
+
+ &:before
+ background-color $ui-solarized-dark-noteList-backgroundColor
+
+ li[aria-selected="true"]
+ background-color $ui-dark-button--active-backgroundColor
+ color $ui-solarized-dark-text-color
+
+body[data-theme="white"]
+ .TagSelect
+ ul
+ background-color $ui-white-noteList-backgroundColor
+
+ li[aria-selected="true"]
+ background-color $ui-button--active-backgroundColor
\ No newline at end of file
diff --git a/contributing.md b/contributing.md
index 867d4161..8260837b 100644
--- a/contributing.md
+++ b/contributing.md
@@ -86,4 +86,23 @@ Pull requestをすることはその変化分のコードの著作権をBoostIO
如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给BoostIO。
这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益,我们会尝试一些其他的方法,比如说云存储、绑定手机软件等。
-因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。
+因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。
+
+---
+
+# Contributing to Boostnote (Français)
+
+### Lorsque vous signalez un problème ou un bug
+Il n'y a pas de modèle pour un signaler problème. Mais nous vous demandons :
+
+**Merci de founir une capture d'écran de Boostnote avec l'outil de développement ouvert**
+(vous pouvez l'ouvrir avec `Ctrl+Shift+I`)
+
+Merci en avance pour votre aide.
+
+### À propos des droits d'auteurs et des requêtes (`Pull Request`)
+
+Si vous faites une requête, vous acceptez de transmettre les modifications du code à BoostIO.
+
+Cela ne veut pas dire que Boostnote deviendra une application payante. Si nous voulons gagner de l'argent, nous trouverons un autre moyen, comme un service de sauvegarde sur le Cloud, une application mobile ou des options payantes.
+Puisque GPL v3 est trop strict pour être compatible avec n'importe quelle autre licence, nous pensons avoir un jour besoin de la remplacer avec une licence bien plus libre (comme BSD, MIT).
diff --git a/extra_scripts/codemirror/addon/hyperlink/hyperlink.js b/extra_scripts/codemirror/addon/hyperlink/hyperlink.js
new file mode 100755
index 00000000..3f548c51
--- /dev/null
+++ b/extra_scripts/codemirror/addon/hyperlink/hyperlink.js
@@ -0,0 +1,127 @@
+(function (mod) {
+ if (typeof exports === 'object' && typeof module === 'object') { // Common JS
+ mod(require('../codemirror/lib/codemirror'))
+ } else if (typeof define === 'function' && define.amd) { // AMD
+ define(['../codemirror/lib/codemirror'], mod)
+ } else { // Plain browser env
+ mod(CodeMirror)
+ }
+})(function (CodeMirror) {
+ 'use strict'
+
+ const shell = require('electron').shell
+ const yOffset = 2
+
+ const macOS = global.process.platform === 'darwin'
+ const modifier = macOS ? 'metaKey' : 'ctrlKey'
+
+ class HyperLink {
+ constructor(cm) {
+ this.cm = cm
+ this.lineDiv = cm.display.lineDiv
+
+ this.onMouseDown = this.onMouseDown.bind(this)
+ this.onMouseEnter = this.onMouseEnter.bind(this)
+ this.onMouseLeave = this.onMouseLeave.bind(this)
+ this.onMouseMove = this.onMouseMove.bind(this)
+
+ this.tooltip = document.createElement('div')
+ this.tooltipContent = document.createElement('div')
+ this.tooltipIndicator = document.createElement('div')
+ this.tooltip.setAttribute('class', 'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected')
+ this.tooltip.setAttribute('cm-ignore-events', 'true')
+ this.tooltip.appendChild(this.tooltipContent)
+ this.tooltip.appendChild(this.tooltipIndicator)
+ this.tooltipContent.textContent = `${macOS ? 'Cmd(⌘)' : 'Ctrl(^)'} + click to follow link`
+
+ this.lineDiv.addEventListener('mousedown', this.onMouseDown)
+ this.lineDiv.addEventListener('mouseenter', this.onMouseEnter, {
+ capture: true,
+ passive: true
+ })
+ this.lineDiv.addEventListener('mouseleave', this.onMouseLeave, {
+ capture: true,
+ passive: true
+ })
+ this.lineDiv.addEventListener('mousemove', this.onMouseMove, {
+ passive: true
+ })
+ }
+ getUrl(el) {
+ const className = el.className.split(' ')
+
+ if (className.indexOf('cm-url') !== -1) {
+ const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(el.textContent)
+ return match[1] || match[2] || match[3]
+ }
+
+ return null
+ }
+ onMouseDown(e) {
+ const { target } = e
+ if (!e[modifier]) {
+ return
+ }
+
+ const url = this.getUrl(target)
+ if (url) {
+ e.preventDefault()
+
+ shell.openExternal(url)
+ }
+ }
+ onMouseEnter(e) {
+ const { target } = e
+
+ const url = this.getUrl(target)
+ if (url) {
+ if (e[modifier]) {
+ target.classList.add('CodeMirror-activeline-background', 'CodeMirror-hyperlink')
+ }
+ else {
+ target.classList.add('CodeMirror-activeline-background')
+ }
+
+ this.showInfo(target)
+ }
+ }
+ onMouseLeave(e) {
+ if (this.tooltip.parentElement === this.lineDiv) {
+ e.target.classList.remove('CodeMirror-activeline-background', 'CodeMirror-hyperlink')
+
+ this.lineDiv.removeChild(this.tooltip)
+ }
+ }
+ onMouseMove(e) {
+ if (this.tooltip.parentElement === this.lineDiv) {
+ if (e[modifier]) {
+ e.target.classList.add('CodeMirror-hyperlink')
+ }
+ else {
+ e.target.classList.remove('CodeMirror-hyperlink')
+ }
+ }
+ }
+ showInfo(relatedTo) {
+ const b1 = relatedTo.getBoundingClientRect()
+ const b2 = this.lineDiv.getBoundingClientRect()
+ const tdiv = this.tooltip
+
+ tdiv.style.left = (b1.left - b2.left) + 'px'
+ this.lineDiv.appendChild(tdiv)
+
+ const b3 = tdiv.getBoundingClientRect()
+ const top = b1.top - b2.top - b3.height - yOffset
+ if (top < 0) {
+ tdiv.style.top = (b1.top - b2.top + b1.height + yOffset) + 'px'
+ }
+ else {
+ tdiv.style.top = top + 'px'
+ }
+ }
+ }
+
+ CodeMirror.defineOption('hyperlink', true, (cm) => {
+ const addon = new HyperLink(cm)
+ })
+})
\ No newline at end of file
diff --git a/lib/main-app.js b/lib/main-app.js
index 1f3f1320..1ab9f4ca 100644
--- a/lib/main-app.js
+++ b/lib/main-app.js
@@ -78,9 +78,11 @@ app.on('ready', function () {
var template = require('./main-menu')
var menu = Menu.buildFromTemplate(template)
+ var touchBarMenu = require('./touchbar-menu')
switch (process.platform) {
case 'darwin':
Menu.setApplicationMenu(menu)
+ mainWindow.setTouchBar(touchBarMenu)
break
case 'win32':
mainWindow.setMenu(menu)
diff --git a/lib/main-menu.js b/lib/main-menu.js
index cda964c5..fed5eb15 100644
--- a/lib/main-menu.js
+++ b/lib/main-menu.js
@@ -145,6 +145,16 @@ const file = {
{
type: 'separator'
},
+ {
+ label: 'Generate/Update Markdown TOC',
+ accelerator: 'Shift+Ctrl+T',
+ click () {
+ mainWindow.webContents.send('code:generate-toc')
+ }
+ },
+ {
+ type: 'separator'
+ },
{
label: 'Print',
accelerator: 'CommandOrControl+P',
diff --git a/lib/main.html b/lib/main.html
index 7366fa04..663c1d23 100644
--- a/lib/main.html
+++ b/lib/main.html
@@ -10,6 +10,7 @@
+
Boostnote