diff --git a/extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.js b/extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.js index 53c72f85fb1d..fdeb7155dbaf 100644 --- a/extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.js +++ b/extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.js @@ -27,7 +27,10 @@ import {Layout} from '../../../src/layout'; import {user, dev} from '../../../src/log'; import {toggle, setStyle} from '../../../src/style'; import {getData, listen} from '../../../src/event-helper'; -import {LightboxManager} from './service/lightbox-manager-impl'; +import { + LightboxManager, + LightboxedCarouselMetadataDef, +} from './service/lightbox-manager-impl'; import {layoutRectFromDomRect} from '../../../src/layout-rect'; import {closest, elementByTag, scopedQuerySelector} from '../../../src/dom'; import * as st from '../../../src/style'; @@ -74,7 +77,8 @@ let manager_; * @typedef {{ * descriptionText: string, * tagName: string, - * imageViewer: ?Element + * imageViewer: ?Element, + * sourceElement: !Element * }} */ let LightboxElementMetadataDef_; @@ -234,6 +238,7 @@ export class AmpLightboxViewer extends AMP.BaseElement { const metadata = { descriptionText: descText, tagName: clonedNode.tagName, + sourceElement: element, }; let slide = clonedNode; if (clonedNode.tagName === 'AMP-IMG') { @@ -676,17 +681,25 @@ export class AmpLightboxViewer extends AMP.BaseElement { } /** - * @param {!Element} ampImage + * This function verifies that the source element is an amp-img and contains + * an img element and preserves the natural aspect ratio of the original img. + * @param {!Element} element * @return {boolean} * @private */ - aspectRatioChanged_(ampImage) { - const img = elementByTag(dev().assertElement(ampImage), 'img'); + shouldAnimate_(element) { + if (element.tagName !== 'AMP-IMG') { + return false; + } + const img = elementByTag(dev().assertElement(element), 'img'); + if (!img) { + return false; + } const naturalAspectRatio = img.naturalWidth / img.naturalHeight; - const elementHeight = ampImage./*OK*/offsetHeight; - const elementWidth = ampImage./*OK*/offsetWidth; + const elementHeight = element./*OK*/offsetHeight; + const elementWidth = element./*OK*/offsetWidth; const ampImageAspectRatio = elementWidth / elementHeight; - return Math.abs(naturalAspectRatio - ampImageAspectRatio) > EPSILON; + return Math.abs(naturalAspectRatio - ampImageAspectRatio) < EPSILON; } /** @@ -699,6 +712,7 @@ export class AmpLightboxViewer extends AMP.BaseElement { const anim = new Animation(this.element); let duration = MIN_TRANSITION_DURATION; let transLayer = null; + const sourceElement = this.getCurrentElement_().sourceElement; return this.vsync_.measurePromise(() => { // Lightbox background fades in. anim.add(0, tr.setStyles(this.element, { @@ -706,21 +720,20 @@ export class AmpLightboxViewer extends AMP.BaseElement { }), MOTION_DURATION_RATIO, ENTER_CURVE_); // Try to transition from the source image. - if (this.sourceElement_ && isLoaded(this.sourceElement_) - && !this.aspectRatioChanged_(this.sourceElement_)) { + if (sourceElement && isLoaded(sourceElement) + && this.shouldAnimate_(sourceElement)) { // TODO (#13039): implement crop and object fit contain transitions transLayer = this.element.ownerDocument.createElement('div'); transLayer.classList.add('i-amphtml-lightbox-viewer-trans'); this.element.ownerDocument.body.appendChild(transLayer); - const rect = layoutRectFromDomRect(this.sourceElement_ + const rect = layoutRectFromDomRect(sourceElement ./*OK*/getBoundingClientRect()); const imageBox = /**@type {?}*/ (this.getCurrentElement_().imageViewer) .implementation_.getImageBoxWithOffset(); - const clone = this.sourceElement_.cloneNode(true); - + const clone = sourceElement.cloneNode(true); clone.className = ''; st.setStyles(clone, { position: 'absolute', @@ -733,8 +746,6 @@ export class AmpLightboxViewer extends AMP.BaseElement { }); transLayer.appendChild(clone); - this.sourceElement_.classList.add('i-amphtml-ghost'); - // Move and resize the image to the location given by the lightbox. const dx = imageBox.left - rect.left; const dy = imageBox.top - rect.top; @@ -783,21 +794,32 @@ export class AmpLightboxViewer extends AMP.BaseElement { exit_() { const anim = new Animation(this.element); let duration = MIN_TRANSITION_DURATION; - const imageBox = /**@type {?}*/ (this.getCurrentElement_().imageViewer) + const currentElementMetadata = this.getCurrentElement_(); + const imageBox = /**@type {?}*/ (currentElementMetadata.imageViewer) .implementation_.getImageBoxWithOffset(); - const image = /**@type {?}*/ (this.getCurrentElement_().imageViewer) + const image = /**@type {?}*/ (currentElementMetadata.imageViewer) .implementation_.getImage(); + const sourceElement = currentElementMetadata.sourceElement; // Try to transition to the source image. let transLayer = null; + return this.vsync_.measurePromise(() => { - if (this.sourceElement_ && image - && !this.aspectRatioChanged_(this.sourceElement_)) { - // TODO (#13013): if current image is not the original image, don't transition + // Lightbox background fades out. + anim.add(0, tr.setStyles(this.element, { + opacity: tr.numeric(1, 0), + }), MOTION_DURATION_RATIO, ENTER_CURVE_); + + if (sourceElement !== null + && sourceElement.tagName == 'AMP-IMG' + && this.shouldAnimate_(sourceElement) + && (sourceElement == this.sourceElement_ + || this.manager_.hasCarousel(this.currentLightboxGroupId_))) { transLayer = this.element.ownerDocument.createElement('div'); transLayer.classList.add('i-amphtml-lightbox-viewer-trans'); this.element.ownerDocument.body.appendChild(transLayer); + sourceElement.classList.add('i-amphtml-ghost'); - const rect = layoutRectFromDomRect(this.sourceElement_ + const rect = layoutRectFromDomRect(sourceElement ./*OK*/getBoundingClientRect()); const clone = image.cloneNode(true); st.setStyles(clone, { @@ -835,7 +857,7 @@ export class AmpLightboxViewer extends AMP.BaseElement { anim.add(0, (time, complete) => { moveAndScale(time); if (complete) { - this.sourceElement_.classList.remove('i-amphtml-ghost'); + sourceElement.classList.remove('i-amphtml-ghost'); } }, MOTION_DURATION_RATIO, EXIT_CURVE_); @@ -849,8 +871,8 @@ export class AmpLightboxViewer extends AMP.BaseElement { }).then(() => { return anim.start(duration).thenAlways(() => { return this.vsync_.mutatePromise(() => { - if (this.sourceElement_) { - this.sourceElement_.classList.remove('i-amphtml-ghost'); + if (sourceElement) { + sourceElement.classList.remove('i-amphtml-ghost'); } st.setStyles(this.element, { opacity: '', @@ -881,6 +903,27 @@ export class AmpLightboxViewer extends AMP.BaseElement { MAX_TRANSITION_DURATION ); } + + maybeSyncSourceCarousel_() { + if (this.manager_.hasCarousel(this.currentLightboxGroupId_)) { + const lightboxCarouselMetadata = this.manager_ + .getCarouselMetadataForLightboxGroup(this.currentLightboxGroupId_); + + let returnSlideIndex = this.currentElemId_; + + lightboxCarouselMetadata.excludedIndexes.some(i => { + if (i <= returnSlideIndex) { + returnSlideIndex++; + } else { + return true; + } + }); + + /**@type {?}*/ (lightboxCarouselMetadata.sourceCarousel).implementation_ + .showSlideWhenReady(returnSlideIndex); + } + } + /** * Closes the lightbox-viewer * @return {!Promise} @@ -895,6 +938,8 @@ export class AmpLightboxViewer extends AMP.BaseElement { this.cleanupEventListeners_(); + this.maybeSyncSourceCarousel_(); + this.vsync_.mutate(() => { // If there's gallery, set gallery to display none this.container_.removeAttribute('gallery-view'); diff --git a/extensions/amp-lightbox-viewer/0.1/service/lightbox-manager-impl.js b/extensions/amp-lightbox-viewer/0.1/service/lightbox-manager-impl.js index 1b9eed911337..d0b85afdd452 100644 --- a/extensions/amp-lightbox-viewer/0.1/service/lightbox-manager-impl.js +++ b/extensions/amp-lightbox-viewer/0.1/service/lightbox-manager-impl.js @@ -24,6 +24,7 @@ import { } from '../../../../src/dom'; import {toArray} from '../../../../src/types'; import {CommonSignals} from '../../../../src/common-signals'; +import {hasOwn, map} from '../../../../src/utils/object'; const LIGHTBOX_ELIGIBLE_TAGS = { 'amp-img': true, @@ -46,7 +47,13 @@ const VALIDATION_ERROR_MSG = `lightbox attribute is only supported for the * url: string, * element: !Element * }} */ -let LightboxThumbnailDataDef; +export let LightboxThumbnailDataDef; + +/** @typedef {{ + * sourceCarousel: !Element, + * excludedIndexes: !Array + * }} */ +let LightboxedCarouselMetadataDef; /** * LightboxManager is a document-scoped service responsible for: @@ -80,15 +87,22 @@ export class LightboxManager { * Ordered lists of lightboxable elements according to group * @private {!Object>} */ - this.lightboxGroups_ = { + this.lightboxGroups_ = map({ default: [], - }; + }); /** * Counter tracking number of carousels without ids * @private {number} */ this.counter_ = 0; + + /** + * If the lightbox group is a carousel, this object contains a + * mapping of the lightbox group id to the carousel element. + * @private {!Object} + */ + this.lightboxSourceCarousels_ = map(); } /** @@ -103,6 +117,28 @@ export class LightboxManager { return this.initPromise_; } + /** + * Returns a reference to the source carousel of the lightbox + * group if one exists. + * @param {string} lightboxGroupId + * @return {!LightboxedCarouselMetadataDef|null} + */ + getCarouselMetadataForLightboxGroup(lightboxGroupId) { + if (hasOwn(this.lightboxSourceCarousels_, lightboxGroupId)) { + return this.lightboxSourceCarousels_[lightboxGroupId]; + } + return null; + } + + /** + * Returns true if the lightboxGroupId belongs to an amp carousel + * @param {string} lightboxGroupId + * @return {boolean} + */ + hasCarousel(lightboxGroupId) { + return hasOwn(this.lightboxSourceCarousels_, lightboxGroupId); + } + /** * Decides whether an already lightboxable element should automatically get * a tap handler to open in the lightbox. @@ -155,9 +191,23 @@ export class LightboxManager { processLightboxCarousel_(carousel) { const lightboxGroupId = carousel.getAttribute('lightbox') || 'carousel' + (carousel.getAttribute('id') || this.counter_++); + if (carousel.getAttribute('type') == 'slides') { + this.lightboxSourceCarousels_[lightboxGroupId] = map({ + 'sourceCarousel': carousel, + 'excludedIndexes': [], + }); + // TODO (#13011): scroll carousel needs to support goToSlide + // before we can use it for lightbox, so they currently don't count. + } this.getSlidesFromCarousel_(carousel).then(slides => { - slides.forEach(slide => { - if (!slide.hasAttribute('lightbox-exclude')) { + slides.forEach((slide, index) => { + const shouldExcludeSlide = slide.hasAttribute('lightbox-exclude') + || (slide.hasAttribute('lightbox') + && slide.getAttribute('lightbox') !== lightboxGroupId); + if (shouldExcludeSlide) { + this.lightboxSourceCarousels_[lightboxGroupId] + .excludedIndexes.push(index); + } else { slide.setAttribute('lightbox', lightboxGroupId); this.processBaseLightboxElement_(slide, lightboxGroupId); }