diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index f185492a..1f5ada57 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -20,6 +20,6 @@ If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/ - OS Version and name : \ No newline at end of file +Love Boostnote? Please consider supporting us on IssueHunt: +👉 https://issuehunt.io/repos/53266139 +--> diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 91e7683a..1b546ed1 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -5,16 +5,18 @@ 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 TextEditorInterface from 'browser/lib/TextEditorInterface' import eventEmitter from 'browser/main/lib/eventEmitter' import iconv from 'iconv-lite' import crypto from 'crypto' import consts from 'browser/lib/consts' import fs from 'fs' const { ipcRenderer } = require('electron') +import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' -const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace'] const buildCMRulers = (rulers, enableRulers) => enableRulers ? rulers.map(ruler => ({column: ruler})) : [] @@ -48,6 +50,8 @@ export default class CodeEditor extends React.Component { } this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null + + this.formatTable = () => this.handleFormatTable() } handleSearch (msg) { @@ -81,6 +85,10 @@ export default class CodeEditor extends React.Component { }) } + handleFormatTable () { + this.tableEditor.formatAll(options({textWidthOptions: {}})) + } + componentDidMount () { const { rulers, enableRulers } = this.props const expandSnippet = this.expandSnippet.bind(this) @@ -113,7 +121,12 @@ export default class CodeEditor extends React.Component { dragDrop: false, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], - autoCloseBrackets: true, + autoCloseBrackets: { + pairs: '()[]{}\'\'""$$**``', + triples: '```"""\'\'\'', + explode: '[]{}``$$', + override: true + }, extraKeys: { Tab: function (cm) { const cursor = cm.getCursor() @@ -182,6 +195,9 @@ export default class CodeEditor extends React.Component { CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor) CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor) CodeMirror.Vim.map('ZZ', ':q', 'normal') + + this.tableEditor = new TableEditor(new TextEditorInterface(this.editor)) + eventEmitter.on('code:format-table', this.formatTable) } expandSnippet (line, cursor, cm, snippets) { @@ -264,6 +280,8 @@ export default class CodeEditor extends React.Component { this.editor.off('scroll', this.scrollHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.removeEventListener('load', this.loadStyleHandler) + + eventEmitter.off('code:format-table', this.formatTable) } componentDidUpdate (prevProps, prevState) { @@ -495,10 +513,7 @@ export default class CodeEditor extends React.Component { render () { const {className, fontSize} = this.props - let fontFamily = this.props.fontFamily - fontFamily = _.isString(fontFamily) && fontFamily.length > 0 - ? [fontFamily].concat(defaultEditorFontFamily) - : defaultEditorFontFamily + const fontFamily = normalizeEditorFontFamily(this.props.fontFamily) const width = this.props.width return (
+ ` CSS_FILES.forEach((file) => { @@ -410,8 +451,8 @@ export default class MarkdownPreview extends React.Component { value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) }) } - let renderedHTML = this.markdown.render(value) - attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey) + const renderedHTML = this.markdown.render(value) + attachmentManagement.migrateAttachments(value, storagePath, noteKey) this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { @@ -494,6 +535,30 @@ export default class MarkdownPreview extends React.Component { el.innerHTML = 'Sequence diagram parse error: ' + e.message } }) + + _.forEach( + this.refs.root.contentWindow.document.querySelectorAll('.chart'), + (el) => { + try { + const chartConfig = JSON.parse(el.innerHTML) + el.innerHTML = '' + var canvas = document.createElement('canvas') + el.appendChild(canvas) + /* eslint-disable no-new */ + new Chart(canvas, chartConfig) + } catch (e) { + console.error(e) + el.className = 'chart-error' + el.innerHTML = 'chartjs diagram parse error: ' + e.message + } + } + ) + _.forEach( + this.refs.root.contentWindow.document.querySelectorAll('.mermaid'), + (el) => { + mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML)) + } + ) } focus () { diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 6f8a97f1..03503231 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -68,7 +68,7 @@ body padding 5px margin -5px border-radius 5px - .flowchart-error, .sequence-error + .flowchart-error, .sequence-error .chart-error background-color errorBackgroundColor color errorTextColor padding 5px @@ -213,7 +213,7 @@ pre margin 0 0 1em display flex line-height 1.4em - &.flowchart, &.sequence + &.flowchart, &.sequence, &.chart display flex justify-content center background-color white @@ -315,6 +315,8 @@ $admonition-icon position absolute left 1.2rem font-family: "Material Icons" + font-weight: normal; + font-style: normal; font-size: 24px display: inline-block; line-height: 1; @@ -346,27 +348,27 @@ $admonition-title margin-bottom 0 admonition_types = { - note: {border-color: #448aff, title-color: rgba(68,138,255,.1), icon: "note"}, - hint: {border-color: #00bfa5, title-color: rgba(0,191,165,.1), icon: "info"}, - danger: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "block"}, - caution: {border-color: #ff9100, title-color: rgba(255,145,0,.1), icon: "warning"}, - error: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "error"}, - attention: {border-color: #64dd17, title-color: rgba(100,221,23,.1), icon: "priority_high"} + note: {color: #0288D1, icon: "note"}, + hint: {color: #009688, icon: "info_outline"}, + danger: {color: #c2185b, icon: "block"}, + caution: {color: #ffa726, icon: "warning"}, + error: {color: #d32f2f, icon: "error_outline"}, + attention: {color: #455a64, icon: "priority_high"} } for name, val in admonition_types .admonition.{name} @extend $admonition - border-left-color: val[border-color] + border-left-color: val[color] .admonition.{name}>.admonition-title @extend $admonition-title - border-bottom-color: .1rem solid val[title-color] - background-color: val[title-color] + border-bottom-color: .1rem solid rgba(val[color], 0.2) + background-color: rgba(val[color], 0.2) .admonition.{name}>.admonition-title:before @extend $admonition-icon - color: val[border-color] + color: val[color] content: val[icon] themeDarkBackground = darken(#21252B, 10%) diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js new file mode 100644 index 00000000..e057cf34 --- /dev/null +++ b/browser/components/render/MermaidRender.js @@ -0,0 +1,28 @@ +import mermaidAPI from 'mermaid' + +function getRandomInt (min, max) { + return Math.floor(Math.random() * (max - min)) + min +} + +function getId () { + var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + var id = 'm-' + for (var i = 0; i < 7; i++) { + id += pool[getRandomInt(0, 16)] + } + return id +} + +function render (element, content) { + try { + mermaidAPI.render(getId(), content, (svgGraph) => { + element.innerHTML = svgGraph + }) + } catch (e) { + console.error(e) + element.className = 'mermaid-error' + element.innerHTML = 'mermaid diagram parse error: ' + e.message + } +} + +export default render diff --git a/browser/lib/TextEditorInterface.js b/browser/lib/TextEditorInterface.js new file mode 100644 index 00000000..53ae2337 --- /dev/null +++ b/browser/lib/TextEditorInterface.js @@ -0,0 +1,53 @@ +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() + } +} diff --git a/browser/lib/consts.js b/browser/lib/consts.js index ec811007..84b962eb 100644 --- a/browser/lib/consts.js +++ b/browser/lib/consts.js @@ -36,7 +36,15 @@ const consts = { 'Violet Eggplant' ], THEMES: ['default'].concat(themes), - SNIPPET_FILE: snippetFile + SNIPPET_FILE: snippetFile, + DEFAULT_EDITOR_FONT_FAMILY: [ + 'Monaco', + 'Menlo', + 'Ubuntu Mono', + 'Consolas', + 'source-code-pro', + 'monospace' + ] } module.exports = consts diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index beec9566..6d1a44b6 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -10,6 +10,9 @@ module.exports = function sanitizePlugin (md, options) { if (state.tokens[tokenIdx].type === 'html_block') { state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options) } + if (state.tokens[tokenIdx].type === 'fence') { + state.tokens[tokenIdx].content = state.tokens[tokenIdx].content.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') + } if (state.tokens[tokenIdx].type === 'inline') { const inlineTokens = state.tokens[tokenIdx].children for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) { diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 4dafa4a3..7ebe5a81 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -40,6 +40,12 @@ class Markdown { if (langType === 'sequence') { return `
${str}
` } + if (langType === 'chart') { + return `
${str}
` + } + if (langType === 'mermaid') { + return `
${str}
` + } return '
' +
           '' + fileName + '' +
           createGutter(str, firstLineNumber) +
@@ -245,4 +251,3 @@ class Markdown {
 }
 
 export default Markdown
-
diff --git a/browser/lib/normalizeEditorFontFamily.js b/browser/lib/normalizeEditorFontFamily.js
new file mode 100644
index 00000000..a2a2ec31
--- /dev/null
+++ b/browser/lib/normalizeEditorFontFamily.js
@@ -0,0 +1,9 @@
+import consts from 'browser/lib/consts'
+import isString from 'lodash/isString'
+
+export default function normalizeEditorFontFamily (fontFamily) {
+  const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
+  return isString(fontFamily) && fontFamily.length > 0
+    ? [fontFamily].concat(defaultEditorFontFamily).join(', ')
+    : defaultEditorFontFamily.join(', ')
+}
diff --git a/browser/lib/utils.js b/browser/lib/utils.js
index 441cfbc7..564ed3d2 100644
--- a/browser/lib/utils.js
+++ b/browser/lib/utils.js
@@ -6,52 +6,64 @@ export function lastFindInArray (array, callback) {
   }
 }
 
-export function escapeHtmlCharacters (text) {
-  const matchHtmlRegExp = /["'&<>]/
-  const str = '' + text
-  const match = matchHtmlRegExp.exec(str)
+export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
+  const matchHtmlRegExp = /["'&<>]/g
+  const escapes = ['"', '&', ''', '<', '>']
+  let match = null
+  const replaceAt = (str, index, replace) =>
+    str.substr(0, index) +
+    replace +
+    str.substr(index + replace.length - (replace.length - 1))
 
-  if (!match) {
-    return str
-  }
-
-  let escape
-  let html = ''
-  let index = 0
-  let lastIndex = 0
-
-  for (index = match.index; index < str.length; index++) {
-    switch (str.charCodeAt(index)) {
-      case 34: // "
-        escape = '"'
-        break
-      case 38: // &
-        escape = '&'
-        break
-      case 39: // '
-        escape = '''
-        break
-      case 60: // <
-        escape = '<'
-        break
-      case 62: // >
-        escape = '>'
-        break
-      default:
+  // detecting code block
+  while ((match = matchHtmlRegExp.exec(html)) != null) {
+    const current = { char: match[0], index: match.index }
+    if (opt.detectCodeBlock) {
+      // position of the nearest line start
+      let previousLineEnd = current.index - 1
+      while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
+        previousLineEnd--
+      }
+      // 4 spaces means this character is in a code block
+      if (
+        html[previousLineEnd + 1] === ' ' &&
+        html[previousLineEnd + 2] === ' ' &&
+        html[previousLineEnd + 3] === ' ' &&
+        html[previousLineEnd + 4] === ' '
+      ) {
+        // so skip it
         continue
+      }
     }
-
-    if (lastIndex !== index) {
-      html += str.substring(lastIndex, index)
+    // otherwise, escape it !!!
+    if (current.char === '&') {
+      let nextStr = ''
+      let nextIndex = current.index
+      let escapedStr = false
+      // maximum length of an escape string is 5. For example ('"')
+      while (nextStr.length <= 5) {
+        nextStr += html[nextIndex]
+        nextIndex++
+        if (escapes.indexOf(nextStr) !== -1) {
+          escapedStr = true
+          break
+        }
+      }
+      if (!escapedStr) {
+        // this & char is not a part of an escaped string
+        html = replaceAt(html, current.index, '&')
+      }
+    } else if (current.char === '"') {
+      html = replaceAt(html, current.index, '"')
+    } else if (current.char === "'") {
+      html = replaceAt(html, current.index, ''')
+    } else if (current.char === '<') {
+      html = replaceAt(html, current.index, '<')
+    } else if (current.char === '>') {
+      html = replaceAt(html, current.index, '>')
     }
-
-    lastIndex = index + 1
-    html += escape
   }
-
-  return lastIndex !== index
-      ? html + str.substring(lastIndex, index)
-      : html
+  return html
 }
 
 export function isObjectEqual (a, b) {
diff --git a/browser/main/Detail/FullscreenButton.js b/browser/main/Detail/FullscreenButton.js
index 3d29c264..ee212603 100644
--- a/browser/main/Detail/FullscreenButton.js
+++ b/browser/main/Detail/FullscreenButton.js
@@ -4,12 +4,14 @@ import CSSModules from 'browser/lib/CSSModules'
 import styles from './FullscreenButton.styl'
 import i18n from 'browser/lib/i18n'
 
+const OSX = global.process.platform === 'darwin'
+const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
 const FullscreenButton = ({
  onClick
 }) => (
   
 )
 
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index a8fc938b..82073162 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -277,6 +277,7 @@ class MarkdownNoteDetail extends React.Component {
 
   handleSwitchMode (type) {
     this.setState({ editorType: type }, () => {
+      this.focus()
       const newConfig = Object.assign({}, this.props.config)
       newConfig.editor.type = type
       ConfigManager.set(newConfig)
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index c65f1425..75be4798 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -32,7 +32,7 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
 
 const electron = require('electron')
 const { remote } = electron
-const { Menu, MenuItem, dialog } = remote
+const { dialog } = remote
 
 class SnippetNoteDetail extends React.Component {
   constructor (props) {
@@ -451,14 +451,14 @@ class SnippetNoteDetail extends React.Component {
   }
 
   handleModeButtonClick (e, index) {
-    const menu = new Menu()
+    const templetes = []
     CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
-      menu.append(new MenuItem({
+      templetes.push({
         label: mode.name,
         click: (e) => this.handleModeOptionClick(index, mode.name)(e)
-      }))
+      })
     })
-    menu.popup(remote.getCurrentWindow())
+    context.popup(templetes)
   }
 
   handleIndentTypeButtonClick (e) {
diff --git a/browser/main/Detail/index.js b/browser/main/Detail/index.js
index 2c451085..b6b6ef14 100644
--- a/browser/main/Detail/index.js
+++ b/browser/main/Detail/index.js
@@ -9,6 +9,7 @@ import ee from 'browser/main/lib/eventEmitter'
 import StatusBar from '../StatusBar'
 import i18n from 'browser/lib/i18n'
 import debounceRender from 'react-debounce-render'
+import searchFromNotes from 'browser/lib/search'
 
 const OSX = global.process.platform === 'darwin'
 
@@ -35,11 +36,38 @@ class Detail extends React.Component {
   }
 
   render () {
-    const { location, data, config } = this.props
+    const { location, data, params, config } = this.props
     let note = null
+
     if (location.query.key != null) {
       const noteKey = location.query.key
-      note = data.noteMap.get(noteKey)
+      const allNotes = data.noteMap.map(note => note)
+      const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
+      let displayedNotes = allNotes
+
+      if (location.pathname.match(/\/searched/)) {
+        const searchStr = params.searchword
+        displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
+          : searchFromNotes(allNotes, searchStr)
+      }
+
+      if (location.pathname.match(/\/tags/)) {
+        const listOfTags = params.tagname.split(' ')
+        displayedNotes = data.noteMap.map(note => note).filter(note =>
+          listOfTags.every(tag => note.tags.includes(tag))
+        )
+      }
+
+      if (location.pathname.match(/\/trashed/)) {
+        displayedNotes = trashedNotes
+      } else {
+        displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
+      }
+
+      const noteKeys = displayedNotes.map(note => note.key)
+      if (noteKeys.includes(noteKey)) {
+        note = data.noteMap.get(noteKey)
+      }
     }
 
     if (note == null) {
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index 3626130d..eeb16a5f 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -21,9 +21,10 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
 import Markdown from '../../lib/markdown'
 import i18n from 'browser/lib/i18n'
 import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
+import context from 'browser/lib/context'
 
 const { remote } = require('electron')
-const { Menu, MenuItem, dialog } = remote
+const { dialog } = remote
 const WP_POST_PATH = '/wp/v2/posts'
 
 function sortByCreatedAt (a, b) {
@@ -491,55 +492,51 @@ class NoteList extends React.Component {
     const updateLabel = i18n.__('Update Blog')
     const openBlogLabel = i18n.__('Open Blog')
 
-    const menu = new Menu()
+    const templates = []
 
     if (location.pathname.match(/\/trash/)) {
-      menu.append(new MenuItem({
+      templates.push({
         label: restoreNote,
         click: this.restoreNote
-      }))
-      menu.append(new MenuItem({
+      }, {
         label: deleteLabel,
         click: this.deleteNote
-      }))
+      })
     } else {
       if (!location.pathname.match(/\/starred/)) {
-        menu.append(new MenuItem({
+        templates.push({
           label: pinLabel,
           click: this.pinToTop
-        }))
+        })
       }
-      menu.append(new MenuItem({
+      templates.push({
         label: deleteLabel,
         click: this.deleteNote
-      }))
-      menu.append(new MenuItem({
+      }, {
         label: cloneNote,
         click: this.cloneNote.bind(this)
-      }))
-      menu.append(new MenuItem({
+      }, {
         label: copyNoteLink,
         click: this.copyNoteLink(note)
-      }))
+      })
       if (note.type === 'MARKDOWN_NOTE') {
         if (note.blog && note.blog.blogLink && note.blog.blogId) {
-          menu.append(new MenuItem({
+          templates.push({
             label: updateLabel,
             click: this.publishMarkdown.bind(this)
-          }))
-          menu.append(new MenuItem({
+          }, {
             label: openBlogLabel,
             click: () => this.openBlog.bind(this)(note)
-          }))
+          })
         } else {
-          menu.append(new MenuItem({
+          templates.push({
             label: publishLabel,
             click: this.publishMarkdown.bind(this)
-          }))
+          })
         }
       }
     }
-    menu.popup()
+    context.popup(templates)
   }
 
   updateSelectedNotes (updateFunc, cleanSelection = true) {
diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js
index 93e9157f..d72f0a8f 100644
--- a/browser/main/SideNav/StorageItem.js
+++ b/browser/main/SideNav/StorageItem.js
@@ -11,9 +11,10 @@ import StorageItemChild from 'browser/components/StorageItem'
 import _ from 'lodash'
 import { SortableElement } from 'react-sortable-hoc'
 import i18n from 'browser/lib/i18n'
+import context from 'browser/lib/context'
 
 const { remote } = require('electron')
-const { Menu, dialog } = remote
+const { dialog } = remote
 const escapeStringRegexp = require('escape-string-regexp')
 const path = require('path')
 
@@ -21,13 +22,15 @@ class StorageItem extends React.Component {
   constructor (props) {
     super(props)
 
+    const { storage } = this.props
+
     this.state = {
-      isOpen: true
+      isOpen: !!storage.isOpen
     }
   }
 
   handleHeaderContextMenu (e) {
-    const menu = Menu.buildFromTemplate([
+    context.popup([
       {
         label: i18n.__('Add Folder'),
         click: (e) => this.handleAddFolderButtonClick(e)
@@ -40,8 +43,6 @@ class StorageItem extends React.Component {
         click: (e) => this.handleUnlinkStorageClick(e)
       }
     ])
-
-    menu.popup()
   }
 
   handleUnlinkStorageClick (e) {
@@ -68,8 +69,18 @@ class StorageItem extends React.Component {
   }
 
   handleToggleButtonClick (e) {
+    const { storage, dispatch } = this.props
+    const isOpen = !this.state.isOpen
+    dataApi.toggleStorage(storage.key, isOpen)
+      .then((storage) => {
+        dispatch({
+          type: 'EXPAND_STORAGE',
+          storage,
+          isOpen
+        })
+      })
     this.setState({
-      isOpen: !this.state.isOpen
+      isOpen: isOpen
     })
   }
 
@@ -94,7 +105,7 @@ class StorageItem extends React.Component {
   }
 
   handleFolderButtonContextMenu (e, folder) {
-    const menu = Menu.buildFromTemplate([
+    context.popup([
       {
         label: i18n.__('Rename Folder'),
         click: (e) => this.handleRenameFolderClick(e, folder)
@@ -123,8 +134,6 @@ class StorageItem extends React.Component {
         click: (e) => this.handleFolderDeleteClick(e, folder)
       }
     ])
-
-    menu.popup()
   }
 
   handleRenameFolderClick (e, folder) {
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index 67adf700..c4fa417b 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -1,8 +1,6 @@
 import PropTypes from 'prop-types'
 import React from 'react'
 import CSSModules from 'browser/lib/CSSModules'
-const { remote } = require('electron')
-const { Menu } = remote
 import dataApi from 'browser/main/lib/dataApi'
 import styles from './SideNav.styl'
 import { openModal } from 'browser/main/lib/modal'
@@ -19,6 +17,7 @@ import ListButton from './ListButton'
 import TagButton from './TagButton'
 import {SortableContainer} from 'react-sortable-hoc'
 import i18n from 'browser/lib/i18n'
+import context from 'browser/lib/context'
 
 class SideNav extends React.Component {
   // TODO: should not use electron stuff v0.7
@@ -254,10 +253,9 @@ class SideNav extends React.Component {
   handleFilterButtonContextMenu (event) {
     const { data } = this.props
     const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
-    const menu = Menu.buildFromTemplate([
+    context.popup([
       { label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
     ])
-    menu.popup()
   }
 
   render () {
diff --git a/browser/main/StatusBar/index.js b/browser/main/StatusBar/index.js
index e5f5ae1a..8b48e3d3 100644
--- a/browser/main/StatusBar/index.js
+++ b/browser/main/StatusBar/index.js
@@ -4,10 +4,11 @@ import CSSModules from 'browser/lib/CSSModules'
 import styles from './StatusBar.styl'
 import ZoomManager from 'browser/main/lib/ZoomManager'
 import i18n from 'browser/lib/i18n'
+import context from 'browser/lib/context'
 
 const electron = require('electron')
 const { remote, ipcRenderer } = electron
-const { Menu, MenuItem, dialog } = remote
+const { dialog } = remote
 
 const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
 
@@ -26,16 +27,16 @@ class StatusBar extends React.Component {
   }
 
   handleZoomButtonClick (e) {
-    const menu = new Menu()
+    const templates = []
 
     zoomOptions.forEach((zoom) => {
-      menu.append(new MenuItem({
+      templates.push({
         label: Math.floor(zoom * 100) + '%',
         click: () => this.handleZoomMenuItemClick(zoom)
-      }))
+      })
     })
 
-    menu.popup(remote.getCurrentWindow())
+    context.popup(templates)
   }
 
   handleZoomMenuItemClick (zoomFactor) {
diff --git a/browser/main/TopBar/index.js b/browser/main/TopBar/index.js
index ae4d9664..a5687ecb 100644
--- a/browser/main/TopBar/index.js
+++ b/browser/main/TopBar/index.js
@@ -156,8 +156,7 @@ class TopBar extends React.Component {
     if (this.state.isSearching) {
       el.blur()
     } else {
-      el.focus()
-      el.setSelectionRange(0, el.value.length)
+      el.select()
     }
   }
 
diff --git a/browser/main/global.styl b/browser/main/global.styl
index 7025163f..8f3216ef 100644
--- a/browser/main/global.styl
+++ b/browser/main/global.styl
@@ -15,6 +15,12 @@ body
   font-weight 200
   -webkit-font-smoothing antialiased
 
+::-webkit-scrollbar
+  width 12px
+
+::-webkit-scrollbar-thumb
+  background-color rgba(0, 0, 0, 0.15)
+
 button, input, select, textarea
   font-family DEFAULT_FONTS
 
@@ -85,9 +91,11 @@ modalBackColor = white
     absolute top left bottom right
     background-color modalBackColor
     z-index modalZIndex + 1
-    
+
 
 body[data-theme="dark"]
+  ::-webkit-scrollbar-thumb
+    background-color rgba(0, 0, 0, 0.3)
   .ModalBase
     .modalBack
       background-color $ui-dark-backgroundColor
@@ -128,6 +136,8 @@ body[data-theme="dark"]
   z-index modalZIndex + 5
 
 body[data-theme="solarized-dark"]
+  ::-webkit-scrollbar-thumb
+    background-color rgba(0, 0, 0, 0.3)
   .ModalBase
     .modalBack
       background-color $ui-solarized-dark-backgroundColor
@@ -135,9 +145,10 @@ body[data-theme="solarized-dark"]
     color: $ui-solarized-dark-text-color
 
 body[data-theme="monokai"]
+  ::-webkit-scrollbar-thumb
+    background-color rgba(0, 0, 0, 0.3)
   .ModalBase
     .modalBack
       background-color $ui-monokai-backgroundColor
   .sortableItemHelper
     color: $ui-monokai-text-color
-
diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js
index 0f070fc6..0f6264be 100644
--- a/browser/main/lib/ConfigManager.js
+++ b/browser/main/lib/ConfigManager.js
@@ -21,8 +21,8 @@ export const DEFAULT_CONFIG = {
   listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
   amaEnabled: true,
   hotkey: {
-    toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E',
-    toggleMode: OSX ? 'Cmd + M' : 'Ctrl + M'
+    toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
+    toggleMode: OSX ? 'Command + M' : 'Ctrl + M'
   },
   ui: {
     language: 'en',
@@ -182,6 +182,17 @@ function assignConfigValues (originalConfig, rcConfig) {
   config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
   config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
   config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
+
+  rewriteHotkey(config)
+
+  return config
+}
+
+function rewriteHotkey (config) {
+  const keys = [...Object.keys(config.hotkey)]
+  keys.forEach(key => {
+    config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
+  })
   return config
 }
 
diff --git a/browser/main/lib/dataApi/addStorage.js b/browser/main/lib/dataApi/addStorage.js
index 630c0bd3..bfd6698a 100644
--- a/browser/main/lib/dataApi/addStorage.js
+++ b/browser/main/lib/dataApi/addStorage.js
@@ -37,7 +37,8 @@ function addStorage (input) {
     key,
     name: input.name,
     type: input.type,
-    path: input.path
+    path: input.path,
+    isOpen: false
   }
 
   return Promise.resolve(newStorage)
@@ -48,7 +49,8 @@ function addStorage (input) {
         key: newStorage.key,
         type: newStorage.type,
         name: newStorage.name,
-        path: newStorage.path
+        path: newStorage.path,
+        isOpen: false
       })
 
       localStorage.setItem('storages', JSON.stringify(rawStorages))
diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js
index aec59927..088fb054 100644
--- a/browser/main/lib/dataApi/attachmentManagement.js
+++ b/browser/main/lib/dataApi/attachmentManagement.js
@@ -10,6 +10,7 @@ import i18n from 'browser/lib/i18n'
 
 const STORAGE_FOLDER_PLACEHOLDER = ':storage'
 const DESTINATION_FOLDER = 'attachments'
+const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
 
 /**
  * @description
@@ -76,13 +77,13 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
 
 /**
  * @description Moves attachments from the old location ('/images') to the new one ('/attachments/noteKey)
- * @param renderedHTML HTML of the current note
+ * @param markdownContent of the current note
  * @param storagePath Storage path of the current note
  * @param noteKey Key of the current note
  */
-function migrateAttachments (renderedHTML, storagePath, noteKey) {
+function migrateAttachments (markdownContent, storagePath, noteKey) {
   if (sander.existsSync(path.join(storagePath, 'images'))) {
-    const attachments = getAttachmentsInContent(renderedHTML) || []
+    const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
     if (attachments !== []) {
       createAttachmentDestinationFolder(storagePath, noteKey)
     }
@@ -106,7 +107,10 @@ function migrateAttachments (renderedHTML, storagePath, noteKey) {
  * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
  */
 function fixLocalURLS (renderedHTML, storagePath) {
-  return renderedHTML.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
+  return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
+    var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
+    return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
+  })
 }
 
 /**
@@ -186,13 +190,13 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
 }
 
 /**
- * @description Returns all attachment paths of the given markdown
- * @param {String} markdownContent content in which the attachment paths should be found
- * @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
- */
-function getAttachmentsInContent (markdownContent) {
-  const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep)
-  const regexp = new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + '?([a-zA-Z0-9]|-)*' + escapeStringRegexp(path.sep) + '[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?', 'g')
+* @description Returns all attachment paths of the given markdown
+* @param {String} markdownContent content in which the attachment paths should be found
+* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
+*/
+function getAttachmentsInMarkdownContent (markdownContent) {
+  const preparedInput = markdownContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
+  const regexp = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + ')' + '?([a-zA-Z0-9]|-)*' + '(' + escapeStringRegexp(path.sep) + ')' + '([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?', 'g')
   return preparedInput.match(regexp)
 }
 
@@ -203,7 +207,7 @@ function getAttachmentsInContent (markdownContent) {
  * @returns {String[]} Absolute paths of the referenced attachments
  */
 function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
-  const temp = getAttachmentsInContent(markdownContent) || []
+  const temp = getAttachmentsInMarkdownContent(markdownContent) || []
   const result = []
   for (const relativePath of temp) {
     result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)))
@@ -239,7 +243,8 @@ function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) {
  */
 function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
   if (noteContent) {
-    return noteContent.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
+    const preparedInput = noteContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
+    return preparedInput.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
   }
   return noteContent
 }
@@ -277,7 +282,7 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
   }
   const targetStorage = findStorage.findStorage(storageKey)
   const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
-  const attachmentsInNote = getAttachmentsInContent(markdownContent)
+  const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
   const attachmentsInNoteOnlyFileNames = []
   if (attachmentsInNote) {
     for (let i = 0; i < attachmentsInNote.length; i++) {
@@ -348,7 +353,7 @@ function generateFileNotFoundMarkdown () {
  */
 function isAttachmentLink (text) {
   if (text) {
-    return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + escapeStringRegexp(path.sep) + '.*\\).*', 'gi')) != null
+    return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + '[' + PATH_SEPARATORS + ']' + '.*\\).*', 'gi')) != null
   }
   return false
 }
@@ -364,7 +369,7 @@ function isAttachmentLink (text) {
 function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
   if (storageKey != null && noteKey != null && linkText != null) {
     const storagePath = findStorage.findStorage(storageKey).path
-    const attachments = getAttachmentsInContent(linkText) || []
+    const attachments = getAttachmentsInMarkdownContent(linkText) || []
     const replaceInstructions = []
     const copies = []
     for (const attachment of attachments) {
@@ -373,13 +378,13 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
         sander.exists(absPathOfAttachment)
           .then((fileExists) => {
             if (!fileExists) {
-              const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + escapeStringRegexp(path.sep) + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
+              const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
               replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()})
               return Promise.resolve()
             }
             return this.copyAttachment(absPathOfAttachment, storageKey, noteKey)
               .then((fileName) => {
-                const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + escapeStringRegexp(path.sep) + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
+                const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
                 replaceInstructions.push({
                   regexp: replaceLinkRegExp,
                   replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')'
@@ -408,7 +413,7 @@ module.exports = {
   generateAttachmentMarkdown,
   handleAttachmentDrop,
   handlePastImageEvent,
-  getAttachmentsInContent,
+  getAttachmentsInMarkdownContent,
   getAbsolutePathsOfAttachmentsInContent,
   removeStorageAndNoteReferences,
   deleteAttachmentFolder,
diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js
index 7c57e016..4e2f0061 100644
--- a/browser/main/lib/dataApi/index.js
+++ b/browser/main/lib/dataApi/index.js
@@ -1,5 +1,6 @@
 const dataApi = {
   init: require('./init'),
+  toggleStorage: require('./toggleStorage'),
   addStorage: require('./addStorage'),
   renameStorage: require('./renameStorage'),
   removeStorage: require('./removeStorage'),
diff --git a/browser/main/lib/dataApi/resolveStorageData.js b/browser/main/lib/dataApi/resolveStorageData.js
index af040c5d..681a102e 100644
--- a/browser/main/lib/dataApi/resolveStorageData.js
+++ b/browser/main/lib/dataApi/resolveStorageData.js
@@ -8,7 +8,8 @@ function resolveStorageData (storageCache) {
     key: storageCache.key,
     name: storageCache.name,
     type: storageCache.type,
-    path: storageCache.path
+    path: storageCache.path,
+    isOpen: storageCache.isOpen
   }
 
   const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
diff --git a/browser/main/lib/dataApi/toggleStorage.js b/browser/main/lib/dataApi/toggleStorage.js
new file mode 100644
index 00000000..dbb625c3
--- /dev/null
+++ b/browser/main/lib/dataApi/toggleStorage.js
@@ -0,0 +1,28 @@
+const _ = require('lodash')
+const resolveStorageData = require('./resolveStorageData')
+
+/**
+ * @param {String} key
+ * @param {Boolean} isOpen
+ * @return {Object} Storage meta data
+ */
+function toggleStorage (key, isOpen) {
+  let cachedStorageList
+  try {
+    cachedStorageList = JSON.parse(localStorage.getItem('storages'))
+    if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
+  } catch (err) {
+    console.log('error got')
+    console.error(err)
+    return Promise.reject(err)
+  }
+  const targetStorage = _.find(cachedStorageList, {key: key})
+  if (targetStorage == null) return Promise.reject('Storage')
+
+  targetStorage.isOpen = isOpen
+  localStorage.setItem('storages', JSON.stringify(cachedStorageList))
+
+  return resolveStorageData(targetStorage)
+}
+
+module.exports = toggleStorage
diff --git a/browser/main/lib/shortcutManager.js b/browser/main/lib/shortcutManager.js
index 2b937aea..ac2a3a08 100644
--- a/browser/main/lib/shortcutManager.js
+++ b/browser/main/lib/shortcutManager.js
@@ -3,7 +3,7 @@ import CM from 'browser/main/lib/ConfigManager'
 import ee from 'browser/main/lib/eventEmitter'
 import { isObjectEqual } from 'browser/lib/utils'
 require('mousetrap-global-bind')
-const functions = require('./shortcut')
+import functions from './shortcut'
 
 let shortcuts = CM.get().hotkey
 
diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js
index 185004e7..b748587c 100644
--- a/browser/main/modals/NewNoteModal.js
+++ b/browser/main/modals/NewNoteModal.js
@@ -12,8 +12,7 @@ class NewNoteModal extends React.Component {
   constructor (props) {
     super(props)
 
-    this.state = {
-    }
+    this.state = {}
   }
 
   componentDidMount () {
@@ -35,19 +34,20 @@ class NewNoteModal extends React.Component {
         title: '',
         content: ''
       })
-      .then((note) => {
+      .then(note => {
         const noteHash = note.key
         dispatch({
           type: 'UPDATE_NOTE',
           note: note
         })
+
         hashHistory.push({
           pathname: location.pathname,
-          query: {key: noteHash}
+          query: { key: noteHash }
         })
         ee.emit('list:jump', noteHash)
         ee.emit('detail:focus')
-        this.props.close()
+        setTimeout(this.props.close, 200)
       })
   }
 
@@ -69,13 +69,15 @@ class NewNoteModal extends React.Component {
         folder: folder,
         title: '',
         description: '',
-        snippets: [{
-          name: '',
-          mode: 'text',
-          content: ''
-        }]
+        snippets: [
+          {
+            name: '',
+            mode: 'text',
+            content: ''
+          }
+        ]
       })
-      .then((note) => {
+      .then(note => {
         const noteHash = note.key
         dispatch({
           type: 'UPDATE_NOTE',
@@ -83,11 +85,11 @@ class NewNoteModal extends React.Component {
         })
         hashHistory.push({
           pathname: location.pathname,
-          query: {key: noteHash}
+          query: { key: noteHash }
         })
         ee.emit('list:jump', noteHash)
         ee.emit('detail:focus')
-        this.props.close()
+        setTimeout(this.props.close, 200)
       })
   }
 
@@ -106,49 +108,65 @@ class NewNoteModal extends React.Component {
 
   render () {
     return (
-      
this.handleKeyDown(e)} + onKeyDown={e => this.handleKeyDown(e)} >
{i18n.__('Make a note')}
- this.handleCloseButtonClick(e)} /> + this.handleCloseButtonClick(e)} + />
- -
-
{i18n.__('Tab to switch format')}
+
+ {i18n.__('Tab to switch format')} +
) } } -NewNoteModal.propTypes = { -} +NewNoteModal.propTypes = {} export default CSSModules(NewNoteModal, styles) diff --git a/browser/main/modals/PreferencesModal/SnippetEditor.js b/browser/main/modals/PreferencesModal/SnippetEditor.js index f0e93dec..4ce5dc34 100644 --- a/browser/main/modals/PreferencesModal/SnippetEditor.js +++ b/browser/main/modals/PreferencesModal/SnippetEditor.js @@ -27,7 +27,12 @@ class SnippetEditor extends React.Component { dragDrop: false, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], - autoCloseBrackets: true, + autoCloseBrackets: { + pairs: '()[]{}\'\'""$$**``', + triples: '```"""\'\'\'', + explode: '[]{}``$$', + override: true + }, mode: 'null' }) this.cm.setSize('100%', '100%') diff --git a/browser/main/modals/PreferencesModal/SnippetList.js b/browser/main/modals/PreferencesModal/SnippetList.js index 3cf28cf6..3e892f97 100644 --- a/browser/main/modals/PreferencesModal/SnippetList.js +++ b/browser/main/modals/PreferencesModal/SnippetList.js @@ -4,8 +4,7 @@ import CSSModules from 'browser/lib/CSSModules' import dataApi from 'browser/main/lib/dataApi' import i18n from 'browser/lib/i18n' import eventEmitter from 'browser/main/lib/eventEmitter' -const { remote } = require('electron') -const { Menu, MenuItem } = remote +import context from 'browser/lib/context' class SnippetList extends React.Component { constructor (props) { @@ -21,18 +20,17 @@ class SnippetList extends React.Component { } reloadSnippetList () { - dataApi.fetchSnippet().then(snippets => this.setState({snippets})) + dataApi.fetchSnippet().then(snippets => { + this.setState({snippets}) + this.props.onSnippetSelect(snippets[0]) + }) } handleSnippetContextMenu (snippet) { - const menu = new Menu() - menu.append(new MenuItem({ + context.popup([{ label: i18n.__('Delete snippet'), - click: () => { - this.deleteSnippet(snippet) - } - })) - menu.popup() + click: () => this.deleteSnippet(snippet) + }]) } deleteSnippet (snippet) { @@ -43,7 +41,7 @@ class SnippetList extends React.Component { } handleSnippetClick (snippet) { - this.props.onSnippetClick(snippet) + this.props.onSnippetSelect(snippet) } createSnippet () { @@ -55,6 +53,16 @@ class SnippetList extends React.Component { }).catch(err => { throw err }) } + defineSnippetStyleName (snippet) { + const { currentSnippet } = this.props + if (currentSnippet == null) return + if (currentSnippet.id === snippet.id) { + return 'snippet-item-selected' + } else { + return 'snippet-item' + } + } + render () { const { snippets } = this.state return ( @@ -70,7 +78,7 @@ class SnippetList extends React.Component { { snippets.map((snippet) => (
  • this.handleSnippetContextMenu(snippet)} onClick={() => this.handleSnippetClick(snippet)}> diff --git a/browser/main/modals/PreferencesModal/SnippetTab.js b/browser/main/modals/PreferencesModal/SnippetTab.js index 67e9ace6..e35ecd69 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.js +++ b/browser/main/modals/PreferencesModal/SnippetTab.js @@ -25,7 +25,7 @@ class SnippetTab extends React.Component { }, 500) } - handleSnippetClick (snippet) { + handleSnippetSelect (snippet) { const { currentSnippet } = this.state if (currentSnippet === null || currentSnippet.id !== snippet.id) { dataApi.fetchSnippet(snippet.id).then(changedSnippet => { @@ -66,8 +66,9 @@ class SnippetTab extends React.Component {
    {i18n.__('Snippets')}
    + onSnippetSelect={this.handleSnippetSelect.bind(this)} + onSnippetDeleted={this.handleDeleteSnippet.bind(this)} + currentSnippet={currentSnippet} />
    {i18n.__('Snippet name')}
    diff --git a/browser/main/modals/PreferencesModal/SnippetTab.styl b/browser/main/modals/PreferencesModal/SnippetTab.styl index 118c56ed..02307b64 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.styl +++ b/browser/main/modals/PreferencesModal/SnippetTab.styl @@ -122,6 +122,10 @@ &:hover background darken(#f5f5f5, 5) + .snippet-item-selected + @extend .snippet-list .snippet-item + background darken(#f5f5f5, 5) + .snippet-detail width 70% height calc(100% - 200px) @@ -142,6 +146,8 @@ body[data-theme="default"], body[data-theme="white"] background $ui-borderColor &:hover background darken($ui-backgroundColor, 5) + .snippet-item-selected + background darken($ui-backgroundColor, 5) body[data-theme="dark"] .snippets @@ -152,8 +158,12 @@ body[data-theme="dark"] background $ui-dark-borderColor &:hover background darken($ui-dark-backgroundColor, 5) + .snippet-item-selected + background darken($ui-dark-backgroundColor, 5) .snippet-detail color white + .group-control-button + colorDarkPrimaryButton() body[data-theme="solarized-dark"] .snippets @@ -164,8 +174,12 @@ body[data-theme="solarized-dark"] background $ui-solarized-dark-borderColor &:hover background darken($ui-solarized-dark-backgroundColor, 5) + .snippet-item-selected + background darken($ui-solarized-dark-backgroundColor, 5) .snippet-detail color white + .group-control-button + colorSolarizedDarkPrimaryButton() body[data-theme="monokai"] .snippets @@ -176,5 +190,9 @@ body[data-theme="monokai"] background $ui-monokai-borderColor &:hover background darken($ui-monokai-backgroundColor, 5) + .snippet-item-selected + background darken($ui-monokai-backgroundColor, 5) .snippet-detail color white + .group-control-button + colorMonokaiPrimaryButton() diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index ce149f65..aa3568e7 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -11,6 +11,7 @@ import 'codemirror-mode-elixir' import _ from 'lodash' import i18n from 'browser/lib/i18n' import { getLanguages } from 'browser/lib/Languages' +import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const OSX = global.process.platform === 'darwin' @@ -164,7 +165,7 @@ class UiTab extends React.Component { const { config, codemirrorTheme } = this.state const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};' const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none' - const customCSS = config.preview.customCSS + const fontFamily = normalizeEditorFontFamily(config.editor.fontFamily) return (
    @@ -262,8 +263,16 @@ class UiTab extends React.Component { }) } -
    - (this.codeMirrorInstance = e)} value={codemirrorSampleCode} options={{ lineNumbers: true, readOnly: true, mode: 'javascript', theme: codemirrorTheme }} /> +
    + (this.codeMirrorInstance = e)} + value={codemirrorSampleCode} + options={{ + lineNumbers: true, + readOnly: true, + mode: 'javascript', + theme: codemirrorTheme + }} />
    @@ -596,7 +605,19 @@ class UiTab extends React.Component { type='checkbox' />  {i18n.__('Allow custom CSS for preview')} - this.handleUIChange(e)} ref={e => (this.customCSSCM = e)} value={config.preview.customCSS} options={{ lineNumbers: true, mode: 'css', theme: codemirrorTheme }} /> +
    + this.handleUIChange(e)} + ref={e => (this.customCSSCM = e)} + value={config.preview.customCSS} + options={{ + lineNumbers: true, + mode: 'css', + theme: codemirrorTheme + }} /> +
    diff --git a/browser/main/store.js b/browser/main/store.js index 7ea6decb..a1b6b791 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -360,6 +360,12 @@ function data (state = defaultDataMap(), action) { 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) + action.storage.isOpen = action.isOpen + state.storageMap.set(action.storage.key, action.storage) + return state } return state } diff --git a/lib/main-app.js b/lib/main-app.js index e7e52715..1f3f1320 100644 --- a/lib/main-app.js +++ b/lib/main-app.js @@ -90,7 +90,7 @@ app.on('ready', function () { mainWindow.setMenu(menu) } - // Check update every hour + // Check update every day setInterval(function () { checkUpdate() }, 1000 * 60 * 60 * 24) @@ -106,7 +106,7 @@ app.on('ready', function () { checkUpdate() } }) - }, 10000) + }, 10 * 1000) ipcServer = require('./ipcServer') ipcServer.server.start() }) diff --git a/lib/main-menu.js b/lib/main-menu.js index 9345bd67..1d816ef3 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -136,6 +136,15 @@ const file = { { type: 'separator' }, + { + label: 'Format Table', + click () { + mainWindow.webContents.send('code:format-table') + } + }, + { + type: 'separator' + }, { label: 'Print', accelerator: 'CommandOrControl+P', @@ -235,14 +244,14 @@ const view = { }, { label: 'Next Note', - accelerator: 'Control+J', + accelerator: 'CommandOrControl+]', click () { mainWindow.webContents.send('list:next') } }, { label: 'Previous Note', - accelerator: 'Control+K', + accelerator: 'CommandOrControl+[', click () { mainWindow.webContents.send('list:prior') } @@ -267,6 +276,19 @@ const view = { mainWindow.setFullScreen(!mainWindow.isFullScreen()) } }, + { + type: 'separator' + }, + { + label: 'Toggle Side Bar', + accelerator: 'CommandOrControl+B', + click () { + mainWindow.webContents.send('editor:fullscreen') + } + }, + { + type: 'separator' + }, { role: 'zoomin', accelerator: macOS ? 'CommandOrControl+Plus' : 'Control+=' diff --git a/lib/main-window.js b/lib/main-window.js index 86b85a24..a6d129fc 100644 --- a/lib/main-window.js +++ b/lib/main-window.js @@ -17,7 +17,7 @@ const mainWindow = new BrowserWindow({ autoHideMenuBar: showMenu, webPreferences: { zoomFactor: 1.0, - blinkFeatures: 'OverlayScrollbars' + enableBlinkFeatures: 'OverlayScrollbars' }, icon: path.resolve(__dirname, '../resources/app.png') }) diff --git a/lib/main.html b/lib/main.html index 15e2bbeb..22527d99 100644 --- a/lib/main.html +++ b/lib/main.html @@ -115,7 +115,7 @@