feat(composer): new composer styles

Summary:
tooltip styling

new styled floating toolbar

move buttons to bottom

Test Plan: edgehill --test

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://review.inboxapp.com/D1305
This commit is contained in:
Evan Morikawa 2015-03-17 16:19:40 -07:00
parent b020795b3b
commit d283cb432b
22 changed files with 315 additions and 163 deletions

View file

@ -123,29 +123,6 @@ ComposerView = React.createClass
style={display: (if @state.isSending then "block" else "none")}>
</div>
<div className="composer-action-bar-wrap">
<div className="composer-action-bar-content">
<button className="btn btn-toolbar pull-right btn-trash"
data-tooltip="Delete draft"
onClick={@_destroyDraft}><RetinaImg name="toolbar-trash.png" /></button>
<button className="btn btn-toolbar pull-right btn-attach"
data-tooltip="Attach file"
onClick={@_attachFile}><RetinaImg name="toolbar-attach.png"/></button>
<button className="btn btn-toolbar pull-right btn-popout"
data-tooltip="Popout composer"
style={display: (@props.mode is "fullwindow") and 'none' or 'initial'}
onClick={@_popoutComposer}><RetinaImg name="toolbar-popout.png"/></button>
<button className="btn btn-toolbar btn-send"
data-tooltip="Send message"
ref="sendButton"
onClick={@_sendDraft}><RetinaImg name="toolbar-send.png" /></button>
{@_footerComponents()}
</div>
</div>
<div className="composer-content-wrap">
<div className="composer-participant-actions">
@ -160,6 +137,12 @@ ComposerView = React.createClass
<span className="header-action"
style={display: @state.showsubject and 'none' or 'initial'}
onClick={=> @setState {showsubject: true}}>Subject</span>
<span className="header-action"
data-tooltip="Popout composer"
style={{display: ((@props.mode is "fullwindow") and 'none' or 'initial'), paddingLeft: "1.5em"}}
onClick={@_popoutComposer}><RetinaImg name="composer-popout.png" style={{position: "relative", top: "-2px"}}/></span>
</div>
<ParticipantsTextField
@ -205,8 +188,9 @@ ComposerView = React.createClass
html={@state.body}
onChange={@_onChangeBody}
style={@_precalcComposerCss}
initialEditQuotedText={@state.showQuotedText}
initialSelectionSnapshot={@_recoveredSelection}
mode={{showQuotedText: @state.showQuotedText}}
onChangeMode={@_onChangeEditableMode}
tabIndex="109" />
</div>
@ -216,6 +200,25 @@ ComposerView = React.createClass
</div>
</div>
<div className="composer-action-bar-wrap">
<div className="composer-action-bar-content">
<button className="btn btn-toolbar pull-right btn-trash"
data-tooltip="Delete draft"
onClick={@_destroyDraft}><RetinaImg name="toolbar-trash.png" /></button>
<button className="btn btn-toolbar pull-right btn-attach"
data-tooltip="Attach file"
onClick={@_attachFile}><RetinaImg name="toolbar-attach.png"/></button>
<button className="btn btn-toolbar btn-send"
data-tooltip="Send message"
ref="sendButton"
onClick={@_sendDraft}><RetinaImg name="toolbar-send.png" /></button>
{@_footerComponents()}
</div>
</div>
</div>
# Focus the composer view. Chooses the appropriate field to start
@ -291,6 +294,9 @@ ComposerView = React.createClass
_onChangeSubject: (event) -> @_addToProxy(subject: event.target.value)
_onChangeBody: (event) -> @_addToProxy(body: event.target.value)
_onChangeEditableMode: ({showQuotedText}) ->
@setState showQuotedText: showQuotedText
_addToProxy: (changes={}, source={}) ->
selections = @_getSelections()

View file

