1. 1 : /**
  2. 2 : * @file clickable-component.js
  3. 3 : */
  4. 4 : import Component from './component';
  5. 5 : import * as Dom from './utils/dom.js';
  6. 6 : import log from './utils/log.js';
  7. 7 : import keycode from 'keycode';
  8. 8 :
  9. 9 : /**
  10. 10 : * Component which is clickable or keyboard actionable, but is not a
  11. 11 : * native HTML button.
  12. 12 : *
  13. 13 : * @extends Component
  14. 14 : */
  15. 15 : class ClickableComponent extends Component {
  16. 16 :
  17. 17 : /**
  18. 18 : * Creates an instance of this class.
  19. 19 : *
  20. 20 : * @param { import('./player').default } player
  21. 21 : * The `Player` that this class should be attached to.
  22. 22 : *
  23. 23 : * @param {Object} [options]
  24. 24 : * The key/value store of component options.
  25. 25 : *
  26. 26 : * @param {function} [options.clickHandler]
  27. 27 : * The function to call when the button is clicked / activated
  28. 28 : *
  29. 29 : * @param {string} [options.controlText]
  30. 30 : * The text to set on the button
  31. 31 : *
  32. 32 : * @param {string} [options.className]
  33. 33 : * A class or space separated list of classes to add the component
  34. 34 : *
  35. 35 : */
  36. 36 : constructor(player, options) {
  37. 37 :
  38. 38 : super(player, options);
  39. 39 :
  40. 40 : if (this.options_.controlText) {
  41. 41 : this.controlText(this.options_.controlText);
  42. 42 : }
  43. 43 :
  44. 44 : this.handleMouseOver_ = (e) => this.handleMouseOver(e);
  45. 45 : this.handleMouseOut_ = (e) => this.handleMouseOut(e);
  46. 46 : this.handleClick_ = (e) => this.handleClick(e);
  47. 47 : this.handleKeyDown_ = (e) => this.handleKeyDown(e);
  48. 48 :
  49. 49 : this.emitTapEvents();
  50. 50 :
  51. 51 : this.enable();
  52. 52 : }
  53. 53 :
  54. 54 : /**
  55. 55 : * Create the `ClickableComponent`s DOM element.
  56. 56 : *
  57. 57 : * @param {string} [tag=div]
  58. 58 : * The element's node type.
  59. 59 : *
  60. 60 : * @param {Object} [props={}]
  61. 61 : * An object of properties that should be set on the element.
  62. 62 : *
  63. 63 : * @param {Object} [attributes={}]
  64. 64 : * An object of attributes that should be set on the element.
  65. 65 : *
  66. 66 : * @return {Element}
  67. 67 : * The element that gets created.
  68. 68 : */
  69. 69 : createEl(tag = 'div', props = {}, attributes = {}) {
  70. 70 : props = Object.assign({
  71. 71 : className: this.buildCSSClass(),
  72. 72 : tabIndex: 0
  73. 73 : }, props);
  74. 74 :
  75. 75 : if (tag === 'button') {
  76. 76 : log.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
  77. 77 : }
  78. 78 :
  79. 79 : // Add ARIA attributes for clickable element which is not a native HTML button
  80. 80 : attributes = Object.assign({
  81. 81 : role: 'button'
  82. 82 : }, attributes);
  83. 83 :
  84. 84 : this.tabIndex_ = props.tabIndex;
  85. 85 :
  86. 86 : const el = Dom.createEl(tag, props, attributes);
  87. 87 :
  88. 88 : el.appendChild(Dom.createEl('span', {
  89. 89 : className: 'vjs-icon-placeholder'
  90. 90 : }, {
  91. 91 : 'aria-hidden': true
  92. 92 : }));
  93. 93 :
  94. 94 : this.createControlTextEl(el);
  95. 95 :
  96. 96 : return el;
  97. 97 : }
  98. 98 :
  99. 99 : dispose() {
  100. 100 : // remove controlTextEl_ on dispose
  101. 101 : this.controlTextEl_ = null;
  102. 102 :
  103. 103 : super.dispose();
  104. 104 : }
  105. 105 :
  106. 106 : /**
  107. 107 : * Create a control text element on this `ClickableComponent`
  108. 108 : *
  109. 109 : * @param {Element} [el]
  110. 110 : * Parent element for the control text.
  111. 111 : *
  112. 112 : * @return {Element}
  113. 113 : * The control text element that gets created.
  114. 114 : */
  115. 115 : createControlTextEl(el) {
  116. 116 : this.controlTextEl_ = Dom.createEl('span', {
  117. 117 : className: 'vjs-control-text'
  118. 118 : }, {
  119. 119 : // let the screen reader user know that the text of the element may change
  120. 120 : 'aria-live': 'polite'
  121. 121 : });
  122. 122 :
  123. 123 : if (el) {
  124. 124 : el.appendChild(this.controlTextEl_);
  125. 125 : }
  126. 126 :
  127. 127 : this.controlText(this.controlText_, el);
  128. 128 :
  129. 129 : return this.controlTextEl_;
  130. 130 : }
  131. 131 :
  132. 132 : /**
  133. 133 : * Get or set the localize text to use for the controls on the `ClickableComponent`.
  134. 134 : *
  135. 135 : * @param {string} [text]
  136. 136 : * Control text for element.
  137. 137 : *
  138. 138 : * @param {Element} [el=this.el()]
  139. 139 : * Element to set the title on.
  140. 140 : *
  141. 141 : * @return {string}
  142. 142 : * - The control text when getting
  143. 143 : */
  144. 144 : controlText(text, el = this.el()) {
  145. 145 : if (text === undefined) {
  146. 146 : return this.controlText_ || 'Need Text';
  147. 147 : }
  148. 148 :
  149. 149 : const localizedText = this.localize(text);
  150. 150 :
  151. 151 : /** @protected */
  152. 152 : this.controlText_ = text;
  153. 153 : Dom.textContent(this.controlTextEl_, localizedText);
  154. 154 : if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
  155. 155 : // Set title attribute if only an icon is shown
  156. 156 : el.setAttribute('title', localizedText);
  157. 157 : }
  158. 158 : }
  159. 159 :
  160. 160 : /**
  161. 161 : * Builds the default DOM `className`.
  162. 162 : *
  163. 163 : * @return {string}
  164. 164 : * The DOM `className` for this object.
  165. 165 : */
  166. 166 : buildCSSClass() {
  167. 167 : return `vjs-control vjs-button ${super.buildCSSClass()}`;
  168. 168 : }
  169. 169 :
  170. 170 : /**
  171. 171 : * Enable this `ClickableComponent`
  172. 172 : */
  173. 173 : enable() {
  174. 174 : if (!this.enabled_) {
  175. 175 : this.enabled_ = true;
  176. 176 : this.removeClass('vjs-disabled');
  177. 177 : this.el_.setAttribute('aria-disabled', 'false');
  178. 178 : if (typeof this.tabIndex_ !== 'undefined') {
  179. 179 : this.el_.setAttribute('tabIndex', this.tabIndex_);
  180. 180 : }
  181. 181 : this.on(['tap', 'click'], this.handleClick_);
  182. 182 : this.on('keydown', this.handleKeyDown_);
  183. 183 : }
  184. 184 : }
  185. 185 :
  186. 186 : /**
  187. 187 : * Disable this `ClickableComponent`
  188. 188 : */
  189. 189 : disable() {
  190. 190 : this.enabled_ = false;
  191. 191 : this.addClass('vjs-disabled');
  192. 192 : this.el_.setAttribute('aria-disabled', 'true');
  193. 193 : if (typeof this.tabIndex_ !== 'undefined') {
  194. 194 : this.el_.removeAttribute('tabIndex');
  195. 195 : }
  196. 196 : this.off('mouseover', this.handleMouseOver_);
  197. 197 : this.off('mouseout', this.handleMouseOut_);
  198. 198 : this.off(['tap', 'click'], this.handleClick_);
  199. 199 : this.off('keydown', this.handleKeyDown_);
  200. 200 : }
  201. 201 :
  202. 202 : /**
  203. 203 : * Handles language change in ClickableComponent for the player in components
  204. 204 : *
  205. 205 : *
  206. 206 : */
  207. 207 : handleLanguagechange() {
  208. 208 : this.controlText(this.controlText_);
  209. 209 : }
  210. 210 :
  211. 211 : /**
  212. 212 : * Event handler that is called when a `ClickableComponent` receives a
  213. 213 : * `click` or `tap` event.
  214. 214 : *
  215. 215 : * @param {Event} event
  216. 216 : * The `tap` or `click` event that caused this function to be called.
  217. 217 : *
  218. 218 : * @listens tap
  219. 219 : * @listens click
  220. 220 : * @abstract
  221. 221 : */
  222. 222 : handleClick(event) {
  223. 223 : if (this.options_.clickHandler) {
  224. 224 : this.options_.clickHandler.call(this, arguments);
  225. 225 : }
  226. 226 : }
  227. 227 :
  228. 228 : /**
  229. 229 : * Event handler that is called when a `ClickableComponent` receives a
  230. 230 : * `keydown` event.
  231. 231 : *
  232. 232 : * By default, if the key is Space or Enter, it will trigger a `click` event.
  233. 233 : *
  234. 234 : * @param {Event} event
  235. 235 : * The `keydown` event that caused this function to be called.
  236. 236 : *
  237. 237 : * @listens keydown
  238. 238 : */
  239. 239 : handleKeyDown(event) {
  240. 240 :
  241. 241 : // Support Space or Enter key operation to fire a click event. Also,
  242. 242 : // prevent the event from propagating through the DOM and triggering
  243. 243 : // Player hotkeys.
  244. 244 : if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) {
  245. 245 : event.preventDefault();
  246. 246 : event.stopPropagation();
  247. 247 : this.trigger('click');
  248. 248 : } else {
  249. 249 :
  250. 250 : // Pass keypress handling up for unsupported keys
  251. 251 : super.handleKeyDown(event);
  252. 252 : }
  253. 253 : }
  254. 254 : }
  255. 255 :
  256. 256 : Component.registerComponent('ClickableComponent', ClickableComponent);
  257. 257 : export default ClickableComponent;