Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove measure APIs from amp-auto-lightbox #33663

Merged
merged 4 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});