@ -4,6 +4,9 @@ sanitizeHtml = require 'sanitize-html'
{Utils} = require 'inbox-exports'
FloatingToolbar = require './floating-toolbar.cjsx'
linkUUID = 0
genLinkId = -> linkUUID += 1; return linkUUID
module.exports =
ContenteditableComponent = React.createClass
propTypes:
@ -11,14 +14,17 @@ ContenteditableComponent = React.createClass
style: React.PropTypes.object
tabIndex: React.PropTypes.string
onChange: React.PropTypes.func.isRequired
mode: React.PropTypes.object
onChangeMode: React.PropTypes.func
initialSelectionSnapshot: React.PropTypes.object
getInitialState: ->
toolbarTop: 0
toolbarMode: "buttons"
toolbarLeft: 0
toolbarPos: "above"
editAreaWidth: 9999 # This will get set on first selection
toolbarVisible: false
editQuotedText: @props.initialEditQuotedText ? false
componentDidMount: ->
@_setupSelectionListeners()
@ -29,9 +35,9 @@ ContenteditableComponent = React.createClass
@_teardownLinkHoverListeners()
componentWillReceiveProps: (nextProps) ->
@setState editQuotedText: nextProps.initialEditQuotedText
if nextProps.initialSelectionSnapshot?
@_setSelectionSnapshot(nextProps.initialSelectionSnapshot)
@_refreshToolbarState()
componentWillUpdate: ->
@_teardownLinkHoverListeners()
@ -45,6 +51,7 @@ ContenteditableComponent = React.createClass
<FloatingToolbar ref="floatingToolbar"
top={@state.toolbarTop}
left={@state.toolbarLeft}
pos={@state.toolbarPos}
visible={@state.toolbarVisible}
tabIndex={@props.tabIndex}
onSaveUrl={@_onSaveUrl}
@ -94,11 +101,11 @@ ContenteditableComponent = React.createClass
__html: @_applyHTMLDisplayFilters(@props.html)
_applyHTMLDisplayFilters: (html) ->
html = @_removeQuotedTextFromHTML(html) unless @state.editQuotedText
html = @_removeQuotedTextFromHTML(html) unless @props.mode?.showQuotedText
return html
_unapplyHTMLDisplayFilters: (html) ->
html = @_addQuotedTextToHTML(html) unless @state.editQuotedText
html = @_addQuotedTextToHTML(html) unless @props.mode?.showQuotedText
return html
@ -157,7 +164,7 @@ ContenteditableComponent = React.createClass
# state.
#
# We can't use React's `state` variable because cursor position is not
# natrually supported in the virtual DOM.
# naturally supported in the virtual DOM.
#
# We also need to make sure that node references are cloned so they
# don't change out from underneath us.
@ -172,7 +179,6 @@ ContenteditableComponent = React.createClass
_setNewSelectionState: ->
selection = document.getSelection()
return if not @_selectionInScope(selection)
# @_setSelectionMarkers()
return if @_checkSameSelection(selection)
try
@ -182,6 +188,12 @@ ContenteditableComponent = React.createClass
return if not range?
@_previousSelection = @_selection
if selection.isCollapsed
selectionRect = null
else
selectionRect = range.getBoundingClientRect()
@_selection =
startNode: range.startContainer?.cloneNode(true)
startOffset: range.startOffset
@ -189,19 +201,15 @@ ContenteditableComponent = React.createClass
endNode: range.endContainer?.cloneNode(true)
endOffset: range.endOffset
endNodeIndex: @_getNodeIndex(range.endContainer)
isCollapsed: selection.isCollapsed
selectionRect: selectionRect
@_refreshToolbarState()
return @_selection
# _setSelectionMarkers: (selection) ->
# startMarker = document.createElement("SPAN")
# startMarker.setAttribute "id", "nilas-start-marker"
# endMarker = document.createElement("SPAN")
# endMarker.setAttribute "id", "nilas-end-marker"
# selection.anchorNode.parentNode.insertBefore(startMarker, selection.anchorNode)
# selection.focusNode.parentNode.insertBefore(endMarker, selection.focusNode)
_setSelectionSnapshot: (selection) -> @_selection = selection
_setSelectionSnapshot: (selection) ->
@_previousSelection = @_selection
@_selection = selection
# When we're dragging we don't want to the restoring the cursor as we're
# dragging. Doing so caused selecting backwards to break because the
@ -210,10 +218,10 @@ ContenteditableComponent = React.createClass
# state.
_onMouseDown: (event) ->
@_ignoreSelectionRestoration = true
return true
_onMouseUp: ->
return event
_onMouseUp: (event) ->
@_ignoreSelectionRestoration = false
return true
return event
# We manually restore the selection on every render and when we need to
# move the selection around manually.
@ -236,11 +244,21 @@ ContenteditableComponent = React.createClass
return if not startNode? or not endNode?
startIndex = Math.min(@_selection.startOffset ? 0, @_selection.endOffset ? 0)
startIndex = Math.min(startIndex, startNode.length)
# We want to not care about the selection direction.
# Selecting from index 1 to index 5 is the same as selecting from
# index 5 to index 1. However, this only works if the nodes we are
# grabbing the index from are the same. If they are different, then we
# can no longer make this gaurantee and have to grab their listed
# offsets.
if startNode is endNode
startIndex = Math.min(@_selection.startOffset ? 0, @_selection.endOffset ? 0)
startIndex = Math.min(startIndex, startNode.length)
endIndex = Math.max(@_selection.startOffset ? 0, @_selection.endOffset ? 0)
endIndex = Math.min(endIndex, endNode.length)
endIndex = Math.max(@_selection.startOffset ? 0, @_selection.endOffset ? 0)
endIndex = Math.min(endIndex, endNode.length)
else
startIndex = @_selection.startOffset
endIndex = @_selection.endOffset
if collapse is "end"
startNode = endNode
@ -264,35 +282,35 @@ ContenteditableComponent = React.createClass
# We need to break each node apart and cache since the `selection`
# object will mutate underneath us.
_checkSameSelection: (selection) ->
return true if not selection?
return false if not @_previousSelection
return false if not selection.anchorNode? or not selection.focusNode?
_checkSameSelection: (newSelection) ->
return true if not newSelection?
return false if not @_selection
return false if not newSelection.anchorNode? or not newSelection.focusNode?
anchorIndex = @_getNodeIndex(selection.anchorNode)
focusIndex = @_getNodeIndex(selection.focusNode)
anchorIndex = @_getNodeIndex(newSelection.anchorNode)
focusIndex = @_getNodeIndex(newSelection.focusNode)
anchorEqual = selection.anchorNode.isEqualNode @_previousSelection.startNode
anchorIndexEqual = anchorIndex is @_previousSelection.startNodeIndex
focusEqual = selection.focusNode.isEqualNode @_previousSelection.endNode
focusIndexEqual = focusIndex is @_previousSelection.endNodeIndex
anchorEqual = newSelection.anchorNode.isEqualNode @_selection.startNode
anchorIndexEqual = anchorIndex is @_selection.startNodeIndex
focusEqual = newSelection.focusNode.isEqualNode @_selection.endNode
focusIndexEqual = focusIndex is @_selection.endNodeIndex
if not anchorEqual and not focusEqual
# This means the selection is the same, but just from the opposite
# This means the newSelection is the same, but just from the opposite
# direction. We don't care in this case, so check the reciprocal as
# well.
anchorEqual = selection.anchorNode.isEqualNode @_previousSelection.endNode
anchorIndexEqual = anchorIndex is @_previousSelection.endNodeIndex
focusEqual = selection.focusNode.isEqualNode @_previousSelection.startNode
focusIndexEqual = focusIndex is @_previousSelection.startndNodeIndex
anchorEqual = newSelection.anchorNode.isEqualNode @_selection.endNode
anchorIndexEqual = anchorIndex is @_selection.endNodeIndex
focusEqual = newSelection.focusNode.isEqualNode @_selection.startNode
focusIndexEqual = focusIndex is @_selection.startndNodeIndex
anchorOffsetEqual = selection.anchorOffset == @_previousSelection.startOffset
focusOffsetEqual = selection.focusOffset == @_previousSelection.endOffset
anchorOffsetEqual = newSelection.anchorOffset == @_selection.startOffset
focusOffsetEqual = newSelection.focusOffset == @_selection.endOffset
if not anchorOffsetEqual and not focusOffsetEqual
# This means the selection is the same, but just from the opposite
# This means the newSelection is the same, but just from the opposite
# direction. We don't care in this case, so check the reciprocal as
# well.
anchorOffsetEqual = selection.anchorOffset == @_previousSelection.focusOffset
focusOffsetEqual = selection.focusOffset == @_previousSelection.anchorOffset
anchorOffsetEqual = newSelection.anchorOffset == @_selection.focusOffset
focusOffsetEqual = newSelection.focusOffset == @_selection.anchorOffset
if (anchorEqual and
anchorIndexEqual and
@ -338,35 +356,32 @@ ContenteditableComponent = React.createClass
if @_linkHoveringOver
url = @_linkHoveringOver.getAttribute('href')
rect = @_linkHoveringOver.getBoundingClientRect()
[left, top, editAreaWidth] = @_getToolbarPos(rect)
[left, top, editAreaWidth, toolbarPos] = @_getToolbarPos(rect)
@setState
toolbarVisible: true
toolbarMode: "edit-link"
toolbarTop: top
toolbarLeft: left
toolbarPos: toolbarPos
linkToModify: @_linkHoveringOver
editAreaWidth: editAreaWidth
else
selection = document.getSelection()
# TODO do something smarter then this in the future
linksInside = [] # @_linksInside(selection)
if selection.isCollapsed and linksInside.length is 0
if not @_selection? or @_selection.isCollapsed
@_hideToolbar()
else
if selection.isCollapsed and linksInside.length > 0
if @_selection.isCollapsed
linkRect = linksInside[0].getBoundingClientRect()
[left, top, editAreaWidth] = @_getToolbarPos(linkRect)
[left, top, editAreaWidth, toolbarPos] = @_getToolbarPos(linkRect)
else
selectionRect = selection.getRangeAt(0).getBoundingClientRect()
[left, top, editAreaWidth] = @_getToolbarPos(selectionRect)
selectionRect = @_selection.selectionRect
[left, top, editAreaWidth, toolbarPos] = @_getToolbarPos(selectionRect)
@setState
toolbarVisible: true
toolbarMode: "buttons"
toolbarTop: top
toolbarLeft: left
toolbarPos: toolbarPos
linkToModify: null
editAreaWidth: editAreaWidth
@ -384,14 +399,20 @@ ContenteditableComponent = React.createClass
TOP_PADDING = 10
BORDER_RADIUS_PADDING = 15
editArea = @refs.contenteditable.getDOMNode().getBoundingClientRect()
calcLeft = (referenceRect.left - editArea.left) + referenceRect.width/2
calcLeft = Math.min(Math.max(calcLeft, @CONTENT_PADDING), editArea.width)
calcLeft = Math.min(Math.max(calcLeft, @CONTENT_PADDING+BORDER_RADIUS_PADDING), editArea.width - BORDER_RADIUS_PADDING)
calcTop = referenceRect.top - editArea.top + referenceRect.height + TOP_PADDING
calcTop = referenceRect.top - editArea.top - 48
toolbarPos = "above"
if calcTop < TOP_PADDING
calcTop = referenceRect.top - editArea.top + referenceRect.height + TOP_PADDING + 4
toolbarPos = "below"
return [calcLeft, calcTop, editArea.width]
return [calcLeft, calcTop, editArea.width, toolbarPos]
_hideToolbar: ->
if not @_focusedOnToolbar() and @state.toolbarVisible
@ -437,36 +458,40 @@ ContenteditableComponent = React.createClass
_setupLinkHoverListeners: ->
HOVER_IN_DELAY = 250
HOVER_OUT_DELAY = 1000
@_links = []
@_links = {}
links = @_getAllLinks()
return if links.length is 0
links.forEach (link) =>
enterTimeout = null
leaveTimeout = null
link.hoverId = genLinkId()
@_links[link.hoverId] = {}
enterListener = (event) =>
enterTimeout = setTimeout =>
@_clearLinkTimeouts()
@_linkHoveringOver = link
@_links[link.hoverId].enterTimeout = setTimeout =>
return unless @isMounted()
@_linkHoveringOver = link
@_refreshToolbarState()
, HOVER_IN_DELAY
leaveListener = (event) =>
leaveTimeout = setTimeout =>
@_linkHoveringOver = null
@_clearLinkTimeouts()
@_linkHoveringOver = null
@_links[link.hoverId].leaveTimeout = setTimeout =>
return unless @isMounted()
return if @refs.floatingToolbar.isHovering
@_refreshToolbarState()
, HOVER_OUT_DELAY
clearTimeout(enterTimeout)
link.addEventListener "mouseenter", enterListener
link.addEventListener "mouseleave", leaveListener
@_links.push
link: link
enterTimeout: enterTimeout
leaveTimeout: leaveTimeout
enterListener: enterListener
leaveListener: leaveListener
@_links[link.hoverId].link = link
@_links[link.hoverId].enterListener = enterListener
@_links[link.hoverId].leaveListener = leaveListener
_clearLinkTimeouts: ->
for hoverId, linkData of @_links
clearTimeout(linkData.enterTimeout) if linkData.enterTimeout?
clearTimeout(linkData.leaveTimeout) if linkData.leaveTimeout?
_onTooltipMouseEnter: ->
clearTimeout(@_clearTooltipTimeout) if @_clearTooltipTimeout?
@ -477,12 +502,12 @@ ContenteditableComponent = React.createClass
, 500
_teardownLinkHoverListeners: ->
while @_links.length > 0
linkData = @_links.pop()
for hoverId, linkData of @_links
clearTimeout linkData.enterTimeout
clearTimeout linkData.leaveTimeout
linkData.link.removeEventListener "mouseenter", linkData.enterListener
linkData.link.removeEventListener "mouseleave", linkData.leaveListener
@_links = {}
@ -531,13 +556,12 @@ ContenteditableComponent = React.createClass
####### QUOTED TEXT #########
_onToggleQuotedText: ->
@setState
editQuotedText: !@state.editQuotedText
@props.onChangeMode?(showQuotedText: !@props.mode?.showQuotedText)
_quotedTextClasses: -> React.addons.classSet
"quoted-text-control": true
"no-quoted-text": @_htmlQuotedTextStart() is -1
"show-quoted-text": @state.editQuotedText
"show-quoted-text": @props.mode?.showQuotedText
_htmlQuotedTextStart: ->
@props.html.search(/<[^>]*gmail_quote/)

View file

@ -1,6 +1,7 @@
_ = require 'underscore-plus'
React = require 'react'
{CompositeDisposable} = require 'event-kit'
{RetinaImg} = require 'ui-components'
module.exports =
FloatingToolbar = React.createClass
@ -31,7 +32,7 @@ FloatingToolbar = React.createClass
render: ->
<div ref="floatingToolbar"
className="floating-toolbar toolbar" style={@_toolbarStyles()}>
className="floating-toolbar toolbar #{@props.pos}" style={@_toolbarStyles()}>
<div className="toolbar-pointer" style={@_toolbarPointerStyles()}></div>
{@_toolbarType()}
</div>
@ -43,15 +44,18 @@ FloatingToolbar = React.createClass
_renderButtons: ->
<div className="toolbar-buttons">
<button className="btn btn-bold btn-icon"
<button className="btn btn-bold toolbar-btn"
onClick={@_execCommand}
data-command-name="bold"><strong>B</strong></button>
<button className="btn btn-italic btn-icon"
data-command-name="bold"></button>
<button className="btn btn-italic toolbar-btn"
onClick={@_execCommand}
data-command-name="italic"><em>I</em></button>
<button className="btn btn-link btn-icon"
data-command-name="italic"></button>
<button className="btn btn-underline toolbar-btn"
onClick={@_execCommand}
data-command-name="underline"></button>
<button className="btn btn-link toolbar-btn"
onClick={@_showLink}
data-command-name="link"><i className="fa fa-link"></i></button>
data-command-name="link"></button>
</div>
_renderLink: ->
@ -106,6 +110,7 @@ FloatingToolbar = React.createClass
@props.onSaveUrl "", @props.linkToModify
__saveUrl: ->
return unless @isMounted() and @state.urlInputValue?
@props.onSaveUrl @state.urlInputValue, @props.linkToModify
_execCommand: (event) ->
@ -143,7 +148,7 @@ FloatingToolbar = React.createClass
# We can't calculate the width of the floating toolbar declaratively
# because it hasn't been rendered yet. As such, we'll keep the width
# fixed to make it much eaier.
TOOLBAR_BUTTONS_WIDTH = 86#px
TOOLBAR_BUTTONS_WIDTH = 114#px
TOOLBAR_URL_WIDTH = 210#px
if @state.mode is "buttons"

View file

@ -16,9 +16,11 @@ describe "ContenteditableComponent", ->
<ContenteditableComponent html={html} onChange={@onChange}/>
)
html = 'Test <strong>HTML</strong><br><blockquote class="gmail_quote">QUOTE</blockquote>'
@htmlWithQuote = 'Test <strong>HTML</strong><br><blockquote class="gmail_quote">QUOTE</blockquote>'
@componentWithQuote = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={html} onChange={@onChange}/>
<ContenteditableComponent html={@htmlWithQuote}
onChange={@onChange}
mode={showQuotedText: false}/>
)
describe "quoted-text-control", ->
@ -29,13 +31,15 @@ describe "ContenteditableComponent", ->
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
expect(@toggle.props.className.indexOf('no-quoted-text') >= 0).toBe(false)
it "should be have `show-quoted-text` if editQuotedText is true", ->
@componentWithQuote.setState(editQuotedText: true)
it "should be have `show-quoted-text` if showQuotedText is true", ->
@componentWithQuote = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote} onChange={@onChange} mode={showQuotedText: true}/>
)
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
expect(@toggle.props.className.indexOf('show-quoted-text') >= 0).toBe(true)
it "should not have `show-quoted-text` if editQuotedText is false", ->
@componentWithQuote.setState(editQuotedText: false)
it "should not have `show-quoted-text` if showQuotedText is false", ->
@componentWithQuote.setState(showQuotedText: false)
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@componentWithQuote, 'quoted-text-control')
expect(@toggle.props.className.indexOf('show-quoted-text') >= 0).toBe(false)
@ -43,20 +47,27 @@ describe "ContenteditableComponent", ->
@toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
expect(@toggle.props.className.indexOf('no-quoted-text') >= 0).toBe(true)
describe "when editQuotedText is false", ->
describe "when showQuotedText is false", ->
it "should only display HTML up to the beginning of the quoted text", ->
@editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(@componentWithQuote, 'contentEditable')
expect(@editDiv.getDOMNode().innerHTML.indexOf('gmail_quote') >= 0).toBe(false)
describe "when editQuotedText is true", ->
describe "when showQuotedText is true", ->
beforeEach ->
@componentWithQuote = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote}
onChange={@onChange}
mode={showQuotedText: true}/>
)
it "should display all the HTML", ->
@componentWithQuote.setState(editQuotedText: true)
@componentWithQuote.setState(showQuotedText: true)
@editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(@componentWithQuote, 'contentEditable')
expect(@editDiv.getDOMNode().innerHTML.indexOf('gmail_quote') >= 0).toBe(true)
describe "editQuotedText", ->
describe "showQuotedText", ->
it "should default to false", ->
expect(@component.state.editQuotedText).toBe(false)
expect(@component.props.mode?.showQuotedText).toBeUndefined()
describe "when the html is changed", ->
beforeEach ->
@ -68,30 +79,37 @@ describe "ContenteditableComponent", ->
editDiv.getDOMNode().innerHTML = newHTML
ReactTestUtils.Simulate.input(editDiv, {target: {value: newHTML}})
describe "when editQuotedText is true", ->
describe "when showQuotedText is true", ->
beforeEach ->
@componentWithQuote = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={@htmlWithQuote}
onChange={@onChange}
mode={showQuotedText: true}/>
)
it "should call `props.onChange` with the entire HTML string", ->
@componentWithQuote.setState(editQuotedText: true)
@componentWithQuote.setState(showQuotedText: true)
@performEdit(@changedHtmlWithQuote)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(@changedHtmlWithQuote)
it "should allow the quoted text to be changed", ->
changed = 'Test <strong>NEW 1 HTML</strong><br><blockquote class="gmail_quote">QUOTE CHANGED!!!</blockquote>'
@componentWithQuote.setState(editQuotedText: true)
@componentWithQuote.setState(showQuotedText: true)
@performEdit(changed)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(changed)
describe "when editQuotedText is false", ->
describe "when showQuotedText is false", ->
it "should call `props.onChange` with the entire HTML string, even though the div being edited only contains some of it", ->
@componentWithQuote.setState(editQuotedText: false)
@componentWithQuote.setState(showQuotedText: false)
@performEdit(@changedHtmlWithoutQuote)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(@changedHtmlWithQuote)
it "should work if the component does not contain quoted text", ->
changed = 'Hallooo! <strong>NEW 1 HTML HTML HTML</strong><br>'
@component.setState(editQuotedText: true)
@component.setState(showQuotedText: true)
@performEdit(changed, @component)
ev = @onChange.mostRecentCall.args[0]
expect(ev.target.value).toEqual(changed)

