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:
Ben Gotow 2015-06-08 12:41:31 -07:00
parent b74372a7f4
commit 1f589be4ad
4 changed files with 136 additions and 13 deletions

View file

@ -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)

View file

@ -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"

View file

@ -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":

View file

@ -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: ""