-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
✨ Visibility trigger support for querySelectorAll #26886
Changes from 14 commits
db28aad
7ba9784
74eaef3
926e4bf
cbdb344
1003500
62ab379
beb8f58
7a43664
df942d2
7456e8d
813992f
7683e34
e9efdfe
b4228b8
97c92a9
c4b9cc5
2108ac7
b6ea443
71c6b80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -279,6 +279,30 @@ export class AnalyticsRoot { | |
}); | ||
} | ||
|
||
/** | ||
* @param {string} selector DOM query selector. | ||
* @return {!Promise<!Array<!Element>>} Element corresponding to the selector. | ||
*/ | ||
getElementsByQuerySelectorAll_(selector) { | ||
// Wait for document-ready to avoid false missed searches | ||
return this.ampdoc.whenReady().then(() => { | ||
let foundElements; | ||
try { | ||
foundElements = this.getRoot().querySelectorAll(selector); | ||
} catch (e) { | ||
userAssert(false, `Invalid query selector ${selector}`); | ||
} | ||
const elements = []; | ||
for (let i = 0; i < foundElements.length; i++) { | ||
if (this.contains(foundElements[i])) { | ||
elements.push(foundElements[i]); | ||
} | ||
} | ||
userAssert(elements.length, `Element "${selector}" not found`); | ||
return elements; | ||
}); | ||
} | ||
|
||
/** | ||
* Searches the AMP element that matches the selector within the scope of the | ||
* analytics root in relationship to the specified context node. | ||
|
@@ -287,18 +311,71 @@ export class AnalyticsRoot { | |
* @param {string} selector DOM query selector. | ||
* @param {?string=} selectionMethod Allowed values are `null`, | ||
* `'closest'` and `'scope'`. | ||
* @param {boolean=} opt_multiSelectorOn multi-selector expriment | ||
* @return {!Promise<!AmpElement>} AMP element corresponding to the selector if found. | ||
*/ | ||
getAmpElement(context, selector, selectionMethod, opt_multiSelectorOn) { | ||
getAmpElement(context, selector, selectionMethod) { | ||
return this.getElement(context, selector, selectionMethod).then(element => { | ||
this.verifyAmpElements_([element], selector); | ||
return element; | ||
}); | ||
} | ||
|
||
/** | ||
* Searches for the AMP element(s) that matches the selector | ||
* within the scope of the analytics root in relationship to | ||
* the specified context node. | ||
* | ||
* @param {!Element} context | ||
* @param {!Array<string>} selectors Array of DOM query selector. | ||
* @param {?string=} selectionMethod Allowed values are `null`, | ||
* `'closest'` and `'scope'`. | ||
* @param {boolean=} opt_multiSelectorOn multi-selector expriment | ||
* @return {!Promise<!Array<!AmpElement>>} Array of AMP elements corresponding to the selector if found. | ||
*/ | ||
getAmpElements(context, selectors, selectionMethod, opt_multiSelectorOn) { | ||
micajuine-ho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (opt_multiSelectorOn) { | ||
userAssert( | ||
!selectionMethod, | ||
'Cannot have selectionMethod defined with an array selector.' | ||
micajuine-ho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
const elementsListsPromise = []; | ||
for (let i = 0; i < selectors.length; i++) { | ||
elementsListsPromise.push( | ||
micajuine-ho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.getElementsByQuerySelectorAll_(selectors[i]) | ||
); | ||
} | ||
return Promise.all(elementsListsPromise).then(elementsLists => { | ||
const uniqueElements = []; | ||
for (let i = 0; i < elementsLists.length; i++) { | ||
this.verifyAmpElements_(elementsLists[i], selectors[i]); | ||
for (let j = 0; j < elementsLists[i].length; j++) { | ||
if (uniqueElements.indexOf(elementsLists[i][j]) === -1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little concern about the performance here. Would it be faster to assign a unique attribute to element instead? so that we don't need to iterate the array everytime. Could you please verify? cc @jridgewell who might know : ) What's the best way to find unique element out of an array? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For small lists, this will be quick enough. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @zhouyx I think the list will be relatively small. What do you think? |
||
uniqueElements.push(elementsLists[i][j]); | ||
} | ||
} | ||
} | ||
return uniqueElements; | ||
}); | ||
} | ||
return this.getAmpElement( | ||
context, | ||
selectors[0], | ||
selectionMethod | ||
).then(element => [element]); | ||
} | ||
|
||
/** | ||
* @param {!Array<Element>} elements | ||
* @param {string} selector | ||
*/ | ||
verifyAmpElements_(elements, selector) { | ||
for (let i = 0; i < elements.length; i++) { | ||
userAssert( | ||
element.classList.contains('i-amphtml-element'), | ||
elements[i].classList.contains('i-amphtml-element'), | ||
'Element "%s" is required to be an AMP element', | ||
selector | ||
); | ||
return element; | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -333,25 +333,108 @@ describes.realWin('AmpdocAnalyticsRoot', {amp: 1}, env => { | |
addTestInstance(root3.getElement(body, '#target'), null); | ||
}); | ||
|
||
it('should find an AMP element for AMP search', () => { | ||
it('should find an AMP element for AMP search', async () => { | ||
child.classList.add('i-amphtml-element'); | ||
return root.getAmpElement(body, '#child').then(element => { | ||
expect(element).to.equal(child); | ||
}); | ||
const elements = await root.getAmpElementOrElements(body, '#child'); | ||
micajuine-ho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
expect(elements[0]).to.equal(child); | ||
}); | ||
|
||
it('should allow not-found element for AMP search', () => { | ||
return root.getAmpElement(body, '#unknown').catch(error => { | ||
it('should allow not-found element for AMP search', async () => { | ||
micajuine-ho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await root.getAmpElementOrElements(body, '#unknown').catch(error => { | ||
expect(error).to.match(/Element "#unknown" not found/); | ||
}); | ||
}); | ||
|
||
it('should fail if the found element is not AMP for AMP search', () => { | ||
it('should fail if the found element is not AMP for AMP search', async () => { | ||
child.classList.remove('i-amphtml-element'); | ||
return root.getAmpElement(body, '#child').catch(error => { | ||
await root.getAmpElementOrElements(body, '#child').catch(error => { | ||
expect(error).to.match(/required to be an AMP element/); | ||
}); | ||
}); | ||
|
||
describe('get amp elements', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To confirm, what will be the API provided after all changes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We will have Adding test coverage for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will we deprecate usage of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, |
||
let child2; | ||
let elements; | ||
let error; | ||
|
||
beforeEach(() => { | ||
error = false; | ||
child2 = win.document.createElement('child'); | ||
body.appendChild(child2); | ||
child.classList.add('i-amphtml-element'); | ||
child2.classList.add('i-amphtml-element'); | ||
}); | ||
|
||
afterEach(() => { | ||
if (!error) { | ||
expect(elements).to.contain(child); | ||
expect(elements).to.contain(child2); | ||
expect(elements.length).to.equal(2); | ||
} | ||
}); | ||
|
||
it('should find elements by ID', async () => { | ||
micajuine-ho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
child.id = 'myId'; | ||
child2.id = 'myId'; | ||
elements = await root.getAmpElementOrElements( | ||
body, | ||
'#myId', | ||
null, | ||
true | ||
); | ||
}); | ||
|
||
it('should find element by class', async () => { | ||
child.classList.add('myClass'); | ||
child2.classList.add('myClass'); | ||
elements = await root.getAmpElementOrElements( | ||
body, | ||
'.myClass', | ||
null, | ||
true | ||
); | ||
}); | ||
|
||
it('should find element by tag name', async () => { | ||
elements = await root.getAmpElementOrElements( | ||
body, | ||
'child', | ||
null, | ||
true | ||
); | ||
}); | ||
|
||
it('should find element by selector', async () => { | ||
child.id = 'myId'; | ||
child2.id = 'myId'; | ||
child.classList.add('myClass'); | ||
child2.classList.add('myClass'); | ||
micajuine-ho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
elements = await root.getAmpElementOrElements( | ||
body, | ||
'#myId.myClass', | ||
null, | ||
true | ||
); | ||
}); | ||
|
||
it('should allow not-found element for AMP search', async () => { | ||
error = true; | ||
await root | ||
.getAmpElementOrElements(body, '#unknown', null, true) | ||
.catch(error => { | ||
expect(error).to.match(/Element "#unknown" not found/); | ||
}); | ||
}); | ||
|
||
it('should fail if the found element is not AMP for AMP search', async () => { | ||
error = true; | ||
await root | ||
.getAmpElementOrElements(body, '#child', null, true) | ||
.catch(error => { | ||
expect(error).to.match(/required to be an AMP element/); | ||
}); | ||
}); | ||
}); | ||
micajuine-ho marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
describe('createSelectiveListener', () => { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.contains
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Used in the original getAmpElements(), to make sure that the element is a descendant of the
analytics-root
: defined here