View file

@ -184,6 +184,37 @@
margin-left: 0.4em;
}
}
.token {
background: transparent;
&.selected,
&.dragging {
.participant-secondary {
color: @text-color-inverse-very-subtle;
}
}
// &:hover {
// background: transparent;
// }
// &.selected,
// &.dragging {
// background: transparent;
// color: @text-color;
//
// .action { color: @text-color-subtle; }
// }
}
// .btn.btn-send {
// font-weight: @font-weight-semi-bold;
// color: @accent-primary-dark;
// border-top: 1px solid fade(@accent-primary, 39%);
// border-right: 1px solid fade(@accent-primary, 39%);
// border-left: 1px solid fade(@accent-primary, 39%);
// }
}
body.is-blurred .composer-inner-wrap .tokenizing-field .token {
background: transparent;
}
// Overrides for the full-window popout composer
@ -225,8 +256,8 @@
// Overrides for the composer in a message-list
#message-list {
.message-item-wrap.composer-outer-wrap {
border-top: 2px solid @border-color-divider;
&:last-child { padding-bottom: 0; }
padding-top: @spacing-standard;
background: @background-off-primary;
}
}
@ -268,10 +299,6 @@
color: @text-color-very-subtle;
}
}
.token.selected .participant-secondary, .token.dragging .participant-secondary {
color: @text-color-inverse-subtle;
}
}
@ -281,28 +308,38 @@
.floating-toolbar {
z-index: 10;
position: absolute;
background: @background-tertiary;
background: #fff;
box-shadow: 0px 10px 20px rgba(0,0,0,0.19), inset 0px 0px 1px rgba(0,0,0,0.5);
border-radius: @border-radius-base;
color: @text-color-inverse;
box-shadow: 0 0 8px rgba(0,0,0,0.4);
color: @text-color;
.toolbar-pointer {
content: " ";
position: absolute;
width: 0;
height: 0;
top: -13px;
left: 50%;
margin-left: -6px;
border: 7px solid transparent;
border-bottom-color: @background-tertiary;
border-bottom-width: 6px;
width: 22.5px;
height: 10px;
background: transparent url('images/tooltip/tooltip-bg-pointer@2x.png') no-repeat;
background-size: 22.5px 10.5px;
margin-left: -11.2px;
}
&.above {
.toolbar-pointer {
transform: rotate(0deg);
bottom: -9px;
}
}
&.below {
.toolbar-pointer {
transform: rotate(180deg);
top: -9px;
}
}
.floating-toolbar-input {
display: inline;
width: auto;
color: @text-color-inverse;
color: @text-color;
}
@padding: 0.5em;
@ -313,7 +350,7 @@
border-radius: 0;
padding: @padding*0.75 @padding;
margin: 0;
color: @text-color-inverse;
color: @text-color;
box-shadow: none;
&:first-child {
padding-left: 1.5*@padding;
@ -332,4 +369,34 @@
top: 1px;
padding: 0 @padding;
}
.toolbar-btn {
@padding-top: 4px;
@padding-left: 8px;
width: 12.5px + 2*@padding-left;
height: 12.5px + 2*@padding-top;
margin: 7.5px 0;
border-right: 1px solid @border-color-divider;
&:last-child { border-right: 0 }
background: no-repeat;
background-size: 12.5px 12.5px;
background-position: @padding-left @padding-top;
&.btn-bold { background-image: url("images/composer/tooltip-bold-black@2x.png") }
&.btn-italic { background-image: url("images/composer/tooltip-italic-black@2x.png") }
&.btn-underline { background-image: url("images/composer/tooltip-underline-black@2x.png") }
&.btn-link { background-image: url("images/composer/tooltip-link-black@2x.png") }
&:hover {
cursor: pointer;
background: no-repeat;
background-size: 12.5px 12.5px;
background-position: @padding-left @padding-top;
&.btn-bold { background-image: url("images/composer/tooltip-bold-blue@2x.png") }
&.btn-italic { background-image: url("images/composer/tooltip-italic-blue@2x.png") }
&.btn-underline { background-image: url("images/composer/tooltip-underline-blue@2x.png") }
&.btn-link { background-image: url("images/composer/tooltip-link-blue@2x.png") }
}
}
}

