diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 7f6b4bbd..c36a50c1 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -292,6 +292,10 @@ export default class CodeEditor extends React.Component { this.editor.on('cursorActivity', this.editorActivityHandler) this.editor.on('changes', this.editorActivityHandler) } + + this.setState({ + clientWidth: this.refs.root.clientWidth + }) } expandSnippet (line, cursor, cm, snippets) { @@ -441,6 +445,14 @@ export default class CodeEditor extends React.Component { this.editor.setOption('extraKeys', this.defaultKeyMap) } + if (this.state.clientWidth !== this.refs.root.clientWidth) { + this.setState({ + clientWidth: this.refs.root.clientWidth + }) + + needRefresh = true + } + if (needRefresh) { this.editor.refresh() } @@ -604,7 +616,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/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 5ddc7598..eacc4e21 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -18,8 +18,11 @@ import mdurl from 'mdurl' import exportNote from 'browser/main/lib/dataApi/exportNote' import { escapeHtmlCharacters } from 'browser/lib/utils' import yaml from 'js-yaml' +import context from 'browser/lib/context' +import i18n from 'browser/lib/i18n' +import fs from 'fs' -const { remote } = require('electron') +const { remote, shell } = require('electron') const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') const { app } = remote @@ -28,6 +31,8 @@ const fileUrl = require('file-url') const dialog = remote.dialog +const uri2path = require('file-uri-to-path') + const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] const appPath = fileUrl( process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve() @@ -162,7 +167,6 @@ const scrollBarDarkStyle = ` } ` -const { shell } = require('electron') const OSX = global.process.platform === 'darwin' const defaultFontFamily = ['helvetica', 'arial', 'sans-serif'] @@ -220,8 +224,32 @@ export default class MarkdownPreview extends React.Component { } } - handleContextMenu (e) { - this.props.onContextMenu(e) + handleContextMenu (event) { + // If a contextMenu handler was passed to us, use it instead of the self-defined one -> return + if (_.isFunction(this.props.onContextMenu)) { + this.props.onContextMenu(event) + return + } + // No contextMenu was passed to us -> execute our own link-opener + if (event.target.tagName.toLowerCase() === 'a') { + const href = event.target.href + const isLocalFile = href.startsWith('file:') + if (isLocalFile) { + const absPath = uri2path(href) + try { + if (fs.lstatSync(absPath).isFile()) { + context.popup([ + { + label: i18n.__('Show in explorer'), + click: (e) => shell.showItemInFolder(absPath) + } + ]) + } + } catch (e) { + console.log('Error while evaluating if the file is locally available', e) + } + } + } } handleDoubleClick (e) { @@ -705,7 +733,6 @@ export default class MarkdownPreview extends React.Component { el.addEventListener('click', this.linkClickHandler) }) } catch (e) { - console.error(e) el.className = 'flowchart-error' el.innerHTML = 'Flowchart parse error: ' + e.message } @@ -726,7 +753,6 @@ export default class MarkdownPreview extends React.Component { el.addEventListener('click', this.linkClickHandler) }) } catch (e) { - console.error(e) el.className = 'sequence-error' el.innerHTML = 'Sequence diagram parse error: ' + e.message } @@ -752,7 +778,6 @@ export default class MarkdownPreview extends React.Component { const chart = new Chart(canvas, chartConfig) } catch (e) { - console.error(e) el.className = 'chart-error' el.innerHTML = 'chartjs diagram parse error: ' + e.message } diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 5073dc73..600b7e2d 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -74,24 +74,22 @@ const NoteItem = ({ ? note.title : {i18n.__('Empty note')}} - {['ALL', 'STORAGE'].includes(viewType) && -
-
{dateDisplay}
-
-
- {viewType === 'ALL' && storageName} - {viewType === 'STORAGE' && folderName} -
+
+
{dateDisplay}
+
+
+ {viewType === 'ALL' && storageName} + {viewType === 'STORAGE' && folderName}
-
} - +
+
{note.tags.length > 0 diff --git a/browser/components/NoteItem.styl b/browser/components/NoteItem.styl index 017ef6d0..e545ed99 100644 --- a/browser/components/NoteItem.styl +++ b/browser/components/NoteItem.styl @@ -368,13 +368,13 @@ body[data-theme="monokai"] .item-title .item-title-icon .item-bottom-time - color $ui-monokai-text-color + color $ui-monokai-active-color .item-bottom-tagList-item background-color alpha(white, 10%) color $ui-monokai-text-color &:hover // background-color alpha($ui-monokai-button--active-backgroundColor, 60%) - color #c0392b + color #f92672 .item-bottom-tagList-item background-color alpha(#fff, 20%) diff --git a/browser/components/NoteItemSimple.styl b/browser/components/NoteItemSimple.styl index 04f57fdc..4bb6f2b1 100644 --- a/browser/components/NoteItemSimple.styl +++ b/browser/components/NoteItemSimple.styl @@ -240,7 +240,7 @@ body[data-theme="monokai"] .item-simple-title-icon .item-simple-bottom-time transition 0.15s - color $ui-solarized-dark-text-color + color $ui-monokai-text-color .item-simple-bottom-tagList-item transition 0.15s background-color alpha(#fff, 20%) diff --git a/browser/components/TodoListPercentage.styl b/browser/components/TodoListPercentage.styl index 6116cd58..94e75599 100644 --- a/browser/components/TodoListPercentage.styl +++ b/browser/components/TodoListPercentage.styl @@ -39,7 +39,7 @@ body[data-theme="dark"] .percentageText color $ui-dark-text-color - + body[data-theme="solarized-dark"] .percentageBar background-color #002b36 @@ -52,10 +52,10 @@ body[data-theme="solarized-dark"] body[data-theme="monokai"] .percentageBar - background-color #f92672 + background-color: $ui-monokai-borderColor .progressBar - background-color: #373831 + background-color $ui-monokai-active-color .percentageText - color #fdf6e3 \ No newline at end of file + color $ui-monokai-text-color \ No newline at end of file diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index ea4219af..2e17a75b 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -206,11 +206,11 @@ code text-decoration none margin-right 2px pre - padding 0.5em !important + padding 0.5rem !important border solid 1px #D1D1D1 border-radius 5px overflow-x auto - margin 0 0 1em + margin 0 0 1rem display flex line-height 1.4em code @@ -226,8 +226,8 @@ pre flex 1 overflow-x auto &>span.filename - margin -8px 100% 8px -8px - padding 2px 6px + margin -0.5rem 100% 0.5rem -0.5rem + padding 0.125rem 0.375rem background-color #777; color white &:empty @@ -235,8 +235,8 @@ pre &>span.lineNumber display none font-size 1em - padding 0.5em 0 - margin -0.5em 0.5em -0.5em -0.5em + padding 0.5rem 0 + margin -0.5rem 0.5rem -0.5rem -0.5rem border-right 1px solid text-align right border-top-left-radius 4px diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js index f04d8cd0..0ea1960d 100644 --- a/browser/components/render/MermaidRender.js +++ b/browser/components/render/MermaidRender.js @@ -35,7 +35,6 @@ function render (element, content, theme) { element.innerHTML = svgGraph }) } catch (e) { - console.error(e) element.className = 'mermaid-error' element.innerHTML = 'mermaid diagram parse error: ' + e.message } 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-fence.js b/browser/lib/markdown-it-fence.js index 983ed71e..fd1c759d 100644 --- a/browser/lib/markdown-it-fence.js +++ b/browser/lib/markdown-it-fence.js @@ -66,7 +66,7 @@ module.exports = function (md, renderers, defaultRenderer) { const parameters = {} let langType = '' let fileName = '' - let firstLineNumber = 0 + let firstLineNumber = 1 let match = paramsRE.exec(params) if (match) { @@ -122,7 +122,7 @@ module.exports = function (md, renderers, defaultRenderer) { alt: ['paragraph', 'reference', 'blockquote', 'list'] }) - for (let name in renderers) { + for (const name in renderers) { md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index]) } 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-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index 05e5e7be..fb7267ee 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -14,7 +14,7 @@ module.exports = function sanitizePlugin (md, options) { options ) } - if (state.tokens[tokenIdx].type === 'fence') { + if (state.tokens[tokenIdx].type === '_fence') { // escapeHtmlCharacters has better performance state.tokens[tokenIdx].content = escapeHtmlCharacters( state.tokens[tokenIdx].content, 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 87cf86aa..f8ae7f05 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -122,7 +122,9 @@ class Markdown { } }) this.md.use(require('markdown-it-kbd')) - this.md.use(require('markdown-it-admonition')) + + this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']}) + this.md.use(require('./markdown-it-frontmatter')) this.md.use(require('./markdown-it-fence'), { chart: token => { @@ -130,31 +132,31 @@ class Markdown { token.parameters.format = 'yaml' } - return `
+        return `
           ${token.fileName}
           
${token.content}
` }, flowchart: token => { - return `
+        return `
           ${token.fileName}
           
${token.content}
` }, mermaid: token => { - return `
+        return `
           ${token.fileName}
           
${token.content}
` }, sequence: token => { - return `
+        return `
           ${token.fileName}
           
${token.content}
` } }, token => { - return `
+      return `
         ${token.fileName}
         ${createGutter(token.content, token.firstLineNumber)}
         ${token.content}
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 3564d6bf..9356a02c 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()
           }
@@ -746,6 +759,7 @@ class SnippetNoteDetail extends React.Component {
          this.handleChange(e)}
         />
       
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index e251dd42..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,41 @@ 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) { + const { router } = this.context + router.push(`/tags/${tag}`) } handleTagRemoveButtonClick (tag) { @@ -105,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 @@ -118,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 @@ -127,7 +187,7 @@ class TagSelect extends React.Component { - #{tag} + this.handleTagLabelClick(tag)}>#{tag}
-
{i18n.__('Toggle editor mode')}
+
{i18n.__('Toggle Editor Mode')}
this.handleHotkeyChange(e)} diff --git a/browser/main/modals/PreferencesModal/StorageItem.styl b/browser/main/modals/PreferencesModal/StorageItem.styl index 29dfbd0b..adcc483e 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.styl +++ b/browser/main/modals/PreferencesModal/StorageItem.styl @@ -9,13 +9,17 @@ box-sizing border-box border-bottom $default-border margin-bottom 5px + display flex .header-label - float left cursor pointer &:hover .header-label-editButton opacity 1 + flex 1 + white-space nowrap + text-overflow ellipsis + overflow hidden .header-label-path color $ui-inactive-text-color @@ -38,8 +42,8 @@ outline none .header-control - float right - + -webkit-box-flex: 1 + white-space nowrap .header-control-button width 30px height 25px diff --git a/browser/main/modals/PreferencesModal/StoragesTab.js b/browser/main/modals/PreferencesModal/StoragesTab.js index ad7472d2..046b24e6 100644 --- a/browser/main/modals/PreferencesModal/StoragesTab.js +++ b/browser/main/modals/PreferencesModal/StoragesTab.js @@ -70,7 +70,7 @@ class StoragesTab extends React.Component { }) return (
-
{i18n.__('Storages')}
+
{i18n.__('Storage Locations')}
{storageList.length > 0 ? storageList :
{i18n.__('No storage found.')}
diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 02a235ce..a45e1387 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -40,7 +40,7 @@ class UiTab extends React.Component { this.handleSettingError = (err) => { this.setState({UiAlert: { type: 'error', - message: err.message != null ? err.message : i18n.__('Error occurs!') + message: err.message != null ? err.message : i18n.__('An error occurred!') }}) } ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) @@ -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, @@ -124,7 +125,7 @@ class UiTab extends React.Component { this.props.haveToSave({ tab: 'UI', type: 'warning', - message: i18n.__('You have to save!') + message: i18n.__('Unsaved Changes!') }) } }) @@ -174,7 +175,9 @@ class UiTab extends React.Component {
{i18n.__('Interface')}
- {i18n.__('Interface Theme')} +
+ {i18n.__('Interface Theme')} +
this.handleUIChange(e)} @@ -203,6 +208,22 @@ class UiTab extends React.Component {
+
+
+ {i18n.__('Default New Note')} +
+
+ +
+
+
-
{i18n.__('Code block Theme')}
+
{i18n.__('Code Block Theme')}