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
|
@ -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()
|
||||
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
BIN
static/images/composer/composer-caret@2x.png
Normal file
After Width: | Height: | Size: 341 B |
BIN
static/images/composer/composer-popout@2x.png
Normal file
After Width: | Height: | Size: 753 B |
BIN
static/images/composer/tooltip-bold-black@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
static/images/composer/tooltip-bold-blue@2x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/images/composer/tooltip-italic-black@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
static/images/composer/tooltip-italic-blue@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
static/images/composer/tooltip-link-black@2x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/composer/tooltip-link-blue@2x.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/composer/tooltip-underline-black@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
static/images/composer/tooltip-underline-blue@2x.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
static/images/tooltip/tooltip-bg-pointer@2x.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
|
@ -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;
|
||||
|
||||
|
|