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 (
${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) => (