diff --git a/internal_packages/composer-signature/lib/preferences-signatures.cjsx b/internal_packages/composer-signature/lib/preferences-signatures.cjsx
index 28bc5e81c..7d80070cc 100644
--- a/internal_packages/composer-signature/lib/preferences-signatures.cjsx
+++ b/internal_packages/composer-signature/lib/preferences-signatures.cjsx
@@ -70,7 +70,7 @@ class PreferencesSignatures extends React.Component
_renderCurrentSignature: ->
diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx
index aa599be16..0940918e9 100644
--- a/internal_packages/composer/lib/composer-view.cjsx
+++ b/internal_packages/composer/lib/composer-view.cjsx
@@ -26,8 +26,6 @@ ImageFileUpload = require './image-file-upload'
ExpandedParticipants = require './expanded-participants'
CollapsedParticipants = require './collapsed-participants'
-ContenteditableFilter = require './contenteditable-filter'
-
Fields = require './fields'
# The ComposerView is a unique React component because it (currently) is a
@@ -269,21 +267,20 @@ class ComposerView extends React.Component
_renderBody: =>
-
+
{@_renderBodyContenteditable()}
+ {@_renderQuotedTextControl()}
{@_renderAttachments()}
_renderBodyContenteditable: ->
@setState focusedField: Fields.Body}
- filters={@_editableFilters()}
onChange={@_onChangeBody}
onScrollTo={@props.onRequestScrollTo}
onFilePaste={@_onFilePaste}
- footerElements={@_editableFooterElements()}
onScrollToBottom={@_onScrollToBottom()}
lifecycleCallbacks={@_contenteditableLifecycleCallbacks()}
getComposerBoundingRect={@_getComposerBoundingRect}
@@ -333,18 +330,6 @@ class ComposerView extends React.Component
position: ScrollRegion.ScrollPosition.Bottom
else return null
- _editableFilters: ->
- return [@_quotedTextFilter()]
-
- _quotedTextFilter: ->
- filter = new ContenteditableFilter
- filter.beforeDisplay = @_removeQuotedText
- filter.afterDisplay = @_showQuotedText
- return filter
-
- _editableFooterElements: ->
- @_renderQuotedTextControl()
-
_removeQuotedText: (html) =>
if @state.showQuotedText then return html
else return QuotedHTMLParser.removeQuotedHTML(html)
@@ -614,6 +599,8 @@ class ComposerView extends React.Component
_onChangeBody: (event) =>
return unless @_proxy
+ newBody = @_showQuotedText(event.target.value)
+
# The body changes extremely frequently (on every key stroke). To keep
# performance up, we don't want to trigger every single key stroke
# since that will cause an entire composer re-render. We, however,
@@ -623,7 +610,7 @@ class ComposerView extends React.Component
# We want to use debounce instead of throttle because we don't want ot
# trigger janky re-renders mid quick-type. Let's just do it at the end
# when you're done typing and about to move onto something else.
- @_addToProxy({body: event.target.value}, {fromBodyChange: true})
+ @_addToProxy({body: newBody}, {fromBodyChange: true})
@_throttledTrigger ?= _.debounce =>
@_ignoreNextTrigger = false
@_proxy.trigger()
diff --git a/internal_packages/composer/lib/contenteditable-filter.coffee b/internal_packages/composer/lib/contenteditable-filter.coffee
deleted file mode 100644
index 610eaa904..000000000
--- a/internal_packages/composer/lib/contenteditable-filter.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-class ContenteditableFilter
- # Gets called immediately before insert the HTML into the DOM. This is
- # useful for modifying what the user sees compared to the data we're
- # storing.
- beforeDisplay: ->
-
- # Gets called just after the content has changed but just before we save
- # out the new HTML. The inverse of `beforeDisplay`
- afterDisplay: ->
-
-module.exports = ContenteditableFilter
diff --git a/internal_packages/composer/spec/contenteditable-quoted-text-spec.cjsx b/internal_packages/composer/spec/quoted-text-spec.cjsx
similarity index 75%
rename from internal_packages/composer/spec/contenteditable-quoted-text-spec.cjsx
rename to internal_packages/composer/spec/quoted-text-spec.cjsx
index 9ed7c641b..276b6e921 100644
--- a/internal_packages/composer/spec/contenteditable-quoted-text-spec.cjsx
+++ b/internal_packages/composer/spec/quoted-text-spec.cjsx
@@ -8,17 +8,26 @@ React = require "react/addons"
ReactTestUtils = React.addons.TestUtils
Fields = require '../lib/fields'
-Composer = require "../lib/composer-view",
-{Contenteditable} = require 'nylas-component-kit'
+Composer = require "../lib/composer-view"
+{DraftStore} = require 'nylas-exports'
-describe "Contenteditable", ->
+describe "Composer Quoted Text", ->
beforeEach ->
@onChange = jasmine.createSpy('onChange')
@htmlNoQuote = 'Test HTML
'
@htmlWithQuote = 'Test HTML
QUOTE
'
@composer = ReactTestUtils.renderIntoDocument()
- spyOn(@composer, "_onChangeBody")
+ @composer._proxy = trigger: ->
+ spyOn(@composer, "_addToProxy")
+
+ spyOn(@composer, "_setupSession")
+ spyOn(@composer, "_teardownForDraft")
+ spyOn(@composer, "_deleteDraftIfEmpty")
+ spyOn(@composer, "_renderAttachments")
+
+ afterEach ->
+ DraftStore._cleanupAllSessions()
# Must be called with the test's scope
setHTML = (newHTML) ->
@@ -34,6 +43,7 @@ describe "Contenteditable", ->
showQuotedText: true
@contentEditable = @composer.refs[Fields.Body]
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))
+ @$composerBodyWrap = React.findDOMNode(@composer.refs.composerBodyWrap)
it 'should not display any quoted text', ->
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
@@ -42,11 +52,11 @@ describe "Contenteditable", ->
textToAdd = "MORE TEXT!"
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
setHTML.call(@, textToAdd + @htmlNoQuote)
- ev = @composer._onChangeBody.mostRecentCall.args[0]
- expect(ev.target.value).toEqual(textToAdd + @htmlNoQuote)
+ ev = @composer._addToProxy.mostRecentCall.args[0].body
+ expect(ev).toEqual(textToAdd + @htmlNoQuote)
it 'should not render the quoted-text-control toggle', ->
- toggles = ReactTestUtils.scryRenderedDOMComponentsWithClass(@contentEditable, 'quoted-text-control')
+ toggles = ReactTestUtils.scryRenderedDOMComponentsWithClass(@composer, 'quoted-text-control')
expect(toggles.length).toBe 0
@@ -57,27 +67,28 @@ describe "Contenteditable", ->
showQuotedText: true
@contentEditable = @composer.refs[Fields.Body]
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))
+ @$composerBodyWrap = React.findDOMNode(@composer.refs.composerBodyWrap)
it 'should display the quoted text', ->
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote
- it "should call `_onChangeBody` with the entire HTML string", ->
+ it "should call `_addToProxy` with the entire HTML string", ->
textToAdd = "MORE TEXT!"
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote
setHTML.call(@, textToAdd + @htmlWithQuote)
- ev = @composer._onChangeBody.mostRecentCall.args[0]
- expect(ev.target.value).toEqual(textToAdd + @htmlWithQuote)
+ ev = @composer._addToProxy.mostRecentCall.args[0].body
+ expect(ev).toEqual(textToAdd + @htmlWithQuote)
it "should allow the quoted text to be changed", ->
newText = 'Test NEW 1 HTMLQUOTE CHANGED!!!
'
expect(@$contentEditable.innerHTML).toBe @htmlWithQuote
setHTML.call(@, newText)
- ev = @composer._onChangeBody.mostRecentCall.args[0]
- expect(ev.target.value).toEqual(newText)
+ ev = @composer._addToProxy.mostRecentCall.args[0].body
+ expect(ev).toEqual(newText)
describe 'quoted text control toggle button', ->
beforeEach ->
- @toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@contentEditable, 'quoted-text-control')
+ @toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, 'quoted-text-control')
it 'should be rendered', ->
expect(@toggle).toBeDefined()
@@ -92,6 +103,7 @@ describe "Contenteditable", ->
showQuotedText: false
@contentEditable = @composer.refs[Fields.Body]
@$contentEditable = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@contentEditable, 'contentEditable'))
+ @$composerBodyWrap = React.findDOMNode(@composer.refs.composerBodyWrap)
# The quoted text dom parser wraps stuff inertly in body tags
wrapBody = (html) -> "#{html}"
@@ -99,27 +111,27 @@ describe "Contenteditable", ->
it 'should not display any quoted text', ->
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
- it "should let you change the text, and then append the quoted text part to the end before firing `_onChangeBody`", ->
+ it "should let you change the text, and then append the quoted text part to the end before firing `_addToProxy`", ->
textToAdd = "MORE TEXT!"
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
setHTML.call(@, textToAdd + @htmlNoQuote)
- ev = @composer._onChangeBody.mostRecentCall.args[0]
+ ev = @composer._addToProxy.mostRecentCall.args[0].body
# Note that we expect the version WITH a quote while setting the
# version withOUT a quote.
- expect(ev.target.value).toEqual(wrapBody(textToAdd + @htmlWithQuote))
+ expect(ev).toEqual(wrapBody(textToAdd + @htmlWithQuote))
it "should let you add more html that looks like quoted text, and still properly appends the old quoted text", ->
textToAdd = "Yo I'm a fake quote
"
expect(@$contentEditable.innerHTML).toBe @htmlNoQuote
setHTML.call(@, textToAdd + @htmlNoQuote)
- ev = @composer._onChangeBody.mostRecentCall.args[0]
+ ev = @composer._addToProxy.mostRecentCall.args[0].body
# Note that we expect the version WITH a quote while setting the
# version withOUT a quote.
- expect(ev.target.value).toEqual(wrapBody(textToAdd + @htmlWithQuote))
+ expect(ev).toEqual(wrapBody(textToAdd + @htmlWithQuote))
describe 'quoted text control toggle button', ->
beforeEach ->
- @toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@contentEditable, 'quoted-text-control')
+ @toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, 'quoted-text-control')
it 'should be rendered', ->
expect(@toggle).toBeDefined()
diff --git a/spec/components/clipboard-service-spec.coffee b/spec/components/clipboard-service-spec.coffee
index 1c71df431..9f35bf690 100644
--- a/spec/components/clipboard-service-spec.coffee
+++ b/spec/components/clipboard-service-spec.coffee
@@ -1,4 +1,4 @@
-ClipboardService = require '../../src/components/clipboard-service'
+ClipboardService = require '../../src/components/contenteditable/clipboard-service'
describe "ClipboardService", ->
beforeEach ->
diff --git a/spec/components/contenteditable-component-spec.cjsx b/spec/components/contenteditable-component-spec.cjsx
index 8f753431c..b1c04fed2 100644
--- a/spec/components/contenteditable-component-spec.cjsx
+++ b/spec/components/contenteditable-component-spec.cjsx
@@ -6,7 +6,7 @@ _ = require "underscore"
fs = require 'fs'
React = require "react/addons"
ReactTestUtils = React.addons.TestUtils
-Contenteditable = require "../../src/components/contenteditable",
+Contenteditable = require "../../src/components/contenteditable/contenteditable",
describe "Contenteditable", ->
beforeEach ->
diff --git a/spec/undo-manager-spec.coffee b/spec/undo-manager-spec.coffee
index 1818e398a..b403dccb3 100644
--- a/spec/undo-manager-spec.coffee
+++ b/spec/undo-manager-spec.coffee
@@ -1,4 +1,4 @@
-UndoManager = require "../src/flux/undo-manager"
+UndoManager = require "../src/undo-manager"
describe "UndoManager", ->
beforeEach ->
diff --git a/src/components/clipboard-service.coffee b/src/components/contenteditable/clipboard-service.coffee
similarity index 100%
rename from src/components/clipboard-service.coffee
rename to src/components/contenteditable/clipboard-service.coffee
diff --git a/src/components/contenteditable.cjsx b/src/components/contenteditable/contenteditable.cjsx
similarity index 96%
rename from src/components/contenteditable.cjsx
rename to src/components/contenteditable/contenteditable.cjsx
index 9a6c988cc..15aa13567 100644
--- a/src/components/contenteditable.cjsx
+++ b/src/components/contenteditable/contenteditable.cjsx
@@ -1,20 +1,38 @@
_ = require 'underscore'
React = require 'react'
-{Utils,
- DOMUtils} = require 'nylas-exports'
-
+{Utils, DOMUtils} = require 'nylas-exports'
ClipboardService = require './clipboard-service'
FloatingToolbarContainer = require './floating-toolbar-container'
+###
+Public: A modern, well-behaved, React-compatible contenteditable
+
+This component is fully React-compatible and behaves
+like a standard controlled input.
+
+```javascript
+getInitialState: function() {
+ return {value: 'Hello!'};
+},
+handleChange: function(event) {
+ this.setState({value: event.target.value});
+},
+render: function() {
+ var value = this.state.value;
+ return ;
+}
+```
+###
class Contenteditable extends React.Component
@displayName: "Contenteditable"
- @propTypes:
- html: React.PropTypes.string
- initialSelectionSnapshot: React.PropTypes.object
- filters: React.PropTypes.array
- footerElements: React.PropTypes.node
+ @propTypes:
+
+ # The current html state, as a string, of the contenteditable.
+ value: React.PropTypes.string
+
+ initialSelectionSnapshot: React.PropTypes.object
# Passes an absolute top coordinate to scroll to.
onChange: React.PropTypes.func.isRequired
@@ -23,7 +41,7 @@ class Contenteditable extends React.Component
onScrollToBottom: React.PropTypes.func
# A series of callbacks that can get executed at various points along
- # the contenteditable. Has the keys:
+ # the contenteditable.
lifecycleCallbacks: React.PropTypes.object
spellcheck: React.PropTypes.bool
@@ -31,7 +49,6 @@ class Contenteditable extends React.Component
floatingToolbar: React.PropTypes.bool
@defaultProps:
- filters: []
spellcheck: true
floatingToolbar: true
lifecycleCallbacks:
@@ -105,16 +122,20 @@ class Contenteditable extends React.Component
ref="contenteditable"
contentEditable
spellCheck={false}
- onBlur={@_onBlur}
- onFocus={@_onFocus}
- onClick={@_onClick}
- onPaste={@clipboardService.onPaste}
- onInput={@_onInput}
- onKeyDown={@_onKeyDown}
- dangerouslySetInnerHTML={@_dangerouslySetInnerHTML()}>
- {@props.footerElements}
+ dangerouslySetInnerHTML={__html: @props.value}
+ {...@_eventHandlers()}>
+ _eventHandlers: =>
+ onBlur: @_onBlur
+ onFocus: @_onFocus
+ onClick: @_onClick
+ onPaste: @clipboardService.onPaste
+ onInput: @_onInput
+ onKeyDown: @_onKeyDown
+ onCompositionEnd: @_onCompositionEnd
+ onCompositionStart: @_onCompositionStart
+
focus: =>
@_editableNode().focus()
@@ -145,6 +166,13 @@ class Contenteditable extends React.Component
# Note: Related to composer-view#_onClickComposeBody
event.stopPropagation()
+ _onCompositionStart: =>
+ @_teardownSelectionListeners()
+
+ _onCompositionEnd: =>
+ @_setupSelectionListeners()
+ @_onInput()
+
_onKeyDown: (event) =>
if event.key is "Tab"
@_onTabDown(event)
@@ -184,10 +212,7 @@ class Contenteditable extends React.Component
@_createLists()
_saveNewHtml: ->
- html = @_editableNode().innerHTML
- for filter in @props.filters
- html = filter.afterDisplay(html)
- @props.onChange(target: {value: html})
+ @props.onChange(target: {value: @_editableNode().innerHTML})
# Determines if the user wants to add an ordered or unordered list.
_createLists: ->
@@ -479,12 +504,6 @@ class Contenteditable extends React.Component
_editableNode: =>
React.findDOMNode(@refs.contenteditable)
- _dangerouslySetInnerHTML: =>
- html = @props.html
- for filter in @props.filters
- html = filter.beforeDisplay(html)
- return __html: html
-
######### SELECTION MANAGEMENT ##########
#
# Saving and restoring a selection is difficult with React.
diff --git a/src/components/floating-toolbar-container.cjsx b/src/components/contenteditable/floating-toolbar-container.cjsx
similarity index 100%
rename from src/components/floating-toolbar-container.cjsx
rename to src/components/contenteditable/floating-toolbar-container.cjsx
diff --git a/src/components/floating-toolbar.cjsx b/src/components/contenteditable/floating-toolbar.cjsx
similarity index 100%
rename from src/components/floating-toolbar.cjsx
rename to src/components/contenteditable/floating-toolbar.cjsx
diff --git a/src/flux/stores/draft-store.coffee b/src/flux/stores/draft-store.coffee
index 79f8beab9..8bb4626f8 100644
--- a/src/flux/stores/draft-store.coffee
+++ b/src/flux/stores/draft-store.coffee
@@ -157,6 +157,10 @@ class DraftStore
session.teardown()
delete @_draftSessions[session.draftClientId]
+ _cleanupAllSessions: ->
+ for draftClientId, session of @_draftSessions
+ @_doneWithSession(session)
+
_onBeforeUnload: =>
promises = []
diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee
index 309b043df..91eca954e 100644
--- a/src/global/nylas-component-kit.coffee
+++ b/src/global/nylas-component-kit.coffee
@@ -21,7 +21,7 @@ class NylasComponentKit
@load "DraggableImg", 'draggable-img'
@load "EventedIFrame", 'evented-iframe'
@load "ButtonDropdown", 'button-dropdown'
- @load "Contenteditable", 'contenteditable'
+ @load "Contenteditable", 'contenteditable/contenteditable'
@load "MultiselectList", 'multiselect-list'
@load "InjectedComponent", 'injected-component'
@load "TokenizingTextField", 'tokenizing-text-field'
diff --git a/src/global/nylas-exports.coffee b/src/global/nylas-exports.coffee
index 9490d5f44..15727e53a 100644
--- a/src/global/nylas-exports.coffee
+++ b/src/global/nylas-exports.coffee
@@ -128,7 +128,7 @@ class NylasExports
@load "MessageUtils", 'flux/models/message-utils'
# Services
- @load "UndoManager", 'flux/undo-manager'
+ @load "UndoManager", 'undo-manager'
@load "SoundRegistry", 'sound-registry'
@load "QuotedHTMLParser", 'services/quoted-html-parser'
@load "QuotedPlainTextParser", 'services/quoted-plain-text-parser'
diff --git a/src/flux/undo-manager.coffee b/src/undo-manager.coffee
similarity index 100%
rename from src/flux/undo-manager.coffee
rename to src/undo-manager.coffee