feat(signatures): add signature support in preferences

Summary:
Adding signature support in preferences

Extracting out DraftStore extensions from the Contenteditable component

Moved Contenteditable to the nylas component kit

Build react remote window selection synchronization.

Test Plan: todo

Reviewers: bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2204
This commit is contained in:
Evan Morikawa 2015-10-29 17:20:41 -04:00
parent fca5db4e45
commit fa3a2ee631
27 changed files with 508 additions and 189 deletions

View file

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -1,11 +1,41 @@
{ComponentRegistry, DraftStore} = require 'nylas-exports'
Extension = require './draft-extension'
{PreferencesSectionStore, DraftStore} = require 'nylas-exports'
SignatureDraftExtension = require './signature-draft-extension'
module.exports =
activate: (@state={}) ->
DraftStore.registerExtension(Extension)
DraftStore.registerExtension(SignatureDraftExtension)
@sectionConfig = new PreferencesSectionStore.SectionConfig
# TODO: Fix RetinaImg to handle plugin images
icon: ->
if process.platform is "win32"
"nylas://composer-signature/images/ic-settings-signatures-win32@2x.png"
else
"nylas://composer-signature/images/ic-settings-signatures@2x.png"
sectionId: "Signatures"
displayName: "Signatures"
component: require "./preferences-signatures"
PreferencesSectionStore.registerPreferenceSection(@sectionConfig)
## TODO:
# PreferencesSectionStore.registerPreferences "composer-signatures", [
# {
# section: PreferencesSectionStore.Section.Signatures
# type: "richtext"
# label: "Signature:"
# perAccount: true
# defaultValue: "- Sent from N1"
# }, {
# section: PreferencesSectionStore.Section.Signatures
# type: "toggle"
# label: "Include on replies"
# defaultValue: true
# }
# ]
deactivate: ->
DraftStore.unregisterExtension(Extension)
DraftStore.unregisterExtension(SignatureDraftExtension)
PreferencesSectionStore.unregisterPreferenceSection(@sectionConfig.sectionId)
serialize: -> @state

View file

@ -0,0 +1,105 @@
React = require 'react'
_ = require 'underscore'
{Contenteditable, RetinaImg, Flexbox} = require 'nylas-component-kit'
{AccountStore, Utils} = require 'nylas-exports'
class PreferencesSignatures extends React.Component
@displayName: 'PreferencesSignatures'
constructor: (@props) ->
@_signatureSaveQueue = {}
selectedAccountId = AccountStore.current()?.id
if selectedAccountId
key = @_configKey(selectedAccountId)
initialSig = @props.config.get(key)
else
initialSig = ""
@state =
accounts: AccountStore.items()
currentSignature: initialSig
selectedAccountId: selectedAccountId
componentDidMount: ->
@usub = AccountStore.listen @_onChange
shouldComponentUpdate: (nextProps, nextState) =>
nextState.selectedAccountId isnt @state.selectedAccountId
componentWillUnmount: ->
@usub()
@_saveSignatureNow(@state.selectedAccountId, @state.currentSignature)
_saveSignatureNow: (accountId, value) =>
key = @_configKey(accountId)
@props.config.set(key, value)
_saveSignatureSoon: (accountId, value) =>
@_signatureSaveQueue[accountId] = value
@_saveSignaturesFromCache()
__saveSignaturesFromCache: =>
for accountId, value of @_signatureSaveQueue
@_saveSignatureNow(accountId, value)
@_signatureSaveQueue = {}
_saveSignaturesFromCache: _.debounce(PreferencesSignatures::__saveSignaturesFromCache, 500)
_onChange: =>
@setState @_getStateFromStores()
_getStateFromStores: ->
accounts = AccountStore.items()
selectedAccountId = @state.selectedAccountId
currentSignature = @state.currentSignature
if not @state.selectedAccountId in _.pluck(accounts, "id")
selectedAccountId = null
currentSignature = ""
return {accounts, selectedAccountId, currentSignature}
_renderAccountPicker: ->
options = @state.accounts.map (account) ->
<option value={account.id}>{account.emailAddress}</option>
<select value={@state.selectedAccountId} onChange={@_onSelectAccount}>
{options}
</select>
_renderCurrentSignature: ->
<Contenteditable
ref="signatureInput"
html={@state.currentSignature}
onChange={@_onEditSignature}
spellcheck={false}
floatingToolbar={false} />
_onEditSignature: (event) =>
html = event.target.value
@setState currentSignature: html
@_saveSignatureSoon(@state.selectedAccountId, html)
_configKey: (accountId) ->
"nylas.account-#{accountId}.signature"
_onSelectAccount: (event) =>
@_saveSignatureNow(@state.selectedAccountId, @state.currentSignature)
selectedAccountId = event.target.value
key = @_configKey(selectedAccountId)
initialSig = @props.config.get(key) ? ""
@setState
currentSignature: initialSig
selectedAccountId: selectedAccountId
render: =>
<div className="container-signatures">
<div className="section-title">
Signature for: {@_renderAccountPicker()}
</div>
<div>
{@_renderCurrentSignature()}
</div>
</div>
module.exports = PreferencesSignatures

