diff --git a/extensions/amp-analytics/0.1/analytics-root.js b/extensions/amp-analytics/0.1/analytics-root.js index b8efa75cb7af..1afac3573ae5 100644 --- a/extensions/amp-analytics/0.1/analytics-root.js +++ b/extensions/amp-analytics/0.1/analytics-root.js @@ -266,7 +266,6 @@ export class AnalyticsRoot { } elementArray = this.getDataVarsElements_(elementArray, selector); userAssert(elementArray.length, `Element "${selector}" not found`); - this.verifyAmpElements_(elementArray, selector); elements = elements.concat(elementArray); } // Return unique @@ -331,7 +330,7 @@ export class AnalyticsRoot { } /** - * Searches for the AMP element(s) that matches the selector + * Searches for the element(s) that matches the selector * within the scope of the analytics root in relationship to * the specified context node. * @@ -339,9 +338,9 @@ export class AnalyticsRoot { * @param {!Array|string} selectors DOM query selector(s). * @param {?string=} selectionMethod Allowed values are `null`, * `'closest'` and `'scope'`. - * @return {!Promise>} Array of AMP elements corresponding to the selector if found. + * @return {!Promise>} Array of elements corresponding to the selector if found. */ - getAmpElements(context, selectors, selectionMethod) { + getElements(context, selectors, selectionMethod) { if ( isExperimentOn(this.ampdoc.win, 'visibility-trigger-improvements') && isArray(selectors) @@ -355,7 +354,7 @@ export class AnalyticsRoot { /** @type {!Array} */ (selectors) ); } - return this.getAmpElement( + return this.getElement( context, /** @type {string} */ (selectors), selectionMethod diff --git a/extensions/amp-analytics/0.1/events.js b/extensions/amp-analytics/0.1/events.js index a8b49ea051d6..598068344062 100644 --- a/extensions/amp-analytics/0.1/events.js +++ b/extensions/amp-analytics/0.1/events.js @@ -25,7 +25,7 @@ import { import {deepMerge, dict, hasOwn} from '../../../src/utils/object'; import {dev, devAssert, user, userAssert} from '../../../src/log'; import {getData} from '../../../src/event-helper'; -import {getDataParamsFromAttributes} from '../../../src/dom'; +import {getDataParamsFromAttributes, isAmpElement} from '../../../src/dom'; import {isArray, isEnumValue, isFiniteNumber} from '../../../src/types'; import {startsWith} from '../../../src/string'; @@ -1446,7 +1446,6 @@ export class VisibilityTracker extends EventTracker { const visibilitySpec = config['visibilitySpec'] || {}; const selector = config['selector'] || visibilitySpec['selector']; const waitForSpec = visibilitySpec['waitFor']; - let readyPromiseWaitForSpec; let reportWhenSpec = visibilitySpec['reportWhen']; let createReportReadyPromiseFunc = null; if (reportWhenSpec) { @@ -1489,7 +1488,8 @@ export class VisibilityTracker extends EventTracker { if (!selector || selector == ':root' || selector == ':host') { // When `selector` is specified, we always use "ini-load" signal as // a "ready" signal. - readyPromiseWaitForSpec = waitForSpec || (selector ? 'ini-load' : null); + const readyPromiseWaitForSpec = + waitForSpec || (selector ? 'ini-load' : 'none'); return visibilityManager.listenRoot( visibilitySpec, this.getReadyPromise(readyPromiseWaitForSpec), @@ -1503,19 +1503,14 @@ export class VisibilityTracker extends EventTracker { ); } - // An AMP-element. Wait for DOM to be fully parsed to avoid + // An element. Wait for DOM to be fully parsed to avoid // false missed searches. // Array selectors do not suppor the special cases: ':host' & ':root' const selectionMethod = config['selectionMethod'] || visibilitySpec['selectionMethod']; - readyPromiseWaitForSpec = waitForSpec || 'ini-load'; this.assertUniqueSelectors_(selector); const unlistenPromise = this.root - .getAmpElements( - context.parentElement || context, - selector, - selectionMethod - ) + .getElements(context.parentElement || context, selector, selectionMethod) .then((elements) => { const unlistenCallbacks = []; for (let i = 0; i < elements.length; i++) { @@ -1523,7 +1518,7 @@ export class VisibilityTracker extends EventTracker { visibilityManager.listenElement( elements[i], visibilitySpec, - this.getReadyPromise(readyPromiseWaitForSpec, elements[i]), + this.getReadyPromise(waitForSpec, elements[i]), createReportReadyPromiseFunc, this.onEvent_.bind(this, eventType, listener, elements[i]) ) @@ -1645,14 +1640,26 @@ export class VisibilityTracker extends EventTracker { * @visibleForTesting */ getReadyPromise(waitForSpec, opt_element) { - if (!waitForSpec) { + if (opt_element) { + if (!isAmpElement(opt_element)) { + userAssert( + !waitForSpec || waitForSpec == 'none', + 'waitFor for non-AMP elements must be none or null. Found %s', + waitForSpec + ); + } else { + waitForSpec = waitForSpec || 'ini-load'; + } + } + + if (!waitForSpec || waitForSpec == 'none') { // Default case, waitFor selector is not defined, wait for nothing return null; } const trackerAllowlist = getTrackerTypesForParentType('visible'); userAssert( - waitForSpec == 'none' || trackerAllowlist[waitForSpec] !== undefined, + trackerAllowlist[waitForSpec] !== undefined, 'waitFor value %s not supported', waitForSpec ); diff --git a/extensions/amp-analytics/0.1/test/test-analytics-root.js b/extensions/amp-analytics/0.1/test/test-analytics-root.js index 3ebdb96a7c76..ded4c69b791e 100644 --- a/extensions/amp-analytics/0.1/test/test-analytics-root.js +++ b/extensions/amp-analytics/0.1/test/test-analytics-root.js @@ -323,7 +323,7 @@ describes.realWin('AmpdocAnalyticsRoot', {amp: 1}, (env) => { }); }); - describe('get amp elements', () => { + describe('get elements', () => { let child2; let child3; @@ -350,13 +350,14 @@ describes.realWin('AmpdocAnalyticsRoot', {amp: 1}, (env) => { child.classList.add('myClass'); child2.classList.add('myClass'); child3.classList.add('notMyClass'); - expect( - await root.getAmpElements(body, ['.myClass'], null) - ).to.deep.equal([child, child2]); + expect(await root.getElements(body, ['.myClass'], null)).to.deep.equal([ + child, + child2, + ]); // Check that non-experiment works toggleExperiment(win, 'visibility-trigger-improvements', false); expect( - await root.getAmpElements(body, '.notMyClass', null) + await root.getElements(body, '.notMyClass', null) ).to.deep.equal([child3]); }); @@ -368,7 +369,7 @@ describes.realWin('AmpdocAnalyticsRoot', {amp: 1}, (env) => { child3.classList.add('myClass'); child3.removeAttribute('data-vars-id'); - const children = await root.getAmpElements(body, ['.myClass']); + const children = await root.getElements(body, ['.myClass']); expect(spy).callCount(1); expect(spy).to.have.been.calledWith( 'amp-analytics/analytics-root', @@ -383,7 +384,7 @@ describes.realWin('AmpdocAnalyticsRoot', {amp: 1}, (env) => { child.id = 'myId'; child.classList.add('myClass'); expect( - await root.getAmpElements(body, ['.myClass', '#myId'], null) + await root.getElements(body, ['.myClass', '#myId'], null) ).to.deep.equal([child]); }); @@ -391,35 +392,47 @@ describes.realWin('AmpdocAnalyticsRoot', {amp: 1}, (env) => { child.classList.add('myClass'); expectAsyncConsoleError(/Element ":host" not found/, 1); await expect( - root.getAmpElements(body, [':host'], null) + root.getElements(body, [':host'], null) ).to.be.rejectedWith(/Element ":host" not found​​​/); }); it('should handle missing selector for AMP search', async () => { expectAsyncConsoleError(/Element "#unknown" not found/, 1); await expect( - root.getAmpElements(body, ['#unknown'], null) + root.getElements(body, ['#unknown'], null) ).to.be.rejectedWith(/Element "#unknown" not found​​​/); }); it('should handle invalid selector', async () => { expectAsyncConsoleError(/Invalid query selector 12345/, 1); - await expect( - root.getAmpElements(body, [12345], null) - ).to.be.rejectedWith(/Invalid query selector 12345​​​/); + await expect(root.getElements(body, [12345], null)).to.be.rejectedWith( + /Invalid query selector 12345​​​/ + ); }); - it('should fail if the found element is not AMP for AMP search', async () => { - expectAsyncConsoleError(/required to be an AMP element/, 1); + it('should find both AMP and non AMP elements within array selector', async () => { child.classList.remove('i-amphtml-element'); - await expect( - root.getAmpElements(body, ['#child'], null) - ).to.be.rejectedWith(/required to be an AMP element/); + child.classList.add('myClass'); + child2.classList.add('myClass'); + expect(await root.getElements(body, ['.myClass'], null)).to.deep.equal([ + child, + child2, + ]); + }); + + it('should find non AMP element with single selector', async () => { + toggleExperiment(win, 'visibility-trigger-improvements', false); + child.classList.remove('i-amphtml-element'); + child.removeAttribute('data-vars-id'); + child.classList.add('myClass'); + expect(await root.getElements(body, '.myClass', null)).to.deep.equal([ + child, + ]); }); it('should fail if selection method is found', async () => { try { - await root.getAmpElements(body, ['#child'], 'scope'); + await root.getElements(body, ['#child'], 'scope'); } catch (e) { expect(e).to.match( /Cannot have selectionMethod scope defined with an array selector/ @@ -716,7 +729,7 @@ describes.realWin( }); }); - describe('get amp elements', () => { + describe('get elements', () => { let child2; let child3; @@ -751,13 +764,13 @@ describes.realWin( }); it('should find all elements by selector', async () => { - const elements = await root.getAmpElements(body, ['.myClass'], null); + const elements = await root.getElements(body, ['.myClass'], null); expect(elements).to.deep.equals([child, child2]); // Check that non-experiment version works toggleExperiment(win, 'visibility-trigger-improvements', false); expect( - await root.getAmpElements(body, '.notMyClass', null) + await root.getElements(body, '.notMyClass', null) ).to.deep.equals([child3]); }); @@ -768,7 +781,7 @@ describes.realWin( parentChild.classList.add('i-amphtml-element'); parentChild.setAttribute('data-vars-id', 'abc'); - const elements = await root.getAmpElements(body, ['.myClass'], null); + const elements = await root.getElements(body, ['.myClass'], null); expect(elements).to.deep.equals([child, child2]); }); @@ -778,7 +791,7 @@ describes.realWin( child3.classList.add('myClass'); child3.removeAttribute('data-vars-id'); - const children = await root.getAmpElements(body, ['.myClass']); + const children = await root.getElements(body, ['.myClass']); expect(spy).callCount(1); expect(spy).to.have.been.calledWith( 'amp-analytics/analytics-root', @@ -796,24 +809,32 @@ describes.realWin( child2.classList.add('myClass'); // Each selector should find both elements, but only report once expect( - await root.getAmpElements(body, ['.myClass', '#myId'], null) + await root.getElements(body, ['.myClass', '#myId'], null) ).to.deep.equal([child, child2]); }); it('should handle missing selector for AMP search', async () => { expectAsyncConsoleError(/Element "#unknown" not found/, 1); await expect( - root.getAmpElements(body, ['#unknown'], null) + root.getElements(body, ['#unknown'], null) ).to.be.rejectedWith(/Element "#unknown" not found​​​/); }); - it('should fail if the found element is not AMP for AMP search', async () => { - expectAsyncConsoleError(/required to be an AMP element/, 1); + it('should find both AMP and non AMP elements', async () => { child.classList.remove('i-amphtml-element'); child.setAttribute('data-vars-id', '123'); - await expect( - root.getAmpElements(body, ['#child'], null) - ).to.be.rejectedWith(/required to be an AMP element/); + expect(await root.getElements(body, ['.myClass'], null)).to.deep.equal([ + child, + child2, + ]); + }); + + it('should find non AMP element with single selector', async () => { + toggleExperiment(win, 'visibility-trigger-improvements', false); + child.classList.remove('i-amphtml-element'); + expect(await root.getElements(body, '.myClass', null)).to.deep.equal([ + child, + ]); }); }); diff --git a/extensions/amp-analytics/0.1/test/test-events.js b/extensions/amp-analytics/0.1/test/test-events.js index f35cba75f71c..245621f4020b 100644 --- a/extensions/amp-analytics/0.1/test/test-events.js +++ b/extensions/amp-analytics/0.1/test/test-events.js @@ -41,9 +41,7 @@ describes.realWin('Events', {amp: 1}, (env) => { let handler; let analyticsElement; let target; - let target2; let child; - let child2; beforeEach(() => { win = env.win; @@ -58,17 +56,9 @@ describes.realWin('Events', {amp: 1}, (env) => { target.classList.add('target'); win.document.body.appendChild(target); - target2 = win.document.createElement('div'); - target2.classList.add('target2'); - win.document.body.appendChild(target2); - child = win.document.createElement('div'); child.classList.add('child'); target.appendChild(child); - - child2 = win.document.createElement('div'); - child2.classList.add('child2'); - target2.appendChild(child2); }); describe('AnalyticsEvent', () => { @@ -1820,12 +1810,12 @@ describes.realWin('Events', {amp: 1}, (env) => { let saveCallback; let matchEmptySpec; let matchFunc; - let getAmpElementSpy; + let getElementSpy; beforeEach(() => { tracker = root.getTracker('visible', VisibilityTracker); visibilityManagerMock = env.sandbox.mock(root.getVisibilityManager()); - getAmpElementSpy = env.sandbox.spy(root, 'getAmpElement'); + getElementSpy = env.sandbox.spy(root, 'getElement'); tracker.waitForTrackers_['ini-load'] = root.getTracker( 'ini-load', IniLoadTracker @@ -1834,6 +1824,16 @@ describes.realWin('Events', {amp: 1}, (env) => { tracker.waitForTrackers_['ini-load'] ); + target.parentNode.removeChild(target); + + target = win.document.createElement('amp-list'); + target.classList.add('target'); + win.document.body.appendChild(target); + + child = win.document.createElement('div'); + child.classList.add('child'); + target.appendChild(child); + target.classList.add('i-amphtml-element'); targetSignals = new Signals(); target.signals = () => targetSignals; @@ -1976,7 +1976,7 @@ describes.realWin('Events', {amp: 1}, (env) => { eventResolver ); expect(res).to.be.a('function'); - const unlistenReady = getAmpElementSpy.returnValues[0]; + const unlistenReady = getElementSpy.returnValues[0]; // #getAmpElement Promise await unlistenReady; // #assertMeasurable_ Promise @@ -1998,6 +1998,7 @@ describes.realWin('Events', {amp: 1}, (env) => { let eventsSpy; let res; let error; + let target2; beforeEach(() => { toggleExperiment(win, 'visibility-trigger-improvements', true); @@ -2008,6 +2009,10 @@ describes.realWin('Events', {amp: 1}, (env) => { eventsSpy = env.sandbox.spy(tracker, 'onEvent_'); + target2 = win.document.createElement('amp-list'); + win.document.body.appendChild(target2); + + target2.classList.add('target2'); target2.classList.add('i-amphtml-element'); targetSignals2 = new Signals(); target2.signals = () => targetSignals2; @@ -2082,8 +2087,8 @@ describes.realWin('Events', {amp: 1}, (env) => { .once(); // Dispose function res = tracker.add(analyticsElement, 'visible', config, eventResolver); - const unlistenReady = getAmpElementSpy.returnValues[0]; - const unlistenReady2 = getAmpElementSpy.returnValues[1]; + const unlistenReady = getElementSpy.returnValues[0]; + const unlistenReady2 = getElementSpy.returnValues[1]; // #getAmpElement Promise await unlistenReady; await unlistenReady2; @@ -2153,7 +2158,7 @@ describes.realWin('Events', {amp: 1}, (env) => { eventResolver ); expect(res).to.be.a('function'); - const unlistenReady = getAmpElementSpy.returnValues[0]; + const unlistenReady = getElementSpy.returnValues[0]; // #getAmpElement Promise await unlistenReady; // #assertMeasurable_ Promise @@ -2178,8 +2183,8 @@ describes.realWin('Events', {amp: 1}, (env) => { .returns(null) .once(); tracker.add(analyticsElement, 'hidden', config, eventResolver); - const unlistenReady = getAmpElementSpy.returnValues[0]; - // #getAmpElement Promise + const unlistenReady = getElementSpy.returnValues[0]; + // #getElement Promise yield unlistenReady; // #assertMeasurable_ Promise yield macroTask(); @@ -2240,6 +2245,50 @@ describes.realWin('Events', {amp: 1}, (env) => { return promise2; }); }); + + describe('non AMP elements', () => { + it('with non AMP element and waitFor NONE or null', () => { + const element = win.document.createElement('p'); + expect(tracker.getReadyPromise('none', element)).to.be.null; + expect(tracker.getReadyPromise(null, element)).to.be.null; + expect(tracker.getReadyPromise(undefined, element)).to.be.null; + }); + + it('error with non AMP element and waitFor not NONE or null', () => { + const element = win.document.createElement('p'); + expect(() => tracker.getReadyPromise('ini-load', element)).to.throw( + /waitFor for non-AMP elements must be none or null. Found ini-load/ + ); + }); + + it('should set default waitFor for AMP element', async () => { + const element = win.document.createElement('amp-list'); + tracker.waitForTrackers_['render-start'] = root.getTracker( + 'render-start', + SignalTracker + ); + const signalTrackerMock = env.sandbox.mock( + tracker.waitForTrackers_['render-start'] + ); + const iniLoadTrackerMock = env.sandbox.mock( + tracker.waitForTrackers_['ini-load'] + ); + + await tracker.getReadyPromise(null, element); + iniLoadTrackerMock + .expects('getElementSignal') + .withExactArgs('ini-load', element) + .returns(Promise.resolve()) + .once(); + + await tracker.getReadyPromise('render-start', element); + signalTrackerMock + .expects('getElementSignal') + .withExactArgs('render-start') + .returns(Promise.resolve()) + .once(); + }); + }); }); describe('should create correct reportReadyPromise', () => { diff --git a/extensions/amp-analytics/amp-analytics.md b/extensions/amp-analytics/amp-analytics.md index de1929a4fee3..c879f06612df 100644 --- a/extensions/amp-analytics/amp-analytics.md +++ b/extensions/amp-analytics/amp-analytics.md @@ -811,7 +811,7 @@ page becomes visible. The firing of this trigger can be configured using } ``` -The element visibility trigger can be configured for any AMP element or a +The element visibility trigger can be configured for any AMP or non-AMP element or a document root using [`selector`](#element-selector). The trigger will fire when the specified element matches the visibility parameters that can be customized using the `visibilitySpec`. @@ -1116,8 +1116,7 @@ Configuration properties supported in `visibilitySpec` are: - `waitFor`: This property indicates that the visibility trigger should wait for a certain signal before tracking visibility. The supported values are `none`, `ini-load`, and `render-start`. If `waitFor` is undefined, it is - defaulted to [`ini-load`](#ini-load) when selector is specified, or to - `none` otherwise. + defaulted to [`ini-load`](#ini-load) (for AMP elements) when selector is specified, or to `none` otherwise. When tracking non-AMP elements, only `none` is supported, which is its default value. Tracking non-AMP elements may not always work as intended. For example, tracking a `
` element that contains an ``, may not accurately wait for the iframe to load before sending the signal out. - `reportWhen`: This property indicates that the visibility trigger should wait for a certain signal before sending the trigger. The only supported diff --git a/test/manual/amp-analytics/amp-analytics-multi-selector.html b/test/manual/amp-analytics/amp-analytics-multi-selector.html index e82445342b4d..a6a51cd32fe6 100644 --- a/test/manual/amp-analytics/amp-analytics-multi-selector.html +++ b/test/manual/amp-analytics/amp-analytics-multi-selector.html @@ -1,333 +1,554 @@ - + - - - Ad visible repeat examples - - - - - - - + .fill { + height: 750px; + width: 10px; + background-color: yellow; + opacity: 0.2; + } + + + + + + + - - - + - - -
-
- + +
+
+ - - - + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ - - - + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad3" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad4" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad5" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad6" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad7" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad8" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad9" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad10" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed vehicula justo, quis blandit tortor. Curabitur in mollis elit. Sed sed lorem vel mi elementum eleifend ut vel sapien. Suspendisse sed blandit nulla. Sed dignissim + eros nec felis malesuada consectetur. Etiam quis nunc quis sapien tempus euismod in eget velit. +

+ + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad11" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad12" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad13" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad14" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad15" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad16" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad17" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad18" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad19" + data-ad-width="300" + data-ad-height="250" + class="myClass" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad20" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad21" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad22" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad23" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-vars-id="ad24" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - + data-url="https://lh3.googleusercontent.com/pSECrJ82R7-AqeBCOEPGPM9iG9OEIQ_QXcbubWIOdkY=w400-h300-no-n" + data-valid="true" + data-ad-width="300" + data-ad-height="250" + class="myClass restrictDataVars" + > - -
- - +
+ +