diff --git a/build/Gruntfile.coffee b/build/Gruntfile.coffee index c6ff6affc..027c45cec 100644 --- a/build/Gruntfile.coffee +++ b/build/Gruntfile.coffee @@ -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' diff --git a/build/package.json b/build/package.json index 496a06793..75fe9111f 100644 --- a/build/package.json +++ b/build/package.json @@ -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", diff --git a/build/tasks/docs-task.coffee b/build/tasks/docs-task.coffee index 2be567ddb..832d9cd4e 100644 --- a/build/tasks/docs-task.coffee +++ b/build/tasks/docs-task.coffee @@ -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], "#{match[1]}") + 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, "#{label}") + 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) diff --git a/coffeelint.json b/coffeelint.json index d8e19fc46..38ed9d3cb 100644 --- a/coffeelint.json +++ b/coffeelint.json @@ -8,6 +8,9 @@ "arrow_spacing": { "level": "error" }, + "no_unnecessary_fat_arrows": { + "level": "ignore" + }, "no_interpolation_in_single_quotes": { "level": "error" }, diff --git a/exports/ui-components.coffee b/exports/ui-components.coffee index 23073d945..623b5abe3 100644 --- a/exports/ui-components.coffee +++ b/exports/ui-components.coffee @@ -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 diff --git a/internal_packages/account-sidebar/lib/account-sidebar-sheet-item.cjsx b/internal_packages/account-sidebar/lib/account-sidebar-sheet-item.cjsx index 6d4216f8a..7c7061dbe 100644 --- a/internal_packages/account-sidebar/lib/account-sidebar-sheet-item.cjsx +++ b/internal_packages/account-sidebar/lib/account-sidebar-sheet-item.cjsx @@ -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 diff --git a/internal_packages/account-sidebar/lib/account-sidebar-tag-item.cjsx b/internal_packages/account-sidebar/lib/account-sidebar-tag-item.cjsx index 8aa28f79b..d6923b703 100644 --- a/internal_packages/account-sidebar/lib/account-sidebar-tag-item.cjsx +++ b/internal_packages/account-sidebar/lib/account-sidebar-tag-item.cjsx @@ -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 =
{@props.item.unreadCount}
- classSet = React.addons.classSet + coontainerClass = classNames 'item': true 'selected': @props.select -
+
{@props.item.name} {unread} diff --git a/internal_packages/composer/lib/composer-view.cjsx b/internal_packages/composer/lib/composer-view.cjsx index 0931256c3..32762bf93 100644 --- a/internal_packages/composer/lib/composer-view.cjsx +++ b/internal_packages/composer/lib/composer-view.cjsx @@ -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" />
-
- {@_fileComponents()} - -
- + {@_renderFooterRegions()}
- - - - - - -
- - - - + {@_renderActionsRegion()}
+ _renderFooterRegions: -> + return
unless @props.localId + + +
+ { + (@state.files ? []).map (file) => + + } + +
+ +
+ + _renderActionsRegion: -> + return
unless @props.localId + + + + + + + +
+ + + + + # 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) => - - _onDraftChanged: -> draft = @_proxy.draft() if not @_initialHistorySave diff --git a/internal_packages/composer/lib/contenteditable-component.cjsx b/internal_packages/composer/lib/contenteditable-component.cjsx index d46e80fa9..c6ad73e49 100644 --- a/internal_packages/composer/lib/contenteditable-component.cjsx +++ b/internal_packages/composer/lib/contenteditable-component.cjsx @@ -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: =>
- 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
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, "
"), 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(/()?()?<[^>]*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 \ No newline at end of file diff --git a/internal_packages/composer/lib/contenteditable-toolbar.cjsx b/internal_packages/composer/lib/contenteditable-toolbar.cjsx index 90f74612c..a988ffd05 100644 --- a/internal_packages/composer/lib/contenteditable-toolbar.cjsx +++ b/internal_packages/composer/lib/contenteditable-toolbar.cjsx @@ -7,7 +7,7 @@ # # module.exports = # ContenteditableToolbar = React.createClass -# render: -> +# render: => # style = # display: @state.show and 'initial' or 'none' #
@@ -21,19 +21,19 @@ #
#
# -# 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 diff --git a/internal_packages/composer/lib/floating-toolbar.cjsx b/internal_packages/composer/lib/floating-toolbar.cjsx index bebda2409..586640e27 100644 --- a/internal_packages/composer/lib/floating-toolbar.cjsx +++ b/internal_packages/composer/lib/floating-toolbar.cjsx @@ -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: =>
{@_toolbarType()}
- _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
- _renderButtons: -> + _renderButtons: =>
- _renderLink: -> + _renderLink: => removeBtn = "" withRemove = "" if @_initialUrl() @@ -101,49 +102,49 @@ FloatingToolbar = React.createClass {removeBtn} - _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 diff --git a/internal_packages/composer/spec/contenteditable-component-spec.cjsx b/internal_packages/composer/spec/contenteditable-component-spec.cjsx index 59627bc34..f9663535e 100644 --- a/internal_packages/composer/spec/contenteditable-component-spec.cjsx +++ b/internal_packages/composer/spec/contenteditable-component-spec.cjsx @@ -14,7 +14,7 @@ describe "ContenteditableComponent", -> @component = ReactTestUtils.renderIntoDocument( ) - @editableNode = ReactTestUtils.findRenderedDOMComponentWithAttr(@component, 'contentEditable').getDOMNode() + @editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(@component, 'contentEditable')) describe "render", -> it 'should render into the document', -> diff --git a/internal_packages/composer/spec/contenteditable-quoted-text-spec.cjsx b/internal_packages/composer/spec/contenteditable-quoted-text-spec.cjsx index d390bbe27..9e441d37f 100644 --- a/internal_packages/composer/spec/contenteditable-quoted-text-spec.cjsx +++ b/internal_packages/composer/spec/contenteditable-quoted-text-spec.cjsx @@ -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", -> diff --git a/internal_packages/composer/spec/inbox-composer-view-spec.cjsx b/internal_packages/composer/spec/inbox-composer-view-spec.cjsx index 6ebc81871..591b61880 100644 --- a/internal_packages/composer/spec/inbox-composer-view-spec.cjsx +++ b/internal_packages/composer/spec/inbox-composer-view-spec.cjsx @@ -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() diff --git a/internal_packages/composer/spec/participants-text-field-spec.cjsx b/internal_packages/composer/spec/participants-text-field-spec.cjsx index 3fe57c7f6..c2bab76b8 100644 --- a/internal_packages/composer/spec/participants-text-field-spec.cjsx +++ b/internal_packages/composer/spec/participants-text-field-spec.cjsx @@ -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}}) diff --git a/internal_packages/inbox-activity-bar/lib/activity-bar-task.cjsx b/internal_packages/inbox-activity-bar/lib/activity-bar-task.cjsx index 48439afca..e47b15464 100644 --- a/internal_packages/inbox-activity-bar/lib/activity-bar-task.cjsx +++ b/internal_packages/inbox-activity-bar/lib/activity-bar-task.cjsx @@ -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" diff --git a/internal_packages/message-list/lib/email-frame.cjsx b/internal_packages/message-list/lib/email-frame.cjsx index ae208d236..18003fbb0 100644 --- a/internal_packages/message-list/lib/email-frame.cjsx +++ b/internal_packages/message-list/lib/email-frame.cjsx @@ -105,48 +105,43 @@ EmailFixingStyles = """ """ -module.exports = -EmailFrame = React.createClass - displayName: 'EmailFrame' +class EmailFrame extends React.Component + @displayName = 'EmailFrame' - render: -> + render: => - 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("
#{@_emailContent()}
") 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 \ No newline at end of file diff --git a/internal_packages/message-list/lib/message-item.cjsx b/internal_packages/message-list/lib/message-item.cjsx index 32215760e..68d18e084 100644 --- a/internal_packages/message-list/lib/message-item.cjsx +++ b/internal_packages/message-list/lib/message-item.cjsx @@ -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: =>
@@ -66,7 +66,7 @@ MessageItem = React.createClass
- _renderFull: -> + _renderFull: =>
{@_renderHeader()} @@ -78,7 +78,7 @@ MessageItem = React.createClass
- _renderHeader: -> + _renderHeader: =>
@@ -86,7 +86,7 @@ MessageItem = React.createClass isDetailed={@state.detailedHeaders} date={@props.message.date} /> - @@ -107,22 +107,22 @@ MessageItem = React.createClass
- _renderAttachments: -> + _renderAttachments: => attachments = @_attachmentComponents() if attachments.length > 0
{attachments}
else
- _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 ## 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}> # - _renderMessageActions: -> + _renderMessageActions: =>
- @@ -148,22 +148,22 @@ MessageItem = React.createClass - +
- _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
+ _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) => - + - _isForwardedMessage: -> + _isForwardedMessage: => Utils.isForwardedMessage(@props.message) - _onDownloadStoreChange: -> + _onDownloadStoreChange: => @setState downloads: FileDownloadStore.downloadsForFileIds(@props.message.fileIds()) + +module.exports = MessageItem \ No newline at end of file diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx index a98b7b5f0..7f9bce8ad 100755 --- a/internal_packages/message-list/lib/message-list.cjsx +++ b/internal_packages/message-list/lib/message-list.cjsx @@ -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
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"> - - @@ -93,7 +93,7 @@ MessageList = React.createClass
- _renderReplyArea: -> + _renderReplyArea: => if @_hasReplyArea()
@@ -102,17 +102,17 @@ MessageList = React.createClass
else return
- _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 - 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 diff --git a/internal_packages/message-list/lib/message-participants.cjsx b/internal_packages/message-list/lib/message-participants.cjsx index 23c90290e..b9b872160 100644 --- a/internal_packages/message-list/lib/message-participants.cjsx +++ b/internal_packages/message-list/lib/message-participants.cjsx @@ -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 diff --git a/internal_packages/message-list/lib/message-toolbar-items.cjsx b/internal_packages/message-list/lib/message-toolbar-items.cjsx index f2377e5b5..c03104c8e 100644 --- a/internal_packages/message-list/lib/message-toolbar-items.cjsx +++ b/internal_packages/message-list/lib/message-toolbar-items.cjsx @@ -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 diff --git a/internal_packages/message-list/lib/sidebar-thread-participants.cjsx b/internal_packages/message-list/lib/sidebar-thread-participants.cjsx index 4811266b3..631a211d0 100644 --- a/internal_packages/message-list/lib/sidebar-thread-participants.cjsx +++ b/internal_packages/message-list/lib/sidebar-thread-participants.cjsx @@ -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: =>