View file

@ -14,6 +14,7 @@ Tooltip = React.createClass
getInitialState: ->
top: 0
pos: "below"
left: 0
width: 0
pointerLeft: 0
@ -33,7 +34,7 @@ Tooltip = React.createClass
clearTimeout @_showDelayTimeout
render: ->
<div className="tooltip-wrap" style={@_positionStyles()}>
<div className="tooltip-wrap #{@state.pos}" style={@_positionStyles()}>
<div className="tooltip-content">{@state.content}</div>
<div className="tooltip-pointer" style={left: @state.pointerLeft}></div>
</div>
@ -86,10 +87,19 @@ Tooltip = React.createClass
content = target.dataset.tooltip
guessedWidth = @_guessWidth(content)
dim = target.getBoundingClientRect()
top = dim.top + dim.height + 11
left = dim.left + dim.width / 2
TOOLTIP_HEIGHT = 50
FLIP_THRESHOLD = TOOLTIP_HEIGHT + 30
top = dim.top + dim.height + 14
tooltipPos = "below"
if top + FLIP_THRESHOLD > @_windowHeight()
tooltipPos = "above"
top = dim.top - TOOLTIP_HEIGHT
@setState
top: top
pos: tooltipPos
left: @_tooltipLeft(left, guessedWidth)
width: guessedWidth
pointerLeft: @_tooltipPointerLeft(left, guessedWidth)
@ -119,6 +129,9 @@ Tooltip = React.createClass
_windowWidth: ->
document.getElementsByTagName('body')[0].getBoundingClientRect().width
_windowHeight: ->
document.getElementsByTagName('body')[0].getBoundingClientRect().height
_hideTooltip: ->
return unless @isMounted()
@setState

