mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-11-12 04:25:31 +08:00
3eba41b8d8
Summary: Fixes T3510 Fixes T3509 Fixes T3508 Fixes T3549 Extracted clipboard service Remove unused style prop Begin extracting quoted text from composer. Spec for clipboard service Fix contenteditable specs Begin to extract floating toolbar Extract out DOMUtils and further extract floating toolbar Further extracting domutils and floating toolbar composer floating toolbar extracted Fixes to hover and link states Collapse adjacent ul lists Fix outdent when deleting on a bulleted list Fix bullet controls Fixes to list creation and deletion Add underline keyboard shortcut Test Plan: manual :( Reviewers: dillon, bengotow Reviewed By: bengotow Maniphest Tasks: T3508, T3509, T3510, T3549 Differential Revision: https://phab.nylas.com/D2036
245 lines
7.7 KiB
CoffeeScript
245 lines
7.7 KiB
CoffeeScript
_ = require 'underscore'
|
|
React = require 'react/addons'
|
|
classNames = require 'classnames'
|
|
{CompositeDisposable} = require 'event-kit'
|
|
{RetinaImg} = require 'nylas-component-kit'
|
|
{DraftStore} = require 'nylas-exports'
|
|
|
|
class FloatingToolbar extends React.Component
|
|
@displayName = "FloatingToolbar"
|
|
|
|
@propTypes:
|
|
top: React.PropTypes.number
|
|
left: React.PropTypes.number
|
|
mode: React.PropTypes.string
|
|
onMouseEnter: React.PropTypes.func
|
|
onMouseLeave: React.PropTypes.func
|
|
|
|
# When an extension wants to mutate the DOM, it passes `onDomMutator`
|
|
# a mutator function. That mutator is expecting to be passed the
|
|
# latest DOM object and may modify it in place.
|
|
onDomMutator: React.PropTypes.func
|
|
|
|
@defaultProps:
|
|
mode: "buttons"
|
|
onMouseEnter: ->
|
|
onMouseLeave: ->
|
|
|
|
constructor: (@props) ->
|
|
@state =
|
|
urlInputValue: @_initialUrl() ? ""
|
|
componentWidth: 0
|
|
|
|
componentDidMount: =>
|
|
@subscriptions = new CompositeDisposable()
|
|
|
|
componentWillReceiveProps: (nextProps) =>
|
|
@setState
|
|
urlInputValue: @_initialUrl(nextProps)
|
|
|
|
componentWillUnmount: =>
|
|
@subscriptions?.dispose()
|
|
|
|
componentDidUpdate: =>
|
|
if @props.mode is "edit-link" and not @props.linkToModify
|
|
# Note, it's important that we're focused on the urlInput because
|
|
# the parent of this component needs to know to not hide us on their
|
|
# onBlur method.
|
|
React.findDOMNode(@refs.urlInput).focus()
|
|
|
|
render: =>
|
|
<div ref="floatingToolbar"
|
|
className={@_toolbarClasses()} style={@_toolbarStyles()}>
|
|
<div className="toolbar-pointer" style={@_toolbarPointerStyles()}></div>
|
|
{@_toolbarType()}
|
|
</div>
|
|
|
|
_toolbarClasses: =>
|
|
classes = {}
|
|
classes[@props.pos] = true
|
|
classNames _.extend classes,
|
|
"floating-toolbar": true
|
|
"toolbar": true
|
|
"toolbar-visible": @props.visible
|
|
|
|
_toolbarStyles: =>
|
|
styles =
|
|
left: @_toolbarLeft()
|
|
top: @props.top
|
|
width: @_width()
|
|
return styles
|
|
|
|
_toolbarType: =>
|
|
if @props.mode is "buttons" then @_renderButtons()
|
|
else if @props.mode is "edit-link" then @_renderLink()
|
|
else return <div></div>
|
|
|
|
_renderButtons: =>
|
|
<div className="toolbar-buttons">
|
|
<button className="btn btn-bold toolbar-btn"
|
|
onClick={@_execCommand}
|
|
data-command-name="bold"></button>
|
|
<button className="btn btn-italic toolbar-btn"
|
|
onClick={@_execCommand}
|
|
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"></button>
|
|
{@_toolbarExtensions()}
|
|
</div>
|
|
|
|
_toolbarExtensions: ->
|
|
buttons = []
|
|
for extension in DraftStore.extensions()
|
|
toolbarItem = extension.composerToolbar?()
|
|
if toolbarItem
|
|
buttons.push(
|
|
<button className="btn btn-extension"
|
|
onClick={ => @_extensionMutateDom(toolbarItem.mutator)}
|
|
data-tooltip="#{toolbarItem.tooltip}"><RetinaImg mode={RetinaImg.Mode.ContentIsMask} url="#{toolbarItem.iconUrl}" /></button>)
|
|
return buttons
|
|
|
|
_extensionMutateDom: (mutator) =>
|
|
@props.onDomMutator(mutator)
|
|
|
|
_renderLink: =>
|
|
removeBtn = ""
|
|
withRemove = ""
|
|
if @_initialUrl()
|
|
withRemove = "with-remove"
|
|
removeBtn = <button className="btn btn-icon"
|
|
ref="removeBtn"
|
|
onMouseDown={@_removeUrl}><i className="fa fa-times"></i></button>
|
|
|
|
<div className="toolbar-new-link"
|
|
onMouseEnter={@_onMouseEnter}
|
|
onMouseLeave={@_onMouseLeave}>
|
|
<i className="fa fa-link preview-btn-icon" onClick={@_onPreventToolbarClose}></i>
|
|
<input type="text"
|
|
ref="urlInput"
|
|
value={@state.urlInputValue}
|
|
onBlur={@_onBlur}
|
|
onFocus={@_onFocus}
|
|
onClick={@_onPreventToolbarClose}
|
|
onKeyPress={@_saveUrlOnEnter}
|
|
onChange={@_onInputChange}
|
|
className="floating-toolbar-input #{withRemove}"
|
|
placeholder="Paste or type a link" />
|
|
<button className="btn btn-icon"
|
|
ref="saveBtn"
|
|
onKeyPress={@_saveUrlOnEnter}
|
|
onMouseDown={@_saveUrl}><i className="fa fa-check"></i></button>
|
|
{removeBtn}
|
|
</div>
|
|
|
|
_onPreventToolbarClose: (event) =>
|
|
event.stopPropagation()
|
|
|
|
_onMouseEnter: =>
|
|
@props.onMouseEnter?()
|
|
|
|
_onMouseLeave: =>
|
|
if @props.linkToModify and document.activeElement isnt React.findDOMNode(@refs.urlInput)
|
|
@props.onMouseLeave?()
|
|
|
|
_initialUrl: (props=@props) =>
|
|
props.linkToModify?.getAttribute?('href')
|
|
|
|
_onInputChange: (event) =>
|
|
@setState urlInputValue: event.target.value
|
|
|
|
_saveUrlOnEnter: (event) =>
|
|
if event.key is "Enter"
|
|
if (@state.urlInputValue ? "").trim().length > 0
|
|
@_saveUrl()
|
|
else
|
|
@_removeUrl()
|
|
|
|
# We signify the removal of a url with an empty string. This protects us
|
|
# from the case where people delete the url text and hit save. In that
|
|
# case we also want to remove the link.
|
|
_removeUrl: =>
|
|
@setState urlInputValue: ""
|
|
@props.onSaveUrl "", @props.linkToModify
|
|
@props.onChangeMode("buttons")
|
|
|
|
_onFocus: =>
|
|
@props.onChangeFocus(true)
|
|
|
|
# Clicking the save or remove buttons will take precendent over simply
|
|
# bluring the field.
|
|
_onBlur: (event) =>
|
|
targets = []
|
|
if @refs["saveBtn"]
|
|
targets.push React.findDOMNode(@refs["saveBtn"])
|
|
if @refs["removeBtn"]
|
|
targets.push React.findDOMNode(@refs["removeBtn"])
|
|
|
|
if event.relatedTarget in targets
|
|
event.preventDefault()
|
|
return
|
|
else
|
|
@_saveUrl()
|
|
@props.onChangeFocus(false)
|
|
|
|
_saveUrl: =>
|
|
if (@state.urlInputValue ? "").trim().length > 0
|
|
@props.onSaveUrl @state.urlInputValue, @props.linkToModify
|
|
@props.onChangeMode("buttons")
|
|
|
|
_execCommand: (event) =>
|
|
cmd = event.currentTarget.getAttribute 'data-command-name'
|
|
document.execCommand(cmd, false, null)
|
|
true
|
|
|
|
_toolbarLeft: =>
|
|
CONTENT_PADDING = @props.contentPadding ? 15
|
|
max = @props.editAreaWidth - @_width() - CONTENT_PADDING
|
|
left = Math.min(Math.max(@props.left - @_width()/2, CONTENT_PADDING), max)
|
|
return left
|
|
|
|
_toolbarPointerStyles: =>
|
|
CONTENT_PADDING = @props.contentPadding ? 15
|
|
POINTER_WIDTH = 6 + 2 #2px of border-radius
|
|
max = @props.editAreaWidth - CONTENT_PADDING
|
|
min = CONTENT_PADDING
|
|
absoluteLeft = Math.max(Math.min(@props.left, max), min)
|
|
relativeLeft = absoluteLeft - @_toolbarLeft()
|
|
|
|
left = Math.max(Math.min(relativeLeft, @_width()-POINTER_WIDTH), POINTER_WIDTH)
|
|
styles =
|
|
left: left
|
|
return styles
|
|
|
|
_width: =>
|
|
# 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 = 114#px
|
|
TOOLBAR_URL_WIDTH = 210#px
|
|
|
|
# If we have a long link, we want to make a larger text area. It's not
|
|
# super important to get the length exactly so let's just get within
|
|
# the ballpark by guessing charcter lengths
|
|
WIDTH_PER_CHAR = 11
|
|
max = @props.editAreaWidth - (@props.contentPadding ? 15)*2
|
|
|
|
if @props.mode is "buttons"
|
|
return TOOLBAR_BUTTONS_WIDTH
|
|
else if @props.mode is "edit-link"
|
|
url = @_initialUrl()
|
|
if url?.length > 0
|
|
fullWidth = Math.max(Math.min(url.length * WIDTH_PER_CHAR, max), TOOLBAR_URL_WIDTH)
|
|
return fullWidth
|
|
else
|
|
return TOOLBAR_URL_WIDTH
|
|
else
|
|
return TOOLBAR_BUTTONS_WIDTH
|
|
|
|
_showLink: =>
|
|
@props.onChangeMode("edit-link")
|
|
|
|
module.exports = FloatingToolbar
|