import { action } from '@ember/object';
import Component from '@glimmer/component';
import { isNone } from '@ember/utils';
import { next } from '@ember/runloop';
import transitionEnd from 'ember-bootstrap/utils/transition-end';
import deprecateSubclassing from 'ember-bootstrap/utils/deprecate-subclassing';
import { ref } from 'ember-ref-bucket';
import arg from '../utils/decorators/arg';
import { tracked } from '@glimmer/tracking';
/**
An Ember component that mimics the behaviour of [Bootstrap's collapse.js plugin](http://getbootstrap.com/javascript/#collapse)
### Usage
```hbs
<BsCollapse @collapsed={{this.collapsed}}>
<div class="well">
<h2>Collapse</h2>
<p>This is collapsible content</p>
</div>
</BsCollapse>
```
*Note that only invoking the component in a template as shown above is considered part of its public API. Extending from it (subclassing) is generally not supported, and may break at any time.*
@class Collapse
@namespace Components
@extends Glimmer.Component
@public
*/
@deprecateSubclassing
export default class Collapse extends Component {
/**
* @property _element
* @type null | HTMLElement
* @private
*/
@ref('mainNode') _element = null;
/**
* Collapsed/expanded state
*
* @property collapsed
* @type boolean
* @default true
* @public
*/
@arg
collapsed = true;
/**
* True if this item is expanded
*
* @property active
* @private
*/
active = !this.collapsed;
get collapse() {
return !this.transitioning;
}
get showContent() {
return this.collapse && this.active;
}
/**
* true if the component is currently transitioning
*
* @property transitioning
* @type boolean
* @private
*/
@tracked
transitioning = false;
/**
* The size of the element when collapsed. Defaults to 0.
*
* @property collapsedSize
* @type number
* @default 0
* @public
*/
@arg
collapsedSize = 0;
/**
* The size of the element when expanded. When null the value is calculated automatically to fit the containing elements.
*
* @property expandedSize
* @type number
* @default null
* @public
*/
@arg
expandedSize = null;
/**
* Calculates a hash for style attribute.
*/
get cssStyle() {
if (isNone(this.collapseSize)) {
return {};
}
return {
[this.collapseDimension]: `${this.collapseSize}px`,
};
}
/**
* Usually the size (height) of the element is only set while transitioning, and reseted afterwards. Set to true to always set a size.
*
* @property resetSizeWhenNotCollapsing
* @type boolean
* @default true
* @private
*/
resetSizeWhenNotCollapsing = true;
/**
* The direction (height/width) of the collapse animation.
* When setting this to 'width' you should also define custom CSS transitions for the width property, as the Bootstrap
* CSS does only support collapsible elements for the height direction.
*
* @property collapseDimension
* @type string
* @default 'height'
* @public
*/
@arg
collapseDimension = 'height';
/**
* The duration of the fade transition
*
* @property transitionDuration
* @type number
* @default 350
* @public
*/
@arg
transitionDuration = 350;
@tracked
collapseSize = null;
/**
* The action to be sent when the element is about to be hidden.
*
* @event onHide
* @public
*/
/**
* The action to be sent after the element has been completely hidden (including the CSS transition).
*
* @event onHidden
* @public
*/
/**
* The action to be sent when the element is about to be shown.
*
* @event onShow
* @public
*/
/**
* The action to be sent after the element has been completely shown (including the CSS transition).
*
* @event onShown
* @public
*/
/**
* Triggers the show transition
*
* @method show
* @protected
*/
show() {
this.args.onShow?.();
this.transitioning = true;
this.active = true;
this.collapseSize = this.collapsedSize;
transitionEnd(this._element, this.transitionDuration).then(() => {
if (this.isDestroyed) {
return;
}
this.transitioning = false;
if (this.resetSizeWhenNotCollapsing) {
this.collapseSize = null;
}
this.args.onShown?.();
});
next(this, function () {
if (!this.isDestroyed) {
this.collapseSize = this.getExpandedSize('show');
}
});
}
/**
* Get the size of the element when expanded
*
* @method getExpandedSize
* @param action
* @return {Number}
* @private
*/
getExpandedSize(action) {
let expandedSize = this.expandedSize;
if (expandedSize != null) {
return expandedSize;
}
let collapseElement = this._element;
let prefix = action === 'show' ? 'scroll' : 'offset';
let measureProperty = `${prefix}${this.collapseDimension
.substring(0, 1)
.toUpperCase()}${this.collapseDimension.substring(1)}`;
return collapseElement[measureProperty];
}
/**
* Triggers the hide transition
*
* @method hide
* @protected
*/
hide() {
this.args.onHide?.();
this.transitioning = true;
this.active = false;
this.collapseSize = this.getExpandedSize('hide');
transitionEnd(this._element, this.transitionDuration).then(() => {
if (this.isDestroyed) {
return;
}
this.transitioning = false;
if (this.resetSizeWhenNotCollapsing) {
this.collapseSize = null;
}
this.args.onHidden?.();
});
next(this, function () {
if (!this.isDestroyed) {
this.collapseSize = this.collapsedSize;
}
});
}
@action
_onCollapsedChange() {
let collapsed = this.collapsed;
let active = this.active;
if (collapsed !== active) {
return;
}
if (collapsed === false) {
this.show();
} else {
this.hide();
}
}
@action
_updateCollapsedSize() {
if (!this.resetSizeWhenNotCollapsing && this.collapsed && !this.collapsing) {
this.collapseSize = this.collapsedSize;
}
}
@action
_updateExpandedSize() {
if (!this.resetSizeWhenNotCollapsing && !this.collapsed && !this.collapsing) {
this.collapseSize = this.expandedSize;
}
}
}