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