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, true);
  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 : * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false.
  196. 196 : *
  197. 197 : * @listens mousemove
  198. 198 : * @listens touchmove
  199. 199 : */
  200. 200 : handleMouseMove(event) {}
  201. 201 :
  202. 202 : /**
  203. 203 : * Handle `mouseup` or `touchend` events on the `Slider`.
  204. 204 : *
  205. 205 : * @param {EventTarget~Event} event
  206. 206 : * `mouseup` or `touchend` event that triggered this function.
  207. 207 : *
  208. 208 : * @listens touchend
  209. 209 : * @listens mouseup
  210. 210 : * @fires Slider#sliderinactive
  211. 211 : */
  212. 212 : handleMouseUp() {
  213. 213 : const doc = this.bar.el_.ownerDocument;
  214. 214 :
  215. 215 : Dom.unblockTextSelection();
  216. 216 :
  217. 217 : this.removeClass('vjs-sliding');
  218. 218 : /**
  219. 219 : * Triggered when the slider is no longer in an active state.
  220. 220 : *
  221. 221 : * @event Slider#sliderinactive
  222. 222 : * @type {EventTarget~Event}
  223. 223 : */
  224. 224 : this.trigger('sliderinactive');
  225. 225 :
  226. 226 : this.off(doc, 'mousemove', this.handleMouseMove_);
  227. 227 : this.off(doc, 'mouseup', this.handleMouseUp_);
  228. 228 : this.off(doc, 'touchmove', this.handleMouseMove_);
  229. 229 : this.off(doc, 'touchend', this.handleMouseUp_);
  230. 230 :
  231. 231 : this.update();
  232. 232 : }
  233. 233 :
  234. 234 : /**
  235. 235 : * Update the progress bar of the `Slider`.
  236. 236 : *
  237. 237 : * @return {number}
  238. 238 : * The percentage of progress the progress bar represents as a
  239. 239 : * number from 0 to 1.
  240. 240 : */
  241. 241 : update() {
  242. 242 : // In VolumeBar init we have a setTimeout for update that pops and update
  243. 243 : // to the end of the execution stack. The player is destroyed before then
  244. 244 : // update will cause an error
  245. 245 : // If there's no bar...
  246. 246 : if (!this.el_ || !this.bar) {
  247. 247 : return;
  248. 248 : }
  249. 249 :
  250. 250 : // clamp progress between 0 and 1
  251. 251 : // and only round to four decimal places, as we round to two below
  252. 252 : const progress = this.getProgress();
  253. 253 :
  254. 254 : if (progress === this.progress_) {
  255. 255 : return progress;
  256. 256 : }
  257. 257 :
  258. 258 : this.progress_ = progress;
  259. 259 :
  260. 260 : this.requestNamedAnimationFrame('Slider#update', () => {
  261. 261 : // Set the new bar width or height
  262. 262 : const sizeKey = this.vertical() ? 'height' : 'width';
  263. 263 :
  264. 264 : // Convert to a percentage for css value
  265. 265 : this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
  266. 266 : });
  267. 267 :
  268. 268 : return progress;
  269. 269 : }
  270. 270 :
  271. 271 : /**
  272. 272 : * Get the percentage of the bar that should be filled
  273. 273 : * but clamped and rounded.
  274. 274 : *
  275. 275 : * @return {number}
  276. 276 : * percentage filled that the slider is
  277. 277 : */
  278. 278 : getProgress() {
  279. 279 : return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
  280. 280 : }
  281. 281 :
  282. 282 : /**
  283. 283 : * Calculate distance for slider
  284. 284 : *
  285. 285 : * @param {EventTarget~Event} event
  286. 286 : * The event that caused this function to run.
  287. 287 : *
  288. 288 : * @return {number}
  289. 289 : * The current position of the Slider.
  290. 290 : * - position.x for vertical `Slider`s
  291. 291 : * - position.y for horizontal `Slider`s
  292. 292 : */
  293. 293 : calculateDistance(event) {
  294. 294 : const position = Dom.getPointerPosition(this.el_, event);
  295. 295 :
  296. 296 : if (this.vertical()) {
  297. 297 : return position.y;
  298. 298 : }
  299. 299 : return position.x;
  300. 300 : }
  301. 301 :
  302. 302 : /**
  303. 303 : * Handle a `keydown` event on the `Slider`. Watches for left, rigth, up, and down
  304. 304 : * arrow keys. This function will only be called when the slider has focus. See
  305. 305 : * {@link Slider#handleFocus} and {@link Slider#handleBlur}.
  306. 306 : *
  307. 307 : * @param {EventTarget~Event} event
  308. 308 : * the `keydown` event that caused this function to run.
  309. 309 : *
  310. 310 : * @listens keydown
  311. 311 : */
  312. 312 : handleKeyDown(event) {
  313. 313 :
  314. 314 : // Left and Down Arrows
  315. 315 : if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) {
  316. 316 : event.preventDefault();
  317. 317 : event.stopPropagation();
  318. 318 : this.stepBack();
  319. 319 :
  320. 320 : // Up and Right Arrows
  321. 321 : } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) {
  322. 322 : event.preventDefault();
  323. 323 : event.stopPropagation();
  324. 324 : this.stepForward();
  325. 325 : } else {
  326. 326 :
  327. 327 : // Pass keydown handling up for unsupported keys
  328. 328 : super.handleKeyDown(event);
  329. 329 : }
  330. 330 : }
  331. 331 :
  332. 332 : /**
  333. 333 : * Listener for click events on slider, used to prevent clicks
  334. 334 : * from bubbling up to parent elements like button menus.
  335. 335 : *
  336. 336 : * @param {Object} event
  337. 337 : * Event that caused this object to run
  338. 338 : */
  339. 339 : handleClick(event) {
  340. 340 : event.stopPropagation();
  341. 341 : event.preventDefault();
  342. 342 : }
  343. 343 :
  344. 344 : /**
  345. 345 : * Get/set if slider is horizontal for vertical
  346. 346 : *
  347. 347 : * @param {boolean} [bool]
  348. 348 : * - true if slider is vertical,
  349. 349 : * - false is horizontal
  350. 350 : *
  351. 351 : * @return {boolean}
  352. 352 : * - true if slider is vertical, and getting
  353. 353 : * - false if the slider is horizontal, and getting
  354. 354 : */
  355. 355 : vertical(bool) {
  356. 356 : if (bool === undefined) {
  357. 357 : return this.vertical_ || false;
  358. 358 : }
  359. 359 :
  360. 360 : this.vertical_ = !!bool;
  361. 361 :
  362. 362 : if (this.vertical_) {
  363. 363 : this.addClass('vjs-slider-vertical');
  364. 364 : } else {
  365. 365 : this.addClass('vjs-slider-horizontal');
  366. 366 : }
  367. 367 : }
  368. 368 : }
  369. 369 :
  370. 370 : Component.registerComponent('Slider', Slider);
  371. 371 : export default Slider;