1. 1 : /**
  2. 2 : * @file slider.js
  3. 3 : */
  4. 4 : import Component from '../component.js';
  5. 5 : import * as Dom from '../utils/dom.js';
  6. 6 : import {assign} from '../utils/obj';
  7. 7 : import {IS_CHROME} from '../utils/browser.js';
  8. 8 : import clamp from '../utils/clamp.js';
  9. 9 : import keycode from 'keycode';
  10. 10 :
  11. 11 : /**
  12. 12 : * The base functionality for a slider. Can be vertical or horizontal.
  13. 13 : * For instance the volume bar or the seek bar on a video is a slider.
  14. 14 : *
  15. 15 : * @extends Component
  16. 16 : */
  17. 17 : class Slider extends Component {
  18. 18 :
  19. 19 : /**
  20. 20 : * Create an instance of this class
  21. 21 : *
  22. 22 : * @param {Player} player
  23. 23 : * The `Player` that this class should be attached to.
  24. 24 : *
  25. 25 : * @param {Object} [options]
  26. 26 : * The key/value store of player options.
  27. 27 : */
  28. 28 : constructor(player, options) {
  29. 29 : super(player, options);
  30. 30 :
  31. 31 : this.handleMouseDown_ = (e) => this.handleMouseDown(e);
  32. 32 : this.handleMouseUp_ = (e) => this.handleMouseUp(e);
  33. 33 : this.handleKeyDown_ = (e) => this.handleKeyDown(e);
  34. 34 : this.handleClick_ = (e) => this.handleClick(e);
  35. 35 : this.handleMouseMove_ = (e) => this.handleMouseMove(e);
  36. 36 : this.update_ = (e) => this.update(e);
  37. 37 :
  38. 38 : // Set property names to bar to match with the child Slider class is looking for
  39. 39 : this.bar = this.getChild(this.options_.barName);
  40. 40 :
  41. 41 : // Set a horizontal or vertical class on the slider depending on the slider type
  42. 42 : this.vertical(!!this.options_.vertical);
  43. 43 :
  44. 44 : this.enable();
  45. 45 : }
  46. 46 :
  47. 47 : /**
  48. 48 : * Are controls are currently enabled for this slider or not.
  49. 49 : *
  50. 50 : * @return {boolean}
  51. 51 : * true if controls are enabled, false otherwise
  52. 52 : */
  53. 53 : enabled() {
  54. 54 : return this.enabled_;
  55. 55 : }
  56. 56 :
  57. 57 : /**
  58. 58 : * Enable controls for this slider if they are disabled
  59. 59 : */
  60. 60 : enable() {
  61. 61 : if (this.enabled()) {
  62. 62 : return;
  63. 63 : }
  64. 64 :
  65. 65 : this.on('mousedown', this.handleMouseDown_);
  66. 66 : this.on('touchstart', this.handleMouseDown_);
  67. 67 : this.on('keydown', this.handleKeyDown_);
  68. 68 : this.on('click', this.handleClick_);
  69. 69 :
  70. 70 : // TODO: deprecated, controlsvisible does not seem to be fired
  71. 71 : this.on(this.player_, 'controlsvisible', this.update);
  72. 72 :
  73. 73 : if (this.playerEvent) {
  74. 74 : this.on(this.player_, this.playerEvent, this.update);
  75. 75 : }
  76. 76 :
  77. 77 : this.removeClass('disabled');
  78. 78 : this.setAttribute('tabindex', 0);
  79. 79 :
  80. 80 : this.enabled_ = true;
  81. 81 : }
  82. 82 :
  83. 83 : /**
  84. 84 : * Disable controls for this slider if they are enabled
  85. 85 : */
  86. 86 : disable() {
  87. 87 : if (!this.enabled()) {
  88. 88 : return;
  89. 89 : }
  90. 90 : const doc = this.bar.el_.ownerDocument;
  91. 91 :
  92. 92 : this.off('mousedown', this.handleMouseDown_);
  93. 93 : this.off('touchstart', this.handleMouseDown_);
  94. 94 : this.off('keydown', this.handleKeyDown_);
  95. 95 : this.off('click', this.handleClick_);
  96. 96 : this.off(this.player_, 'controlsvisible', this.update_);
  97. 97 : this.off(doc, 'mousemove', this.handleMouseMove_);
  98. 98 : this.off(doc, 'mouseup', this.handleMouseUp_);
  99. 99 : this.off(doc, 'touchmove', this.handleMouseMove_);
  100. 100 : this.off(doc, 'touchend', this.handleMouseUp_);
  101. 101 : this.removeAttribute('tabindex');
  102. 102 :
  103. 103 : this.addClass('disabled');
  104. 104 :
  105. 105 : if (this.playerEvent) {
  106. 106 : this.off(this.player_, this.playerEvent, this.update);
  107. 107 : }
  108. 108 : this.enabled_ = false;
  109. 109 : }
  110. 110 :
  111. 111 : /**
  112. 112 : * Create the `Slider`s DOM element.
  113. 113 : *
  114. 114 : * @param {string} type
  115. 115 : * Type of element to create.
  116. 116 : *
  117. 117 : * @param {Object} [props={}]
  118. 118 : * List of properties in Object form.
  119. 119 : *
  120. 120 : * @param {Object} [attributes={}]
  121. 121 : * list of attributes in Object form.
  122. 122 : *
  123. 123 : * @return {Element}
  124. 124 : * The element that gets created.
  125. 125 : */
  126. 126 : createEl(type, props = {}, attributes = {}) {
  127. 127 : // Add the slider element class to all sub classes
  128. 128 : props.className = props.className + ' vjs-slider';
  129. 129 : props = assign({
  130. 130 : tabIndex: 0
  131. 131 : }, props);
  132. 132 :
  133. 133 : attributes = assign({
  134. 134 : 'role': 'slider',
  135. 135 : 'aria-valuenow': 0,
  136. 136 : 'aria-valuemin': 0,
  137. 137 : 'aria-valuemax': 100,
  138. 138 : 'tabIndex': 0
  139. 139 : }, attributes);
  140. 140 :
  141. 141 : return super.createEl(type, props, attributes);
  142. 142 : }
  143. 143 :
  144. 144 : /**
  145. 145 : * Handle `mousedown` or `touchstart` events on the `Slider`.
  146. 146 : *
  147. 147 : * @param {EventTarget~Event} event
  148. 148 : * `mousedown` or `touchstart` event that triggered this function
  149. 149 : *
  150. 150 : * @listens mousedown
  151. 151 : * @listens touchstart
  152. 152 : * @fires Slider#slideractive
  153. 153 : */
  154. 154 : handleMouseDown(event) {
  155. 155 : const doc = this.bar.el_.ownerDocument;
  156. 156 :
  157. 157 : if (event.type === 'mousedown') {
  158. 158 : event.preventDefault();
  159. 159 : }
  160. 160 : // Do not call preventDefault() on touchstart in Chrome
  161. 161 : // to avoid console warnings. Use a 'touch-action: none' style
  162. 162 : // instead to prevent unintented scrolling.
  163. 163 : // https://developers.google.com/web/updates/2017/01/scrolling-intervention
  164. 164 : if (event.type === 'touchstart' && !IS_CHROME) {
  165. 165 : event.preventDefault();
  166. 166 : }
  167. 167 : Dom.blockTextSelection();
  168. 168 :
  169. 169 : this.addClass('vjs-sliding');
  170. 170 : /**
  171. 171 : * Triggered when the slider is in an active state
  172. 172 : *
  173. 173 : * @event Slider#slideractive
  174. 174 : * @type {EventTarget~Event}
  175. 175 : */
  176. 176 : this.trigger('slideractive');
  177. 177 :
  178. 178 : this.on(doc, 'mousemove', this.handleMouseMove_);
  179. 179 : this.on(doc, 'mouseup', this.handleMouseUp_);
  180. 180 : this.on(doc, 'touchmove', this.handleMouseMove_);
  181. 181 : this.on(doc, 'touchend', this.handleMouseUp_);
  182. 182 :
  183. 183 : this.handleMouseMove(event);
  184. 184 : }
  185. 185 :
  186. 186 : /**
  187. 187 : * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`.
  188. 188 : * The `mousemove` and `touchmove` events will only only trigger this function during
  189. 189 : * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and
  190. 190 : * {@link Slider#handleMouseUp}.
  191. 191 : *
  192. 192 : * @param {EventTarget~Event} event
  193. 193 : * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered
  194. 194 : * this function
  195. 195 : *
  196. 196 : * @listens mousemove
  197. 197 : * @listens touchmove
  198. 198 : */
  199. 199 : handleMouseMove(event) {}
  200. 200 :
  201. 201 : /**
  202. 202 : * Handle `mouseup` or `touchend` events on the `Slider`.
  203. 203 : *
  204. 204 : * @param {EventTarget~Event} event
  205. 205 : * `mouseup` or `touchend` event that triggered this function.
  206. 206 : *
  207. 207 : * @listens touchend
  208. 208 : * @listens mouseup
  209. 209 : * @fires Slider#sliderinactive
  210. 210 : */
  211. 211 : handleMouseUp() {
  212. 212 : const doc = this.bar.el_.ownerDocument;
  213. 213 :
  214. 214 : Dom.unblockTextSelection();
  215. 215 :
  216. 216 : this.removeClass('vjs-sliding');
  217. 217 : /**
  218. 218 : * Triggered when the slider is no longer in an active state.
  219. 219 : *
  220. 220 : * @event Slider#sliderinactive
  221. 221 : * @type {EventTarget~Event}
  222. 222 : */
  223. 223 : this.trigger('sliderinactive');
  224. 224 :
  225. 225 : this.off(doc, 'mousemove', this.handleMouseMove_);
  226. 226 : this.off(doc, 'mouseup', this.handleMouseUp_);
  227. 227 : this.off(doc, 'touchmove', this.handleMouseMove_);
  228. 228 : this.off(doc, 'touchend', this.handleMouseUp_);
  229. 229 :
  230. 230 : this.update();
  231. 231 : }
  232. 232 :
  233. 233 : /**
  234. 234 : * Update the progress bar of the `Slider`.
  235. 235 : *
  236. 236 : * @return {number}
  237. 237 : * The percentage of progress the progress bar represents as a
  238. 238 : * number from 0 to 1.
  239. 239 : */
  240. 240 : update() {
  241. 241 : // In VolumeBar init we have a setTimeout for update that pops and update
  242. 242 : // to the end of the execution stack. The player is destroyed before then
  243. 243 : // update will cause an error
  244. 244 : // If there's no bar...
  245. 245 : if (!this.el_ || !this.bar) {
  246. 246 : return;
  247. 247 : }
  248. 248 :
  249. 249 : // clamp progress between 0 and 1
  250. 250 : // and only round to four decimal places, as we round to two below
  251. 251 : const progress = this.getProgress();
  252. 252 :
  253. 253 : if (progress === this.progress_) {
  254. 254 : return progress;
  255. 255 : }
  256. 256 :
  257. 257 : this.progress_ = progress;
  258. 258 :
  259. 259 : this.requestNamedAnimationFrame('Slider#update', () => {
  260. 260 : // Set the new bar width or height
  261. 261 : const sizeKey = this.vertical() ? 'height' : 'width';
  262. 262 :
  263. 263 : // Convert to a percentage for css value
  264. 264 : this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
  265. 265 : });
  266. 266 :
  267. 267 : return progress;
  268. 268 : }
  269. 269 :
  270. 270 : /**
  271. 271 : * Get the percentage of the bar that should be filled
  272. 272 : * but clamped and rounded.
  273. 273 : *
  274. 274 : * @return {number}
  275. 275 : * percentage filled that the slider is
  276. 276 : */
  277. 277 : getProgress() {
  278. 278 : return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
  279. 279 : }
  280. 280 :
  281. 281 : /**
  282. 282 : * Calculate distance for slider
  283. 283 : *
  284. 284 : * @param {EventTarget~Event} event
  285. 285 : * The event that caused this function to run.
  286. 286 : *
  287. 287 : * @return {number}
  288. 288 : * The current position of the Slider.
  289. 289 : * - position.x for vertical `Slider`s
  290. 290 : * - position.y for horizontal `Slider`s
  291. 291 : */
  292. 292 : calculateDistance(event) {
  293. 293 : const position = Dom.getPointerPosition(this.el_, event);
  294. 294 :
  295. 295 : if (this.vertical()) {
  296. 296 : return position.y;
  297. 297 : }
  298. 298 : return position.x;
  299. 299 : }
  300. 300 :
  301. 301 : /**
  302. 302 : * Handle a `keydown` event on the `Slider`. Watches for left, rigth, up, and down
  303. 303 : * arrow keys. This function will only be called when the slider has focus. See
  304. 304 : * {@link Slider#handleFocus} and {@link Slider#handleBlur}.
  305. 305 : *
  306. 306 : * @param {EventTarget~Event} event
  307. 307 : * the `keydown` event that caused this function to run.
  308. 308 : *
  309. 309 : * @listens keydown
  310. 310 : */
  311. 311 : handleKeyDown(event) {
  312. 312 :
  313. 313 : // Left and Down Arrows
  314. 314 : if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
  315. 315 : event.preventDefault();
  316. 316 : event.stopPropagation();
  317. 317 : this.stepBack();
  318. 318 :
  319. 319 : // Up and Right Arrows
  320. 320 : } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
  321. 321 : event.preventDefault();
  322. 322 : event.stopPropagation();
  323. 323 : this.stepForward();
  324. 324 : } else {
  325. 325 :
  326. 326 : // Pass keydown handling up for unsupported keys
  327. 327 : super.handleKeyDown(event);
  328. 328 : }
  329. 329 : }
  330. 330 :
  331. 331 : /**
  332. 332 : * Listener for click events on slider, used to prevent clicks
  333. 333 : * from bubbling up to parent elements like button menus.
  334. 334 : *
  335. 335 : * @param {Object} event
  336. 336 : * Event that caused this object to run
  337. 337 : */
  338. 338 : handleClick(event) {
  339. 339 : event.stopPropagation();
  340. 340 : event.preventDefault();
  341. 341 : }
  342. 342 :
  343. 343 : /**
  344. 344 : * Get/set if slider is horizontal for vertical
  345. 345 : *
  346. 346 : * @param {boolean} [bool]
  347. 347 : * - true if slider is vertical,
  348. 348 : * - false is horizontal
  349. 349 : *
  350. 350 : * @return {boolean}
  351. 351 : * - true if slider is vertical, and getting
  352. 352 : * - false if the slider is horizontal, and getting
  353. 353 : */
  354. 354 : vertical(bool) {
  355. 355 : if (bool === undefined) {
  356. 356 : return this.vertical_ || false;
  357. 357 : }
  358. 358 :
  359. 359 : this.vertical_ = !!bool;
  360. 360 :
  361. 361 : if (this.vertical_) {
  362. 362 : this.addClass('vjs-slider-vertical');
  363. 363 : } else {
  364. 364 : this.addClass('vjs-slider-horizontal');
  365. 365 : }
  366. 366 : }
  367. 367 : }
  368. 368 :
  369. 369 : Component.registerComponent('Slider', Slider);
  370. 370 : export default Slider;