View file

@ -4,16 +4,21 @@
top: 0;
left: 0;
}
@tooltipHeight: 156px / 2;
@wingWidth: 42px / 2;
.tooltip-wrap {
position: absolute;
display: none;
// Position and display are set by React
background: @black;
background: #fff;
box-shadow: 0px 10px 20px rgba(0,0,0,0.19), inset 0px 0px 1px rgba(0,0,0,0.5);
border-radius: @border-radius-base;
color: @text-color-inverse;
color: @text-color;
font-weight: @font-weight-medium;
box-shadow: 0 0 8px rgba(0,0,0,0.4);
text-align: center;
text-transform: capitalize;
@ -23,13 +28,23 @@
.tooltip-pointer {
position: absolute;
width: 0;
height: 0;
top: -13px;
left: 50%;
margin-left: -6px;
border: 7px solid transparent;
border-bottom-color: @black;
border-bottom-width: 6px;
width: 22.5px;
height: 10px;
background: transparent url('images/tooltip/tooltip-bg-pointer@2x.png') no-repeat;
background-size: 22.5px 10.5px;
margin-left: -11.2px;
}
&.above {
.tooltip-pointer {
transform: rotate(0deg);
bottom: -9px;
}
}
&.below {
.tooltip-pointer {
transform: rotate(180deg);
top: -9px;
}
}
}