View file

@ -3,7 +3,7 @@
class SignatureDraftStoreExtension extends DraftStoreExtension
@prepareNewDraft: (draft) ->
accountId = AccountStore.current().id
signature = atom.config.get("signatures.#{accountId}")
signature = atom.config.get("nylas.account-#{accountId}.signature")
return unless signature
insertionPoint = draft.body.indexOf('<blockquote')

View file

@ -1,6 +1,6 @@
{Message} = require 'nylas-exports'
SignatureDraftStoreExtension = require '../lib/draft-extension'
SignatureDraftStoreExtension = require '../lib/signature-draft-extension'
describe "SignatureDraftStoreExtension", ->
describe "prepareNewDraft", ->

View file

@ -15,6 +15,7 @@ React = require 'react'
{DropZone,
RetinaImg,
ScrollRegion,
Contenteditable,
InjectedComponent,
FocusTrackingRegion,
InjectedComponentSet} = require 'nylas-component-kit'
@ -26,7 +27,6 @@ ExpandedParticipants = require './expanded-participants'
CollapsedParticipants = require './collapsed-participants'
ContenteditableFilter = require './contenteditable-filter'
ContenteditableComponent = require './contenteditable-component'
Fields = require './fields'
@ -275,7 +275,7 @@ class ComposerView extends React.Component
</span>
_renderBodyContenteditable: ->
<ContenteditableComponent
<Contenteditable
ref={Fields.Body}
html={@state.body}
onFocus={ => @setState focusedField: Fields.Body}
@ -285,9 +285,39 @@ class ComposerView extends React.Component
onFilePaste={@_onFilePaste}
footerElements={@_editableFooterElements()}
onScrollToBottom={@_onScrollToBottom()}
lifecycleCallbacks={@_contenteditableLifecycleCallbacks()}
getComposerBoundingRect={@_getComposerBoundingRect}
initialSelectionSnapshot={@_recoveredSelection} />
_contenteditableLifecycleCallbacks: ->
componentDidUpdate: (editableNode) =>
for extension in DraftStore.extensions()
extension.onComponentDidUpdate?(editableNode)
onInput: (editableNode, event) =>
for extension in DraftStore.extensions()
extension.onInput?(editableNode, event)
onTabDown: (editableNode, event, range) =>
for extension in DraftStore.extensions()
extension.onTabDown?(editableNode, range, event)
onSubstitutionPerformed: (editableNode) =>
for extension in DraftStore.extensions()
extension.onSubstitutionPerformed?(editableNode)
onLearnSpelling: (editableNode, text) =>
for extension in DraftStore.extensions()
extension.onLearnSpelling?(editableNode, text)
onMouseUp: (editableNode, event, range) =>
return unless range
try
for extension in DraftStore.extensions()
extension.onMouseUp?(editableNode, range, event)
catch e
console.error('DraftStore extension raised an error: '+e.toString())
# The contenteditable decides when to request a scroll based on the
# position of the cursor and its relative distance to this composer
# component. We provide it our boundingClientRect so it can calculate

View file

