fix(message-list-scrolling): Move scroll logic to ScrollRegion

Summary: This is still a work in progress and needs more specs

Test Plan: Run tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D1808
This commit is contained in:
Ben Gotow 2015-07-30 18:29:38 -07:00
parent cab654ec48
commit 43f9b0b8c3
8 changed files with 222 additions and 190 deletions

View file

@ -157,7 +157,7 @@ class ComposerView extends React.Component
</div>
_wrapClasses: =>
"composer-outer-wrap #{@props.className ? ""}"
"message-item-white-wrap composer-outer-wrap #{@props.className ? ""}"
_renderComposer: =>
<DropZone className="composer-inner-wrap"
@ -287,6 +287,11 @@ class ComposerView extends React.Component
</ScrollRegion>
_renderBodyContenteditable: =>
onScrollToBottom = null
if @props.onRequestScrollTo
onScrollToBottom = =>
@props.onRequestScrollTo({messageId: @_proxy.draft().id})
<ContenteditableComponent ref="contentBody"
html={@state.body}
onChange={@_onChangeBody}
@ -295,7 +300,8 @@ class ComposerView extends React.Component
initialSelectionSnapshot={@_recoveredSelection}
mode={{showQuotedText: @state.showQuotedText}}
onChangeMode={@_onChangeEditableMode}
onRequestScrollTo={@props.onRequestScrollTo}
onScrollTo={@props.onRequestScrollTo}
onScrollToBottom={onScrollToBottom}
tabIndex="109" />
_renderFooterRegions: =>
return <div></div> unless @props.localId
@ -521,8 +527,6 @@ class ComposerView extends React.Component
_onChangeBody: (event) =>
return unless @_proxy
if @_getSelections().currentSelection?.atEndOfContent
@props.onRequestScrollTo?(messageId: @_proxy.draft().id, location: "bottom")
# The body changes extremely frequently (on every key stroke). To keep
# performance up, we don't want to trigger every single key stroke

View file

@ -21,7 +21,8 @@ class ContenteditableComponent extends React.Component
initialSelectionSnapshot: React.PropTypes.object
# Passes an absolute top coordinate to scroll to.
onRequestScrollTo: React.PropTypes.func
onScrollTo: React.PropTypes.func
onScrollToBottom: React.PropTypes.func
constructor: (@props) ->
@state =
@ -304,11 +305,10 @@ class ContenteditableComponent extends React.Component
# http://www.w3.org/TR/selection-api/#selectstart-event
_setupSelectionListeners: =>
@_onSelectionChange = => @_saveSelectionState()
document.addEventListener "selectionchange", @_onSelectionChange
document.addEventListener("selectionchange", @_saveSelectionState)
_teardownSelectionListeners: =>
document.removeEventListener("selectionchange", @_onSelectionChange)
document.removeEventListener("selectionchange", @_saveSelectionState)
getCurrentSelection: => _.clone(@_selection ? {})
getPreviousSelection: => _.clone(@_previousSelection ? {})
@ -378,8 +378,8 @@ class ContenteditableComponent extends React.Component
endOffset: selection.focusOffset
endNodeIndex: @_getNodeIndex(selection.focusNode)
isCollapsed: selection.isCollapsed
atEndOfContent: @_atEndOfContent(selection)
@_ensureSelectionVisible(selection)
@_refreshToolbarState()
return @_selection
@ -464,9 +464,6 @@ class ContenteditableComponent extends React.Component
@_previousSelection = @_selection
@_selection = selection
# We need to use a boolean flag here because this runs before anything
# might be rendered yet.
@_selectionManuallyChanged = true
# When the selectionState gets set by a parent (e.g. undo-ing and
# redo-ing) we need to make sure it's visible to the user.
@ -478,28 +475,34 @@ class ContenteditableComponent extends React.Component
# container is a direct parent of the requested element. In this case
# the scroll container may be many levels up.
_ensureSelectionVisible: (selection) ->
rect = @_getRangeInScope().getBoundingClientRect()
return if @_isEmptyBoudingRect(rect)
top = @_getSelectionTop(selection, rect)
@props.onRequestScrollTo?(selectionTop: top)
# If our parent supports scroll to bottom, check for that
if @props.onScrollToBottom and @_atEndOfContent(selection)
@props.onScrollToBottom()
# Don't bother computing client rects if no scroll method has been provided
else if @props.onScrollTo
rect = @_getRangeInScope().getBoundingClientRect()
if @_isEmptyBoudingRect(rect)
rect = @_getSelectionRectFromDOM(selection)
if rect
@props.onScrollTo({rect})
@_refreshToolbarState()
_isEmptyBoudingRect: (rect) ->
rect.top is 0 and rect.bottom is 0 and rect.left is 0 and rect.right is 0
_getSelectionTop: (selection, rect) ->
if rect then return rect.top + (Math.abs(rect.top - rect.bottom) / 2)
_getSelectionRectFromDOM: (selection) ->
node = selection.anchorNode
if node.nodeType is Node.TEXT_NODE
r = document.createRange()
r.selectNodeContents(node)
rect = r.getBoundingClientRect()
return r.getBoundingClientRect()
else if node.nodeType is Node.ELEMENT_NODE
rect = node.getBoundingClientRect()
return node.getBoundingClientRect()
else
rect = {top: 0, bottom: 0}
return rect.top + Math.abs(rect.top - rect.bottom) / 2
return null
# We use global listeners to determine whether or not dragging is
# happening. This is because dragging may stop outside the scope of
@ -666,9 +669,8 @@ class ContenteditableComponent extends React.Component
@_selection.startOffset,
newEndNode,
@_selection.endOffset)
if @_selectionManuallyChanged
@_ensureSelectionVisible(selection)
@_selectionManuallyChanged = false
@_ensureSelectionVisible(selection)
@_setupSelectionListeners()
# We need to break each node apart and cache since the `selection`
@ -949,7 +951,6 @@ class ContenteditableComponent extends React.Component
if inputText.length > 0
cleanHtml = @_sanitizeInput(inputText, type)
document.execCommand("insertHTML", false, cleanHtml)
@_selectionManuallyChanged = true
return

