mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-29 16:06:31 +08:00
feat(ui): Updates from March 17th mockups
Summary: There are two known issues: - toolbar is not draggable in some areas when in three-pane mode. - archive button appears over very long subjects. Propose moving this button elsewhere. WIP WIP Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://review.inboxapp.com/D1311
This commit is contained in:
parent
015bd62937
commit
aad21317e5
27 changed files with 507 additions and 159 deletions
|
@ -80,7 +80,7 @@ module.exports =
|
|||
ComponentRegistry.register
|
||||
view: NewComposeButton
|
||||
name: 'NewComposeButton'
|
||||
role: 'Global:Toolbar'
|
||||
role: 'Root:Left:Toolbar'
|
||||
|
||||
_showInitialErrorDialog: (msg) ->
|
||||
remote = require('remote')
|
||||
|
|
|
@ -5,7 +5,7 @@ React = require 'react'
|
|||
module.exports =
|
||||
NewComposeButton = React.createClass
|
||||
render: ->
|
||||
<button style={order: -100}
|
||||
<button style={order: 101}
|
||||
className="btn btn-toolbar"
|
||||
data-tooltip="Compose new message"
|
||||
onClick={@_onNewCompose}>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@import "buttons";
|
||||
|
||||
@compose-width: 800px;
|
||||
@compose-min-height: 250px;
|
||||
@compose-min-height: 150px;
|
||||
|
||||
.composer-inner-wrap {
|
||||
position: relative;
|
||||
|
@ -257,7 +257,9 @@ body.is-blurred .composer-inner-wrap .tokenizing-field .token {
|
|||
#message-list {
|
||||
.message-item-wrap.composer-outer-wrap {
|
||||
padding-top: @spacing-standard;
|
||||
background: @background-off-primary;
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(@background-off-primary), to(@background-primary));
|
||||
background-repeat:no-repeat;
|
||||
-webkit-background-size:100% 150px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -189,7 +189,7 @@ EmailFrame = React.createClass
|
|||
@getDOMNode().dispatchEvent(new KeyboardEvent(event.type, event))
|
||||
|
||||
_delegateMouseEvents: (doc, method="addEventListener") ->
|
||||
for type in ["mousemove", "mouseup", "mousedown", "mouseover", "mouseout"]
|
||||
for type in ["mouseup", "mousedown"]
|
||||
doc?[method]?(type, @_onMouseEvent)
|
||||
|
||||
_onMouseEvent: (event) ->
|
||||
|
|
|
@ -1,7 +1,28 @@
|
|||
React = require "react"
|
||||
MessageList = require "./message-list"
|
||||
MessageToolbarItems = require "./message-toolbar-items.cjsx"
|
||||
MessageToolbarItems = require "./message-toolbar-items"
|
||||
MessageSubjectItem = require "./message-subject-item"
|
||||
{ComponentRegistry} = require 'inbox-exports'
|
||||
{RetinaImg} = require 'ui-components'
|
||||
|
||||
DownButton = React.createClass
|
||||
render: ->
|
||||
<div className="message-toolbar-arrow down" onClick={@_onClick}>
|
||||
<RetinaImg name="toolbar-down-arrow.png"/>
|
||||
</div>
|
||||
|
||||
_onClick: ->
|
||||
atom.commands.dispatch(document.body, 'application:next-item')
|
||||
|
||||
UpButton = React.createClass
|
||||
render: ->
|
||||
<div className="message-toolbar-arrow up" onClick={@_onClick}>
|
||||
<RetinaImg name="toolbar-up-arrow.png"/>
|
||||
</div>
|
||||
|
||||
_onClick: ->
|
||||
atom.commands.dispatch(document.body, 'application:previous-item')
|
||||
|
||||
|
||||
module.exports =
|
||||
item: null # The DOM item the main React component renders into
|
||||
|
@ -32,6 +53,24 @@ module.exports =
|
|||
mode: 'list'
|
||||
view: MessageToolbarItems
|
||||
|
||||
ComponentRegistry.register
|
||||
name: 'MessageSubjectItem'
|
||||
role: 'Thread:Center:Toolbar'
|
||||
mode: 'list'
|
||||
view: MessageSubjectItem
|
||||
|
||||
ComponentRegistry.register
|
||||
name: 'DownButton'
|
||||
role: 'Thread:Right:Toolbar'
|
||||
mode: 'list'
|
||||
view: DownButton
|
||||
|
||||
ComponentRegistry.register
|
||||
name: 'UpButton'
|
||||
role: 'Thread:Right:Toolbar'
|
||||
mode: 'list'
|
||||
view: UpButton
|
||||
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister 'MessageToolbarItems'
|
||||
|
|
|
@ -36,6 +36,9 @@ MessageItem = React.createClass
|
|||
componentWillUnmount: ->
|
||||
@_storeUnlisten() if @_storeUnlisten
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
|
||||
|
||||
render: ->
|
||||
messageIndicators = ComponentRegistry.findAllViewsByRole('MessageIndicator')
|
||||
attachments = @_attachmentComponents()
|
||||
|
|
|
@ -23,6 +23,9 @@ MessageList = React.createClass
|
|||
componentWillUnmount: ->
|
||||
unsubscribe() for unsubscribe in @_unsubscribers
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
|
||||
|
||||
componentDidUpdate: (prevProps, prevState) ->
|
||||
didLoad = prevState.messages.length is 0 and @state.messages.length > 0
|
||||
|
||||
|
@ -48,6 +51,7 @@ MessageList = React.createClass
|
|||
|
||||
render: ->
|
||||
return <div></div> if not @state.currentThread?
|
||||
|
||||
wrapClass = React.addons.classSet
|
||||
"messages-wrap": true
|
||||
"ready": @state.ready
|
||||
|
@ -74,7 +78,7 @@ MessageList = React.createClass
|
|||
lastHeight = -1
|
||||
stableCount = 0
|
||||
scrollIfSettled = =>
|
||||
return done() unless @isMounted()
|
||||
return unless @isMounted()
|
||||
|
||||
messageWrapHeight = messageWrap.getBoundingClientRect().height
|
||||
if messageWrapHeight isnt lastHeight
|
||||
|
@ -101,8 +105,6 @@ MessageList = React.createClass
|
|||
MessageListHeaders = ComponentRegistry.findAllViewsByRole('MessageListHeader')
|
||||
|
||||
<div className="message-list-headers">
|
||||
<h2 className="message-subject">{@state.currentThread.subject}</h2>
|
||||
|
||||
{for MessageListHeader in MessageListHeaders
|
||||
<MessageListHeader thread={@state.currentThread} />
|
||||
}
|
||||
|
@ -151,9 +153,12 @@ MessageList = React.createClass
|
|||
ready: if MessageStore.itemsLoading() then false else @state?.ready ? false
|
||||
|
||||
_prepareContentForDisplay: ->
|
||||
focusedMessage = @getDOMNode().querySelector(".initial-focus")
|
||||
@scrollToMessage focusedMessage, =>
|
||||
@setState(ready: true)
|
||||
_.delay =>
|
||||
return unless @isMounted()
|
||||
focusedMessage = @getDOMNode().querySelector(".initial-focus")
|
||||
@scrollToMessage focusedMessage, =>
|
||||
@setState(ready: true)
|
||||
, 100
|
||||
|
||||
_threadParticipants: ->
|
||||
# We calculate the list of participants instead of grabbing it from
|
||||
|
|
25
internal_packages/message-list/lib/message-subject-item.cjsx
Normal file
25
internal_packages/message-list/lib/message-subject-item.cjsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
React = require 'react'
|
||||
{ThreadStore} = require 'inbox-exports'
|
||||
|
||||
module.exports =
|
||||
MessageSubjectItem = React.createClass
|
||||
displayName: 'MessageSubjectItem'
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
|
||||
componentDidMount: ->
|
||||
@_unsubscriber = ThreadStore.listen @_onChange
|
||||
|
||||
componentWillUnmount: ->
|
||||
@_unsubscriber() if @_unsubscriber
|
||||
|
||||
render: ->
|
||||
<div className="message-toolbar-subject">{@state.thread?.subject}</div>
|
||||
|
||||
_onChange: ->
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: ->
|
||||
thread: ThreadStore.selectedThread()
|
||||
|
|
@ -71,9 +71,6 @@ module.exports = React.createClass
|
|||
|
||||
<div className={classes}>
|
||||
<div className="message-toolbar-items-inner">
|
||||
<ReplyButton />
|
||||
<ReplyAllButton />
|
||||
<ForwardButton />
|
||||
<ArchiveButton />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,26 +3,56 @@
|
|||
|
||||
@message-max-width: 800px;
|
||||
|
||||
// This class wraps the items that appear above the message list in the
|
||||
// toolbar. We want the toolbar items to sit right above the centered
|
||||
// content, so we need another 800px-wide container in the toolbar...
|
||||
.message-toolbar-items {
|
||||
order: -100;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
.sheet-toolbar {
|
||||
// This class wraps the items that appear above the message list in the
|
||||
// toolbar. We want the toolbar items to sit right above the centered
|
||||
// content, so we need another 800px-wide container in the toolbar...
|
||||
.message-toolbar-items {
|
||||
order: -10000;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
|
||||
.message-toolbar-items-inner {
|
||||
margin: auto;
|
||||
max-width: @message-max-width - @spacing-three-quarters*2;
|
||||
text-align: left;
|
||||
pointer-events: auto;
|
||||
.message-toolbar-items-inner {
|
||||
margin: auto;
|
||||
max-width: @message-max-width - @spacing-three-quarters*2;
|
||||
text-align: right;
|
||||
pointer-events: none;
|
||||
& > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-toolbar-items.hidden {
|
||||
opacity: 0;
|
||||
.message-toolbar-items.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.message-toolbar-subject {
|
||||
order:-99;
|
||||
cursor: default;
|
||||
color:@text-color-heading;
|
||||
-webkit-app-region: drag;
|
||||
margin:0;
|
||||
margin-top:13px;
|
||||
font-size: @font-size-h4;
|
||||
font-weight: @font-weight-normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.message-toolbar-arrow.down {
|
||||
order:101;
|
||||
padding-top:6px;
|
||||
}
|
||||
.message-toolbar-arrow.up {
|
||||
order:102;
|
||||
padding-top:6px;
|
||||
// <1 because of hit region padding on the button
|
||||
margin-right: @spacing-standard * 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
#message-list {
|
||||
|
@ -36,8 +66,6 @@
|
|||
|
||||
.message-list-headers {
|
||||
margin: 0 auto;
|
||||
padding: @spacing-double;
|
||||
padding-bottom: @spacing-standard;
|
||||
width: 100%;
|
||||
max-width: @message-max-width;
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{ComponentRegistry} = require 'inbox-exports'
|
||||
ModeSwitch = require './mode-switch'
|
||||
ModeToggle = require './mode-toggle'
|
||||
|
||||
module.exports =
|
||||
activate: (state) ->
|
||||
ComponentRegistry.register
|
||||
name: 'ModeSwitch'
|
||||
view: ModeSwitch
|
||||
role: 'Root:Toolbar'
|
||||
name: 'ModeToggle'
|
||||
view: ModeToggle
|
||||
role: 'Root:Center:Toolbar'
|
||||
|
|
38
internal_packages/mode-switch/lib/mode-toggle.cjsx
Normal file
38
internal_packages/mode-switch/lib/mode-toggle.cjsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
{ComponentRegistry,
|
||||
WorkspaceStore,
|
||||
Actions} = require "inbox-exports"
|
||||
{RetinaImg} = require 'ui-components'
|
||||
React = require "react"
|
||||
_ = require "underscore-plus"
|
||||
|
||||
module.exports =
|
||||
ModeToggle = React.createClass
|
||||
displayName: 'ModeToggle'
|
||||
|
||||
getInitialState: ->
|
||||
mode: WorkspaceStore.selectedLayoutMode()
|
||||
|
||||
componentDidMount: ->
|
||||
@unsubscribe = WorkspaceStore.listen(@_onStateChanged, @)
|
||||
|
||||
componentWillUnmount: ->
|
||||
@unsubscribe?()
|
||||
|
||||
render: ->
|
||||
<div className="mode-switch"
|
||||
style={order:51, marginTop:10, marginRight:14}
|
||||
onClick={@_onToggleMode}>
|
||||
<RetinaImg
|
||||
name="toolbar-icon-toggle-pane.png"
|
||||
onClick={@_onToggleMode} />
|
||||
</div>
|
||||
|
||||
_onStateChanged: ->
|
||||
@setState
|
||||
mode: WorkspaceStore.selectedLayoutMode()
|
||||
|
||||
_onToggleMode: ->
|
||||
if @state.mode is 'list'
|
||||
Actions.selectLayoutMode('split')
|
||||
else
|
||||
Actions.selectLayoutMode('list')
|
|
@ -13,7 +13,7 @@ module.exports =
|
|||
ComponentRegistry.register
|
||||
view: SearchBar
|
||||
name: 'SearchBar'
|
||||
role: 'Global:Toolbar'
|
||||
role: 'Root:Center:Toolbar'
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister 'SearchBar'
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
order: 100;
|
||||
order: -100;
|
||||
overflow: visible;
|
||||
z-index: 100;
|
||||
-webkit-user-select: none;
|
||||
width:270px;
|
||||
width:450px;
|
||||
margin-top: (50px - 30px) / 2;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
|
@ -19,12 +19,12 @@
|
|||
border:none;
|
||||
|
||||
input {
|
||||
padding-left:25px;
|
||||
padding-left:30px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
}
|
||||
input.empty {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
input.empty:focus {
|
||||
text-align: left;
|
||||
|
|
|
@ -8,8 +8,18 @@ Spinner = React.createClass
|
|||
style: React.PropTypes.object
|
||||
|
||||
getInitialState: ->
|
||||
hidden: false
|
||||
paused: false
|
||||
hidden: true
|
||||
paused: true
|
||||
|
||||
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
|
||||
# it's associated animation delay.
|
||||
if @props.visible
|
||||
_.defer =>
|
||||
return unless @isMounted()
|
||||
@setState({paused: false, hidden: false})
|
||||
|
||||
componentWillReceiveProps: (nextProps) ->
|
||||
hidden = if nextProps.visible? then !nextProps.visible else false
|
||||
|
|
162
src/components/timeout-transition-group.cjsx
Normal file
162
src/components/timeout-transition-group.cjsx
Normal file
|
@ -0,0 +1,162 @@
|
|||
# WHY IS THIS FILE HERE? ReactCSSTransitionGroup is causing
|
||||
# inconsitency exceptions when you hammer on the animations and don't let them
|
||||
# finish. This is from http://khan.github.io/react-components/#timeout-transition-group
|
||||
# and uses timeouts to clean up elements rather than listeners on CSS events, which
|
||||
# don't always seem to fire.
|
||||
|
||||
# https://github.com/facebook/react/issues/1707
|
||||
|
||||
###*
|
||||
# The CSSTransitionGroup component uses the 'transitionend' event, which
|
||||
# browsers will not send for any number of reasons, including the
|
||||
# transitioning node not being painted or in an unfocused tab.
|
||||
#
|
||||
# This TimeoutTransitionGroup instead uses a user-defined timeout to determine
|
||||
# when it is a good time to remove the component. Currently there is only one
|
||||
# timeout specified, but in the future it would be nice to be able to specify
|
||||
# separate timeouts for enter and leave, in case the timeouts for those
|
||||
# animations differ. Even nicer would be some sort of inspection of the CSS to
|
||||
# automatically determine the duration of the animation or transition.
|
||||
#
|
||||
# This is adapted from Facebook's CSSTransitionGroup which is in the React
|
||||
# addons and under the Apache 2.0 License.
|
||||
###
|
||||
|
||||
React = require('react/addons')
|
||||
ReactTransitionGroup = React.addons.TransitionGroup
|
||||
TICK = 17
|
||||
|
||||
endEvents = ['webkitTransitionEnd', 'webkitAnimationEnd']
|
||||
|
||||
animationSupported = ->
|
||||
endEvents.length != 0
|
||||
|
||||
###*
|
||||
# Functions for element class management to replace dependency on jQuery
|
||||
# addClass, removeClass and hasClass
|
||||
###
|
||||
|
||||
addClass = (element, className) ->
|
||||
if element.classList
|
||||
element.classList.add className
|
||||
else if !hasClass(element, className)
|
||||
element.className = element.className + ' ' + className
|
||||
element
|
||||
|
||||
removeClass = (element, className) ->
|
||||
if hasClass(className)
|
||||
if element.classList
|
||||
element.classList.remove className
|
||||
else
|
||||
element.className = (' ' + element.className + ' ').replace(' ' + className + ' ', ' ').trim()
|
||||
element
|
||||
|
||||
hasClass = (element, className) ->
|
||||
if element.classList
|
||||
element.classList.contains className
|
||||
else
|
||||
(' ' + element.className + ' ').indexOf(' ' + className + ' ') > -1
|
||||
|
||||
TimeoutTransitionGroupChild = React.createClass(
|
||||
transition: (animationType, finishCallback) ->
|
||||
node = @getDOMNode()
|
||||
className = @props.name + '-' + animationType
|
||||
activeClassName = className + '-active'
|
||||
|
||||
endListener = ->
|
||||
removeClass node, className
|
||||
removeClass node, activeClassName
|
||||
# Usually this optional callback is used for informing an owner of
|
||||
# a leave animation and telling it to remove the child.
|
||||
finishCallback and finishCallback()
|
||||
return
|
||||
|
||||
if !animationSupported()
|
||||
endListener()
|
||||
else
|
||||
if animationType == 'enter'
|
||||
@animationTimeout = setTimeout(endListener, @props.enterTimeout)
|
||||
else if animationType == 'leave'
|
||||
@animationTimeout = setTimeout(endListener, @props.leaveTimeout)
|
||||
addClass node, className
|
||||
# Need to do this to actually trigger a transition.
|
||||
@queueClass activeClassName
|
||||
return
|
||||
|
||||
queueClass: (className) ->
|
||||
@classNameQueue.push className
|
||||
if !@timeout
|
||||
@timeout = setTimeout(@flushClassNameQueue, TICK)
|
||||
return
|
||||
|
||||
flushClassNameQueue: ->
|
||||
if @isMounted()
|
||||
@classNameQueue.forEach ((name) ->
|
||||
addClass @getDOMNode(), name
|
||||
return
|
||||
).bind(this)
|
||||
@classNameQueue.length = 0
|
||||
@timeout = null
|
||||
return
|
||||
|
||||
componentWillMount: ->
|
||||
@classNameQueue = []
|
||||
return
|
||||
|
||||
componentWillUnmount: ->
|
||||
if @timeout
|
||||
clearTimeout @timeout
|
||||
if @animationTimeout
|
||||
clearTimeout @animationTimeout
|
||||
return
|
||||
|
||||
componentWillEnter: (done) ->
|
||||
if @props.enter
|
||||
@transition 'enter', done
|
||||
else
|
||||
done()
|
||||
return
|
||||
|
||||
componentWillLeave: (done) ->
|
||||
if @props.leave
|
||||
@transition 'leave', done
|
||||
else
|
||||
done()
|
||||
return
|
||||
|
||||
render: ->
|
||||
React.Children.only @props.children
|
||||
)
|
||||
|
||||
TimeoutTransitionGroup = React.createClass(
|
||||
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: ->
|
||||
transitionEnter: true
|
||||
transitionLeave: true
|
||||
|
||||
_wrapChild: (child) ->
|
||||
<TimeoutTransitionGroupChild
|
||||
enterTimeout={@props.enterTimeout}
|
||||
leaveTimeout={@props.leaveTimeout}
|
||||
name={@props.transitionName}
|
||||
enter={@props.transitionEnter}
|
||||
leave={@props.transitionLeave}>
|
||||
{child}
|
||||
</TimeoutTransitionGroupChild>
|
||||
|
||||
render: ->
|
||||
<ReactTransitionGroup
|
||||
{...@props}
|
||||
childFactory={@_wrapChild} />
|
||||
)
|
||||
|
||||
module.exports = TimeoutTransitionGroup
|
||||
|
||||
# ---
|
||||
# generated by js2coffee 2.0.1
|
|
@ -147,6 +147,10 @@ class AttributeCollection extends Attribute
|
|||
objs = []
|
||||
for objJSON in json
|
||||
obj = new @itemClass(objJSON)
|
||||
# Important: if no ids are in the JSON, don't make them up randomly.
|
||||
# This causes an object to be "different" each time it's de-serialized
|
||||
# even if it's actually the same, makes React components re-render!
|
||||
obj.id = undefined
|
||||
obj.fromJSON(objJSON) if obj.fromJSON?
|
||||
objs.push(obj)
|
||||
objs
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
React = require 'react/addons'
|
||||
Sheet = require './sheet'
|
||||
TitleBar = require './titlebar'
|
||||
Flexbox = require './components/flexbox.cjsx'
|
||||
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
|
||||
RetinaImg = require './components/retina-img'
|
||||
TimeoutTransitionGroup = require './components/timeout-transition-group'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
{Actions,
|
||||
ComponentRegistry,
|
||||
|
@ -12,15 +13,39 @@ ToolbarSpacer = React.createClass
|
|||
className: 'ToolbarSpacer'
|
||||
propTypes:
|
||||
order: React.PropTypes.number
|
||||
|
||||
render: ->
|
||||
<div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div>
|
||||
|
||||
ToolbarBack = React.createClass
|
||||
className: 'ToolbarBack'
|
||||
render: ->
|
||||
<div className="item-back" onClick={@_onClick}>
|
||||
<RetinaImg name="sheet-back.png" />
|
||||
</div>
|
||||
|
||||
_onClick: ->
|
||||
Actions.popSheet()
|
||||
|
||||
ToolbarWindowControls = React.createClass
|
||||
displayName: 'ToolbarWindowControls'
|
||||
render: ->
|
||||
<div name="ToolbarWindowControls" className="toolbar-window-controls">
|
||||
<button className="close" onClick={ -> atom.close()}></button>
|
||||
<button className="minimize" onClick={ -> atom.minimize()}></button>
|
||||
<button className="maximize" onClick={ -> atom.maximize()}></button>
|
||||
</div>
|
||||
|
||||
ComponentRegistry.register
|
||||
view: ToolbarWindowControls
|
||||
name: 'ToolbarWindowControls'
|
||||
role: 'Global:Left:Toolbar'
|
||||
|
||||
Toolbar = React.createClass
|
||||
className: 'Toolbar'
|
||||
|
||||
propTypes:
|
||||
type: React.PropTypes.string
|
||||
depth: React.PropTypes.number
|
||||
|
||||
getInitialState: ->
|
||||
@_getStateFromStores()
|
||||
|
@ -31,54 +56,60 @@ Toolbar = React.createClass
|
|||
@setState(@_getStateFromStores())
|
||||
@unlisteners.push ComponentRegistry.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
window.addEventListener "resize", (event) =>
|
||||
@recomputeLayout()
|
||||
window.addEventListener("resize", @_onWindowResize)
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
componentWillUnmount: ->
|
||||
@unlistener() if @unlistener
|
||||
window.removeEventListener("resize", @_onWindowResize)
|
||||
unlistener() for unlistener in @unlisteners
|
||||
|
||||
componentWillReceiveProps: (props) ->
|
||||
@setState(@_getStateFromStores(props))
|
||||
@replaceState(@_getStateFromStores(props))
|
||||
|
||||
componentDidUpdate: ->
|
||||
# Wait for other components that are dirty (the actual columns in the sheet)
|
||||
# to update as well.
|
||||
setTimeout(( => @recomputeLayout()), 1)
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
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: ->
|
||||
# The main toolbar contains items with roles <sheet type>:Toolbar
|
||||
# and Global:Toolbar
|
||||
mainToolbar = @_flexboxForItems(@state.items)
|
||||
style =
|
||||
position:'absolute'
|
||||
backgroundColor:'white'
|
||||
width:'100%'
|
||||
height:'100%'
|
||||
zIndex: 1
|
||||
|
||||
# Column toolbars contain items with roles attaching them to items
|
||||
# in the sheet. Ex: MessageList:Toolbar items appear in the column
|
||||
# toolbar for the column containing <MessageList/>.
|
||||
columnToolbars = @state.itemsForColumns.map ({column, name, items}) =>
|
||||
toolbars = @state.itemsForColumns.map ({column, items}) =>
|
||||
<div style={position: 'absolute', top:0, display:'none'}
|
||||
data-owner-name={name}
|
||||
data-column={column}
|
||||
key={column}>
|
||||
{@_flexboxForItems(items)}
|
||||
</div>
|
||||
|
||||
<ReactCSSTransitionGroup transitionName="sheet-toolbar">
|
||||
{mainToolbar}
|
||||
{columnToolbars}
|
||||
</ReactCSSTransitionGroup>
|
||||
<div style={style}>
|
||||
{toolbars}
|
||||
</div>
|
||||
|
||||
_flexboxForItems: (items) ->
|
||||
components = items.map ({view, name}) =>
|
||||
<view key={name} {...@props} />
|
||||
|
||||
<ReactCSSTransitionGroup
|
||||
<TimeoutTransitionGroup
|
||||
className="item-container"
|
||||
component={Flexbox}
|
||||
direction="row"
|
||||
leaveTimeout={200}
|
||||
enterTimeout={200}
|
||||
transitionName="sheet-toolbar">
|
||||
{components}
|
||||
<ToolbarSpacer key="spacer-50" order={-50}/>
|
||||
<ToolbarSpacer key="spacer+50" order={50}/>
|
||||
</ReactCSSTransitionGroup>
|
||||
</TimeoutTransitionGroup>
|
||||
|
||||
recomputeLayout: ->
|
||||
return unless @isMounted()
|
||||
|
@ -87,8 +118,9 @@ Toolbar = React.createClass
|
|||
columnToolbarEls = @getDOMNode().querySelectorAll('[data-column]')
|
||||
|
||||
# Find the top sheet in the stack
|
||||
sheet = document.querySelector("[name='Sheet']:last-child")
|
||||
|
||||
sheet = document.querySelector("[name='Sheet']:nth-child(#{@props.depth+1})")
|
||||
return unless sheet
|
||||
|
||||
# Position item containers so they have the position and width
|
||||
# as their respective columns in the top sheet
|
||||
for columnToolbarEl in columnToolbarEls
|
||||
|
@ -100,32 +132,42 @@ Toolbar = React.createClass
|
|||
columnToolbarEl.style.left = "#{columnEl.offsetLeft}px"
|
||||
columnToolbarEl.style.width = "#{columnEl.offsetWidth}px"
|
||||
|
||||
_onWindowResize: ->
|
||||
@recomputeLayout()
|
||||
|
||||
_getStateFromStores: (props) ->
|
||||
props ?= @props
|
||||
state =
|
||||
mode: WorkspaceStore.selectedLayoutMode()
|
||||
items: []
|
||||
itemsForColumns: []
|
||||
|
||||
for role in ["Global:Toolbar", "#{props.type}:Toolbar"]
|
||||
for entry in ComponentRegistry.findAllByRole(role)
|
||||
continue if entry.mode? and entry.mode != state.mode
|
||||
state.items.push(entry)
|
||||
|
||||
items = {}
|
||||
for column in ["Left", "Center", "Right"]
|
||||
role = "#{props.type}:#{column}:Toolbar"
|
||||
items = []
|
||||
for entry in ComponentRegistry.findAllByRole(role)
|
||||
continue if entry.mode? and entry.mode != state.mode
|
||||
items.push(entry)
|
||||
if items.length > 0
|
||||
state.itemsForColumns.push({column, name, items})
|
||||
items[column] = []
|
||||
for role in ["Global:#{column}:Toolbar", "#{props.type}:#{column}:Toolbar"]
|
||||
for entry in ComponentRegistry.findAllByRole(role)
|
||||
continue if entry.mode? and entry.mode != state.mode
|
||||
items[column].push(entry)
|
||||
|
||||
if @props.depth > 0
|
||||
items['Left'].push(view: ToolbarBack, name: 'ToolbarBack')
|
||||
|
||||
# If the left or right column does not contain any components, it won't
|
||||
# be in the sheet. Go ahead and shift those toolbar items into the center
|
||||
# region.
|
||||
for column in ["Left", "Right"]
|
||||
if ComponentRegistry.findAllByRole("#{props.type}:#{column}").length is 0
|
||||
items['Center'].push(items[column]...)
|
||||
delete items[column]
|
||||
|
||||
for key, val of items
|
||||
state.itemsForColumns.push({column: key, items: val}) if val.length > 0
|
||||
state
|
||||
|
||||
|
||||
FlexboxForRoles = React.createClass
|
||||
className: 'FlexboxForRoles'
|
||||
|
||||
propTypes:
|
||||
roles: React.PropTypes.arrayOf(React.PropTypes.string)
|
||||
|
||||
|
@ -139,9 +181,17 @@ FlexboxForRoles = React.createClass
|
|||
componentWillUnmount: ->
|
||||
@unlistener() if @unlistener
|
||||
|
||||
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.
|
||||
nextItemNames = nextState.items.map (i) -> i.name
|
||||
itemNames = @state.items?.map (i) -> i.name
|
||||
!_.isEqual(nextItemNames, itemNames)
|
||||
|
||||
render: ->
|
||||
components = @state.items.map ({view, name}) =>
|
||||
<view key={name} {...@props} />
|
||||
<view key={name} />
|
||||
|
||||
<Flexbox direction="row">
|
||||
{components}
|
||||
|
@ -171,26 +221,43 @@ SheetContainer = React.createClass
|
|||
|
||||
render: ->
|
||||
topSheetType = @state.stack[@state.stack.length - 1]
|
||||
|
||||
<Flexbox direction="column">
|
||||
<TitleBar />
|
||||
<div name="Toolbar" style={order:0} className="sheet-toolbar">
|
||||
<Toolbar ref="toolbar" type={topSheetType}/>
|
||||
</div>
|
||||
<TimeoutTransitionGroup name="Toolbar"
|
||||
style={order:0}
|
||||
leaveTimeout={200}
|
||||
enterTimeout={200}
|
||||
className="sheet-toolbar"
|
||||
transitionName="sheet-toolbar">
|
||||
{@_toolbarComponents()}
|
||||
</TimeoutTransitionGroup>
|
||||
|
||||
<div name="Top" style={order:1}>
|
||||
<FlexboxForRoles roles={["Global:Top", "#{topSheetType}:Top"]}
|
||||
type={topSheetType}/>
|
||||
</div>
|
||||
<div name="Center" style={order:2, flex: 1, position:'relative'}>
|
||||
<ReactCSSTransitionGroup transitionName="sheet-stack">
|
||||
{@_sheetComponents()}
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
|
||||
<TimeoutTransitionGroup name="Center"
|
||||
style={order:2, flex: 1, position:'relative'}
|
||||
leaveTimeout={150}
|
||||
enterTimeout={150}
|
||||
transitionName="sheet-stack">
|
||||
{@_sheetComponents()}
|
||||
</TimeoutTransitionGroup>
|
||||
|
||||
<div name="Footer" style={order:3}>
|
||||
<FlexboxForRoles roles={["Global:Footer", "#{topSheetType}:Footer"]}
|
||||
type={topSheetType}/>
|
||||
</div>
|
||||
</Flexbox>
|
||||
|
||||
_toolbarComponents: ->
|
||||
@state.stack.map (type, index) ->
|
||||
<Toolbar type={type}
|
||||
ref={"toolbar-#{index}"}
|
||||
depth={index}
|
||||
key={index} />
|
||||
|
||||
_sheetComponents: ->
|
||||
@state.stack.map (type, index) =>
|
||||
<Sheet type={type}
|
||||
|
@ -198,11 +265,11 @@ SheetContainer = React.createClass
|
|||
key={index}
|
||||
onColumnSizeChanged={@_onColumnSizeChanged} />
|
||||
|
||||
_onColumnSizeChanged: ->
|
||||
@refs.toolbar.recomputeLayout()
|
||||
_onColumnSizeChanged: (sheet) ->
|
||||
@refs["toolbar-#{sheet.props.depth}"]?.recomputeLayout()
|
||||
|
||||
_onStoreChange: ->
|
||||
@setState @_getStateFromStores()
|
||||
_.defer => @setState(@_getStateFromStores())
|
||||
|
||||
_getStateFromStores: ->
|
||||
stack: WorkspaceStore.sheetStack()
|
||||
|
|
|
@ -28,6 +28,12 @@ Sheet = React.createClass
|
|||
@unlisteners.push WorkspaceStore.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
componentDidUpdate: ->
|
||||
@props.onColumnSizeChanged(@) if @props.onColumnSizeChanged
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) ->
|
||||
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
|
||||
|
||||
componentWillUnmount: ->
|
||||
unlisten() for unlisten in @unlisteners
|
||||
|
||||
|
@ -49,24 +55,16 @@ Sheet = React.createClass
|
|||
style={style}
|
||||
data-type={@props.type}>
|
||||
<Flexbox direction="row">
|
||||
{@_backButtonComponent()}
|
||||
{@_columnFlexboxComponents()}
|
||||
</Flexbox>
|
||||
</div>
|
||||
|
||||
_backButtonComponent: ->
|
||||
return [] if @props.depth is 0
|
||||
<div className="sheet-edge" onClick={@_pop} key="back">
|
||||
<div className="gradient"></div>
|
||||
<div className="x"><RetinaImg name="sheet-back.png"/></div>
|
||||
</div>
|
||||
|
||||
_columnFlexboxComponents: ->
|
||||
@props.columns.map (column) =>
|
||||
classes = @state[column] || []
|
||||
return if classes.length is 0
|
||||
|
||||
components = classes.map ({name, view}) => <view key={name} {...@props} />
|
||||
components = classes.map ({name, view}) -> <view key={name} />
|
||||
|
||||
maxWidth = _.reduce classes, ((m,{view}) -> Math.min(view.maxWidth ? 10000, m)), 10000
|
||||
minWidth = _.reduce classes, ((m,{view}) -> Math.max(view.minWidth ? 0, m)), 0
|
||||
|
@ -78,7 +76,7 @@ Sheet = React.createClass
|
|||
<ResizableRegion key={"#{@props.type}:#{column}"}
|
||||
name={"#{@props.type}:#{column}"}
|
||||
data-column={column}
|
||||
onResize={@props.onColumnSizeChanged}
|
||||
onResize={ => @props.onColumnSizeChanged(@) }
|
||||
minWidth={minWidth}
|
||||
maxWidth={maxWidth}
|
||||
handle={handle}>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
React = require 'react'
|
||||
|
||||
module.exports =
|
||||
TitleBar = React.createClass
|
||||
displayName: 'TitleBar'
|
||||
|
||||
render: ->
|
||||
<div name="TitleBar" className="sheet-title-bar">
|
||||
{atom.getCurrentWindow().getTitle()}
|
||||
<button className="close" onClick={ -> atom.close()}></button>
|
||||
<button className="minimize" onClick={ -> atom.minimize()}></button>
|
||||
<button className="maximize" onClick={ -> atom.maximize()}></button>
|
||||
</div>
|
|
@ -7,6 +7,7 @@
|
|||
opacity: 1;
|
||||
-webkit-transition: opacity 0.2s linear 0.3s; //transition in
|
||||
}
|
||||
|
||||
.spinner.hidden {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear; //transition out
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
}
|
||||
&:hover {
|
||||
background-color: darken(@background-secondary, 5%);
|
||||
cursor: -webkit-grab;
|
||||
cursor: default;
|
||||
}
|
||||
&.selected,
|
||||
&.dragging {
|
||||
|
|
BIN
static/images/message-list/toolbar-down-arrow@2x.png
Normal file
BIN
static/images/message-list/toolbar-down-arrow@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 265 KiB |
BIN
static/images/message-list/toolbar-up-arrow@2x.png
Normal file
BIN
static/images/message-list/toolbar-up-arrow@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 264 KiB |
Binary file not shown.
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 263 KiB |
BIN
static/images/splitpane/toolbar-icon-toggle-pane@2x.png
Normal file
BIN
static/images/splitpane/toolbar-icon-toggle-pane@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 230 KiB |
|
@ -36,33 +36,33 @@ atom-workspace {
|
|||
}
|
||||
|
||||
.sheet-stack-enter {
|
||||
left:100%;
|
||||
transition: left .20s ease-out;
|
||||
left:7%;
|
||||
opacity: 0;
|
||||
transition: all .15s ease-out;
|
||||
}
|
||||
|
||||
.sheet-stack-enter.sheet-stack-enter-active {
|
||||
left:0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sheet-stack-leave {
|
||||
left:0;
|
||||
transition: left .20s ease-in;
|
||||
opacity: 1;
|
||||
transition: all .15s ease-in;
|
||||
}
|
||||
|
||||
.sheet-stack-leave.sheet-stack-leave-active {
|
||||
left:100%;
|
||||
left:7%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sheet-title-bar {
|
||||
height:24px;
|
||||
line-height: 24px;
|
||||
cursor: default;
|
||||
background: @toolbar-background-color;
|
||||
-webkit-app-region: drag;
|
||||
padding: 3px;
|
||||
.toolbar-window-controls {
|
||||
padding-top:14px;
|
||||
padding-left:@spacing-half;
|
||||
padding-right:60px;
|
||||
text-align: center;
|
||||
order: -1000;
|
||||
min-width: 102px;
|
||||
width: 102px;
|
||||
|
||||
button {
|
||||
-webkit-app-region: no-drag;
|
||||
|
@ -105,13 +105,13 @@ atom-workspace {
|
|||
}
|
||||
|
||||
body.platform-win32, body.platform-linux {
|
||||
.sheet-title-bar {
|
||||
.toolbar-window-controls {
|
||||
display:none;
|
||||
}
|
||||
}
|
||||
|
||||
body.is-blurred {
|
||||
.sheet-title-bar {
|
||||
.toolbar-window-controls {
|
||||
button {
|
||||
background-color: desaturate(fade(#FCB40A, 20%), 100%);
|
||||
}
|
||||
|
@ -121,11 +121,12 @@ body.is-blurred {
|
|||
.sheet-toolbar {
|
||||
position: relative;
|
||||
-webkit-app-region: drag;
|
||||
-webkit-user-select:none;
|
||||
background: @toolbar-background-color;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
|
||||
|
||||
// prevent flexbox from ever, ever resizing toolbars, no matter
|
||||
// how much it thinks other content is being squished
|
||||
min-height: 50px;
|
||||
|
@ -141,6 +142,11 @@ body.is-blurred {
|
|||
.item-spacer {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
.item-back {
|
||||
order:-999;
|
||||
padding-top: 5px;
|
||||
padding-left: @spacing-three-quarters;
|
||||
}
|
||||
|
||||
.btn-toolbar {
|
||||
margin-top: @spacing-half;
|
||||
|
@ -167,30 +173,6 @@ body.is-blurred {
|
|||
opacity:0;
|
||||
}
|
||||
|
||||
.sheet-edge {
|
||||
height:100%;
|
||||
z-index: @zindex-popover;
|
||||
position: absolute;
|
||||
|
||||
.x {
|
||||
position: absolute;
|
||||
top: @spacing-standard * 1.5;
|
||||
left: @spacing-standard * 1.5;
|
||||
}
|
||||
.gradient {
|
||||
width:9px;
|
||||
height:100%;
|
||||
background-color: #f4f4f4;
|
||||
background-image: -webkit-gradient(linear, left center, right center, from(rgb(244, 244, 244)), to(rgb(209, 209, 209)));
|
||||
background-image: -webkit-linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
|
||||
background-image: -moz-linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
|
||||
background-image: -o-linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
|
||||
background-image: -ms-linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
|
||||
background-image: linear-gradient(left, rgb(244, 244, 244), rgb(209, 209, 209));
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=1,StartColorStr='#f4f4f4', EndColorStr='#d1d1d1');
|
||||
}
|
||||
}
|
||||
|
||||
.flexbox-handle-horizontal {
|
||||
width: 6px;
|
||||
top: 0;
|
||||
|
|
Loading…
Add table
Reference in a new issue