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.container.appendChild(this.bar);
  59. this.parent.appendChild(this.container);
  60. this.eventManager.listen(this.bar, 'mousedown', (e) => {
  61. if (this.controls.isOpaque()) {
  62. this.isChanging_ = true;
  63. this.onChangeStart();
  64. }
  65. e.stopPropagation();
  66. });
  67. this.eventManager.listen(this.bar, 'touchstart', (e) => {
  68. if (this.controls.isOpaque()) {
  69. this.isChanging_ = true;
  70. this.setBarValueForTouch_(e);
  71. this.onChangeStart();
  72. }
  73. e.stopPropagation();
  74. });
  75. this.eventManager.listen(this.bar, 'input', () => {
  76. this.onChange();
  77. });
  78. this.eventManager.listen(this.bar, 'touchmove', (e) => {
  79. if (this.isChanging_) {
  80. this.setBarValueForTouch_(e);
  81. this.onChange();
  82. }
  83. e.stopPropagation();
  84. });
  85. this.eventManager.listen(this.bar, 'touchend', (e) => {
  86. if (this.isChanging_) {
  87. this.isChanging_ = false;
  88. this.setBarValueForTouch_(e);
  89. this.onChangeEnd();
  90. }
  91. e.stopPropagation();
  92. });
  93. this.eventManager.listen(this.bar, 'touchcancel', (e) => {
  94. if (this.isChanging_) {
  95. this.isChanging_ = false;
  96. this.setBarValueForTouch_(e);
  97. this.onChangeEnd();
  98. }
  99. e.stopPropagation();
  100. });
  101. this.eventManager.listen(this.bar, 'mouseup', (e) => {
  102. if (this.isChanging_) {
  103. this.isChanging_ = false;
  104. this.onChangeEnd();
  105. }
  106. e.stopPropagation();
  107. });
  108. this.eventManager.listen(this.bar, 'blur', () => {
  109. if (this.isChanging_) {
  110. this.isChanging_ = false;
  111. this.onChangeEnd();
  112. }
  113. });
  114. this.eventManager.listen(this.bar, 'contextmenu', (e) => {
  115. e.preventDefault();
  116. e.stopPropagation();
  117. });
  118. }
  119. /** @override */
  120. release() {
  121. if (this.endFakeChangeTimer_) {
  122. this.endFakeChangeTimer_.stop();
  123. this.endFakeChangeTimer_ = null;
  124. }
  125. super.release();
  126. }
  127. /**
  128. * @override
  129. * @export
  130. */
  131. setRange(min, max) {
  132. this.bar.min = min;
  133. this.bar.max = max;
  134. }
  135. /**
  136. * Called when user interaction begins.
  137. * To be overridden by subclasses.
  138. * @override
  139. * @export
  140. */
  141. onChangeStart() {}
  142. /**
  143. * Called when a new value is set by user interaction.
  144. * To be overridden by subclasses.
  145. * @override
  146. * @export
  147. */
  148. onChange() {}
  149. /**
  150. * Called when user interaction ends.
  151. * To be overridden by subclasses.
  152. * @override
  153. * @export
  154. */
  155. onChangeEnd() {}
  156. /**
  157. * Called to implement keyboard-based changes, where this is no clear "end".
  158. * This will simulate events like onChangeStart(), onChange(), and
  159. * onChangeEnd() as appropriate.
  160. *
  161. * @override
  162. * @export
  163. */
  164. changeTo(value) {
  165. if (!this.isChanging_) {
  166. this.isChanging_ = true;
  167. this.onChangeStart();
  168. }
  169. const min = parseFloat(this.bar.min);
  170. const max = parseFloat(this.bar.max);
  171. if (value > max) {
  172. this.bar.value = max;
  173. } else if (value < min) {
  174. this.bar.value = min;
  175. } else {
  176. this.bar.value = value;
  177. }
  178. this.onChange();
  179. this.endFakeChangeTimer_.tickAfter(/* seconds= */ 0.5);
  180. }
  181. /**
  182. * @override
  183. * @export
  184. */
  185. getValue() {
  186. return parseFloat(this.bar.value);
  187. }
  188. /**
  189. * @override
  190. * @export
  191. */
  192. setValue(value) {
  193. // The user interaction overrides any external values being pushed in.
  194. if (this.isChanging_) {
  195. return;
  196. }
  197. this.bar.value = value;
  198. }
  199. /**
  200. * Synchronize the touch position with the range value.
  201. * Comes in handy on iOS, where users have to grab the handle in order
  202. * to start seeking.
  203. * @param {Event} event
  204. * @private
  205. */
  206. setBarValueForTouch_(event) {
  207. event.preventDefault();
  208. const changedTouch = /** @type {TouchEvent} */ (event).changedTouches[0];
  209. const rect = this.bar.getBoundingClientRect();
  210. const min = parseFloat(this.bar.min);
  211. const max = parseFloat(this.bar.max);
  212. // Calculate the range value based on the touch position.
  213. // Pixels from the left of the range element
  214. const touchPosition = changedTouch.clientX - rect.left;
  215. // Pixels per unit value of the range element.
  216. const scale = (max - min) / rect.width;
  217. // Touch position in units, which may be outside the allowed range.
  218. let value = min + scale * touchPosition;
  219. // Keep value within bounds.
  220. if (value < min) {
  221. value = min;
  222. } else if (value > max) {
  223. value = max;
  224. }
  225. this.bar.value = value;
  226. }
  227. };