Source: lib/util/dom_utils.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.util.Dom');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.util.Timer');
  9. // TODO: revisit this when Closure Compiler supports partially-exported classes.
  10. /** @export */
  11. shaka.util.Dom = class {
  12. /**
  13. * Creates an element, and cast the type from Element to HTMLElement.
  14. *
  15. * @param {string} tagName
  16. * @return {!HTMLElement}
  17. */
  18. static createHTMLElement(tagName) {
  19. const element =
  20. /** @type {!HTMLElement} */ (document.createElement(tagName));
  21. return element;
  22. }
  23. /**
  24. * Create a "button" element with the correct type.
  25. *
  26. * The compiler is very picky about the use of the "disabled" property on
  27. * HTMLElement, since it is only defined on certain subclasses of that. This
  28. * method merely creates a button and casts it to the correct type.
  29. *
  30. * @return {!HTMLButtonElement}
  31. */
  32. static createButton() {
  33. const button = document.createElement('button');
  34. button.setAttribute('type', 'button');
  35. return /** @type {!HTMLButtonElement} */ (button);
  36. }
  37. /**
  38. * @param {string} url
  39. * @param {string=} mimeType
  40. * @return {!HTMLSourceElement}
  41. */
  42. static createSourceElement(url, mimeType = '') {
  43. const source =
  44. /** @type {HTMLSourceElement} */ (document.createElement('source'));
  45. source.src = url;
  46. source.type = mimeType;
  47. return source;
  48. }
  49. /**
  50. * Remove all source elements and src attribute from a video element.
  51. * Returns true if any change was made.
  52. * @param {!HTMLMediaElement} video
  53. * @export
  54. * @return {boolean}
  55. */
  56. static clearSourceFromVideo(video) {
  57. let result = false;
  58. const sources = video.getElementsByTagName('source');
  59. for (let i = sources.length - 1; i >= 0; --i) {
  60. video.removeChild(sources[i]);
  61. result = true;
  62. }
  63. if (video.src) {
  64. video.removeAttribute('src');
  65. result = true;
  66. }
  67. return result;
  68. }
  69. /**
  70. * Cast a Node/Element to an HTMLElement
  71. *
  72. * @param {!Node|!Element} original
  73. * @return {!HTMLElement}
  74. */
  75. static asHTMLElement(original) {
  76. return /** @type {!HTMLElement}*/ (original);
  77. }
  78. /**
  79. * Cast a Node/Element to an HTMLCanvasElement
  80. *
  81. * @param {!Node|!Element} original
  82. * @return {!HTMLCanvasElement}
  83. */
  84. static asHTMLCanvasElement(original) {
  85. return /** @type {!HTMLCanvasElement}*/ (original);
  86. }
  87. /**
  88. * Cast a Node/Element to an HTMLMediaElement
  89. *
  90. * @param {!Node|!Element} original
  91. * @return {!HTMLMediaElement}
  92. */
  93. static asHTMLMediaElement(original) {
  94. return /** @type {!HTMLMediaElement}*/ (original);
  95. }
  96. /**
  97. * Returns the element with a given class name.
  98. * Assumes the class name to be unique for a given parent.
  99. *
  100. * @param {string} className
  101. * @param {!HTMLElement} parent
  102. * @return {!HTMLElement}
  103. */
  104. static getElementByClassName(className, parent) {
  105. const elements = parent.getElementsByClassName(className);
  106. goog.asserts.assert(elements.length == 1,
  107. 'Should only be one element with class name ' + className);
  108. return shaka.util.Dom.asHTMLElement(elements[0]);
  109. }
  110. /**
  111. * Returns the element with a given class name if it exists.
  112. * Assumes the class name to be unique for a given parent.
  113. *
  114. * @param {string} className
  115. * @param {!HTMLElement} parent
  116. * @return {?HTMLElement}
  117. */
  118. static getElementByClassNameIfItExists(className, parent) {
  119. const elements = parent.getElementsByClassName(className);
  120. if (!elements.length) {
  121. return null;
  122. }
  123. goog.asserts.assert(elements.length == 1,
  124. 'Should only be one element with class name ' + className);
  125. return shaka.util.Dom.asHTMLElement(elements[0]);
  126. }
  127. /**
  128. * Remove all of the child nodes of an element.
  129. * @param {!Element} element
  130. * @export
  131. */
  132. static removeAllChildren(element) {
  133. while (element.firstChild) {
  134. element.removeChild(element.firstChild);
  135. }
  136. }
  137. /**
  138. * For canPlayType queries, we just need any instance.
  139. *
  140. * First, use a cached element from a previous query.
  141. * Second, search the page for one.
  142. * Third, create a temporary one.
  143. *
  144. * Cached elements expire in one second so that they can be GC'd or removed.
  145. *
  146. * @return {!HTMLMediaElement}
  147. */
  148. static anyMediaElement() {
  149. if (shaka.util.Dom.cachedMediaElement_) {
  150. return shaka.util.Dom.cachedMediaElement_;
  151. }
  152. if (!shaka.util.Dom.cacheExpirationTimer_) {
  153. shaka.util.Dom.cacheExpirationTimer_ = new shaka.util.Timer(() => {
  154. shaka.util.Dom.cachedMediaElement_ = null;
  155. });
  156. }
  157. shaka.util.Dom.cachedMediaElement_ = /** @type {HTMLMediaElement} */(
  158. document.getElementsByTagName('video')[0] ||
  159. document.getElementsByTagName('audio')[0]);
  160. if (!shaka.util.Dom.cachedMediaElement_) {
  161. shaka.util.Dom.cachedMediaElement_ = /** @type {!HTMLMediaElement} */(
  162. document.createElement('video'));
  163. }
  164. shaka.util.Dom.cacheExpirationTimer_.tickAfter(/* seconds= */ 1);
  165. return shaka.util.Dom.cachedMediaElement_;
  166. }
  167. /**
  168. * Load a new font on the page. If the font was already loaded, it does
  169. * nothing.
  170. *
  171. * @param {string} name
  172. * @param {string} url
  173. * @return {!Promise<void>}
  174. */
  175. static async addFont(name, url) {
  176. if (!('fonts' in document && 'FontFace' in window)) {
  177. return;
  178. }
  179. await document.fonts.ready;
  180. if (!('entries' in document.fonts)) {
  181. return;
  182. }
  183. const fontFaceSetIteratorToArray = (target) => {
  184. const iterable = target.entries();
  185. const results = [];
  186. let iterator = iterable.next();
  187. while (iterator.done === false) {
  188. results.push(iterator.value);
  189. iterator = iterable.next();
  190. }
  191. return results;
  192. };
  193. for (const fontFace of fontFaceSetIteratorToArray(document.fonts)) {
  194. if (fontFace.family === name && fontFace.display === 'swap') {
  195. // Font already loaded.
  196. return;
  197. }
  198. }
  199. const fontFace = new FontFace(name, `url(${url})`, {display: 'swap'});
  200. document.fonts.add(fontFace);
  201. }
  202. };
  203. /** @private {shaka.util.Timer} */
  204. shaka.util.Dom.cacheExpirationTimer_ = null;
  205. /** @private {HTMLMediaElement} */
  206. shaka.util.Dom.cachedMediaElement_ = null;