@ -9,9 +9,9 @@ ReactTestUtils = React.addons.TestUtils
Fields = require '../lib/fields'
Composer = require "../lib/composer-view",
ContenteditableComponent = require "../lib/contenteditable-component",
{Contenteditable} = require 'nylas-component-kit'
describe "ContenteditableComponent", ->
describe "Contenteditable", ->
beforeEach ->
@onChange = jasmine.createSpy('onChange')
@htmlNoQuote = 'Test <strong>HTML</strong><br>'

View file

@ -1,4 +1,4 @@
PreferencesStore = require './preferences-store'
{PreferencesSectionStore} = require 'nylas-exports'
module.exports =
activate: (@state={}) ->
@ -6,42 +6,43 @@ module.exports =
React = require 'react'
{Actions} = require('nylas-exports')
Actions.registerPreferencesTab({
icon: 'ic-settings-general.png'
name: 'General'
component: require './tabs/preferences-general'
})
Actions.registerPreferencesTab({
icon: 'ic-settings-accounts.png'
name: 'Accounts'
component: require './tabs/preferences-accounts'
})
# Actions.registerPreferencesTab({
# icon: 'ic-settings-mailrules.png'
# name: 'Mail Rules'
# component: require './tabs/preferences-mailrules'
# })
Actions.registerPreferencesTab({
icon: 'ic-settings-shortcuts.png'
name: 'Shortcuts'
component: require './tabs/preferences-keymaps'
})
Actions.registerPreferencesTab({
icon: 'ic-settings-notifications.png'
name: 'Notifications'
component: require './tabs/preferences-notifications'
})
Actions.registerPreferencesTab({
icon: 'ic-settings-appearance.png'
name: 'Appearance'
component: require './tabs/preferences-appearance'
})
Cfg = PreferencesSectionStore.SectionConfig
# Actions.registerPreferencesTab({
# icon: 'ic-settings-signatures.png'
# name: 'Signatures'
# component: require './tabs/preferences-signatures'
# })
PreferencesSectionStore.registerPreferenceSection(new Cfg {
icon: 'ic-settings-general.png'
sectionId: 'General'
displayName: 'General'
component: require './tabs/preferences-general'
order: 1
})
PreferencesSectionStore.registerPreferenceSection(new Cfg {
icon: 'ic-settings-accounts.png'
sectionId: 'Accounts'
displayName: 'Accounts'
component: require './tabs/preferences-accounts'
order: 2
})
PreferencesSectionStore.registerPreferenceSection(new Cfg {
icon: 'ic-settings-shortcuts.png'
sectionId: 'Shortcuts'
displayName: 'Shortcuts'
component: require './tabs/preferences-keymaps'
order: 3
})
PreferencesSectionStore.registerPreferenceSection(new Cfg {
icon: 'ic-settings-notifications.png'
sectionId: 'Notifications'
displayName: 'Notifications'
component: require './tabs/preferences-notifications'
order: 4
})
PreferencesSectionStore.registerPreferenceSection(new Cfg {
icon: 'ic-settings-appearance.png'
sectionId: 'Appearance'
displayName: 'Appearance'
component: require './tabs/preferences-appearance'
order: 5
})
Actions.openPreferences.listen(@_openPreferences)
ipc.on 'open-preferences', => @_openPreferences()

View file

