/**
* @file clickable-component.js
*/
import Component from './component';
import * as Dom from './utils/dom.js';
import log from './utils/log.js';
import keycode from 'keycode';
/**
* Component which is clickable or keyboard actionable, but is not a
* native HTML button.
*
* @extends Component
*/
class ClickableComponent extends Component {
/**
* Creates an instance of this class.
*
* @param { import('./player').default } player
* The `Player` that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of component options.
*
* @param {function} [options.clickHandler]
* The function to call when the button is clicked / activated
*
* @param {string} [options.controlText]
* The text to set on the button
*
* @param {string} [options.className]
* A class or space separated list of classes to add the component
*
*/
constructor(player, options) {
super(player, options);
if (this.options_.controlText) {
this.controlText(this.options_.controlText);
}
this.handleMouseOver_ = (e) => this.handleMouseOver(e);
this.handleMouseOut_ = (e) => this.handleMouseOut(e);
this.handleClick_ = (e) => this.handleClick(e);
this.handleKeyDown_ = (e) => this.handleKeyDown(e);
this.emitTapEvents();
this.enable();
}
/**
* Create the `ClickableComponent`s DOM element.
*
* @param {string} [tag=div]
* The element's node type.
*
* @param {Object} [props={}]
* An object of properties that should be set on the element.
*
* @param {Object} [attributes={}]
* An object of attributes that should be set on the element.
*
* @return {Element}
* The element that gets created.
*/
createEl(tag = 'div', props = {}, attributes = {}) {
props = Object.assign({
className: this.buildCSSClass(),
tabIndex: 0
}, props);
if (tag === 'button') {
log.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
}
// Add ARIA attributes for clickable element which is not a native HTML button
attributes = Object.assign({
role: 'button'
}, attributes);
this.tabIndex_ = props.tabIndex;
const el = Dom.createEl(tag, props, attributes);
if (!this.player_.options_.experimentalSvgIcons) {
el.appendChild(Dom.createEl('span', {
className: 'vjs-icon-placeholder'
}, {
'aria-hidden': true
}));
}
this.createControlTextEl(el);
return el;
}
dispose() {
// remove controlTextEl_ on dispose
this.controlTextEl_ = null;
super.dispose();
}
/**
* Create a control text element on this `ClickableComponent`
*
* @param {Element} [el]
* Parent element for the control text.
*
* @return {Element}
* The control text element that gets created.
*/
createControlTextEl(el) {
this.controlTextEl_ = Dom.createEl('span', {
className: 'vjs-control-text'
}, {
// let the screen reader user know that the text of the element may change
'aria-live': 'polite'
});
if (el) {
el.appendChild(this.controlTextEl_);
}
this.controlText(this.controlText_, el);
return this.controlTextEl_;
}
/**
* Get or set the localize text to use for the controls on the `ClickableComponent`.
*
* @param {string} [text]
* Control text for element.
*
* @param {Element} [el=this.el()]
* Element to set the title on.
*
* @return {string}
* - The control text when getting
*/
controlText(text, el = this.el()) {
if (text === undefined) {
return this.controlText_ || 'Need Text';
}
const localizedText = this.localize(text);
/** @protected */
this.controlText_ = text;
Dom.textContent(this.controlTextEl_, localizedText);
if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
// Set title attribute if only an icon is shown
el.setAttribute('title', localizedText);
}
}
/**
* Builds the default DOM `className`.
*
* @return {string}
* The DOM `className` for this object.
*/
buildCSSClass() {
return `vjs-control vjs-button ${super.buildCSSClass()}`;
}
/**
* Enable this `ClickableComponent`
*/
enable() {
if (!this.enabled_) {
this.enabled_ = true;
this.removeClass('vjs-disabled');
this.el_.setAttribute('aria-disabled', 'false');
if (typeof this.tabIndex_ !== 'undefined') {
this.el_.setAttribute('tabIndex', this.tabIndex_);
}
this.on(['tap', 'click'], this.handleClick_);
this.on('keydown', this.handleKeyDown_);
}
}
/**
* Disable this `ClickableComponent`
*/
disable() {
this.enabled_ = false;
this.addClass('vjs-disabled');
this.el_.setAttribute('aria-disabled', 'true');
if (typeof this.tabIndex_ !== 'undefined') {
this.el_.removeAttribute('tabIndex');
}
this.off('mouseover', this.handleMouseOver_);
this.off('mouseout', this.handleMouseOut_);
this.off(['tap', 'click'], this.handleClick_);
this.off('keydown', this.handleKeyDown_);
}
/**
* Handles language change in ClickableComponent for the player in components
*
*
*/
handleLanguagechange() {
this.controlText(this.controlText_);
}
/**
* Event handler that is called when a `ClickableComponent` receives a
* `click` or `tap` event.
*
* @param {Event} event
* The `tap` or `click` event that caused this function to be called.
*
* @listens tap
* @listens click
* @abstract
*/
handleClick(event) {
if (this.options_.clickHandler) {
this.options_.clickHandler.call(this, arguments);
}
}
/**
* Event handler that is called when a `ClickableComponent` receives a
* `keydown` event.
*
* By default, if the key is Space or Enter, it will trigger a `click` event.
*
* @param {KeyboardEvent} event
* The `keydown` event that caused this function to be called.
*
* @listens keydown
*/
handleKeyDown(event) {
// Support Space or Enter key operation to fire a click event. Also,
// prevent the event from propagating through the DOM and triggering
// Player hotkeys.
if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
event.preventDefault();
event.stopPropagation();
this.trigger('click');
} else {
// Pass keypress handling up for unsupported keys
super.handleKeyDown(event);
}
}
}
Component.registerComponent('ClickableComponent', ClickableComponent);
export default ClickableComponent;