View file

@ -58,30 +58,34 @@ class MessageItem extends React.Component
attachmentIcon = <div className="collapsed-attachment"></div>
<div className={@props.className} onClick={@_toggleCollapsed}>
<div className="message-item-area">
<div className="collapsed-from">
{@props.message.from?[0]?.displayFirstName()}
<div className="message-item-white-wrap">
<div className="message-item-area">
<div className="collapsed-from">
{@props.message.from?[0]?.displayFirstName()}
</div>
<div className="collapsed-snippet">
{@props.message.snippet}
</div>
<div className="collapsed-timestamp">
<MessageTimestamp date={@props.message.date} />
</div>
{attachmentIcon}
</div>
<div className="collapsed-snippet">
{@props.message.snippet}
</div>
<div className="collapsed-timestamp">
<MessageTimestamp date={@props.message.date} />
</div>
{attachmentIcon}
</div>
</div>
_renderFull: =>
<div className={@props.className}>
<div className="message-item-area">
{@_renderHeader()}
<EmailFrame showQuotedText={@state.showQuotedText}>
{@_formatBody()}
</EmailFrame>
<a className={@_quotedTextClasses()} onClick={@_toggleQuotedText}></a>
{@_renderEvents()}
{@_renderAttachments()}
<div className="message-item-white-wrap">
<div className="message-item-area">
{@_renderHeader()}
<EmailFrame showQuotedText={@state.showQuotedText}>
{@_formatBody()}
</EmailFrame>
<a className={@_quotedTextClasses()} onClick={@_toggleQuotedText}></a>
{@_renderEvents()}
{@_renderAttachments()}
</div>
</div>
</div>

View file