@ -15,18 +15,19 @@ class PreferencesHeader extends React.Component
imgMode = RetinaImg.Mode.ContentIsMask
else
imgMode = RetinaImg.Mode.ContentPreserve
<div className="preference-header">
{ @props.tabs.map (tab) =>
classname = "preference-header-item"
classname += " active" if tab is @props.activeTab
<div className={classname} onClick={ => @props.changeActiveTab(tab) } key={tab.name}>
<div className="preference-header">
{ @props.tabs.map (sectionConfig) =>
classname = "preference-header-item"
classname += " active" if sectionConfig is @props.activeTab
<div className={classname} onClick={ => @props.changeActiveTab(sectionConfig) } key={sectionConfig.sectionId}>
<div className="phi-container">
<div className="icon">
<RetinaImg mode={imgMode} name={tab.icon} />
<RetinaImg mode={imgMode} {...sectionConfig.nameOrUrl()} />
</div>
<div className="name">
{tab.name}
{sectionConfig.displayName}
</div>
</div>
</div>

View file

@ -1,19 +0,0 @@
Reflux = require 'reflux'
_ = require 'underscore'
NylasStore = require 'nylas-store'
{Actions} = require 'nylas-exports'
class PreferencesStore extends NylasStore
constructor: ->
@_tabs = []
@listenTo Actions.registerPreferencesTab, @_registerTab
tabs: =>
@_tabs
_registerTab: (tabConfig) =>
@_tabs.push(tabConfig)
@_triggerSoon ?= _.debounce(( => @trigger()), 20)
@_triggerSoon()
module.exports = new PreferencesStore()

View file

@ -1,15 +1,15 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox, ConfigPropContainer} = require 'nylas-component-kit'
{PreferencesSectionStore} = require 'nylas-exports'
PreferencesStore = require './preferences-store'
PreferencesHeader = require './preferences-header'
class Preferences extends React.Component
@displayName: 'Preferences'
constructor: (@props) ->
tabs = PreferencesStore.tabs()
tabs = PreferencesSectionStore.sections()
if @props.initialTab
activeTab = _.find tabs, (t) => t.name is @props.initialTab
activeTab ||= tabs[0]
@ -18,7 +18,7 @@ class Preferences extends React.Component
componentDidMount: =>
@unlisteners = []
@unlisteners.push PreferencesStore.listen =>
@unlisteners.push PreferencesSectionStore.listen =>
@setState(@getStateFromStores())
componentWillUnmount: =>
@ -29,7 +29,7 @@ class Preferences extends React.Component
@setState(activeTab: @state.tabs[0])
getStateFromStores: =>
tabs: PreferencesStore.tabs()
tabs: PreferencesSectionStore.sections()
render: =>
if @state.activeTab

View file

@ -1,53 +0,0 @@
React = require 'react'
_ = require 'underscore'
{RetinaImg, Flexbox} = require 'nylas-component-kit'
class PreferencesSignatures extends React.Component
@displayName: 'PreferencesSignatures'
render: =>
<div className="container-signatures">
<div className="section-signaturces">
<div className="section-title">
Signatures
</div>
<div className="section-body">
<Flexbox direction="row" style={alignItems: "top"}>
<div style={flex: 2}>
<div className="menu">
<ul className="menu-items">
<li>Personal</li>
<li>Corporate</li>
</ul>
</div>
<div className="menu-footer">
<div className="menu-horizontal">
<ul className="menu-items">
<li>+</li>
<li>-</li>
</ul>
</div>
</div>
</div>
<div style={flex: 5}>
<div className="signature-area">
Signature
</div>
<div className="signature-footer">
<button className="edit-html-button btn">Edit HTML</button>
<div className="menu-horizontal">
<ul className="menu-items">
<li><b>B</b></li>
<li><i>I</i></li>
<li><u>u</u></li>
</ul>
</div>
</div>
</div>
</Flexbox>
</div>
</div>
</div>
module.exports = PreferencesSignatures

View file

@ -99,6 +99,13 @@ body.platform-darwin {
margin-left: 2.5%;
margin-right: 2.5%;
.contenteditable-container {
border: 1px solid @input-border-color;
padding: 10px;
margin-top: 20px;
min-height: 200px;
}
.section-body {
padding: 10px 0 0 0;

View file

@ -1,4 +1,4 @@
ClipboardService = require '../lib/clipboard-service'
ClipboardService = require '../../src/components/clipboard-service'
describe "ClipboardService", ->
beforeEach ->

View file

@ -6,20 +6,20 @@ _ = require "underscore"
fs = require 'fs'
React = require "react/addons"
ReactTestUtils = React.addons.TestUtils
ContenteditableComponent = require "../lib/contenteditable-component",
Contenteditable = require "../../src/components/contenteditable",
describe "ContenteditableComponent", ->
describe "Contenteditable", ->
beforeEach ->
@onChange = jasmine.createSpy('onChange')
html = 'Test <strong>HTML</strong>'
@component = ReactTestUtils.renderIntoDocument(
<ContenteditableComponent html={html} onChange={@onChange}/>
<Contenteditable html={html} onChange={@onChange}/>
)
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@component, 'contentEditable'))
describe "render", ->
it 'should render into the document', ->
expect(ReactTestUtils.isCompositeComponentWithType @component, ContenteditableComponent).toBe true
expect(ReactTestUtils.isCompositeComponentWithType @component, Contenteditable).toBe true
it "should include a content-editable div", ->
expect(@editableNode).toBeDefined()
@ -62,7 +62,7 @@ describe "ContenteditableComponent", ->
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} />
<Contenteditable html={''} onChange={@onChange} onFilePaste={onPaste} />
)
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@component, 'contentEditable'))
runs ->

