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:
Ben Gotow 2015-04-24 11:33:10 -07:00
parent 365fe400f7
commit 68343ec472
61 changed files with 1021 additions and 805 deletions

View file

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

View file

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

View file

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

View file

@ -8,6 +8,9 @@
"arrow_spacing": {
"level": "error"
},
"no_unnecessary_fat_arrows": {
"level": "ignore"
},
"no_interpolation_in_single_quotes": {
"level": "error"
},

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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', ->

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,6 +23,7 @@ TemplatePicker = React.createClass
<RetinaImg name="toolbar-templates.png"/>
<RetinaImg name="toolbar-chevron.png"/>
</button>
headerComponents = [
<input type="text"
tabIndex="1"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
##

View file

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

View file

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

View file

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

View file

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

View file

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