Thread Participants

{@_renderSortedContacts()}
- _renderSortedContacts: -> + _renderSortedContacts: => contacts = [] @state.sortedContacts.forEach (contact) => if contact is @state.focusedContact @@ -31,18 +31,21 @@ SidebarThreadParticipants = React.createClass contacts.push(
@_onSelectContact(contact)} - key={contact.id}> + key={contact.email+contact.name}> {contact.name}
) return contacts - _onSelectContact: (contact) -> + _onSelectContact: (contact) => Actions.focusContact(contact) - _onChange: -> + _onChange: => @setState(@_getStateFromStores()) - _getStateFromStores: -> + _getStateFromStores: => sortedContacts: FocusedContactsStore.sortedContacts() focusedContact: FocusedContactsStore.focusedContact() + + +module.exports = SidebarThreadParticipants \ No newline at end of file diff --git a/internal_packages/message-list/spec/message-item-spec.cjsx b/internal_packages/message-list/spec/message-item-spec.cjsx index 9abcb5b09..a065833bd 100644 --- a/internal_packages/message-list/spec/message-item-spec.cjsx +++ b/internal_packages/message-list/spec/message-item-spec.cjsx @@ -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) diff --git a/internal_packages/message-list/spec/message-list-spec.cjsx b/internal_packages/message-list/spec/message-list-spec.cjsx index e14936dc9..35f8c77cd 100644 --- a/internal_packages/message-list/spec/message-list-spec.cjsx +++ b/internal_packages/message-list/spec/message-list-spec.cjsx @@ -197,7 +197,7 @@ describe "MessageList", -> false @message_list = TestUtils.renderIntoDocument() - @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 diff --git a/internal_packages/message-list/spec/message-participants-spec.cjsx b/internal_packages/message-list/spec/message-participants-spec.cjsx index 4acb58502..ff3cdbadf 100644 --- a/internal_packages/message-list/spec/message-participants-spec.cjsx +++ b/internal_packages/message-list/spec/message-participants-spec.cjsx @@ -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 " + expect(React.findDOMNode(to).innerText.trim()).toEqual "User Two " # TODO: We no longer display "to everyone" diff --git a/internal_packages/message-list/spec/message-toolbar-items-spec.cjsx b/internal_packages/message-list/spec/message-toolbar-items-spec.cjsx index 374c7b5ab..5b3c0fa74 100644 --- a/internal_packages/message-list/spec/message-toolbar-items-spec.cjsx +++ b/internal_packages/message-list/spec/message-toolbar-items-spec.cjsx @@ -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() diff --git a/internal_packages/message-templates/lib/template-picker.cjsx b/internal_packages/message-templates/lib/template-picker.cjsx index 655145689..e8017c341 100644 --- a/internal_packages/message-templates/lib/template-picker.cjsx +++ b/internal_packages/message-templates/lib/template-picker.cjsx @@ -23,6 +23,7 @@ TemplatePicker = React.createClass + headerComponents = [ + 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: => {@_pageComponent()}
- _pageComponent: -> + _pageComponent: => if @state.error alert =
{@state.error}
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() }) }
@@ -118,7 +118,7 @@ ContainerView = React.createClass
- _environmentComponent: -> + _environmentComponent: => return [] unless atom.inDevMode()
- _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() diff --git a/internal_packages/search-bar/lib/search-bar.cjsx b/internal_packages/search-bar/lib/search-bar.cjsx index e56e3698d..812aba648 100644 --- a/internal_packages/search-bar/lib/search-bar.cjsx +++ b/internal_packages/search-bar/lib/search-bar.cjsx @@ -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}>
] - itemContentFunc = (item) -> + itemContentFunc = (item) => if item.divider else if item.contact @@ -75,19 +77,18 @@ SearchBar = React.createClass /> - _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 \ No newline at end of file diff --git a/internal_packages/search-bar/stylesheets/search-bar.less b/internal_packages/search-bar/stylesheets/search-bar.less index 1e1e37f34..b435d2d3f 100644 --- a/internal_packages/search-bar/stylesheets/search-bar.less +++ b/internal_packages/search-bar/stylesheets/search-bar.less @@ -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; diff --git a/internal_packages/search-playground/lib/search-bar.cjsx b/internal_packages/search-playground/lib/search-bar.cjsx index 9ac177b62..8a813a943 100644 --- a/internal_packages/search-playground/lib/search-bar.cjsx +++ b/internal_packages/search-playground/lib/search-bar.cjsx @@ -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}> ] - itemContentFunc = (item) -> + itemContentFunc = (item) => if item.divider else if item.contact @@ -76,19 +77,19 @@ SearchBar = React.createClass /> - _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 \ No newline at end of file diff --git a/internal_packages/thread-list/lib/thread-buttons.cjsx b/internal_packages/thread-list/lib/thread-buttons.cjsx index 769d487d2..b640b2264 100644 --- a/internal_packages/thread-list/lib/thread-buttons.cjsx +++ b/internal_packages/thread-list/lib/thread-buttons.cjsx @@ -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 _classSet: -> - React.addons.classSet + classNames "message-toolbar-arrow": true "down": true "disabled": @state.disabled @@ -77,7 +78,7 @@ UpButton = React.createClass _classSet: -> - React.addons.classSet + classNames "message-toolbar-arrow": true "up": true "disabled": @state.disabled diff --git a/internal_packages/thread-list/lib/thread-list-store.coffee b/internal_packages/thread-list/lib/thread-list-store.coffee index bfde9dae2..ed01d244b 100644 --- a/internal_packages/thread-list/lib/thread-list-store.coffee +++ b/internal_packages/thread-list/lib/thread-list-store.coffee @@ -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: -> diff --git a/internal_packages/thread-list/lib/thread-list.cjsx b/internal_packages/thread-list/lib/thread-list.cjsx index 4af80b4a2..6eb8389b4 100644 --- a/internal_packages/thread-list/lib/thread-list.cjsx +++ b/internal_packages/thread-list/lib/thread-list.cjsx @@ -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: -> diff --git a/internal_packages/thread-list/spec/thread-list-spec.cjsx b/internal_packages/thread-list/spec/thread-list-spec.cjsx index 9e84aad07..3162f9e64 100644 --- a/internal_packages/thread-list/spec/thread-list-spec.cjsx +++ b/internal_packages/thread-list/spec/thread-list-spec.cjsx @@ -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", -> diff --git a/package.json b/package.json index 806a061c7..13d384662 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec-inbox/components/tokenizing-text-field-spec.cjsx b/spec-inbox/components/tokenizing-text-field-spec.cjsx index 8422f05ca..1552bd089 100644 --- a/spec-inbox/components/tokenizing-text-field-spec.cjsx +++ b/spec-inbox/components/tokenizing-text-field-spec.cjsx @@ -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) -> diff --git a/spec-inbox/database-view-spec.coffee b/spec-inbox/database-view-spec.coffee index 2ca5954d0..e454f4f63 100644 --- a/spec-inbox/database-view-spec.coffee +++ b/spec-inbox/database-view-spec.coffee @@ -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') diff --git a/src/component-registry.coffee b/src/component-registry.coffee index 0496085e4..94b091114 100644 --- a/src/component-registry.coffee +++ b/src/component-registry.coffee @@ -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 = diff --git a/src/components/evented-iframe.cjsx b/src/components/evented-iframe.cjsx index 9eaed3534..23922108b 100644 --- a/src/components/evented-iframe.cjsx +++ b/src/components/evented-iframe.cjsx @@ -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: =>