@ -95,9 +95,6 @@ class MessageList extends React.Component
@MINIFY_THRESHOLD = 3
componentDidMount: =>
@_mounted = true
window.addEventListener("resize", @_onResize)
@_unsubscribers = []
@_unsubscribers.push MessageStore.listen @_onChange
@ -109,19 +106,10 @@ class MessageList extends React.Component
@command_unsubscriber = atom.commands.add('body', commands)
# We don't need to listen to ThreadStore bcause MessageStore already
# listens to thead selection changes
if not @state.loading
@_prepareContentForDisplay()
componentWillUnmount: =>
@_mounted = false
unsubscribe() for unsubscribe in @_unsubscribers
@command_unsubscriber.dispose()
window.removeEventListener("resize", @_onResize)
shouldComponentUpdate: (nextProps, nextState) =>
not Utils.isEqualReact(nextProps, @props) or
not Utils.isEqualReact(nextState, @state)
@ -129,32 +117,24 @@ class MessageList extends React.Component
componentDidUpdate: (prevProps, prevState) =>
return if @state.loading
if prevState.loading
@_prepareContentForDisplay()
else
newDraftIds = @_newDraftIds(prevState)
newMessageIds = @_newMessageIds(prevState)
if newMessageIds.length > 0
@_prepareContentForDisplay()
else if newDraftIds.length > 0
@_focusDraft(@_getDraftElement(newDraftIds[0]))
@_prepareContentForDisplay()
newDraftIds = @_newDraftIds(prevState)
if newDraftIds.length > 0
@_focusDraft(@_getMessageElement(newDraftIds[0]))
_newDraftIds: (prevState) =>
oldDraftIds = _.map(_.filter((prevState.messages ? []), (m) -> m.draft), (m) -> m.id)
newDraftIds = _.map(_.filter((@state.messages ? []), (m) -> m.draft), (m) -> m.id)
return _.difference(newDraftIds, oldDraftIds) ? []
_newMessageIds: (prevState) =>
oldMessageIds = _.map(_.reject((prevState.messages ? []), (m) -> m.draft), (m) -> m.id)
newMessageIds = _.map(_.reject((@state.messages ? []), (m) -> m.draft), (m) -> m.id)
return _.difference(newMessageIds, oldMessageIds) ? []
_getDraftElement: (draftId) =>
@refs["composerItem-#{draftId}"]
_getMessageElement: (id) =>
@refs["message-#{id}"]
_focusDraft: (draftElement) =>
draftElement.focus()
@refs.messageWrap.scrollTo(draftElement, {
position: ScrollRegion.ScrollPosition.Bottom,
settle: true
})
_createReplyOrUpdateExistingDraft: (type) =>
unless type in ['reply', 'reply-all']
@ -204,7 +184,7 @@ class MessageList extends React.Component
updated[key].push(contact) unless _.findWhere(updated[key], {email: contact.email})
session.changes.add(updated)
@_focusDraft(@_getDraftElement(last.id))
@_focusDraft(@_getMessageElement(last.id))
else
if type is 'reply'
@ -229,13 +209,12 @@ class MessageList extends React.Component
wrapClass = classNames
"messages-wrap": true
"ready": @state.ready
"ready": not @state.loading
<div className="message-list" id="message-list">
<ScrollRegion tabIndex="-1"
className={wrapClass}
scrollTooltipComponent={MessageListScrollTooltip}
onScroll={_.debounce(@_cacheScrollPos, 100)}
ref="messageWrap">
{@_renderSubject()}
<div className="headers" style={position:'relative'}>
@ -250,7 +229,7 @@ class MessageList extends React.Component
</div>
{@_messageComponents()}
</ScrollRegion>
<Spinner visible={!@state.ready} />
<Spinner visible={@state.loading} />
</div>
_renderSubject: ->
@ -273,7 +252,8 @@ class MessageList extends React.Component
<span className="reply-text">Write a reply…</span>
</div>
</div>
else return <div key={Utils.generateTempId()}></div>
else
<div key={Utils.generateTempId()}></div>
_hasReplyArea: =>
not _.last(@state.messages)?.draft
@ -294,38 +274,7 @@ class MessageList extends React.Component
return unless @state.currentThread
@_createReplyOrUpdateExistingDraft(@_replyType())
# There may be a lot of iframes to load which may take an indeterminate
# amount of time. As long as there is more content being painted onto
# the page and our height is changing, keep waiting. Then scroll to message.
scrollToMessage: (msgDOMNode, done, location="top", stability=5) =>
return done() unless msgDOMNode?
messageWrap = React.findDOMNode(@refs.messageWrap)
lastHeight = -1
stableCount = 0
scrollIfSettled = =>
return unless @_mounted
messageWrapHeight = messageWrap.getBoundingClientRect().height
if messageWrapHeight isnt lastHeight
lastHeight = messageWrapHeight
stableCount = 0
else
stableCount += 1
if stableCount is stability
if location is "top"
messageWrap.scrollTop = msgDOMNode.offsetTop
else if location is "bottom"
offsetTop = msgDOMNode.offsetTop
messageHeight = msgDOMNode.getBoundingClientRect().height
messageWrap.scrollTop = offsetTop - (messageWrapHeight - messageHeight)
return done()
window.requestAnimationFrame -> scrollIfSettled(msgDOMNode, done)
scrollIfSettled()
_messageComponents: =>
appliedInitialScroll = false
components = []
messages = @_messagesWithMinification(@state.messages)
@ -337,29 +286,23 @@ class MessageList extends React.Component
collapsed = !@state.messagesExpandedState[message.id]
initialScroll = not appliedInitialScroll and not collapsed and
((message.draft) or
(message.unread) or
(idx is @state.messages.length - 1 and idx > 0))
appliedInitialScroll ||= initialScroll
className = classNames
"message-item-wrap": true
"before-reply-area": (messages.length - 1 is idx) and @_hasReplyArea()
"initial-scroll": initialScroll
"unread": message.unread
"draft": message.draft
"collapsed": collapsed
if message.draft
components.push <InjectedComponent matching={role:"Composer"}
exposedProps={ mode:"inline", localId:@state.messageLocalIds[message.id], onRequestScrollTo:@_onRequestScrollToComposer, threadId:@state.currentThread.id }
ref={"composerItem-#{message.id}"}
exposedProps={ mode:"inline", localId:@state.messageLocalIds[message.id], onRequestScrollTo:@_onChildScrollRequest, threadId:@state.currentThread.id }
ref={"message-#{message.id}"}
key={@state.messageLocalIds[message.id]}
className={className} />
else
components.push <MessageItem key={message.id}
thread={@state.currentThread}
ref={"message-#{message.id}"}
message={message}
className={className}
collapsed={collapsed}
@ -370,7 +313,6 @@ class MessageList extends React.Component
return components
_renderMinifiedBundle: (bundle) ->
BUNDLE_HEIGHT = 36
lines = bundle.messages[0...10]
h = Math.round(BUNDLE_HEIGHT / lines.length)
@ -433,22 +375,17 @@ class MessageList extends React.Component
#
# If messageId and location are defined, that means we want to scroll
# smoothly to the top of a particular message.
_onRequestScrollToComposer: ({messageId, location, selectionTop}={}) =>
composer = React.findDOMNode(@_getDraftElement(messageId))
if selectionTop
messageWrap = React.findDOMNode(@refs.messageWrap)
wrapRect = messageWrap.getBoundingClientRect()
if selectionTop < wrapRect.top or selectionTop > wrapRect.bottom
wrapMid = wrapRect.top + Math.abs(wrapRect.top - wrapRect.bottom) / 2
diff = selectionTop - wrapMid
messageWrap.scrollTop += diff
_onChildScrollRequest: ({messageId, rect}={}) =>
if messageId
@refs.messageWrap.scrollTo(@_getMessageElement(messageId), {
position: ScrollRegion.ScrollPosition.Visible
})
else if rect
@refs.messageWrap.scrollToRect(rect, {
position: ScrollRegion.ScrollPosition.CenterIfInvisible
})
else
done = ->
location ?= "bottom"
@scrollToMessage(composer, done, location, 1)
_makeRectVisible: (rect) ->
messageWrap = React.findDOMNode(@refs.messageWrap)
throw new Error("onChildScrollRequest: expected messageId or rect")
_onChange: =>
newState = @_getStateFromStores()
@ -462,34 +399,5 @@ class MessageList extends React.Component
messagesExpandedState: MessageStore.itemsExpandedState()
currentThread: MessageStore.thread()
loading: MessageStore.itemsLoading()
ready: if MessageStore.itemsLoading() then false else @state?.ready ? false
_prepareContentForDisplay: =>
node = React.findDOMNode(@)
return unless node
initialScrollNode = node.querySelector(".initial-scroll")
@scrollToMessage initialScrollNode, =>
@setState(ready: true)
@_cacheScrollPos()
_onResize: (event) =>
@_scrollToBottom() if @_wasAtBottom()
@_cacheScrollPos()
_scrollToBottom: =>
messageWrap = React.findDOMNode(@refs.messageWrap)
return unless messageWrap
messageWrap.scrollTop = messageWrap.scrollHeight
_cacheScrollPos: =>
messageWrap = React.findDOMNode(@refs.messageWrap)
return unless messageWrap
@_lastScrollTop = messageWrap.scrollTop
@_lastHeight = messageWrap.getBoundingClientRect().height
@_lastScrollHeight = messageWrap.scrollHeight
_wasAtBottom: =>
(@_lastScrollTop + @_lastHeight) >= @_lastScrollHeight
module.exports = MessageList