View file

@ -333,6 +333,9 @@ class Application
ipc.on 'from-react-remote-window', (event, json) =>
@windowManager.sendToMainWindow('from-react-remote-window', json)
ipc.on 'from-react-remote-window-selection', (event, json) =>
@windowManager.sendToMainWindow('from-react-remote-window-selection', json)
ipc.on 'inline-style-parse', (event, {body, clientId}) =>
juice = require 'juice'
try

View file

@ -2,14 +2,13 @@ _ = require 'underscore'
React = require 'react'
{Utils,
DOMUtils,
DraftStore} = require 'nylas-exports'
DOMUtils} = require 'nylas-exports'
ClipboardService = require './clipboard-service'
FloatingToolbarContainer = require './floating-toolbar-container'
class ContenteditableComponent extends React.Component
@displayName: "ContenteditableComponent"
class Contenteditable extends React.Component
@displayName: "Contenteditable"
@propTypes:
html: React.PropTypes.string
initialSelectionSnapshot: React.PropTypes.object
@ -23,8 +22,26 @@ class ContenteditableComponent extends React.Component
onScrollTo: React.PropTypes.func
onScrollToBottom: React.PropTypes.func
# A series of callbacks that can get executed at various points along
# the contenteditable. Has the keys:
lifecycleCallbacks: React.PropTypes.object
spellcheck: React.PropTypes.bool
floatingToolbar: React.PropTypes.bool
@defaultProps:
filters: []
spellcheck: true
floatingToolbar: true
lifecycleCallbacks:
componentDidUpdate: (editableNode) ->
onInput: (editableNode, event) ->
onTabDown: (editableNode, event, range) ->
onLearnSpelling: (editableNode, text) ->
onSubstitutionPerformed: (editableNode) ->
onMouseUp: (editableNode, event, range) ->
constructor: (@props) ->
@innerState = {}
@ -66,19 +83,23 @@ class ContenteditableComponent extends React.Component
@_restoreSelection()
editableNode = @_editableNode()
for extension in DraftStore.extensions()
extension.onComponentDidUpdate(@_editableNode()) if extension.onComponentDidUpdate
@props.lifecycleCallbacks.componentDidUpdate(editableNode)
@setInnerState
links: editableNode.querySelectorAll("*[href]")
editableNode: editableNode
_renderFloatingToolbar: ->
return unless @props.floatingToolbar
<FloatingToolbarContainer
ref="toolbarController"
onSaveUrl={@_onSaveUrl}
onDomMutator={@_onDomMutator} />
render: =>
<div className="contenteditable-container">
<FloatingToolbarContainer
ref="toolbarController"
onSaveUrl={@_onSaveUrl}
onDomMutator={@_onDomMutator} />
{@_renderFloatingToolbar()}
<div id="contenteditable"
ref="contenteditable"
@ -143,7 +164,7 @@ class ContenteditableComponent extends React.Component
@_runCoreFilters()
@_runExtensionFilters(event)
@props.lifecycleCallbacks.onInput(@_editableNode(), event)
@_normalize()
@ -162,10 +183,6 @@ class ContenteditableComponent extends React.Component
_runCoreFilters: ->
@_createLists()
_runExtensionFilters: (event) ->
for extension in DraftStore.extensions()
extension.onInput(@_editableNode(), event) if extension.onInput
_saveNewHtml: ->
html = @_editableNode().innerHTML
for filter in @props.filters
@ -262,8 +279,7 @@ class ContenteditableComponent extends React.Component
editableNode = @_editableNode()
range = DOMUtils.getRangeInScope(editableNode)
for extension in DraftStore.extensions()
extension.onTabDown(editableNode, range, event) if extension.onTabDown
@props.lifecycleCallbacks.onTabDown(editableNode, event, range)
return if event.defaultPrevented
@_onTabDownDefaultBehavior(event)
@ -684,7 +700,6 @@ class ContenteditableComponent extends React.Component
clipboard = require('clipboard')
Menu = remote.require('menu')
MenuItem = remote.require('menu-item')
spellchecker = require('spellchecker')
apply = (newtext) =>
range.deleteContents()
@ -693,15 +708,7 @@ class ContenteditableComponent extends React.Component
range.selectNode(node)
selection.removeAllRanges()
selection.addRange(range)
for extension in DraftStore.extensions()
if extension.onSubstitutionPerformed
extension.onSubstitutionPerformed(@_editableNode())
learnSpelling = =>
spellchecker.add(text)
for extension in DraftStore.extensions()
if extension.onLearnSpelling
extension.onLearnSpelling(@_editableNode(), text)
@props.lifecycleCallbacks.onSubstitutionPerformed(@_editableNode())
cut = =>
clipboard.writeText(text)
@ -715,17 +722,23 @@ class ContenteditableComponent extends React.Component
menu = new Menu()
if spellchecker.isMisspelled(text)
corrections = spellchecker.getCorrectionsForMisspelling(text)
if corrections.length > 0
corrections.forEach (correction) ->
menu.append(new MenuItem({ label: correction, click:( -> apply(correction))}))
else
menu.append(new MenuItem({ label: 'No Guesses Found', enabled: false}))
## TODO, move into spellcheck package
if @props.spellcheck
spellchecker = require('spellchecker')
learnSpelling = =>
spellchecker.add(text)
@props.lifecycleCallbacks.onLearnSpelling(@_editableNode(), text)
if spellchecker.isMisspelled(text)
corrections = spellchecker.getCorrectionsForMisspelling(text)
if corrections.length > 0
corrections.forEach (correction) ->
menu.append(new MenuItem({ label: correction, click:( -> apply(correction))}))
else
menu.append(new MenuItem({ label: 'No Guesses Found', enabled: false}))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: 'Learn Spelling', click: learnSpelling}))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: 'Learn Spelling', click: learnSpelling}))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: 'Cut', click:cut}))
menu.append(new MenuItem({ label: 'Copy', click:copy}))
@ -771,14 +784,10 @@ class ContenteditableComponent extends React.Component
return event unless DOMUtils.selectionInScope(selection, editableNode)
range = DOMUtils.getRangeInScope(editableNode)
if range
try
for extension in DraftStore.extensions()
extension.onMouseUp(editableNode, range, event) if extension.onMouseUp
catch e
console.log('DraftStore extension raised an error: '+e.toString())
event
@props.lifecycleCallbacks.onMouseUp(editableNode, event, range)
return event
_onDragStart: (event) =>
editable = @_editableNode()
@ -881,4 +890,4 @@ class ContenteditableComponent extends React.Component
@_setupSelectionListeners()
@_onInput()
module.exports = ContenteditableComponent
module.exports = Contenteditable

