562 lines
21 KiB
JavaScript
562 lines
21 KiB
JavaScript
var Piwik_Overlay_FollowingPages = (function () {
|
|
|
|
/** jQuery */
|
|
var $ = jQuery;
|
|
|
|
/** Info about the following pages */
|
|
var followingPages = [];
|
|
|
|
/** List of excluded get parameters */
|
|
var excludedParams = [];
|
|
|
|
/** Index of the links on the page */
|
|
var linksOnPage = {};
|
|
|
|
/** Reference to create element function */
|
|
var c;
|
|
|
|
/** Counter for the largest clickRate on the page */
|
|
var maxClickRate = 0;
|
|
|
|
/** Load the following pages */
|
|
function load(callback) {
|
|
// normalize current location
|
|
var location = window.location.href;
|
|
location = Piwik_Overlay_UrlNormalizer.normalize(location);
|
|
location = (("https:" == document.location.protocol) ? 'https' : 'http') + '://' + location;
|
|
|
|
var excludedParamsLoaded = false;
|
|
var followingPagesLoaded = false;
|
|
|
|
// load excluded params
|
|
Piwik_Overlay_Client.api('getExcludedQueryParameters', function (data) {
|
|
for (var i = 0; i < data.length; i++) {
|
|
if (typeof data[i] == 'object') {
|
|
data[i] = data[i][0];
|
|
}
|
|
}
|
|
excludedParams = data;
|
|
|
|
excludedParamsLoaded = true;
|
|
if (followingPagesLoaded) {
|
|
callback();
|
|
}
|
|
});
|
|
|
|
// load following pages
|
|
Piwik_Overlay_Client.api('getFollowingPages', function (data) {
|
|
followingPages = data;
|
|
processFollowingPages();
|
|
|
|
followingPagesLoaded = true;
|
|
if (excludedParamsLoaded) {
|
|
callback();
|
|
}
|
|
}, 'url=' + encodeURIComponent(location));
|
|
}
|
|
|
|
/** Normalize the URLs of following pages and aggregate some stats */
|
|
function processFollowingPages() {
|
|
var totalClicks = 0;
|
|
for (var i = 0; i < followingPages.length; i++) {
|
|
var page = followingPages[i];
|
|
// though the following pages are returned without the prefix, downloads
|
|
// and outlinks still have it.
|
|
page.label = Piwik_Overlay_UrlNormalizer.removeUrlPrefix(page.label);
|
|
totalClicks += followingPages[i].referrals;
|
|
}
|
|
for (i = 0; i < followingPages.length; i++) {
|
|
var clickRate = followingPages[i].referrals / totalClicks * 100;
|
|
followingPages[i].clickRate = clickRate;
|
|
if (clickRate > maxClickRate) maxClickRate = clickRate;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build an index of links on the page.
|
|
* This function is passed to $('a').each()
|
|
*/
|
|
var processLinkDelta = false;
|
|
|
|
function processLink() {
|
|
var a = $(this);
|
|
a[0].piwikDiscovered = true;
|
|
|
|
var href = a.attr('href');
|
|
href = Piwik_Overlay_UrlNormalizer.normalize(href);
|
|
|
|
if (href) {
|
|
if (typeof linksOnPage[href] == 'undefined') {
|
|
linksOnPage[href] = [a];
|
|
}
|
|
else {
|
|
linksOnPage[href].push(a);
|
|
}
|
|
}
|
|
|
|
if (href && processLinkDelta !== false) {
|
|
if (typeof processLinkDelta[href] == 'undefined') {
|
|
processLinkDelta[href] = [a];
|
|
}
|
|
else {
|
|
processLinkDelta[href].push(a);
|
|
}
|
|
}
|
|
}
|
|
|
|
var repositionTimeout = false;
|
|
var resizeTimeout = false;
|
|
|
|
function build(callback) {
|
|
// build an index of all links on the page
|
|
$('a').each(processLink);
|
|
|
|
// add tags to known following pages
|
|
createLinkTags(linksOnPage);
|
|
|
|
// position the tags
|
|
positionLinkTags();
|
|
|
|
callback();
|
|
|
|
// check on a regular basis whether new links have appeared.
|
|
// we use a timeout instead of an interval to make sure one call is done before
|
|
// the next one is triggered
|
|
var repositionAfterTimeout;
|
|
repositionAfterTimeout = function () {
|
|
repositionTimeout = window.setTimeout(function () {
|
|
findNewLinks();
|
|
positionLinkTags(repositionAfterTimeout);
|
|
}, 1800);
|
|
};
|
|
repositionAfterTimeout();
|
|
|
|
// reposition link tags on window resize
|
|
$(window).resize(function () {
|
|
if (repositionTimeout) {
|
|
window.clearTimeout(repositionTimeout);
|
|
}
|
|
if (resizeTimeout) {
|
|
window.clearTimeout(resizeTimeout);
|
|
}
|
|
resizeTimeout = window.setTimeout(function () {
|
|
positionLinkTags();
|
|
repositionAfterTimeout();
|
|
}, 70);
|
|
});
|
|
}
|
|
|
|
/** Create a batch of link tags */
|
|
function createLinkTags(links) {
|
|
var body = $('body');
|
|
for (var i = 0; i < followingPages.length; i++) {
|
|
var url = followingPages[i].label;
|
|
if (typeof links[url] != 'undefined') {
|
|
for (var j = 0; j < links[url].length; j++) {
|
|
createLinkTag(links[url][j], url, followingPages[i], body);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Create the link tag element */
|
|
function createLinkTag(linkTag, linkUrl, data, body) {
|
|
if (typeof linkTag[0].piwikTagElement != 'undefined' && linkTag[0].piwikTagElement !== null) {
|
|
// this link tag already has a tag element. happens in rare cases.
|
|
return;
|
|
}
|
|
|
|
linkTag[0].piwikTagElement = true;
|
|
|
|
var rate = data.clickRate;
|
|
|
|
if( rate < 0.001 ) {
|
|
rate = '<0.001';
|
|
} else if (rate < 10) {
|
|
rate = Math.round(rate * 10) / 10;
|
|
} else {
|
|
rate = Math.round(rate);
|
|
}
|
|
|
|
var span = c('span').html(rate + '%');
|
|
var tagElement = c('div', 'LinkTag').append(span).hide();
|
|
|
|
tagElement.attr({'data-rateofmax': Math.round(100 * rate/maxClickRate)/100});
|
|
|
|
body.prepend(tagElement);
|
|
|
|
linkTag.add(tagElement).hover(function () {
|
|
highlightLink(linkTag, linkUrl, data);
|
|
}, function () {
|
|
unHighlightLink(linkTag, linkUrl);
|
|
});
|
|
|
|
// attach the tag element to the link element. we can't use .data() because jquery
|
|
// would remove it when removing the link from the dom. but we still need to find
|
|
// the tag element to remove it as well.
|
|
linkTag[0].piwikTagElement = tagElement;
|
|
}
|
|
|
|
/** Position the link tags next to the links */
|
|
function positionLinkTags(callback) {
|
|
var url, linkTag, tagElement, offset, top, left, isRight, hasOneChild, inlineChild;
|
|
var tagWidth = 36, tagHeight = 21;
|
|
var tagsToRemove = [];
|
|
|
|
for (var i = 0; i < followingPages.length; i++) {
|
|
url = followingPages[i].label;
|
|
if (typeof linksOnPage[url] != 'undefined') {
|
|
for (var j = 0; j < linksOnPage[url].length; j++) {
|
|
linkTag = linksOnPage[url][j];
|
|
tagElement = linkTag[0].piwikTagElement;
|
|
|
|
if (linkTag.closest('html').length == 0 || !tagElement) {
|
|
// the link has been removed from the dom
|
|
if (tagElement) {
|
|
tagElement.hide();
|
|
}
|
|
// mark for deletion. don't delete it now because we
|
|
// are iterating of the array it's in. it will be deleted
|
|
// below this for loop.
|
|
tagsToRemove.push({
|
|
index1: url,
|
|
index2: j
|
|
});
|
|
continue;
|
|
}
|
|
|
|
hasOneChild = checkHasOneChild(linkTag);
|
|
inlineChild = false;
|
|
if (hasOneChild && linkTag.css('display') != 'block') {
|
|
inlineChild = linkTag.children().eq(0);
|
|
}
|
|
|
|
if (getVisibility(linkTag) == 'hidden' || (
|
|
// in case of hasOneChild: jquery always returns linkTag.is(':visible')=false
|
|
!linkTag.is(':visible') && !(hasOneChild && inlineChild && inlineChild.is(':visible'))
|
|
)) {
|
|
// link is not visible
|
|
tagElement.hide();
|
|
continue;
|
|
}
|
|
|
|
tagElement.attr('class', 'PIS_LinkTag'); // reset class
|
|
if (tagElement[0].piwikHighlighted) {
|
|
tagElement.addClass('PIS_Highlighted');
|
|
}
|
|
|
|
// see comment in highlightLink()
|
|
if (hasOneChild && linkTag.find('> img').length === 1) {
|
|
offset = linkTag.find('> img').offset();
|
|
if (offset.left == 0 && offset.top == 0) {
|
|
offset = linkTag.offset();
|
|
}
|
|
} else if (inlineChild !== false) {
|
|
offset = inlineChild.offset();
|
|
} else {
|
|
offset = linkTag.offset();
|
|
}
|
|
|
|
var zoomFactor = 1 + +tagElement.attr('data-rateofmax');
|
|
top = offset.top - tagHeight + 6;
|
|
left = offset.left - tagWidth + 10;
|
|
|
|
if (isRight = (left < zoomFactor * tagWidth - tagWidth ) ) {
|
|
tagElement.addClass('PIS_Right');
|
|
left = offset.left + linkTag.outerWidth() - 10;
|
|
}
|
|
|
|
if (top < zoomFactor * tagHeight - tagHeight ) {
|
|
tagElement.addClass(isRight ? 'PIS_BottomRight' : 'PIS_Bottom');
|
|
top = offset.top + linkTag.outerHeight() - 6;
|
|
}
|
|
|
|
tagElement.css({
|
|
'-webkit-transform': 'translate(' + left + 'px, ' + top + 'px) scale(' + zoomFactor + ')',
|
|
'-moz-transform': 'translate(' + left + 'px, ' + top + 'px) scale(' + zoomFactor + ')',
|
|
'-ms-transform': 'translate(' + left + 'px, ' + top + 'px) scale(' + zoomFactor + ')',
|
|
'-o-transform': 'translate(' + left + 'px, ' + top + 'px) scale(' + zoomFactor + ')',
|
|
'transform': 'translate(' + left + 'px, ' + top + 'px) scale(' + zoomFactor + ')',
|
|
'opacity': zoomFactor/2
|
|
});
|
|
|
|
tagElement.show();
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// walk tagsToRemove from back to front because it contains the indexes in ascending
|
|
// order. removing something from the front will impact the indexes that come after-
|
|
// wards. this can be avoided by starting in the back.
|
|
for (var k = tagsToRemove.length - 1; k >= 0; k--) {
|
|
var tagToRemove = tagsToRemove[k];
|
|
linkTag = linksOnPage[tagToRemove.index1][tagToRemove.index2];
|
|
// remove the tag element from the dom
|
|
if (linkTag && linkTag[0] && linkTag[0].piwikTagElement) {
|
|
tagElement = linkTag[0].piwikTagElement;
|
|
if (tagElement[0].piwikHighlighted) {
|
|
unHighlightLink(linkTag, tagToRemove.index1);
|
|
}
|
|
tagElement.remove();
|
|
linkTag[0].piwikTagElement = null;
|
|
}
|
|
// remove the link from the index
|
|
linksOnPage[tagToRemove.index1].splice(tagToRemove.index2, 1);
|
|
if (linksOnPage[tagToRemove.index1].length == 0) {
|
|
delete linksOnPage[tagToRemove.index1];
|
|
}
|
|
}
|
|
|
|
if (typeof callback == 'function') {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
/** Get the visibility of an element */
|
|
function getVisibility(el) {
|
|
var visibility = el.css('visibility');
|
|
if (visibility == 'inherit') {
|
|
el = el.parent();
|
|
if (el.length) {
|
|
return getVisibility(el);
|
|
}
|
|
}
|
|
return visibility;
|
|
}
|
|
|
|
/**
|
|
* Find out whether a link has only one child. Using .children().length === 1 doesn't work
|
|
* because it doesn't take additional text nodes into account.
|
|
*/
|
|
function checkHasOneChild(linkTag) {
|
|
var hasOneChild = (linkTag.children().length === 1);
|
|
if (hasOneChild) {
|
|
// if the element contains one tag and some text, hasOneChild is set incorrectly
|
|
var contents = linkTag.contents();
|
|
if (contents.length > 1) {
|
|
// find non-empty text nodes
|
|
contents = contents.filter(function () {
|
|
return this.nodeType == 3 && // text node
|
|
$.trim(this.data).length > 0; // contains more than whitespaces
|
|
});
|
|
if (contents.length) {
|
|
hasOneChild = false;
|
|
}
|
|
}
|
|
}
|
|
return hasOneChild;
|
|
}
|
|
|
|
/** Check whether new links have been added to the dom */
|
|
function findNewLinks() {
|
|
var newLinks = $('a').filter(function () {
|
|
return typeof this.piwikDiscovered == 'undefined' || this.piwikDiscovered === null;
|
|
});
|
|
|
|
if (!newLinks.length) {
|
|
return;
|
|
}
|
|
|
|
processLinkDelta = {};
|
|
newLinks.each(processLink);
|
|
createLinkTags(processLinkDelta);
|
|
processLinkDelta = false;
|
|
}
|
|
|
|
/** Dom elements used for drawing a box around the link */
|
|
var highlightElements = [];
|
|
|
|
/** Highlight a link on hover */
|
|
function highlightLink(linkTag, linkUrl, data) {
|
|
if (highlightElements.length == 0) {
|
|
highlightElements.push(c('div', 'LinkHighlightBoxTop'));
|
|
highlightElements.push(c('div', 'LinkHighlightBoxRight'));
|
|
highlightElements.push(c('div', 'LinkHighlightBoxLeft'));
|
|
|
|
highlightElements.push(c('div', 'LinkHighlightBoxText'));
|
|
|
|
var body = $('body');
|
|
for (var i = 0; i < highlightElements.length; i++) {
|
|
body.prepend(highlightElements[i].css({display: 'none'}));
|
|
}
|
|
}
|
|
|
|
var width = linkTag.outerWidth();
|
|
|
|
var offset, height;
|
|
var hasOneChild = checkHasOneChild(linkTag);
|
|
if (hasOneChild && linkTag.find('img').length === 1) {
|
|
// if the <a> tag contains only an <img>, the offset and height methods don't work properly.
|
|
// as a result, the box around the image link would be wrong. we use the image to derive
|
|
// the offset and height instead of the link to get correct values.
|
|
var img = linkTag.find('img');
|
|
offset = img.offset();
|
|
height = img.outerHeight();
|
|
}
|
|
if (hasOneChild && linkTag.css('display') != 'block') {
|
|
// if the <a> tag is not displayed as block and has only one child, using the child to
|
|
// derive the offset and dimensions is more robust.
|
|
var child = linkTag.children().eq(0);
|
|
offset = child.offset();
|
|
height = child.outerHeight();
|
|
width = child.outerWidth();
|
|
} else {
|
|
offset = linkTag.offset();
|
|
height = linkTag.outerHeight();
|
|
}
|
|
|
|
var numLinks = linksOnPage[linkUrl].length;
|
|
|
|
putBoxAroundLink(offset, width, height, numLinks, data.referrals);
|
|
|
|
// highlight tags
|
|
for (var j = 0; j < numLinks; j++) {
|
|
var tag = linksOnPage[linkUrl][j][0].piwikTagElement;
|
|
tag.addClass('PIS_Highlighted');
|
|
tag[0].piwikHighlighted = true;
|
|
}
|
|
|
|
// Sometimes it fails to remove the notification when the hovered element is removed.
|
|
// To make sure we don't display more than one location at a time, we hide all before showing the new one.
|
|
Piwik_Overlay_Client.hideNotifications('LinkLocation');
|
|
|
|
// we don't use .data() because jquery would remove the callback when the link tag is removed
|
|
linkTag[0].piwikHideNotification = Piwik_Overlay_Client.notification(
|
|
Piwik_Overlay_Translations.get('link') + ': ' + linkUrl, 'LinkLocation');
|
|
}
|
|
|
|
function putBoxAroundLink(offset, width, height, numLinks, numReferrals) {
|
|
var borderWidth = 2;
|
|
var padding = 4; // the distance between the link and the border
|
|
|
|
// top border
|
|
highlightElements[0]
|
|
.width(width + 2 * padding)
|
|
.css({
|
|
top: offset.top - borderWidth - padding,
|
|
left: offset.left - padding
|
|
}).show();
|
|
|
|
// right border
|
|
highlightElements[1]
|
|
.height(height + 2 * borderWidth + 2 * padding)
|
|
.css({
|
|
top: offset.top - borderWidth - padding,
|
|
left: offset.left + width + padding
|
|
}).show();
|
|
|
|
// left border
|
|
highlightElements[2]
|
|
.height(height + 2 * borderWidth + 2 * padding)
|
|
.css({
|
|
top: offset.top - borderWidth - padding,
|
|
left: offset.left - borderWidth - padding
|
|
}).show();
|
|
|
|
// bottom box text
|
|
var text;
|
|
if (numLinks > 1) {
|
|
text = Piwik_Overlay_Translations.get('clicksFromXLinks')
|
|
.replace(/%1\$s/, numReferrals)
|
|
.replace(/%2\$s/, numLinks);
|
|
} else if (numReferrals == 1) {
|
|
text = Piwik_Overlay_Translations.get('oneClick');
|
|
} else {
|
|
text = Piwik_Overlay_Translations.get('clicks')
|
|
.replace(/%s/, numReferrals);
|
|
}
|
|
|
|
// bottom box position and dimension
|
|
var textPadding = ' ';
|
|
highlightElements[3].html(textPadding + text + textPadding).css({
|
|
width: 'auto',
|
|
top: offset.top + height + padding,
|
|
left: offset.left - borderWidth - padding
|
|
}).show();
|
|
|
|
var minBoxWidth = width + 2 * borderWidth + 2 * padding;
|
|
if (highlightElements[3].width() < minBoxWidth) {
|
|
// we cannot use minWidth because of IE7
|
|
highlightElements[3].width(minBoxWidth);
|
|
}
|
|
}
|
|
|
|
/** Remove highlight from link */
|
|
function unHighlightLink(linkTag, linkUrl) {
|
|
for (var i = 0; i < highlightElements.length; i++) {
|
|
highlightElements[i].hide();
|
|
}
|
|
|
|
var numLinks = linksOnPage[linkUrl].length;
|
|
for (var j = 0; j < numLinks; j++) {
|
|
var tag = linksOnPage[linkUrl][j][0].piwikTagElement;
|
|
if (tag) {
|
|
tag.removeClass('PIS_Highlighted');
|
|
tag[0].piwikHighlighted = false;
|
|
}
|
|
}
|
|
|
|
if ((typeof linkTag[0].piwikHideNotification) == 'function') {
|
|
linkTag[0].piwikHideNotification();
|
|
linkTag[0].piwikHideNotification = null;
|
|
}
|
|
}
|
|
|
|
return {
|
|
|
|
/**
|
|
* The main method
|
|
*/
|
|
initialize: function (finishCallback) {
|
|
c = Piwik_Overlay_Client.createElement;
|
|
Piwik_Overlay_Client.loadScript('plugins/Overlay/client/urlnormalizer.js', function () {
|
|
Piwik_Overlay_UrlNormalizer.initialize();
|
|
load(function () {
|
|
Piwik_Overlay_UrlNormalizer.setExcludedParameters(excludedParams);
|
|
build(function () {
|
|
finishCallback();
|
|
})
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Remove everything from the dom and terminate timeouts.
|
|
* This can be used from the console in order to load a new implementation for debugging afterwards.
|
|
* If you add `Piwik_Overlay_FollowingPages.remove();` to the beginning and
|
|
* `Piwik_Overlay_FollowingPages.initialize(function(){});` to the end of this file, you can just
|
|
* paste it into the console to inject the new implementation.
|
|
*/
|
|
remove: function () {
|
|
for (var i = 0; i < followingPages.length; i++) {
|
|
var url = followingPages[i].label;
|
|
if (typeof linksOnPage[url] != 'undefined') {
|
|
for (var j = 0; j < linksOnPage[url].length; j++) {
|
|
var linkTag = linksOnPage[url][j];
|
|
var tagElement = linkTag[0].piwikTagElement;
|
|
if (tagElement) {
|
|
tagElement.remove();
|
|
}
|
|
linkTag[0].piwikTagElement = null;
|
|
|
|
$(linkTag).unbind('mouseenter').unbind('mouseleave');
|
|
}
|
|
}
|
|
}
|
|
for (i = 0; i < highlightElements.length; i++) {
|
|
highlightElements[i].remove();
|
|
}
|
|
if (repositionTimeout) {
|
|
window.clearTimeout(repositionTimeout);
|
|
}
|
|
if (resizeTimeout) {
|
|
window.clearTimeout(resizeTimeout);
|
|
}
|
|
$(window).unbind('resize');
|
|
}
|
|
|
|
};
|
|
|
|
})();
|