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