From 10e0fcc965f226a498a8343e442042e1f5fc690e Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Wed, 2 Mar 2016 14:46:27 -0800 Subject: [PATCH] feat(find-in-thread): add the ability to find in a thread Summary: Find in thread Test Plan: todo Reviewers: bengotow, juan Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2660 --- .../message-list/lib/email-frame.cjsx | 3 +- .../message-list/lib/find-in-thread.jsx | 136 ++++++++++++ .../message-list/lib/message-list.cjsx | 13 +- .../stylesheets/find-in-thread.less | 57 +++++ .../stylesheets/message-list.less | 9 + keymaps/base.cson | 4 + keymaps/templates/Outlook.cson | 4 + menus/darwin.cson | 6 + menus/linux.cson | 6 + menus/win32.cson | 6 + src/components/evented-iframe.cjsx | 32 ++- src/components/scroll-region.cjsx | 33 +++ src/components/scrollbar-ticks.jsx | 36 +++ src/dom-utils.coffee | 27 ++- src/flux/actions.coffee | 5 +- .../stores/searchable-component-store.es6 | 209 ++++++++++++++++++ src/global/nylas-exports.coffee | 4 + src/searchable-components/iframe-searcher.es6 | 17 ++ src/searchable-components/real-dom-parser.es6 | 92 ++++++++ src/searchable-components/search-match.jsx | 21 ++ .../searchable-component-maker.jsx | 83 +++++++ .../unified-dom-parser.es6 | 193 ++++++++++++++++ .../virtual-dom-parser.es6 | 161 ++++++++++++++ src/virtual-dom-utils.es6 | 28 +++ static/components/scroll-region.less | 27 +++ static/email-frame.less | 9 + .../message-list/ic-findinthread-close@1x.png | Bin 0 -> 17806 bytes .../message-list/ic-findinthread-close@2x.png | Bin 0 -> 17920 bytes .../message-list/ic-findinthread-next@1x.png | Bin 0 -> 17790 bytes .../message-list/ic-findinthread-next@2x.png | Bin 0 -> 17922 bytes .../ic-findinthread-previous@1x.png | Bin 0 -> 17790 bytes .../ic-findinthread-previous@2x.png | Bin 0 -> 17911 bytes static/variables/ui-variables.less | 3 + 33 files changed, 1210 insertions(+), 14 deletions(-) create mode 100644 internal_packages/message-list/lib/find-in-thread.jsx create mode 100644 internal_packages/message-list/stylesheets/find-in-thread.less create mode 100644 src/components/scrollbar-ticks.jsx create mode 100644 src/flux/stores/searchable-component-store.es6 create mode 100644 src/searchable-components/iframe-searcher.es6 create mode 100644 src/searchable-components/real-dom-parser.es6 create mode 100644 src/searchable-components/search-match.jsx create mode 100644 src/searchable-components/searchable-component-maker.jsx create mode 100644 src/searchable-components/unified-dom-parser.es6 create mode 100644 src/searchable-components/virtual-dom-parser.es6 create mode 100644 src/virtual-dom-utils.es6 create mode 100644 static/images/message-list/ic-findinthread-close@1x.png create mode 100644 static/images/message-list/ic-findinthread-close@2x.png create mode 100644 static/images/message-list/ic-findinthread-next@1x.png create mode 100644 static/images/message-list/ic-findinthread-next@2x.png create mode 100644 static/images/message-list/ic-findinthread-previous@1x.png create mode 100644 static/images/message-list/ic-findinthread-previous@2x.png diff --git a/internal_packages/message-list/lib/email-frame.cjsx b/internal_packages/message-list/lib/email-frame.cjsx index b116fd9f9..deecf0de6 100644 --- a/internal_packages/message-list/lib/email-frame.cjsx +++ b/internal_packages/message-list/lib/email-frame.cjsx @@ -12,7 +12,8 @@ class EmailFrame extends React.Component content: React.PropTypes.string.isRequired render: => - + componentDidMount: => @_mounted = true diff --git a/internal_packages/message-list/lib/find-in-thread.jsx b/internal_packages/message-list/lib/find-in-thread.jsx new file mode 100644 index 000000000..fa3f6e052 --- /dev/null +++ b/internal_packages/message-list/lib/find-in-thread.jsx @@ -0,0 +1,136 @@ +import React from 'react' +import classnames from 'classnames' +import {Actions, MessageStore, SearchableComponentStore} from 'nylas-exports' +import {RetinaImg, KeyCommandsRegion} from 'nylas-component-kit' + +export default class FindInThread extends React.Component { + static displayName = "FindInThread"; + + constructor(props) { + super(props); + this.state = SearchableComponentStore.getCurrentSearchData() + } + + componentDidMount() { + this._usub = SearchableComponentStore.listen(this._onSearchableChange) + } + + componentWillUnmount() { + this._usub() + } + + _globalKeymapHandlers() { + return { + 'application:find-in-thread': this._onFindInThread, + 'application:find-in-thread-next': this._onNextResult, + 'application:find-in-thread-previous': this._onPrevResult, + } + } + + _onFindInThread = () => { + if (this.state.searchTerm === null) { + Actions.findInThread(""); + if (MessageStore.hasCollapsedItems()) { + Actions.toggleAllMessagesExpanded() + } + } + this._focusSearch() + } + + _onSearchableChange = () => { + this.setState(SearchableComponentStore.getCurrentSearchData()) + } + + _onFindChange = (event) => { + Actions.findInThread(event.target.value) + } + + _onFindKeyDown = (event) => { + if (event.key === "Enter") { + return event.shiftKey ? this._onPrevResult() : this._onNextResult() + } else if (event.key === "Escape") { + this._clearSearch() + React.findDOMNode(this.refs.searchBox).blur() + } + } + + _selectionText() { + if (this.state.globalIndex !== null && this.state.resultsLength > 0) { + return `${this.state.globalIndex + 1} of ${this.state.resultsLength}` + } + return "" + } + + _navEnabled() { return this.state.resultsLength > 0 } + + _onPrevResult = () => { + if (this._navEnabled()) { Actions.previousSearchResult() } + } + + _onNextResult = () => { + if (this._navEnabled()) { Actions.nextSearchResult() } + } + + _clearSearch = () => { + Actions.findInThread(null) + } + + _focusSearch = (event) => { + const cw = React.findDOMNode(this.refs.controlsWrap) + if (!event || !(cw && cw.contains(event.target))) { + React.findDOMNode(this.refs.searchBox).focus() + } + } + + render() { + const rootCls = classnames({ + "find-in-thread": true, + "enabled": this.state.searchTerm !== null, + }) + const btnCls = "btn btn-find-in-thread"; + return ( +
+ +
+
+ + + +
{this._selectionText()}
+ +
+ + + +
+ +
+ + +
+
+
+ ) + } + +} + diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx index 1cdc9b4a7..0a1ee9c21 100755 --- a/internal_packages/message-list/lib/message-list.cjsx +++ b/internal_packages/message-list/lib/message-list.cjsx @@ -1,6 +1,7 @@ _ = require 'underscore' React = require 'react' classNames = require 'classnames' +FindInThread = require './find-in-thread' MessageItemContainer = require './message-item-container' {Utils, @@ -13,7 +14,9 @@ MessageItemContainer = require './message-item-container' WorkspaceStore, ChangeLabelsTask, ComponentRegistry, - ChangeStarredTask} = require("nylas-exports") + ChangeStarredTask, + SearchableComponentStore + SearchableComponentMaker} = require("nylas-exports") {Spinner, RetinaImg, @@ -88,7 +91,7 @@ class MessageList extends React.Component if newDraftClientIds.length > 0 @_focusDraft(@_getMessageContainer(newDraftClientIds[0])) - _keymapHandlers: -> + _globalKeymapHandlers: -> 'application:reply': => @_createReplyOrUpdateExistingDraft('reply') 'application:reply-all': => @_createReplyOrUpdateExistingDraft('reply-all') 'application:forward': => @_onForward() @@ -190,10 +193,12 @@ class MessageList extends React.Component "messages-wrap": true "ready": not @state.loading - + +
{@_renderSubject()} @@ -402,4 +407,4 @@ class MessageList extends React.Component currentThread: MessageStore.thread() loading: MessageStore.itemsLoading() -module.exports = MessageList +module.exports = SearchableComponentMaker.extend(MessageList) diff --git a/internal_packages/message-list/stylesheets/find-in-thread.less b/internal_packages/message-list/stylesheets/find-in-thread.less new file mode 100644 index 000000000..3b7e66cae --- /dev/null +++ b/internal_packages/message-list/stylesheets/find-in-thread.less @@ -0,0 +1,57 @@ +@import 'ui-variables'; + +body.platform-win32 { + .find-in-thread { + } +} + +.find-in-thread { + background: @background-secondary; + border-bottom: 1px solid @border-secondary-bg; + text-align: right; + overflow: hidden; + + height: 0; + padding: 0 8px; + transition: all 125ms ease-in-out; + &.enabled { + padding: 4px 8px; + height: 35px; + } + + .controls-wrap { + display: inline-block; + } + + .selection-progress { + color: @text-color-very-subtle; + position: absolute; + top: 4px; + right: 54px; + font-size: 12px; + } + + .btn.btn-find-in-thread { + border: 0; + box-shadow: 0 0 0; + border-radius: 0; + background: transparent; + display: inline-block; + } + .input-wrap { + display: inline-block; + position: relative; + input { + height: 26px; + width: 230px; + padding-left: 8px; + font-size: 12px; + } + .btn-wrap { + width: 54px; + position: absolute; + top: 0; + right: 0; + } + } +} diff --git a/internal_packages/message-list/stylesheets/message-list.less b/internal_packages/message-list/stylesheets/message-list.less index e28b4afb1..24e06f3e2 100644 --- a/internal_packages/message-list/stylesheets/message-list.less +++ b/internal_packages/message-list/stylesheets/message-list.less @@ -120,6 +120,15 @@ body.platform-win32 { padding: 0; order: 2; + search-match, .search-match { + background: @text-color-search-match; + border-radius: @border-radius-base; + box-shadow: 0 0.5px 0.5px rgba(0,0,0,0.25); + &.current-match { + background: @text-color-search-current-match; + } + } + .show-hidden-messages { background-color: darken(@background-secondary, 4%); border: 1px solid darken(@background-secondary, 8%); diff --git a/keymaps/base.cson b/keymaps/base.cson index 4c81508a2..d7c62fbfa 100644 --- a/keymaps/base.cson +++ b/keymaps/base.cson @@ -66,6 +66,10 @@ 'cmdctrl-8': 'application:select-account-7' 'cmdctrl-9': 'application:select-account-8' + 'cmdctrl-f': 'application:find-in-thread' + 'cmdctrl-g': 'application:find-in-thread-next' + 'cmdctrl-shift-g': 'application:find-in-thread-previous' + 'body *[contenteditable].contenteditable': ### Basic formatting commands ### 'cmdctrl-u': 'contenteditable:underline' diff --git a/keymaps/templates/Outlook.cson b/keymaps/templates/Outlook.cson index ed7391ffc..7f90f3169 100644 --- a/keymaps/templates/Outlook.cson +++ b/keymaps/templates/Outlook.cson @@ -16,3 +16,7 @@ 'cmdctrl-n' : 'application:new-message' 'cmdctrl-shift-m': 'application:new-message' 'cmdctrl-enter': 'send' + + 'F4': 'application:find-in-thread' + 'shift-F4': 'application:find-in-thread-next' + 'ctrl-shift-F4': 'application:find-in-thread-previous' diff --git a/menus/darwin.cson b/menus/darwin.cson index 4e7a59ebf..548d53624 100644 --- a/menus/darwin.cson +++ b/menus/darwin.cson @@ -44,6 +44,12 @@ { label: 'Paste', command: 'core:paste' } { label: 'Paste and Match Style', command: 'core:paste-and-match-style' } { label: 'Select All', command: 'core:select-all' } + { type: 'separator' } + { label: 'Find', submenu: [ + { label: 'Find in Thread...', command: 'application:find-in-thread' } + { label: 'Find Next', command: 'application:find-in-thread-next' } + { label: 'Find Previous', command: 'application:find-in-thread-previous' } + ] } ] } diff --git a/menus/linux.cson b/menus/linux.cson index 289f6a0ee..6525e9667 100644 --- a/menus/linux.cson +++ b/menus/linux.cson @@ -25,6 +25,12 @@ { label: 'Paste and Match Style', command: 'core:paste-and-match-style' } { label: 'Select &All', command: 'core:select-all' } { type: 'separator' } + { label: 'Find', submenu: [ + { label: 'Find in Thread...', command: 'application:find-in-thread' } + { label: 'Find Next', command: 'application:find-in-thread-next' } + { label: 'Find Previous', command: 'application:find-in-thread-previous' } + ] } + { type: 'separator' } { label: 'Preferences', command: 'application:open-preferences' } ] } diff --git a/menus/win32.cson b/menus/win32.cson index fa887394f..2f04f16c9 100644 --- a/menus/win32.cson +++ b/menus/win32.cson @@ -11,6 +11,12 @@ { label: '&Paste', command: 'core:paste' } { label: 'Paste and Match Style', command: 'core:paste-and-match-style' } { label: 'Select &All', command: 'core:select-all' } + { type: 'separator' } + { label: 'Find', submenu: [ + { label: 'Find in Thread...', command: 'application:find-in-thread' } + { label: 'Find Next', command: 'application:find-in-thread-next' } + { label: 'Find Previous', command: 'application:find-in-thread-previous' } + ] } ] } diff --git a/src/components/evented-iframe.cjsx b/src/components/evented-iframe.cjsx index dca04917d..0990203f6 100644 --- a/src/components/evented-iframe.cjsx +++ b/src/components/evented-iframe.cjsx @@ -1,5 +1,9 @@ React = require 'react' -{RegExpUtils}= require 'nylas-exports' +{Utils, + RegExpUtils, + SearchableComponentMaker, + SearchableComponentStore}= require 'nylas-exports' +IFrameSearcher = require '../searchable-components/iframe-searcher' url = require 'url' _ = require "underscore" @@ -27,10 +31,25 @@ class EventedIFrame extends React.Component