Source: ui/range_element.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.RangeElement');
  7. goog.require('shaka.ui.Element');
  8. goog.require('shaka.util.Dom');
  9. goog.require('shaka.util.Timer');
  10. goog.requireType('shaka.ui.Controls');
  11. /**
  12. * A range element, built to work across browsers.
  13. *
  14. * In particular, getting styles to work right on IE requires a specific
  15. * structure.
  16. *
  17. * This also handles the case where the range element is being manipulated and
  18. * updated at the same time. This can happen when seeking during playback or
  19. * when casting.
  20. *
  21. * @implements {shaka.extern.IUIRangeElement}
  22. * @export
  23. */
  24. shaka.ui.RangeElement = class extends shaka.ui.Element {
  25. /**
  26. * @param {!HTMLElement} parent
  27. * @param {!shaka.ui.Controls} controls
  28. * @param {!Array<string>} containerClassNames
  29. * @param {!Array<string>} barClassNames
  30. */
  31. constructor(parent, controls, containerClassNames, barClassNames) {
  32. super(parent, controls);
  33. /**
  34. * This container is to support IE 11. See detailed notes in
  35. * less/range_elements.less for a complete explanation.
  36. * @protected {!HTMLElement}
  37. */
  38. this.container = shaka.util.Dom.createHTMLElement('div');
  39. this.container.classList.add('shaka-range-container');
  40. this.container.classList.add(...containerClassNames);
  41. /** @private {boolean} */
  42. this.isChanging_ = false;
  43. /** @protected {!HTMLInputElement} */
  44. this.bar =
  45. /** @type {!HTMLInputElement} */ (document.createElement('input'));
  46. /** @private {shaka.util.Timer} */
  47. this.endFakeChangeTimer_ = new shaka.util.Timer(() => {
  48. this.onChangeEnd();
  49. this.isChanging_ = false;
  50. });
  51. this.bar.classList.add('shaka-range-element');
  52. this.bar.classList.add(...barClassNames);
  53. this.bar.type = 'range';
  54. this.bar.step = 'any';
  55. this.bar.min = '0';
  56. this.bar.max = '1';
  57. this.bar.value = '0';
  58. this.bar.disabled = !this.controls.isOpaque();
  59. this.container.appendChild(this.bar);
  60. this.parent.appendChild(this.container);
  61. this.showingUITimer_ = new shaka.util.Timer(() => {
  62. this.bar.disabled = false;
  63. });
  64. this.eventManager.listen(this.controls, 'showingui', (e) => {
  65. this.showingUITimer_.tickAfter(/* seconds= */ 0);
  66. });
  67. this.eventManager.listen(this.controls, 'hidingui', (e) => {
  68. this.showingUITimer_.stop();
  69. this.bar.disabled = true;
  70. });
  71. this.eventManager.listen(this.bar, 'mousedown', (e) => {
  72. if (!this.bar.disabled) {
  73. this.isChanging_ = true;
  74. this.onChangeStart();
  75. e.stopPropagation();
  76. }
  77. });
  78. this.eventManager.listen(this.bar, 'touchstart', (e) => {
  79. if (!this.bar.disabled) {
  80. this.isChanging_ = true;
  81. this.setBarValueForTouch_(e);
  82. this.onChangeStart();
  83. e.stopPropagation();
  84. }
  85. });
  86. this.eventManager.listen(this.bar, 'input', () => {
  87. this.onChange();
  88. });
  89. this.eventManager.listen(this.bar, 'touchmove', (e) => {
  90. if (this.isChanging_) {
  91. this.setBarValueForTouch_(e);
  92. this.onChange();
  93. e.stopPropagation();
  94. }
  95. });
  96. this.eventManager.listen(this.bar, 'touchend', (e) => {
  97. if (this.isChanging_) {
  98. this.isChanging_ = false;
  99. this.setBarValueForTouch_(e);
  100. this.onChangeEnd();
  101. e.stopPropagation();
  102. }
  103. });
  104. this.eventManager.listen(this.bar, 'touchcancel', (e) => {
  105. if (this.isChanging_) {
  106. this.isChanging_ = false;
  107. this.setBarValueForTouch_(e);
  108. this.onChangeEnd();
  109. e.stopPropagation();
  110. }
  111. });
  112. this.eventManager.listen(this.bar, 'mouseup', (e) => {
  113. if (this.isChanging_) {
  114. this.isChanging_ = false;
  115. this.onChangeEnd();
  116. e.stopPropagation();
  117. }
  118. });
  119. this.eventManager.listen(this.bar, 'blur', () => {
  120. if (this.isChanging_) {
  121. this.isChanging_ = false;
  122. this.onChangeEnd();
  123. }
  124. });
  125. this.eventManager.listen(this.bar, 'contextmenu', (e) => {
  126. e.preventDefault();
  127. e.stopPropagation();
  128. });
  129. }
  130. /** @override */
  131. release() {
  132. if (this.endFakeChangeTimer_) {
  133. this.endFakeChangeTimer_.stop();
  134. this.endFakeChangeTimer_ = null;
  135. }
  136. super.release();
  137. }
  138. /**
  139. * @override
  140. * @export
  141. */
  142. setRange(min, max) {
  143. this.bar.min = min;
  144. this.bar.max = max;
  145. }
  146. /**
  147. * Called when user interaction begins.
  148. * To be overridden by subclasses.
  149. * @override
  150. * @export
  151. */
  152. onChangeStart() {}
  153. /**
  154. * Called when a new value is set by user interaction.
  155. * To be overridden by subclasses.
  156. * @override
  157. * @export
  158. */
  159. onChange() {}
  160. /**
  161. * Called when user interaction ends.
  162. * To be overridden by subclasses.
  163. * @override
  164. * @export
  165. */
  166. onChangeEnd() {}
  167. /**
  168. * Called to implement keyboard-based changes, where this is no clear "end".
  169. * This will simulate events like onChangeStart(), onChange(), and
  170. * onChangeEnd() as appropriate.
  171. *
  172. * @override
  173. * @export
  174. */
  175. changeTo(value) {
  176. if (!this.isChanging_) {
  177. this.isChanging_ = true;
  178. this.onChangeStart();
  179. }
  180. const min = parseFloat(this.bar.min);
  181. const max = parseFloat(this.bar.max);
  182. if (value > max) {
  183. this.bar.value = max;
  184. } else if (value < min) {
  185. this.bar.value = min;
  186. } else {
  187. this.bar.value = value;
  188. }
  189. this.onChange();
  190. this.endFakeChangeTimer_.tickAfter(/* seconds= */ 0.5);
  191. }
  192. /**
  193. * @override
  194. * @export
  195. */
  196. getValue() {
  197. return parseFloat(this.bar.value);
  198. }
  199. /**
  200. * @override
  201. * @export
  202. */
  203. setValue(value) {
  204. // The user interaction overrides any external values being pushed in.
  205. if (this.isChanging_) {
  206. return;
  207. }
  208. this.bar.value = value;
  209. }
  210. /**
  211. * Synchronize the touch position with the range value.
  212. * Comes in handy on iOS, where users have to grab the handle in order
  213. * to start seeking.
  214. * @param {Event} event
  215. * @private
  216. */
  217. setBarValueForTouch_(event) {
  218. event.preventDefault();
  219. const changedTouch = /** @type {TouchEvent} */ (event).changedTouches[0];
  220. const rect = this.bar.getBoundingClientRect();
  221. const min = parseFloat(this.bar.min);
  222. const max = parseFloat(this.bar.max);
  223. // Calculate the range value based on the touch position.
  224. // Pixels from the left of the range element
  225. const touchPosition = changedTouch.clientX - rect.left;
  226. // Pixels per unit value of the range element.
  227. const scale = (max - min) / rect.width;
  228. // Touch position in units, which may be outside the allowed range.
  229. let value = min + scale * touchPosition;
  230. // Keep value within bounds.
  231. if (value < min) {
  232. value = min;
  233. } else if (value > max) {
  234. value = max;
  235. }
  236. this.bar.value = value;
  237. }
  238. };