View file

@ -0,0 +1,75 @@
_ = require 'underscore'
NylasStore = require 'nylas-store'
class SectionConfig
constructor: (opts={}) ->
opts.order ?= Infinity
_.extend(@, opts)
nameOrUrl: ->
if _.isFunction(@icon)
icon = @icon()
else
icon = @icon
if icon.indexOf("nylas://") is 0
return {url: icon}
else
return {name: icon}
class PreferencesSectionStore extends NylasStore
constructor: ->
@_sectionConfigs = []
@_triggerSoon ?= _.debounce(( => @trigger()), 20)
@Section = {}
@SectionConfig = SectionConfig
sections: =>
@_sectionConfigs
# TODO: Use our <GeneratedForm /> Class
# TODO: Add in a "richtext" input type in addition to standard input
# types.
registerPreferences: (packageId, config) ->
throw new Error("Not implemented yet")
unregisterPreferences: (packageId) ->
throw new Error("Not implemented yet")
###
Public: Register a new top-level section to preferences
- `sectionConfig` a `PreferencesSectionStore.SectionConfig` object
- `icon` A `nylas://` url or image name. Can be a function that
resolves to one of these
schema definitions on the PreferencesSectionStore.Section.MySectionId
- `sectionId` A unique name to access the Section by
- `displayName` The display name. This may go through i18n.
- `component` The Preference section's React Component.
Most Preference sections include an area where a {PreferencesForm} is
rendered. This is a type of {GeneratedForm} that uses the schema passed
into {PreferencesSectionStore::registerPreferences}
Note that `icon` gets passed into the `url` field of a {RetinaImg}. This
will, in an ideal case, expect to find the following images:
- my-icon-darwin@1x.png
- my-icon-darwin@2x.png
- my-icon-win32@1x.png
- my-icon-win32@2x.png
###
registerPreferenceSection: (sectionConfig) ->
@Section[sectionConfig.sectionId] = sectionConfig.sectionId
@_sectionConfigs.push(sectionConfig)
@_sectionConfigs = _.sortBy(@_sectionConfigs, "order")
@_triggerSoon()
unregisterPreferenceSection: (sectionId) ->
delete @Section[sectionId]
@_sectionConfigs = _.reject @_sectionConfigs, (sectionConfig) ->
sectionConfig.sectionId is sectionId
@_triggerSoon()
module.exports = new PreferencesSectionStore()

