Skip to content

Commit

Permalink
Remove measure APIs from amp-auto-lightbox (ampproject#33663)
Browse files Browse the repository at this point in the history
* Remove measure APIs from amp-auto-lightbox

* fix presubmits

* moved measurer to src/utils

* lints
  • Loading branch information
Dima Voytenko authored and rochapablo committed Aug 30, 2021
1 parent cc62615 commit 8ce8ea7
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 30 deletions.
1 change: 0 additions & 1 deletion build-system/test-configs/forbidden-terms.js
Original file line number Diff line number Diff line change
Expand Up @@ -1101,7 +1101,6 @@ const forbiddenTermsSrcInclusive = {
'src/service/resources-impl.js',
'src/service/video-manager-impl.js',
'extensions/amp-a4a/0.1/amp-a4a.js',
'extensions/amp-auto-lightbox/0.1/amp-auto-lightbox.js',
'extensions/amp-fx-flying-carpet/0.1/amp-fx-flying-carpet.js',
'extensions/amp-script/0.1/amp-script.js',
'extensions/amp-story/1.0/amp-story-page.js',
Expand Down
28 changes: 18 additions & 10 deletions extensions/amp-auto-lightbox/0.1/amp-auto-lightbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
whenUpgradedToCustomElement,
} from '../../../src/dom';
import {dev} from '../../../src/log';
import {measureIntersectionNoRoot} from '../../../src/utils/intersection-no-root';
import {toArray} from '../../../src/types';
import {tryParseJson} from '../../../src/json';

Expand Down Expand Up @@ -124,11 +125,13 @@ const getRootNode = (ampdoc) => ampdoc.getRootNode();
export class Criteria {
/**
* @param {!Element} element
* @param {number} renderWidth
* @param {number} renderHeight
* @return {boolean}
*/
static meetsAll(element) {
static meetsAll(element, renderWidth, renderHeight) {
return (
Criteria.meetsSizingCriteria(element) &&
Criteria.meetsSizingCriteria(element, renderWidth, renderHeight) &&
Criteria.meetsTreeShapeCriteria(element)
);
}
Expand All @@ -152,15 +155,15 @@ export class Criteria {

/**
* @param {!Element} element
* @param {number} renderWidth
* @param {number} renderHeight
* @return {boolean}
*/
static meetsSizingCriteria(element) {
static meetsSizingCriteria(element, renderWidth, renderHeight) {
const {naturalWidth, naturalHeight} = getMaxNaturalDimensions(
dev().assertElement(element.querySelector('img'))
);

const {width: renderWidth, height: renderHeight} = element.getLayoutSize();

const viewport = Services.viewportForDoc(element);
const {width: vw, height: vh} = viewport.getSize();

Expand Down Expand Up @@ -440,11 +443,16 @@ export function runCandidates(ampdoc, candidates) {
if (candidate.signals().get(CommonSignals.UNLOAD)) {
return;
}
if (!Criteria.meetsAll(candidate)) {
return;
}
dev().info(TAG, 'apply', candidate);
return apply(ampdoc, candidate);
return measureIntersectionNoRoot(candidate).then(
({boundingClientRect}) => {
const {width, height} = boundingClientRect;
if (!Criteria.meetsAll(candidate, width, height)) {
return;
}
dev().info(TAG, 'apply', candidate);
return apply(ampdoc, candidate);
}
);
}, NOOP)
);
}
Expand Down
19 changes: 0 additions & 19 deletions extensions/amp-auto-lightbox/0.1/utils/promise.js

This file was deleted.

91 changes: 91 additions & 0 deletions src/utils/intersection-no-root.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Copyright 2020 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 {Deferred} from './promise';
import {createViewportObserver} from '../viewport-observer';
import {toWin} from '../types';

/**
* @fileoverview
* This utility is similar to the `src/utils/intersection`, but it doesn't
* require the `rootBounds` and thus can use a simpler version of the
* intersection observer that's supported natively on more platforms.
*
* TODO(#33678): Dedupe intersection measurement utils once the native
* support is better.
*/

/** @type {WeakMap<!Element, Deferred>} */
let intersectionDeferreds;

/** @type {WeakMap<!Window, IntersectionObserver>} */
let intersectionObservers;

/**
* @param {!Window} win
* @return {!IntersectionObserve}
*/
function getInOb(win) {
if (!intersectionDeferreds) {
intersectionDeferreds = new WeakMap();
intersectionObservers = new WeakMap();
}

if (!intersectionObservers.has(win)) {
const observer = createViewportObserver(
(entries) => {
const seen = new Set();
for (let i = entries.length - 1; i >= 0; i--) {
const {target} = entries[i];
if (seen.has(target)) {
continue;
}
seen.add(target);

observer.unobserve(target);
intersectionDeferreds.get(target).resolve(entries[i]);
intersectionDeferreds.delete(target);
}
},
win,
{needsRootBounds: false}
);
intersectionObservers.set(win, observer);
return observer;
}
return intersectionObservers.get(win);
}

/**
* Returns a promise that resolves with the intersection entry for the given element.
*
* If multiple measures for the same element occur very quickly, they will
* dedupe to the same promise.
*
* @param {!Element} el
* @return {!Promise<IntersectionObserverEntry>}
*/
export function measureIntersectionNoRoot(el) {
if (intersectionDeferreds && intersectionDeferreds.has(el)) {
return intersectionDeferreds.get(el).promise;
}

const inOb = getInOb(toWin(el.ownerDocument.defaultView));
inOb.observe(el);

const deferred = new Deferred();
intersectionDeferreds.set(el, deferred);
return deferred.promise;
}
126 changes: 126 additions & 0 deletions test/unit/utils/test-intersection-no-root.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Copyright 2020 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 {measureIntersectionNoRoot} from '../../../src/utils/intersection-no-root';

describes.fakeWin('utils/intersection', {}, (env) => {
function getInObConstructorStub() {
const ctor = (cb) => {
if (ctor.callback) {
throw new Error('Only a single InOb instance allowed per Window.');
}
const observedEls = new Set();
ctor.callback = (entries) => {
if (entries.some((x) => !observedEls.has(x.target))) {
throw new Error(
'Attempted to fire intersection for unobserved element.'
);
}
cb(entries);
};
return {
observe: (e) => observedEls.add(e),
unobserve: (e) => observedEls.delete(e),
disconnect: () => observedEls.clear(),
};
};
return ctor;
}

function fireIntersections(entries) {
if (entries.length == 0) {
return;
}
const win = entries[0].target.ownerDocument.defaultView;
win.IntersectionObserver.callback(entries);
}

let el;
beforeEach(() => {
env.win.IntersectionObserver = getInObConstructorStub();
el = env.win.document.createElement('p');
env.win.document.body.appendChild(el);
});

it('should measure intersection for an element', async () => {
const intersection = measureIntersectionNoRoot(el);
fireIntersections([{x: 100, target: el}]);
expect(await intersection).eql({x: 100, target: el});
});

it('should dedupe multiple measures', async () => {
const measure1 = measureIntersectionNoRoot(el);
const measure2 = measureIntersectionNoRoot(el);
expect(measure1).equal(measure2);
});

it('should not dedupe multiple measures with entries in between', async () => {
const measure1 = measureIntersectionNoRoot(el);
fireIntersections([{x: 100, target: el}]);
const measure2 = measureIntersectionNoRoot(el);

expect(measure1).not.equal(measure2);
});

it('should only use the latest entry', async () => {
const intersection = measureIntersectionNoRoot(el);
const firstEntry = {x: 0, target: el};
const secondEntry = {x: 100, target: el};

fireIntersections([firstEntry, secondEntry]);
expect(await intersection).equal(secondEntry);
});

it('should measure multiple elements', async () => {
const el2 = env.win.document.createElement('p');
env.win.document.body.appendChild(el2);

const intersection1 = measureIntersectionNoRoot(el);
const intersection2 = measureIntersectionNoRoot(el2);

const firstEntry = {x: 0, target: el};
const secondEntry = {x: 2, target: el2};

fireIntersections([secondEntry]);
fireIntersections([firstEntry]);

expect(await intersection1).equal(firstEntry);
expect(await intersection2).equal(secondEntry);
});

it('should support measuring elements from multiple windows', async () => {
const el1 = {
ownerDocument: {
defaultView: {IntersectionObserver: getInObConstructorStub()},
},
};
const el2 = {
ownerDocument: {
defaultView: {IntersectionObserver: getInObConstructorStub()},
},
};

const intersection1 = measureIntersectionNoRoot(el1);
const intersection2 = measureIntersectionNoRoot(el2);
const firstEntry = {target: el1};
const secondEntry = {target: el2};
fireIntersections([firstEntry]);
fireIntersections([secondEntry]);

expect(await intersection1).equal(firstEntry);
expect(await intersection2).equal(secondEntry);
});
});

0 comments on commit 8ce8ea7

Please sign in to comment.