diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx
index 567754061..fd258f062 100644
--- a/internal_packages/composer/lib/composer-view.cjsx
+++ b/internal_packages/composer/lib/composer-view.cjsx
@@ -11,6 +11,7 @@ _ = require 'underscore'
{ResizableRegion,
InjectedComponentSet,
InjectedComponent,
+ ScrollRegion,
RetinaImg} = require 'nylas-component-kit'
FileUpload = require './file-upload'
@@ -151,58 +152,46 @@ class ComposerView extends React.Component
_renderComposer: =>
-
+ {@_renderBodyScrollbar()}
-
-
@_showAndFocusCc()}>Cc
+
+
+ @_showAndFocusCc()}>Cc
- @_showAndFocusBcc()}>Bcc
+ @_showAndFocusBcc()}>Bcc
- @setState {showsubject: true}}>Subject
+ @setState {showsubject: true}}>Subject
-
-
-
+
+
+
+
+ {@_renderFields()}
+
+
+ {@_renderBody()}
+ {@_renderFooterRegions()}
+
- {@_renderFields()}
-
-
-
-
- {@_renderFooterRegions()}
-
-
-
{@_renderActionsRegion()}
@@ -263,6 +252,31 @@ class ComposerView extends React.Component
fields
+ _renderBodyScrollbar: =>
+ if @props.mode is "inline"
+ []
+ else
+
@refs.scrollregion } />
+
+ _renderBody: =>
+ if @props.mode is "inline"
+ @_renderBodyContenteditable()
+ else
+ @refs.scrollbar }>
+ {@_renderBodyContenteditable()}
+
+
+ _renderBodyContenteditable: =>
+
_renderFooterRegions: =>
return unless @props.localId
@@ -389,10 +403,10 @@ class ComposerView extends React.Component
Utils.isForwardedMessage(draft)
# This lets us click outside of the `contenteditable`'s `contentBody`
- # and still focus on the contenteditable
+ # and simulate what happens when you click beneath the text *in* the
+ # contentEditable.
_onClickComposeBody: (event) =>
- if event.target is React.findDOMNode(@refs.composeBody)
- @focus("contentBody")
+ @refs.contentBody.selectEnd()
_onDraftChanged: =>
return unless @_proxy
diff --git a/internal_packages/composer/lib/contenteditable-component.cjsx b/internal_packages/composer/lib/contenteditable-component.cjsx
index fa7ee6d27..b6820815f 100644
--- a/internal_packages/composer/lib/contenteditable-component.cjsx
+++ b/internal_packages/composer/lib/contenteditable-component.cjsx
@@ -99,6 +99,7 @@ class ContenteditableComponent extends React.Component
tabIndex={@props.tabIndex}
style={@props.style ? {}}
onBlur={@_onBlur}
+ onClick={@_onClick}
onPaste={@_onPaste}
onInput={@_onInput}
dangerouslySetInnerHTML={@_dangerouslySetInnerHTML()}>
@@ -108,6 +109,21 @@ class ContenteditableComponent extends React.Component
focus: =>
@_editableNode().focus()
+ selectEnd: =>
+ range = document.createRange()
+ range.selectNodeContents(@_editableNode())
+ range.collapse(false)
+ @_editableNode().focus()
+ selection = window.getSelection()
+ selection.removeAllRanges()
+ selection.addRange(range)
+
+ _onClick: (event) ->
+ # We handle mouseDown, mouseMove, mouseUp, but we want to stop propagation
+ # of `click` to make it clear that we've handled the event.
+ # Note: Related to composer-view#_onClickComposeBody
+ event.stopPropagation()
+
_onInput: (event) =>
@_dragging = false
diff --git a/internal_packages/composer/stylesheets/composer.less b/internal_packages/composer/stylesheets/composer.less
index 72df0c742..5f962e984 100644
--- a/internal_packages/composer/stylesheets/composer.less
+++ b/internal_packages/composer/stylesheets/composer.less
@@ -44,20 +44,24 @@
}
.composer-content-wrap {
- position: relative;
z-index: 1;
-
- width: 100%;
- max-width: @compose-width;
- margin: 0 auto;
padding: 0;
- padding-top: 5px;
-
flex: 1;
display: flex;
+ position: relative;
flex-flow: column;
}
+ .composer-centered {
+ display:flex;
+ position: relative;
+ flex-direction: column;
+ flex: 1;
+ width: 100%;
+ max-width: @compose-width;
+ margin: 0 auto;
+ margin-top:@spacing-standard;
+ }
.text-actions {
text-align: right;
line-height: 1.4;
@@ -133,6 +137,10 @@
}
}
+ .compose-body-scroll {
+ position:initial;
+ }
+
.compose-body {
flex: 1;
z-index: 1;
@@ -200,10 +208,6 @@ body.is-blurred .composer-inner-wrap .tokenizing-field .token {
height: 100%;
}
- .composer-content-wrap {
- margin-top: @spacing-standard;
- }
-
.composer-inner-wrap {
.composer-action-bar-wrap {
diff --git a/src/components/scroll-region.cjsx b/src/components/scroll-region.cjsx
index 1a811a75f..8053c42a6 100644
--- a/src/components/scroll-region.cjsx
+++ b/src/components/scroll-region.cjsx
@@ -3,10 +3,109 @@ React = require 'react/addons'
{Utils} = require 'nylas-exports'
classNames = require 'classnames'
+class Scrollbar extends React.Component
+ @displayName: 'Scrollbar'
+ @propTypes:
+ scrollTooltipComponent: React.PropTypes.func
+ getScrollRegion: React.PropTypes.func
+
+ constructor: (@props) ->
+ @state =
+ totalHeight: 0
+ trackHeight: 0
+ viewportHeight: 0
+ viewportScrollTop: 0
+ dragging: false
+ scrolling: false
+
+ componentWillUnmount: =>
+ @_onHandleUp({preventDefault: -> })
+
+ setStateFromScrollRegion: (state) ->
+ @setState(state)
+
+ render: ->
+ containerClasses = classNames
+ 'scrollbar-track': true
+ 'dragging': @state.dragging
+ 'scrolling': @state.scrolling
+
+ tooltip = []
+ if @props.scrollTooltipComponent
+ tooltip = <@props.scrollTooltipComponent viewportCenter={@state.viewportScrollTop + @state.viewportHeight / 2} totalHeight={@state.totalHeight} />
+
+
+
+ recomputeDimensions: (options = {}) =>
+ if @props.getScrollRegion?
+ @props.getScrollRegion()._recomputeDimensions(options)
+ @_recomputeDimensions(options)
+
+ _recomputeDimensions: ({avoidForcingLayout}) =>
+ if not avoidForcingLayout
+ trackNode = React.findDOMNode(@refs.track)
+ trackHeight = trackNode.clientHeight
+ if trackHeight isnt @state.trackHeight
+ @setState({trackHeight})
+
+ _scrollbarHandleStyles: =>
+ handleHeight = @_getHandleHeight()
+ handleTop = (@state.viewportScrollTop / (@state.totalHeight - @state.viewportHeight)) * (@state.trackHeight - handleHeight)
+
+ position:'relative'
+ height: handleHeight
+ top: handleTop
+
+ _scrollbarWrapStyles: =>
+ position:'absolute'
+ top: 0
+ bottom: 0
+ right: 0
+ zIndex: 2
+
+ _onHandleDown: (event) =>
+ handleNode = React.findDOMNode(@refs.handle)
+ @_trackOffset = React.findDOMNode(@refs.track).getBoundingClientRect().top
+ @_mouseOffsetWithinHandle = event.pageY - handleNode.getBoundingClientRect().top
+ window.addEventListener("mousemove", @_onHandleMove)
+ window.addEventListener("mouseup", @_onHandleUp)
+ @setState(dragging: true)
+ event.preventDefault()
+
+ _onHandleMove: (event) =>
+ trackY = event.pageY - @_trackOffset - @_mouseOffsetWithinHandle
+ trackPxToViewportPx = (@state.totalHeight - @state.viewportHeight) / (@state.trackHeight - @_getHandleHeight())
+ @props.getScrollRegion().scrollTop = trackY * trackPxToViewportPx
+ event.preventDefault()
+
+ _onHandleUp: (event) =>
+ window.removeEventListener("mousemove", @_onHandleMove)
+ window.removeEventListener("mouseup", @_onHandleUp)
+ @setState(dragging: false)
+ event.preventDefault()
+
+ _onHandleClick: (event) =>
+ # Avoid event propogating up to track
+ event.stopPropagation()
+
+ _onScrollJump: (event) =>
+ @_trackOffset = React.findDOMNode(@refs.track).getBoundingClientRect().top
+ @_mouseOffsetWithinHandle = @_getHandleHeight() / 2
+ @_onHandleMove(event)
+
+ _getHandleHeight: =>
+ Math.min(@state.totalHeight, Math.max(40, (@state.trackHeight / @state.totalHeight) * @state.trackHeight))
+
+
###
The ScrollRegion component attaches a custom scrollbar.
###
-
class ScrollRegion extends React.Component
@displayName: "ScrollRegion"
@@ -16,13 +115,13 @@ class ScrollRegion extends React.Component
className: React.PropTypes.string
scrollTooltipComponent: React.PropTypes.func
children: React.PropTypes.oneOfType([React.PropTypes.element, React.PropTypes.array])
+ getScrollbar: React.PropTypes.func
constructor: (@props) ->
@state =
totalHeight:0
viewportHeight: 0
viewportScrollTop: 0
- dragging: false
scrolling: false
Object.defineProperty(@, 'scrollTop', {
@@ -31,10 +130,7 @@ class ScrollRegion extends React.Component
})
componentDidMount: =>
- @_recomputeDimensions()
-
- componentWillUnmount: =>
- @_onHandleUp()
+ @recomputeDimensions()
shouldComponentUpdate: (newProps, newState) =>
# Because this component renders @props.children, it needs to update
@@ -48,20 +144,17 @@ class ScrollRegion extends React.Component
'dragging': @state.dragging
'scrolling': @state.scrolling
+ scrollbar = []
+ if not @props.getScrollbar
+ scrollbar =
+
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
- tooltip = []
- if @props.scrollTooltipComponent
- tooltip = <@props.scrollTooltipComponent viewportCenter={@state.viewportScrollTop + @state.viewportHeight / 2} totalHeight={@state.totalHeight} />
-
-
+ {scrollbar}
{@props.children}
@@ -76,29 +169,15 @@ class ScrollRegion extends React.Component
adjustment = Utils.scrollAdjustmentToMakeNodeVisibleInContainer(node, container)
@scrollTop += adjustment if adjustment isnt 0
- _scrollbarWrapStyles: =>
- position:'absolute'
- top: 0
- bottom: 0
- right: 0
- zIndex: 2
+ recomputeDimensions: (options = {}) =>
+ scrollbar = @props.getScrollbar?() ? @refs.scrollbar
+ scrollbar._recomputeDimensions(options)
+ @_recomputeDimensions(options)
- _scrollbarHandleStyles: =>
- handleHeight = @_getHandleHeight()
- handleTop = (@state.viewportScrollTop / (@state.totalHeight - @state.viewportHeight)) * (@state.trackHeight - handleHeight)
-
- position:'relative'
- height: handleHeight
- top: handleTop
-
- _getHandleHeight: =>
- Math.min(@state.totalHeight, Math.max(40, (@state.trackHeight / @state.totalHeight) * @state.trackHeight))
-
- _recomputeDimensions: ({avoidForcingLayout} = {}) =>
+ _recomputeDimensions: ({avoidForcingLayout}) =>
return unless @refs.content
contentNode = React.findDOMNode(@refs.content)
- trackNode = React.findDOMNode(@refs.track)
viewportScrollTop = contentNode.scrollTop
# While we're scrolling, calls to contentNode.scrollHeight / clientHeight
@@ -111,58 +190,37 @@ class ScrollRegion extends React.Component
viewportHeight = @state.viewportHeight ? contentNode.clientHeight
else
totalHeight = contentNode.scrollHeight
- trackHeight = trackNode.clientHeight
viewportHeight = contentNode.clientHeight
if @state.totalHeight != totalHeight or
- @state.trackHeight != trackHeight or
@state.viewportHeight != viewportHeight or
@state.viewportScrollTop != viewportScrollTop
- @setState({totalHeight, trackHeight, viewportScrollTop, viewportHeight})
+ @_setSharedState({totalHeight, viewportScrollTop, viewportHeight})
- _onHandleDown: (event) =>
- handleNode = React.findDOMNode(@refs.handle)
- @_trackOffset = React.findDOMNode(@refs.track).getBoundingClientRect().top
- @_mouseOffsetWithinHandle = event.pageY - handleNode.getBoundingClientRect().top
- window.addEventListener("mousemove", @_onHandleMove)
- window.addEventListener("mouseup", @_onHandleUp)
- @setState(dragging: true)
-
- _onHandleMove: (event) =>
- trackY = event.pageY - @_trackOffset - @_mouseOffsetWithinHandle
- trackPxToViewportPx = (@state.totalHeight - @state.viewportHeight) / (@state.trackHeight - @_getHandleHeight())
-
- contentNode = React.findDOMNode(@refs.content)
- contentNode.scrollTop = trackY * trackPxToViewportPx
-
- _onHandleUp: (event) =>
- window.removeEventListener("mousemove", @_onHandleMove)
- window.removeEventListener("mouseup", @_onHandleUp)
- @setState(dragging: false)
-
- _onHandleClick: (event) =>
- # Avoid event propogating up to track
- event.stopPropagation()
-
- _onScrollJump: (event) =>
- @_trackOffset = React.findDOMNode(@refs.track).getBoundingClientRect().top
- @_mouseOffsetWithinHandle = @_getHandleHeight() / 2
- @_onHandleMove(event)
+ _setSharedState: (state) ->
+ scrollbar = @props.getScrollbar?() ? @refs.scrollbar
+ scrollbar.setStateFromScrollRegion(state)
+ @setState(state)
_onScroll: (event) =>
if not @state.scrolling
- @_recomputeDimensions()
- @setState(scrolling: true)
+ @recomputeDimensions()
+ @_setSharedState(scrolling: true)
else
- @_recomputeDimensions({avoidForcingLayout: true})
+ @recomputeDimensions({avoidForcingLayout: true})
@props.onScroll?(event)
@_onScrollEnd ?= _.debounce =>
- @setState(scrolling: false)
+ @_setSharedState(scrolling: false)
@props.onScrollEnd?(event)
, 250
@_onScrollEnd()
+ _getSelf: =>
+ @
+
+
+ScrollRegion.Scrollbar = Scrollbar
module.exports = ScrollRegion
diff --git a/static/components/scroll-region.less b/static/components/scroll-region.less
index 8fe557e6e..9e0ce8f66 100644
--- a/static/components/scroll-region.less
+++ b/static/components/scroll-region.less
@@ -54,54 +54,34 @@
.scroll-region-content-inner {
transform:translate3d(0,0,0);
}
-
- .scrollbar-track {
- opacity: 0;
- transition: opacity 0.3s;
- transition-delay: 0.5s;
- padding:3px;
- width:17px;
- background: @list-bg;
- border-left: 1px solid @border-color-divider;
- &:hover {
- opacity: 1;
- transition-delay: 0s;
- }
-
- /* Used to read the track height with padding applied. */
- .scrollbar-track-inner {
- height:100%;
- }
-
- .scrollbar-handle {
- background-color: lighten(@gray, 40%);
- border:1px solid lighten(@gray, 30%);
- border-radius:8px;
- .tooltip {
- opacity: 0;
- display:none;
- transition: opacity 0.3s;
- top: 50%;
- transform: translate(-100%, -50%);
- position: absolute;
- pointer-events: none;
- }
- }
- }
}
-.scroll-region.scrolling {
+.scroll-region.scrolling {
.scroll-region-content-inner {
pointer-events: none;
}
+}
- .scrollbar-track {
+.scrollbar-track {
+ opacity: 0;
+ transition: opacity 0.3s;
+ transition-delay: 0.5s;
+ padding:3px;
+ width:17px;
+ background: @list-bg;
+ border-left: 1px solid @border-color-divider;
+
+ &:hover {
opacity: 1;
transition-delay: 0s;
}
-}
-.scroll-region.dragging {
- .scrollbar-track {
+
+ &.scrolling {
+ opacity: 1;
+ transition-delay: 0s;
+ }
+
+ &.dragging {
opacity: 1;
.scrollbar-handle {
cursor: default;
@@ -113,4 +93,24 @@
}
}
}
+
+ /* Used to read the track height with padding applied. */
+ .scrollbar-track-inner {
+ height:100%;
+ }
+
+ .scrollbar-handle {
+ background-color: lighten(@gray, 40%);
+ border:1px solid lighten(@gray, 30%);
+ border-radius:8px;
+ .tooltip {
+ opacity: 0;
+ display:none;
+ transition: opacity 0.3s;
+ top: 50%;
+ transform: translate(-100%, -50%);
+ position: absolute;
+ pointer-events: none;
+ }
+ }
}