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