Convert Dock to an etch component

This commit is contained in:
Matthew Dapena-Tretter
2018-02-28 15:03:44 -08:00
parent 429f2bdd09
commit 113453a219
3 changed files with 165 additions and 118 deletions

View File

@@ -1,5 +1,7 @@
'use strict'
/** @babel */
/** @jsx etch.dom */
const etch = require('etch')
const _ = require('underscore-plus')
const {CompositeDisposable, Emitter} = require('event-kit')
const PaneContainer = require('./pane-container')
@@ -26,6 +28,8 @@ module.exports = class Dock {
this.handleMouseUp = this.handleMouseUp.bind(this)
this.handleDrag = _.throttle(this.handleDrag.bind(this), 30)
this.handleDragEnd = this.handleDragEnd.bind(this)
this.handleToggleButtonDragEnter = this.handleToggleButtonDragEnter.bind(this)
this.toggle = this.toggle.bind(this)
this.location = params.location
this.widthOrHeight = getWidthOrHeight(this.location)
@@ -67,16 +71,17 @@ module.exports = class Dock {
this.paneContainer.onDidChangeActivePaneItem((item) => params.didChangeActivePaneItem(this, item)),
this.paneContainer.onDidDestroyPaneItem((item) => params.didDestroyPaneItem(item))
)
etch.initialize(this)
}
// This method is called explicitly by the object which adds the Dock to the document.
elementAttached () {
// Re-render when the dock is attached to make sure we remeasure sizes defined in CSS.
this.render(this.state)
etch.update(this)
}
getElement () {
if (!this.element) this.render(this.state)
return this.element
}
@@ -151,9 +156,14 @@ module.exports = class Dock {
}
this.state = nextState
this.render(this.state)
const {hovered, visible} = this.state
// Render immediately if the dock becomes visible or the size changes in case people are
// measuring after opening, for example.
if ((visible && !prevState.visible) || (this.state.size !== prevState.size)) etch.updateSync(this)
else etch.update(this)
if (hovered !== prevState.hovered) {
this.emitter.emit('did-change-hovered', hovered)
}
@@ -162,80 +172,74 @@ module.exports = class Dock {
}
}
render (state) {
if (this.element == null) {
this.element = document.createElement('atom-dock')
this.element.classList.add(this.location)
this.innerElement = document.createElement('div')
this.innerElement.classList.add('atom-dock-inner', this.location)
this.maskElement = document.createElement('div')
this.maskElement.classList.add('atom-dock-mask')
this.wrapperElement = document.createElement('div')
this.wrapperElement.classList.add('atom-dock-content-wrapper', this.location)
this.resizeHandle = new DockResizeHandle({
location: this.location,
onResizeStart: this.handleResizeHandleDragStart,
onResizeToFit: this.handleResizeToFit
})
this.toggleButton = new DockToggleButton({
onDragEnter: this.handleToggleButtonDragEnter.bind(this),
location: this.location,
toggle: this.toggle.bind(this)
})
this.cursorOverlayElement = document.createElement('div')
this.cursorOverlayElement.classList.add('atom-dock-cursor-overlay', this.location)
render () {
const innerElementClassList = ['atom-dock-inner', this.location]
if (this.state.visible) innerElementClassList.push(VISIBLE_CLASS)
// Add the children to the DOM tree
this.element.appendChild(this.innerElement)
this.innerElement.appendChild(this.maskElement)
this.maskElement.appendChild(this.wrapperElement)
this.wrapperElement.appendChild(this.resizeHandle.getElement())
this.wrapperElement.appendChild(this.paneContainer.getElement())
this.wrapperElement.appendChild(this.cursorOverlayElement)
// The toggle button must be rendered outside the mask because (1) it shouldn't be masked and
// (2) if we made the mask larger to avoid masking it, the mask would block mouse events.
this.innerElement.appendChild(this.toggleButton.getElement())
}
const maskElementClassList = ['atom-dock-mask']
if (this.state.shouldAnimate) maskElementClassList.push(SHOULD_ANIMATE_CLASS)
if (state.visible) {
this.innerElement.classList.add(VISIBLE_CLASS)
} else {
this.innerElement.classList.remove(VISIBLE_CLASS)
}
const cursorOverlayElementClassList = ['atom-dock-cursor-overlay', this.location]
if (this.state.resizing) cursorOverlayElementClassList.push(CURSOR_OVERLAY_VISIBLE_CLASS)
if (state.shouldAnimate) {
this.maskElement.classList.add(SHOULD_ANIMATE_CLASS)
} else {
this.maskElement.classList.remove(SHOULD_ANIMATE_CLASS)
}
if (state.resizing) {
this.cursorOverlayElement.classList.add(CURSOR_OVERLAY_VISIBLE_CLASS)
} else {
this.cursorOverlayElement.classList.remove(CURSOR_OVERLAY_VISIBLE_CLASS)
}
const shouldBeVisible = state.visible || state.showDropTarget
const shouldBeVisible = this.state.visible || this.state.showDropTarget
const size = Math.max(MINIMUM_SIZE,
state.size ||
(state.draggingItem && getPreferredSize(state.draggingItem, this.location)) ||
this.state.size ||
(this.state.draggingItem && getPreferredSize(this.state.draggingItem, this.location)) ||
DEFAULT_INITIAL_SIZE
)
// We need to change the size of the mask...
this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : 0}px`
const maskStyle = {[this.widthOrHeight]: `${shouldBeVisible ? size : 0}px`}
// ...but the content needs to maintain a constant size.
this.wrapperElement.style[this.widthOrHeight] = `${size}px`
const wrapperStyle = {[this.widthOrHeight]: `${size}px`}
this.resizeHandle.update({dockIsVisible: this.state.visible})
this.toggleButton.update({
dockIsVisible: shouldBeVisible,
visible:
// Don't show the toggle button if the dock is closed and empty...
(state.hovered && (this.state.visible || this.getPaneItems().length > 0)) ||
// ...or if the item can't be dropped in that dock.
(!shouldBeVisible && state.draggingItem && isItemAllowed(state.draggingItem, this.location))
})
return (
<atom-dock className={this.location}>
<div ref='innerElement' className={innerElementClassList.join(' ')}>
<div
className={maskElementClassList.join(' ')}
style={maskStyle}>
<div
ref='wrapperElement'
className={`atom-dock-content-wrapper ${this.location}`}
style={wrapperStyle}>
<DockResizeHandle
location={this.location}
onResizeStart={this.handleResizeHandleDragStart}
onResizeToFit={this.handleResizeToFit}
dockIsVisible={this.state.visible}
/>
<ElementComponent element={this.paneContainer.getElement()} />
<div className={cursorOverlayElementClassList.join(' ')} />
</div>
</div>
{/*
The toggle button must be rendered outside the mask because (1) it shouldn't be masked
and (2) if we made the mask larger to avoid masking it, the mask would block mouse
events.
*/}
<DockToggleButton
ref='toggleButton'
onDragEnter={this.handleToggleButtonDragEnter}
location={this.location}
toggle={this.toggle}
dockIsVisible={shouldBeVisible}
visible={
// Don't show the toggle button if the dock is closed and empty...
(this.state.hovered && (this.state.visible || this.getPaneItems().length > 0)) ||
// ...or if the item can't be dropped in that dock.
(!shouldBeVisible && this.state.draggingItem && isItemAllowed(this.state.draggingItem, this.location))
}
/>
</div>
</atom-dock>
)
}
update (props) {
// Since we're interopping with non-etch stuff, this method's actually never called.
return etch.update(this)
}
handleDidAddPaneItem () {
@@ -321,7 +325,7 @@ module.exports = class Dock {
// area considered when detecting exit MUST fully encompass the area considered when detecting
// entry.
pointWithinHoverArea (point, detectingExit) {
const dockBounds = this.innerElement.getBoundingClientRect()
const dockBounds = this.refs.innerElement.getBoundingClientRect()
// Copy the bounds object since we can't mutate it.
const bounds = {
@@ -370,7 +374,7 @@ module.exports = class Dock {
// remove it as an argument and determine whether we're inside the toggle button using
// mouseenter/leave events on it. This class would still need to keep track of the mouse
// position (via a mousemove listener) for the other measurements, though.
const toggleButtonBounds = this.toggleButton.getBounds()
const toggleButtonBounds = this.refs.toggleButton.getBounds()
if (rectContainsPoint(toggleButtonBounds, point)) return true
// The area used when detecting exit is actually larger than when detecting entrances. Expand
@@ -707,13 +711,20 @@ module.exports = class Dock {
class DockResizeHandle {
constructor (props) {
this.handleMouseDown = this.handleMouseDown.bind(this)
this.element = document.createElement('div')
this.element.classList.add('atom-dock-resize-handle', props.location)
this.element.addEventListener('mousedown', this.handleMouseDown)
this.props = props
this.update(props)
etch.initialize(this)
}
render () {
const classList = ['atom-dock-resize-handle', this.props.location]
if (this.props.dockIsVisible) classList.push(RESIZE_HANDLE_RESIZABLE_CLASS)
return (
<div
className={classList.join(' ')}
on={{mousedown: this.handleMouseDown}}
/>
)
}
getElement () {
@@ -729,12 +740,7 @@ class DockResizeHandle {
update (newProps) {
this.props = Object.assign({}, this.props, newProps)
if (this.props.dockIsVisible) {
this.element.classList.add(RESIZE_HANDLE_RESIZABLE_CLASS)
} else {
this.element.classList.remove(RESIZE_HANDLE_RESIZABLE_CLASS)
}
return etch.update(this)
}
handleMouseDown (event) {
@@ -748,22 +754,26 @@ class DockResizeHandle {
class DockToggleButton {
constructor (props) {
this.handleClick = this.handleClick.bind(this)
this.handleDragEnter = this.handleDragEnter.bind(this)
this.element = document.createElement('div')
this.element.classList.add('atom-dock-toggle-button', props.location)
this.element.classList.add(props.location)
this.innerElement = document.createElement('div')
this.innerElement.classList.add('atom-dock-toggle-button-inner', props.location)
this.innerElement.addEventListener('click', this.handleClick)
this.innerElement.addEventListener('dragenter', this.handleDragEnter)
this.iconElement = document.createElement('span')
this.innerElement.appendChild(this.iconElement)
this.element.appendChild(this.innerElement)
this.props = props
this.update(props)
etch.initialize(this)
}
render () {
const classList = ['atom-dock-toggle-button', this.props.location]
if (this.props.visible) classList.push(TOGGLE_BUTTON_VISIBLE_CLASS)
return (
<div className={classList.join(' ')}>
<div
ref='innerElement'
className={`atom-dock-toggle-button-inner ${this.props.location}`}
on={{click: this.handleClick, dragenter: this.handleDragEnter}}>
<span
ref='iconElement'
className={`icon ${getIconName(this.props.location, this.props.dockIsVisible)}`} />
</div>
</div>
)
}
getElement () {
@@ -771,19 +781,12 @@ class DockToggleButton {
}
getBounds () {
return this.innerElement.getBoundingClientRect()
return this.refs.innerElement.getBoundingClientRect()
}
update (newProps) {
this.props = Object.assign({}, this.props, newProps)
if (this.props.visible) {
this.element.classList.add(TOGGLE_BUTTON_VISIBLE_CLASS)
} else {
this.element.classList.remove(TOGGLE_BUTTON_VISIBLE_CLASS)
}
this.iconElement.className = 'icon ' + getIconName(this.props.location, this.props.dockIsVisible)
return etch.update(this)
}
handleClick () {
@@ -795,6 +798,18 @@ class DockToggleButton {
}
}
// An etch component that doesn't use etch, this component provides a gateway from JSX back into
// the mutable DOM world.
class ElementComponent {
constructor (props) {
this.element = props.element
}
update (props) {
this.element = props.element
}
}
function getWidthOrHeight (location) {
return location === 'left' || location === 'right' ? 'width' : 'height'
}