From 54437cec197dbb0efa700da945863fbc04b8d71c Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Thu, 8 Sep 2016 18:30:30 +0900 Subject: [PATCH 1/9] confirm on blur --- .../modals/PreferencesModal/StorageItem.js | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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' + >
From 6fc421810feed633123439baf3e73eddd62aa62e Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Thu, 8 Sep 2016 22:01:27 +0900 Subject: [PATCH 3/9] fix bugs Auto scroll method should not be called when selecting note out of list. SearchInput bug --- browser/main/NoteList/index.js | 1 + browser/main/TopBar/index.js | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index 70e54aef..90114c0e 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -84,6 +84,7 @@ class NoteList extends React.Component { 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) { diff --git a/browser/main/TopBar/index.js b/browser/main/TopBar/index.js index 6cf478ad..f6433581 100644 --- a/browser/main/TopBar/index.js +++ b/browser/main/TopBar/index.js @@ -63,8 +63,9 @@ class TopBar extends React.Component { } getOptions () { - let { notes } = this.props + let { data } = this.props let { search } = this.state + let notes = data.noteMap.map((note) => note) if (search.trim().length === 0) return [] let searchBlocks = search.split(' ') searchBlocks.forEach((block) => { @@ -135,14 +136,14 @@ class TopBar extends React.Component { } render () { - let { config, style, storages } = this.props + let { config, style, data } = this.props let searchOptionList = this.getOptions() .map((note) => { - let storage = _.find(storages, {key: note.storage}) + let storage = data.storageMap.get(note.storage) let folder = _.find(storage.folders, {key: note.folder}) return
this.handleOptionClick(note.uniqueKey)(e)} + key={note.storage + '-' + note.key} + onClick={(e) => this.handleOptionClick(note.storage + '-' + note.key)(e)} >
From 0a707b3f020e2c765b688ad000ef7e050d60e223 Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Thu, 8 Sep 2016 22:02:07 +0900 Subject: [PATCH 4/9] improve FolderSelect maximum height and filtering by name --- browser/main/Detail/FolderSelect.js | 5 +++++ browser/main/Detail/FolderSelect.styl | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) 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 From 27e0252ccdd845db21e01953609d443438395ac6 Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Thu, 8 Sep 2016 22:36:59 +0900 Subject: [PATCH 5/9] resizable SideNav --- browser/main/Main.js | 65 ++++++++++++++++++++++----- browser/main/SideNav/SideNav.styl | 2 + browser/main/SideNav/StorageItem.styl | 1 + browser/main/SideNav/index.js | 4 +- browser/main/lib/ConfigManager.js | 1 + browser/main/store.js | 3 ++ 6 files changed, 65 insertions(+), 11 deletions(-) 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} />
}) - + let style = {} + if (!isFolded) style.width = this.props.width return (
{note.snippets.length > 1 && From 2b85aa1b88b706622ba9f1a9728b1f550d350383 Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Sat, 10 Sep 2016 10:07:41 +0900 Subject: [PATCH 8/9] default note type --- browser/main/TopBar/TopBar.styl | 39 +++++---- browser/main/TopBar/index.js | 139 ++++++++++++++++++++++++++++-- browser/main/lib/ConfigManager.js | 3 +- 3 files changed, 157 insertions(+), 24 deletions(-) diff --git a/browser/main/TopBar/TopBar.styl b/browser/main/TopBar/TopBar.styl index dafe6a54..b2cfef67 100644 --- a/browser/main/TopBar/TopBar.styl +++ b/browser/main/TopBar/TopBar.styl @@ -15,10 +15,13 @@ $control-height = 34px border $ui-border border-radius 20px overflow hidden + display flex + .control-search - absolute top left bottom - right 40px + height 32px + flex 1 background-color white + position relative .control-search-icon absolute top bottom left @@ -76,10 +79,24 @@ $control-height = 34px color $ui-inactive-text-color line-height 150px text-align center + +.control-contextButton + display block + width 20px + height $control-height - 2 + navButtonColor() + border-left $ui-border + font-size 14px + line-height 28px + padding 0 + &:active + border-color $ui-button--active-backgroundColor + &:hover .control-newPostButton-tooltip + opacity 1 + .control-newPostButton display block - absolute top right bottom - width 40px + width 36px height $control-height - 2 navButtonColor() border-left $ui-border @@ -143,10 +160,9 @@ body[data-theme="dark"] color $ui-inactive-text-color padding-right 3px .control-search-optionList-empty - height 150px color $ui-inactive-text-color - line-height 150px - text-align center + + .control-contextButton, .control-newPostButton colorDarkDefaultButton() border-color $ui-dark-borderColor @@ -155,12 +171,3 @@ body[data-theme="dark"] .control-newPostButton-tooltip darkTooltip() - position fixed - pointer-events none - top 45px - left 385px - z-index 10 - padding 5px - line-height normal - opacity 0 - transition 0.1s diff --git a/browser/main/TopBar/index.js b/browser/main/TopBar/index.js index f6433581..1f156bdc 100644 --- a/browser/main/TopBar/index.js +++ b/browser/main/TopBar/index.js @@ -6,8 +6,12 @@ import modal from 'browser/main/lib/modal' import NewNoteModal from 'browser/main/modals/NewNoteModal' import { hashHistory } from 'react-router' import ee from 'browser/main/lib/eventEmitter' +import ConfigManager from 'browser/main/lib/ConfigManager' +import dataApi from 'browser/main/lib/dataApi' const OSX = window.process.platform === 'darwin' +const { remote } = require('electron') +const { Menu, MenuItem } = remote class TopBar extends React.Component { constructor (props) { @@ -33,7 +37,30 @@ class TopBar extends React.Component { } handleNewPostButtonClick (e) { - let { data, params, dispatch, location } = this.props + let { config } = this.props + + switch (config.ui.defaultNote) { + case 'MARKDOWN_NOTE': + this.createNote('MARKDOWN_NOTE') + break + case 'SNIPPET_NOTE': + this.createNote('SNIPPET_NOTE') + break + case 'ALWAYS_ASK': + let { dispatch, location } = this.props + let { storage, folder } = this.resolveTargetFolder() + + modal.open(NewNoteModal, { + storage: storage.key, + folder: folder.key, + dispatch, + location + }) + } + } + + resolveTargetFolder () { + let { data, params } = this.props let storage = data.storageMap.get(params.storageKey) // Find first storage @@ -48,12 +75,10 @@ class TopBar extends React.Component { if (folder == null) folder = storage.folders[0] if (folder == null) throw new Error('No folder to craete a note') - modal.open(NewNoteModal, { - storage: storage.key, - folder: folder.key, - dispatch, - location - }) + return { + storage, + folder + } } handleSearchChange (e) { @@ -135,6 +160,101 @@ class TopBar extends React.Component { } } + handleContextButtonClick (e) { + let { config } = this.props + + let menu = new Menu() + menu.append(new MenuItem({ + label: 'Create Markdown Note', + click: (e) => this.createNote('MARKDOWN_NOTE') + })) + menu.append(new MenuItem({ + label: 'Create Snippet Note', + click: (e) => this.createNote('SNIPPET_NOTE') + })) + menu.append(new MenuItem({ + type: 'separator' + })) + menu.append(new MenuItem({ + label: 'Change Default Note', + submenu: [ + { + type: 'radio', + label: 'Markdown Note', + checked: config .ui.defaultNote === 'MARKDOWN_NOTE', + click: (e) => this.setDefaultNote('MARKDOWN_NOTE') + }, + { + type: 'radio', + label: 'Snippet Note', + checked: config.ui.defaultNote === 'SNIPPET_NOTE', + click: (e) => this.setDefaultNote('SNIPPET_NOTE') + }, + { + type: 'radio', + label: 'Always Ask', + checked: config.ui.defaultNote === 'ALWAYS_ASK', + click: (e) => this.setDefaultNote('ALWAYS_ASK') + } + ] + })) + menu.popup(remote.getCurrentWindow()) + } + + createNote (noteType) { + let { dispatch, location } = this.props + if (noteType !== 'MARKDOWN_NOTE' && noteType !== 'SNIPPET_NOTE') throw new Error('Invalid note type.') + + let { storage, folder } = this.resolveTargetFolder() + + let newNote = noteType === 'MARKDOWN_NOTE' + ? { + type: 'MARKDOWN_NOTE', + folder: folder.key, + title: '', + content: '' + } + : { + type: 'SNIPPET_NOTE', + folder: folder.key, + title: '', + description: '', + snippets: [{ + name: '', + mode: 'text', + content: '' + }] + } + + dataApi + .createNote(storage.key, newNote) + .then((note) => { + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + hashHistory.push({ + pathname: location.pathname, + query: {key: note.storage + '-' + note.key} + }) + ee.emit('detail:focus') + }) + } + + setDefaultNote (defaultNote) { + let { config, dispatch } = this.props + let ui = Object.assign(config.ui) + ui.defaultNote = defaultNote + ConfigManager.set({ + ui + }) + + dispatch({ + type: 'SET_UI', + config: ConfigManager.get() + }) + } + render () { let { config, style, data } = this.props let searchOptionList = this.getOptions() @@ -204,6 +324,11 @@ class TopBar extends React.Component { New Note {OSX ? '⌘' : '^'} + n +
) diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 53d907ae..0fe9dbc8 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -15,7 +15,8 @@ const defaultConfig = { }, ui: { theme: 'default', - disableDirectWrite: false + disableDirectWrite: false, + defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE' }, editor: { theme: 'xcode', From 9f4dd909a88972c9d72398b9352804d688ea1156 Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Sat, 10 Sep 2016 14:25:45 +0900 Subject: [PATCH 9/9] right click to delete a note --- browser/main/NoteList/index.js | 68 ++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index 90114c0e..bd016a47 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -4,6 +4,10 @@ import styles from './NoteList.styl' import moment from 'moment' import _ from 'lodash' import ee from 'browser/main/lib/eventEmitter' +import dataApi from 'browser/main/lib/dataApi' + +const { remote } = require('electron') +const { Menu, MenuItem, dialog } = remote class NoteList extends React.Component { constructor (props) { @@ -213,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() + } + }) } } @@ -254,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)} >