mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-04 11:44:47 +08:00
feat(docs): New docs tasks and React 0.13.2
Summary: This diff moves us up to React 0.13.2 and transitions some of the core React components to the new syntax based on plain Javascript objects. `setInitialState` is now just code in the constructor, `getDOMNode(@)` is now `React.findDOMNode(@)`, and `isMounted` is no longer necessary or available. This diff also adds `RegisteredComponent` to match `RegisteredRegion`. In another diff, I think we should change the names of these to be `DynamicComponent` and `DynamicComponentSet`. This diff also includes preliminary API Reference docs for Menu.cjsx and Popover.cjsx. You can build the docs using `grunt docs` from the build folder. It produces a simple html format now, but it's easy to customize. Also we now ignore "Unnecessary fat arrow" Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1437
This commit is contained in:
parent
365fe400f7
commit
68343ec472
61 changed files with 1021 additions and 805 deletions
|
@ -300,7 +300,7 @@ module.exports = (grunt) ->
|
|||
grunt.registerTask('compile', ['coffee', 'cjsx', 'prebuild-less', 'cson', 'peg'])
|
||||
grunt.registerTask('lint', ['coffeelint', 'csslint', 'lesslint'])
|
||||
grunt.registerTask('test', ['shell:kill-atom', 'run-edgehill-specs'])
|
||||
grunt.registerTask('docs', ['markdown:guides', 'build-docs'])
|
||||
grunt.registerTask('docs', ['build-docs', 'render-docs'])
|
||||
|
||||
ciTasks = ['output-disk-space', 'download-atom-shell', 'build']
|
||||
ciTasks.push('dump-symbols') if process.platform isnt 'win32'
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"archiver": "^0.13",
|
||||
"async": "~0.2.9",
|
||||
"bluebird": "^2.3",
|
||||
"donna": "1.0.7",
|
||||
"coffee-react-transform": "^3.1.0",
|
||||
"donna": "1.0.10",
|
||||
"formidable": "~1.0.14",
|
||||
"fs-plus": "2.x",
|
||||
"github-releases": "~0.2.0",
|
||||
|
@ -27,9 +28,11 @@
|
|||
"grunt-markdown": "^0.7.0",
|
||||
"grunt-peg": "~1.1.0",
|
||||
"grunt-shell": "~0.3.1",
|
||||
"handlebars": "^3.0.2",
|
||||
"harmony-collections": "~0.3.8",
|
||||
"json-front-matter": "^1.0.0",
|
||||
"legal-eagle": "~0.9.0",
|
||||
"markdown": "^0.5.0",
|
||||
"minidump": "~0.8",
|
||||
"moment": "^2.8",
|
||||
"npm": "~1.4.5",
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
path = require 'path'
|
||||
Handlebars = require 'handlebars'
|
||||
markdown = require('markdown').markdown
|
||||
cjsxtransform = require 'coffee-react-transform'
|
||||
rimraf = require 'rimraf'
|
||||
|
||||
fs = require 'fs-plus'
|
||||
_ = require 'underscore-plus'
|
||||
|
@ -10,12 +14,37 @@ moduleBlacklist = [
|
|||
'space-pen'
|
||||
]
|
||||
|
||||
standardClassURLRoot = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/'
|
||||
|
||||
standardClasses = [
|
||||
'string',
|
||||
'object',
|
||||
'array',
|
||||
'function',
|
||||
'number',
|
||||
'date',
|
||||
'error',
|
||||
'boolean',
|
||||
'null',
|
||||
'undefined',
|
||||
'json',
|
||||
'set',
|
||||
'map',
|
||||
'typeerror',
|
||||
'syntaxerror',
|
||||
'referenceerror',
|
||||
'rangeerror'
|
||||
]
|
||||
|
||||
module.exports = (grunt) ->
|
||||
getClassesToInclude = ->
|
||||
modulesPath = path.resolve(__dirname, '..', '..', 'node_modules')
|
||||
modulesPath = path.resolve(__dirname, '..', '..', 'internal_packages')
|
||||
classes = {}
|
||||
fs.traverseTreeSync modulesPath, (modulePath) ->
|
||||
return false if modulePath.match(/node_modules/g).length > 1 # dont need the dependencies of the dependencies
|
||||
# Don't traverse inside dependencies
|
||||
return false if modulePath.match(/node_modules/g)
|
||||
|
||||
# Don't traverse blacklisted packages (that have docs, but we don't want to include)
|
||||
return false if path.basename(modulePath) in moduleBlacklist
|
||||
return true unless path.basename(modulePath) is 'package.json'
|
||||
return true unless fs.isFileSync(modulePath)
|
||||
|
@ -32,14 +61,102 @@ module.exports = (grunt) ->
|
|||
sortedClasses[className] = classes[className]
|
||||
sortedClasses
|
||||
|
||||
processFields = (json, fields = [], tasks = []) ->
|
||||
if json instanceof Array
|
||||
for val in json
|
||||
processFields(val, fields, tasks)
|
||||
else
|
||||
for key, val of json
|
||||
if key in fields
|
||||
for task in tasks
|
||||
val = task(val)
|
||||
json[key] = val
|
||||
if _.isObject(val)
|
||||
processFields(val, fields, tasks)
|
||||
|
||||
|
||||
grunt.registerTask 'build-docs', 'Builds the API docs in src', ->
|
||||
done = @async()
|
||||
|
||||
# Convert CJSX into coffeescript that can be read by Donna
|
||||
|
||||
docsOutputDir = grunt.config.get('docsOutputDir')
|
||||
cjsxOutputDir = path.join(docsOutputDir, 'temp-cjsx')
|
||||
rimraf cjsxOutputDir, ->
|
||||
fs.mkdir(cjsxOutputDir)
|
||||
srcPath = path.resolve(__dirname, '..', '..', 'src')
|
||||
fs.traverseTreeSync srcPath, (file) ->
|
||||
if path.extname(file) is '.cjsx'
|
||||
transformed = cjsxtransform(grunt.file.read(file))
|
||||
# Only attempt to parse this file as documentation if it contains
|
||||
# real Coffeescript classes.
|
||||
if transformed.indexOf('\nclass ') > 0
|
||||
grunt.file.write(path.join(cjsxOutputDir, path.basename(file)[0..-5]+'coffee'), transformed)
|
||||
true
|
||||
|
||||
metadata = donna.generateMetadata(['.'])
|
||||
api = tello.digest(metadata)
|
||||
_.extend(api.classes, getClassesToInclude())
|
||||
api.classes = sortClasses(api.classes)
|
||||
# Process coffeescript source
|
||||
|
||||
apiJson = JSON.stringify(api, null, 2)
|
||||
metadata = donna.generateMetadata(['.', cjsxOutputDir])
|
||||
api = tello.digest(metadata)
|
||||
_.extend(api.classes, getClassesToInclude())
|
||||
api.classes = sortClasses(api.classes)
|
||||
|
||||
apiJson = JSON.stringify(api, null, 2)
|
||||
apiJsonPath = path.join(docsOutputDir, 'api.json')
|
||||
grunt.file.write(apiJsonPath, apiJson)
|
||||
done()
|
||||
|
||||
grunt.registerTask 'render-docs', 'Builds html from the API docs', ->
|
||||
docsOutputDir = grunt.config.get('docsOutputDir')
|
||||
apiJsonPath = path.join(docsOutputDir, 'api.json')
|
||||
grunt.file.write(apiJsonPath, apiJson)
|
||||
|
||||
templatesPath = path.resolve(__dirname, '..', '..', 'docs-templates')
|
||||
grunt.file.recurse templatesPath, (abspath, root, subdir, filename) ->
|
||||
if filename[0] is '_' and path.extname(filename) is '.html'
|
||||
Handlebars.registerPartial(filename[0..-6], grunt.file.read(abspath))
|
||||
|
||||
templatePath = path.join(templatesPath, 'class.html')
|
||||
template = Handlebars.compile(grunt.file.read(templatePath))
|
||||
|
||||
api = JSON.parse(grunt.file.read(apiJsonPath))
|
||||
classnames = _.map Object.keys(api.classes), (s) -> s.toLowerCase()
|
||||
console.log("Generating HTML for #{classnames.length} classes")
|
||||
|
||||
expandTypeReferences = (val) ->
|
||||
refRegex = /{([\w]*)}/g
|
||||
while (match = refRegex.exec(val)) isnt null
|
||||
classname = match[1].toLowerCase()
|
||||
url = false
|
||||
if classname in standardClasses
|
||||
url = standardClassURLRoot+classname
|
||||
else if classname in classnames
|
||||
url = "./#{classname}.html"
|
||||
else
|
||||
console.warn("Cannot find class named #{classname}")
|
||||
|
||||
if url
|
||||
val = val.replace(match[0], "<a href='#{url}'>#{match[1]}</a>")
|
||||
val
|
||||
|
||||
expandFuncReferences = (val) ->
|
||||
refRegex = /{([\w])?::([\w]*)}/g
|
||||
while (match = refRegex.exec(val)) isnt null
|
||||
[text, a, b] = match
|
||||
url = false
|
||||
if a and b
|
||||
url = "#{a}.html##{b}"
|
||||
label = "#{a}::#{b}"
|
||||
else
|
||||
url = "##{b}"
|
||||
label = "#{b}"
|
||||
if url
|
||||
val = val.replace(text, "<a href='#{url}'>#{label}</a>")
|
||||
val
|
||||
|
||||
for classname, contents of api.classes
|
||||
processFields(contents, ['description'], [markdown.toHTML, expandTypeReferences, expandFuncReferences])
|
||||
processFields(contents, ['type'], [expandTypeReferences])
|
||||
|
||||
result = template(contents)
|
||||
resultPath = path.join(docsOutputDir, "#{classname}.html")
|
||||
grunt.file.write(resultPath, result)
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
"arrow_spacing": {
|
||||
"level": "error"
|
||||
},
|
||||
"no_unnecessary_fat_arrows": {
|
||||
"level": "ignore"
|
||||
},
|
||||
"no_interpolation_in_single_quotes": {
|
||||
"level": "error"
|
||||
},
|
||||
|
|
|
@ -15,7 +15,8 @@ module.exports =
|
|||
MultiselectList: require '../src/components/multiselect-list'
|
||||
MultiselectActionBar: require '../src/components/multiselect-action-bar'
|
||||
ResizableRegion: require '../src/components/resizable-region'
|
||||
RegisteredRegion: require '../src/components/registered-region'
|
||||
InjectedComponentSet: require '../src/components/injected-component-set'
|
||||
InjectedComponent: require '../src/components/injected-component'
|
||||
TokenizingTextField: require '../src/components/tokenizing-text-field'
|
||||
FormItem: FormItem
|
||||
GeneratedForm: GeneratedForm
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
{Actions, Utils, WorkspaceStore} = require 'inbox-exports'
|
||||
{RetinaImg} = require 'ui-components'
|
||||
|
||||
|
@ -7,7 +8,7 @@ AccountSidebarSheetItem = React.createClass
|
|||
displayName: 'AccountSidebarSheetItem'
|
||||
|
||||
render: ->
|
||||
classSet = React.addons.classSet
|
||||
classSet = classNames
|
||||
'item': true
|
||||
'selected': @props.select
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
{Actions, Utils, WorkspaceStore} = require 'inbox-exports'
|
||||
{RetinaImg} = require 'ui-components'
|
||||
|
||||
|
@ -16,11 +17,11 @@ AccountSidebarTagItem = React.createClass
|
|||
if @props.item.unreadCount > 0
|
||||
unread = <div className="unread item-count-box">{@props.item.unreadCount}</div>
|
||||
|
||||
classSet = React.addons.classSet
|
||||
coontainerClass = classNames
|
||||
'item': true
|
||||
'selected': @props.select
|
||||
|
||||
<div className={classSet} onClick={@_onClick} id={@props.item.id}>
|
||||
<div className={coontainerClass} onClick={@_onClick} id={@props.item.id}>
|
||||
<RetinaImg name={"#{@props.item.id}.png"} fallback={'folder.png'} colorfill={@props.select} />
|
||||
<span className="name"> {@props.item.name}</span>
|
||||
{unread}
|
||||
|
|
|
@ -5,11 +5,11 @@ _ = require 'underscore-plus'
|
|||
Actions,
|
||||
UndoManager,
|
||||
DraftStore,
|
||||
FileUploadStore,
|
||||
ComponentRegistry} = require 'inbox-exports'
|
||||
FileUploadStore} = require 'inbox-exports'
|
||||
|
||||
{ResizableRegion,
|
||||
RegisteredRegion,
|
||||
InjectedComponentSet,
|
||||
InjectedComponent,
|
||||
RetinaImg} = require 'ui-components'
|
||||
|
||||
FileUploads = require './file-uploads'
|
||||
|
@ -25,24 +25,18 @@ ComposerView = React.createClass
|
|||
displayName: 'ComposerView'
|
||||
|
||||
getInitialState: ->
|
||||
state = @getComponentRegistryState()
|
||||
_.extend state,
|
||||
populated: false
|
||||
to: []
|
||||
cc: []
|
||||
bcc: []
|
||||
body: ""
|
||||
subject: ""
|
||||
showcc: false
|
||||
showbcc: false
|
||||
showsubject: false
|
||||
showQuotedText: false
|
||||
isSending: DraftStore.sendingState(@props.localId)
|
||||
state
|
||||
populated: false
|
||||
to: []
|
||||
cc: []
|
||||
bcc: []
|
||||
body: ""
|
||||
subject: ""
|
||||
showcc: false
|
||||
showbcc: false
|
||||
showsubject: false
|
||||
showQuotedText: false
|
||||
isSending: DraftStore.sendingState(@props.localId)
|
||||
|
||||
getComponentRegistryState: ->
|
||||
AttachmentComponent: ComponentRegistry.findViewByName 'AttachmentComponent'
|
||||
|
||||
componentWillMount: ->
|
||||
@_prepareForDraft(@props.localId)
|
||||
|
||||
|
@ -85,9 +79,6 @@ ComposerView = React.createClass
|
|||
|
||||
_prepareForDraft: (localId) ->
|
||||
@unlisteners = []
|
||||
@unlisteners.push ComponentRegistry.listen (event) =>
|
||||
@setState(@getComponentRegistryState())
|
||||
|
||||
return unless localId
|
||||
|
||||
# UndoManager must be ready before we call _onDraftChanged for the first time
|
||||
|
@ -195,38 +186,56 @@ ComposerView = React.createClass
|
|||
tabIndex="109" />
|
||||
</div>
|
||||
|
||||
<div className="attachments-area" >
|
||||
{@_fileComponents()}
|
||||
<FileUploads localId={@props.localId} />
|
||||
</div>
|
||||
<RegisteredRegion location="Composer:Footer"
|
||||
draftLocalId={@props.localId}/>
|
||||
{@_renderFooterRegions()}
|
||||
</div>
|
||||
|
||||
<div className="composer-action-bar-wrap">
|
||||
<RegisteredRegion className="composer-action-bar-content"
|
||||
location="Composer:ActionButton"
|
||||
draftLocalId={@props.localId}>
|
||||
|
||||
<button className="btn btn-toolbar btn-trash" style={order: 100}
|
||||
data-tooltip="Delete draft"
|
||||
onClick={@_destroyDraft}><RetinaImg name="toolbar-trash.png" /></button>
|
||||
|
||||
<button className="btn btn-toolbar btn-attach" style={order: 50}
|
||||
data-tooltip="Attach file"
|
||||
onClick={@_attachFile}><RetinaImg name="toolbar-attach.png"/></button>
|
||||
|
||||
<div style={order: 0, flex: 1} />
|
||||
|
||||
<button className="btn btn-toolbar btn-emphasis btn-send" style={order: -100}
|
||||
data-tooltip="Send message"
|
||||
ref="sendButton"
|
||||
onClick={@_sendDraft}><RetinaImg name="toolbar-send.png" /> Send</button>
|
||||
|
||||
</RegisteredRegion>
|
||||
{@_renderActionsRegion()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
_renderFooterRegions: ->
|
||||
return <div></div> unless @props.localId
|
||||
|
||||
<span>
|
||||
<div className="attachments-area">
|
||||
{
|
||||
(@state.files ? []).map (file) =>
|
||||
<InjectedComponent name="Attachment"
|
||||
file={file}
|
||||
key={file.filename}
|
||||
removable={true}
|
||||
messageLocalId={@props.localId} />
|
||||
}
|
||||
<FileUploads localId={@props.localId} />
|
||||
</div>
|
||||
<InjectedComponentSet location="Composer:Footer" draftLocalId={@props.localId}/>
|
||||
</span>
|
||||
|
||||
_renderActionsRegion: ->
|
||||
return <div></div> unless @props.localId
|
||||
|
||||
<InjectedComponentSet className="composer-action-bar-content"
|
||||
location="Composer:ActionButton"
|
||||
draftLocalId={@props.localId}>
|
||||
|
||||
<button className="btn btn-toolbar btn-trash" style={order: 100}
|
||||
data-tooltip="Delete draft"
|
||||
onClick={@_destroyDraft}><RetinaImg name="toolbar-trash.png" /></button>
|
||||
|
||||
<button className="btn btn-toolbar btn-attach" style={order: 50}
|
||||
data-tooltip="Attach file"
|
||||
onClick={@_attachFile}><RetinaImg name="toolbar-attach.png"/></button>
|
||||
|
||||
<div style={order: 0, flex: 1} />
|
||||
|
||||
<button className="btn btn-toolbar btn-emphasis btn-send" style={order: -100}
|
||||
data-tooltip="Send message"
|
||||
ref="sendButton"
|
||||
onClick={@_sendDraft}><RetinaImg name="toolbar-send.png" /> Send</button>
|
||||
|
||||
</InjectedComponentSet>
|
||||
|
||||
# Focus the composer view. Chooses the appropriate field to start
|
||||
# focused depending on the draft type, or you can pass a field as
|
||||
# the first parameter.
|
||||
|
@ -247,14 +256,6 @@ ComposerView = React.createClass
|
|||
draft = @_proxy.draft()
|
||||
Utils.isForwardedMessage(draft)
|
||||
|
||||
_fileComponents: ->
|
||||
AttachmentComponent = @state.AttachmentComponent
|
||||
(@state.files ? []).map (file) =>
|
||||
<AttachmentComponent file={file}
|
||||
key={file.filename}
|
||||
removable={true}
|
||||
messageLocalId={@props.localId} />
|
||||
|
||||
_onDraftChanged: ->
|
||||
draft = @_proxy.draft()
|
||||
if not @_initialHistorySave
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
_ = require 'underscore-plus'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
sanitizeHtml = require 'sanitize-html'
|
||||
{Utils, DraftStore} = require 'inbox-exports'
|
||||
FloatingToolbar = require './floating-toolbar'
|
||||
|
@ -7,11 +8,9 @@ FloatingToolbar = require './floating-toolbar'
|
|||
linkUUID = 0
|
||||
genLinkId = -> linkUUID += 1; return linkUUID
|
||||
|
||||
module.exports =
|
||||
ContenteditableComponent = React.createClass
|
||||
displayName: "Contenteditable"
|
||||
|
||||
propTypes:
|
||||
class ContenteditableComponent extends React.Component
|
||||
@displayName = "Contenteditable"
|
||||
@propTypes =
|
||||
html: React.PropTypes.string
|
||||
style: React.PropTypes.object
|
||||
tabIndex: React.PropTypes.string
|
||||
|
@ -20,15 +19,16 @@ ContenteditableComponent = React.createClass
|
|||
onChangeMode: React.PropTypes.func
|
||||
initialSelectionSnapshot: React.PropTypes.object
|
||||
|
||||
getInitialState: ->
|
||||
toolbarTop: 0
|
||||
toolbarMode: "buttons"
|
||||
toolbarLeft: 0
|
||||
toolbarPos: "above"
|
||||
editAreaWidth: 9999 # This will get set on first selection
|
||||
toolbarVisible: false
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
toolbarTop: 0
|
||||
toolbarMode: "buttons"
|
||||
toolbarLeft: 0
|
||||
toolbarPos: "above"
|
||||
editAreaWidth: 9999 # This will get set on first selection
|
||||
toolbarVisible: false
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@_editableNode().addEventListener('contextmenu', @_onShowContextualMenu)
|
||||
@_setupSelectionListeners()
|
||||
@_setupLinkHoverListeners()
|
||||
|
@ -49,26 +49,26 @@ ContenteditableComponent = React.createClass
|
|||
extension.onFocusPrevious(editableNode, range, event) if extension.onFocusPrevious
|
||||
}
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@_editableNode().removeEventListener('contextmenu', @_onShowContextualMenu)
|
||||
@_teardownSelectionListeners()
|
||||
@_teardownLinkHoverListeners()
|
||||
@_teardownGlobalMouseListener()
|
||||
@_disposable.dispose()
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
componentWillReceiveProps: (nextProps) =>
|
||||
if nextProps.initialSelectionSnapshot?
|
||||
@_setSelectionSnapshot(nextProps.initialSelectionSnapshot)
|
||||
@_refreshToolbarState()
|
||||
|
||||
componentWillUpdate: ->
|
||||
componentWillUpdate: =>
|
||||
@_teardownLinkHoverListeners()
|
||||
|
||||
componentDidUpdate: ->
|
||||
componentDidUpdate: =>
|
||||
@_setupLinkHoverListeners()
|
||||
@_restoreSelection()
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
<div className="contenteditable-container">
|
||||
<FloatingToolbar ref="floatingToolbar"
|
||||
top={@state.toolbarTop}
|
||||
|
@ -95,10 +95,10 @@ ContenteditableComponent = React.createClass
|
|||
<a className={@_quotedTextClasses()} onClick={@_onToggleQuotedText}></a>
|
||||
</div>
|
||||
|
||||
focus: ->
|
||||
@_editableNode().focus() if @isMounted()
|
||||
focus: =>
|
||||
@_editableNode().focus()
|
||||
|
||||
_onInput: (event) ->
|
||||
_onInput: (event) =>
|
||||
@_dragging = false
|
||||
editableNode = @_editableNode()
|
||||
editableNode.normalize()
|
||||
|
@ -112,27 +112,26 @@ ContenteditableComponent = React.createClass
|
|||
html = @_unapplyHTMLDisplayFilters(editableNode.innerHTML)
|
||||
@props.onChange(target: {value: html})
|
||||
|
||||
_onBlur: (event) ->
|
||||
_onBlur: (event) =>
|
||||
# The delay here is necessary to see if the blur was caused by us
|
||||
# navigating to the toolbar and focusing on the set-url input.
|
||||
_.delay =>
|
||||
return unless @isMounted() # Who knows what can happen in 50ms
|
||||
@_hideToolbar()
|
||||
, 50
|
||||
|
||||
_editableNode: -> @refs.contenteditable.getDOMNode()
|
||||
_editableNode: => React.findDOMNode(@refs.contenteditable)
|
||||
|
||||
_getAllLinks: ->
|
||||
_getAllLinks: =>
|
||||
Array.prototype.slice.call(@_editableNode().querySelectorAll("*[href]"))
|
||||
|
||||
_dangerouslySetInnerHTML: ->
|
||||
_dangerouslySetInnerHTML: =>
|
||||
__html: @_applyHTMLDisplayFilters(@props.html)
|
||||
|
||||
_applyHTMLDisplayFilters: (html) ->
|
||||
_applyHTMLDisplayFilters: (html) =>
|
||||
html = @_removeQuotedTextFromHTML(html) unless @props.mode?.showQuotedText
|
||||
return html
|
||||
|
||||
_unapplyHTMLDisplayFilters: (html) ->
|
||||
_unapplyHTMLDisplayFilters: (html) =>
|
||||
html = @_addQuotedTextToHTML(html) unless @props.mode?.showQuotedText
|
||||
return html
|
||||
|
||||
|
@ -178,17 +177,17 @@ ContenteditableComponent = React.createClass
|
|||
# which node is most likely the matching one.
|
||||
|
||||
# http://www.w3.org/TR/selection-api/#selectstart-event
|
||||
_setupSelectionListeners: ->
|
||||
_setupSelectionListeners: =>
|
||||
@_onSelectionChange = => @_setNewSelectionState()
|
||||
document.addEventListener "selectionchange", @_onSelectionChange
|
||||
|
||||
_teardownSelectionListeners: ->
|
||||
_teardownSelectionListeners: =>
|
||||
document.removeEventListener("selectionchange", @_onSelectionChange)
|
||||
|
||||
getCurrentSelection: -> _.clone(@_selection ? {})
|
||||
getPreviousSelection: -> _.clone(@_previousSelection ? {})
|
||||
getCurrentSelection: => _.clone(@_selection ? {})
|
||||
getPreviousSelection: => _.clone(@_previousSelection ? {})
|
||||
|
||||
_getRangeInScope: ->
|
||||
_getRangeInScope: =>
|
||||
selection = document.getSelection()
|
||||
return null if not @_selectionInScope(selection)
|
||||
try
|
||||
|
@ -213,9 +212,8 @@ ContenteditableComponent = React.createClass
|
|||
# our anchorNodes are divs with nested <br> tags. If we don't do a deep
|
||||
# clone then when `isEqualNode` is run it will erroneously return false
|
||||
# and our selection restoration will fail
|
||||
_setNewSelectionState: ->
|
||||
_setNewSelectionState: =>
|
||||
selection = document.getSelection()
|
||||
return unless @isMounted()
|
||||
return if @_checkSameSelection(selection)
|
||||
|
||||
range = @_getRangeInScope()
|
||||
|
@ -242,7 +240,7 @@ ContenteditableComponent = React.createClass
|
|||
@_refreshToolbarState()
|
||||
return @_selection
|
||||
|
||||
_atEndOfContent: (range, selection) ->
|
||||
_atEndOfContent: (range, selection) =>
|
||||
if selection.isCollapsed
|
||||
lastChild = @_editableNode().lastElementChild
|
||||
return false unless lastChild
|
||||
|
@ -256,7 +254,7 @@ ContenteditableComponent = React.createClass
|
|||
return (inLastChild or isLastChild) and atEndIndex
|
||||
else return false
|
||||
|
||||
_setSelectionSnapshot: (selection) ->
|
||||
_setSelectionSnapshot: (selection) =>
|
||||
@_previousSelection = @_selection
|
||||
@_selection = selection
|
||||
|
||||
|
@ -265,18 +263,18 @@ ContenteditableComponent = React.createClass
|
|||
# happening. This is because dragging may stop outside the scope of
|
||||
# this element. Note that the `dragstart` and `dragend` events don't
|
||||
# detect text selection. They are for drag & drop.
|
||||
_setupGlobalMouseListener: ->
|
||||
_setupGlobalMouseListener: =>
|
||||
@__onMouseDown = _.bind(@_onMouseDown, @)
|
||||
@__onMouseMove = _.bind(@_onMouseMove, @)
|
||||
@__onMouseUp = _.bind(@_onMouseUp, @)
|
||||
window.addEventListener("mousedown", @__onMouseDown)
|
||||
window.addEventListener("mouseup", @__onMouseUp)
|
||||
|
||||
_teardownGlobalMouseListener: ->
|
||||
_teardownGlobalMouseListener: =>
|
||||
window.removeEventListener("mousedown", @__onMouseDown)
|
||||
window.removeEventListener("mouseup", @__onMouseUp)
|
||||
|
||||
_onShowContextualMenu: (event) ->
|
||||
_onShowContextualMenu: (event) =>
|
||||
@_hideToolbar()
|
||||
event.preventDefault()
|
||||
|
||||
|
@ -290,7 +288,7 @@ ContenteditableComponent = React.createClass
|
|||
MenuItem = remote.require('menu-item')
|
||||
spellchecker = require('spellchecker')
|
||||
|
||||
apply = (newtext) ->
|
||||
apply = (newtext) =>
|
||||
range.deleteContents()
|
||||
node = document.createTextNode(newtext)
|
||||
range.insertNode(node)
|
||||
|
@ -298,14 +296,14 @@ ContenteditableComponent = React.createClass
|
|||
selection.removeAllRanges()
|
||||
selection.addRange(range)
|
||||
|
||||
cut = ->
|
||||
cut = =>
|
||||
clipboard.writeText(text)
|
||||
apply('')
|
||||
|
||||
copy = ->
|
||||
copy = =>
|
||||
clipboard.writeText(text)
|
||||
|
||||
paste = ->
|
||||
paste = =>
|
||||
apply(clipboard.readText())
|
||||
|
||||
menu = new Menu()
|
||||
|
@ -324,7 +322,7 @@ ContenteditableComponent = React.createClass
|
|||
menu.append(new MenuItem({ label: 'Paste', click:paste}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
|
||||
_onMouseDown: (event) ->
|
||||
_onMouseDown: (event) =>
|
||||
@_mouseDownEvent = event
|
||||
@_mouseHasMoved = false
|
||||
window.addEventListener("mousemove", @__onMouseMove)
|
||||
|
@ -337,18 +335,17 @@ ContenteditableComponent = React.createClass
|
|||
else
|
||||
@_lastMouseDown = Date.now()
|
||||
|
||||
_onDoubleDown: (event) ->
|
||||
return unless @isMounted()
|
||||
editable = @refs.contenteditable.getDOMNode()
|
||||
_onDoubleDown: (event) =>
|
||||
editable = React.findDOMNode(@refs.contenteditable)
|
||||
if editable is event.target or editable.contains(event.target)
|
||||
@_doubleDown = true
|
||||
|
||||
_onMouseMove: (event) ->
|
||||
_onMouseMove: (event) =>
|
||||
if not @_mouseHasMoved
|
||||
@_onDragStart(@_mouseDownEvent)
|
||||
@_mouseHasMoved = true
|
||||
|
||||
_onMouseUp: (event) ->
|
||||
_onMouseUp: (event) =>
|
||||
window.removeEventListener("mousemove", @__onMouseMove)
|
||||
|
||||
if @_doubleDown
|
||||
|
@ -373,14 +370,12 @@ ContenteditableComponent = React.createClass
|
|||
|
||||
event
|
||||
|
||||
_onDragStart: (event) ->
|
||||
return unless @isMounted()
|
||||
editable = @refs.contenteditable.getDOMNode()
|
||||
_onDragStart: (event) =>
|
||||
editable = React.findDOMNode(@refs.contenteditable)
|
||||
if editable is event.target or editable.contains(event.target)
|
||||
@_dragging = true
|
||||
|
||||
_onDragEnd: (event) ->
|
||||
return unless @isMounted()
|
||||
_onDragEnd: (event) =>
|
||||
if @_dragging
|
||||
@_dragging = false
|
||||
@_refreshToolbarState()
|
||||
|
@ -395,7 +390,7 @@ ContenteditableComponent = React.createClass
|
|||
# collapse - Can either be "end" or "start". When we reset the
|
||||
# selection, we'll collapse the range into a single caret
|
||||
# position
|
||||
_restoreSelection: ({force, collapse}={}) ->
|
||||
_restoreSelection: ({force, collapse}={}) =>
|
||||
return if @_dragging
|
||||
return if not @_selection?
|
||||
return if document.activeElement isnt @_editableNode() and not force
|
||||
|
@ -445,7 +440,7 @@ ContenteditableComponent = React.createClass
|
|||
|
||||
# We need to break each node apart and cache since the `selection`
|
||||
# object will mutate underneath us.
|
||||
_checkSameSelection: (newSelection) ->
|
||||
_checkSameSelection: (newSelection) =>
|
||||
return true if not newSelection?
|
||||
return false if not @_selection
|
||||
return false if not newSelection.anchorNode? or not newSelection.focusNode?
|
||||
|
@ -485,10 +480,10 @@ ContenteditableComponent = React.createClass
|
|||
else
|
||||
return false
|
||||
|
||||
_getNodeIndex: (nodeToFind) ->
|
||||
_getNodeIndex: (nodeToFind) =>
|
||||
@_findSimilarNodes(nodeToFind).indexOf nodeToFind
|
||||
|
||||
_findSimilarNodes: (nodeToFind) ->
|
||||
_findSimilarNodes: (nodeToFind) =>
|
||||
nodeList = []
|
||||
treeWalker = document.createTreeWalker @_editableNode()
|
||||
while treeWalker.nextNode()
|
||||
|
@ -497,9 +492,9 @@ ContenteditableComponent = React.createClass
|
|||
|
||||
return nodeList
|
||||
|
||||
_isEqualNode: ->
|
||||
_isEqualNode: =>
|
||||
|
||||
_linksInside: (selection) ->
|
||||
_linksInside: (selection) =>
|
||||
return _.filter @_getAllLinks(), (link) ->
|
||||
selection.containsNode(link, true)
|
||||
|
||||
|
@ -515,8 +510,7 @@ ContenteditableComponent = React.createClass
|
|||
# 1. When you're hovering over a link
|
||||
# 2. When you've arrow-keyed the cursor into a link
|
||||
# 3. When you have selected a range of text.
|
||||
__refreshToolbarState: ->
|
||||
return unless @isMounted()
|
||||
__refreshToolbarState: =>
|
||||
return if @_dragging or (@_doubleDown and not @state.toolbarVisible)
|
||||
if @_linkHoveringOver
|
||||
url = @_linkHoveringOver.getAttribute('href')
|
||||
|
@ -551,22 +545,21 @@ ContenteditableComponent = React.createClass
|
|||
editAreaWidth: editAreaWidth
|
||||
|
||||
# See selection API: http://www.w3.org/TR/selection-api/
|
||||
_selectionInScope: (selection) ->
|
||||
_selectionInScope: (selection) =>
|
||||
return false if not selection?
|
||||
return false if not @isMounted()
|
||||
editNode = @refs.contenteditable.getDOMNode()
|
||||
editNode = React.findDOMNode(@refs.contenteditable)
|
||||
return (editNode.contains(selection.anchorNode) and
|
||||
editNode.contains(selection.focusNode))
|
||||
|
||||
CONTENT_PADDING: 15
|
||||
|
||||
_getToolbarPos: (referenceRect) ->
|
||||
_getToolbarPos: (referenceRect) =>
|
||||
|
||||
TOP_PADDING = 10
|
||||
|
||||
BORDER_RADIUS_PADDING = 15
|
||||
|
||||
editArea = @refs.contenteditable.getDOMNode().getBoundingClientRect()
|
||||
editArea = React.findDOMNode(@refs.contenteditable).getBoundingClientRect()
|
||||
|
||||
calcLeft = (referenceRect.left - editArea.left) + referenceRect.width/2
|
||||
calcLeft = Math.min(Math.max(calcLeft, @CONTENT_PADDING+BORDER_RADIUS_PADDING), editArea.width - BORDER_RADIUS_PADDING)
|
||||
|
@ -579,18 +572,18 @@ ContenteditableComponent = React.createClass
|
|||
|
||||
return [calcLeft, calcTop, editArea.width, toolbarPos]
|
||||
|
||||
_hideToolbar: ->
|
||||
_hideToolbar: =>
|
||||
if not @_focusedOnToolbar() and @state.toolbarVisible
|
||||
@setState toolbarVisible: false
|
||||
|
||||
_focusedOnToolbar: ->
|
||||
@refs.floatingToolbar.getDOMNode().contains(document.activeElement)
|
||||
_focusedOnToolbar: =>
|
||||
React.findDOMNode(@refs.floatingToolbar).contains(document.activeElement)
|
||||
|
||||
# This needs to be in the contenteditable area because we need to first
|
||||
# restore the selection before calling the `execCommand`
|
||||
#
|
||||
# If the url is empty, that means we want to remove the url.
|
||||
_onSaveUrl: (url, linkToModify) ->
|
||||
_onSaveUrl: (url, linkToModify) =>
|
||||
if linkToModify?
|
||||
linkToModify = @_findSimilarNodes(linkToModify)?[0]?.childNodes[0]
|
||||
return if not linkToModify?
|
||||
|
@ -620,7 +613,7 @@ ContenteditableComponent = React.createClass
|
|||
document.execCommand("createLink", false, url)
|
||||
@_restoreSelection(force: true, collapse: "end")
|
||||
|
||||
_setupLinkHoverListeners: ->
|
||||
_setupLinkHoverListeners: =>
|
||||
HOVER_IN_DELAY = 250
|
||||
HOVER_OUT_DELAY = 1000
|
||||
@_links = {}
|
||||
|
@ -634,7 +627,6 @@ ContenteditableComponent = React.createClass
|
|||
@_clearLinkTimeouts()
|
||||
@_linkHoveringOver = link
|
||||
@_links[link.hoverId].enterTimeout = setTimeout =>
|
||||
return unless @isMounted()
|
||||
@_refreshToolbarState()
|
||||
, HOVER_IN_DELAY
|
||||
|
||||
|
@ -642,7 +634,6 @@ ContenteditableComponent = React.createClass
|
|||
@_clearLinkTimeouts()
|
||||
@_linkHoveringOver = null
|
||||
@_links[link.hoverId].leaveTimeout = setTimeout =>
|
||||
return unless @isMounted()
|
||||
return if @refs.floatingToolbar.isHovering
|
||||
@_refreshToolbarState()
|
||||
, HOVER_OUT_DELAY
|
||||
|
@ -653,20 +644,20 @@ ContenteditableComponent = React.createClass
|
|||
@_links[link.hoverId].enterListener = enterListener
|
||||
@_links[link.hoverId].leaveListener = leaveListener
|
||||
|
||||
_clearLinkTimeouts: ->
|
||||
_clearLinkTimeouts: =>
|
||||
for hoverId, linkData of @_links
|
||||
clearTimeout(linkData.enterTimeout) if linkData.enterTimeout?
|
||||
clearTimeout(linkData.leaveTimeout) if linkData.leaveTimeout?
|
||||
|
||||
_onTooltipMouseEnter: ->
|
||||
_onTooltipMouseEnter: =>
|
||||
clearTimeout(@_clearTooltipTimeout) if @_clearTooltipTimeout?
|
||||
|
||||
_onTooltipMouseLeave: ->
|
||||
_onTooltipMouseLeave: =>
|
||||
@_clearTooltipTimeout = setTimeout =>
|
||||
@_refreshToolbarState()
|
||||
, 500
|
||||
|
||||
_teardownLinkHoverListeners: ->
|
||||
_teardownLinkHoverListeners: =>
|
||||
for hoverId, linkData of @_links
|
||||
clearTimeout linkData.enterTimeout
|
||||
clearTimeout linkData.leaveTimeout
|
||||
|
@ -679,7 +670,7 @@ ContenteditableComponent = React.createClass
|
|||
|
||||
####### CLEAN PASTE #########
|
||||
|
||||
_onPaste: (evt) ->
|
||||
_onPaste: (evt) =>
|
||||
html = evt.clipboardData.getData("text/html") ? ""
|
||||
if html.length is 0
|
||||
text = evt.clipboardData.getData("text/plain") ? ""
|
||||
|
@ -695,7 +686,7 @@ ContenteditableComponent = React.createClass
|
|||
return false
|
||||
|
||||
# This is used primarily when pasting text in
|
||||
_sanitizeHtml: (html) ->
|
||||
_sanitizeHtml: (html) =>
|
||||
cleanHTML = sanitizeHtml html.replace(/[\n\r]/g, "<br/>"),
|
||||
allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike']
|
||||
allowedAttributes:
|
||||
|
@ -728,23 +719,26 @@ ContenteditableComponent = React.createClass
|
|||
|
||||
####### QUOTED TEXT #########
|
||||
|
||||
_onToggleQuotedText: ->
|
||||
_onToggleQuotedText: =>
|
||||
@props.onChangeMode?(showQuotedText: !@props.mode?.showQuotedText)
|
||||
|
||||
_quotedTextClasses: -> React.addons.classSet
|
||||
_quotedTextClasses: => classNames
|
||||
"quoted-text-control": true
|
||||
"no-quoted-text": @_htmlQuotedTextStart() is -1
|
||||
"show-quoted-text": @props.mode?.showQuotedText
|
||||
|
||||
_htmlQuotedTextStart: ->
|
||||
_htmlQuotedTextStart: =>
|
||||
@props.html.search(/(<br\/?>)?(<br\/?>)?<[^>]*gmail_quote/)
|
||||
|
||||
_removeQuotedTextFromHTML: (html) ->
|
||||
_removeQuotedTextFromHTML: (html) =>
|
||||
quoteStart = @_htmlQuotedTextStart()
|
||||
if quoteStart is -1 then return html
|
||||
else return html.substr(0, quoteStart)
|
||||
|
||||
_addQuotedTextToHTML: (innerHTML) ->
|
||||
_addQuotedTextToHTML: (innerHTML) =>
|
||||
quoteStart = @_htmlQuotedTextStart()
|
||||
if quoteStart is -1 then return innerHTML
|
||||
else return (innerHTML + @props.html.substr(quoteStart))
|
||||
|
||||
|
||||
module.exports = ContenteditableComponent
|
|
@ -7,7 +7,7 @@
|
|||
#
|
||||
# module.exports =
|
||||
# ContenteditableToolbar = React.createClass
|
||||
# render: ->
|
||||
# render: =>
|
||||
# style =
|
||||
# display: @state.show and 'initial' or 'none'
|
||||
# <div className="compose-toolbar-wrap" onBlur={@onBlur}>
|
||||
|
@ -21,19 +21,19 @@
|
|||
# </div>
|
||||
# </div>
|
||||
#
|
||||
# getInitialState: ->
|
||||
# getInitialState: =>
|
||||
# show: false
|
||||
#
|
||||
# componentDidUpdate: (lastProps, lastState) ->
|
||||
# componentDidUpdate: (lastProps, lastState) =>
|
||||
# if !lastState.show and @state.show
|
||||
# @refs.toolbar.getDOMNode().focus()
|
||||
# @refs.toolbar.findDOMNode().focus()
|
||||
#
|
||||
# onClick: (event) ->
|
||||
# onClick: (event) =>
|
||||
# cmd = event.currentTarget.getAttribute 'data-command-name'
|
||||
# document.execCommand(cmd, false, null)
|
||||
# true
|
||||
#
|
||||
# onBlur: (event) ->
|
||||
# onBlur: (event) =>
|
||||
# target = event.nativeEvent.relatedTarget
|
||||
# if target? and target.getAttribute 'data-command-name'
|
||||
# return
|
||||
|
|
|
@ -1,65 +1,66 @@
|
|||
_ = require 'underscore-plus'
|
||||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
{RetinaImg} = require 'ui-components'
|
||||
|
||||
module.exports =
|
||||
FloatingToolbar = React.createClass
|
||||
displayName: "FloatingToolbar"
|
||||
class FloatingToolbar extends React.Component
|
||||
@displayName = "FloatingToolbar"
|
||||
|
||||
getInitialState: ->
|
||||
mode: "buttons"
|
||||
urlInputValue: @_initialUrl() ? ""
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
mode: "buttons"
|
||||
urlInputValue: @_initialUrl() ? ""
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@isHovering = false
|
||||
@subscriptions = new CompositeDisposable()
|
||||
@_saveUrl = _.debounce @__saveUrl, 10
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
componentWillReceiveProps: (nextProps) =>
|
||||
@setState
|
||||
mode: nextProps.initialMode
|
||||
urlInputValue: @_initialUrl(nextProps)
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@subscriptions?.dispose()
|
||||
@isHovering = false
|
||||
|
||||
componentDidUpdate: ->
|
||||
componentDidUpdate: =>
|
||||
if @state.mode is "edit-link" and not @props.linkToModify
|
||||
# Note, it's important that we're focused on the urlInput because
|
||||
# the parent of this component needs to know to not hide us on their
|
||||
# onBlur method.
|
||||
@refs.urlInput.getDOMNode().focus() if @isMounted()
|
||||
React.findDOMNode(@refs.urlInput).focus()
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
<div ref="floatingToolbar"
|
||||
className={@_toolbarClasses()} style={@_toolbarStyles()}>
|
||||
<div className="toolbar-pointer" style={@_toolbarPointerStyles()}></div>
|
||||
{@_toolbarType()}
|
||||
</div>
|
||||
|
||||
_toolbarClasses: ->
|
||||
_toolbarClasses: =>
|
||||
classes = {}
|
||||
classes[@props.pos] = true
|
||||
React.addons.classSet _.extend classes,
|
||||
classNames _.extend classes,
|
||||
"floating-toolbar": true
|
||||
"toolbar": true
|
||||
"toolbar-visible": @props.visible
|
||||
|
||||
_toolbarStyles: ->
|
||||
_toolbarStyles: =>
|
||||
styles =
|
||||
left: @_toolbarLeft()
|
||||
top: @props.top
|
||||
width: @_width()
|
||||
return styles
|
||||
|
||||
_toolbarType: ->
|
||||
_toolbarType: =>
|
||||
if @state.mode is "buttons" then @_renderButtons()
|
||||
else if @state.mode is "edit-link" then @_renderLink()
|
||||
else return <div></div>
|
||||
|
||||
_renderButtons: ->
|
||||
_renderButtons: =>
|
||||
<div className="toolbar-buttons">
|
||||
<button className="btn btn-bold toolbar-btn"
|
||||
onClick={@_execCommand}
|
||||
|
@ -75,7 +76,7 @@ FloatingToolbar = React.createClass
|
|||
data-command-name="link"></button>
|
||||
</div>
|
||||
|
||||
_renderLink: ->
|
||||
_renderLink: =>
|
||||
removeBtn = ""
|
||||
withRemove = ""
|
||||
if @_initialUrl()
|
||||
|
@ -101,49 +102,49 @@ FloatingToolbar = React.createClass
|
|||
{removeBtn}
|
||||
</div>
|
||||
|
||||
_onMouseEnter: ->
|
||||
_onMouseEnter: =>
|
||||
@isHovering = true
|
||||
@props.onMouseEnter?()
|
||||
|
||||
_onMouseLeave: ->
|
||||
_onMouseLeave: =>
|
||||
@isHovering = false
|
||||
if @props.linkToModify and document.activeElement isnt @refs.urlInput.getDOMNode()
|
||||
if @props.linkToModify and document.activeElement isnt React.findDOMNode(@refs.urlInput)
|
||||
@props.onMouseLeave?()
|
||||
|
||||
|
||||
_initialUrl: (props=@props) ->
|
||||
_initialUrl: (props=@props) =>
|
||||
props.linkToModify?.getAttribute?('href')
|
||||
|
||||
_onInputChange: (event) ->
|
||||
_onInputChange: (event) =>
|
||||
@setState urlInputValue: event.target.value
|
||||
|
||||
_saveUrlOnEnter: (event) ->
|
||||
_saveUrlOnEnter: (event) =>
|
||||
if event.key is "Enter" and @state.urlInputValue.trim().length > 0
|
||||
@_saveUrl()
|
||||
|
||||
# We signify the removal of a url with an empty string. This protects us
|
||||
# from the case where people delete the url text and hit save. In that
|
||||
# case we also want to remove the link.
|
||||
_removeUrl: ->
|
||||
_removeUrl: =>
|
||||
@setState urlInputValue: ""
|
||||
@props.onSaveUrl "", @props.linkToModify
|
||||
|
||||
__saveUrl: ->
|
||||
return unless @isMounted() and @state.urlInputValue?
|
||||
__saveUrl: =>
|
||||
return unless @state.urlInputValue?
|
||||
@props.onSaveUrl @state.urlInputValue, @props.linkToModify
|
||||
|
||||
_execCommand: (event) ->
|
||||
_execCommand: (event) =>
|
||||
cmd = event.currentTarget.getAttribute 'data-command-name'
|
||||
document.execCommand(cmd, false, null)
|
||||
true
|
||||
|
||||
_toolbarLeft: ->
|
||||
_toolbarLeft: =>
|
||||
CONTENT_PADDING = @props.contentPadding ? 15
|
||||
max = @props.editAreaWidth - @_width() - CONTENT_PADDING
|
||||
left = Math.min(Math.max(@props.left - @_width()/2, CONTENT_PADDING), max)
|
||||
return left
|
||||
|
||||
_toolbarPointerStyles: ->
|
||||
_toolbarPointerStyles: =>
|
||||
CONTENT_PADDING = @props.contentPadding ? 15
|
||||
POINTER_WIDTH = 6 + 2 #2px of border-radius
|
||||
max = @props.editAreaWidth - CONTENT_PADDING
|
||||
|
@ -156,7 +157,7 @@ FloatingToolbar = React.createClass
|
|||
left: left
|
||||
return styles
|
||||
|
||||
_width: ->
|
||||
_width: =>
|
||||
# We can't calculate the width of the floating toolbar declaratively
|
||||
# because it hasn't been rendered yet. As such, we'll keep the width
|
||||
# fixed to make it much eaier.
|
||||
|
@ -181,5 +182,7 @@ FloatingToolbar = React.createClass
|
|||
else
|
||||
return TOOLBAR_BUTTONS_WIDTH
|
||||
|
||||
_showLink: ->
|
||||
_showLink: =>
|
||||
@setState mode: "edit-link"
|
||||
|
||||
module.exports = FloatingToolbar
|
||||
|
|
|
@ -14,7 +14,7 @@ describe "ContenteditableComponent", ->
|
|||
@component = ReactTestUtils.renderIntoDocument(
|
||||
<ContenteditableComponent html={html} onChange={@onChange}/>
|
||||
)
|
||||
@editableNode = ReactTestUtils.findRenderedDOMComponentWithAttr(@component, 'contentEditable').getDOMNode()
|
||||
@editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@component, 'contentEditable'))
|
||||
|
||||
describe "render", ->
|
||||
it 'should render into the document', ->
|
||||
|
|
|
@ -50,7 +50,7 @@ describe "ContenteditableComponent", ->
|
|||
describe "when showQuotedText is false", ->
|
||||
it "should only display HTML up to the beginning of the quoted text", ->
|
||||
@editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(@componentWithQuote, 'contentEditable')
|
||||
expect(@editDiv.getDOMNode().innerHTML.indexOf('gmail_quote') >= 0).toBe(false)
|
||||
expect(React.findDOMNode(@editDiv).innerHTML.indexOf('gmail_quote') >= 0).toBe(false)
|
||||
|
||||
describe "when showQuotedText is true", ->
|
||||
beforeEach ->
|
||||
|
@ -63,7 +63,7 @@ describe "ContenteditableComponent", ->
|
|||
it "should display all the HTML", ->
|
||||
@componentWithQuote.setState(showQuotedText: true)
|
||||
@editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(@componentWithQuote, 'contentEditable')
|
||||
expect(@editDiv.getDOMNode().innerHTML.indexOf('gmail_quote') >= 0).toBe(true)
|
||||
expect(React.findDOMNode(@editDiv).innerHTML.indexOf('gmail_quote') >= 0).toBe(true)
|
||||
|
||||
describe "showQuotedText", ->
|
||||
it "should default to false", ->
|
||||
|
@ -76,7 +76,7 @@ describe "ContenteditableComponent", ->
|
|||
|
||||
@performEdit = (newHTML, component = @componentWithQuote) =>
|
||||
editDiv = ReactTestUtils.findRenderedDOMComponentWithAttr(component, 'contentEditable')
|
||||
editDiv.getDOMNode().innerHTML = newHTML
|
||||
React.findDOMNode(editDiv).innerHTML = newHTML
|
||||
ReactTestUtils.Simulate.input(editDiv, {target: {value: newHTML}})
|
||||
|
||||
describe "when showQuotedText is true", ->
|
||||
|
|
|
@ -250,7 +250,7 @@ describe "populated composer", ->
|
|||
|
||||
it "sends when you click the send button", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = @composer.refs.sendButton.getDOMNode()
|
||||
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_LOCAL_ID)
|
||||
expect(Actions.sendDraft.calls.length).toBe 1
|
||||
|
@ -261,7 +261,7 @@ describe "populated composer", ->
|
|||
|
||||
it "doesn't send twice if you double click", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = @composer.refs.sendButton.getDOMNode()
|
||||
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
simulateDraftStore()
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
|
@ -270,17 +270,17 @@ describe "populated composer", ->
|
|||
|
||||
it "disables the composer once sending has started", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = @composer.refs.sendButton.getDOMNode()
|
||||
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
||||
cover = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "composer-cover")
|
||||
expect(cover.getDOMNode().style.display).toBe "none"
|
||||
expect(React.findDOMNode(cover).style.display).toBe "none"
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
simulateDraftStore()
|
||||
expect(cover.getDOMNode().style.display).toBe "block"
|
||||
expect(React.findDOMNode(cover).style.display).toBe "block"
|
||||
expect(@composer.state.isSending).toBe true
|
||||
|
||||
it "re-enables the composer if sending threw an error", ->
|
||||
useFullDraft.apply(@); makeComposer.call(@)
|
||||
sendBtn = @composer.refs.sendButton.getDOMNode()
|
||||
sendBtn = React.findDOMNode(@composer.refs.sendButton)
|
||||
ReactTestUtils.Simulate.click sendBtn
|
||||
simulateDraftStore()
|
||||
expect(@composer.state.isSending).toBe true
|
||||
|
@ -297,21 +297,21 @@ describe "populated composer", ->
|
|||
InboxTestUtils.loadKeymap "internal_packages/composer/keymaps/composer.cson"
|
||||
|
||||
it "sends the draft on cmd-enter", ->
|
||||
InboxTestUtils.keyPress("cmd-enter", @composer.getDOMNode())
|
||||
InboxTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
|
||||
expect(@composer._sendDraft).toHaveBeenCalled()
|
||||
|
||||
it "does not send the draft on enter if the button isn't in focus", ->
|
||||
InboxTestUtils.keyPress("enter", @composer.getDOMNode())
|
||||
InboxTestUtils.keyPress("enter", React.findDOMNode(@composer))
|
||||
expect(@composer._sendDraft).not.toHaveBeenCalled()
|
||||
|
||||
it "sends the draft on enter when the button is in focus", ->
|
||||
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
|
||||
InboxTestUtils.keyPress("enter", sendBtn.getDOMNode())
|
||||
InboxTestUtils.keyPress("enter", React.findDOMNode(sendBtn))
|
||||
expect(@composer._sendDraft).toHaveBeenCalled()
|
||||
|
||||
it "doesn't let you send twice", ->
|
||||
sendBtn = ReactTestUtils.findRenderedDOMComponentWithClass(@composer, "btn-send")
|
||||
InboxTestUtils.keyPress("enter", sendBtn.getDOMNode())
|
||||
InboxTestUtils.keyPress("enter", React.findDOMNode(sendBtn))
|
||||
expect(@composer._sendDraft).toHaveBeenCalled()
|
||||
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ describe 'ParticipantsTextField', ->
|
|||
participants={@participants}
|
||||
change={@propChange} />
|
||||
)
|
||||
@renderedInput = ReactTestUtils.findRenderedDOMComponentWithTag(@renderedField, 'input').getDOMNode()
|
||||
@renderedInput = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(@renderedField, 'input'))
|
||||
|
||||
@expectInputToYield = (input, expected) ->
|
||||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: input}})
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
|
||||
module.exports =
|
||||
ActivityBarTask = React.createClass
|
||||
|
@ -36,7 +37,7 @@ ActivityBarTask = React.createClass
|
|||
|
||||
_classNames: ->
|
||||
qs = @props.task.queueState ? {}
|
||||
React.addons.classSet
|
||||
classNames
|
||||
"task": true
|
||||
"task-queued": @props.type is "queued"
|
||||
"task-completed": @props.type is "completed"
|
||||
|
|
|
@ -105,48 +105,43 @@ EmailFixingStyles = """
|
|||
</style>
|
||||
"""
|
||||
|
||||
module.exports =
|
||||
EmailFrame = React.createClass
|
||||
displayName: 'EmailFrame'
|
||||
class EmailFrame extends React.Component
|
||||
@displayName = 'EmailFrame'
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
<EventedIFrame seamless="seamless" />
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@_mounted = true
|
||||
@_writeContent()
|
||||
@_setFrameHeight()
|
||||
|
||||
componentDidUpdate: ->
|
||||
componentWillUnmount: =>
|
||||
@_mounted = false
|
||||
|
||||
componentDidUpdate: =>
|
||||
@_writeContent()
|
||||
@_setFrameHeight()
|
||||
|
||||
shouldComponentUpdate: (newProps, newState) ->
|
||||
shouldComponentUpdate: (newProps, newState) =>
|
||||
# Turns out, React is not able to tell if props.children has changed,
|
||||
# so whenever the message list updates each email-frame is repopulated,
|
||||
# often with the exact same content. To avoid unnecessary calls to
|
||||
# _writeContent, we do a quick check for deep equality.
|
||||
!_.isEqual(newProps, @props)
|
||||
|
||||
_writeContent: ->
|
||||
_writeContent: =>
|
||||
wrapperClass = if @props.showQuotedText then "show-quoted-text" else ""
|
||||
doc = @getDOMNode().contentDocument
|
||||
doc = React.findDOMNode(@).contentDocument
|
||||
doc.open()
|
||||
doc.write(EmailFixingStyles)
|
||||
doc.write("<div id='inbox-html-wrapper' class='#{wrapperClass}'>#{@_emailContent()}</div>")
|
||||
doc.close()
|
||||
|
||||
_setFrameHeight: ->
|
||||
_setFrameHeight: =>
|
||||
_.defer =>
|
||||
return unless @isMounted()
|
||||
# Sometimes the _defer will fire after React has tried to clean up
|
||||
# the DOM, at which point @getDOMNode will fail.
|
||||
#
|
||||
# If this happens, try to call this again to catch React next time.
|
||||
try
|
||||
domNode = @getDOMNode()
|
||||
catch
|
||||
return
|
||||
|
||||
return unless @_mounted
|
||||
domNode = React.findDOMNode(@)
|
||||
doc = domNode.contentDocument
|
||||
height = doc.getElementById("inbox-html-wrapper").scrollHeight
|
||||
if domNode.height != "#{height}px"
|
||||
|
@ -155,7 +150,7 @@ EmailFrame = React.createClass
|
|||
unless domNode?.contentDocument?.readyState is 'complete'
|
||||
@_setFrameHeight()
|
||||
|
||||
_emailContent: ->
|
||||
_emailContent: =>
|
||||
email = @props.children
|
||||
|
||||
# When showing quoted text, always return the pure content
|
||||
|
@ -163,3 +158,6 @@ EmailFrame = React.createClass
|
|||
email
|
||||
else
|
||||
Utils.stripQuotedText(email)
|
||||
|
||||
|
||||
module.exports = EmailFrame
|
|
@ -1,4 +1,5 @@
|
|||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
_ = require 'underscore-plus'
|
||||
EmailFrame = require './email-frame'
|
||||
MessageParticipants = require "./message-participants"
|
||||
|
@ -8,50 +9,49 @@ MessageTimestamp = require "./message-timestamp"
|
|||
MessageUtils,
|
||||
ComponentRegistry,
|
||||
FileDownloadStore} = require 'inbox-exports'
|
||||
{RetinaImg, RegisteredRegion} = require 'ui-components'
|
||||
{RetinaImg,
|
||||
InjectedComponentSet,
|
||||
InjectedComponent} = require 'ui-components'
|
||||
Autolinker = require 'autolinker'
|
||||
remote = require 'remote'
|
||||
|
||||
TransparentPixel = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII="
|
||||
MessageBodyWidth = 740
|
||||
|
||||
module.exports =
|
||||
MessageItem = React.createClass
|
||||
displayName: 'MessageItem'
|
||||
class MessageItem extends React.Component
|
||||
@displayName = 'MessageItem'
|
||||
|
||||
propTypes:
|
||||
@propTypes =
|
||||
thread: React.PropTypes.object.isRequired
|
||||
message: React.PropTypes.object.isRequired
|
||||
thread_participants: React.PropTypes.arrayOf(React.PropTypes.object)
|
||||
collapsed: React.PropTypes.bool
|
||||
|
||||
mixins: [ComponentRegistry.Mixin]
|
||||
components: ['AttachmentComponent']
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
# Holds the downloadData (if any) for all of our files. It's a hash
|
||||
# keyed by a fileId. The value is the downloadData.
|
||||
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())
|
||||
showQuotedText: @_isForwardedMessage()
|
||||
detailedHeaders: false
|
||||
|
||||
getInitialState: ->
|
||||
# Holds the downloadData (if any) for all of our files. It's a hash
|
||||
# keyed by a fileId. The value is the downloadData.
|
||||
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())
|
||||
showQuotedText: @_isForwardedMessage()
|
||||
detailedHeaders: false
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@_storeUnlisten = FileDownloadStore.listen(@_onDownloadStoreChange)
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@_storeUnlisten() if @_storeUnlisten
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not Utils.isEqualReact(nextProps, @props) or
|
||||
not Utils.isEqualReact(nextState, @state)
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
if @props.collapsed
|
||||
@_renderCollapsed()
|
||||
else
|
||||
@_renderFull()
|
||||
|
||||
_renderCollapsed: ->
|
||||
_renderCollapsed: =>
|
||||
<div className={@props.className} onClick={@_toggleCollapsed}>
|
||||
<div className="message-item-area">
|
||||
<div className="collapsed-from">
|
||||
|
@ -66,7 +66,7 @@ MessageItem = React.createClass
|
|||
</div>
|
||||
</div>
|
||||
|
||||
_renderFull: ->
|
||||
_renderFull: =>
|
||||
<div className={@props.className}>
|
||||
<div className="message-item-area">
|
||||
{@_renderHeader()}
|
||||
|
@ -78,7 +78,7 @@ MessageItem = React.createClass
|
|||
</div>
|
||||
</div>
|
||||
|
||||
_renderHeader: ->
|
||||
_renderHeader: =>
|
||||
<header className="message-header">
|
||||
|
||||
<div className="message-header-right">
|
||||
|
@ -86,7 +86,7 @@ MessageItem = React.createClass
|
|||
isDetailed={@state.detailedHeaders}
|
||||
date={@props.message.date} />
|
||||
|
||||
<RegisteredRegion className="message-indicator"
|
||||
<InjectedComponentSet className="message-indicator"
|
||||
location="MessageIndicator"
|
||||
message={@props.message}/>
|
||||
|
||||
|
@ -107,22 +107,22 @@ MessageItem = React.createClass
|
|||
|
||||
</header>
|
||||
|
||||
_renderAttachments: ->
|
||||
_renderAttachments: =>
|
||||
attachments = @_attachmentComponents()
|
||||
if attachments.length > 0
|
||||
<div className="attachments-area">{attachments}</div>
|
||||
else
|
||||
<div></div>
|
||||
|
||||
_quotedTextClasses: -> React.addons.classSet
|
||||
_quotedTextClasses: => classNames
|
||||
"quoted-text-control": true
|
||||
'no-quoted-text': (Utils.quotedTextIndex(@props.message.body) is -1)
|
||||
'show-quoted-text': @state.showQuotedText
|
||||
|
||||
_renderMessageActionsInline: ->
|
||||
_renderMessageActionsInline: =>
|
||||
@_renderMessageActions()
|
||||
|
||||
_renderMessageActionsTooltip: ->
|
||||
_renderMessageActionsTooltip: =>
|
||||
return <span></span>
|
||||
## TODO: For now leave blank. There may be an alternative UI in the
|
||||
#future.
|
||||
|
@ -130,12 +130,12 @@ MessageItem = React.createClass
|
|||
# onClick={=> @setState detailedHeaders: true}>
|
||||
# <RetinaImg name={"message-show-more.png"}/></span>
|
||||
|
||||
_renderMessageActions: ->
|
||||
_renderMessageActions: =>
|
||||
<div className="message-actions-wrap">
|
||||
<div className="message-actions-ellipsis" onClick={@_onShowActionsMenu}>
|
||||
<RetinaImg name={"message-actions-ellipsis.png"}/>
|
||||
</div>
|
||||
<RegisteredRegion className="message-actions"
|
||||
<InjectedComponentSet className="message-actions"
|
||||
location="MessageAction"
|
||||
thread={@props.thread}
|
||||
message={@props.message}>
|
||||
|
@ -148,22 +148,22 @@ MessageItem = React.createClass
|
|||
<button className="btn btn-icon" onClick={@_onForward}>
|
||||
<RetinaImg name={"message-forward.png"}/>
|
||||
</button>
|
||||
</RegisteredRegion>
|
||||
</InjectedComponentSet>
|
||||
</div>
|
||||
|
||||
_onReply: ->
|
||||
_onReply: =>
|
||||
tId = @props.thread.id; mId = @props.message.id
|
||||
Actions.composeReply(threadId: tId, messageId: mId) if (tId and mId)
|
||||
|
||||
_onReplyAll: ->
|
||||
_onReplyAll: =>
|
||||
tId = @props.thread.id; mId = @props.message.id
|
||||
Actions.composeReplyAll(threadId: tId, messageId: mId) if (tId and mId)
|
||||
|
||||
_onForward: ->
|
||||
_onForward: =>
|
||||
tId = @props.thread.id; mId = @props.message.id
|
||||
Actions.composeForward(threadId: tId, messageId: mId) if (tId and mId)
|
||||
|
||||
_onReport: (issueType) ->
|
||||
_onReport: (issueType) =>
|
||||
{Contact, Message, DatabaseStore, NamespaceStore} = require 'inbox-exports'
|
||||
|
||||
draft = new Message
|
||||
|
@ -175,8 +175,8 @@ MessageItem = React.createClass
|
|||
namespaceId: NamespaceStore.current().id
|
||||
body: @props.message.body
|
||||
|
||||
DatabaseStore.persistModel(draft).then ->
|
||||
DatabaseStore.localIdForModel(draft).then (localId) ->
|
||||
DatabaseStore.persistModel(draft).then =>
|
||||
DatabaseStore.localIdForModel(draft).then (localId) =>
|
||||
Actions.sendDraft(localId)
|
||||
|
||||
dialog = remote.require('dialog')
|
||||
|
@ -187,7 +187,7 @@ MessageItem = React.createClass
|
|||
detail: "The contents of this message have been sent to the Edgehill team and we added to a test suite."
|
||||
}
|
||||
|
||||
_onShowOriginal: ->
|
||||
_onShowOriginal: =>
|
||||
fs = require 'fs'
|
||||
path = require 'path'
|
||||
BrowserWindow = remote.require('browser-window')
|
||||
|
@ -204,7 +204,7 @@ MessageItem = React.createClass
|
|||
window = new BrowserWindow(width: 800, height: 600, title: "#{@props.message.subject} - RFC822")
|
||||
window.loadUrl('file://'+tmpfile)
|
||||
|
||||
_onShowActionsMenu: ->
|
||||
_onShowActionsMenu: =>
|
||||
remote = require('remote')
|
||||
Menu = remote.require('menu')
|
||||
MenuItem = remote.require('menu-item')
|
||||
|
@ -218,7 +218,7 @@ MessageItem = React.createClass
|
|||
menu.append(new MenuItem({ label: 'Show Original', click: => @_onShowOriginal()}))
|
||||
menu.popup(remote.getCurrentWindow())
|
||||
|
||||
_renderCollapseControl: ->
|
||||
_renderCollapseControl: =>
|
||||
if @state.detailedHeaders
|
||||
<div className="collapse-control"
|
||||
style={top: "4px", left: "-17px"}
|
||||
|
@ -234,7 +234,7 @@ MessageItem = React.createClass
|
|||
|
||||
# Eventually, _formatBody will run a series of registered body transformers.
|
||||
# For now, it just runs a few we've hardcoded here, which are all synchronous.
|
||||
_formatBody: ->
|
||||
_formatBody: =>
|
||||
return "" unless @props.message and @props.message.body
|
||||
|
||||
body = @props.message.body
|
||||
|
@ -274,29 +274,30 @@ MessageItem = React.createClass
|
|||
|
||||
body
|
||||
|
||||
_toggleQuotedText: ->
|
||||
_toggleQuotedText: =>
|
||||
@setState
|
||||
showQuotedText: !@state.showQuotedText
|
||||
|
||||
_toggleCollapsed: ->
|
||||
_toggleCollapsed: =>
|
||||
Actions.toggleMessageIdExpanded(@props.message.id)
|
||||
|
||||
_formatContacts: (contacts=[]) ->
|
||||
_formatContacts: (contacts=[]) =>
|
||||
|
||||
_attachmentComponents: ->
|
||||
_attachmentComponents: =>
|
||||
return [] unless @props.message.body
|
||||
|
||||
AttachmentComponent = @state.AttachmentComponent
|
||||
attachments = _.filter @props.message.files, (f) =>
|
||||
inBody = f.contentId? and @props.message.body.indexOf(f.contentId) > 0
|
||||
not inBody and f.filename.length > 0
|
||||
|
||||
attachments.map (file) =>
|
||||
<AttachmentComponent file={file} key={file.id} download={@state.downloads[file.id]}/>
|
||||
<InjectedComponent name="AttachmentComponent" file={file} key={file.id} download={@state.downloads[file.id]}/>
|
||||
|
||||
_isForwardedMessage: ->
|
||||
_isForwardedMessage: =>
|
||||
Utils.isForwardedMessage(@props.message)
|
||||
|
||||
_onDownloadStoreChange: ->
|
||||
_onDownloadStoreChange: =>
|
||||
@setState
|
||||
downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds())
|
||||
|
||||
module.exports = MessageItem
|
|
@ -1,21 +1,22 @@
|
|||
_ = require 'underscore-plus'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
MessageItem = require "./message-item"
|
||||
{Utils, Actions, MessageStore, ComponentRegistry} = require("inbox-exports")
|
||||
{Spinner, ResizableRegion, RetinaImg, RegisteredRegion} = require('ui-components')
|
||||
{Spinner,
|
||||
ResizableRegion,
|
||||
RetinaImg,
|
||||
InjectedComponentSet,
|
||||
InjectedComponent} = require('ui-components')
|
||||
|
||||
module.exports =
|
||||
MessageList = React.createClass
|
||||
mixins: [ComponentRegistry.Mixin]
|
||||
components: ['Composer']
|
||||
displayName: 'MessageList'
|
||||
class MessageList extends React.Component
|
||||
@displayName = 'MessageList'
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: ->
|
||||
@__onResize = _.bind @_onResize, @
|
||||
window.addEventListener("resize", @__onResize)
|
||||
componentDidMount: =>
|
||||
window.addEventListener("resize", @_onResize)
|
||||
@_unsubscribers = []
|
||||
@_unsubscribers.push MessageStore.listen @_onChange
|
||||
|
||||
|
@ -25,15 +26,15 @@ MessageList = React.createClass
|
|||
if not @state.loading
|
||||
@_prepareContentForDisplay()
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
unsubscribe() for unsubscribe in @_unsubscribers
|
||||
window.removeEventListener("resize", @__onResize)
|
||||
window.removeEventListener("resize", @_onResize)
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not Utils.isEqualReact(nextProps, @props) or
|
||||
not Utils.isEqualReact(nextState, @state)
|
||||
|
||||
componentDidUpdate: (prevProps, prevState) ->
|
||||
componentDidUpdate: (prevProps, prevState) =>
|
||||
return if @state.loading
|
||||
|
||||
if prevState.loading
|
||||
|
@ -47,29 +48,28 @@ MessageList = React.createClass
|
|||
else if newMessageIds.length > 0
|
||||
@_prepareContentForDisplay()
|
||||
|
||||
_newDraftIds: (prevState) ->
|
||||
_newDraftIds: (prevState) =>
|
||||
oldDraftIds = _.map(_.filter((prevState.messages ? []), (m) -> m.draft), (m) -> m.id)
|
||||
newDraftIds = _.map(_.filter((@state.messages ? []), (m) -> m.draft), (m) -> m.id)
|
||||
return _.difference(newDraftIds, oldDraftIds) ? []
|
||||
|
||||
_newMessageIds: (prevState) ->
|
||||
_newMessageIds: (prevState) =>
|
||||
oldMessageIds = _.map(_.reject((prevState.messages ? []), (m) -> m.draft), (m) -> m.id)
|
||||
newMessageIds = _.map(_.reject((@state.messages ? []), (m) -> m.draft), (m) -> m.id)
|
||||
return _.difference(newMessageIds, oldMessageIds) ? []
|
||||
|
||||
_focusDraft: (draftDOMNode) ->
|
||||
_focusDraft: (draftElement) =>
|
||||
# We need a 100ms delay so the DOM can finish painting the elements on
|
||||
# the page. The focus doesn't work for some reason while the paint is in
|
||||
# process.
|
||||
_.delay =>
|
||||
return unless @isMounted
|
||||
draftDOMNode.focus()
|
||||
draftElement.focus()
|
||||
,100
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
return <div></div> if not @state.currentThread?
|
||||
|
||||
wrapClass = React.addons.classSet
|
||||
wrapClass = classNames
|
||||
"messages-wrap": true
|
||||
"ready": @state.ready
|
||||
|
||||
|
@ -79,11 +79,11 @@ MessageList = React.createClass
|
|||
onScroll={_.debounce(@_cacheScrollPos, 100)}
|
||||
ref="messageWrap">
|
||||
|
||||
<RegisteredRegion className="message-list-notification-bars"
|
||||
<InjectedComponentSet className="message-list-notification-bars"
|
||||
location="MessageListNotificationBar"
|
||||
|
||||
thread={@state.currentThread}/>
|
||||
<RegisteredRegion className="message-list-headers"
|
||||
<InjectedComponentSet className="message-list-headers"
|
||||
location="MessageListHeaders"
|
||||
thread={@state.currentThread}/>
|
||||
|
||||
|
@ -93,7 +93,7 @@ MessageList = React.createClass
|
|||
<Spinner visible={!@state.ready} />
|
||||
</div>
|
||||
|
||||
_renderReplyArea: ->
|
||||
_renderReplyArea: =>
|
||||
if @_hasReplyArea()
|
||||
<div className="footer-reply-area-wrap" onClick={@_onClickReplyArea}>
|
||||
<div className="footer-reply-area">
|
||||
|
@ -102,17 +102,17 @@ MessageList = React.createClass
|
|||
</div>
|
||||
else return <div></div>
|
||||
|
||||
_hasReplyArea: ->
|
||||
_hasReplyArea: =>
|
||||
not _.last(@state.messages)?.draft
|
||||
|
||||
# Either returns "reply" or "reply-all"
|
||||
_replyType: ->
|
||||
_replyType: =>
|
||||
lastMsg = _.last(_.filter((@state.messages ? []), (m) -> not m.draft))
|
||||
if lastMsg?.cc.length is 0 and lastMsg?.to.length is 1
|
||||
return "reply"
|
||||
else return "reply-all"
|
||||
|
||||
_onClickReplyArea: ->
|
||||
_onClickReplyArea: =>
|
||||
return unless @state.currentThread?.id
|
||||
if @_replyType() is "reply-all"
|
||||
Actions.composeReplyAll(threadId: @state.currentThread.id)
|
||||
|
@ -122,15 +122,13 @@ MessageList = React.createClass
|
|||
# There may be a lot of iframes to load which may take an indeterminate
|
||||
# amount of time. As long as there is more content being painted onto
|
||||
# the page and our height is changing, keep waiting. Then scroll to message.
|
||||
scrollToMessage: (msgDOMNode, done, location="top", stability=5) ->
|
||||
scrollToMessage: (msgDOMNode, done, location="top", stability=5) =>
|
||||
return done() unless msgDOMNode?
|
||||
|
||||
messageWrap = @refs.messageWrap?.getDOMNode()
|
||||
messageWrap = React.findDOMNode(@refs.messageWrap)
|
||||
lastHeight = -1
|
||||
stableCount = 0
|
||||
scrollIfSettled = =>
|
||||
return unless @isMounted()
|
||||
|
||||
messageWrapHeight = messageWrap.getBoundingClientRect().height
|
||||
if messageWrapHeight isnt lastHeight
|
||||
lastHeight = messageWrapHeight
|
||||
|
@ -150,8 +148,7 @@ MessageList = React.createClass
|
|||
|
||||
scrollIfSettled()
|
||||
|
||||
_messageComponents: ->
|
||||
ComposerItem = @state.Composer
|
||||
_messageComponents: =>
|
||||
appliedInitialFocus = false
|
||||
components = []
|
||||
|
||||
|
@ -164,7 +161,7 @@ MessageList = React.createClass
|
|||
(idx is @state.messages.length - 1 and idx > 0))
|
||||
appliedInitialFocus ||= initialFocus
|
||||
|
||||
className = React.addons.classSet
|
||||
className = classNames
|
||||
"message-item-wrap": true
|
||||
"initial-focus": initialFocus
|
||||
"unread": message.unread
|
||||
|
@ -172,7 +169,8 @@ MessageList = React.createClass
|
|||
"collapsed": collapsed
|
||||
|
||||
if message.draft
|
||||
components.push <ComposerItem mode="inline"
|
||||
components.push <InjectedComponent name="Composer"
|
||||
mode="inline"
|
||||
ref="composerItem-#{message.id}"
|
||||
key={@state.messageLocalIds[message.id]}
|
||||
localId={@state.messageLocalIds[message.id]}
|
||||
|
@ -198,17 +196,16 @@ MessageList = React.createClass
|
|||
|
||||
# Some child components (like the compser) might request that we scroll
|
||||
# to the bottom of the component.
|
||||
_onRequestScrollToComposer: ({messageId, location}={}) ->
|
||||
return unless @isMounted()
|
||||
_onRequestScrollToComposer: ({messageId, location}={}) =>
|
||||
done = ->
|
||||
location ?= "bottom"
|
||||
composer = @refs["composerItem-#{messageId}"]?.getDOMNode()
|
||||
composer = React.findDOMNode(@refs["composerItem-#{messageId}"])
|
||||
@scrollToMessage(composer, done, location, 1)
|
||||
|
||||
_onChange: ->
|
||||
_onChange: =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: ->
|
||||
_getStateFromStores: =>
|
||||
messages: (MessageStore.items() ? [])
|
||||
messageLocalIds: MessageStore.itemLocalIds()
|
||||
messagesExpandedState: MessageStore.itemsExpandedState()
|
||||
|
@ -216,16 +213,15 @@ MessageList = React.createClass
|
|||
loading: MessageStore.itemsLoading()
|
||||
ready: if MessageStore.itemsLoading() then false else @state?.ready ? false
|
||||
|
||||
_prepareContentForDisplay: ->
|
||||
_prepareContentForDisplay: =>
|
||||
_.delay =>
|
||||
return unless @isMounted()
|
||||
focusedMessage = @getDOMNode().querySelector(".initial-focus")
|
||||
focusedMessage = React.findDOMNode(@).querySelector(".initial-focus")
|
||||
@scrollToMessage focusedMessage, =>
|
||||
@setState(ready: true)
|
||||
@_cacheScrollPos()
|
||||
, 100
|
||||
|
||||
_threadParticipants: ->
|
||||
_threadParticipants: =>
|
||||
# We calculate the list of participants instead of grabbing it from
|
||||
# `@state.currentThread.participants` because it makes it easier to
|
||||
# test, is a better source of ground truth, and saves us from more
|
||||
|
@ -238,24 +234,25 @@ MessageList = React.createClass
|
|||
participants[contact.email] = contact
|
||||
return _.values(participants)
|
||||
|
||||
_onResize: (event) ->
|
||||
return unless @isMounted()
|
||||
_onResize: (event) =>
|
||||
@_scrollToBottom() if @_wasAtBottom()
|
||||
@_cacheScrollPos()
|
||||
|
||||
_scrollToBottom: ->
|
||||
messageWrap = @refs.messageWrap?.getDOMNode()
|
||||
_scrollToBottom: =>
|
||||
messageWrap = React.findDOMNode(@refs.messageWrap)
|
||||
messageWrap.scrollTop = messageWrap.scrollHeight
|
||||
|
||||
_cacheScrollPos: ->
|
||||
messageWrap = @refs.messageWrap?.getDOMNode()
|
||||
_cacheScrollPos: =>
|
||||
messageWrap = React.findDOMNode(@refs.messageWrap)
|
||||
return unless messageWrap
|
||||
@_lastScrollTop = messageWrap.scrollTop
|
||||
@_lastHeight = messageWrap.getBoundingClientRect().height
|
||||
@_lastScrollHeight = messageWrap.scrollHeight
|
||||
|
||||
_wasAtBottom: ->
|
||||
_wasAtBottom: =>
|
||||
(@_lastScrollTop + @_lastHeight) >= @_lastScrollHeight
|
||||
|
||||
MessageList.minWidth = 500
|
||||
MessageList.maxWidth = 900
|
||||
|
||||
module.exports = MessageList
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
_ = require 'underscore-plus'
|
||||
React = require "react"
|
||||
classNames = require 'classnames'
|
||||
|
||||
module.exports =
|
||||
MessageParticipants = React.createClass
|
||||
displayName: 'MessageParticipants'
|
||||
|
||||
render: ->
|
||||
classSet = React.addons.classSet
|
||||
classSet = classNames
|
||||
"participants": true
|
||||
"message-participants": true
|
||||
"collapsed": not @props.isDetailed
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
_ = require 'underscore-plus'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
{Actions, Utils, FocusedContentStore, WorkspaceStore} = require 'inbox-exports'
|
||||
{RetinaImg} = require 'ui-components'
|
||||
|
||||
|
@ -65,7 +66,7 @@ MessageToolbarItems = React.createClass
|
|||
threadIsSelected: FocusedContentStore.focusedId('thread')?
|
||||
|
||||
render: ->
|
||||
classes = React.addons.classSet
|
||||
classes = classNames
|
||||
"message-toolbar-items": true
|
||||
"hidden": !@state.threadIsSelected
|
||||
|
||||
|
|
|
@ -3,26 +3,26 @@ React = require "react"
|
|||
|
||||
{Actions, FocusedContactsStore} = require("inbox-exports")
|
||||
|
||||
module.exports =
|
||||
SidebarThreadParticipants = React.createClass
|
||||
displayName: 'SidebarThreadParticipants'
|
||||
class SidebarThreadParticipants extends React.Component
|
||||
@displayName = 'SidebarThreadParticipants'
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
@_getStateFromStores()
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@unsubscribe = FocusedContactsStore.listen @_onChange
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe()
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
<div className="sidebar-thread-participants">
|
||||
<h2 className="sidebar-h2">Thread Participants</h2>
|
||||
{@_renderSortedContacts()}
|
||||
</div>
|
||||
|
||||
_renderSortedContacts: ->
|
||||
_renderSortedContacts: =>
|
||||
contacts = []
|
||||
@state.sortedContacts.forEach (contact) =>
|
||||
if contact is @state.focusedContact
|
||||
|
@ -31,18 +31,21 @@ SidebarThreadParticipants = React.createClass
|
|||
contacts.push(
|
||||
<div className="other-contact #{selected}"
|
||||
onClick={=> @_onSelectContact(contact)}
|
||||
key={contact.id}>
|
||||
key={contact.email+contact.name}>
|
||||
{contact.name}
|
||||
</div>
|
||||
)
|
||||
return contacts
|
||||
|
||||
_onSelectContact: (contact) ->
|
||||
_onSelectContact: (contact) =>
|
||||
Actions.focusContact(contact)
|
||||
|
||||
_onChange: ->
|
||||
_onChange: =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: ->
|
||||
_getStateFromStores: =>
|
||||
sortedContacts: FocusedContactsStore.sortedContacts()
|
||||
focusedContact: FocusedContactsStore.focusedContact()
|
||||
|
||||
|
||||
module.exports = SidebarThreadParticipants
|
|
@ -134,7 +134,7 @@ describe "MessageItem", ->
|
|||
# expect( -> ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)).toThrow()
|
||||
#
|
||||
# it "should have the `collapsed` class", ->
|
||||
# expect(@component.getDOMNode().className.indexOf('collapsed') >= 0).toBe(true)
|
||||
# expect(React.findDOMNode(@component).className.indexOf('collapsed') >= 0).toBe(true)
|
||||
|
||||
describe "when displaying detailed headers", ->
|
||||
beforeEach ->
|
||||
|
@ -159,7 +159,7 @@ describe "MessageItem", ->
|
|||
expect(frame).toBeDefined()
|
||||
|
||||
it "should not have the `collapsed` class", ->
|
||||
expect(@component.getDOMNode().className.indexOf('collapsed') >= 0).toBe(false)
|
||||
expect(React.findDOMNode(@component).className.indexOf('collapsed') >= 0).toBe(false)
|
||||
|
||||
describe "when the message contains attachments", ->
|
||||
beforeEach ->
|
||||
|
@ -236,14 +236,14 @@ describe "MessageItem", ->
|
|||
it "should show the `show quoted text` toggle in the off state", ->
|
||||
@createComponent()
|
||||
toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
||||
expect(toggle.getDOMNode().className.indexOf('show-quoted-text')).toBe(-1)
|
||||
expect(React.findDOMNode(toggle).className.indexOf('show-quoted-text')).toBe(-1)
|
||||
|
||||
it "should have the `no quoted text` class if there is no quoted text in the message", ->
|
||||
spyOn(Utils, 'quotedTextIndex').andCallFake -> -1
|
||||
|
||||
@createComponent()
|
||||
toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
||||
expect(toggle.getDOMNode().className.indexOf('no-quoted-text')).not.toBe(-1)
|
||||
expect(React.findDOMNode(toggle).className.indexOf('no-quoted-text')).not.toBe(-1)
|
||||
|
||||
it "should be initialized to true if the message contains `Forwarded`...", ->
|
||||
@message.body = """
|
||||
|
@ -284,7 +284,7 @@ describe "MessageItem", ->
|
|||
|
||||
it "should show the `show quoted text` toggle in the on state", ->
|
||||
toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')
|
||||
expect(toggle.getDOMNode().className.indexOf('show-quoted-text') > 0).toBe(true)
|
||||
expect(React.findDOMNode(toggle).className.indexOf('show-quoted-text') > 0).toBe(true)
|
||||
|
||||
it "should pass the value into the EmailFrame", ->
|
||||
frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)
|
||||
|
|
|
@ -197,7 +197,7 @@ describe "MessageList", ->
|
|||
false
|
||||
|
||||
@message_list = TestUtils.renderIntoDocument(<MessageList />)
|
||||
@message_list_node = @message_list.getDOMNode()
|
||||
@message_list_node = React.findDOMNode(@message_list)
|
||||
|
||||
it "renders into the document", ->
|
||||
expect(TestUtils.isCompositeComponentWithType(@message_list,
|
||||
|
@ -241,11 +241,8 @@ describe "MessageList", ->
|
|||
@message_list.setState
|
||||
messages: msgs.concat(draftMessages)
|
||||
|
||||
items = TestUtils.scryRenderedComponentsWithType(@message_list,
|
||||
ComposerItem)
|
||||
|
||||
expect(items.length).toBe 1
|
||||
expect(@message_list._focusDraft).toHaveBeenCalledWith(items[0])
|
||||
expect(@message_list._focusDraft).toHaveBeenCalled()
|
||||
expect(@message_list._focusDraft.mostRecentCall.args[0].props.localId).toEqual(draftMessages[0].id)
|
||||
|
||||
it "doesn't focus if we're just navigating through messages", ->
|
||||
spyOn(@message_list, "scrollToMessage")
|
||||
|
@ -267,7 +264,6 @@ describe "MessageList", ->
|
|||
items = TestUtils.scryRenderedComponentsWithType(@message_list,
|
||||
ComposerItem)
|
||||
expect(@message_list.state.messages.length).toBe 6
|
||||
expect(@message_list.state.Composer).toEqual ComposerItem
|
||||
expect(items.length).toBe 1
|
||||
|
||||
expect(items.length).toBe 1
|
||||
|
|
|
@ -71,7 +71,7 @@ describe "MessageParticipants", ->
|
|||
|
||||
it "uses short names", ->
|
||||
to = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, "to-contact")
|
||||
expect(to.getDOMNode().innerHTML).toBe "User"
|
||||
expect(React.findDOMNode(to).innerHTML).toBe "User"
|
||||
|
||||
describe "when expanded", ->
|
||||
beforeEach ->
|
||||
|
@ -90,7 +90,7 @@ describe "MessageParticipants", ->
|
|||
|
||||
it "uses full names", ->
|
||||
to = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, "to-contact")
|
||||
expect(to.getDOMNode().innerText.trim()).toEqual "User Two <user2@nilas.com>"
|
||||
expect(React.findDOMNode(to).innerText.trim()).toEqual "User Two <user2@nilas.com>"
|
||||
|
||||
|
||||
# TODO: We no longer display "to everyone"
|
||||
|
|
|
@ -16,10 +16,10 @@ describe "MessageToolbarItems", ->
|
|||
|
||||
it "archives in split mode", ->
|
||||
spyOn(WorkspaceStore, "layoutMode").andReturn "split"
|
||||
ReactTestUtils.Simulate.click(@archiveButton.getDOMNode())
|
||||
ReactTestUtils.Simulate.click(React.findDOMNode(@archiveButton))
|
||||
expect(Actions.archive).toHaveBeenCalled()
|
||||
|
||||
it "archives in list mode", ->
|
||||
spyOn(WorkspaceStore, "layoutMode").andReturn "list"
|
||||
ReactTestUtils.Simulate.click(@archiveButton.getDOMNode())
|
||||
ReactTestUtils.Simulate.click(React.findDOMNode(@archiveButton))
|
||||
expect(Actions.archive).toHaveBeenCalled()
|
||||
|
|
|
@ -23,6 +23,7 @@ TemplatePicker = React.createClass
|
|||
<RetinaImg name="toolbar-templates.png"/>
|
||||
<RetinaImg name="toolbar-chevron.png"/>
|
||||
</button>
|
||||
|
||||
headerComponents = [
|
||||
<input type="text"
|
||||
tabIndex="1"
|
||||
|
|
|
@ -9,28 +9,28 @@ querystring = require 'querystring'
|
|||
module.exports =
|
||||
ContainerView = React.createClass
|
||||
|
||||
getInitialState: ->
|
||||
getInitialState: =>
|
||||
@getStateFromStore()
|
||||
|
||||
getStateFromStore: ->
|
||||
getStateFromStore: =>
|
||||
page: OnboardingStore.page()
|
||||
error: OnboardingStore.error()
|
||||
environment: OnboardingStore.environment()
|
||||
connectType: OnboardingStore.connectType()
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@unsubscribe = OnboardingStore.listen(@_onStateChanged, @)
|
||||
|
||||
# It's important that every React class explicitly stops listening to
|
||||
# atom events before it unmounts. Thank you event-kit
|
||||
# This can be fixed via a Reflux mixin
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe() if @unsubscribe
|
||||
|
||||
componentDidUpdate: ->
|
||||
webview = this.refs['connect-iframe']
|
||||
componentDidUpdate: =>
|
||||
webview = @refs['connect-iframe']
|
||||
if webview
|
||||
node = webview.getDOMNode()
|
||||
node = React.findDOMNode(webview)
|
||||
if node.hasListeners is undefined
|
||||
node.addEventListener 'did-start-loading', (e) ->
|
||||
if node.hasMobileUserAgent is undefined
|
||||
|
@ -46,13 +46,13 @@ ContainerView = React.createClass
|
|||
if node.getUrl().indexOf('cancelled') != -1
|
||||
OnboardingActions.moveToPreviousPage()
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
<ReactCSSTransitionGroup transitionName="page">
|
||||
{@_pageComponent()}
|
||||
<div className="dragRegion" style={"WebkitAppRegion": "drag", position: 'absolute', top:0, left:40, right:0, height: 20, zIndex:100}></div>
|
||||
</ReactCSSTransitionGroup>
|
||||
|
||||
_pageComponent: ->
|
||||
_pageComponent: =>
|
||||
if @state.error
|
||||
alert = <div className="alert alert-danger" role="alert">{@state.error}</div>
|
||||
else
|
||||
|
@ -99,8 +99,8 @@ ContainerView = React.createClass
|
|||
{
|
||||
React.createElement('webview',{
|
||||
"ref": "connect-iframe",
|
||||
"key": this.state.page,
|
||||
"src": this._connectWebViewURL()
|
||||
"key": @state.page,
|
||||
"src": @_connectWebViewURL()
|
||||
})
|
||||
}
|
||||
<div className="back" onClick={@_fireMoveToPrevPage}>
|
||||
|
@ -118,7 +118,7 @@ ContainerView = React.createClass
|
|||
</div>
|
||||
</div>
|
||||
|
||||
_environmentComponent: ->
|
||||
_environmentComponent: =>
|
||||
return [] unless atom.inDevMode()
|
||||
<div className="environment-selector">
|
||||
<select value={@state.environment} onChange={@_fireSetEnvironment}>
|
||||
|
@ -128,35 +128,35 @@ ContainerView = React.createClass
|
|||
</select>
|
||||
</div>
|
||||
|
||||
_connectWebViewURL: ->
|
||||
_connectWebViewURL: =>
|
||||
EdgehillAPI.urlForConnecting(@state.connectType, @state.email)
|
||||
|
||||
_onStateChanged: ->
|
||||
_onStateChanged: =>
|
||||
@setState(@getStateFromStore())
|
||||
|
||||
_onValueChange: (event) ->
|
||||
_onValueChange: (event) =>
|
||||
changes = {}
|
||||
changes[event.target.id] = event.target.value
|
||||
@setState(changes)
|
||||
|
||||
_fireDismiss: ->
|
||||
_fireDismiss: =>
|
||||
atom.close()
|
||||
|
||||
_fireQuit: ->
|
||||
_fireQuit: =>
|
||||
require('ipc').send('command', 'application:quit')
|
||||
|
||||
_fireSetEnvironment: (event) ->
|
||||
_fireSetEnvironment: (event) =>
|
||||
OnboardingActions.setEnvironment(event.target.value)
|
||||
|
||||
_fireStart: (e) ->
|
||||
_fireStart: (e) =>
|
||||
OnboardingActions.startConnect('inbox')
|
||||
|
||||
_fireAuthAccount: (service) ->
|
||||
_fireAuthAccount: (service) =>
|
||||
OnboardingActions.startConnect(service)
|
||||
|
||||
_fireMoveToPage: (page) ->
|
||||
_fireMoveToPage: (page) =>
|
||||
OnboardingActions.moveToPage(page)
|
||||
|
||||
_fireMoveToPrevPage: ->
|
||||
_fireMoveToPrevPage: =>
|
||||
OnboardingActions.moveToPreviousPage()
|
||||
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
{Actions} = require 'inbox-exports'
|
||||
{Menu, RetinaImg} = require 'ui-components'
|
||||
SearchSuggestionStore = require './search-suggestion-store'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
SearchBar = React.createClass
|
||||
displayName: 'SearchBar'
|
||||
|
||||
getInitialState: ->
|
||||
query: ""
|
||||
focused: false
|
||||
suggestions: []
|
||||
committedQuery: ""
|
||||
class SearchBar extends React.Component
|
||||
@displayName = 'SearchBar'
|
||||
|
||||
componentDidMount: ->
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
query: ""
|
||||
focused: false
|
||||
suggestions: []
|
||||
committedQuery: ""
|
||||
|
||||
componentDidMount: =>
|
||||
@unsubscribe = SearchSuggestionStore.listen @_onStoreChange
|
||||
@body_unsubscriber = atom.commands.add 'body', {
|
||||
'application:focus-search': @_onFocusSearch
|
||||
|
@ -26,13 +28,13 @@ SearchBar = React.createClass
|
|||
# It's important that every React class explicitly stops listening to
|
||||
# atom events before it unmounts. Thank you event-kit
|
||||
# This can be fixed via a Reflux mixin
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe()
|
||||
@body_unsubscriber.dispose()
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
inputValue = @_queryToString(@state.query)
|
||||
inputClass = React.addons.classSet
|
||||
inputClass = classNames
|
||||
'input-bordered': true
|
||||
'empty': inputValue.length is 0
|
||||
|
||||
|
@ -56,7 +58,7 @@ SearchBar = React.createClass
|
|||
onClick={@_onClearSearch}><i className="fa fa-remove"></i></div>
|
||||
]
|
||||
|
||||
itemContentFunc = (item) ->
|
||||
itemContentFunc = (item) =>
|
||||
if item.divider
|
||||
<Menu.Item divider={item.divider} />
|
||||
else if item.contact
|
||||
|
@ -75,19 +77,18 @@ SearchBar = React.createClass
|
|||
/>
|
||||
</div>
|
||||
|
||||
_onFocusSearch: ->
|
||||
return unless @isMounted()
|
||||
@refs.searchInput.getDOMNode().focus()
|
||||
_onFocusSearch: =>
|
||||
React.findDOMNode(@refs.searchInput).focus()
|
||||
|
||||
_containerClasses: ->
|
||||
React.addons.classSet
|
||||
_containerClasses: =>
|
||||
classNames
|
||||
'focused': @state.focused
|
||||
'showing-query': @state.query?.length > 0
|
||||
'committed-query': @state.committedQuery.length > 0
|
||||
'search-container': true
|
||||
'showing-suggestions': @state.suggestions?.length > 0
|
||||
|
||||
_queryToString: (query) ->
|
||||
_queryToString: (query) =>
|
||||
return "" unless query instanceof Array
|
||||
str = ""
|
||||
for term in query
|
||||
|
@ -97,7 +98,7 @@ SearchBar = React.createClass
|
|||
else
|
||||
str += "#{key}:#{val}"
|
||||
|
||||
_stringToQuery: (str) ->
|
||||
_stringToQuery: (str) =>
|
||||
return [] unless str
|
||||
|
||||
# note: right now this only works if there's one term. In the future,
|
||||
|
@ -110,39 +111,41 @@ SearchBar = React.createClass
|
|||
term["all"] = a
|
||||
[term]
|
||||
|
||||
_onValueChange: (event) ->
|
||||
_onValueChange: (event) =>
|
||||
Actions.searchQueryChanged(@_stringToQuery(event.target.value))
|
||||
if (event.target.value is '')
|
||||
@_onClearSearch()
|
||||
|
||||
_onSelectSuggestion: (item) ->
|
||||
_onSelectSuggestion: (item) =>
|
||||
Actions.searchQueryCommitted(item.value)
|
||||
|
||||
_onClearSearch: (event) ->
|
||||
_onClearSearch: (event) =>
|
||||
Actions.searchQueryCommitted('')
|
||||
|
||||
_clearAndBlur: ->
|
||||
_clearAndBlur: =>
|
||||
@_onClearSearch()
|
||||
@refs.searchInput?.getDOMNode().blur()
|
||||
React.findDOMNode(@refs.searchInput)?.blur()
|
||||
|
||||
_onFocus: ->
|
||||
_onFocus: =>
|
||||
@setState focused: true
|
||||
|
||||
_onBlur: ->
|
||||
_onBlur: =>
|
||||
# Don't immediately hide the menu when the text input is blurred,
|
||||
# because the user might have clicked an item in the menu. Wait to
|
||||
# handle the touch event, then dismiss the menu.
|
||||
setTimeout =>
|
||||
Actions.searchBlurred()
|
||||
if @isMounted()
|
||||
@setState(focused: false)
|
||||
@setState(focused: false)
|
||||
, 150
|
||||
|
||||
_doSearch: ->
|
||||
_doSearch: =>
|
||||
Actions.searchQueryCommitted(@state.query)
|
||||
|
||||
_onStoreChange: ->
|
||||
_onStoreChange: =>
|
||||
@setState
|
||||
query: SearchSuggestionStore.query()
|
||||
suggestions: SearchSuggestionStore.suggestions()
|
||||
committedQuery: SearchSuggestionStore.committedQuery()
|
||||
|
||||
|
||||
module.exports = SearchBar
|
|
@ -48,13 +48,9 @@
|
|||
font-size: @font-size-small;
|
||||
text-transform: uppercase;
|
||||
margin-top: 10px;
|
||||
padding: 4px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: 4px;
|
||||
padding-left: 15px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
{Actions} = require 'inbox-exports'
|
||||
{Menu, RetinaImg} = require 'ui-components'
|
||||
SearchSuggestionStore = require './search-suggestion-store'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
SearchBar = React.createClass
|
||||
displayName: 'SearchBar'
|
||||
class SearchBar extends React.Component
|
||||
@displayName = 'SearchBar'
|
||||
|
||||
getInitialState: ->
|
||||
query: ""
|
||||
focused: false
|
||||
suggestions: []
|
||||
committedQuery: ""
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
query: ""
|
||||
focused: false
|
||||
suggestions: []
|
||||
committedQuery: ""
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@unsubscribe = SearchSuggestionStore.listen @_onStoreChange
|
||||
@body_unsubscriber = atom.commands.add 'body', {
|
||||
'application:focus-search': @_onFocusSearch
|
||||
|
@ -26,13 +27,13 @@ SearchBar = React.createClass
|
|||
# It's important that every React class explicitly stops listening to
|
||||
# atom events before it unmounts. Thank you event-kit
|
||||
# This can be fixed via a Reflux mixin
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe()
|
||||
@body_unsubscriber.dispose()
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
inputValue = @_queryToString(@state.query)
|
||||
inputClass = React.addons.classSet
|
||||
inputClass = classNames
|
||||
'input-bordered': true
|
||||
'empty': inputValue.length is 0
|
||||
|
||||
|
@ -56,7 +57,7 @@ SearchBar = React.createClass
|
|||
onClick={@_onClearSearch}><i className="fa fa-remove"></i></div>
|
||||
]
|
||||
|
||||
itemContentFunc = (item) ->
|
||||
itemContentFunc = (item) =>
|
||||
if item.divider
|
||||
<Menu.Item divider={item.divider} />
|
||||
else if item.contact
|
||||
|
@ -76,19 +77,19 @@ SearchBar = React.createClass
|
|||
/>
|
||||
</div>
|
||||
|
||||
_onFocusSearch: ->
|
||||
_onFocusSearch: =>
|
||||
return unless @isMounted()
|
||||
@refs.searchInput.getDOMNode().focus()
|
||||
@refs.searchInput.findDOMNode().focus()
|
||||
|
||||
_containerClasses: ->
|
||||
React.addons.classSet
|
||||
_containerClasses: =>
|
||||
classNames
|
||||
'focused': @state.focused
|
||||
'showing-query': @state.query?.length > 0
|
||||
'committed-query': @state.committedQuery.length > 0
|
||||
'search-container': true
|
||||
'showing-suggestions': @state.suggestions?.length > 0
|
||||
|
||||
_queryToString: (query) ->
|
||||
_queryToString: (query) =>
|
||||
return "" unless query instanceof Array
|
||||
str = ""
|
||||
for term in query
|
||||
|
@ -98,7 +99,7 @@ SearchBar = React.createClass
|
|||
else
|
||||
str += "#{key}:#{val}"
|
||||
|
||||
_stringToQuery: (str) ->
|
||||
_stringToQuery: (str) =>
|
||||
return [] unless str
|
||||
|
||||
# note: right now this only works if there's one term. In the future,
|
||||
|
@ -111,25 +112,25 @@ SearchBar = React.createClass
|
|||
term["all"] = a
|
||||
[term]
|
||||
|
||||
_onValueChange: (event) ->
|
||||
_onValueChange: (event) =>
|
||||
Actions.searchQueryChanged(@_stringToQuery(event.target.value))
|
||||
if (event.target.value is '')
|
||||
@_onClearSearch()
|
||||
|
||||
_onSelectSuggestion: (item) ->
|
||||
_onSelectSuggestion: (item) =>
|
||||
Actions.searchQueryCommitted(item.value)
|
||||
|
||||
_onClearSearch: (event) ->
|
||||
_onClearSearch: (event) =>
|
||||
Actions.searchQueryCommitted('')
|
||||
|
||||
_clearAndBlur: ->
|
||||
_clearAndBlur: =>
|
||||
@_onClearSearch()
|
||||
@refs.searchInput?.getDOMNode().blur()
|
||||
@refs.searchInput?.findDOMNode().blur()
|
||||
|
||||
_onFocus: ->
|
||||
_onFocus: =>
|
||||
@setState focused: true
|
||||
|
||||
_onBlur: ->
|
||||
_onBlur: =>
|
||||
# Don't immediately hide the menu when the text input is blurred,
|
||||
# because the user might have clicked an item in the menu. Wait to
|
||||
# handle the touch event, then dismiss the menu.
|
||||
|
@ -139,11 +140,14 @@ SearchBar = React.createClass
|
|||
@setState focused: false
|
||||
, 150
|
||||
|
||||
_doSearch: ->
|
||||
_doSearch: =>
|
||||
Actions.searchQueryCommitted(@state.query)
|
||||
|
||||
_onStoreChange: ->
|
||||
_onStoreChange: =>
|
||||
@setState
|
||||
query: SearchSuggestionStore.query()
|
||||
suggestions: SearchSuggestionStore.suggestions()
|
||||
committedQuery: SearchSuggestionStore.committedQuery()
|
||||
|
||||
|
||||
module.exports = SearchBar
|
|
@ -1,4 +1,5 @@
|
|||
React = require "react/addons"
|
||||
classNames = require 'classnames'
|
||||
ThreadListStore = require './thread-list-store'
|
||||
{RetinaImg} = require 'ui-components'
|
||||
{Actions, AddRemoveTagsTask, FocusedContentStore} = require "inbox-exports"
|
||||
|
@ -56,7 +57,7 @@ DownButton = React.createClass
|
|||
</div>
|
||||
|
||||
_classSet: ->
|
||||
React.addons.classSet
|
||||
classNames
|
||||
"message-toolbar-arrow": true
|
||||
"down": true
|
||||
"disabled": @state.disabled
|
||||
|
@ -77,7 +78,7 @@ UpButton = React.createClass
|
|||
</div>
|
||||
|
||||
_classSet: ->
|
||||
React.addons.classSet
|
||||
classNames
|
||||
"message-toolbar-arrow": true
|
||||
"up": true
|
||||
"disabled": @state.disabled
|
||||
|
|
|
@ -14,6 +14,9 @@ _ = require 'underscore-plus'
|
|||
Thread,
|
||||
Message} = require 'inbox-exports'
|
||||
|
||||
# Public: A mutable text container with undo/redo support and the ability to
|
||||
# annotate logical regions in the text.
|
||||
#
|
||||
module.exports =
|
||||
ThreadListStore = Reflux.createStore
|
||||
init: ->
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
_ = require 'underscore-plus'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
{ListTabular, MultiselectList} = require 'ui-components'
|
||||
{timestamp, subject} = require './formatting-utils'
|
||||
{Actions,
|
||||
|
@ -78,7 +79,7 @@ ThreadList = React.createClass
|
|||
'application:reply-all': @_onReplyAll
|
||||
'application:forward': @_onForward
|
||||
@itemPropsProvider = (item) ->
|
||||
className: React.addons.classSet
|
||||
className: classNames
|
||||
'unread': item.isUnread()
|
||||
|
||||
render: ->
|
||||
|
|
|
@ -291,7 +291,7 @@ describe "ThreadList", ->
|
|||
ThreadStore._view = view
|
||||
ThreadStore._focusedId = null
|
||||
ThreadStore.trigger(ThreadStore)
|
||||
@thread_list_node = @thread_list.getDOMNode()
|
||||
@thread_list_node = @thread_list.findDOMNode()
|
||||
spyOn(@thread_list, "setState").andCallThrough()
|
||||
|
||||
it "renders all of the thread list items", ->
|
||||
|
@ -331,7 +331,7 @@ describe "ThreadList", ->
|
|||
# ThreadStore._items = test_threads()
|
||||
# ThreadStore._focusedId = null
|
||||
# ThreadStore.trigger()
|
||||
# @thread_list_node = @thread_list.getDOMNode()
|
||||
# @thread_list_node = @thread_list.findDOMNode()
|
||||
|
||||
# it "renders all of the thread list items", ->
|
||||
# items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list,
|
||||
|
@ -402,7 +402,7 @@ describe "ThreadList", ->
|
|||
|
||||
# it "fires the appropriate Action on click", ->
|
||||
# spyOn(Actions, "selectThreadId")
|
||||
# ReactTestUtils.Simulate.click @thread_list_item.getDOMNode()
|
||||
# ReactTestUtils.Simulate.click @thread_list_item.findDOMNode()
|
||||
# expect(Actions.focusThreadId).toHaveBeenCalledWith("111")
|
||||
|
||||
# it "sets the selected state on the thread item", ->
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"coffee-react": "^2.0.0",
|
||||
"coffee-script": "1.9.0",
|
||||
"coffeestack": "0.8.0",
|
||||
"classnames": "1.2.1",
|
||||
"color": "^0.7.3",
|
||||
"delegato": "^1",
|
||||
"emissary": "^1.3.1",
|
||||
|
@ -54,7 +55,7 @@
|
|||
"property-accessors": "^1",
|
||||
"q": "^1.0.1",
|
||||
"raven": "0.7.2",
|
||||
"react": "^0.12.2",
|
||||
"react": "^0.13.2",
|
||||
"react-atom-fork": "^0.11.5",
|
||||
"react-dnd": "^0.9.8",
|
||||
"reactionary-atom-fork": "^1.0.0",
|
||||
|
|
|
@ -148,7 +148,7 @@ describe 'TokenizingTextField', ->
|
|||
ReactTestUtils.Simulate.change(@renderedInput, {target: {value: 'abc'}})
|
||||
components = ReactTestUtils.scryRenderedComponentsWithType(@renderedField, Menu.Item)
|
||||
menuItem = components[0]
|
||||
ReactTestUtils.Simulate.mouseDown(menuItem.getDOMNode())
|
||||
ReactTestUtils.Simulate.mouseDown(React.findDOMNode(menuItem))
|
||||
expect(@propAdd).toHaveBeenCalledWith([participant4])
|
||||
|
||||
['enter', ','].forEach (key) ->
|
||||
|
|
|
@ -116,9 +116,11 @@ describe "DatabaseView", ->
|
|||
|
||||
describe "invalidateMetadataFor", ->
|
||||
it "should clear cached metadata for just the items whose ids are provided", ->
|
||||
expect(@view._pages[0].metadata).toEqual({'a': 'a-metadata', 'b': 'b-metadata', 'c': 'c-metadata'})
|
||||
expect(@view._pages[1].metadata).toEqual({'d': 'd-metadata', 'e': 'e-metadata', 'f': 'f-metadata'})
|
||||
@view.invalidateMetadataFor(['b', 'e'])
|
||||
expect(@view._pages[0].metadata).toEqual({'a': 'a-metadata', 'c': 'c-metadata'})
|
||||
expect(@view._pages[1].metadata).toEqual({'d': 'd-metadata', 'f': 'f-metadata'})
|
||||
expect(@view._pages[0].metadata['b']).toBe(undefined)
|
||||
expect(@view._pages[1].metadata['e']).toBe(undefined)
|
||||
|
||||
it "should re-retrieve page metadata for only impacted pages", ->
|
||||
spyOn(@view, 'retrievePageMetadata')
|
||||
|
|
|
@ -13,8 +13,7 @@ getViewsByName = (components) ->
|
|||
requested ?= registered
|
||||
state[requested] = ComponentRegistry.findViewByName registered
|
||||
if not state[requested]?
|
||||
console.log("""Warning: No component found for #{requested} in
|
||||
#{@constructor.displayName}. Loaded: #{Object.keys(registry)}""")
|
||||
console.log("Warning: No component found for #{requested} in #{@constructor.displayName}. Loaded: #{Object.keys(registry)}")
|
||||
state
|
||||
|
||||
Mixin =
|
||||
|
|
|
@ -1,23 +1,22 @@
|
|||
React = require 'react'
|
||||
_ = require "underscore-plus"
|
||||
|
||||
module.exports =
|
||||
EventedIFrame = React.createClass
|
||||
displayName: 'EventedIFrame'
|
||||
class EventedIFrame extends React.Component
|
||||
@displayName = 'EventedIFrame'
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
<iframe seamless="seamless" {...@props} />
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@_subscribeToIFrameEvents()
|
||||
|
||||
componentWillUnmount: ->
|
||||
doc = @getDOMNode().contentDocument
|
||||
componentWillUnmount: =>
|
||||
doc = React.findDOMNode(@).contentDocument
|
||||
for e in ['click', 'keydown', 'mousedown', 'mousemove', 'mouseup']
|
||||
doc?.removeEventListener?(e)
|
||||
|
||||
_subscribeToIFrameEvents: ->
|
||||
doc = @getDOMNode().contentDocument
|
||||
_subscribeToIFrameEvents: =>
|
||||
doc = React.findDOMNode(@).contentDocument
|
||||
_.defer =>
|
||||
doc.addEventListener "click", @_onIFrameClick
|
||||
doc.addEventListener "keydown", @_onIFrameKeydown
|
||||
|
@ -29,7 +28,7 @@ EventedIFrame = React.createClass
|
|||
# interesting behaviors. For example, when you drag and release over the
|
||||
# iFrame, the mouseup never fires in the parent window.
|
||||
|
||||
_onIFrameClick: (e) ->
|
||||
_onIFrameClick: (e) =>
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
target = e.target
|
||||
|
@ -42,15 +41,19 @@ EventedIFrame = React.createClass
|
|||
else
|
||||
target = target.parentElement
|
||||
|
||||
_onIFrameMouseEvent: (event) ->
|
||||
nodeRect = @getDOMNode().getBoundingClientRect()
|
||||
@getDOMNode().dispatchEvent(new MouseEvent(event.type, _.extend({}, event, {
|
||||
_onIFrameMouseEvent: (event) =>
|
||||
node = React.findDOMNode(@)
|
||||
nodeRect = node.getBoundingClientRect()
|
||||
node.dispatchEvent(new MouseEvent(event.type, _.extend({}, event, {
|
||||
clientX: event.clientX + nodeRect.left
|
||||
clientY: event.clientY + nodeRect.top
|
||||
pageX: event.pageX + nodeRect.left
|
||||
pageY: event.pageY + nodeRect.top
|
||||
})))
|
||||
|
||||
_onIFrameKeydown: (event) ->
|
||||
_onIFrameKeydown: (event) =>
|
||||
return if event.metaKey or event.altKey or event.ctrlKey
|
||||
@getDOMNode().dispatchEvent(new KeyboardEvent(event.type, event))
|
||||
React.findDOMNode(@).dispatchEvent(new KeyboardEvent(event.type, event))
|
||||
|
||||
|
||||
module.exports = EventedIFrame
|
|
@ -5,8 +5,8 @@ _ = require 'underscore-plus'
|
|||
ComponentRegistry} = require "inbox-exports"
|
||||
|
||||
module.exports =
|
||||
RegisteredRegion = React.createClass
|
||||
displayName: 'RegisteredRegion'
|
||||
InjectedComponentSet = React.createClass
|
||||
displayName: 'InjectedComponentSet'
|
||||
|
||||
propTypes:
|
||||
location: React.PropTypes.string.isRequired
|
46
src/components/injected-component.cjsx
Normal file
46
src/components/injected-component.cjsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore-plus'
|
||||
{Actions,
|
||||
WorkspaceStore,
|
||||
ComponentRegistry} = require "inbox-exports"
|
||||
|
||||
class InjectedComponent extends React.Component
|
||||
|
||||
@displayName = 'InjectedComponent'
|
||||
@propTypes =
|
||||
name: React.PropTypes.string.isRequired
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@_componentUnlistener = ComponentRegistry.listen =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
componentWillUnmount: =>
|
||||
@_componentUnlistener() if @_componentUnlistener
|
||||
|
||||
componentWillReceiveProps: (newProps) =>
|
||||
if newProps.name isnt @props?.name
|
||||
@setState(@_getStateFromStores(newProps))
|
||||
|
||||
render: =>
|
||||
view = @state.component
|
||||
return <div></div> unless view
|
||||
props = _.omit(@props, _.keys(@constructor.propTypes))
|
||||
|
||||
<view ref="inner" key={name} {...props} />
|
||||
|
||||
focus: =>
|
||||
# Not forwarding event - just a method call
|
||||
@refs.inner.focus() if @refs.inner.focus?
|
||||
|
||||
blur: =>
|
||||
# Not forwarding an event - just a method call
|
||||
@refs.inner.blur() if @refs.inner.blur?
|
||||
|
||||
_getStateFromStores: (props) =>
|
||||
props ?= @props
|
||||
component: ComponentRegistry.findViewByName(props.name)
|
||||
|
||||
module.exports = InjectedComponent
|
|
@ -1,5 +1,6 @@
|
|||
_ = require 'underscore-plus'
|
||||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
{ComponentRegistry} = require 'inbox-exports'
|
||||
|
||||
ThreadListItemMixin = require './thread-list-item-mixin'
|
||||
|
@ -35,7 +36,7 @@ ThreadListNarrowItem = React.createClass
|
|||
</div>
|
||||
|
||||
_containerClasses: ->
|
||||
React.addons.classSet
|
||||
classNames
|
||||
'unread': @props.unread
|
||||
'selected': @props.selected
|
||||
'thread-list-item': true
|
||||
|
|
|
@ -6,9 +6,9 @@ RangeChunkSize = 10
|
|||
class ListColumn
|
||||
constructor: ({@name, @resolver, @flex, @width}) ->
|
||||
|
||||
ListTabularItem = React.createClass
|
||||
displayName: 'ListTabularItem'
|
||||
propTypes:
|
||||
class ListTabularItem extends React.Component
|
||||
@displayName = 'ListTabularItem'
|
||||
@propTypes =
|
||||
metrics: React.PropTypes.object
|
||||
columns: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
|
||||
item: React.PropTypes.object.isRequired
|
||||
|
@ -21,12 +21,12 @@ ListTabularItem = React.createClass
|
|||
# DO NOT DELETE unless you know what you're doing! This method cuts
|
||||
# React.Perf.wasted-time from ~300msec to 20msec by doing a deep
|
||||
# comparison of props before triggering a re-render.
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
# Quick check to avoid running isEqual if our item === existing item
|
||||
return false if _.isEqual(@props, nextProps)
|
||||
true
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
className = "list-item list-tabular-item #{@props.itemProps?.className}"
|
||||
props = _.omit(@props.itemProps ? {}, 'className')
|
||||
|
||||
|
@ -34,7 +34,7 @@ ListTabularItem = React.createClass
|
|||
{@_columns()}
|
||||
</div>
|
||||
|
||||
_columns: ->
|
||||
_columns: =>
|
||||
for column in (@props.columns ? [])
|
||||
<div key={column.name}
|
||||
displayName={column.name}
|
||||
|
@ -43,7 +43,7 @@ ListTabularItem = React.createClass
|
|||
{column.resolver(@props.item, @)}
|
||||
</div>
|
||||
|
||||
_onClick: (event) ->
|
||||
_onClick: (event) =>
|
||||
@props.onSelect?(@props.item, event)
|
||||
|
||||
@props.onClick?(@props.item, event)
|
||||
|
@ -53,10 +53,9 @@ ListTabularItem = React.createClass
|
|||
@_lastClickTime = Date.now()
|
||||
|
||||
|
||||
module.exports =
|
||||
ListTabular = React.createClass
|
||||
displayName: 'ListTabular'
|
||||
propTypes:
|
||||
class ListTabular extends React.Component
|
||||
@displayName = 'ListTabular'
|
||||
@propTypes =
|
||||
columns: React.PropTypes.arrayOf(React.PropTypes.object).isRequired
|
||||
dataView: React.PropTypes.object
|
||||
itemPropsProvider: React.PropTypes.func
|
||||
|
@ -64,32 +63,31 @@ ListTabular = React.createClass
|
|||
onClick: React.PropTypes.func
|
||||
onDoubleClick: React.PropTypes.func
|
||||
|
||||
getInitialState: ->
|
||||
renderedRangeStart: -1
|
||||
renderedRangeEnd: -1
|
||||
scrollTop: 0
|
||||
scrollInProgress: false
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
renderedRangeStart: -1
|
||||
renderedRangeEnd: -1
|
||||
scrollTop: 0
|
||||
scrollInProgress: false
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@updateRangeState()
|
||||
|
||||
componentDidUpdate: (prevProps, prevState) ->
|
||||
componentDidUpdate: (prevProps, prevState) =>
|
||||
# If our view has been swapped out for an entirely different one,
|
||||
# reset our scroll position to the top.
|
||||
if prevProps.dataView isnt @props.dataView
|
||||
container = @refs.container.getDOMNode()
|
||||
container = React.findDOMNode(@refs.container)
|
||||
container.scrollTop = 0
|
||||
@updateRangeState()
|
||||
|
||||
updateScrollState: ->
|
||||
updateScrollState: =>
|
||||
window.requestAnimationFrame =>
|
||||
return unless @isMounted()
|
||||
container = @refs.container.getDOMNode()
|
||||
container = React.findDOMNode(@refs.container)
|
||||
|
||||
# Create an event that fires when we stop receiving scroll events.
|
||||
# There is no "scrollend" event, but we really need one.
|
||||
@_scrollTick ?= _.debounce =>
|
||||
return unless @isMounted()
|
||||
@onDoneReceivingScrollEvents()
|
||||
, 100
|
||||
@_scrollTick()
|
||||
|
@ -103,13 +101,13 @@ ListTabular = React.createClass
|
|||
if Math.abs(@state.scrollTop - container.scrollTop) >= @_rowHeight() * RangeChunkSize
|
||||
@updateRangeState()
|
||||
|
||||
onDoneReceivingScrollEvents: ->
|
||||
onDoneReceivingScrollEvents: =>
|
||||
@setState({scrollInProgress: false})
|
||||
@updateRangeState()
|
||||
|
||||
updateRangeState: ->
|
||||
updateRangeState: =>
|
||||
container = @refs.container
|
||||
scrollTop = container?.getDOMNode().scrollTop
|
||||
scrollTop = React.findDOMNode(container)?.scrollTop
|
||||
|
||||
rowHeight = @_rowHeight()
|
||||
|
||||
|
@ -149,7 +147,7 @@ ListTabular = React.createClass
|
|||
renderedRangeStart: rangeStart
|
||||
renderedRangeEnd: rangeEnd
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
innerStyles =
|
||||
height: @props.dataView.count() * @_rowHeight()
|
||||
pointerEvents: if @state.scrollInProgress then 'none' else 'auto'
|
||||
|
@ -161,10 +159,10 @@ ListTabular = React.createClass
|
|||
</div>
|
||||
</div>
|
||||
|
||||
_rowHeight: ->
|
||||
_rowHeight: =>
|
||||
39
|
||||
|
||||
_headers: ->
|
||||
_headers: =>
|
||||
return [] unless @props.displayHeaders
|
||||
|
||||
headerColumns = @props.columns.map (column) ->
|
||||
|
@ -178,7 +176,7 @@ ListTabular = React.createClass
|
|||
{headerColumns}
|
||||
</div>
|
||||
|
||||
_rows: ->
|
||||
_rows: =>
|
||||
rowHeight = @_rowHeight()
|
||||
rows = []
|
||||
|
||||
|
@ -203,3 +201,5 @@ ListTabular = React.createClass
|
|||
|
||||
ListTabular.Item = ListTabularItem
|
||||
ListTabular.Column = ListColumn
|
||||
|
||||
module.exports = ListTabular
|
|
@ -1,71 +1,29 @@
|
|||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
_ = require 'underscore-plus'
|
||||
{Utils} = require 'inbox-exports'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
|
||||
###
|
||||
The Menu component allows you to display a list of items. Menu takes care of
|
||||
several important things, ensuring that your menu is consistent with the rest
|
||||
of the Edgehill application and offers a near-native experience:
|
||||
|
||||
- Keyboard Interaction with the Up and Down arrow keys, Enter to select
|
||||
- Maintaining selection across content changes
|
||||
- Highlighted state
|
||||
|
||||
Menus are often, but not always, used in conjunction with `Popover` to display
|
||||
a floating "popup" menu. See `template-picker.cjsx` for an example.
|
||||
|
||||
Populating the Menu
|
||||
-------------------
|
||||
|
||||
When you render a Menu component, you need to provide three important props:
|
||||
|
||||
`items`:
|
||||
An array of arbitrary objects the menu should display.
|
||||
|
||||
`itemContent`:
|
||||
A function that returns a MenuItem, string, or React component for the given
|
||||
`item`.
|
||||
|
||||
If you return a MenuItem, your item is injected into the list directly.
|
||||
|
||||
If you return a string or React component, the result is placed within a
|
||||
MenuItem, resulting in the following DOM:
|
||||
`<div className="item [selected]">{your content}</div>`.
|
||||
|
||||
To create dividers and other special menu items, return an instance of:
|
||||
<Menu.Item divider content="Label">
|
||||
|
||||
`itemKey`:
|
||||
A function that returns a unique string key for the given `item`. Keys are
|
||||
important for efficient React rendering when `items` is changed, and a
|
||||
key function is required.
|
||||
|
||||
The Menu also exposes "header" and "footer" regions you can fill with arbitrary
|
||||
components by providing the `headerComponents` and `footerComponents` props.
|
||||
These items are nested within `.header-container`. and `.footer-container`,
|
||||
and you can customize their appearance by providing CSS selectors scoped to your
|
||||
component's Menu instance:
|
||||
|
||||
```
|
||||
.template-picker .menu .header-container {
|
||||
height: 100px;
|
||||
}
|
||||
```
|
||||
|
||||
Events
|
||||
------
|
||||
|
||||
`onSelect`:
|
||||
Called with the selected item when the user clicks an item in the menu
|
||||
or confirms their selection with the Enter key.
|
||||
|
||||
Public: `MenuItem` components can be provided to the {Menu} by the `itemContent` function.
|
||||
MenuItem's props allow you to display dividers as well as standard items.
|
||||
###
|
||||
class MenuItem extends React.Component
|
||||
@displayName = 'MenuItem'
|
||||
|
||||
###
|
||||
Public: React `props` supported by MenuItem:
|
||||
|
||||
- `divider` (optional) Pass a {String} to render the menu item as a section divider.
|
||||
- `key` (optional)
|
||||
- `selected` (optional)
|
||||
###
|
||||
@propTypes:
|
||||
divider: React.PropTypes.string
|
||||
key: React.PropTypes.string
|
||||
selected: React.PropTypes.bool
|
||||
|
||||
MenuItem = React.createClass
|
||||
displayName: 'MenuItem'
|
||||
render: ->
|
||||
render: =>
|
||||
if @props.divider
|
||||
<div className="divider">{@props.divider}</div>
|
||||
else
|
||||
|
@ -73,14 +31,23 @@ MenuItem = React.createClass
|
|||
className += " selected" if @props.selected
|
||||
<div className={className} key={@props.key} onMouseDown={@props.onMouseDown}>{@props.content}</div>
|
||||
|
||||
###
|
||||
Public: React component for a {Menu} item that displays a name and email address.
|
||||
###
|
||||
class MenuNameEmailItem extends React.Component
|
||||
@displayName: 'MenuNameEmailItem'
|
||||
|
||||
MenuNameEmailItem = React.createClass
|
||||
displayName: 'MenuNameEmailItem'
|
||||
propTypes:
|
||||
###
|
||||
Public: React `props` supported by MenuNameEmailItem:
|
||||
|
||||
- `name` (optional) The {String} name to be displayed.
|
||||
- `email` (optional) The {String} email address to be displayed.
|
||||
###
|
||||
@propTypes:
|
||||
name: React.PropTypes.string
|
||||
email: React.PropTypes.string
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
if @props.name?.length > 0 and @props.name isnt @props.email
|
||||
<span>
|
||||
<span className="primary">{@props.name}</span>
|
||||
|
@ -89,26 +56,88 @@ MenuNameEmailItem = React.createClass
|
|||
else
|
||||
<span className="primary">{@props.email}</span>
|
||||
|
||||
###
|
||||
Public: React component for multi-section Menus with key binding
|
||||
|
||||
Menu = React.createClass
|
||||
The Menu component allows you to display a list of items. Menu takes care of
|
||||
several important things, ensuring that your menu is consistent with the rest
|
||||
of the Edgehill application and offers a near-native experience:
|
||||
|
||||
propTypes:
|
||||
- Keyboard Interaction with the Up and Down arrow keys, Enter to select
|
||||
- Maintaining selection across content changes
|
||||
- Highlighted state
|
||||
|
||||
Menus are often, but not always, used in conjunction with {Popover} to display
|
||||
a floating "popup" menu. See `template-picker.cjsx` for an example.
|
||||
|
||||
The Menu also exposes "header" and "footer" regions you can fill with arbitrary
|
||||
components by providing the `headerComponents` and `footerComponents` props.
|
||||
These items are nested within `.header-container`. and `.footer-container`,
|
||||
and you can customize their appearance by providing CSS selectors scoped to your
|
||||
component's Menu instance:
|
||||
|
||||
```css
|
||||
.template-picker .menu .header-container {
|
||||
height: 100px;
|
||||
}
|
||||
```
|
||||
###
|
||||
|
||||
class Menu extends React.Component
|
||||
@displayName: 'Menu'
|
||||
|
||||
###
|
||||
Public: React `props` supported by Menu:
|
||||
|
||||
- `className` (optional) The {String} class name applied to the Menu
|
||||
|
||||
- `itemContent` A {Function} that returns a {MenuItem}, {String}, or
|
||||
React component for the given `item`.
|
||||
|
||||
If you return a {MenuItem}, your item is injected into the list directly.
|
||||
|
||||
If you return a string or React component, the result is placed within a
|
||||
{MenuItem}, resulting in the following DOM:
|
||||
`<div className="item [selected]">{your content}</div>`.
|
||||
|
||||
To create dividers and other special menu items, return an instance of:
|
||||
<Menu.Item divider content="Label">
|
||||
|
||||
- `itemKey` A {Function} that returns a unique string key for the given `item`.
|
||||
Keys are important for efficient React rendering when `items` is changed, and a
|
||||
key function is required.
|
||||
|
||||
- `items` An {Array} of arbitrary objects the menu should display.
|
||||
|
||||
- `onSelect` A {Function} called with the selected item when the user clicks
|
||||
an item in the menu or confirms their selection with the Enter key.
|
||||
|
||||
###
|
||||
@propTypes:
|
||||
className: React.PropTypes.string,
|
||||
footerComponents: React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
headerComponents: React.PropTypes.arrayOf(React.PropTypes.element),
|
||||
itemContent: React.PropTypes.func.isRequired,
|
||||
itemKey: React.PropTypes.func.isRequired,
|
||||
|
||||
items: React.PropTypes.array.isRequired
|
||||
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
|
||||
getInitialState: ->
|
||||
selectedIndex: -1
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
selectedIndex: -1
|
||||
|
||||
getSelectedItem: ->
|
||||
# Public: Takes an argument and does some stuff.
|
||||
#
|
||||
# a - A {String}
|
||||
#
|
||||
# Returns {Boolean}.
|
||||
#
|
||||
getSelectedItem: =>
|
||||
@props.items[@state.selectedIndex]
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@subscriptions = new CompositeDisposable()
|
||||
@subscriptions.add atom.commands.add '.menu', {
|
||||
'menu:move-up': => @_onShiftSelectedIndex(-1)
|
||||
|
@ -116,7 +145,7 @@ Menu = React.createClass
|
|||
'menu:enter': => @_onEnter()
|
||||
}
|
||||
|
||||
componentWillReceiveProps: (newProps) ->
|
||||
componentWillReceiveProps: (newProps) =>
|
||||
# Attempt to preserve selection across props.items changes by
|
||||
# finding an item in the new list with a key matching the old
|
||||
# selected item's key
|
||||
|
@ -131,15 +160,15 @@ Menu = React.createClass
|
|||
@setState
|
||||
selectedIndex: newSelectionIndex
|
||||
|
||||
componentDidUpdate: ->
|
||||
item = @getDOMNode().querySelector(".selected")
|
||||
container = @getDOMNode().querySelector(".content-container")
|
||||
componentDidUpdate: =>
|
||||
item = React.findDOMNode(@).querySelector(".selected")
|
||||
container = React.findDOMNode(@).querySelector(".content-container")
|
||||
Utils.scrollNodeToVisibleInContainer(item, container)
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@subscriptions?.dispose()
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
hc = @props.headerComponents ? []
|
||||
if hc.length is 0 then hc = <span></span>
|
||||
fc = @props.footerComponents ? []
|
||||
|
@ -154,9 +183,9 @@ Menu = React.createClass
|
|||
</div>
|
||||
</div>
|
||||
|
||||
_contentContainer: ->
|
||||
_contentContainer: =>
|
||||
items = @props.items.map(@_itemComponentForItem) ? []
|
||||
contentClass = React.addons.classSet
|
||||
contentClass = classNames
|
||||
'content-container': true
|
||||
'empty': items.length is 0
|
||||
|
||||
|
@ -164,9 +193,9 @@ Menu = React.createClass
|
|||
{items}
|
||||
</div>
|
||||
|
||||
_itemComponentForItem: (item, i) ->
|
||||
_itemComponentForItem: (item, i) =>
|
||||
content = @props.itemContent(item)
|
||||
return content if content.type is MenuItem.type
|
||||
return content if content instanceof MenuItem
|
||||
|
||||
onMouseDown = (event) =>
|
||||
event.preventDefault()
|
||||
|
@ -177,7 +206,7 @@ Menu = React.createClass
|
|||
content={content}
|
||||
selected={@state.selectedIndex is i} />
|
||||
|
||||
_onShiftSelectedIndex: (delta) ->
|
||||
_onShiftSelectedIndex: (delta) =>
|
||||
return if @props.items.length is 0
|
||||
index = @state.selectedIndex + delta
|
||||
index = Math.max(0, Math.min(@props.items.length-1, index))
|
||||
|
@ -192,7 +221,7 @@ Menu = React.createClass
|
|||
isDivider = itemContent.props?.divider
|
||||
@_onShiftSelectedIndex(delta) if isDivider
|
||||
|
||||
_onEnter: ->
|
||||
_onEnter: =>
|
||||
item = @props.items[@state.selectedIndex]
|
||||
@props.onSelect(item) if item?
|
||||
|
||||
|
@ -200,4 +229,4 @@ Menu = React.createClass
|
|||
Menu.Item = MenuItem
|
||||
Menu.NameEmailItem = MenuNameEmailItem
|
||||
|
||||
module.exports = Menu
|
||||
module.exports = Menu
|
|
@ -1,10 +1,11 @@
|
|||
React = require "react/addons"
|
||||
classNames = require 'classnames'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
{Actions,
|
||||
AddRemoveTagsTask,
|
||||
WorkspaceStore} = require "inbox-exports"
|
||||
RegisteredRegion = require './registered-region'
|
||||
InjectedComponentSet = require './injected-component-set'
|
||||
RetinaImg = require './retina-img'
|
||||
|
||||
module.exports =
|
||||
|
@ -61,7 +62,7 @@ MultiselectActionBar = React.createClass
|
|||
|
||||
_renderActions: ->
|
||||
return <div></div> unless @state.view
|
||||
<RegisteredRegion location={"#{@props.collection}:BulkAction"}
|
||||
<InjectedComponentSet location={"#{@props.collection}:BulkAction"}
|
||||
selection={@state.view.selection} />
|
||||
|
||||
_label: ->
|
||||
|
@ -73,7 +74,7 @@ MultiselectActionBar = React.createClass
|
|||
""
|
||||
|
||||
_classSet: ->
|
||||
React.addons.classSet
|
||||
classNames
|
||||
"selection-bar": true
|
||||
"enabled": @state.count > 0
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
_ = require 'underscore-plus'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
ListTabular = require './list-tabular'
|
||||
Spinner = require './spinner'
|
||||
{Actions,
|
||||
|
@ -9,11 +10,10 @@ Spinner = require './spinner'
|
|||
NamespaceStore} = require 'inbox-exports'
|
||||
EventEmitter = require('events').EventEmitter
|
||||
|
||||
module.exports =
|
||||
MultiselectList = React.createClass
|
||||
displayName: 'MultiselectList'
|
||||
class MultiselectList extends React.Component
|
||||
@displayName = 'MultiselectList'
|
||||
|
||||
propTypes:
|
||||
@propTypes =
|
||||
className: React.PropTypes.string.isRequired
|
||||
collection: React.PropTypes.string.isRequired
|
||||
commands: React.PropTypes.object.isRequired
|
||||
|
@ -21,36 +21,36 @@ MultiselectList = React.createClass
|
|||
dataStore: React.PropTypes.object.isRequired
|
||||
itemPropsProvider: React.PropTypes.func.isRequired
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@setupForProps(@props)
|
||||
|
||||
componentWillReceiveProps: (newProps) ->
|
||||
componentWillReceiveProps: (newProps) =>
|
||||
return if _.isEqual(@props, newProps)
|
||||
@teardownForProps()
|
||||
@setupForProps(newProps)
|
||||
@setState(@_getStateFromStores(newProps))
|
||||
|
||||
componentDidUpdate: (prevProps, prevState) ->
|
||||
componentDidUpdate: (prevProps, prevState) =>
|
||||
if prevState.focusedId isnt @state.focusedId or
|
||||
prevState.keyboardCursorId isnt @state.keyboardCursorId
|
||||
|
||||
item = @getDOMNode().querySelector(".focused")
|
||||
item ?= @getDOMNode().querySelector(".keyboard-cursor")
|
||||
list = @refs.list.getDOMNode()
|
||||
item = React.findDOMNode(@).querySelector(".focused")
|
||||
item ?= React.findDOMNode(@).querySelector(".keyboard-cursor")
|
||||
list = React.findDOMNode(@refs.list)
|
||||
Utils.scrollNodeToVisibleInContainer(item, list)
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@teardownForProps()
|
||||
|
||||
teardownForProps: ->
|
||||
teardownForProps: =>
|
||||
return unless @unsubscribers
|
||||
unsubscribe() for unsubscribe in @unsubscribers
|
||||
@command_unsubscriber.dispose()
|
||||
|
||||
setupForProps: (props) ->
|
||||
setupForProps: (props) =>
|
||||
commands = _.extend {},
|
||||
'core:focus-item': => @_onEnter()
|
||||
'core:select-item': => @_onSelect()
|
||||
|
@ -66,8 +66,8 @@ MultiselectList = React.createClass
|
|||
|
||||
checkmarkColumn = new ListTabular.Column
|
||||
name: ""
|
||||
resolver: (thread) ->
|
||||
toggle = (event) ->
|
||||
resolver: (thread) =>
|
||||
toggle = (event) =>
|
||||
props.dataStore.view().selection.toggle(thread)
|
||||
event.stopPropagation()
|
||||
<div className="checkmark" onClick={toggle}><div className="inner"></div></div>
|
||||
|
@ -79,7 +79,7 @@ MultiselectList = React.createClass
|
|||
@unsubscribers.push FocusedContentStore.listen @_onChange
|
||||
@command_unsubscriber = atom.commands.add('body', commands)
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
# IMPORTANT: DO NOT pass inline functions as props. _.isEqual thinks these
|
||||
# are "different", and will re-render everything. Instead, declare them with ?=,
|
||||
# pass a reference. (Alternatively, ignore these in children's shouldComponentUpdate.)
|
||||
|
@ -93,7 +93,7 @@ MultiselectList = React.createClass
|
|||
@itemPropsProvider ?= (item) =>
|
||||
props = @props.itemPropsProvider(item)
|
||||
props.className ?= ''
|
||||
props.className += " " + React.addons.classSet
|
||||
props.className += " " + classNames
|
||||
'selected': item.id in @state.selectedIds
|
||||
'focused': @state.showFocus and item.id is @state.focusedId
|
||||
'keyboard-cursor': @state.showKeyboardCursor and item.id is @state.keyboardCursorId
|
||||
|
@ -115,7 +115,7 @@ MultiselectList = React.createClass
|
|||
<Spinner visible={@state.ready is false} />
|
||||
</div>
|
||||
|
||||
_onClickItem: (item, event) ->
|
||||
_onClickItem: (item, event) =>
|
||||
if event.metaKey
|
||||
@state.dataView.selection.toggle(item)
|
||||
if @state.showKeyboardCursor
|
||||
|
@ -127,13 +127,13 @@ MultiselectList = React.createClass
|
|||
else
|
||||
Actions.focusInCollection({collection: @props.collection, item: item})
|
||||
|
||||
_onEnter: ->
|
||||
_onEnter: =>
|
||||
return unless @state.showKeyboardCursor
|
||||
item = @state.dataView.getById(@state.keyboardCursorId)
|
||||
if item
|
||||
Actions.focusInCollection({collection: @props.collection, item: item})
|
||||
|
||||
_onSelect: ->
|
||||
_onSelect: =>
|
||||
if @state.showKeyboardCursor and @_visible()
|
||||
id = @state.keyboardCursorId
|
||||
else
|
||||
|
@ -142,7 +142,7 @@ MultiselectList = React.createClass
|
|||
return unless id
|
||||
@state.dataView.selection.toggle(@state.dataView.getById(id))
|
||||
|
||||
_onShift: (delta, options = {}) ->
|
||||
_onShift: (delta, options = {}) =>
|
||||
if @state.showKeyboardCursor and @_visible()
|
||||
id = @state.keyboardCursorId
|
||||
action = Actions.focusKeyboardInCollection
|
||||
|
@ -160,7 +160,7 @@ MultiselectList = React.createClass
|
|||
if options.select
|
||||
@state.dataView.selection.walk({current, next})
|
||||
|
||||
_visible: ->
|
||||
_visible: =>
|
||||
if WorkspaceStore.layoutMode() is "list"
|
||||
WorkspaceStore.topSheet().root
|
||||
else
|
||||
|
@ -170,12 +170,11 @@ MultiselectList = React.createClass
|
|||
# Since they're on the same event listner, and the event listeners are
|
||||
# unordered, we need a way to push thread list updates later back in the
|
||||
# queue.
|
||||
_onChange: -> _.delay =>
|
||||
return unless @isMounted()
|
||||
_onChange: => _.delay =>
|
||||
@setState(@_getStateFromStores())
|
||||
, 1
|
||||
|
||||
_getStateFromStores: (props) ->
|
||||
_getStateFromStores: (props) =>
|
||||
props ?= @props
|
||||
|
||||
view = props.dataStore?.view()
|
||||
|
@ -188,3 +187,6 @@ MultiselectList = React.createClass
|
|||
keyboardCursorId: FocusedContentStore.keyboardCursorId(props.collection)
|
||||
showFocus: !FocusedContentStore.keyboardCursorEnabled()
|
||||
showKeyboardCursor: FocusedContentStore.keyboardCursorEnabled()
|
||||
|
||||
|
||||
module.exports = MultiselectList
|
|
@ -3,8 +3,8 @@ _ = require 'underscore-plus'
|
|||
{CompositeDisposable} = require 'event-kit'
|
||||
|
||||
###
|
||||
The Popover component makes it easy to display a sheet or popup menu when the user
|
||||
clicks the React element provided as `buttonComponent`. In Edgehill, the Popover
|
||||
Public: The Popover component makes it easy to display a sheet or popup menu when the
|
||||
user clicks the React element provided as `buttonComponent`. In Edgehill, the Popover
|
||||
component is used to create rich dropdown menus, detail popups, etc. with consistent
|
||||
look and feel and behavior.
|
||||
|
||||
|
@ -24,54 +24,54 @@ The Popover component handles:
|
|||
|
||||
- Automatically focusing the item with the lowest tabIndex inside the popover
|
||||
|
||||
Input Focus
|
||||
-----------
|
||||
## Input Focus
|
||||
|
||||
If your Popover contains an input, like a search bar, give it a tabIndex and
|
||||
Popover will automatically focus it when the popover is opened.
|
||||
|
||||
Advanced Use
|
||||
------------
|
||||
## Advanced Use
|
||||
|
||||
If you don't want to use the Popover in conjunction with a triggering button,
|
||||
you can manually call `open()` and `close()` to display it. A typical scenario
|
||||
looks like this:
|
||||
|
||||
```
|
||||
render: ->
|
||||
```coffeescript
|
||||
render: =>
|
||||
<Popover ref="myPopover"> Popover Contents </Popover>
|
||||
|
||||
showMyPopover: ->
|
||||
showMyPopover: =>
|
||||
@refs.myPopover.open()
|
||||
|
||||
```
|
||||
|
||||
###
|
||||
module.exports =
|
||||
Popover = React.createClass
|
||||
|
||||
propTypes:
|
||||
buttonComponent: React.PropTypes.element,
|
||||
class Popover extends React.Component
|
||||
|
||||
getInitialState: ->
|
||||
showing: false
|
||||
@propTypes =
|
||||
buttonComponent: React.PropTypes.element
|
||||
|
||||
componentDidMount: ->
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
showing: false
|
||||
|
||||
componentDidMount: =>
|
||||
@subscriptions = new CompositeDisposable()
|
||||
@subscriptions.add atom.commands.add '.popover-container', {
|
||||
'popover:close': => @close()
|
||||
}
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@subscriptions?.dispose()
|
||||
|
||||
open: ->
|
||||
open: =>
|
||||
@setState
|
||||
showing: true
|
||||
|
||||
close: ->
|
||||
close: =>
|
||||
@setState
|
||||
showing: false
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
wrappedButtonComponent = []
|
||||
if @props.buttonComponent
|
||||
wrappedButtonComponent = <div onClick={@_onClick}>{@props.buttonComponent}</div>
|
||||
|
@ -88,21 +88,22 @@ Popover = React.createClass
|
|||
{popoverComponent}
|
||||
</div>
|
||||
|
||||
_onClick: ->
|
||||
_onClick: =>
|
||||
showing = !@state.showing
|
||||
@setState({showing})
|
||||
|
||||
if showing
|
||||
setTimeout =>
|
||||
# Automatically focus the element inside us with the lowest tab index
|
||||
node = @refs.popover.getDOMNode()
|
||||
node = @refs.popover.findDOMNode()
|
||||
matches = _.sortBy node.querySelectorAll("[tabIndex]"), (a,b) -> a.tabIndex < b.tabIndex
|
||||
matches[0].focus() if matches[0]
|
||||
|
||||
_onBlur: (event) ->
|
||||
_onBlur: (event) =>
|
||||
target = event.nativeEvent.relatedTarget
|
||||
if target? and @refs.container.getDOMNode().contains(target)
|
||||
if target? and @refs.container.findDOMNode().contains(target)
|
||||
return
|
||||
@setState
|
||||
showing:false
|
||||
|
||||
module.exports = Popover
|
|
@ -27,11 +27,10 @@ ResizableHandle =
|
|||
'width': Math.min(props.maxWidth ? 10000, Math.max(props.minWidth ? 0, event.pageX - state.bcr.left))
|
||||
|
||||
|
||||
module.exports =
|
||||
ResizableRegion = React.createClass
|
||||
displayName: 'ResizableRegion'
|
||||
class ResizableRegion extends React.Component
|
||||
@displayName = 'ResizableRegion'
|
||||
|
||||
propTypes:
|
||||
@propTypes =
|
||||
handle: React.PropTypes.object.isRequired
|
||||
onResize: React.PropTypes.func
|
||||
|
||||
|
@ -43,13 +42,12 @@ ResizableRegion = React.createClass
|
|||
minHeight: React.PropTypes.number
|
||||
maxHeight: React.PropTypes.number
|
||||
|
||||
getDefaultProps: ->
|
||||
handle: ResizableHandle.Right
|
||||
constructor: (@props = {}) ->
|
||||
@props.handle ?= ResizableHandle.Right
|
||||
@state =
|
||||
dragging: false
|
||||
|
||||
getInitialState: ->
|
||||
dragging: false
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
if @props.handle.axis is 'horizontal'
|
||||
containerStyle =
|
||||
'minWidth': @props.minWidth
|
||||
|
@ -85,7 +83,7 @@ ResizableRegion = React.createClass
|
|||
</div>
|
||||
</div>
|
||||
|
||||
componentDidUpdate: (lastProps, lastState) ->
|
||||
componentDidUpdate: (lastProps, lastState) =>
|
||||
if lastState.dragging and not @state.dragging
|
||||
document.removeEventListener('mousemove', @_mouseMove)
|
||||
document.removeEventListener('mouseup', @_mouseUp)
|
||||
|
@ -93,19 +91,19 @@ ResizableRegion = React.createClass
|
|||
document.addEventListener('mousemove', @_mouseMove)
|
||||
document.addEventListener('mouseup', @_mouseUp)
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
componentWillReceiveProps: (nextProps) =>
|
||||
if nextProps.handle.axis is 'vertical' and nextProps.initialHeight != @props.initialHeight
|
||||
@setState(height: nextProps.initialHeight)
|
||||
if nextProps.handle.axis is 'horizontal' and nextProps.initialWidth != @props.initialWidth
|
||||
@setState(width: nextProps.initialWidth)
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
PriorityUICoordinator.endPriorityTask(@_taskId) if @_taskId
|
||||
@_taskId = null
|
||||
|
||||
_mouseDown: (event) ->
|
||||
_mouseDown: (event) =>
|
||||
return if event.button != 0
|
||||
bcr = @getDOMNode().getBoundingClientRect()
|
||||
bcr = React.findDOMNode(@).getBoundingClientRect()
|
||||
@setState
|
||||
dragging: true
|
||||
bcr: bcr
|
||||
|
@ -115,7 +113,7 @@ ResizableRegion = React.createClass
|
|||
PriorityUICoordinator.endPriorityTask(@_taskId) if @_taskId
|
||||
@_taskId = PriorityUICoordinator.beginPriorityTask()
|
||||
|
||||
_mouseUp: (event) ->
|
||||
_mouseUp: (event) =>
|
||||
return if event.button != 0
|
||||
@setState
|
||||
dragging: false
|
||||
|
@ -126,7 +124,7 @@ ResizableRegion = React.createClass
|
|||
PriorityUICoordinator.endPriorityTask(@_taskId)
|
||||
@_taskId = null
|
||||
|
||||
_mouseMove: (event) ->
|
||||
_mouseMove: (event) =>
|
||||
return if !@state.dragging
|
||||
@setState @props.handle.transform(@state, @props, event)
|
||||
@props.onResize(@state.height ? @state.width) if @props.onResize
|
||||
|
@ -134,3 +132,5 @@ ResizableRegion = React.createClass
|
|||
event.preventDefault()
|
||||
|
||||
ResizableRegion.Handle = ResizableHandle
|
||||
|
||||
module.exports = ResizableRegion
|
|
@ -1,17 +1,18 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore-plus'
|
||||
classNames = require 'classnames'
|
||||
|
||||
module.exports =
|
||||
Spinner = React.createClass
|
||||
propTypes:
|
||||
class Spinner extends React.Component
|
||||
@propTypes =
|
||||
visible: React.PropTypes.bool
|
||||
style: React.PropTypes.object
|
||||
|
||||
getInitialState: ->
|
||||
hidden: true
|
||||
paused: true
|
||||
constructor: (@props) ->
|
||||
@state =
|
||||
hidden: true
|
||||
paused: true
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
# The spinner always starts hidden. After it's mounted, it unhides itself
|
||||
# if it's set to visible. This is a bit strange, but ensures that the CSS
|
||||
# transition from .spinner.hidden => .spinner always happens, along with
|
||||
|
@ -19,7 +20,7 @@ Spinner = React.createClass
|
|||
if @props.visible and @state.hidden
|
||||
@showAfterDelay()
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
componentWillReceiveProps: (nextProps) =>
|
||||
hidden = if nextProps.visible? then !nextProps.visible else false
|
||||
|
||||
if @state.hidden is false and hidden is true
|
||||
|
@ -28,21 +29,19 @@ Spinner = React.createClass
|
|||
else if @state.hidden is true and hidden is false
|
||||
@showAfterDelay()
|
||||
|
||||
pauseAfterDelay: ->
|
||||
pauseAfterDelay: =>
|
||||
_.delay =>
|
||||
return unless @isMounted()
|
||||
return if @props.visible
|
||||
@setState({paused: true})
|
||||
,250
|
||||
|
||||
showAfterDelay: ->
|
||||
showAfterDelay: =>
|
||||
_.delay =>
|
||||
return unless @isMounted()
|
||||
return if @props.visible isnt true
|
||||
@setState({paused: false, hidden: false})
|
||||
, 300
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
if @props.withCover
|
||||
@_renderDotsWithCover()
|
||||
else
|
||||
|
@ -51,7 +50,7 @@ Spinner = React.createClass
|
|||
# This displays an extra div that's a partially transparent white cover.
|
||||
# If you don't want to make your own background for the loading state,
|
||||
# this is a convenient default.
|
||||
_renderDotsWithCover: ->
|
||||
_renderDotsWithCover: =>
|
||||
coverClasses = React.addons.classSet
|
||||
"spinner-cover": true
|
||||
"hidden": @state.hidden
|
||||
|
@ -70,8 +69,8 @@ Spinner = React.createClass
|
|||
{@_renderSpinnerDots()}
|
||||
</div>
|
||||
|
||||
_renderSpinnerDots: ->
|
||||
spinnerClass = React.addons.classSet
|
||||
_renderSpinnerDots: =>
|
||||
spinnerClass = classNames
|
||||
'spinner': true
|
||||
'hidden': @state.hidden
|
||||
'paused': @state.paused
|
||||
|
@ -81,7 +80,7 @@ Spinner = React.createClass
|
|||
'left': '50%'
|
||||
'top': '50%'
|
||||
'zIndex': @props.zIndex+1 ? 1001
|
||||
'transform':'translate(-50%,-50%);'
|
||||
'transform':'translate(-50%,-50%)'
|
||||
|
||||
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
|
||||
|
||||
|
@ -90,3 +89,5 @@ Spinner = React.createClass
|
|||
<div className="bounce2"></div>
|
||||
<div className="bounce3"></div>
|
||||
</div>
|
||||
|
||||
module.exports = Spinner
|
||||
|
|
|
@ -29,21 +29,21 @@ TICK = 17
|
|||
|
||||
endEvents = ['webkitTransitionEnd', 'webkitAnimationEnd']
|
||||
|
||||
animationSupported = -> true
|
||||
animationSupported = => true
|
||||
|
||||
###*
|
||||
# Functions for element class management to replace dependency on jQuery
|
||||
# addClass, removeClass and hasClass
|
||||
###
|
||||
|
||||
addClass = (element, className) ->
|
||||
addClass = (element, className) =>
|
||||
if element.classList
|
||||
element.classList.add className
|
||||
else if !hasClass(element, className)
|
||||
element.className = element.className + ' ' + className
|
||||
element
|
||||
|
||||
removeClass = (element, className) ->
|
||||
removeClass = (element, className) =>
|
||||
if hasClass(className)
|
||||
if element.classList
|
||||
element.classList.remove className
|
||||
|
@ -51,15 +51,17 @@ removeClass = (element, className) ->
|
|||
element.className = (' ' + element.className + ' ').replace(' ' + className + ' ', ' ').trim()
|
||||
element
|
||||
|
||||
hasClass = (element, className) ->
|
||||
hasClass = (element, className) =>
|
||||
if element.classList
|
||||
element.classList.contains className
|
||||
else
|
||||
(' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1
|
||||
|
||||
TimeoutTransitionGroupChild = React.createClass(
|
||||
transition: (animationType, finishCallback) ->
|
||||
node = @getDOMNode()
|
||||
|
||||
class TimeoutTransitionGroupChild extends React.Component
|
||||
|
||||
transition: (animationType, finishCallback) =>
|
||||
node = React.findDOMNode(@)
|
||||
className = @props.name + '-' + animationType
|
||||
activeClassName = className + '-active'
|
||||
|
||||
|
@ -104,29 +106,28 @@ TimeoutTransitionGroupChild = React.createClass(
|
|||
@queueClass activeClassName
|
||||
return
|
||||
|
||||
queueClass: (className) ->
|
||||
queueClass: (className) =>
|
||||
@classNameQueue.push className
|
||||
if !@timeout
|
||||
@timeout = setTimeout(@flushClassNameQueue, TICK)
|
||||
return
|
||||
|
||||
flushClassNameQueue: ->
|
||||
if @isMounted()
|
||||
@classNameQueue.forEach ((name) ->
|
||||
addClass(@getDOMNode(), name)
|
||||
return
|
||||
).bind(this)
|
||||
flushClassNameQueue: =>
|
||||
@classNameQueue.forEach ((name) =>
|
||||
addClass(React.findDOMNode(@), name)
|
||||
return
|
||||
).bind(this)
|
||||
@classNameQueue.length = 0
|
||||
@timeout = null
|
||||
return
|
||||
|
||||
componentWillMount: ->
|
||||
componentWillMount: =>
|
||||
@classNameQueue = []
|
||||
@animationTimeout = null
|
||||
@animationTaskId = null
|
||||
return
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
if @timeout
|
||||
clearTimeout(@timeout)
|
||||
if @animationTimeout
|
||||
|
@ -137,37 +138,36 @@ TimeoutTransitionGroupChild = React.createClass(
|
|||
@animationTaskId = null
|
||||
return
|
||||
|
||||
componentWillEnter: (done) ->
|
||||
componentWillEnter: (done) =>
|
||||
if @props.enter
|
||||
@transition 'enter', done
|
||||
else
|
||||
done()
|
||||
return
|
||||
|
||||
componentWillLeave: (done) ->
|
||||
componentWillLeave: (done) =>
|
||||
if @props.leave
|
||||
@transition 'leave', done
|
||||
else
|
||||
done()
|
||||
return
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
React.Children.only @props.children
|
||||
)
|
||||
|
||||
TimeoutTransitionGroup = React.createClass(
|
||||
propTypes:
|
||||
class TimeoutTransitionGroup extends React.Component
|
||||
@propTypes =
|
||||
enterTimeout: React.PropTypes.number.isRequired
|
||||
leaveTimeout: React.PropTypes.number.isRequired
|
||||
transitionName: React.PropTypes.string.isRequired
|
||||
transitionEnter: React.PropTypes.bool
|
||||
transitionLeave: React.PropTypes.bool
|
||||
|
||||
getDefaultProps: ->
|
||||
@defaultProps =
|
||||
transitionEnter: true
|
||||
transitionLeave: true
|
||||
|
||||
_wrapChild: (child) ->
|
||||
_wrapChild: (child) =>
|
||||
<TimeoutTransitionGroupChild
|
||||
enterTimeout={@props.enterTimeout}
|
||||
leaveTimeout={@props.leaveTimeout}
|
||||
|
@ -177,13 +177,10 @@ TimeoutTransitionGroup = React.createClass(
|
|||
{child}
|
||||
</TimeoutTransitionGroupChild>
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
<ReactTransitionGroup
|
||||
{...@props}
|
||||
childFactory={@_wrapChild} />
|
||||
)
|
||||
|
||||
module.exports = TimeoutTransitionGroup
|
||||
|
||||
# ---
|
||||
# generated by js2coffee 2.0.1
|
||||
module.exports = TimeoutTransitionGroup
|
|
@ -1,4 +1,5 @@
|
|||
React = require 'react/addons'
|
||||
classNames = require 'classnames'
|
||||
_ = require 'underscore-plus'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
{Contact, ContactStore} = require 'inbox-exports'
|
||||
|
@ -25,7 +26,7 @@ Token = React.createClass
|
|||
})
|
||||
|
||||
render: ->
|
||||
classes = React.addons.classSet
|
||||
classes = classNames
|
||||
"token": true
|
||||
"dragging": @getDragState('token').isDragging
|
||||
"selected": @props.selected
|
||||
|
@ -196,7 +197,7 @@ TokenizingTextField = React.createClass
|
|||
render: ->
|
||||
{Menu} = require 'ui-components'
|
||||
|
||||
classes = React.addons.classSet _.extend (@props.menuClassSet ? {}),
|
||||
classes = classNames _.extend (@props.menuClassSet ? {}),
|
||||
"tokenizing-field": true
|
||||
"focused": @state.focus
|
||||
"native-key-bindings": true
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
module.exports =
|
||||
CustomEventMixin =
|
||||
componentWillMount: ->
|
||||
@customEventListeners = {}
|
||||
|
||||
componentWillUnmount: ->
|
||||
for name, listeners in @customEventListeners
|
||||
for listener in listeners
|
||||
@getDOMNode().removeEventListener(name, listener)
|
||||
|
||||
addCustomEventListeners: (customEventListeners) ->
|
||||
for name, listener of customEventListeners
|
||||
@customEventListeners[name] ?= []
|
||||
@customEventListeners[name].push(listener)
|
||||
@getDOMNode().addEventListener(name, listener)
|
|
@ -14,7 +14,8 @@ TargetWindows =
|
|||
Message =
|
||||
DATABASE_STORE_TRIGGER: 'db-store-trigger'
|
||||
|
||||
###
|
||||
# Public: ActionBridge
|
||||
#
|
||||
# The ActionBridge has two responsibilities:
|
||||
# 1. When you're in a secondary window, the ActionBridge observes all Root actions. When a
|
||||
# Root action is fired, it converts it's payload to JSON, tunnels it to the main window
|
||||
|
@ -26,7 +27,7 @@ Message =
|
|||
# into all of the windows of the application. This is important, because the DatabaseStore
|
||||
# in all secondary windows is a read-replica. Only the DatabaseStore in the main window
|
||||
# of the application consumes persistModel actions and writes changes to the database.
|
||||
###
|
||||
|
||||
class ActionBridge
|
||||
@Role: Role
|
||||
@Message: Message
|
||||
|
|
|
@ -3,8 +3,6 @@ Actions = require '../actions'
|
|||
Attributes = require '../attributes'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
|
||||
##
|
||||
# Files are small objects that wrap attachments and other files available via the API.
|
||||
#
|
||||
|
@ -34,3 +32,6 @@ class File extends Model
|
|||
'contentId': Attributes.String
|
||||
modelKey: 'contentId'
|
||||
jsonKey: 'content_id'
|
||||
|
||||
|
||||
module.exports = File
|
|
@ -1,11 +1,8 @@
|
|||
{Matcher, NullPlaceholder, AttributeJoinedData} = require '../attributes'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
###
|
||||
# ModelQuery exposes an ActiveRecord-style syntax for building database queries.
|
||||
# Database: ModelQuery exposes an ActiveRecord-style syntax for building database queries.
|
||||
#
|
||||
# @namespace Application
|
||||
###
|
||||
class ModelQuery
|
||||
|
||||
##
|
||||
|
|
|
@ -9,10 +9,8 @@ Attributes = require '../attributes'
|
|||
Function::getter = (prop, get) ->
|
||||
Object.defineProperty @prototype, prop, {get, configurable: yes}
|
||||
|
||||
module.exports =
|
||||
##
|
||||
# @class Thread
|
||||
# @namespace Models
|
||||
#
|
||||
# Public: Thread
|
||||
#
|
||||
class Thread extends Model
|
||||
|
||||
|
@ -120,3 +118,6 @@ class Thread extends Model
|
|||
AddRemoveTagsTask = require '../tasks/add-remove-tags'
|
||||
task = new AddRemoveTagsTask(@id, tagIdsToAdd, tagIdsToRemove)
|
||||
Actions.queueTask(task)
|
||||
|
||||
|
||||
module.exports = Thread
|
|
@ -66,7 +66,9 @@ DraftStore = Reflux.createStore
|
|||
# Returns a promise
|
||||
|
||||
sessionForLocalId: (localId) ->
|
||||
throw new Error("sessionForLocalId requires a localId") unless localId
|
||||
if not localId
|
||||
console.log((new Error).stack)
|
||||
throw new Error("sessionForLocalId requires a localId")
|
||||
@_draftSessions[localId] ?= new DraftStoreProxy(localId)
|
||||
@_draftSessions[localId]
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ Utils = require '../models/utils'
|
|||
DatabaseStore = require './database-store'
|
||||
ModelView = require './model-view'
|
||||
|
||||
module.exports =
|
||||
# Public: Class here!
|
||||
class SearchView extends ModelView
|
||||
|
||||
constructor: (@_query, @_namespaceId) ->
|
||||
|
@ -13,6 +13,11 @@ class SearchView extends ModelView
|
|||
_.defer => @retrievePage(0)
|
||||
@
|
||||
|
||||
# Public: Takes an argument and does some stuff.
|
||||
#
|
||||
# a - A {String}
|
||||
#
|
||||
# Returns {Boolean}.
|
||||
query: ->
|
||||
@_query
|
||||
|
||||
|
@ -30,6 +35,11 @@ class SearchView extends ModelView
|
|||
# to retrieve pages.
|
||||
{start: start, end: end + 100}
|
||||
|
||||
# Public: It's my song turn it up.
|
||||
#
|
||||
# a - A {String}
|
||||
#
|
||||
# Returns {Boolean}.
|
||||
count: ->
|
||||
@_queryResultTotal
|
||||
|
||||
|
@ -72,3 +82,6 @@ class SearchView extends ModelView
|
|||
@_emitter.emit('trigger')
|
||||
|
||||
console.log("Search view fetched #{idx} in #{Date.now() - start} msec.")
|
||||
|
||||
|
||||
module.exports = SearchView
|
||||
|
|
|
@ -9,26 +9,27 @@ _ = require 'underscore-plus'
|
|||
ComponentRegistry,
|
||||
WorkspaceStore} = require "inbox-exports"
|
||||
|
||||
ToolbarSpacer = React.createClass
|
||||
className: 'ToolbarSpacer'
|
||||
propTypes:
|
||||
class ToolbarSpacer extends React.Component
|
||||
@displayName = 'ToolbarSpacer'
|
||||
@propTypes =
|
||||
order: React.PropTypes.number
|
||||
render: ->
|
||||
|
||||
render: =>
|
||||
<div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div>
|
||||
|
||||
ToolbarBack = React.createClass
|
||||
className: 'ToolbarBack'
|
||||
render: ->
|
||||
class ToolbarBack extends React.Component
|
||||
@displayName = 'ToolbarBack'
|
||||
render: =>
|
||||
<div className="item-back" onClick={@_onClick}>
|
||||
<RetinaImg name="sheet-back.png" />
|
||||
</div>
|
||||
|
||||
_onClick: ->
|
||||
_onClick: =>
|
||||
Actions.popSheet()
|
||||
|
||||
ToolbarWindowControls = React.createClass
|
||||
displayName: 'ToolbarWindowControls'
|
||||
render: ->
|
||||
class ToolbarWindowControls extends React.Component
|
||||
@displayName = 'ToolbarWindowControls'
|
||||
render: =>
|
||||
<div name="ToolbarWindowControls" className="toolbar-window-controls">
|
||||
<button className="close" onClick={ -> atom.close()}></button>
|
||||
<button className="minimize" onClick={ -> atom.minimize()}></button>
|
||||
|
@ -40,17 +41,17 @@ ComponentRegistry.register
|
|||
name: 'ToolbarWindowControls'
|
||||
location: WorkspaceStore.Sheet.Global.Toolbar.Left
|
||||
|
||||
Toolbar = React.createClass
|
||||
className: 'Toolbar'
|
||||
class Toolbar extends React.Component
|
||||
displayName = 'Toolbar'
|
||||
|
||||
propTypes:
|
||||
propTypes =
|
||||
data: React.PropTypes.object
|
||||
depth: React.PropTypes.number
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@unlisteners = []
|
||||
@unlisteners.push WorkspaceStore.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
@ -59,24 +60,24 @@ Toolbar = React.createClass
|
|||
window.addEventListener("resize", @_onWindowResize)
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
window.removeEventListener("resize", @_onWindowResize)
|
||||
unlistener() for unlistener in @unlisteners
|
||||
|
||||
componentWillReceiveProps: (props) ->
|
||||
@replaceState(@_getStateFromStores(props))
|
||||
componentWillReceiveProps: (props) =>
|
||||
@setState(@_getStateFromStores(props))
|
||||
|
||||
componentDidUpdate: ->
|
||||
componentDidUpdate: =>
|
||||
# Wait for other components that are dirty (the actual columns in the sheet)
|
||||
# to update as well.
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
# This is very important. Because toolbar uses ReactCSSTransitionGroup,
|
||||
# repetitive unnecessary updates can break animations and cause performance issues.
|
||||
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
style =
|
||||
position:'absolute'
|
||||
width:'100%'
|
||||
|
@ -94,7 +95,7 @@ Toolbar = React.createClass
|
|||
{toolbars}
|
||||
</div>
|
||||
|
||||
_flexboxForItems: (items) ->
|
||||
_flexboxForItems: (items) =>
|
||||
elements = items.map ({view, name}) =>
|
||||
<view key={name} {...@props} />
|
||||
|
||||
|
@ -110,11 +111,9 @@ Toolbar = React.createClass
|
|||
<ToolbarSpacer key="spacer+50" order={50}/>
|
||||
</TimeoutTransitionGroup>
|
||||
|
||||
recomputeLayout: ->
|
||||
return unless @isMounted()
|
||||
|
||||
recomputeLayout: =>
|
||||
# Find our item containers that are tied to specific columns
|
||||
columnToolbarEls = @getDOMNode().querySelectorAll('[data-column]')
|
||||
columnToolbarEls = React.findDOMNode(@).querySelectorAll('[data-column]')
|
||||
|
||||
# Find the top sheet in the stack
|
||||
sheet = document.querySelectorAll("[name='Sheet']")[@props.depth]
|
||||
|
@ -131,10 +130,10 @@ Toolbar = React.createClass
|
|||
columnToolbarEl.style.left = "#{columnEl.offsetLeft}px"
|
||||
columnToolbarEl.style.width = "#{columnEl.offsetWidth}px"
|
||||
|
||||
_onWindowResize: ->
|
||||
_onWindowResize: =>
|
||||
@recomputeLayout()
|
||||
|
||||
_getStateFromStores: (props) ->
|
||||
_getStateFromStores: (props) =>
|
||||
props ?= @props
|
||||
state =
|
||||
mode: WorkspaceStore.layoutMode()
|
||||
|
@ -160,23 +159,23 @@ Toolbar = React.createClass
|
|||
state
|
||||
|
||||
|
||||
FlexboxForLocations = React.createClass
|
||||
className: 'FlexboxForLocations'
|
||||
class FlexboxForLocations extends React.Component
|
||||
@displayName = 'FlexboxForLocations'
|
||||
|
||||
propTypes:
|
||||
@propTypes =
|
||||
locations: React.PropTypes.arrayOf(React.PropTypes.object)
|
||||
|
||||
getInitialState: ->
|
||||
@_getComponentRegistryState()
|
||||
constructor: (@props) ->
|
||||
@state = @_getComponentRegistryState()
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@unlistener = ComponentRegistry.listen (event) =>
|
||||
@setState(@_getComponentRegistryState())
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@unlistener() if @unlistener
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
# Note: we actually ignore props.roles. If roles change, but we get
|
||||
# the same items, we don't need to re-render. Our render function is
|
||||
# a function of state only.
|
||||
|
@ -184,7 +183,7 @@ FlexboxForLocations = React.createClass
|
|||
itemNames = @state.items?.map (i) -> i.name
|
||||
!_.isEqual(nextItemNames, itemNames)
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
elements = @state.items.map ({view, name}) =>
|
||||
<view key={name} />
|
||||
|
||||
|
@ -192,30 +191,29 @@ FlexboxForLocations = React.createClass
|
|||
{elements}
|
||||
</Flexbox>
|
||||
|
||||
_getComponentRegistryState: ->
|
||||
_getComponentRegistryState: =>
|
||||
items = []
|
||||
mode = WorkspaceStore.layoutMode()
|
||||
for location in @props.locations
|
||||
items = items.concat(ComponentRegistry.findAllByLocationAndMode(location, mode))
|
||||
{items}
|
||||
|
||||
module.exports =
|
||||
SheetContainer = React.createClass
|
||||
className: 'SheetContainer'
|
||||
class SheetContainer extends React.Component
|
||||
displayName = 'SheetContainer'
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@unsubscribe = WorkspaceStore.listen @_onStoreChange
|
||||
|
||||
# It's important that every React class explicitly stops listening to
|
||||
# atom events before it unmounts. Thank you event-kit
|
||||
# This can be fixed via a Reflux mixin
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
@unsubscribe() if @unsubscribe
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
totalSheets = @state.stack.length
|
||||
topSheet = @state.stack[totalSheets - 1]
|
||||
|
||||
|
@ -252,26 +250,28 @@ SheetContainer = React.createClass
|
|||
</div>
|
||||
</Flexbox>
|
||||
|
||||
_toolbarElements: ->
|
||||
_toolbarElements: =>
|
||||
@state.stack.map (sheet, index) ->
|
||||
<Toolbar data={sheet}
|
||||
ref={"toolbar-#{index}"}
|
||||
key={"#{index}:#{sheet.id}:toolbar"}
|
||||
depth={index} />
|
||||
|
||||
_sheetElements: ->
|
||||
_sheetElements: =>
|
||||
@state.stack.map (sheet, index) =>
|
||||
<Sheet data={sheet}
|
||||
depth={index}
|
||||
key={"#{index}:#{sheet.id}"}
|
||||
onColumnSizeChanged={@_onColumnSizeChanged} />
|
||||
|
||||
_onColumnSizeChanged: (sheet) ->
|
||||
_onColumnSizeChanged: (sheet) =>
|
||||
@refs["toolbar-#{sheet.props.depth}"]?.recomputeLayout()
|
||||
|
||||
_onStoreChange: ->
|
||||
_onStoreChange: =>
|
||||
_.defer => @setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: ->
|
||||
_getStateFromStores: =>
|
||||
stack: WorkspaceStore.sheetStack()
|
||||
|
||||
|
||||
module.exports = SheetContainer
|
|
@ -7,35 +7,34 @@ ResizableRegion = require './components/resizable-region'
|
|||
|
||||
FLEX = 10000
|
||||
|
||||
module.exports =
|
||||
Sheet = React.createClass
|
||||
displayName: 'Sheet'
|
||||
class Sheet extends React.Component
|
||||
@displayName = 'Sheet'
|
||||
|
||||
propTypes:
|
||||
@propTypes =
|
||||
data: React.PropTypes.object.isRequired
|
||||
depth: React.PropTypes.number.isRequired
|
||||
onColumnSizeChanged: React.PropTypes.func
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: ->
|
||||
componentDidMount: =>
|
||||
@unlisteners ?= []
|
||||
@unlisteners.push ComponentRegistry.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
@unlisteners.push WorkspaceStore.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
componentDidUpdate: ->
|
||||
componentDidUpdate: =>
|
||||
@props.onColumnSizeChanged(@) if @props.onColumnSizeChanged
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
|
||||
|
||||
componentWillUnmount: ->
|
||||
componentWillUnmount: =>
|
||||
unlisten() for unlisten in @unlisteners
|
||||
|
||||
render: ->
|
||||
render: =>
|
||||
style =
|
||||
position:'absolute'
|
||||
width:'100%'
|
||||
|
@ -57,7 +56,7 @@ Sheet = React.createClass
|
|||
</Flexbox>
|
||||
</div>
|
||||
|
||||
_columnFlexboxElements: ->
|
||||
_columnFlexboxElements: =>
|
||||
@state.columns.map ({entries, maxWidth, minWidth, handle, id}, idx) =>
|
||||
elements = entries.map ({name, view}) -> <view key={name} />
|
||||
if minWidth != maxWidth and maxWidth < FLEX
|
||||
|
@ -83,7 +82,7 @@ Sheet = React.createClass
|
|||
{elements}
|
||||
</Flexbox>
|
||||
|
||||
_getStateFromStores: ->
|
||||
_getStateFromStores: =>
|
||||
state =
|
||||
mode: WorkspaceStore.layoutMode()
|
||||
columns: []
|
||||
|
@ -116,5 +115,7 @@ Sheet = React.createClass
|
|||
state.columns[i].handle = ResizableRegion.Handle.Left for i in [widest..state.columns.length-1] by 1
|
||||
state
|
||||
|
||||
_pop: ->
|
||||
_pop: =>
|
||||
Actions.popSheet()
|
||||
|
||||
module.exports = Sheet
|
Loading…
Add table
Reference in a new issue