/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
extensions/amp-loader/0.1/amp-loader.js
431 строка
14 KB
Kristofer Baxter
♻️ Remove Notice from JS and Markdown (#35717)
19 авг 2021, 21:21
Не верифицирован
19 авг 2021, 21:21
f564b7e
Код
Авторство
О чём код?
import {isIframeVideoPlayerComponent} from '#core/dom/layout'; import {htmlFor} from '#core/dom/static-template'; import {setImportantStyles, setStyle} from '#core/dom/style'; import {Services} from '#service'; import {CSS} from '../../../build/amp-loader-0.1.css'; import {installStylesForDoc} from '../../../src/style-installer'; /** * @fileoverview This file implements the new AMP loader as an extension. This * allows loading the runtime without incurring the cost of the loader code. * The loader has a 600ms delay before appearing. This delay is offset by * the amount of time it took to load the extension. */ // How long before the loader appears, in milliseconds. This matches the // minimum animation delay specified in the CSS. const LOADER_APPEAR_TIME = 600; /** * Elements will get a default gray background if they don't already have a * placeholder. This list does not include video players which are detected * using `isIframeVideoPlayerComponent` * @enum {boolean} * @private Visible for testing only! */ const LOADER_BACKGROUND_TAGS = { 'AMP-IMG': true, 'AMP-ANIM': true, 'AMP-PINTEREST': true, 'AMP-INSTAGRAM': true, 'AMP-GOOGLE-DOCUMENT-EMBED': true, }; /** * Used to cache the loader DOM once created, so we do not need to recreate it * each time. * @type {?Element} */ let loaderDom = null; /** * @param {function(!Array<string>):!Element} html * @return {!Element} */ function createSpinnerDom(html) { // Extra wrapping div here is to workaround: // https://bugs.chromium.org/p/chromium/issues/detail?id=1002748 // eslint-disable max-len const content = html` <div class="i-amphtml-new-loader-spinner-wrapper"> <svg class="i-amphtml-new-loader-spinner" viewBox="0 0 48 48"> <path class="i-amphtml-new-loader-spinner-path" fill="none" d="M24 2a22 22 0 10.01 0m33.27 5.65a22 22 0 102.74-2.1m46.13 1.35a22 22 0 105.96-3.44m42.96 2.74a22 22 0 109.49-3.93m39.46 3.28a22 22 0 1013.13-3.52M253 4.95a22 22 0 1016.69-2.2m32.32 1.65a22 22 0 1019.98 0m29.06-.5a22 22 0 1022.79 3m26.28-3.44a22 22 0 1024.98 6.69m24.1-7.07a22 22 0 1026.4 10.94m22.71-11.27a22 22 0 1026.94 15.56m22.18-15.83a22 22 0 1026.54 20.37m22.59-20.58a22 22 0 1025.17 25.17M645.7 2.12a22 22 0 1022.84 29.76m26.31-29.85a22 22 0 1019.6 33.95M744 2a22 22 0 1015.56 37.56m33.59-37.53a22 22 0 1010.83 40.42M842.3 2.12a22 22 0 105.58 42.42m43.56-42.27a22 22 0 100 43.46m49.13-43.25a22 22 0 10-5.73 43.49m54.85-43.22a22 22 0 10-11.39 42.5m60.5-42.17a22 22 0 00-16.79 40.53m65.87-40.15a22 22 0 00-21.73 37.64m70.8-37.2a22 22 0 00-26.05 33.94m75.09-33.44a22 22 0 00-29.59 29.59M1235 4.95a22 22 0 00-32.25 24.75m81.23-24.15a22 22 0 00-33.95 19.6m82.9-18.95a22 22 0 00-34.66 14.36m83.58-13.66a22 22 0 00-34.38 9.21m83.25-8.46a22 22 0 00-33.17 4.37m82.01-3.58a22 22 0 00-31.11 0m81.35 2.63a22 22 0 00-32.52-3.42m82.32 6.36a22 22 0 00-33.45-7.11m82.77 10.3a22 22 0 00-33.85-11m82.66 14.36a22 22 0 00-33.71-15.01M1726 24a22 22 0 00-33-19.05m80.73 22.49a22 22 0 00-31.72-23.04m78.91 26.4a22 22 0 00-29.87-26.9m76.55 30.09a22 22 0 00-27.49-30.53m73.69 33.47a22 22 0 00-24.6-33.85m70.36 36.48a22 22 0 00-21.25-36.81m66.62 39.05a22 22 0 00-17.51-39.32m62.57 41.12a22 22 0 00-13.43-41.33m58.24 42.65a22 22 0 00-9.1-42.8m53.74 43.61a22 22 0 00-4.59-43.7M2184 46a22 22 0 100-44m44.56 43.73a22 22 0 104.59-43.7m40.05 42.89a22 22 0 109.1-42.8m35.71 41.48a22 22 0 1013.43-41.33m31.63 39.53a22 22 0 1017.51-39.32m27.86 37.08a22 22 0 1021.25-36.81m24.51 34.18a22 22 0 1024.6-33.85m21.6 30.91a22 22 0 1027.49-30.53m19.19 27.34a22 22 0 1029.87-26.9m17.32 23.54a22 22 0 1031.72-23.04M2642 24a22 22 0 1033-19.05m15.27 15.61a22 22 0 1033.71-15.01m15.1 11.65a22 22 0 1033.85-11m15.47 7.81a22 22 0 1033.45-7.11m16.35 4.17a22 22 0 1032.52-3.42m17.72.79a22 22 0 1031.11 0m17.73-.79a22 22 0 1032.52 3.42m16.35-4.17a22 22 0 1033.45 7.11m15.47-7.81a22 22 0 1033.85 11m15.1-11.65a22 22 0 1033.71 15.01M3133 4.95A22 22 0 103166 24m16.01-19.6a22 22 0 1031.72 23.04m17.32-23.54a22 22 0 1029.87 26.9m19.2-27.34a22 22 0 1027.49 30.53m21.59-30.91a22 22 0 1024.6 33.85m24.51-34.18a22 22 0 1021.25 36.81m27.87-37.08a22 22 0 1017.51 39.32m31.62-39.53a22 22 0 1013.43 41.33m35.71-41.48a22 22 0 109.1 42.8m40.05-42.89a22 22 0 104.59 43.7M3624 2a22 22 0 100 44m49.15-43.97a22 22 0 00-4.59 43.7m53.74-43.61a22 22 0 00-9.1 42.8m58.24-42.65a22 22 0 00-13.43 41.33m62.56-41.12a22 22 0 00-17.51 39.32m66.63-39.05a22 22 0 00-21.25 36.81m70.36-36.48a22 22 0 00-24.6 33.85m73.68-33.47a22 22 0 00-27.49 30.53m76.56-30.09a22 22 0 00-29.87 26.9m78.91-26.4a22 22 0 00-31.72 23.04M4115 4.95A22 22 0 004082 24m81.98-18.45a22 22 0 00-33.71 15.01m82.66-14.36a22 22 0 00-33.85 11m82.77-10.3a22 22 0 00-33.45 7.11m82.32-6.36a22 22 0 00-32.52 3.42" ></path> </svg> </div> `; // eslint-enable max-len return content; } /** * @param {!AmpElement} element Used to get a document to build HTML with. * @return {!Element} The loader DOM. */ function getLoaderDom(element) { if (!loaderDom) { const html = htmlFor(element); /* * The outer div here is needed for two reasons: * 1. Applying a background color when there is no placeholder. * 2. Backwards compatibility with the existing method and documentation * for customizing loaders, which includes a style to hide the old three * dots via: * ``` * .my-custom-loader .amp-active > div { * display: none; * } * ``` */ loaderDom = html` <div class="i-amphtml-new-loader"> <div class="i-amphtml-new-loader-shim"></div> <div class="i-amphtml-new-loader-logo"></div> </div> `; loaderDom.appendChild(createSpinnerDom(html)); } return loaderDom.cloneNode(true); } /** * Helper class to build the new loader's DOM. */ class LoaderBuilder { /** * @param {!AmpElement} element * @param {!Element} domRoot * @param {number} elementWidth * @param {number} elementHeight */ constructor(element, domRoot, elementWidth, elementHeight) { /** @private @const {!AmpElement} */ this.element_ = element; /** @private @const {?Element} */ this.domRoot_ = domRoot; /** @private @const {number} */ this.layoutWidth_ = elementWidth; /** @private @const {number} */ this.layoutHeight_ = elementHeight; /** @private {?Element} */ this.loaderRoot_ = null; } /** * Builds the loader's DOM. */ build() { this.loaderRoot_ = getLoaderDom(this.element_); this.domRoot_.appendChild(this.loaderRoot_); this.maybeAddLoadingBackground_(); this.maybeAddLoaderAnimation_(); } /** * Adds a combination of spinner/logo if element is eligible based on * certain heuristics. */ maybeAddLoaderAnimation_() { // If very small or already has image placeholder, no loader animation. if (this.isTiny_() || this.hasBlurryImagePlaceholder_()) { return; } this.setSize_(); if (this.requiresBackgroundShim_()) { this.loaderRoot_.classList.add('i-amphtml-new-loader-has-shim'); } this.addLogo_(); } /** * Sets the size of the loader based element's size and a few special cases. * @private * @return {undefined} */ setSize_() { const sizeClassDefault = 'i-amphtml-new-loader-size-default'; const sizeClassSmall = 'i-amphtml-new-loader-size-small'; const sizeClassLarge = 'i-amphtml-new-loader-size-large'; // Ads always get the default spinner regardless of the element size if (this.isAd_()) { return this.loaderRoot_.classList.add(sizeClassDefault); } // Other than Ads, small spinner is always used if element is small. if (this.isSmall_()) { return this.loaderRoot_.classList.add(sizeClassSmall); } // If host is not small, default size spinner is normally used // unless due to branding guidelines (e.g. Instagram) a larger spinner is // required. if (this.requiresLargeSpinner_()) { return this.loaderRoot_.classList.add(sizeClassLarge); } return this.loaderRoot_.classList.add(sizeClassDefault); } /** * Adds the spinner. * @private */ addLogo_() { const {color, content = this.getDefaultLogo_()} = this.getCustomLogo_(); this.loaderRoot_ .querySelector('.i-amphtml-new-loader-logo') .appendChild(content); if (color) { setStyle(this.loaderRoot_, 'color', color); } } /** * @return {boolean} True if the currently loading element has background * content via a placeholder or poster. * @private */ hasBackgroundContent_() { const hasPlaceholder = !!this.element_.getPlaceholder(); const hasPoster = this.element_.hasAttribute('poster'); return hasPlaceholder || hasPoster; } /** * @return {boolean} True if the loaderBackground should be used for the * element. * @private */ tagNeedsBackground_() { const {tagName} = this.element_; return ( LOADER_BACKGROUND_TAGS[tagName] || isIframeVideoPlayerComponent(tagName) ); } /** * Add a gray loading background if needed based on the element's content * and tagName. * @private */ maybeAddLoadingBackground_() { if (!this.hasBackgroundContent_() && this.tagNeedsBackground_()) { this.domRoot_.classList.add('i-amphtml-loader-background'); } } /** * Returns the custom logo for the element if there is one. * @private * @return {{ * content: (!Element|undefined), * color: (string|undefined), * }} */ getCustomLogo_() { if (this.isAd_()) { return { content: this.getAdsLogo_(), }; } if (isIframeVideoPlayerComponent(this.element_.tagName)) { return { content: this.getVideoPlayerLogo_(), }; } return this.element_.createLoaderLogo(); } /** * @return {!Element} The logo for video players * @private */ getVideoPlayerLogo_() { // Keeping the video logo here short term. // This is because there is no single CSS for all players, there is // video-interface but not all players implement it. Also the SVG is not // that big. // TODO(sparhami) Figure out how to move this out of amp-loader. const html = htmlFor(this.element_); return html` <svg viewBox="0 0 72 72"> <path class="i-amphtml-new-loader-white-on-shim" fill="currentColor" d="M41,34.5V31c0-0.5-0.4-1-1-1H27c-0.5,0-1,0.5-1,1v10c0,0.6,0.5,1,1,1h13c0.6,0,1-0.4,1-1v-3.5l5,4v-11L41,34.5z" ></path> </svg> `; } /** * Returns the default logo. * @return {!Element} * @private */ getDefaultLogo_() { const html = htmlFor(this.element_); return html` <svg class="i-amphtml-new-loader-logo-default" viewBox="0 0 72 72"> <circle cx="36" cy="36" r="12"></circle> </svg> `; } /** * `<amp-ad>s` have several different classes, so putting the code here for * now since it is the safest way to make sure that they all get the correct * loader. * * Since the implementation may have a delay before loading, we would need to * make sure the ads loader is present, even if the implementation has not * yet downloaded. * * TODO(sparhami) Move this out of amp-loader into something common for ads. * @return {!Element} * @private */ getAdsLogo_() { const html = htmlFor(this.element_); return html` <div class="i-amphtml-new-loader-ad-logo"> <span class="i-amphtml-new-loader-ad-label"> Ad </span> </div> `; } /** * Whether the element is an Ad. Note that this does not cover amp-embed * currently. * @private * @return {boolean} */ isAd_() { return this.element_.tagName == 'AMP-AD'; } /** * Whether the element is small. * Small elements get a different loader with does not have a logo and is * just a spinner. * @private * @return {boolean} */ isSmall_() { return ( !this.isTiny_() && (this.layoutWidth_ <= 100 || this.layoutHeight_ <= 100) ); } /** * Very small layout are not eligible for new loaders. * @return {boolean} */ isTiny_() { return this.layoutWidth_ < 50 || this.layoutHeight_ < 50; } /** * Whether element has an image blurry placeholder * @return {boolean} */ hasBlurryImagePlaceholder_() { const placeholder = this.element_.getPlaceholder(); return ( placeholder && placeholder.classList.contains('i-amphtml-blurry-placeholder') ); } /** * Whether loaders needs the translucent background shim, this is normally * needed when the loader is on top of an image placeholder: * - placeholder is `amp-img` or `img` (`img` handles component * placeholders like `amp-youtube`) * - Element has implicit placeholder like a `poster` on video * @private * @return {boolean} */ requiresBackgroundShim_() { if (this.element_.hasAttribute('poster')) { return true; } const placeholder = this.element_.getPlaceholder(); if (!placeholder) { return false; } if (placeholder.tagName == 'AMP-IMG' || placeholder.tagName == 'IMG') { return true; } return false; } /** * Some components such as Instagram require larger spinner due to * branding guidelines. * @private * @return {boolean} */ requiresLargeSpinner_() { // Not Implemented return false; } } export class LoaderService { /** * @param {!AmpElement} element * @param {!Element} loaderRoot * @param {number} initDelay * @param {number} elementWidth * @param {number} elementHeight */ initializeLoader( element, loaderRoot, initDelay, elementWidth, elementHeight ) { // Cap the loader delay so that the loader appears immediately, rather than // starting part way through the animation. const loaderDelay = Math.min(initDelay, LOADER_APPEAR_TIME); const lb = new LoaderBuilder( element, loaderRoot, elementWidth, elementHeight ); lb.build(); setImportantStyles(element, { '--loader-delay-offset': `${loaderDelay}ms`, }); } } AMP.extension('amp-loader', '0.1', (AMP) => { AMP.registerServiceForDoc('loader', LoaderService); Services.extensionsFor(AMP.win).addDocFactory((ampDoc) => { installStylesForDoc(ampDoc, CSS, () => {}, false, 'amp-loader'); }); });