Skip to content

Commit

Permalink
V1: allow lightbox to add its own intersection observer to the scheduler
Browse files Browse the repository at this point in the history
  • Loading branch information
Dima Voytenko committed Mar 18, 2021
1 parent 9087f42 commit a88b2e1
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 13 deletions.
9 changes: 9 additions & 0 deletions extensions/amp-lightbox/0.1/amp-lightbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {htmlFor} from '../../../src/static-template';
import {isInFie} from '../../../src/iframe-helper';
import {toArray} from '../../../src/types';
import {tryFocus} from '../../../src/dom';
import {unmountAllWithin} from '../../../src/utils/resource-container-helper';

/** @const {string} */
const TAG = 'amp-lightbox';
Expand Down Expand Up @@ -391,6 +392,8 @@ class AmpLightbox extends AMP.BaseElement {
element.addEventListener('transitionend', onAnimationEnd);
element.addEventListener('animationend', onAnimationEnd);

this.setAsContainer();

// TODO: instead of laying out children all at once, layout children based
// on visibility.
const owners = Services.ownersForDoc(this.element);
Expand Down Expand Up @@ -616,6 +619,12 @@ class AmpLightbox extends AMP.BaseElement {

this.untieCloseButton_();

this.removeAsContainer();

// Unmount all children when the lightbox is closed. They will automatically
// remount when the lightbox is opened again.
unmountAllWithin(this.element);

Services.ownersForDoc(this.element).schedulePause(
this.element,
dev().assertElement(this.container_)
Expand Down
18 changes: 18 additions & 0 deletions src/base-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,24 @@ export class BaseElement {
// Subclasses may override.
}

/**
* Set itself as a container element that can be monitored by the scheduler
* for auto-mounting.
*
* @param {!Element} opt_scroller A child of the container that should be
* monitored. Typically a scrollable element.
*/
setAsContainer(opt_scroller) {
this.element.setAsContainerInternal(opt_scroller);
}

/**
* Removes itself as a container. See `setAsContainer`.
*/
removeAsContainer() {
this.element.removeAsContainerInternal();
}

/**
* Subclasses can override this method to indicate that it is has
* render-blocking service.
Expand Down
24 changes: 24 additions & 0 deletions src/custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,30 @@ function createBaseCustomElementClass(win, elementConnectedCallback) {
});
}

/**
* See `BaseElement.setAsContainer`.
*
* @param {!Element} opt_scroller A child of the container that should be
* monitored. Typically a scrollable element.
* @restricted
* @final
*/
setAsContainerInternal(opt_scroller) {
devAssert(!opt_scroller || this.contains(opt_scroller));
const builder = getSchedulerForDoc(this.getAmpDoc());
builder.setContainer(this, opt_scroller);
}

/**
* See `BaseElement.setAsContainer`.
* @restricted
* @final
*/
removeAsContainerInternal() {
const builder = getSchedulerForDoc(this.getAmpDoc());
builder.removeContainer(this);
}

/**
* Update the internal ready state.
*
Expand Down
11 changes: 11 additions & 0 deletions src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -980,3 +980,14 @@ export function dispatchCustomEvent(node, name, opt_data, opt_options) {
event.initEvent(name, bubbles, cancelable);
node.dispatchEvent(event);
}

/**
* Ensures the child is contained by the parent, but not the parent itself.
*
* @param {!Element} parent
* @param {!Element} child
* @return {boolean}
*/
export function containsNotSelf(parent, child) {
return child !== parent && parent.contains(child);
}
68 changes: 64 additions & 4 deletions src/service/scheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import {LayoutPriority} from '../layout';
import {READY_SCAN_SIGNAL} from './resources-interface';
import {VisibilityState} from '../visibility-state';
import {containsNotSelf, hasNextNodeInDocumentOrder, isIframed} from '../dom';
import {getServiceForDoc, registerServiceBuilderForDoc} from '../service';
import {hasNextNodeInDocumentOrder, isIframed} from '../dom';
import {removeItem} from '../utils/array';

const ID = 'scheduler';

const ROOT_MARGIN = '250% 31.25%';

/** @implements {../service.Disposable} */
export class Scheduler {
/** @param {!./ampdoc-impl.AmpDoc} ampdoc */
Expand All @@ -37,10 +39,12 @@ export class Scheduler {
// Root bounds are not important, so we can use the `root:null` for a
// top-level window.
root: isIframed(win) ? win.document : null,
rootMargin: '250% 31.25%',
threshold: 0.001,
rootMargin: ROOT_MARGIN,
});

/** @private @const {!Map<!Element, !IntersectionObserver>} */
this.containerMap_ = new Map();

/** @private @const {!Map<!AmpElement, {asap: boolean, isIntersecting: boolean}>} */
this.targets_ = new Map();

Expand Down Expand Up @@ -85,6 +89,13 @@ export class Scheduler {
if (target.deferredBuild()) {
this.targets_.set(target, {asap: false, isIntersecting: false});
this.observer_.observe(target);
if (this.containerMap_.size > 0) {
this.containerMap_.forEach((observer, container) => {
if (containsNotSelf(container, target)) {
observer.observe(target);
}
});
}
} else {
this.targets_.set(target, {asap: false, isIntersecting: true});
}
Expand All @@ -103,13 +114,61 @@ export class Scheduler {
this.targets_.delete(target);

this.observer_.unobserve(target);
if (this.containerMap_.size > 0) {
this.containerMap_.forEach((observer) => {
observer.unobserve(target);
});
}

if (this.parsingTargets_) {
removeItem(this.parsingTargets_, target);
this.checkParsing_();
}
}

/**
* Adds the observer for the specified container. The first observer to
* find an intersection will trigger the element's mount.
*
* @param {!Element} container
* @param {!Element=} opt_scroller
*/
setContainer(container, opt_scroller) {
if (this.containerMap_.has(container)) {
return;
}

// Create observer.
const {win} = this.ampdoc_;
const observer = new win.IntersectionObserver((e) => this.observed_(e), {
root: opt_scroller || container,
rootMargin: ROOT_MARGIN,
});
this.containerMap_.set(container, observer);

// Subscribe all pending children.
this.targets_.forEach(({asap}, target) => {
if (!asap && containsNotSelf(container, target)) {
observer.observe(target);
}
});
}

/**
* Removes the container and its observer that were set by the `setContainer`.
*
* @param {!Element} container
*/
removeContainer(container) {
const observer = this.containerMap_.get(container);
if (!observer) {
return;
}

// Disconnect. All children will be unobserved automatically.
observer.disconnect();
}

/** @private*/
signalScanReady_() {
if (this.ampdoc_.isReady() && !this.scheduledReady_) {
Expand Down Expand Up @@ -180,13 +239,14 @@ export class Scheduler {
*/
observed_(entries) {
for (let i = 0; i < entries.length; i++) {
const {target, isIntersecting} = entries[i];
const {target, isIntersecting: isThisIntersecting} = entries[i];

const current = this.targets_.get(target);
if (!current) {
continue;
}

const isIntersecting = isThisIntersecting || current.isIntersecting;
this.targets_.set(target, {asap: current.asap, isIntersecting});
if (isIntersecting) {
this.maybeBuild_(target);
Expand Down
10 changes: 1 addition & 9 deletions src/utils/display-observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import {VisibilityState} from '../visibility-state';
import {containsNotSelf} from '../dom';
import {getServiceForDoc, registerServiceBuilderForDoc} from '../service';
import {pushIfNotExist, removeItem} from './array';
import {rethrowAsync} from '../log';
Expand Down Expand Up @@ -493,15 +494,6 @@ function findObserverByContainer(observers, container) {
return -1;
}

/**
* @param {!Element} container
* @param {!Element} child
* @return {boolean}
*/
function containsNotSelf(container, child) {
return child !== container && container.contains(child);
}

/**
* @param {!Array<function(boolean)>} callbacks
* @param {!Element} target
Expand Down
47 changes: 47 additions & 0 deletions src/utils/resource-container-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {Services} from '../services';
import {containsNotSelf} from '../dom';
import {rethrowAsync} from '../log';

/**
* Unmount all elements within this container.
*
* @param {!Element} container
*/
export function unmountAllWithin(container) {
forAllWithin(container, (element) => element.unmount());
}

/**
* Execute a callback for all elements within the container.
*
* @param {!Element} container
* @param {function(!Element)} callback
*/
export function forAllWithin(container, callback) {
const resources = Services.resourcesForDoc(container).get();
resources.forEach(({element}) => {
if (containsNotSelf(container, element)) {
try {
callback(element);
} catch (e) {
rethrowAsync(e);
}
}
});
}

0 comments on commit a88b2e1

Please sign in to comment.