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} /> + +
+
+
+
{tooltip}
+
+
+
+ + 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} /> -
-
-
-
-
{tooltip}
-
-
-
+ {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; + } + } }