View file

@ -21,6 +21,7 @@ class NylasComponentKit
@load "DraggableImg", 'draggable-img'
@load "EventedIFrame", 'evented-iframe'
@load "ButtonDropdown", 'button-dropdown'
@load "Contenteditable", 'contenteditable'
@load "MultiselectList", 'multiselect-list'
@load "InjectedComponent", 'injected-component'
@load "TokenizingTextField", 'tokenizing-text-field'

View file

@ -112,6 +112,7 @@ class NylasExports
@require "FocusedContactsStore", 'flux/stores/focused-contacts-store'
@require "MessageBodyProcessor", 'flux/stores/message-body-processor'
@require "MessageStoreExtension", 'flux/stores/message-store-extension'
@require "PreferencesSectionStore", 'flux/stores/preferences-section-store'
# React Components
@get "React", -> require 'react' # Our version of React for 3rd party use

View file

@ -1,12 +1,24 @@
var _ = require('underscore')
var container = document.getElementById("container");
var ipc = require('ipc');
var lastSelectionData = {}
document.body.classList.add("platform-"+process.platform);
document.body.classList.add("window-type-react-remote");
exp = require('./selection-listeners.js');
restoreSelection = exp.restoreSelection;
getSelectionData = exp.getSelectionData;
var receiveEvent = function (json) {
var remote = require('remote');
if (json.selectionData) {
document.removeEventListener("selectionchange", selectionChange);
restoreSelection(json.selectionData)
document.addEventListener("selectionchange", selectionChange);
}
if (json.html) {
var browserWindow = remote.getCurrentWindow();
browserWindow.on('focus', function() {
@ -86,7 +98,10 @@ events.forEach(function(type) {
if (event.target) {
representation.targetReactId = event.target.dataset.reactid;
}
if (event.target.value !== undefined) {
if (event.target.contentEditable=="true") {
representation.targetValue = event.target.innerHTML;
}
else if (event.target.value !== undefined) {
representation.targetValue = event.target.value;
}
if (event.target.checked !== undefined) {
@ -100,3 +115,14 @@ events.forEach(function(type) {
}
}, true);
});
selectionChange = function() {
selectionData = getSelectionData()
if (_.isEqual(selectionData, lastSelectionData)) { return; }
lastSelectionData = _.clone(selectionData)
var remote = require('remote');
remote.getCurrentWindow().id
ipc.send("from-react-remote-window-selection", selectionData);
}
// document.addEventListener("selectionchange", selectionChange);

View file

@ -5,6 +5,7 @@ var LinkedValueUtils = require('react/lib/LinkedValueUtils');
var ReactDOMComponent = require('react/lib/ReactDOMComponent');
var methods = Object.keys(ReactDOMComponent.BackendIDOperations);
var invocationTargets = [];
var lastSelectionData = {}
var sources = {
CSSPropertyOperations: require('react/lib/CSSPropertyOperations'),
@ -133,7 +134,10 @@ ipc.on('from-react-remote-window', function(json) {
if (rep.targetReactId) {
rep.target = document.querySelector(["[data-reactid='"+rep.targetReactId+"']"]);
}
if (rep.target && (rep.targetValue !== undefined)) {
if (rep.target && rep.target.contentEditable=="true") {
rep.target.innerHTML = rep.targetValue;
}
else if (rep.target && (rep.targetValue !== undefined)) {
rep.target.value = rep.targetValue;
}
if (rep.target && (rep.targetChecked !== undefined)) {
@ -148,16 +152,36 @@ ipc.on('from-react-remote-window', function(json) {
var e = new EventClass(rep.eventType, rep);
process.nextTick(function() {
if (rep.target) {
rep.target.dispatchEvent(e);
} else {
container.dispatchEvent(e);
}
});
if (rep.target) {
rep.target.dispatchEvent(e);
} else {
container.dispatchEvent(e);
}
}
});
exp = require('./selection-listeners.js');
restoreSelection = exp.restoreSelection;
getSelectionData = exp.getSelectionData;
selectionChange = function() {
selectionData = getSelectionData();
if (_.isEqual(selectionData, lastSelectionData)) { return; }
lastSelectionData = _.clone(selectionData)
for (var i = 0; i < invocationTargets.length; i++) {
var target = invocationTargets[i];
target.send({selectionData: selectionData})
}
}
// document.addEventListener("selectionchange", selectionChange);
ipc.on('from-react-remote-window-selection', function(selectionData){
document.removeEventListener("selectionchange", selectionChange)
restoreSelection(selectionData)
document.addEventListener("selectionchange", selectionChange);
});
var parentListenersAttached = false;
var reactRemoteContainer = document.createElement('div');
reactRemoteContainer.style.left = '-10000px';

78
src/react-remote/selection-listeners.js vendored Normal file
View file

@ -0,0 +1,78 @@
pathRelativeToReactNode = function (node, stack) {
if (!node || !node.parentNode) {
return stack;
}
if (node.dataset && node.dataset.reactid) {
return stack
} else {
index = -1;
if (node.parentNode && node.parentNode.childNodes) {
for (var i=0; i < node.parentNode.childNodes.length; i++) {
if (node.parentNode.childNodes[i] == node) {
index = i;
}
}
stack.unshift(index)
}
return pathRelativeToReactNode(node.parentNode, stack)
}
}
restoreSelection = function(selectionData) {
anchorNode = document.querySelector(["[data-reactid='"+selectionData.anchorReactId+"']"]);
focusNode = document.querySelector(["[data-reactid='"+selectionData.focusReactId+"']"]);
if (anchorNode && focusNode) {
for (var i=0; i < selectionData.anchorStack.length; i++) {
childIndex = selectionData.anchorStack[i]
if (anchorNode.childNodes) {
anchorNode = anchorNode.childNodes[childIndex];
}
}
for (var i=0; i < selectionData.focusStack.length; i++) {
childIndex = selectionData.focusStack[i]
if (focusNode.childNodes) {
focusNode = focusNode.childNodes[childIndex]
}
}
selection = document.getSelection();
console.log("Setting selection", anchorNode, selectionData.anchorOffset, focusNode, selectionData.focusOffset)
selection.setBaseAndExtent(anchorNode,
selectionData.anchorOffset,
focusNode,
selectionData.focusOffset)
}
}
getSelectionData = function(){
selection = document.getSelection();
selectionData = {
anchorReactId: null,
focusReactId: null,
anchorOffset: selection.anchorOffset,
focusOffset: selection.focusOffset,
};
if (selection.anchorNode) {
anchorStack = pathRelativeToReactNode(selection.anchorNode, []);
reactNode = selection.anchorNode;
for (var i=0; i < anchorStack.length; i++) {
reactNode = reactNode.parentNode;
}
selectionData.anchorReactId = reactNode.dataset.reactid
selectionData.anchorStack = anchorStack
}
if (selection.focusNode) {
focusStack = pathRelativeToReactNode(selection.focusNode, []);
reactNode = selection.focusNode;
for (var i=0; i < focusStack.length; i++) {
reactNode = reactNode.parentNode;
}
selectionData.focusReactId = reactNode.dataset.reactid
selectionData.focusStack = focusStack
}
return selectionData
}
module.exports = {
restoreSelection: restoreSelection,
getSelectionData: getSelectionData
};