Skip to content

Commit

Permalink
Sync Lightbox with Carousel (ampproject#13050)
Browse files Browse the repository at this point in the history
* Sync slides to lightbox

* Only exit if you're going back to your original element or you're a sync-ed carousel

* Exclude scroll carousel

* Add null check to shouldAnimate

* Handle excluding elements from lightbox
  • Loading branch information
cathyxz authored and RanAbram committed Mar 12, 2018
1 parent da34eb3 commit e5f7e09
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 29 deletions.
93 changes: 69 additions & 24 deletions extensions/amp-lightbox-viewer/0.1/amp-lightbox-viewer.js
Expand Up @@ -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';
Expand Down Expand Up @@ -74,7 +77,8 @@ let manager_;
* @typedef {{
* descriptionText: string,
* tagName: string,
* imageViewer: ?Element
* imageViewer: ?Element,
* sourceElement: !Element
* }}
*/
let LightboxElementMetadataDef_;
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -699,28 +712,28 @@ 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, {
opacity: tr.numeric(0, 1),
}), 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',
Expand All @@ -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;
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -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_);

Expand All @@ -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: '',
Expand Down Expand Up @@ -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}
Expand All @@ -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');
Expand Down
Expand Up @@ -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,
Expand All @@ -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<number>
* }} */
let LightboxedCarouselMetadataDef;

/**
* LightboxManager is a document-scoped service responsible for:
Expand Down Expand Up @@ -80,15 +87,22 @@ export class LightboxManager {
* Ordered lists of lightboxable elements according to group
* @private {!Object<string, !Array<!Element>>}
*/
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<string, !LightboxedCarouselMetadataDef>}
*/
this.lightboxSourceCarousels_ = map();
}

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit e5f7e09

Please sign in to comment.