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

Fix Measuring Child AMP Elements #9279

Closed
Closed
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
5 changes: 5 additions & 0 deletions src/custom-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,11 @@ function createBaseCustomElementClass(win) {
return this.getResources().getResourceForElement(this).getLayoutBox();
}

/** @return {!Layout} */
getLayout() {
return this.layout_;
}

/**
* Returns a previously measured layout box relative to the page. The
* fixed-position elements are relative to the top of the document.
Expand Down
15 changes: 15 additions & 0 deletions src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import {dev} from './log';
import {cssEscape} from '../third_party/css-escape/css-escape';
import {startsWith} from './string';

const HTML_ESCAPE_CHARS = {
'&': '&',
Expand All @@ -28,6 +29,20 @@ const HTML_ESCAPE_CHARS = {
const HTML_ESCAPE_REGEX = /(&|<|>|"|'|`)/g;


/**
* Determines if this element is an AMP element
* @param {!Element} element
* @return {boolean}
*/
export function isAmpElement(element) {
const tag = element.tagName;
// Use prefix to recognize AMP element. This is necessary because stub
// may not be attached yet.
return startsWith(tag, 'AMP-') &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is checking element.classList.contain('i-amphtml-element') not sufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the original comment

We can tell whether the parent has been stubbed by whether a resource has been attached to it.

That means we have to be able to tell regardless of where the element is in the CustomElement lifecycle. And an element has to have been stubbed (#connectedCallback, the first step) to have the i-amphtml-element class.

// Some "amp-*" elements are not really AMP elements. :smh:
!(tag == 'AMP-STICKY-AD-TOP-PADDING' || tag == 'AMP-BODY');
}

/**
* Waits until the child element is constructed. Once the child is found, the
* callback is executed.
Expand Down
76 changes: 56 additions & 20 deletions src/service/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import {
layoutRectsOverlap,
moveLayoutRect,
} from '../layout-rect';
import {Layout} from '../layout';
import {dev} from '../log';
import {startsWith} from '../string';
import {toggle, computedStyle} from '../style';
import {isAmpElement} from '../dom';

const TAG = 'Resource';
const RESOURCE_PROP_ = '__AMP__RESOURCE';
Expand Down Expand Up @@ -147,6 +148,9 @@ export class Resource {
/** @private {!AmpElement|undefined|null} */
this.owner_ = undefined;

/** @private {!AmpElement|undefined|null} */
this.ampAncestor_ = undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be discussed a bit more. But CSS changes seem rather clear to me. Can we merge them first and split this part into another PR? I'd like to get a bit deeper on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


/** @private {!ResourceState} */
this.state_ = element.isBuilt() ? ResourceState.NOT_LAID_OUT :
ResourceState.NOT_BUILT;
Expand Down Expand Up @@ -215,21 +219,41 @@ export class Resource {
this.owner_ = owner;
}

/**
* Returns an ancestor amp element or null.
* @return {?AmpElement}
*/
getAmpAncestor() {
if (this.ampAncestor_ === undefined) {
let ancestor = null;
for (let n = this.element.parentElement; n; n = n.parentElement) {
if (isAmpElement(n)) {
ancestor = n;
break;
}
}
this.ampAncestor_ = ancestor;
}
return this.ampAncestor_;
}

/**
* Returns an owner element or null.
* @return {?AmpElement}
*/
getOwner() {
if (this.owner_ === undefined) {
for (let n = this.element; n; n = n.parentElement) {
if (n[OWNER_PROP_]) {
this.owner_ = n[OWNER_PROP_];
break;
const ancestor = this.getAmpAncestor();
let owner = null;
if (ancestor) {
for (let n = this.element; n !== ancestor; n = n.parentElement) {
if (n[OWNER_PROP_]) {
owner = n[OWNER_PROP_];
break;
}
}
}
if (this.owner_ === undefined) {
this.owner_ = null;
}
this.owner_ = owner;
}
return this.owner_;
}
Expand Down Expand Up @@ -365,19 +389,31 @@ export class Resource {
* transitioned to the "ready for layout" state.
*/
measure() {
let current = this;
let ancestor = this.getAmpAncestor();
// Check if the element is ready to be measured.
// Placeholders are special. They are technically "owned" by parent AMP
// elements, sized by parents, but laid out independently. This means
// that placeholders need to at least wait until the parent element
// has been stubbed. We can tell whether the parent has been stubbed
// by whether a resource has been attached to it.
if (this.isPlaceholder_ &&
this.element.parentElement &&
// Use prefix to recognize AMP element. This is necessary because stub
// may not be attached yet.
startsWith(this.element.parentElement.tagName, 'AMP-') &&
!(RESOURCE_PROP_ in this.element.parentElement)) {
return;
while (ancestor) {
const resource = Resource.forElementOptional(ancestor);
// If there's an AMP ancestor that's still not stubbed (has a Resource),
// we don't know what to do yet.
if (!resource) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe, unfortunately, we do have a very few amp- tags that are not custom elements. Could you pls double-check via validator? Though validator wouldn't give the full picture since some extensions might create them via createElement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I.e.

const paddingBar = this.win.document.createElement(

This code is probably benign, since no one ads stuff into this element from what I can tell. But others might not be so lucky.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhouyx: Is that supposed to be publicly stylable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed, #9283. Will update so those aren't considered.

return;
}
// If the current resource (not the ancestor) is a placeholder, allow it to
// measure. This is because placeholders are laid out independently of
// the ancestor element (even if they are sized and "owned" by it).
if (current.isPlaceholder_) {
break;
}
// If this ancestor isn't built yet, that means we don't know what kind
// of styles will be applied to this element. We must wait.
// Unless the ancestor is a container, in which case the element will
// define its own sizing.
if (!ancestor.isBuilt() && ancestor.getLayout() !== Layout.CONTAINER) {
return;
}
current = resource;
ancestor = current.getAmpAncestor();
}

this.isMeasureRequested_ = false;
Expand Down