View file

@ -2,6 +2,7 @@ React = require 'react/addons'
_ = require 'underscore-plus'
{CompositeDisposable} = require 'event-kit'
{Contact, ContactStore} = require 'inbox-exports'
RetinaImg = require './retina-img'
{DragDropMixin} = require 'react-dnd'
@ -29,7 +30,7 @@ Token = React.createClass
<div {...@dragSourceFor('token')}
className={classes}
onClick={@_onSelect}>
<button className="action" onClick={@_onAction} ><i className="fa fa-chevron-down"></i></button>
<button className="action" onClick={@_onAction} style={marginTop: "2px"}><RetinaImg name="composer-caret.png" /></button>
{@props.children}
</div>

View file

@ -37,7 +37,7 @@ AnalyticsStore = Reflux.createStore
composeReply: ({threadId, messageId}) -> {threadId, messageId}
composeForward: ({threadId, messageId}) -> {threadId, messageId}
composeReplyAll: ({threadId, messageId}) -> {threadId, messageId}
composePopoutDraft: (localId) -> {draftLocalId: draftLocalId}
composePopoutDraft: (draftLocalId) -> {draftLocalId: draftLocalId}
composeNewBlankDraft: -> {}
sendDraft: (draftLocalId) -> {draftLocalId: draftLocalId}
destroyDraft: (draftLocalId) -> {draftLocalId: draftLocalId}

View file

@ -39,15 +39,17 @@
}
&:hover {
background-color: darken(@background-secondary, 5%);
cursor: pointer;
cursor: -webkit-grab;
}
&.selected,
&.dragging {
background-color: @background-tertiary;
color: @text-color-inverse;
.action { color: @text-color-inverse-subtle; }
}
&.dragging {
cursor: -webkit-grabbing;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -36,7 +36,8 @@
@white: #ffffff;
@blue: #2794c3;
@blue-dark: darken(@blue, 10%);
@blue-dark: #1880ab;
// @blue-dark: darken(@blue, 10%);
//== Color descriptors
@accent-primary: @blue;
@ -44,7 +45,7 @@
@accent-secondary: @nilas-yellow;
@background-primary: #ffffff;
@background-off-primary: #fdfdfd;
@background-off-primary: #fbfbfb;
@background-secondary: #f6f6f6;
@background-tertiary: #6d7987;