src/dock.js
const etch = require('etch');
const _ = require('underscore-plus');
const { CompositeDisposable, Emitter } = require('event-kit');
const PaneContainer = require('./pane-container');
const TextEditor = require('./text-editor');
const Grim = require('grim');
const $ = etch.dom;
const MINIMUM_SIZE = 100;
const DEFAULT_INITIAL_SIZE = 300;
const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate';
const VISIBLE_CLASS = 'atom-dock-open';
const RESIZE_HANDLE_RESIZABLE_CLASS = 'atom-dock-resize-handle-resizable';
const TOGGLE_BUTTON_VISIBLE_CLASS = 'atom-dock-toggle-button-visible';
const CURSOR_OVERLAY_VISIBLE_CLASS = 'atom-dock-cursor-overlay-visible';
// Extended: A container at the edges of the editor window capable of holding items.
// You should not create a Dock directly. Instead, access one of the three docks of the workspace
// via {Workspace::getLeftDock}, {Workspace::getRightDock}, and {Workspace::getBottomDock}
// or add an item to a dock via {Workspace::open}.
module.exports = class Dock {
constructor(params) {
this.handleResizeHandleDragStart = this.handleResizeHandleDragStart.bind(
this
);
this.handleResizeToFit = this.handleResizeToFit.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
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);
this.config = params.config;
this.applicationDelegate = params.applicationDelegate;
this.deserializerManager = params.deserializerManager;
this.notificationManager = params.notificationManager;
this.viewRegistry = params.viewRegistry;
this.didActivate = params.didActivate;
this.emitter = new Emitter();
this.paneContainer = new PaneContainer({
location: this.location,
config: this.config,
applicationDelegate: this.applicationDelegate,
deserializerManager: this.deserializerManager,
notificationManager: this.notificationManager,
viewRegistry: this.viewRegistry
});
this.state = {
size: null,
visible: false,
shouldAnimate: false
};
this.subscriptions = new CompositeDisposable(
this.emitter,
this.paneContainer.onDidActivatePane(() => {
this.show();
this.didActivate(this);
}),
this.paneContainer.observePanes(pane => {
pane.onDidAddItem(this.handleDidAddPaneItem.bind(this));
pane.onDidRemoveItem(this.handleDidRemovePaneItem.bind(this));
}),
this.paneContainer.onDidChangeActivePane(item =>
params.didChangeActivePane(this, item)
),
this.paneContainer.onDidChangeActivePaneItem(item =>
params.didChangeActivePaneItem(this, item)
),
this.paneContainer.onDidDestroyPaneItem(item =>
params.didDestroyPaneItem(item)
)
);
}
// 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.
etch.updateSync(this);
}
getElement() {
// Because this code is included in the snapshot, we have to make sure we don't touch the DOM
// during initialization. Therefore, we defer initialization of the component (which creates a
// DOM element) until somebody asks for the element.
if (this.element == null) {
etch.initialize(this);
}
return this.element;
}
getLocation() {
return this.location;
}
destroy() {
this.subscriptions.dispose();
this.paneContainer.destroy();
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('drag', this.handleDrag);
window.removeEventListener('dragend', this.handleDragEnd);
}
setHovered(hovered) {
if (hovered === this.state.hovered) return;
this.setState({ hovered });
}
setDraggingItem(draggingItem) {
if (draggingItem === this.state.draggingItem) return;
this.setState({ draggingItem });
}
// Extended: Show the dock and focus its active {Pane}.
activate() {
this.getActivePane().activate();
}
// Extended: Show the dock without focusing it.
show() {
this.setState({ visible: true });
}
// Extended: Hide the dock and activate the {WorkspaceCenter} if the dock was
// was previously focused.
hide() {
this.setState({ visible: false });
}
// Extended: Toggle the dock's visibility without changing the {Workspace}'s
// active pane container.
toggle() {
const state = { visible: !this.state.visible };
if (!state.visible) state.hovered = false;
this.setState(state);
}
// Extended: Check if the dock is visible.
//
// Returns a {Boolean}.
isVisible() {
return this.state.visible;
}
setState(newState) {
const prevState = this.state;
const nextState = Object.assign({}, prevState, newState);
// Update the `shouldAnimate` state. This needs to be written to the DOM before updating the
// class that changes the animated property. Normally we'd have to defer the class change a
// frame to ensure the property is animated (or not) appropriately, however we luck out in this
// case because the drag start always happens before the item is dragged into the toggle button.
if (nextState.visible !== prevState.visible) {
// Never animate toggling visibility...
nextState.shouldAnimate = false;
} else if (
!nextState.visible &&
nextState.draggingItem &&
!prevState.draggingItem
) {
// ...but do animate if you start dragging while the panel is hidden.
nextState.shouldAnimate = true;
}
this.state = nextState;
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 (this.element != null) {
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);
}
if (visible !== prevState.visible) {
this.emitter.emit('did-change-visible', visible);
}
}
render() {
const innerElementClassList = ['atom-dock-inner', this.location];
if (this.state.visible) innerElementClassList.push(VISIBLE_CLASS);
const maskElementClassList = ['atom-dock-mask'];
if (this.state.shouldAnimate)
maskElementClassList.push(SHOULD_ANIMATE_CLASS);
const cursorOverlayElementClassList = [
'atom-dock-cursor-overlay',
this.location
];
if (this.state.resizing)
cursorOverlayElementClassList.push(CURSOR_OVERLAY_VISIBLE_CLASS);
const shouldBeVisible = this.state.visible || this.state.showDropTarget;
const size = Math.max(
MINIMUM_SIZE,
this.state.size ||
(this.state.draggingItem &&
getPreferredSize(this.state.draggingItem, this.location)) ||
DEFAULT_INITIAL_SIZE
);
// We need to change the size of the mask...
const maskStyle = {
[this.widthOrHeight]: `${shouldBeVisible ? size : 0}px`
};
// ...but the content needs to maintain a constant size.
const wrapperStyle = { [this.widthOrHeight]: `${size}px` };
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(' ') })
)
),
$(DockToggleButton, {
ref: 'toggleButton',
onDragEnter: this.state.draggingItem
? this.handleToggleButtonDragEnter
: null,
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))
})
)
);
}
update(props) {
// Since we're interopping with non-etch stuff, this method's actually never called.
return etch.update(this);
}
handleDidAddPaneItem() {
if (this.state.size == null) {
this.setState({ size: this.getInitialSize() });
}
}
handleDidRemovePaneItem() {
// Hide the dock if you remove the last item.
if (this.paneContainer.getPaneItems().length === 0) {
this.setState({ visible: false, hovered: false, size: null });
}
}
handleResizeHandleDragStart() {
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mouseup', this.handleMouseUp);
this.setState({ resizing: true });
}
handleResizeToFit() {
const item = this.getActivePaneItem();
if (item) {
const size = getPreferredSize(item, this.getLocation());
if (size != null) this.setState({ size });
}
}
handleMouseMove(event) {
if (event.buttons === 0) {
// We missed the mouseup event. For some reason it happens on Windows
this.handleMouseUp(event);
return;
}
let size = 0;
switch (this.location) {
case 'left':
size = event.pageX - this.element.getBoundingClientRect().left;
break;
case 'bottom':
size = this.element.getBoundingClientRect().bottom - event.pageY;
break;
case 'right':
size = this.element.getBoundingClientRect().right - event.pageX;
break;
}
this.setState({ size });
}
handleMouseUp(event) {
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('mouseup', this.handleMouseUp);
this.setState({ resizing: false });
}
handleToggleButtonDragEnter() {
this.setState({ showDropTarget: true });
window.addEventListener('drag', this.handleDrag);
window.addEventListener('dragend', this.handleDragEnd);
}
handleDrag(event) {
if (!this.pointWithinHoverArea({ x: event.pageX, y: event.pageY }, true)) {
this.draggedOut();
}
}
handleDragEnd() {
this.draggedOut();
}
draggedOut() {
this.setState({ showDropTarget: false });
window.removeEventListener('drag', this.handleDrag);
window.removeEventListener('dragend', this.handleDragEnd);
}
// Determine whether the cursor is within the dock hover area. This isn't as simple as just using
// mouseenter/leave because we want to be a little more forgiving. For example, if the cursor is
// over the footer, we want to show the bottom dock's toggle button. Also note that our criteria
// for detecting entry are different than detecting exit but, in order for us to avoid jitter, the
// area considered when detecting exit MUST fully encompass the area considered when detecting
// entry.
pointWithinHoverArea(point, detectingExit) {
const dockBounds = this.refs.innerElement.getBoundingClientRect();
// Copy the bounds object since we can't mutate it.
const bounds = {
top: dockBounds.top,
right: dockBounds.right,
bottom: dockBounds.bottom,
left: dockBounds.left
};
// To provide a minimum target, expand the area toward the center a bit.
switch (this.location) {
case 'right':
bounds.left = Math.min(bounds.left, bounds.right - 2);
break;
case 'bottom':
bounds.top = Math.min(bounds.top, bounds.bottom - 1);
break;
case 'left':
bounds.right = Math.max(bounds.right, bounds.left + 2);
break;
}
// Further expand the area to include all panels that are closer to the edge than the dock.
switch (this.location) {
case 'right':
bounds.right = Number.POSITIVE_INFINITY;
break;
case 'bottom':
bounds.bottom = Number.POSITIVE_INFINITY;
break;
case 'left':
bounds.left = Number.NEGATIVE_INFINITY;
break;
}
// If we're in this area, we know we're within the hover area without having to take further
// measurements.
if (rectContainsPoint(bounds, point)) return true;
// If we're within the toggle button, we're definitely in the hover area. Unfortunately, we
// can't do this measurement conditionally (e.g. only if the toggle button is visible) because
// our knowledge of the toggle's button is incomplete due to CSS animations. (We may think the
// toggle button isn't visible when in actuality it is, but is animating to its hidden state.)
//
// Since `point` is always the current mouse position, one possible optimization would be to
// 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.refs.toggleButton.getBounds();
if (rectContainsPoint(toggleButtonBounds, point)) return true;
// The area used when detecting exit is actually larger than when detecting entrances. Expand
// our bounds and recheck them.
if (detectingExit) {
const hoverMargin = 20;
switch (this.location) {
case 'right':
bounds.left =
Math.min(bounds.left, toggleButtonBounds.left) - hoverMargin;
break;
case 'bottom':
bounds.top =
Math.min(bounds.top, toggleButtonBounds.top) - hoverMargin;
break;
case 'left':
bounds.right =
Math.max(bounds.right, toggleButtonBounds.right) + hoverMargin;
break;
}
if (rectContainsPoint(bounds, point)) return true;
}
return false;
}
getInitialSize() {
// The item may not have been activated yet. If that's the case, just use the first item.
const activePaneItem =
this.paneContainer.getActivePaneItem() ||
this.paneContainer.getPaneItems()[0];
// If there are items, we should have an explicit width; if not, we shouldn't.
return activePaneItem
? getPreferredSize(activePaneItem, this.location) || DEFAULT_INITIAL_SIZE
: null;
}
serialize() {
return {
deserializer: 'Dock',
size: this.state.size,
paneContainer: this.paneContainer.serialize(),
visible: this.state.visible
};
}
deserialize(serialized, deserializerManager) {
this.paneContainer.deserialize(
serialized.paneContainer,
deserializerManager
);
this.setState({
size: serialized.size || this.getInitialSize(),
// If no items could be deserialized, we don't want to show the dock (even if it was visible last time)
visible:
serialized.visible && this.paneContainer.getPaneItems().length > 0
});
}
/*
Section: Event Subscription
*/
// Essential: Invoke the given callback when the visibility of the dock changes.
//
// * `callback` {Function} to be called when the visibility changes.
// * `visible` {Boolean} Is the dock now visible?
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeVisible(callback) {
return this.emitter.on('did-change-visible', callback);
}
// Essential: Invoke the given callback with the current and all future visibilities of the dock.
//
// * `callback` {Function} to be called when the visibility changes.
// * `visible` {Boolean} Is the dock now visible?
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeVisible(callback) {
callback(this.isVisible());
return this.onDidChangeVisible(callback);
}
// Essential: Invoke the given callback with all current and future panes items
// in the dock.
//
// * `callback` {Function} to be called with current and future pane items.
// * `item` An item that is present in {::getPaneItems} at the time of
// subscription or that is added at some later time.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observePaneItems(callback) {
return this.paneContainer.observePaneItems(callback);
}
// Essential: Invoke the given callback when the active pane item changes.
//
// Because observers are invoked synchronously, it's important not to perform
// any expensive operations via this method. Consider
// {::onDidStopChangingActivePaneItem} to delay operations until after changes
// stop occurring.
//
// * `callback` {Function} to be called when the active pane item changes.
// * `item` The active pane item.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeActivePaneItem(callback) {
return this.paneContainer.onDidChangeActivePaneItem(callback);
}
// Essential: Invoke the given callback when the active pane item stops
// changing.
//
// Observers are called asynchronously 100ms after the last active pane item
// change. Handling changes here rather than in the synchronous
// {::onDidChangeActivePaneItem} prevents unneeded work if the user is quickly
// changing or closing tabs and ensures critical UI feedback, like changing the
// highlighted tab, gets priority over work that can be done asynchronously.
//
// * `callback` {Function} to be called when the active pane item stopts
// changing.
// * `item` The active pane item.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidStopChangingActivePaneItem(callback) {
return this.paneContainer.onDidStopChangingActivePaneItem(callback);
}
// Essential: Invoke the given callback with the current active pane item and
// with all future active pane items in the dock.
//
// * `callback` {Function} to be called when the active pane item changes.
// * `item` The current active pane item.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeActivePaneItem(callback) {
return this.paneContainer.observeActivePaneItem(callback);
}
// Extended: Invoke the given callback when a pane is added to the dock.
//
// * `callback` {Function} to be called panes are added.
// * `event` {Object} with the following keys:
// * `pane` The added pane.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddPane(callback) {
return this.paneContainer.onDidAddPane(callback);
}
// Extended: Invoke the given callback before a pane is destroyed in the
// dock.
//
// * `callback` {Function} to be called before panes are destroyed.
// * `event` {Object} with the following keys:
// * `pane` The pane to be destroyed.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onWillDestroyPane(callback) {
return this.paneContainer.onWillDestroyPane(callback);
}
// Extended: Invoke the given callback when a pane is destroyed in the dock.
//
// * `callback` {Function} to be called panes are destroyed.
// * `event` {Object} with the following keys:
// * `pane` The destroyed pane.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidDestroyPane(callback) {
return this.paneContainer.onDidDestroyPane(callback);
}
// Extended: Invoke the given callback with all current and future panes in the
// dock.
//
// * `callback` {Function} to be called with current and future panes.
// * `pane` A {Pane} that is present in {::getPanes} at the time of
// subscription or that is added at some later time.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observePanes(callback) {
return this.paneContainer.observePanes(callback);
}
// Extended: Invoke the given callback when the active pane changes.
//
// * `callback` {Function} to be called when the active pane changes.
// * `pane` A {Pane} that is the current return value of {::getActivePane}.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeActivePane(callback) {
return this.paneContainer.onDidChangeActivePane(callback);
}
// Extended: Invoke the given callback with the current active pane and when
// the active pane changes.
//
// * `callback` {Function} to be called with the current and future active#
// panes.
// * `pane` A {Pane} that is the current return value of {::getActivePane}.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
observeActivePane(callback) {
return this.paneContainer.observeActivePane(callback);
}
// Extended: Invoke the given callback when a pane item is added to the dock.
//
// * `callback` {Function} to be called when pane items are added.
// * `event` {Object} with the following keys:
// * `item` The added pane item.
// * `pane` {Pane} containing the added item.
// * `index` {Number} indicating the index of the added item in its pane.
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidAddPaneItem(callback) {
return this.paneContainer.onDidAddPaneItem(callback);
}
// Extended: Invoke the given callback when a pane item is about to be
// destroyed, before the user is prompted to save it.
//
// * `callback` {Function} to be called before pane items are destroyed.
// * `event` {Object} with the following keys:
// * `item` The item to be destroyed.
// * `pane` {Pane} containing the item to be destroyed.
// * `index` {Number} indicating the index of the item to be destroyed in
// its pane.
//
// Returns a {Disposable} on which `.dispose` can be called to unsubscribe.
onWillDestroyPaneItem(callback) {
return this.paneContainer.onWillDestroyPaneItem(callback);
}
// Extended: Invoke the given callback when a pane item is destroyed.
//
// * `callback` {Function} to be called when pane items are destroyed.
// * `event` {Object} with the following keys:
// * `item` The destroyed item.
// * `pane` {Pane} containing the destroyed item.
// * `index` {Number} indicating the index of the destroyed item in its
// pane.
//
// Returns a {Disposable} on which `.dispose` can be called to unsubscribe.
onDidDestroyPaneItem(callback) {
return this.paneContainer.onDidDestroyPaneItem(callback);
}
// Extended: Invoke the given callback when the hovered state of the dock changes.
//
// * `callback` {Function} to be called when the hovered state changes.
// * `hovered` {Boolean} Is the dock now hovered?
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeHovered(callback) {
return this.emitter.on('did-change-hovered', callback);
}
/*
Section: Pane Items
*/
// Essential: Get all pane items in the dock.
//
// Returns an {Array} of items.
getPaneItems() {
return this.paneContainer.getPaneItems();
}
// Essential: Get the active {Pane}'s active item.
//
// Returns an pane item {Object}.
getActivePaneItem() {
return this.paneContainer.getActivePaneItem();
}
// Deprecated: Get the active item if it is a {TextEditor}.
//
// Returns a {TextEditor} or `undefined` if the current active item is not a
// {TextEditor}.
getActiveTextEditor() {
Grim.deprecate(
'Text editors are not allowed in docks. Use atom.workspace.getActiveTextEditor() instead.'
);
const activeItem = this.getActivePaneItem();
if (activeItem instanceof TextEditor) {
return activeItem;
}
}
// Save all pane items.
saveAll() {
this.paneContainer.saveAll();
}
confirmClose(options) {
return this.paneContainer.confirmClose(options);
}
/*
Section: Panes
*/
// Extended: Get all panes in the dock.
//
// Returns an {Array} of {Pane}s.
getPanes() {
return this.paneContainer.getPanes();
}
// Extended: Get the active {Pane}.
//
// Returns a {Pane}.
getActivePane() {
return this.paneContainer.getActivePane();
}
// Extended: Make the next pane active.
activateNextPane() {
return this.paneContainer.activateNextPane();
}
// Extended: Make the previous pane active.
activatePreviousPane() {
return this.paneContainer.activatePreviousPane();
}
paneForURI(uri) {
return this.paneContainer.paneForURI(uri);
}
paneForItem(item) {
return this.paneContainer.paneForItem(item);
}
// Destroy (close) the active pane.
destroyActivePane() {
const activePane = this.getActivePane();
if (activePane != null) {
activePane.destroy();
}
}
};
class DockResizeHandle {
constructor(props) {
this.props = 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() {
return this.element;
}
getSize() {
if (!this.size) {
this.size = this.element.getBoundingClientRect()[
getWidthOrHeight(this.props.location)
];
}
return this.size;
}
update(newProps) {
this.props = Object.assign({}, this.props, newProps);
return etch.update(this);
}
handleMouseDown(event) {
if (event.detail === 2) {
this.props.onResizeToFit();
} else if (this.props.dockIsVisible) {
this.props.onResizeStart();
}
}
}
class DockToggleButton {
constructor(props) {
this.props = 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.props.onDragEnter
}
},
$.span({
ref: 'iconElement',
className: `icon ${getIconName(
this.props.location,
this.props.dockIsVisible
)}`
})
)
);
}
getElement() {
return this.element;
}
getBounds() {
return this.refs.innerElement.getBoundingClientRect();
}
update(newProps) {
this.props = Object.assign({}, this.props, newProps);
return etch.update(this);
}
handleClick() {
this.props.toggle();
}
}
// 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';
}
function getPreferredSize(item, location) {
switch (location) {
case 'left':
case 'right':
return typeof item.getPreferredWidth === 'function'
? item.getPreferredWidth()
: null;
default:
return typeof item.getPreferredHeight === 'function'
? item.getPreferredHeight()
: null;
}
}
function getIconName(location, visible) {
switch (location) {
case 'right':
return visible ? 'icon-chevron-right' : 'icon-chevron-left';
case 'bottom':
return visible ? 'icon-chevron-down' : 'icon-chevron-up';
case 'left':
return visible ? 'icon-chevron-left' : 'icon-chevron-right';
default:
throw new Error(`Invalid location: ${location}`);
}
}
function rectContainsPoint(rect, point) {
return (
point.x >= rect.left &&
point.y >= rect.top &&
point.x <= rect.right &&
point.y <= rect.bottom
);
}
// Is the item allowed in the given location?
function isItemAllowed(item, location) {
if (typeof item.getAllowedLocations !== 'function') return true;
return item.getAllowedLocations().includes(location);
}