mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-03-14 09:13:01 +08:00
feat(paste): Add files by pasting them into the composer
Summary: You can now add file attachments by pasting them. Test Plan: Run 3 new specs! We only had specs for sanitization so I added some for the paste event handler as well. Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1604
This commit is contained in:
parent
b74372a7f4
commit
1f589be4ad
4 changed files with 136 additions and 13 deletions
|
@ -169,6 +169,7 @@ class ComposerView extends React.Component
|
|||
<ContenteditableComponent ref="contentBody"
|
||||
html={@state.body}
|
||||
onChange={@_onChangeBody}
|
||||
onFilePaste={@_onFilePaste}
|
||||
style={@_precalcComposerCss}
|
||||
initialSelectionSnapshot={@_recoveredSelection}
|
||||
mode={{showQuotedText: @state.showQuotedText}}
|
||||
|
@ -337,6 +338,9 @@ class ComposerView extends React.Component
|
|||
Actions.attachFilePath({path: file.path, messageLocalId: @props.localId})
|
||||
true
|
||||
|
||||
_onFilePaste: (path) =>
|
||||
Actions.attachFilePath({path: path, messageLocalId: @props.localId})
|
||||
|
||||
_onChangeParticipants: (changes={}) => @_addToProxy(changes)
|
||||
_onChangeSubject: (event) => @_addToProxy(subject: event.target.value)
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ class ContenteditableComponent extends React.Component
|
|||
tabIndex: React.PropTypes.string
|
||||
onChange: React.PropTypes.func.isRequired
|
||||
mode: React.PropTypes.object
|
||||
onFilePaste: React.PropTypes.func
|
||||
onChangeMode: React.PropTypes.func
|
||||
initialSelectionSnapshot: React.PropTypes.object
|
||||
|
||||
|
@ -894,19 +895,44 @@ class ContenteditableComponent extends React.Component
|
|||
####### CLEAN PASTE #########
|
||||
|
||||
_onPaste: (evt) =>
|
||||
inputText = evt.clipboardData.getData("text/html") ? ""
|
||||
type = "text/html"
|
||||
if inputText.length is 0
|
||||
inputText = evt.clipboardData.getData("text/plain") ? ""
|
||||
type = "text/plain"
|
||||
|
||||
if inputText.length > 0
|
||||
cleanHtml = @_sanitizeInput(inputText, type)
|
||||
document.execCommand("insertHTML", false, cleanHtml)
|
||||
|
||||
@_selectionManuallyChanged = true
|
||||
return if evt.clipboardData.items.length is 0
|
||||
evt.preventDefault()
|
||||
|
||||
# If the pasteboard has a file on it, stream it to a teporary
|
||||
# file and fire our `onFilePaste` event.
|
||||
item = evt.clipboardData.items[0]
|
||||
|
||||
if item.kind is 'file' and @props.onFilePaste
|
||||
blob = item.getAsFile()
|
||||
ext = {'image/png': '.png', 'image/jpg': '.jpg', 'image/tiff': '.tiff'}[item.type] ? ''
|
||||
temp = require 'temp'
|
||||
path = require 'path'
|
||||
fs = require 'fs'
|
||||
|
||||
reader = new FileReader()
|
||||
reader.addEventListener 'loadend', =>
|
||||
buffer = new Buffer(new Uint8Array(reader.result))
|
||||
tmpFolder = temp.path('-nylas-attachment')
|
||||
tmpPath = path.join(tmpFolder, "Pasted File#{ext}")
|
||||
fs.mkdir tmpFolder, =>
|
||||
fs.writeFile tmpPath, buffer, (err) =>
|
||||
@props.onFilePaste(tmpPath)
|
||||
reader.readAsArrayBuffer(blob)
|
||||
|
||||
else
|
||||
# Look for text/html in any of the clipboard items and fall
|
||||
# back to text/plain.
|
||||
inputText = evt.clipboardData.getData("text/html") ? ""
|
||||
type = "text/html"
|
||||
if inputText.length is 0
|
||||
inputText = evt.clipboardData.getData("text/plain") ? ""
|
||||
type = "text/plain"
|
||||
|
||||
if inputText.length > 0
|
||||
cleanHtml = @_sanitizeInput(inputText, type)
|
||||
document.execCommand("insertHTML", false, cleanHtml)
|
||||
@_selectionManuallyChanged = true
|
||||
|
||||
# This is used primarily when pasting text in
|
||||
_sanitizeInput: (inputText="", type="text/html") =>
|
||||
if type is "text/plain"
|
||||
|
|
|
@ -53,7 +53,7 @@ class FileUploads extends React.Component
|
|||
<FileUpload key={@_key(uploadData)} uploadData={uploadData} />
|
||||
|
||||
_key: (uploadData) =>
|
||||
"#{uploadData.messageLocalId} #{uploadData.filePath}"
|
||||
"#{uploadData.messageLocalId}-#{uploadData.filePath}"
|
||||
|
||||
# fileUploads:
|
||||
# "some_local_msg_id /some/full/path/name":
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
# related test files.
|
||||
#
|
||||
_ = require "underscore"
|
||||
fs = require 'fs'
|
||||
React = require "react/addons"
|
||||
ReactTestUtils = React.addons.TestUtils
|
||||
ContenteditableComponent = require "../lib/contenteditable-component",
|
||||
|
@ -44,7 +45,99 @@ describe "ContenteditableComponent", ->
|
|||
@performEdit(@changedHtmlWithoutQuote)
|
||||
expect(@onChange.callCount).toBe(2)
|
||||
|
||||
describe "pasting behavior", ->
|
||||
describe "pasting", ->
|
||||
beforeEach ->
|
||||
|
||||
describe "when a file item is present", ->
|
||||
beforeEach ->
|
||||
@mockEvent =
|
||||
preventDefault: jasmine.createSpy('preventDefault')
|
||||
clipboardData:
|
||||
items: [{
|
||||
kind: 'file'
|
||||
type: 'image/png'
|
||||
getAsFile: -> new Blob(['12341352312411'], {type : 'image/png'})
|
||||
}]
|
||||
|
||||
it "should save the image to a temporary file and call `onFilePaste`", ->
|
||||
onPaste = jasmine.createSpy('onPaste')
|
||||
@component = ReactTestUtils.renderIntoDocument(
|
||||
<ContenteditableComponent html={''} onChange={@onChange} onFilePaste={onPaste} />
|
||||
)
|
||||
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@component, 'contentEditable'))
|
||||
runs ->
|
||||
ReactTestUtils.Simulate.paste(@editableNode, @mockEvent)
|
||||
waitsFor ->
|
||||
onPaste.callCount > 0
|
||||
runs ->
|
||||
path = require('path')
|
||||
file = onPaste.mostRecentCall.args[0]
|
||||
expect(path.basename(file)).toEqual('Pasted File.png')
|
||||
contents = fs.readFileSync(file)
|
||||
expect(contents.toString()).toEqual('12341352312411')
|
||||
|
||||
describe "when html and plain text parts are present", ->
|
||||
beforeEach ->
|
||||
@mockEvent =
|
||||
preventDefault: jasmine.createSpy('preventDefault')
|
||||
clipboardData:
|
||||
getData: ->
|
||||
return '<strong>This is text</strong>' if 'text/html'
|
||||
return 'This is plain text' if 'text/plain'
|
||||
return null
|
||||
items: [{
|
||||
kind: 'string'
|
||||
type: 'text/html'
|
||||
getAsString: -> '<strong>This is text</strong>'
|
||||
},{
|
||||
kind: 'string'
|
||||
type: 'text/plain'
|
||||
getAsString: -> 'This is plain text'
|
||||
}]
|
||||
|
||||
it "should sanitize the HTML string and call insertHTML", ->
|
||||
spyOn(document, 'execCommand')
|
||||
spyOn(@component, '_sanitizeInput').andCallThrough()
|
||||
|
||||
runs ->
|
||||
ReactTestUtils.Simulate.paste(@editableNode, @mockEvent)
|
||||
waitsFor ->
|
||||
document.execCommand.callCount > 0
|
||||
runs ->
|
||||
expect(@component._sanitizeInput).toHaveBeenCalledWith('<strong>This is text</strong>', 'text/html')
|
||||
[command, a, html] = document.execCommand.mostRecentCall.args
|
||||
expect(command).toEqual('insertHTML')
|
||||
expect(html).toEqual('<strong>This is text</strong>')
|
||||
|
||||
describe "when html and plain text parts are present", ->
|
||||
beforeEach ->
|
||||
@mockEvent =
|
||||
preventDefault: jasmine.createSpy('preventDefault')
|
||||
clipboardData:
|
||||
getData: ->
|
||||
return 'This is plain text' if 'text/plain'
|
||||
return null
|
||||
items: [{
|
||||
kind: 'string'
|
||||
type: 'text/plain'
|
||||
getAsString: -> 'This is plain text'
|
||||
}]
|
||||
|
||||
it "should sanitize the plain text string and call insertHTML", ->
|
||||
spyOn(document, 'execCommand')
|
||||
spyOn(@component, '_sanitizeInput').andCallThrough()
|
||||
|
||||
runs ->
|
||||
ReactTestUtils.Simulate.paste(@editableNode, @mockEvent)
|
||||
waitsFor ->
|
||||
document.execCommand.callCount > 0
|
||||
runs ->
|
||||
expect(@component._sanitizeInput).toHaveBeenCalledWith('This is plain text', 'text/html')
|
||||
[command, a, html] = document.execCommand.mostRecentCall.args
|
||||
expect(command).toEqual('insertHTML')
|
||||
expect(html).toEqual('This is plain text')
|
||||
|
||||
describe "sanitization", ->
|
||||
tests = [
|
||||
{
|
||||
in: ""
|
||||
|
|
Loading…
Reference in a new issue