Skip to content

Commit

Permalink
Added percentageVisible{Max,Min} conditions for visibilitySpec. (#2881)
Browse files Browse the repository at this point in the history
Part of #1297
  • Loading branch information
avimehta committed Apr 20, 2016
1 parent 1933961 commit 6a6bf4f
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 8 deletions.
12 changes: 11 additions & 1 deletion examples/analytics.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
height: 10px;
}
</style>
<script async custom-element="amp-anim" src="https://cdn.ampproject.org/v0/amp-anim-0.1.js"></script>
<script async custom-element="amp-analytics" src="https://cdn.ampproject.org/v0/amp-analytics-0.1.js"></script>
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style><noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>
Expand Down Expand Up @@ -53,6 +54,11 @@
"vars": {
"eventName": "page-loaded",
"eventId": "42"
},
"visibilitySpec": {
"selector": "#anim-id",
"visiblePercentageMin": 20,
"visiblePercentageMax": 80
}
},
"scrollPings": {
Expand Down Expand Up @@ -271,7 +277,7 @@
<script type="application/json">
{
"requests": {
"test-ping": "https://my-analytics.com/ping?title=${title}&acct=${account}"
"test-ping": "https://example.com/ping?title=${title}&acct=${account}"
},
"vars": {
"title": "A page that sends a ping",
Expand Down Expand Up @@ -459,6 +465,10 @@ <h1 id="top">AMP Analytics</h1>
<li>Aenean ac sem eget libero varius viverra sit amet vitae nunc.</li>
</ul>
<p></p>
<amp-anim src="https://goo.gl/FBCKQO" id="anim-id"
width="400" height="225" layout="responsive">
<amp-img placeholder src="https://goo.gl/GjcSkr" width="400" height="225" layout="responsive"></amp-img>
</amp-anim>
</body>
</html>

4 changes: 3 additions & 1 deletion extensions/amp-analytics/0.1/amp-analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {expandTemplate} from '../../../src/string';
import {installCidService} from '../../../src/service/cid-impl';
import {installStorageService} from '../../../src/service/storage-impl';
import {installActivityService} from '../../../src/service/activity-impl';
import {installVisibilityService} from '../../../src/service/visibility-impl';
import {isArray, isObject} from '../../../src/types';
import {sendRequest, sendRequestUsingIframe} from './transport';
import {urlReplacementsFor} from '../../../src/url-replacements';
Expand All @@ -30,9 +31,10 @@ import {xhrFor} from '../../../src/xhr';
import {toggle} from '../../../src/style';


installActivityService(AMP.win);
installCidService(AMP.win);
installStorageService(AMP.win);
installActivityService(AMP.win);
installVisibilityService(AMP.win);
instrumentationServiceFor(AMP.win);

const MAX_REPLACES = 16; // The maximum number of entries in a extraUrlParamsReplaceMap
Expand Down
27 changes: 22 additions & 5 deletions extensions/amp-analytics/0.1/instrumentation.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {timer} from '../../../src/timer';
import {user} from '../../../src/log';
import {viewerFor} from '../../../src/viewer';
import {viewportFor} from '../../../src/viewport';
import {visibilityFor} from '../../../src/visibility';

/** @private @const {number} */
const MIN_TIMER_INTERVAL_SECONDS_ = 0.5;
Expand Down Expand Up @@ -217,15 +218,29 @@ export class InstrumentationService {
return;
}

// TODO(avimehta, #1297): Add all the listeners so that visibility
// conditions are monitored and callback is called when the conditions
// are met.
if (config['visibilitySpec']) {
this.runOrSchedule_(() => {
visibilityFor(this.win_).then(visibility => {
visibility.listenOnce(config['visibilitySpec'], () => {
callback(new AnalyticsEvent(AnalyticsEventType.VISIBLE));
});
});
});
} else {
this.runOrSchedule_(() => {
callback(new AnalyticsEvent(AnalyticsEventType.VISIBLE));
});
}
}

/** @private{function()} function to run or schedule. */
runOrSchedule_(fn) {
if (this.viewer_.isVisible()) {
callback(new AnalyticsEvent(AnalyticsEventType.VISIBLE));
fn();
} else {
this.viewer_.onVisibilityChanged(() => {
if (this.viewer_.isVisible()) {
callback(new AnalyticsEvent(AnalyticsEventType.VISIBLE));
fn();
}
});
}
Expand Down Expand Up @@ -256,6 +271,8 @@ export class InstrumentationService {
/**
* Checks and outputs information about visibilitySpecValidation.
* @param {!JSONObject} config Configuration for instrumentation.
* @return {boolean} True if the spec is valid.
* @private
*/
isVisibilitySpecValid_(config) {
if (!config['visibilitySpec'] || !this.isViewabilityExperimentOn_()) {
Expand Down
2 changes: 1 addition & 1 deletion src/intersection-observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function getIntersectionChangeEntry(
export class IntersectionObserver extends Observable {
/**
* @param {!BaseElement} element.
* @param {!Element} iframe Iframe element to which would request intersection
* @param {!Element} iframe Iframe element which requested the intersection
* data.
* @param {?boolean} opt_is3p Set to `true` when the iframe is 3'rd party.
* @constructor
Expand Down
158 changes: 158 additions & 0 deletions src/service/visibility-impl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* Copyright 2015 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 {getService} from '../service';
import {resourcesFor} from '../resources';
import {timer} from '../timer';
import {viewportFor} from '../viewport';

/** @const {number} */
const LISTENER_INITIAL_RUN_DELAY_ = 100;

/**
* This type signifies a callback that gets called when visibility conditions
* are met.
* @typedef {function()}
*/
let VisibilityListenerCallbackDef;

/**
* @typedef {Object<string, JSONObject|VisibilityListenerCallbackDef>}
*/
let VisibilityListenerDef;

/**
* Allows tracking of AMP elements in the viewport.
*
* This class allows a caller to specify conditions to evaluate when an element
* is in viewport and for how long. If the conditions are satisfied, a provided
* callback is called.
*/
export class Visibility {

/** @param {!Window} */
constructor(win) {
this.win_ = win;

/**
* key: resource id.
* value: [{ config: <config>, callback: <callback>}]
* @type {Object<string, Array.<VisibilityListenerDef>>}
* @private
*/
this.listeners_ = Object.create(null);

/** @private {Array<!Resource>} */
this.resources_ = [];

/** @private @const {function} */
this.boundScrollListener_ = this.scrollListener_.bind(this);

/** @private {boolean} */
this.scrollListenerRegistered_ = false;

/** @private {!Resources} */
this.resourcesService_ = resourcesFor(this.win_);

/** @private {number|string} */
this.scheduledRunId_ = null;
}

/** @private */
registerForViewportEvents_() {
if (!this.scrollListenerRegistered__) {
const viewport = viewportFor(this.win_);

// Currently unlistens are not being used. In the event that no resources
// are actively being monitored, the scrollListener should be very cheap.
viewport.onScroll(this.boundScrollListener_);
viewport.onChanged(this.boundScrollListener_);
this.scrollListenerRegistered_ = true;
}

}

/**
* @param {!JSONObject} config
* @param {!VisibilityListenerCallbackDef} callback
*/
listenOnce(config, callback) {
const element = this.win_.document.getElementById(config['selector']
.slice(1));
const res = this.resourcesService_.getResourceForElement(element);
const resId = res.getId();

this.registerForViewportEvents_();

this.listeners_[resId] = (this.listeners_[resId] || []);
this.listeners_[resId].push({
config: config,
callback: callback,
});
this.resources_.push(res);

if (this.scheduledRunId_ == null) {
this.scheduledRunId_ = timer.delay(() => {
this.scrollListener_();
}, LISTENER_INITIAL_RUN_DELAY_);
}
}

/** @private */
scrollListener_() {
if (this.scheduledRunId_ != null) {
timer.cancel(this.scheduledRunId_);
this.scheduledRunId_ = null;
}

for (let r = this.resources_.length - 1; r >= 0; r--) {
const res = this.resources_[r];
const change = res.element.getIntersectionChangeEntry();
const ir = change.intersectionRect;
const br = change.boundingClientRect;
const visible = ir.width * ir.height * 100 / (br.height * br.width);

const listeners = this.listeners_[res.getId()];
for (let c = listeners.length - 1; c >= 0; c--) {
const config = listeners[c].config;
if (visible < config['visiblePercentageMin']) {
continue; // Maybe later
} else if (visible > config['visiblePercentageMax']) {
listeners.splice(c, 1);
continue;
}

listeners[c].callback();
listeners.splice(c, 1);
}

// Remove resources that have no listeners.
if (listeners.length == 0) {
this.resources_.splice(r, 1);
}
}
}
}

/**
* @param {!Window} win
* @return {!Visibility}
*/
export function installVisibilityService(win) {
return getService(win, 'visibility', () => {
return new Visibility(win);
});
};
25 changes: 25 additions & 0 deletions src/visibility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Copyright 2015 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 {getElementService} from './custom-element';

/**
* @param {!Window} win
* @return {!Promise<!Visibility>}
*/
export function visibilityFor(win) {
return getElementService(win, 'visibility', 'amp-analytics');
};
Loading

0 comments on commit 6a6bf4f

Please sign in to comment.