mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-07 21:24:24 +08:00
Move sheet, sheet toolbar to JSX
This commit is contained in:
parent
d09d3d2633
commit
383db29f27
4 changed files with 578 additions and 407 deletions
|
@ -1,259 +0,0 @@
|
|||
React = require 'react'
|
||||
ReactDOM = require 'react-dom'
|
||||
Sheet = require './sheet'
|
||||
Flexbox = require('./components/flexbox').default
|
||||
RetinaImg = require('./components/retina-img').default
|
||||
Utils = require './flux/models/utils'
|
||||
{remote} = require 'electron'
|
||||
_str = require 'underscore.string'
|
||||
_ = require 'underscore'
|
||||
|
||||
{Actions,
|
||||
ComponentRegistry,
|
||||
WorkspaceStore} = require "nylas-exports"
|
||||
|
||||
class ToolbarSpacer extends React.Component
|
||||
@displayName: 'ToolbarSpacer'
|
||||
@propTypes:
|
||||
order: React.PropTypes.number
|
||||
|
||||
render: =>
|
||||
<div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div>
|
||||
|
||||
class WindowTitle extends React.Component
|
||||
@displayName: "WindowTitle"
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = NylasEnv.getLoadSettings()
|
||||
|
||||
componentDidMount: ->
|
||||
@unlisten = NylasEnv.onWindowPropsReceived (windowProps) =>
|
||||
@setState NylasEnv.getLoadSettings()
|
||||
|
||||
componentWillUnmount: ->
|
||||
@unlisten?()
|
||||
|
||||
render: ->
|
||||
<div className="window-title">{@state.title}</div>
|
||||
|
||||
Category = null
|
||||
FocusedPerspectiveStore = null
|
||||
class ToolbarBack extends React.Component
|
||||
@displayName: 'ToolbarBack'
|
||||
|
||||
# These stores are only required when this Toolbar is actually needed.
|
||||
# This is because loading these stores has database side effects.
|
||||
constructor: (@props) ->
|
||||
Category ?= require('./flux/models/category').default
|
||||
FocusedPerspectiveStore ?= require('./flux/stores/focused-perspective-store').default
|
||||
@state =
|
||||
categoryName: FocusedPerspectiveStore.current().name
|
||||
|
||||
componentDidMount: =>
|
||||
@_unsubscriber = FocusedPerspectiveStore.listen =>
|
||||
@setState(categoryName: FocusedPerspectiveStore.current().name)
|
||||
|
||||
componentWillUnmount: =>
|
||||
@_unsubscriber() if @_unsubscriber
|
||||
|
||||
render: =>
|
||||
if @state.categoryName is Category.AllMailName
|
||||
title = 'All Mail'
|
||||
else if @state.categoryName
|
||||
title = _str.titleize(@state.categoryName)
|
||||
else
|
||||
title = "Back"
|
||||
|
||||
<div className="item-back" onClick={@_onClick} title="Return to #{title}">
|
||||
<RetinaImg name="sheet-back.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<div className="item-back-title">{title}</div>
|
||||
</div>
|
||||
|
||||
_onClick: =>
|
||||
Actions.popSheet()
|
||||
|
||||
class ToolbarWindowControls extends React.Component
|
||||
@displayName: 'ToolbarWindowControls'
|
||||
constructor: (@props) ->
|
||||
@state = {alt: false}
|
||||
|
||||
componentDidMount: =>
|
||||
if process.platform is 'darwin'
|
||||
window.addEventListener('keydown', @_onAlt)
|
||||
window.addEventListener('keyup', @_onAlt)
|
||||
|
||||
componentWillUnmount: =>
|
||||
if process.platform is 'darwin'
|
||||
window.removeEventListener('keydown', @_onAlt)
|
||||
window.removeEventListener('keyup', @_onAlt)
|
||||
|
||||
render: =>
|
||||
<div name="ToolbarWindowControls" className="toolbar-window-controls alt-#{@state.alt}">
|
||||
<button tabIndex={-1} className="close" onClick={ -> NylasEnv.close()}></button>
|
||||
<button tabIndex={-1} className="minimize" onClick={ -> NylasEnv.minimize()}></button>
|
||||
<button tabIndex={-1} className="maximize" onClick={@_onMaximize}></button>
|
||||
</div>
|
||||
|
||||
_onAlt: (event) =>
|
||||
@setState(alt: event.altKey) if @state.alt isnt event.altKey
|
||||
|
||||
_onMaximize: (event) =>
|
||||
if process.platform is 'darwin' and not event.altKey
|
||||
NylasEnv.setFullScreen(!NylasEnv.isFullScreen())
|
||||
else
|
||||
NylasEnv.maximize()
|
||||
|
||||
class ToolbarMenuControl extends React.Component
|
||||
@displayName: 'ToolbarMenuControl'
|
||||
render: =>
|
||||
<div className="toolbar-menu-control">
|
||||
<button tabIndex={-1} className="btn btn-toolbar" onClick={@_openMenu}>
|
||||
<RetinaImg name="windows-menu-icon.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
_openMenu: =>
|
||||
applicationMenu = remote.getGlobal('application').applicationMenu
|
||||
applicationMenu.menu.popup(NylasEnv.getCurrentWindow())
|
||||
|
||||
ComponentRegistry.register ToolbarWindowControls,
|
||||
location: WorkspaceStore.Sheet.Global.Toolbar.Left
|
||||
|
||||
ComponentRegistry.register ToolbarMenuControl,
|
||||
location: WorkspaceStore.Sheet.Global.Toolbar.Right
|
||||
|
||||
class Toolbar extends React.Component
|
||||
@displayName: 'Toolbar'
|
||||
|
||||
@propTypes:
|
||||
data: React.PropTypes.object
|
||||
depth: React.PropTypes.number
|
||||
|
||||
@childContextTypes:
|
||||
sheetDepth: React.PropTypes.number
|
||||
getChildContext: =>
|
||||
sheetDepth: @props.depth
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@mounted = true
|
||||
@unlisteners = []
|
||||
@unlisteners.push WorkspaceStore.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
@unlisteners.push ComponentRegistry.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
window.addEventListener("resize", @_onWindowResize)
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
componentWillUnmount: =>
|
||||
@mounted = false
|
||||
window.removeEventListener("resize", @_onWindowResize)
|
||||
unlistener() for unlistener in @unlisteners
|
||||
|
||||
componentWillReceiveProps: (props) =>
|
||||
@setState(@_getStateFromStores(props))
|
||||
|
||||
componentDidUpdate: =>
|
||||
# Wait for other components that are dirty (the actual columns in the sheet)
|
||||
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 Utils.isEqualReact(nextProps, @props) or not Utils.isEqualReact(nextState, @state)
|
||||
|
||||
render: =>
|
||||
style =
|
||||
position:'absolute'
|
||||
width:'100%'
|
||||
height:'100%'
|
||||
zIndex: 1
|
||||
|
||||
toolbars = @state.columns.map (components, idx) =>
|
||||
<div style={position: 'absolute', top:0, display:'none'}
|
||||
className="toolbar-#{@state.columnNames[idx]}"
|
||||
data-column={idx}
|
||||
key={idx}>
|
||||
{@_flexboxForComponents(components)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={style}
|
||||
className={"sheet-toolbar-container mode-#{@state.mode}"}
|
||||
data-id={@props.data.id}>
|
||||
{toolbars}
|
||||
</div>
|
||||
|
||||
_flexboxForComponents: (components) =>
|
||||
elements = components.map (Component) =>
|
||||
<Component key={Component.displayName} {...@props} />
|
||||
|
||||
<Flexbox className="item-container" direction="row">
|
||||
{elements}
|
||||
<ToolbarSpacer key="spacer-50" order={-50}/>
|
||||
<ToolbarSpacer key="spacer+50" order={50}/>
|
||||
</Flexbox>
|
||||
|
||||
recomputeLayout: =>
|
||||
# Yes this really happens - do not remove!
|
||||
return unless @mounted
|
||||
|
||||
# Find our item containers that are tied to specific columns
|
||||
el = ReactDOM.findDOMNode(@)
|
||||
columnToolbarEls = el.querySelectorAll('[data-column]')
|
||||
|
||||
# Find the top sheet in the stack
|
||||
sheet = document.querySelectorAll("[name='Sheet']")[@props.depth]
|
||||
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
|
||||
column = columnToolbarEl.dataset.column
|
||||
columnEl = sheet.querySelector("[data-column='#{column}']")
|
||||
continue unless columnEl
|
||||
|
||||
columnToolbarEl.style.display = 'inherit'
|
||||
columnToolbarEl.style.left = "#{columnEl.offsetLeft}px"
|
||||
columnToolbarEl.style.width = "#{columnEl.offsetWidth}px"
|
||||
|
||||
# Record our overall height for sheets
|
||||
remote.getCurrentWindow().setSheetOffset(el.clientHeight)
|
||||
|
||||
_onWindowResize: =>
|
||||
@recomputeLayout()
|
||||
|
||||
_getStateFromStores: (props) =>
|
||||
props ?= @props
|
||||
state =
|
||||
mode: WorkspaceStore.layoutMode()
|
||||
columns: []
|
||||
columnNames: []
|
||||
|
||||
# Add items registered to Regions in the current sheet
|
||||
if @props.data?.columns[state.mode]?
|
||||
for loc in @props.data.columns[state.mode]
|
||||
continue if WorkspaceStore.isLocationHidden(loc)
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar, mode: state.mode})
|
||||
state.columns.push(entries)
|
||||
state.columnNames.push(loc.Toolbar.id.split(":")[0]) if entries
|
||||
|
||||
# Add left items registered to the Sheet instead of to a Region
|
||||
for loc in [WorkspaceStore.Sheet.Global, @props.data]
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Left, mode: state.mode})
|
||||
state.columns[0]?.push(entries...)
|
||||
if @props.depth > 0
|
||||
state.columns[0]?.push(ToolbarBack)
|
||||
|
||||
# Add right items registered to the Sheet instead of to a Region
|
||||
for loc in [WorkspaceStore.Sheet.Global, @props.data]
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Right, mode: state.mode})
|
||||
state.columns[state.columns.length - 1]?.push(entries...)
|
||||
if state.mode is "popout"
|
||||
state.columns[0]?.push(WindowTitle)
|
||||
|
||||
state
|
||||
|
||||
module.exports = Toolbar
|
364
packages/client-app/src/sheet-toolbar.jsx
Normal file
364
packages/client-app/src/sheet-toolbar.jsx
Normal file
|
@ -0,0 +1,364 @@
|
|||
/* eslint react/prefer-stateless-function: 0 */
|
||||
/* eslint global-require: 0 */
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {remote} from 'electron'
|
||||
import _str from 'underscore.string'
|
||||
import {
|
||||
Actions,
|
||||
ComponentRegistry,
|
||||
WorkspaceStore,
|
||||
} from "nylas-exports";
|
||||
|
||||
import Flexbox from './components/flexbox'
|
||||
import RetinaImg from './components/retina-img'
|
||||
import Utils from './flux/models/utils'
|
||||
|
||||
let Category = null;
|
||||
let FocusedPerspectiveStore = null;
|
||||
|
||||
class ToolbarSpacer extends React.Component {
|
||||
static displayName = 'ToolbarSpacer';
|
||||
static propTypes = {
|
||||
order: React.PropTypes.number,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="item-spacer" style={{flex: 1, order: this.props.order || 0}} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WindowTitle extends React.Component {
|
||||
static displayName = "WindowTitle";
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = NylasEnv.getLoadSettings();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unlisten = NylasEnv.onWindowPropsReceived(() =>
|
||||
this.setState(NylasEnv.getLoadSettings())
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="window-title">{this.state.title}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarBack extends React.Component {
|
||||
static displayName = 'ToolbarBack';
|
||||
|
||||
// These stores are only required when this Toolbar is actually needed.
|
||||
// This is because loading these stores has database side effects.
|
||||
constructor(props) {
|
||||
super(props);
|
||||
Category = Category || require('./flux/models/category').default
|
||||
FocusedPerspectiveStore = FocusedPerspectiveStore || require('./flux/stores/focused-perspective-store').default
|
||||
this.state = {
|
||||
categoryName: FocusedPerspectiveStore.current().name,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._unsubscriber = FocusedPerspectiveStore.listen(() =>
|
||||
this.setState({categoryName: FocusedPerspectiveStore.current().name})
|
||||
);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._unsubscriber) {
|
||||
this._unsubscriber();
|
||||
}
|
||||
}
|
||||
|
||||
_onClick = () => {
|
||||
Actions.popSheet();
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = "Back";
|
||||
if (this.state.categoryName === Category.AllMailName) {
|
||||
title = 'All Mail'
|
||||
} else if (this.state.categoryName) {
|
||||
title = _str.titleize(this.state.categoryName);
|
||||
}
|
||||
return (
|
||||
<div className="item-back" onClick={this._onClick} title="Return to #{title}">
|
||||
<RetinaImg name="sheet-back.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
<div className="item-back-title">{title}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarWindowControls extends React.Component {
|
||||
static displayName = 'ToolbarWindowControls';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {alt: false};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (process.platform === 'darwin') {
|
||||
window.addEventListener('keydown', this._onAlt);
|
||||
window.addEventListener('keyup', this._onAlt);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (process.platform === 'darwin') {
|
||||
window.removeEventListener('keydown', this._onAlt);
|
||||
window.removeEventListener('keyup', this._onAlt);
|
||||
}
|
||||
}
|
||||
|
||||
_onAlt = (event) => {
|
||||
if (this.state.alt !== event.altKey) {
|
||||
this.setState({alt: event.altKey});
|
||||
}
|
||||
}
|
||||
|
||||
_onMaximize = (event) => {
|
||||
if (process.platform === 'darwin' && !event.altKey) {
|
||||
NylasEnv.setFullScreen(!NylasEnv.isFullScreen());
|
||||
} else {
|
||||
NylasEnv.maximize();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div name="ToolbarWindowControls" className="toolbar-window-controls alt-#{this.state.alt}">
|
||||
<button tabIndex={-1} className="close" onClick={() => NylasEnv.close()} />
|
||||
<button tabIndex={-1} className="minimize" onClick={() => NylasEnv.minimize()} />
|
||||
<button tabIndex={-1} className="maximize" onClick={this._onMaximize} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarMenuControl extends React.Component {
|
||||
static displayName = 'ToolbarMenuControl';
|
||||
|
||||
_onOpenMenu = () => {
|
||||
const {applicationMenu} = remote.getGlobal('application');
|
||||
applicationMenu.menu.popup(NylasEnv.getCurrentWindow());
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="toolbar-menu-control">
|
||||
<button tabIndex={-1} className="btn btn-toolbar" onClick={this._onOpenMenu}>
|
||||
<RetinaImg name="windows-menu-icon.png" mode={RetinaImg.Mode.ContentIsMask} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ComponentRegistry.register(ToolbarWindowControls, {
|
||||
location: WorkspaceStore.Sheet.Global.Toolbar.Left,
|
||||
});
|
||||
|
||||
ComponentRegistry.register(ToolbarMenuControl, {
|
||||
location: WorkspaceStore.Sheet.Global.Toolbar.Right,
|
||||
});
|
||||
|
||||
export default class Toolbar extends React.Component {
|
||||
static displayName= 'Toolbar';
|
||||
|
||||
static propTypes = {
|
||||
data: React.PropTypes.object,
|
||||
depth: React.PropTypes.number,
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
sheetDepth: React.PropTypes.number,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this._getStateFromStores();
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
sheetDepth: this.props.depth,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true
|
||||
this.unlisteners = []
|
||||
this.unlisteners.push(WorkspaceStore.listen(() =>
|
||||
this.setState(this._getStateFromStores())
|
||||
));
|
||||
this.unlisteners.push(ComponentRegistry.listen(() =>
|
||||
this.setState(this._getStateFromStores())
|
||||
));
|
||||
window.addEventListener("resize", this._onWindowResize)
|
||||
window.requestAnimationFrame(() => this.recomputeLayout());
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.setState(this._getStateFromStores(props));
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
// This is very important. Because toolbar uses ReactCSSTransitionGroup,
|
||||
// repetitive unnecessary updates can break animations and cause performance issues.
|
||||
return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Wait for other components that are dirty (the actual columns in the sheet)
|
||||
window.requestAnimationFrame(() => this.recomputeLayout());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false
|
||||
window.removeEventListener("resize", this._onWindowResize);
|
||||
for (const u of this.unlisteners) {
|
||||
u();
|
||||
}
|
||||
}
|
||||
|
||||
recomputeLayout() {
|
||||
// Yes this really happens - do not remove!
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find our item containers that are tied to specific columns
|
||||
const el = ReactDOM.findDOMNode(this);
|
||||
const columnToolbarEls = el.querySelectorAll('[data-column]');
|
||||
|
||||
// Find the top sheet in the stack
|
||||
const sheet = document.querySelectorAll("[name='Sheet']")[this.props.depth];
|
||||
if (!sheet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Position item containers so they have the position and width
|
||||
// as their respective columns in the top sheet
|
||||
for (const columnToolbarEl of columnToolbarEls) {
|
||||
const column = columnToolbarEl.dataset.column;
|
||||
const columnEl = sheet.querySelector(`[data-column='${column}']`);
|
||||
if (!columnEl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
columnToolbarEl.style.display = 'inherit'
|
||||
columnToolbarEl.style.left = `${columnEl.offsetLeft}px`
|
||||
columnToolbarEl.style.width = `${columnEl.offsetWidth}px`;
|
||||
}
|
||||
|
||||
// Record our overall height for sheets
|
||||
remote.getCurrentWindow().setSheetOffset(el.clientHeight);
|
||||
}
|
||||
|
||||
_onWindowResize = () => {
|
||||
this.recomputeLayout();
|
||||
}
|
||||
|
||||
_getStateFromStores(props = this.props) {
|
||||
const state = {
|
||||
mode: WorkspaceStore.layoutMode(),
|
||||
columns: [],
|
||||
columnNames: [],
|
||||
}
|
||||
|
||||
// Add items registered to Regions in the current sheet
|
||||
if (props.data && props.data.columns[state.mode]) {
|
||||
for (const loc of props.data.columns[state.mode]) {
|
||||
if (WorkspaceStore.isLocationHidden(loc)) {
|
||||
continue;
|
||||
}
|
||||
const entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar, mode: state.mode});
|
||||
state.columns.push(entries);
|
||||
if (entries) {
|
||||
state.columnNames.push(loc.Toolbar.id.split(":")[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add left items registered to the Sheet instead of to a Region
|
||||
if (state.columns.length > 0) {
|
||||
for (const loc of [WorkspaceStore.Sheet.Global, props.data]) {
|
||||
const entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Left, mode: state.mode})
|
||||
state.columns[0].push(...entries);
|
||||
}
|
||||
if (props.depth > 0) {
|
||||
state.columns[0].push(ToolbarBack);
|
||||
}
|
||||
|
||||
// Add right items registered to the Sheet instead of to a Region
|
||||
for (const loc of [WorkspaceStore.Sheet.Global, props.data]) {
|
||||
const entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Right, mode: state.mode})
|
||||
state.columns[state.columns.length - 1].push(...entries);
|
||||
}
|
||||
|
||||
if (state.mode === "popout") {
|
||||
state.columns[0].push(WindowTitle);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
_flexboxForComponents(components) {
|
||||
const elements = components.map((Component) =>
|
||||
<Component key={Component.displayName} {...this.props} />
|
||||
);
|
||||
return (
|
||||
<Flexbox className="item-container" direction="row">
|
||||
{elements}
|
||||
<ToolbarSpacer key="spacer-50" order={-50} />
|
||||
<ToolbarSpacer key="spacer+50" order={50} />
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 1,
|
||||
};
|
||||
|
||||
const toolbars = this.state.columns.map((components, idx) =>
|
||||
<div
|
||||
style={{position: 'absolute', top: 0, display: 'none'}}
|
||||
className={`toolbar-${this.state.columnNames[idx]}`}
|
||||
data-column={idx}
|
||||
key={idx}
|
||||
>
|
||||
{this._flexboxForComponents(components)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={"sheet-toolbar-container mode-#{this.state.mode}"}
|
||||
data-id={this.props.data.id}
|
||||
>
|
||||
{toolbars}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
React = require 'react'
|
||||
_ = require 'underscore'
|
||||
{Actions,ComponentRegistry, WorkspaceStore} = require "nylas-exports"
|
||||
RetinaImg = require('./components/retina-img').default
|
||||
Flexbox = require('./components/flexbox').default
|
||||
InjectedComponentSet = require('./components/injected-component-set').default
|
||||
ResizableRegion = require './components/resizable-region'
|
||||
|
||||
FLEX = 10000
|
||||
|
||||
class Sheet extends React.Component
|
||||
@displayName = 'Sheet'
|
||||
|
||||
@propTypes =
|
||||
data: React.PropTypes.object.isRequired
|
||||
depth: React.PropTypes.number.isRequired
|
||||
onColumnSizeChanged: React.PropTypes.func
|
||||
|
||||
@defaultProps:
|
||||
onColumnSizeChanged: ->
|
||||
|
||||
@childContextTypes:
|
||||
sheetDepth: React.PropTypes.number
|
||||
|
||||
getChildContext: =>
|
||||
sheetDepth: @props.depth
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@unlisteners ?= []
|
||||
@unlisteners.push ComponentRegistry.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
@unlisteners.push WorkspaceStore.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
|
||||
componentDidUpdate: =>
|
||||
@props.onColumnSizeChanged(@)
|
||||
minWidth = 0
|
||||
minWidth += col.minWidth for col in @state.columns
|
||||
NylasEnv.setMinimumWidth(minWidth)
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
|
||||
|
||||
componentWillUnmount: =>
|
||||
unlisten() for unlisten in @unlisteners
|
||||
|
||||
render: =>
|
||||
style =
|
||||
position:'absolute'
|
||||
width:'100%'
|
||||
height:'100%'
|
||||
zIndex: 1
|
||||
|
||||
# Note - setting the z-index of the sheet is important, even though it's
|
||||
# always 1. Assigning a z-index creates a "stacking context" in the browser,
|
||||
# so z-indexes inside the sheet are relative to each other, but something in
|
||||
# one sheet cannot be on top of something in another sheet.
|
||||
# http://philipwalton.com/articles/what-no-one-told-you-about-z-index/
|
||||
|
||||
<div name={"Sheet"}
|
||||
style={style}
|
||||
className={"sheet mode-#{@state.mode}"}
|
||||
data-id={@props.data.id}>
|
||||
<Flexbox direction="row" style={overflow: 'hidden'}>
|
||||
{@_columnFlexboxElements()}
|
||||
</Flexbox>
|
||||
</div>
|
||||
|
||||
_columnFlexboxElements: =>
|
||||
@state.columns.map (column, idx) =>
|
||||
{maxWidth, minWidth, handle, location, width} = column
|
||||
if minWidth != maxWidth and maxWidth < FLEX
|
||||
<ResizableRegion
|
||||
key={"#{@props.data.id}:#{idx}"}
|
||||
name={"#{@props.data.id}:#{idx}"}
|
||||
className={"column-#{location.id}"}
|
||||
style={height:'100%'}
|
||||
data-column={idx}
|
||||
onResize={@_onColumnResize.bind(@, column)}
|
||||
initialWidth={width}
|
||||
minWidth={minWidth}
|
||||
maxWidth={maxWidth}
|
||||
handle={handle}>
|
||||
<InjectedComponentSet direction="column" matching={location: location, mode: @state.mode}/>
|
||||
</ResizableRegion>
|
||||
else
|
||||
style =
|
||||
height: '100%'
|
||||
minWidth: minWidth
|
||||
overflow: 'hidden'
|
||||
if maxWidth < FLEX
|
||||
style.width = maxWidth
|
||||
else
|
||||
style.flex = 1
|
||||
<InjectedComponentSet direction="column"
|
||||
key={"#{@props.data.id}:#{idx}"}
|
||||
name={"#{@props.data.id}:#{idx}"}
|
||||
className={"column-#{location.id}"}
|
||||
data-column={idx}
|
||||
style={style}
|
||||
matching={location: location, mode: @state.mode}/>
|
||||
|
||||
_onColumnResize: (column, width) =>
|
||||
NylasEnv.storeColumnWidth(id: column.location.id, width: width)
|
||||
@props.onColumnSizeChanged(@)
|
||||
|
||||
_getStateFromStores: =>
|
||||
state =
|
||||
mode: WorkspaceStore.layoutMode()
|
||||
columns: []
|
||||
|
||||
widest = -1
|
||||
widestWidth = -1
|
||||
|
||||
if @props.data?.columns[state.mode]?
|
||||
for location, idx in @props.data.columns[state.mode]
|
||||
continue if WorkspaceStore.isLocationHidden(location)
|
||||
entries = ComponentRegistry.findComponentsMatching({location: location, mode: state.mode})
|
||||
maxWidth = _.reduce entries, ((m,component) -> Math.min(component.containerStyles?.maxWidth ? 10000, m)), 10000
|
||||
minWidth = _.reduce entries, ((m,component) -> Math.max(component.containerStyles?.minWidth ? 0, m)), 0
|
||||
width = NylasEnv.getColumnWidth(location.id)
|
||||
col = {maxWidth, minWidth, location, width}
|
||||
state.columns.push(col)
|
||||
|
||||
if maxWidth > widestWidth
|
||||
widestWidth = maxWidth
|
||||
widest = idx
|
||||
|
||||
if state.columns.length > 0
|
||||
# Once we've accumulated all the React components for the columns,
|
||||
# ensure that at least one column has a huge max-width so that the columns
|
||||
# expand to fill the window. This may make items in the column unhappy, but
|
||||
# we pick the column with the highest max-width so the effect is minimal.
|
||||
state.columns[widest].maxWidth = FLEX
|
||||
|
||||
# Assign flexible edges based on whether items are to the left or right
|
||||
# of the flexible column (which has no edges)
|
||||
state.columns[i].handle = ResizableRegion.Handle.Right for i in [0..widest-1] by 1
|
||||
state.columns[i].handle = ResizableRegion.Handle.Left for i in [widest..state.columns.length-1] by 1
|
||||
state
|
||||
|
||||
_pop: =>
|
||||
Actions.popSheet()
|
||||
|
||||
module.exports = Sheet
|
214
packages/client-app/src/sheet.jsx
Normal file
214
packages/client-app/src/sheet.jsx
Normal file
|
@ -0,0 +1,214 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {Utils, ComponentRegistry, WorkspaceStore} from "nylas-exports";
|
||||
import InjectedComponentSet from './components/injected-component-set';
|
||||
import ResizableRegion from './components/resizable-region';
|
||||
import Flexbox from './components/flexbox';
|
||||
|
||||
const FLEX = 10000;
|
||||
|
||||
export default class Sheet extends React.Component {
|
||||
static displayName = 'Sheet';
|
||||
|
||||
static propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
onColumnSizeChanged: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onColumnSizeChanged: () => {},
|
||||
};
|
||||
|
||||
static childContextTypes = {
|
||||
sheetDepth: React.PropTypes.number,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.unlisteners = [];
|
||||
this.state = this._getStateFromStores();
|
||||
}
|
||||
|
||||
getChildContext() {
|
||||
return {
|
||||
sheetDepth: this.props.depth,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unlisteners.push(ComponentRegistry.listen(() =>
|
||||
this.setState(this._getStateFromStores())
|
||||
));
|
||||
this.unlisteners.push(WorkspaceStore.listen(() =>
|
||||
this.setState(this._getStateFromStores())
|
||||
));
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.onColumnSizeChanged(this);
|
||||
const minWidth = this.state.columns.reduce((sum, col) => sum + col.minWidth, 0);
|
||||
NylasEnv.setMinimumWidth(minWidth);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
for (const u of this.unlisteners) {
|
||||
u();
|
||||
}
|
||||
this.unlisteners = [];
|
||||
}
|
||||
|
||||
|
||||
_columnFlexboxElements() {
|
||||
return this.state.columns.map((column, idx) => {
|
||||
const {maxWidth, minWidth, handle, location, width} = column;
|
||||
|
||||
if (minWidth !== maxWidth && maxWidth < FLEX) {
|
||||
return (
|
||||
<ResizableRegion
|
||||
key={`${this.props.data.id}:${idx}`}
|
||||
name={`${this.props.data.id}:${idx}`}
|
||||
className={`column-${location.id}`}
|
||||
style={{height: '100%'}}
|
||||
data-column={idx}
|
||||
onResize={(w) => this._onColumnResize(column, w)}
|
||||
initialWidth={width}
|
||||
minWidth={minWidth}
|
||||
maxWidth={maxWidth}
|
||||
handle={handle}
|
||||
>
|
||||
<InjectedComponentSet
|
||||
direction="column"
|
||||
matching={{location: location, mode: this.state.mode}}
|
||||
/>
|
||||
</ResizableRegion>
|
||||
);
|
||||
}
|
||||
|
||||
const style = {
|
||||
height: '100%',
|
||||
minWidth: minWidth,
|
||||
overflow: 'hidden',
|
||||
}
|
||||
if (maxWidth < FLEX) {
|
||||
style.width = maxWidth;
|
||||
} else {
|
||||
style.flex = 1;
|
||||
}
|
||||
return (
|
||||
<InjectedComponentSet
|
||||
direction="column"
|
||||
key={`${this.props.data.id}:${idx}`}
|
||||
name={`${this.props.data.id}:${idx}`}
|
||||
className={`column-${location.id}`}
|
||||
data-column={idx}
|
||||
style={style}
|
||||
matching={{location: location, mode: this.state.mode}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_onColumnResize = (column, width) => {
|
||||
NylasEnv.storeColumnWidth({id: column.location.id, width: width});
|
||||
this.props.onColumnSizeChanged(this);
|
||||
}
|
||||
|
||||
_getStateFromStores() {
|
||||
const state = {
|
||||
mode: WorkspaceStore.layoutMode(),
|
||||
columns: [],
|
||||
}
|
||||
|
||||
let widest = -1;
|
||||
let widestWidth = -1;
|
||||
|
||||
const data = this.props.data || {};
|
||||
|
||||
if (data.columns[state.mode]) {
|
||||
data.columns[state.mode].forEach((location, idx) => {
|
||||
if (WorkspaceStore.isLocationHidden(location)) {
|
||||
return;
|
||||
}
|
||||
const entries = ComponentRegistry.findComponentsMatching({
|
||||
location: location,
|
||||
mode: state.mode,
|
||||
});
|
||||
|
||||
const maxWidth = entries.reduce((m, {containerStyles}) => {
|
||||
if (containerStyles && containerStyles.maxWidth !== undefined && containerStyles.maxWidth < m) {
|
||||
return containerStyles.maxWidth;
|
||||
}
|
||||
return m;
|
||||
}, 10000);
|
||||
|
||||
const minWidth = entries.reduce((m, {containerStyles}) => {
|
||||
if (containerStyles && containerStyles.minWidth !== undefined && containerStyles.minWidth > m) {
|
||||
return containerStyles.minWidth;
|
||||
}
|
||||
return m;
|
||||
}, 0);
|
||||
|
||||
const width = NylasEnv.getColumnWidth(location.id);
|
||||
const col = {maxWidth, minWidth, location, width};
|
||||
state.columns.push(col);
|
||||
|
||||
if (maxWidth > widestWidth) {
|
||||
widestWidth = maxWidth;
|
||||
widest = idx;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state.columns.length > 0) {
|
||||
// Once we've accumulated all the React components for the columns,
|
||||
// ensure that at least one column has a huge max-width so that the columns
|
||||
// expand to fill the window. This may make items in the column unhappy, but
|
||||
// we pick the column with the highest max-width so the effect is minimal.
|
||||
state.columns[widest].maxWidth = FLEX;
|
||||
|
||||
// Assign flexible edges based on whether items are to the left or right
|
||||
// of the flexible column (which has no edges)
|
||||
for (let i = 0; i < widest; i++) {
|
||||
state.columns[i].handle = ResizableRegion.Handle.Right;
|
||||
}
|
||||
for (let i = widest; i < state.columns.length; i++) {
|
||||
state.columns[i].handle = ResizableRegion.Handle.Left;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 1,
|
||||
};
|
||||
|
||||
// Note - setting the z-index of the sheet is important, even though it's
|
||||
// always 1. Assigning a z-index creates a "stacking context" in the browser,
|
||||
// so z-indexes inside the sheet are relative to each other, but something in
|
||||
// one sheet cannot be on top of something in another sheet.
|
||||
// http://philipwalton.com/articles/what-no-one-told-you-about-z-index/
|
||||
|
||||
return (
|
||||
<div
|
||||
name={"Sheet"}
|
||||
style={style}
|
||||
className={`sheet mode-${this.state.mode}`}
|
||||
data-id={this.props.data.id}
|
||||
>
|
||||
<Flexbox direction="row" style={{overflow: 'hidden'}}>
|
||||
{this._columnFlexboxElements()}
|
||||
</Flexbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue