diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 0bd2e397..b90bbfa1 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -140,7 +140,7 @@ export default class MarkdownPreview extends React.Component { ` this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) - this.refs.root.contentWindow.document.body.innerHTML = markdown(value) + this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value) Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { el.addEventListener('click', this.anchorClickHandler) diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 54ef51c8..8a1c9a99 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -129,8 +129,40 @@ md.renderer.render = function render (tokens, options, env) { } window.md = md -export default function markdown (content) { - if (!_.isString(content)) content = '' - - return md.render(content) +function strip (input) { + var output = input + try { + output = output + .replace(/^([\s\t]*)([\*\-\+]|\d\.)\s+/gm, '$1') + .replace(/\n={2,}/g, '\n') + .replace(/~~/g, '') + .replace(/`{3}.*\n/g, '') + .replace(/<(.*?)>/g, '$1') + .replace(/^[=\-]{2,}\s*$/g, '') + .replace(/\[\^.+?\](\: .*?$)?/g, '') + .replace(/\s{0,2}\[.*?\]: .*?$/g, '') + .replace(/\!\[.*?\][\[\(].*?[\]\)]/g, '') + .replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1') + .replace(/>/g, '') + .replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '') + .replace(/^\#{1,6}\s*([^#]*)\s*(\#{1,6})?/gm, '$1') + .replace(/([\*_]{1,3})(\S.*?\S)\1/g, '$2') + .replace(/(`{3,})(.*?)\1/gm, '$2') + .replace(/^-{3,}\s*$/g, '') + .replace(/`(.+?)`/g, '$1') + .replace(/\n{2,}/g, '\n\n') + } catch (e) { + console.error(e) + return input + } + return output } + +const markdown = { + render: function markdown (content) { + if (!_.isString(content)) content = '' + return md.render(content) + }, + strip +} +export default markdown diff --git a/browser/main/Detail/FolderSelect.js b/browser/main/Detail/FolderSelect.js index 0b1b6a37..47fa6bad 100644 --- a/browser/main/Detail/FolderSelect.js +++ b/browser/main/Detail/FolderSelect.js @@ -200,6 +200,11 @@ class FolderSelect extends React.Component { let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0] + if (this.state.search.trim().length > 0) { + let filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i') + options = options.filter((option) => filter.test(option.folder.name)) + } + let optionList = options .map((option, index) => { return ( diff --git a/browser/main/Detail/FolderSelect.styl b/browser/main/Detail/FolderSelect.styl index 41db7525..0abe6589 100644 --- a/browser/main/Detail/FolderSelect.styl +++ b/browser/main/Detail/FolderSelect.styl @@ -57,12 +57,13 @@ .search-optionList position fixed + max-height 450px + overflow auto z-index 200 background-color white border-radius 2px box-shadow 2px 2px 10px gray - .search-optionList-item height 34px width 250px diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 93b3a40a..43b129ec 100644 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -8,6 +8,7 @@ import FolderSelect from './FolderSelect' import dataApi from 'browser/main/lib/dataApi' import { hashHistory } from 'react-router' import ee from 'browser/main/lib/eventEmitter' +import markdown from 'browser/lib/markdown' const electron = require('electron') const { remote } = electron @@ -72,6 +73,8 @@ class MarkdownNoteDetail extends React.Component { } } + title = markdown.strip(title) + return title } diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 449f45e5..af818b43 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -247,25 +247,37 @@ class SnippetNoteDetail extends React.Component { }) } - handleTabButtonClick (index) { - return (e) => { - this.setState({ - snippetIndex: index - }) + handleTabButtonClick (e, index) { + this.setState({ + snippetIndex: index + }) + } + + handleTabDeleteButtonClick (e, index) { + if (this.state.note.snippets.length > 1) { + if (this.state.note.snippets[index].content.trim().length > 0) { + let dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'warning', + message: 'Delete a snippet', + detail: 'This work cannot be undone.', + buttons: ['Confirm', 'Cancel'] + }) + if (dialogIndex === 0) { + this.deleteSnippetByIndex(index) + } + } else { + this.deleteSnippetByIndex(index) + } } } - handleTabDeleteButtonClick (index) { - return (e) => { - if (this.state.note.snippets.length > 1) { - let snippets = this.state.note.snippets.slice() - snippets.splice(index, 1) - this.state.note.snippets = snippets - this.setState({ - note: this.state.note - }) - } - } + deleteSnippetByIndex (index) { + let snippets = this.state.note.snippets.slice() + snippets.splice(index, 1) + this.state.note.snippets = snippets + this.setState({ + note: this.state.note + }) } handleNameInputChange (e, index) { @@ -344,7 +356,7 @@ class SnippetNoteDetail extends React.Component { key={index} > {note.snippets.length > 1 && diff --git a/browser/main/Main.js b/browser/main/Main.js index 9c3138e1..cbc1e7ec 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -20,8 +20,10 @@ class Main extends React.Component { let { config } = props this.state = { - isSliderFocused: false, - listWidth: config.listWidth + isRightSliderFocused: false, + listWidth: config.listWidth, + navWidth: config.listWidth, + isLeftSliderFocused: false } } @@ -49,17 +51,24 @@ class Main extends React.Component { }) } - handleSlideMouseDown (e) { + handleLeftSlideMouseDown (e) { e.preventDefault() this.setState({ - isSliderFocused: true + isLeftSliderFocused: true + }) + } + + handleRightSlideMouseDown (e) { + e.preventDefault() + this.setState({ + isRightSliderFocused: true }) } handleMouseUp (e) { - if (this.state.isSliderFocused) { + if (this.state.isRightSliderFocused) { this.setState({ - isSliderFocused: false + isRightSliderFocused: false }, () => { let { dispatch } = this.props let newListWidth = this.state.listWidth @@ -71,10 +80,24 @@ class Main extends React.Component { }) }) } + if (this.state.isLeftSliderFocused) { + this.setState({ + isLeftSliderFocused: false + }, () => { + let { dispatch } = this.props + let navWidth = this.state.navWidth + // TODO: ConfigManager should dispatch itself. + ConfigManager.set({listWidth: navWidth}) + dispatch({ + type: 'SET_NAV_WIDTH', + listWidth: navWidth + }) + }) + } } handleMouseMove (e) { - if (this.state.isSliderFocused) { + if (this.state.isRightSliderFocused) { let offset = this.refs.body.getBoundingClientRect().left let newListWidth = e.pageX - offset if (newListWidth < 10) { @@ -86,6 +109,17 @@ class Main extends React.Component { listWidth: newListWidth }) } + if (this.state.isLeftSliderFocused) { + let navWidth = e.pageX + if (navWidth < 80) { + navWidth = 80 + } else if (navWidth > 600) { + navWidth = 600 + } + this.setState({ + navWidth: navWidth + }) + } } render () { @@ -105,9 +139,20 @@ class Main extends React.Component { 'config', 'location' ])} + width={this.state.navWidth} /> + {!config.isSideNavFolded && +
this.handleLeftSlideMouseDown(e)} + draggable='false' + > +
+
+ }
-
this.handleSlideMouseDown(e)} + onMouseDown={(e) => this.handleRightSlideMouseDown(e)} draggable='false' >
@@ -143,7 +188,7 @@ class Main extends React.Component { 'params', 'location' ])} - ignorePreviewPointerEvents={this.state.isSliderFocused} + ignorePreviewPointerEvents={this.state.isRightSliderFocused} />
{ this.refs.root.focus() } + + this.state = { + range: 0 + } } componentDidMount () { @@ -28,6 +36,29 @@ class NoteList extends React.Component { ee.on('lost:focus', this.focusHandler) } + componentWillReceiveProps (nextProps) { + if (nextProps.location.pathname !== this.props.location.pathname) { + this.resetScroll() + } + } + + resetScroll () { + this.refs.root.scrollTop = 0 + this.setState({ + range: 0 + }) + } + + handleScroll (e) { + let notes = this.notes + + if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 250 && notes.length > this.state.range * 10 + 10) { + this.setState({ + range: this.state.range + 1 + }) + } + } + componentWillUnmount () { clearInterval(this.refreshTimer) @@ -36,7 +67,7 @@ class NoteList extends React.Component { ee.off('lost:focus', this.focusHandler) } - componentDidUpdate () { + componentDidUpdate (prevProps) { let { location } = this.props if (this.notes.length > 0 && location.query.key == null) { let { router } = this.context @@ -50,13 +81,14 @@ class NoteList extends React.Component { } // Auto scroll - if (_.isString(location.query.key)) { + if (_.isString(location.query.key) && prevProps.location.query.key !== location.query.key) { let targetIndex = _.findIndex(this.notes, (note) => { return note != null && note.storage + '-' + note.key === location.query.key }) if (targetIndex > -1) { let list = this.refs.root let item = list.childNodes[targetIndex] + if (item == null) return false let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0 if (overflowBelow) { @@ -185,17 +217,58 @@ class NoteList extends React.Component { : [] } - handleNoteClick (uniqueKey) { - return (e) => { - let { router } = this.context - let { location } = this.props + handleNoteClick (e, uniqueKey) { + let { router } = this.context + let { location } = this.props - router.push({ - pathname: location.pathname, - query: { - key: uniqueKey - } - }) + router.push({ + pathname: location.pathname, + query: { + key: uniqueKey + } + }) + } + + handleNoteContextMenu (e, uniqueKey) { + let menu = new Menu() + menu.append(new MenuItem({ + label: 'Delete Note', + click: (e) => this.handleDeleteNote(e, uniqueKey) + })) + menu.popup() + } + + handleDeleteNote (e, uniqueKey) { + let index = dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'warning', + message: 'Delete a note', + detail: 'This work cannot be undone.', + buttons: ['Confirm', 'Cancel'] + }) + if (index === 0) { + let { dispatch, location } = this.props + let splitted = uniqueKey.split('-') + let storageKey = splitted.shift() + let noteKey = splitted.shift() + + dataApi + .deleteNote(storageKey, noteKey) + .then((data) => { + let dispatchHandler = () => { + dispatch({ + type: 'DELETE_NOTE', + storageKey: data.storageKey, + noteKey: data.noteKey + }) + } + + if (location.query.key === uniqueKey) { + ee.once('list:moved', dispatchHandler) + ee.emit('list:next') + } else { + dispatchHandler() + } + }) } } @@ -204,7 +277,7 @@ class NoteList extends React.Component { this.notes = notes = this.getNotes() .sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)) - let noteList = notes + let noteList = notes.slice(0, 10 + 10 * this.state.range) .map((note) => { if (note == null) return null let storage = data.storageMap.get(note.storage) @@ -226,7 +299,8 @@ class NoteList extends React.Component { : 'item' } key={note.storage + '-' + note.key} - onClick={(e) => this.handleNoteClick(note.storage + '-' + note.key)(e)} + onClick={(e) => this.handleNoteClick(e, note.storage + '-' + note.key)} + onContextMenu={(e) => this.handleNoteContextMenu(e, note.storage + '-' + note.key)} >
@@ -277,6 +351,7 @@ class NoteList extends React.Component { tabIndex='-1' onKeyDown={(e) => this.handleNoteListKeyDown(e)} style={this.props.style} + onScroll={(e) => this.handleScroll(e)} > {noteList}
diff --git a/browser/main/SideNav/SideNav.styl b/browser/main/SideNav/SideNav.styl index 1fad5584..f6a7de1c 100644 --- a/browser/main/SideNav/SideNav.styl +++ b/browser/main/SideNav/SideNav.styl @@ -22,6 +22,7 @@ .top-menu-label margin-left 5px + overflow ellipsis .menu margin-top 15px @@ -33,6 +34,7 @@ font-size 14px width 100% text-align left + overflow ellipsis .menu-button--active @extend .menu-button diff --git a/browser/main/SideNav/StorageItem.styl b/browser/main/SideNav/StorageItem.styl index 47de54b5..0e301e34 100644 --- a/browser/main/SideNav/StorageItem.styl +++ b/browser/main/SideNav/StorageItem.styl @@ -65,6 +65,7 @@ margin 2px 0 text-align left border none + overflow ellipsis font-size 14px &:first-child margin-top 0 diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 3e541413..09e312ff 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -50,11 +50,13 @@ class SideNav extends React.Component { isFolded={isFolded} /> }) - + let style = {} + if (!isFolded) style.width = this.props.width return (
+
) diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 9c270c2a..0fe9dbc8 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -8,13 +8,15 @@ const defaultConfig = { zoom: 1, isSideNavFolded: false, listWidth: 250, + navWidth: 200, hotkey: { toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S', toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E' }, ui: { theme: 'default', - disableDirectWrite: false + disableDirectWrite: false, + defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' }, editor: { theme: 'xcode', diff --git a/browser/main/modals/PreferencesModal/StorageItem.js b/browser/main/modals/PreferencesModal/StorageItem.js index 314e5cba..6fc124bd 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.js +++ b/browser/main/modals/PreferencesModal/StorageItem.js @@ -35,6 +35,10 @@ class UnstyledFolderItem extends React.Component { } handleConfirmButtonClick (e) { + this.confirm() + } + + confirm () { let { storage, folder } = this.props dataApi .updateFolder(storage.key, folder.key, { @@ -87,6 +91,17 @@ class UnstyledFolderItem extends React.Component { }) } + handleFolderItemBlur (e) { + let el = e.relatedTarget + while (el != null) { + if (el === this.refs.root) { + return false + } + el = el.parentNode + } + this.confirm() + } + renderEdit (e) { const popover = { position: 'absolute', zIndex: 2 } const cover = { @@ -97,7 +112,11 @@ class UnstyledFolderItem extends React.Component { position: 'absolute' }, this.state.folder.colorPickerPos) return ( -
+
this.handleFolderItemBlur(e)} + tabIndex='-1' + ref='root' + >