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 HTML
QUOTE 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