View file

@ -120,30 +120,33 @@
}
.message-item-wrap {
transition: height 0.1s;
position: relative;
max-width: @message-max-width;
width: calc(~"100% - 12px");
margin: @message-spacing auto;
padding: 0;
margin: 0 auto;
padding: @message-spacing 0;
&:last-child {
padding-bottom: @message-spacing * 2;
}
.message-item-white-wrap {
background: @background-primary;
border: 0;
box-shadow: 0 0 0.5px rgba(0, 0, 0, 0.28), 0 1px 1.5px rgba(0, 0, 0, 0.08);
border-radius: 4px;
}
background: @background-primary;
border: 0;
box-shadow: 0 0 0.5px rgba(0, 0, 0, 0.28), 0 1px 1.5px rgba(0, 0, 0, 0.08);
border-radius: 4px;
&:first-child { padding-top: 0; }
&.before-reply-area { margin-bottom: 0; }
&.before-reply-area { padding-bottom: 0; }
&.collapsed {
background-color: darken(@background-primary, 2%);
padding-top: 19px;
padding-bottom: 8px;
margin-bottom: 0;
.message-item-white-wrap {
background-color: darken(@background-primary, 2%);
padding-top: 19px;
padding-bottom: 8px;
margin-bottom: 0;
}
&+.minified-bundle {
margin-top: -@message-spacing

View file

@ -1,6 +1,6 @@
_ = require 'underscore'
React = require 'react/addons'
{DOMUtils} = require 'nylas-exports'
{Utils} = require 'nylas-exports'
classNames = require 'classnames'
class Scrollbar extends React.Component
@ -117,7 +117,25 @@ class ScrollRegion extends React.Component
children: React.PropTypes.oneOfType([React.PropTypes.element, React.PropTypes.array])
getScrollbar: React.PropTypes.func
# Concept from https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UITableView_Class/#//apple_ref/c/tdef/UITableViewScrollPosition
@ScrollPosition:
# Scroll so that the desired region is at the top of the viewport
Top: 'Top'
# Scroll so that the desired region is at the bottom of the viewport
Bottom: 'Bottom'
# Scroll so that the desired region is visible in the viewport, with the
# least movement possible.
Visible: 'Visible'
# Scroll so that the desired region is centered in the viewport
Center: 'Center'
# Scroll so that the desired region is centered in the viewport, only if it
# is currently not visible
CenterIfInvisible: 'CenterIfInvisible'
constructor: (@props) ->
@_scrollToTaskId = 0
@_scrollbarComponent = null
@state =
totalHeight:0
viewportHeight: 0
@ -130,23 +148,35 @@ class ScrollRegion extends React.Component
})
componentDidMount: =>
@_mounted = true
@recomputeDimensions()
componentWillReceiveProps: (props) =>
if @shouldInvalidateScrollbarComponent(props)
@_scrollbarComponent = null
componentWillUnmount: =>
@_mounted = false
shouldComponentUpdate: (newProps, newState) =>
# Because this component renders @props.children, it needs to update
# on props.children changes. Unfortunately, computing isEqual on the
# @props.children tree extremely expensive. Just let React's algorithm do it's work.
true
shouldInvalidateScrollbarComponent: (newProps) =>
return true if newProps.scrollTooltipComponent isnt @props.scrollTooltipComponent
return true if newProps.getScrollbar isnt @props.getScrollbar
return false
render: =>
containerClasses = "#{@props.className ? ''} " + classNames
'scroll-region': true
'dragging': @state.dragging
'scrolling': @state.scrolling
scrollbar = []
if not @props.getScrollbar
scrollbar = <Scrollbar
@_scrollbarComponent ?= <Scrollbar
ref="scrollbar"
scrollTooltipComponent={@props.scrollTooltipComponent}
getScrollRegion={@_getSelf} />
@ -154,7 +184,7 @@ class ScrollRegion extends React.Component
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
<div className={containerClasses} {...otherProps}>
{scrollbar}
{@_scrollbarComponent}
<div className="scroll-region-content" onScroll={@_onScroll} ref="content">
<div className="scroll-region-content-inner">
{@props.children}
@ -164,10 +194,88 @@ class ScrollRegion extends React.Component
# Public: Scroll to the DOM Node provided.
#
scrollTo: (node) =>
container = React.findDOMNode(@)
adjustment = DOMUtils.scrollAdjustmentToMakeNodeVisibleInContainer(node, container)
@scrollTop += adjustment if adjustment isnt 0
scrollTo: (node, {position, settle} = {}) =>
if node instanceof React.Component
node = React.findDOMNode(node)
unless node instanceof Node
throw new Error("ScrollRegion.scrollTo: requires a DOM node or React element. Maybe you meant scrollToRect?")
@_scroll {position, settle}, =>
node.getBoundingClientRect()
# Public: Scroll to the client rectangle provided. Note: This method expects
# a ClientRect or similar object with top, left, width, height relative to the
# window, not the scroll region. This is designed to make it easy to use with
# node.getBoundingClientRect()
scrollToRect: (rect, {position, settle} = {}) ->
if rect instanceof Node
throw new Error("ScrollRegion.scrollToRect: requires a rect. Maybe you meant scrollTo?")
if not rect.top or not rect.height
throw new Error("ScrollRegion.scrollToRect: requires a rect with `top` and `height` attributes.")
@_scroll {position, settle}, => rect
_scroll: ({position, settle}, clientRectProviderCallback) ->
contentNode = React.findDOMNode(@refs.content)
position ?= ScrollRegion.ScrollPosition.Visible
if settle is true
settleFn = @_settleHeight
else
settleFn = (callback) -> callback()
@_scrollToTaskId += 1
taskId = @_scrollToTaskId
settleFn =>
# If another scroll call has been made since ours, don't do anything.
return unless @_scrollToTaskId is taskId
contentClientRect = contentNode.getBoundingClientRect()
rect = _.clone(clientRectProviderCallback())
# For sanity's sake, convert the client rectangle we get into a rect
# relative to the contentRect of our scroll region.
rect.top = rect.top - contentClientRect.top + contentNode.scrollTop
rect.bottom = rect.bottom - contentClientRect.top + contentNode.scrollTop
# Also give ourselves a representation of the visible region, in the same
# coordinate space as `rect`
contentVisibleRect = _.clone(contentClientRect)
contentVisibleRect.top += contentNode.scrollTop
contentVisibleRect.bottom += contentNode.scrollTop
if position is ScrollRegion.ScrollPosition.Top
@scrollTop = rect.top
else if position is ScrollRegion.ScrollPosition.Bottom
@scrollTop = (rect.top + rect.height) - contentClientRect.height
else if position is ScrollRegion.ScrollPosition.Center
@scrollTop = rect.top - (contentClientRect.height - rect.height) / 2
else if position is ScrollRegion.ScrollPosition.CenterIfInvisible
if not Utils.rectVisibleInRect(rect, contentVisibleRect)
@scrollTop = rect.top - (contentClientRect.height - rect.height) / 2
else if position is ScrollRegion.ScrollPosition.Visible
distanceBelowBottom = (rect.top + rect.height) - (contentClientRect.height + contentNode.scrollTop)
distanceAboveTop = @scrollTop - rect.top
if distanceBelowBottom >= 0
@scrollTop += distanceBelowBottom
else if distanceAboveTop >= 0
@scrollTop -= distanceAboveTop
_settleHeight: (callback) =>
contentNode = React.findDOMNode(@refs.content)
lastContentHeight = -1
stableCount = 0
scrollIfSettled = =>
return unless @_mounted
contentRect = contentNode.getBoundingClientRect()
if contentRect.height isnt lastContentHeight
lastContentHeight = contentRect.height
stableCount = 0
else
stableCount += 1
if stableCount is 5
return callback()
window.requestAnimationFrame(scrollIfSettled)
scrollIfSettled()
recomputeDimensions: (options = {}) =>
scrollbar = @props.getScrollbar?() ? @refs.scrollbar

View file

@ -100,16 +100,17 @@ DOMUtils =
scrollAdjustmentToMakeNodeVisibleInContainer: (node, container) ->
return unless node
nodeRect = node.getBoundingClientRect()
containerRect = container.getBoundingClientRect()
return @scrollAdjustmentToMakeRectVisibleInRect(nodeRect, containerRect)
scrollAdjustmentToMakeRectVisibleInRect: (nodeRect, containerRect) ->
distanceBelowBottom = (nodeRect.top + nodeRect.height) - (containerRect.top + containerRect.height)
if distanceBelowBottom > 0
if distanceBelowBottom >= 0
return distanceBelowBottom
distanceAboveTop = containerRect.top - nodeRect.top
if distanceAboveTop > 0
if distanceAboveTop >= 0
return -distanceAboveTop
return 0

View file

@ -249,6 +249,9 @@ Utils =
domain = _.last(email.toLowerCase().trim().split("@"))
return (Utils.commonDomains[domain] ? false)
rectVisibleInRect: (r1, r2) ->
return !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top)
isEqualReact: (a, b, options={}) ->
options.functionsAreEqual = true
options.ignoreKeys = (options.ignoreKeys ? []).push("localId")