diff options
Diffstat (limited to 'js/piwik.js')
-rw-r--r-- | js/piwik.js | 1772 |
1 files changed, 1687 insertions, 85 deletions
diff --git a/js/piwik.js b/js/piwik.js index 8c3a8c9d23..f9309c217f 100644 --- a/js/piwik.js +++ b/js/piwik.js @@ -29,7 +29,7 @@ * @version 2012-10-08 * @link http://www.JSON.org/js.html ************************************************************/ -/*jslint evil: true, regexp: false, bitwise: true*/ +/*jslint evil: true, regexp: false, bitwise: true, white: true */ /*global JSON2:true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, @@ -408,7 +408,7 @@ if (typeof JSON2 !== 'object') { exec, res, width, height, devicePixelRatio, pdf, qt, realp, wma, dir, fla, java, gears, ag, - hook, getHook, getVisitorId, getVisitorInfo, setUserId, setSiteId, setTrackerUrl, appendToTrackingUrl, getRequest, addPlugin, + hook, getHook, getVisitorId, getVisitorInfo, setSiteId, setTrackerUrl, appendToTrackingUrl, getRequest, addPlugin, getAttributionInfo, getAttributionCampaignName, getAttributionCampaignKeyword, getAttributionReferrerTimestamp, getAttributionReferrerUrl, setCustomData, getCustomData, @@ -429,7 +429,32 @@ if (typeof JSON2 !== 'object') { setHeartBeatTimer, killFrame, redirectFile, setCountPreRendered, trackGoal, trackLink, trackPageView, trackSiteSearch, trackEvent, setEcommerceView, addEcommerceItem, trackEcommerceOrder, trackEcommerceCartUpdate, - deleteCookies + deleteCookies, offsetTop, offsetLeft, offsetHeight, offsetWidth, nodeType, defaultView, + innerHTML, scrollLeft, scrollTop, currentStyle, getComputedStyle, querySelectorAll, splice, + getAttribute, hasAttribute, attributes, nodeName, findContentNodes, findContentNodes, findContentNodesWithinNode, + findPieceNode, findTargetNodeNoDefault, findTargetNode, findContentPiece, children, hasNodeCssClass, + getAttributeValueFromNode, hasNodeAttributeWithValue, hasNodeAttribute, findNodesByTagName, findMultiple, + makeNodesUnique, concat, find, htmlCollectionToArray, offsetParent, value, nodeValue, findNodesHavingAttribute, + findFirstNodeHavingAttribute, findFirstNodeHavingAttributeWithValue, getElementsByClassName, + findNodesHavingCssClass, findFirstNodeHavingClass, isLinkElement, findParentContentNode, removeDomainIfIsInLink, + findContentName, findMediaUrlInNode, toAbsoluteUrl, findContentTarget, getLocation, origin, host, isSameDomain, + search, trim, getBoundingClientRect, bottom, right, left, innerWidth, innerHeight, clientWidth, clientHeight, + isOrWasNodeInViewport, isNodeVisible, buildInteractionRequestParams, buildImpressionRequestParams, + shouldIgnoreInteraction, setHrefAttribute, setAttribute, buildContentBlock, collectContent, setLocation, + CONTENT_ATTR, CONTENT_CLASS, CONTENT_NAME_ATTR, CONTENT_PIECE_ATTR, CONTENT_PIECE_CLASS, + CONTENT_TARGET_ATTR, CONTENT_TARGET_CLASS, CONTENT_IGNOREINTERACTION_ATTR, CONTENT_IGNOREINTERACTION_CLASS, + trackCallbackOnLoad, trackCallbackOnReady, buildContentImpressionsRequests, wasContentImpressionAlreadyTracked, + getQuery, getContent, getContentImpressionsRequestsFromNodes, buildContentInteractionTrackingRedirectUrl, + buildContentInteractionRequestNode, buildContentInteractionRequest, buildContentImpressionRequest, + appendContentInteractionToRequestIfPossible, setupInteractionsTracking, trackContentImpressionClickInteraction, + internalIsNodeVisible, clearTrackedContentImpressions, getTrackerUrl, trackAllContentImpressions, + getTrackedContentImpressions, getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet, + contentInteractionTrackingSetupDone, contains, match, pathname, piece, trackContentInteractionNode, + trackContentInteractionNode, trackContentImpressionsWithinNode, trackContentImpression, + enableTrackOnlyVisibleContent, trackContentInteraction, clearEnableTrackOnlyVisibleContent, + trackVisibleContentImpressions, isTrackOnlyVisibleContentEnabled, port, isUrlToCurrentDomain, + isNodeAuthorizedToTriggerInteraction, replaceHrefIfInternalLink, getConfigDownloadExtensions, disableLinkTracking, + substr, setAnyAttribute */ /*global _paq:true */ /*members push */ @@ -976,6 +1001,925 @@ if (typeof Piwik !== 'object') { } /************************************************************ + * Element Visiblility + ************************************************************/ + + /** + * Author: Jason Farrell + * Author URI: http://useallfive.com/ + * + * Description: Checks if a DOM element is truly visible. + * Package URL: https://github.com/UseAllFive/true-visibility + * License: MIT (https://github.com/UseAllFive/true-visibility/blob/master/LICENSE.txt) + */ + function isVisible(node) { + + if (!node) { + return false; + } + + //-- Cross browser method to get style properties: + function _getStyle(el, property) { + if (windowAlias.getComputedStyle) { + return documentAlias.defaultView.getComputedStyle(el,null)[property]; + } + if (el.currentStyle) { + return el.currentStyle[property]; + } + } + + function _elementInDocument(element) { + element = element.parentNode; + + while (element) { + if (element === documentAlias) { + return true; + } + element = element.parentNode; + } + return false; + } + + /** + * Checks if a DOM element is visible. Takes into + * consideration its parents and overflow. + * + * @param (el) the DOM element to check if is visible + * + * These params are optional that are sent in recursively, + * you typically won't use these: + * + * @param (t) Top corner position number + * @param (r) Right corner position number + * @param (b) Bottom corner position number + * @param (l) Left corner position number + * @param (w) Element width number + * @param (h) Element height number + */ + function _isVisible(el, t, r, b, l, w, h) { + var p = el.parentNode, + VISIBLE_PADDING = 1; // has to be visible at least one px of the element + + if (!_elementInDocument(el)) { + return false; + } + + //-- Return true for document node + if (9 === p.nodeType) { + return true; + } + + //-- Return false if our element is invisible + if ( + '0' === _getStyle(el, 'opacity') || + 'none' === _getStyle(el, 'display') || + 'hidden' === _getStyle(el, 'visibility') + ) { + return false; + } + + if (!isDefined(t) || + !isDefined(r) || + !isDefined(b) || + !isDefined(l) || + !isDefined(w) || + !isDefined(h)) { + t = el.offsetTop; + l = el.offsetLeft; + b = t + el.offsetHeight; + r = l + el.offsetWidth; + w = el.offsetWidth; + h = el.offsetHeight; + } + + if ((0 === h || 0 === w) && 'hidden' === _getStyle(el, 'overflow')) { + return false; + } + + //-- If we have a parent, let's continue: + if (p) { + //-- Check if the parent can hide its children. + if (('hidden' === _getStyle(p, 'overflow') || 'scroll' === _getStyle(p, 'overflow'))) { + //-- Only check if the offset is different for the parent + if ( + //-- If the target element is to the right of the parent elm + l + VISIBLE_PADDING > p.offsetWidth + p.scrollLeft || + //-- If the target element is to the left of the parent elm + l + w - VISIBLE_PADDING < p.scrollLeft || + //-- If the target element is under the parent elm + t + VISIBLE_PADDING > p.offsetHeight + p.scrollTop || + //-- If the target element is above the parent elm + t + h - VISIBLE_PADDING < p.scrollTop + ) { + //-- Our target element is out of bounds: + return false; + } + } + //-- Add the offset parent's left/top coords to our element's offset: + if (el.offsetParent === p) { + l += p.offsetLeft; + t += p.offsetTop; + } + //-- Let's recursively check upwards: + return _isVisible(p, t, r, b, l, w, h); + } + return true; + } + + return _isVisible(node); + } + + /************************************************************ + * Query + ************************************************************/ + + var query = { + htmlCollectionToArray: function (foundNodes) + { + var nodes = [], index; + + if (!foundNodes || !foundNodes.length) { + return nodes; + } + + for (index = 0; index < foundNodes.length; index++) { + nodes.push(foundNodes[index]); + } + + return nodes; + }, + find: function (selector) + { + // we use querySelectorAll only on document, not on nodes because of its unexpected behavior. See for + // instance http://stackoverflow.com/questions/11503534/jquery-vs-document-queryselectorall and + // http://jsfiddle.net/QdMc5/ and http://ejohn.org/blog/thoughts-on-queryselectorall + if (!document.querySelectorAll || !selector) { + return []; // we do not support all browsers + } + + var foundNodes = document.querySelectorAll(selector); + + return this.htmlCollectionToArray(foundNodes); + }, + findMultiple: function (selectors) + { + if (!selectors || !selectors.length) { + return []; + } + + var index, foundNodes; + var nodes = []; + for (index = 0; index < selectors.length; index++) { + foundNodes = this.find(selectors[index]); + nodes = nodes.concat(foundNodes); + } + + nodes = this.makeNodesUnique(nodes); + + return nodes; + }, + findNodesByTagName: function (node, tagName) + { + if (!node || !tagName || !node.getElementsByTagName) { + return []; + } + + var foundNodes = node.getElementsByTagName(tagName); + + return this.htmlCollectionToArray(foundNodes); + }, + makeNodesUnique: function (nodes) + { + nodes.sort(function(n1, n2){ + if (n1 === n2) { + return 0; + } + + return n1.innerHTML > n2.innerHTML ? 1 : -1; + }); + + if (nodes.length <= 1) { + return nodes; + } + + var index = 0; + var numDuplicates = 0; + var duplicates = []; + var node; + + node = nodes[index++]; + + while (node) { + if (node === nodes[index]) { + numDuplicates = duplicates.push(index); + } + + node = nodes[index++] || null; + } + + while (numDuplicates--) { + nodes.splice(duplicates[numDuplicates], 1); + } + + return nodes; + }, + getAttributeValueFromNode: function (node, attributeName) + { + if (!this.hasNodeAttribute(node, attributeName)) { + return; + } + + if (node && node.getAttribute) { + return node.getAttribute(attributeName); + } + + if (!node || !node.attributes) { + return; + } + + var typeOfAttr = (typeof node.attributes[attributeName]); + if ('undefined' === typeOfAttr) { + return; + } + + if (node.attributes[attributeName].value) { + return node.attributes[attributeName].value; // nodeValue is deprecated ie Chrome + } + + if (node.attributes[attributeName].nodeValue) { + return node.attributes[attributeName].nodeValue; + } + + var index; + var attrs = node.attributes; + + if (!attrs) { + return; + } + + for (index = 0; index < attrs.length; index++) { + if (attrs[index].nodeName === attributeName) { + return attrs[index].nodeValue; + } + } + + return null; + }, + hasNodeAttributeWithValue: function (node, attributeName) + { + var value = this.getAttributeValueFromNode(node, attributeName); + + return !!value; + }, + hasNodeAttribute: function (node, attributeName) + { + if (node && node.hasAttribute) { + return node.hasAttribute(attributeName); + } + + if (node && node.attributes) { + var typeOfAttr = (typeof node.attributes[attributeName]); + return 'undefined' !== typeOfAttr; + } + + return false; + }, + hasNodeCssClass: function (node, className) + { + if (node && className && node.className) { + var classes = node.className.split(' '); + if (-1 !== classes.indexOf(className)) { + return true; + } + } + + return false; + }, + findNodesHavingAttribute: function (nodeToSearch, attributeName, nodes) + { + if (!nodes) { + nodes = []; + } + + if (!nodeToSearch || !attributeName || !nodeToSearch.children) { + return nodes; + } + + var index, child; + for (index = 0; index < nodeToSearch.children.length; index++) { + child = nodeToSearch.children[index]; + if (this.hasNodeAttribute(child, attributeName)) { + nodes.push(child); + } + + nodes = this.findNodesHavingAttribute(child, attributeName, nodes); + } + + return nodes; + }, + findFirstNodeHavingAttribute: function (node, attributeName) + { + if (!node || !attributeName) { + return; + } + + if (this.hasNodeAttribute(node, attributeName)) { + return node; + } + + var nodes = this.findNodesHavingAttribute(node, attributeName); + + if (nodes && nodes.length) { + return nodes[0]; + } + }, + findFirstNodeHavingAttributeWithValue: function (node, attributeName) + { + if (!node || !attributeName) { + return; + } + + if (this.hasNodeAttributeWithValue(node, attributeName)) { + return node; + } + + var nodes = this.findNodesHavingAttribute(node, attributeName); + + if (!nodes || !nodes.length) { + return; + } + + var index; + for (index = 0; index < nodes.length; index++) { + if (this.getAttributeValueFromNode(nodes[index], attributeName)) { + return nodes[index]; + } + } + }, + findNodesHavingCssClass: function (nodeToSearch, className, nodes) + { + if (!nodes) { + nodes = []; + } + + if (!nodeToSearch || !className) { + return nodes; + } + + if (nodeToSearch.getElementsByClassName) { + var foundNodes = nodeToSearch.getElementsByClassName(className); + return this.htmlCollectionToArray(foundNodes); + } + + if (!nodeToSearch.children) { + return; + } + + var index, child; + for (index = 0; index < nodeToSearch.children.length; index++) { + child = nodeToSearch.children[index]; + if (this.hasNodeCssClass(child, className)) { + nodes.push(child); + } + + nodes = this.findNodesHavingCssClass(child, className, nodes); + } + + return nodes; + }, + findFirstNodeHavingClass: function (node, className) + { + if (!node || !className) { + return; + } + + if (this.hasNodeCssClass(node, className)) { + return node; + } + + var nodes = this.findNodesHavingCssClass(node, className); + + if (nodes && nodes.length) { + return nodes[0]; + } + }, + isLinkElement: function (node) + { + if (!node) { + return false; + } + + var elementName = String(node.nodeName).toLowerCase(); + var linkElementNames = ['a', 'area']; + var pos = linkElementNames.indexOf(elementName); + + return pos !== -1; + }, + setAnyAttribute: function (node, attrName, attrValue) + { + if (!node || !attrName) { + return; + } + + if (node.setAttribute) { + node.setAttribute(attrName, attrValue); + } else { + node[attrName] = attrValue; + } + } + }; + + /************************************************************ + * Content Tracking + ************************************************************/ + + var content = { + CONTENT_ATTR: 'data-track-content', + CONTENT_CLASS: 'piwikTrackContent', + CONTENT_NAME_ATTR: 'data-content-name', + CONTENT_PIECE_ATTR: 'data-content-piece', + CONTENT_PIECE_CLASS: 'piwikContentPiece', + CONTENT_TARGET_ATTR: 'data-content-target', + CONTENT_TARGET_CLASS: 'piwikContentTarget', + CONTENT_IGNOREINTERACTION_ATTR: 'data-content-ignoreinteraction', + CONTENT_IGNOREINTERACTION_CLASS: 'piwikContentIgnoreInteraction', + location: undefined, + + findContentNodes: function () + { + + var cssSelector = '.' + this.CONTENT_CLASS; + var attrSelector = '[' + this.CONTENT_ATTR + ']'; + var contentNodes = query.findMultiple([cssSelector, attrSelector]); + + return contentNodes; + }, + findContentNodesWithinNode: function (node) + { + if (!node) { + return []; + } + + // NOTE: we do not use query.findMultiple here as querySelectorAll would most likely not deliver the result we want + + var nodes1 = query.findNodesHavingCssClass(node, this.CONTENT_CLASS); + var nodes2 = query.findNodesHavingAttribute(node, this.CONTENT_ATTR); + + if (nodes2 && nodes2.length) { + var index; + for (index = 0; index < nodes2.length; index++) { + nodes1.push(nodes2[index]); + } + } + + if (query.hasNodeAttribute(node, this.CONTENT_ATTR)) { + nodes1.push(node); + } else if (query.hasNodeCssClass(node, this.CONTENT_CLASS)) { + nodes1.push(node); + } + + nodes1 = query.makeNodesUnique(nodes1); + + return nodes1; + }, + findParentContentNode: function (anyNode) + { + if (!anyNode) { + return; + } + + var node = anyNode; + var counter = 0; + + while (node && node !== documentAlias && node.parentNode) { + if (query.hasNodeAttribute(node, this.CONTENT_ATTR)) { + return node; + } + if (query.hasNodeCssClass(node, this.CONTENT_CLASS)) { + return node; + } + + node = node.parentNode; + + if (counter > 1000) { + break; // prevent loop, should not happen anyway but better we do this + } + counter++; + } + }, + findPieceNode: function (node) + { + var contentPiece; + + contentPiece = query.findFirstNodeHavingAttribute(node, this.CONTENT_PIECE_ATTR); + + if (!contentPiece) { + contentPiece = query.findFirstNodeHavingClass(node, this.CONTENT_PIECE_CLASS); + } + + if (contentPiece) { + return contentPiece; + } + + return node; + }, + findTargetNodeNoDefault: function (node) + { + if (!node) { + return; + } + + var target = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_TARGET_ATTR); + if (target) { + return target; + } + + target = query.findFirstNodeHavingAttribute(node, this.CONTENT_TARGET_ATTR); + if (target) { + return target; + } + + target = query.findFirstNodeHavingClass(node, this.CONTENT_TARGET_CLASS); + if (target) { + return target; + } + }, + findTargetNode: function (node) + { + var target = this.findTargetNodeNoDefault(node); + if (target) { + return target; + } + + return node; + }, + findContentName: function (node) + { + if (!node) { + return; + } + + var nameNode = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_NAME_ATTR); + + if (nameNode) { + return query.getAttributeValueFromNode(nameNode, this.CONTENT_NAME_ATTR); + } + + var contentPiece = this.findContentPiece(node); + if (contentPiece) { + return this.removeDomainIfIsInLink(contentPiece); + } + + if (query.hasNodeAttributeWithValue(node, 'title')) { + return query.getAttributeValueFromNode(node, 'title'); + } + + var clickUrlNode = this.findPieceNode(node); + + if (query.hasNodeAttributeWithValue(clickUrlNode, 'title')) { + return query.getAttributeValueFromNode(clickUrlNode, 'title'); + } + + var targetNode = this.findTargetNode(node); + + if (query.hasNodeAttributeWithValue(targetNode, 'title')) { + return query.getAttributeValueFromNode(targetNode, 'title'); + } + }, + findContentPiece: function (node) + { + if (!node) { + return; + } + + var nameNode = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_PIECE_ATTR); + + if (nameNode) { + return query.getAttributeValueFromNode(nameNode, this.CONTENT_PIECE_ATTR); + } + + var contentNode = this.findPieceNode(node); + + var media = this.findMediaUrlInNode(contentNode); + if (media) { + return media; + } + }, + findContentTarget: function (node) + { + if (!node) { + return; + } + + var targetNode = this.findTargetNode(node); + + if (query.hasNodeAttributeWithValue(targetNode, this.CONTENT_TARGET_ATTR)) { + return query.getAttributeValueFromNode(targetNode, this.CONTENT_TARGET_ATTR); + } + + var href; + if (query.hasNodeAttributeWithValue(targetNode, 'href')) { + href = query.getAttributeValueFromNode(targetNode, 'href'); + return this.toAbsoluteUrl(href); + } + + var contentNode = this.findPieceNode(node); + + if (query.hasNodeAttributeWithValue(contentNode, 'href')) { + href = query.getAttributeValueFromNode(contentNode, 'href'); + return this.toAbsoluteUrl(href); + } + }, + isSameDomain: function (url) + { + if (!url || !url.indexOf) { + return false; + } + + if (0 === url.indexOf(this.getLocation().origin)) { + return true; + } + + var posHost = url.indexOf(this.getLocation().host); + if (8 >= posHost && 0 <= posHost) { + return true; + } + + return false; + }, + removeDomainIfIsInLink: function (text) + { + // we will only remove if domain === location.origin meaning is not an outlink + var regexContainsProtocol = '^https?:\/\/[^\/]+'; + var regexReplaceDomain = '^.*\/\/[^\/]+'; + + if (text && + text.search && + -1 !== text.search(new RegExp(regexContainsProtocol)) + && this.isSameDomain(text)) { + + text = text.replace(new RegExp(regexReplaceDomain), ''); + if (!text) { + text = '/'; + } + } + + return text; + }, + findMediaUrlInNode: function (node) + { + if (!node) { + return; + } + + var mediaElements = ['img', 'embed', 'video', 'audio']; + var elementName = node.nodeName.toLowerCase(); + + if (-1 !== mediaElements.indexOf(elementName) && + query.findFirstNodeHavingAttributeWithValue(node, 'src')) { + + var sourceNode = query.findFirstNodeHavingAttributeWithValue(node, 'src'); + + return query.getAttributeValueFromNode(sourceNode, 'src'); + } + + if (elementName === 'object' && + query.hasNodeAttributeWithValue(node, 'data')) { + + return query.getAttributeValueFromNode(node, 'data'); + } + + if (elementName === 'object') { + var params = query.findNodesByTagName(node, 'param'); + if (params && params.length) { + var index; + for (index = 0; index < params.length; index++) { + if ('movie' === query.getAttributeValueFromNode(params[index], 'name') && + query.hasNodeAttributeWithValue(params[index], 'value')) { + + return query.getAttributeValueFromNode(params[index], 'value'); + } + } + } + + var embed = query.findNodesByTagName(node, 'embed'); + if (embed && embed.length) { + return this.findMediaUrlInNode(embed[0]); + } + } + }, + trim: function (text) + { + if (text && String(text) === text) { + return text.replace(/^\s+|\s+$/g, ''); + } + + return text; + }, + isOrWasNodeInViewport: function (node) + { + if (!node || !node.getBoundingClientRect || node.nodeType !== 1) { + return true; + } + + var rect = node.getBoundingClientRect(); + var html = documentAlias.documentElement || {}; + + var wasVisible = rect.top < 0; + if (wasVisible && node.offsetTop) { + wasVisible = (node.offsetTop + rect.height) > 0; + } + + return ( + (rect.bottom > 0 || wasVisible) && + rect.right > 0 && + rect.left < (windowAlias.innerWidth || html.clientWidth) && + ((rect.top < (windowAlias.innerHeight || html.clientHeight)) || wasVisible) // rect.top < 0 we assume user has seen all the ones that are above the current viewport + ); + }, + isNodeVisible: function (node) + { + var isItVisible = isVisible(node); + var isInViewport = this.isOrWasNodeInViewport(node); + return isItVisible && isInViewport; + }, + buildInteractionRequestParams: function (interaction, name, piece, target) + { + var params = ''; + + if (interaction) { + params += 'c_i='+ encodeWrapper(interaction); + } + if (name) { + if (params) { + params += '&'; + } + params += 'c_n='+ encodeWrapper(name); + } + if (piece) { + if (params) { + params += '&'; + } + params += 'c_p='+ encodeWrapper(piece); + } + if (target) { + if (params) { + params += '&'; + } + params += 'c_t='+ encodeWrapper(target); + } + + return params; + }, + buildImpressionRequestParams: function (name, piece, target) + { + var params = 'c_n=' + encodeWrapper(name) + + '&c_p=' + encodeWrapper(piece); + + if (target) { + params += '&c_t=' + encodeWrapper(target); + } + + return params; + }, + buildContentBlock: function (node) + { + if (!node) { + return; + } + + var name = this.findContentName(node); + var piece = this.findContentPiece(node); + var target = this.findContentTarget(node); + + name = this.trim(name); + piece = this.trim(piece); + target = this.trim(target); + + return { + name: name || 'Unknown', + piece: piece || 'Unknown', + target: target || '' + }; + }, + collectContent: function (contentNodes) + { + if (!contentNodes || !contentNodes.length) { + return []; + } + + var contents = []; + + var index, contentBlock; + for (index = 0; index < contentNodes.length; index++) { + contentBlock = this.buildContentBlock(contentNodes[index]); + if (isDefined(contentBlock)) { + contents.push(contentBlock); + } + } + + return contents; + }, + setLocation: function (location) + { + this.location = location; + }, + getLocation: function () + { + var locationAlias = this.location || windowAlias.location; + + if (!locationAlias.origin) { + locationAlias.origin = locationAlias.protocol + "//" + locationAlias.hostname + (locationAlias.port ? ':' + locationAlias.port: ''); + } + + return locationAlias; + }, + toAbsoluteUrl: function (url) + { + if ((!url || String(url) !== url) && url !== '') { + // we only handle strings + return url; + } + + if ('' === url) { + return this.getLocation().href; + } + + // Eg //example.com/test.jpg + if (url.search(/^\/\//) !== -1) { + return this.getLocation().protocol + url; + } + + // Eg http://example.com/test.jpg + if (url.search(/:\/\//) !== -1) { + return url; + } + + // Eg #test.jpg + if (0 === url.indexOf('#')) { + return this.getLocation().origin + this.getLocation().pathname + url; + } + + // Eg ?x=5 + if (0 === url.indexOf('?')) { + return this.getLocation().origin + this.getLocation().pathname + url; + } + + // Eg mailto:x@y.z tel:012345, ... market:... sms:..., javasript:... ecmascript: ... and many more + if (0 === url.search('^[a-zA-Z]{2,11}:')) { + return url; + } + + // Eg /test.jpg + if (url.search(/^\//) !== -1) { + return this.getLocation().origin + url; + } + + // Eg test.jpg + var regexMatchDir = '(.*\/)'; + var base = this.getLocation().origin + this.getLocation().pathname.match(new RegExp(regexMatchDir))[0]; + return base + url; + }, + isUrlToCurrentDomain: function (url) { + + var absoluteUrl = this.toAbsoluteUrl(url); + + if (!absoluteUrl) { + return false; + } + + var origin = this.getLocation().origin; + if (origin === absoluteUrl) { + return true; + } + + if (0 === String(absoluteUrl).indexOf(origin)) { + if (':' === String(absoluteUrl).substr(origin.length, 1)) { + return false; // url has port whereas origin has not => different URL + } + + return true; + } + + return false; + }, + setHrefAttribute: function (node, url) + { + if (!node || !url) { + return; + } + + query.setAnyAttribute(node, 'href', url); + }, + shouldIgnoreInteraction: function (targetNode) + { + var hasAttr = query.hasNodeAttribute(targetNode, this.CONTENT_IGNOREINTERACTION_ATTR); + var hasClass = query.hasNodeCssClass(targetNode, this.CONTENT_IGNOREINTERACTION_CLASS); + return hasAttr || hasClass; + } + }; + + /************************************************************ * Page Overlay ************************************************************/ @@ -1100,9 +2044,6 @@ if (typeof Piwik !== 'object') { // Site ID configTrackerSiteId = siteId || '', - // User ID - configUserId = '', - // Document URL configCustomUrl, @@ -1206,6 +2147,10 @@ if (typeof Piwik !== 'object') { // Browser features via client-side data collection browserFeatures = {}, + // Keeps track of previously tracked content impressions + trackedContentImpressions = [], + isTrackOnlyVisibleContentEnabled = false, + // Guard against installing the link tracker more than once per Tracker instance linkTrackingInstalled = false, @@ -1400,7 +2345,7 @@ if (typeof Piwik !== 'object') { function sendRequest(request, delay, callback) { var now = new Date(); - if (!configDoNotTrack) { + if (!configDoNotTrack && request) { if (configRequestMethod === 'POST') { sendXmlHttpRequest(request, callback); } else { @@ -1412,6 +2357,29 @@ if (typeof Piwik !== 'object') { } /* + * Send requests using bulk + */ + function sendBulkRequest(requests, delay) { + + if (configDoNotTrack) { + return; + } + + if (!requests || !requests.length) { + return; + } + + // here we have to prevent 414 request uri too long error in case someone tracks like 1000 + + var now = new Date(); + var bulk = '{"requests":["?' + requests.join('","?') + '"]}'; + + sendXmlHttpRequest(bulk); + + expireDateTime = now.getTime() + delay; + } + + /* * Get cookie name with prefix and domain hash */ function getCookieName(baseName) { @@ -1764,7 +2732,6 @@ if (typeof Piwik !== 'object') { '&h=' + now.getHours() + '&m=' + now.getMinutes() + '&s=' + now.getSeconds() + '&url=' + encodeWrapper(purify(currentUrl)) + (configReferrerUrl.length ? '&urlref=' + encodeWrapper(purify(configReferrerUrl)) : '') + - (configUserId.length ? '&uid=' + encodeWrapper(configUserId) : '') + '&_id=' + uuid + '&_idts=' + createTs + '&_idvc=' + visitCount + '&_idn=' + newVisitor + // currently unused (campaignNameDetected.length ? '&_rcn=' + encodeWrapper(campaignNameDetected) : '') + @@ -1982,18 +2949,478 @@ if (typeof Piwik !== 'object') { } /* + * Construct regular expression of classes + */ + function getClassesRegExp(configClasses, defaultClass) { + var i, + classesRegExp = '(^| )(piwik[_-]' + defaultClass; + + if (configClasses) { + for (i = 0; i < configClasses.length; i++) { + classesRegExp += '|' + configClasses[i]; + } + } + + classesRegExp += ')( |$)'; + + return new RegExp(classesRegExp); + } + + /* + * Link or Download? + */ + function getLinkType(className, href, isInLink) { + if (configTrackerUrl && href && 0 === String(href).indexOf(configTrackerUrl)) { + return 0; + } + + // does class indicate whether it is an (explicit/forced) outlink or a download? + var downloadPattern = getClassesRegExp(configDownloadClasses, 'download'), + linkPattern = getClassesRegExp(configLinkClasses, 'link'), + + // does file extension indicate that it is a download? + downloadExtensionsPattern = new RegExp('\\.(' + configDownloadExtensions + ')([?&#]|$)', 'i'); + + if (linkPattern.test(className)) { + return 'link'; + } + + if (downloadPattern.test(className) || downloadExtensionsPattern.test(href)) { + return 'download'; + } + + if (isInLink) { + return 0; + } + + return 'link'; + } + + function getSourceElement(sourceElement) + { + var parentElement; + + parentElement = sourceElement.parentNode; + while (parentElement !== null && + /* buggy IE5.5 */ + isDefined(parentElement)) { + + if (query.isLinkElement(sourceElement)) { + break; + } + sourceElement = parentElement; + parentElement = sourceElement.parentNode; + } + + return sourceElement; + } + + function getLinkIfShouldBeProcessed(sourceElement) + { + sourceElement = getSourceElement(sourceElement); + + if (!query.hasNodeAttribute(sourceElement, 'href')) { + return; + } + + if (!isDefined(sourceElement.href)) { + return; + } + + var href = query.getAttributeValueFromNode(sourceElement, 'href'); + + if (configTrackerUrl && href && 0 === String(href).indexOf(configTrackerUrl)) { + return; + } + + // browsers, such as Safari, don't downcase hostname and href + var originalSourceHostName = sourceElement.hostname || getHostName(sourceElement.href); + var sourceHostName = originalSourceHostName.toLowerCase(); + var sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName); + + // browsers, such as Safari, don't downcase hostname and href + var scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); + + if (!scriptProtocol.test(sourceHref)) { + // track outlinks and all downloads + var linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostName(sourceHostName)); + + if (linkType) { + return { + type: linkType, + href: sourceHref + }; + } + } + } + + function buildContentInteractionRequest(interaction, name, piece, target) + { + var params = content.buildInteractionRequestParams(interaction, name, piece, target); + + if (!params) { + return; + } + + return getRequest(params, null, 'contentInteraction'); + } + + function buildContentInteractionTrackingRedirectUrl(url, contentInteraction, contentName, contentPiece, contentTarget) + { + if (!isDefined(url)) { + return; + } + + if (url && configTrackerUrl && 0 === String(url).indexOf(configTrackerUrl)) { + return url; + } + + var redirectUrl = content.toAbsoluteUrl(url); + var request = 'redirecturl=' + redirectUrl + '&'; + request += buildContentInteractionRequest(contentInteraction, contentName, contentPiece, (contentTarget || url)); + + var separator = '&'; + if (configTrackerUrl.indexOf('?') < 0) { + separator = '?'; + } + + return configTrackerUrl + separator + request; + } + + function isNodeAuthorizedToTriggerInteraction(contentNode, interactedNode) + { + if (!contentNode || !interactedNode) { + return false; + } + + var targetNode = content.findTargetNode(contentNode); + + if (content.shouldIgnoreInteraction(targetNode)) { + // interaction should be ignored + return false; + } + + targetNode = content.findTargetNodeNoDefault(contentNode); + if (targetNode && !targetNode.contains(interactedNode)) { + /** + * There is a target node defined but the clicked element is not within the target node. example: + * <div data-track-content><a href="Y" data-content-target>Y</a><img src=""/><a href="Z">Z</a></div> + * + * The user clicked in this case on link Z and not on target Y + */ + return false; + } + + return true; + } + + function getContentInteractionToRequestIfPossible (anyNode, interaction, fallbackTarget) + { + if (!anyNode) { + return; + } + + var contentNode = content.findParentContentNode(anyNode); + + if (!contentNode) { + // we are not within a content block + return; + } + + if (!isNodeAuthorizedToTriggerInteraction(contentNode, anyNode)) { + return; + } + + var contentBlock = content.buildContentBlock(contentNode); + + if (!contentBlock) { + return; + } + + if (!contentBlock.target && fallbackTarget) { + contentBlock.target = fallbackTarget; + } + + return content.buildInteractionRequestParams(interaction, contentBlock.name, contentBlock.piece, contentBlock.target); + } + + function wasContentImpressionAlreadyTracked(contentBlock) + { + if (!trackedContentImpressions || !trackedContentImpressions.length) { + return false; + } + + var index, trackedContent; + + for (index = 0; index < trackedContentImpressions.length; index++) { + trackedContent = trackedContentImpressions[index]; + + if (trackedContent && + trackedContent.name === contentBlock.name && + trackedContent.piece === contentBlock.piece && + trackedContent.target === contentBlock.target) { + return true; + } + } + + return false; + } + + function replaceHrefIfInternalLink(contentBlock) + { + if (!contentBlock) { + return false; + } + + var targetNode = content.findTargetNode(contentBlock); + + if (!targetNode || content.shouldIgnoreInteraction(targetNode)) { + return false; + } + + var link = getLinkIfShouldBeProcessed(targetNode); + if (linkTrackingInstalled && link && link.type) { + + return false; // it is an outlink or download. + } + + if (query.isLinkElement(targetNode) && + query.hasNodeAttributeWithValue(targetNode, 'href')) { + var url = String(query.getAttributeValueFromNode(targetNode, 'href')); + + if (0 === url.indexOf('#')) { + return false; + } + + if (configTrackerUrl && 0 === url.indexOf(configTrackerUrl)) { + return true; + } + + if (!content.isUrlToCurrentDomain(url)) { + return false; + } + + var contentName = content.findContentName(contentBlock); + var contentPiece = content.findContentPiece(contentBlock); + var contentTarget = content.findContentTarget(contentBlock); + + var targetUrl = buildContentInteractionTrackingRedirectUrl(url, 'click', contentName, contentPiece, contentTarget); + + // make sure we still track the correct content target when an interaction is happening + if (!query.hasNodeAttributeWithValue(targetNode, content.CONTENT_TARGET_ATTR)) { + query.setAnyAttribute(targetNode, content.CONTENT_TARGET_ATTR, content.toAbsoluteUrl(url)); + } + // location.href does not respect target=_blank so we prefer to use this + content.setHrefAttribute(targetNode, targetUrl); + + return true; + } + + return false; + } + + function replaceHrefsIfInternalLink(contentNodes) + { + if (!contentNodes || !contentNodes.length) { + return; + } + + var index; + for (index = 0; index < contentNodes.length; index++) { + replaceHrefIfInternalLink(contentNodes[index]); + } + } + + function trackContentImpressionClickInteraction (targetNode) + { + return function (event) { + + if (!targetNode) { + return; + } + + var contentBlock = content.findParentContentNode(targetNode); + + var interactedElement; + if (event) { + interactedElement = event.target || event.srcElement; + } + if (!interactedElement) { + interactedElement = targetNode; + } + + if (!isNodeAuthorizedToTriggerInteraction(contentBlock, interactedElement)) { + return; + } + + var link = getLinkIfShouldBeProcessed(targetNode); + + if (linkTrackingInstalled && link && link.type) { + // click ignore, will be tracked via processClick, we do not want to track it twice + + return link.type; + } + + if (replaceHrefIfInternalLink(contentBlock)) { + return 'href'; + } + + var contentName = content.findContentName(contentBlock); + var contentPiece = content.findContentPiece(contentBlock); + var contentTarget = content.findContentTarget(contentBlock); + + // click on any non link element, or on a link element that has not an href attribute or on an anchor + var request = buildContentInteractionRequest('click', contentName, contentPiece, contentTarget); + sendRequest(request, configTrackerPause); + + return request; + }; + + } + + function setupInteractionsTracking(contentNodes) + { + if (!contentNodes || !contentNodes.length) { + return; + } + + var index, targetNode; + for (index = 0; index < contentNodes.length; index++) { + targetNode = content.findTargetNode(contentNodes[index]); + + if (targetNode && !targetNode.contentInteractionTrackingSetupDone) { + targetNode.contentInteractionTrackingSetupDone = true; + + addEventListener(targetNode, 'click', trackContentImpressionClickInteraction(targetNode)); + } + } + } + + /* + * Log all content pieces + */ + function buildContentImpressionsRequests(contents, contentNodes) + { + if (!contents || !contents.length) { + return []; + } + + var index, request; + + for (index = 0; index < contents.length; index++) { + + if (wasContentImpressionAlreadyTracked(contents[index])) { + contents.splice(index, 1); + index--; + } else { + trackedContentImpressions.push(contents[index]); + } + } + + if (!contents || !contents.length) { + return []; + } + + replaceHrefsIfInternalLink(contentNodes); + setupInteractionsTracking(contentNodes); + + var requests = []; + + for (index = 0; index < contents.length; index++) { + + request = getRequest( + content.buildImpressionRequestParams(contents[index].name, contents[index].piece, contents[index].target), + undefined, + 'contentImpressions' + ); + + requests.push(request); + } + + return requests; + } + + /* + * Log all content pieces + */ + function getContentImpressionsRequestsFromNodes(contentNodes) + { + var contents = content.collectContent(contentNodes); + + return buildContentImpressionsRequests(contents, contentNodes); + } + + /* + * Log currently visible content pieces + */ + function getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes) + { + if (!contentNodes || !contentNodes.length) { + return []; + } + + var index; + + for (index = 0; index < contentNodes.length; index++) { + if (!content.isNodeVisible(contentNodes[index])) { + contentNodes.splice(index, 1); + index--; + } + } + + if (!contentNodes || !contentNodes.length) { + return []; + } + + return getContentImpressionsRequestsFromNodes(contentNodes); + } + + function buildContentImpressionRequest(contentName, contentPiece, contentTarget) + { + var params = content.buildImpressionRequestParams(contentName, contentPiece, contentTarget); + + return getRequest(params, null, 'contentImpression'); + } + + function buildContentInteractionRequestNode(node, contentInteraction) + { + if (!node) { + return; + } + + var contentNode = content.findParentContentNode(node); + var contentBlock = content.buildContentBlock(contentNode); + + if (!contentBlock) { + return; + } + + if (!contentInteraction) { + contentInteraction = 'Unknown'; + } + + return buildContentInteractionRequest(contentInteraction, contentBlock.name, contentBlock.piece, contentBlock.target); + } + + function buildEventRequest(category, action, name, value) + { + return 'e_c=' + encodeWrapper(category) + + '&e_a=' + encodeWrapper(action) + + (isDefined(name) ? '&e_n=' + encodeWrapper(name) : '') + + (isDefined(value) ? '&e_v=' + encodeWrapper(value) : ''); + } + + /* * Log the event */ - function logEvent(category, action, name, value, customData) { + function logEvent(category, action, name, value, customData) + { // Category and Action are required parameters if (String(category).length === 0 || String(action).length === 0) { return false; } var request = getRequest( - 'e_c=' + encodeWrapper(category) - + '&e_a=' + encodeWrapper(action) - + (isDefined(name) ? '&e_n=' + encodeWrapper(name) : '') - + (isDefined(value) ? '&e_v=' + encodeWrapper(value) : ''), + buildEventRequest(category, action, name, value), customData, 'event' ); @@ -2024,8 +3451,17 @@ if (typeof Piwik !== 'object') { /* * Log the link or click with the server */ - function logLink(url, linkType, customData, callback) { - var request = getRequest(linkType + '=' + encodeWrapper(purify(url)), customData, 'link'); + function logLink(url, linkType, customData, callback, sourceElement) { + + var linkParams = linkType + '=' + encodeWrapper(purify(url)); + + var interaction = getContentInteractionToRequestIfPossible(sourceElement, 'click', url); + + if (interaction) { + linkParams += '&' + interaction; + } + + var request = getRequest(linkParams, customData, 'link'); sendRequest(request, (callback ? 0 : configTrackerPause), callback); } @@ -2083,77 +3519,46 @@ if (typeof Piwik !== 'object') { callback(); } - /* - * Construct regular expression of classes - */ - function getClassesRegExp(configClasses, defaultClass) { - var i, - classesRegExp = '(^| )(piwik[_-]' + defaultClass; - - if (configClasses) { - for (i = 0; i < configClasses.length; i++) { - classesRegExp += '|' + configClasses[i]; - } + function trackCallbackOnLoad(callback) + { + if (documentAlias.readyState === 'complete') { + callback(); + } else if (windowAlias.addEventListener) { + windowAlias.addEventListener('load', callback); + } else if (windowAlias.attachEvent) { + windowAlias.attachEvent('onLoad', callback); } - - classesRegExp += ')( |$)'; - - return new RegExp(classesRegExp); } - /* - * Link or Download? - */ - function getLinkType(className, href, isInLink) { - // does class indicate whether it is an (explicit/forced) outlink or a download? - var downloadPattern = getClassesRegExp(configDownloadClasses, 'download'), - linkPattern = getClassesRegExp(configLinkClasses, 'link'), + function trackCallbackOnReady(callback) + { + var loaded = false; - // does file extension indicate that it is a download? - downloadExtensionsPattern = new RegExp('\\.(' + configDownloadExtensions + ')([?&#]|$)', 'i'); + if (documentAlias.attachEvent) { + loaded = documentAlias.readyState === "complete"; + } else { + loaded = documentAlias.readyState !== "loading"; + } - // optimization of the if..elseif..else construct below - return linkPattern.test(className) ? 'link' : (downloadPattern.test(className) || downloadExtensionsPattern.test(href) ? 'download' : (isInLink ? 0 : 'link')); + if (loaded) { + callback(); + } else if (documentAlias.addEventListener) { + documentAlias.addEventListener('DOMContentLoaded', callback); + } else if (documentAlias.attachEvent) { + documentAlias.attachEvent('onreadystatechange', callback); + } } /* * Process clicks */ function processClick(sourceElement) { - var parentElement, - tag, - linkType; + var link = getLinkIfShouldBeProcessed(sourceElement); - parentElement = sourceElement.parentNode; - while (parentElement !== null && - /* buggy IE5.5 */ - isDefined(parentElement)) { - tag = sourceElement.tagName.toUpperCase(); - if (tag === 'A' || tag === 'AREA') { - break; - } - sourceElement = parentElement; - parentElement = sourceElement.parentNode; - } - - if (isDefined(sourceElement.href)) { - // browsers, such as Safari, don't downcase hostname and href - var originalSourceHostName = sourceElement.hostname || getHostName(sourceElement.href), - sourceHostName = originalSourceHostName.toLowerCase(), - sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName), - scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); - - // ignore script pseudo-protocol links - if (!scriptProtocol.test(sourceHref)) { - // track outlinks and all downloads - linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostName(sourceHostName)); - - if (linkType) { - // urldecode %xx - sourceHref = urldecode(sourceHref); - logLink(sourceHref, linkType); - } - } + if (link && link.type) { + // urldecode %xx + link.href = urldecode(link.href); + logLink(link.href, link.type, undefined, null, sourceElement); } } @@ -2223,6 +3628,72 @@ if (typeof Piwik !== 'object') { } } } + function enableTrackOnlyVisibleContent (checkOnSroll, timeIntervalInMs, tracker) { + + if (isTrackOnlyVisibleContentEnabled) { + // already enabled, do not register intervals again + return true; + } + + isTrackOnlyVisibleContentEnabled = true; + + var didScroll = false; + var events, index; + + function setDidScroll() { didScroll = true; } + + trackCallbackOnLoad(function () { + + function checkContent(intervalInMs) { + setTimeout(function () { + if (!isTrackOnlyVisibleContentEnabled) { + return; // the tests stopped tracking only visible content + } + didScroll = false; + tracker.trackVisibleContentImpressions(); + checkContent(intervalInMs); + }, intervalInMs); + } + + function checkContentIfDidScroll(intervalInMs) { + + setTimeout(function () { + if (!isTrackOnlyVisibleContentEnabled) { + return; // the tests stopped tracking only visible content + } + + if (didScroll) { + didScroll = false; + tracker.trackVisibleContentImpressions(); + } + + checkContentIfDidScroll(intervalInMs); + }, intervalInMs); + } + + if (checkOnSroll) { + + // scroll event is executed after each pixel, so we make sure not to + // execute event too often. otherwise FPS goes down a lot! + events = ['scroll', 'resize']; + for (index = 0; index < events.length; index++) { + if (documentAlias.addEventListener) { + documentAlias.addEventListener(events[index], setDidScroll); + } else { + windowAlias.attachEvent('on' + events[index], setDidScroll); + } + } + + checkContentIfDidScroll(100); + } + + if (timeIntervalInMs && timeIntervalInMs > 0) { + timeIntervalInMs = parseInt(timeIntervalInMs, 10); + checkContent(timeIntervalInMs); + } + + }); + } /* * Browser features (plugins, resolution, cookies) @@ -2338,6 +3809,50 @@ if (typeof Piwik !== 'object') { getHook: function (hookName) { return registeredHooks[hookName]; }, + getQuery: function () { + return query; + }, + getContent: function () { + return content; + }, + + buildContentImpressionRequest: buildContentImpressionRequest, + buildContentInteractionRequest: buildContentInteractionRequest, + buildContentInteractionRequestNode: buildContentInteractionRequestNode, + buildContentInteractionTrackingRedirectUrl: buildContentInteractionTrackingRedirectUrl, + getContentImpressionsRequestsFromNodes: getContentImpressionsRequestsFromNodes, + getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet: getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet, + trackCallbackOnLoad: trackCallbackOnLoad, + trackCallbackOnReady: trackCallbackOnReady, + buildContentImpressionsRequests: buildContentImpressionsRequests, + wasContentImpressionAlreadyTracked: wasContentImpressionAlreadyTracked, + appendContentInteractionToRequestIfPossible: getContentInteractionToRequestIfPossible, + setupInteractionsTracking: setupInteractionsTracking, + trackContentImpressionClickInteraction: trackContentImpressionClickInteraction, + internalIsNodeVisible: isVisible, + isNodeAuthorizedToTriggerInteraction: isNodeAuthorizedToTriggerInteraction, + replaceHrefIfInternalLink: replaceHrefIfInternalLink, + getConfigDownloadExtensions: function () { + return configDownloadExtensions; + }, + enableTrackOnlyVisibleContent: function (checkOnScroll, timeIntervalInMs) { + return enableTrackOnlyVisibleContent(checkOnScroll, timeIntervalInMs, this); + }, + clearTrackedContentImpressions: function () { + trackedContentImpressions = []; + }, + getTrackedContentImpressions: function () { + return trackedContentImpressions; + }, + getTrackerUrl: function () { + return configTrackerUrl; + }, + clearEnableTrackOnlyVisibleContent: function () { + isTrackOnlyVisibleContentEnabled = false; + }, + disableLinkTracking: function () { + linkTrackingInstalled = false; + }, /*</DEBUG>*/ /** @@ -2429,15 +3944,6 @@ if (typeof Piwik !== 'object') { }, /** - * Sets a User ID to this user (such as an email address or a username) - * - * @param string User ID - */ - setUserId: function (userId) { - configUserId = userId; - }, - - /** * Pass custom data to the server * * Examples: @@ -2452,7 +3958,7 @@ if (typeof Piwik !== 'object') { configCustomData = key_or_obj; } else { if (!configCustomData) { - configCustomData = []; + configCustomData = {}; } configCustomData[key_or_obj] = opt_value; } @@ -3045,6 +4551,8 @@ if (typeof Piwik !== 'object') { * @param mixed customData */ trackPageView: function (customTitle, customData) { + trackedContentImpressions = []; + if (isOverlaySession(configTrackerSiteId)) { trackCallback(function () { injectOverlayScripts(configTrackerUrl, configApiUrl, configTrackerSiteId); @@ -3056,6 +4564,101 @@ if (typeof Piwik !== 'object') { } }, + trackAllContentImpressions: function () { + trackCallback(function () { + trackCallbackOnReady(function () { + // we have to wait till DOM ready + var contentNodes = content.findContentNodes(); + + var requests = getContentImpressionsRequestsFromNodes(contentNodes); + sendBulkRequest(requests, configTrackerPause); + }); + }); + }, + + trackVisibleContentImpressions: function (checkOnSroll, timeIntervalInMs) { + + if (!isDefined(checkOnSroll)) { + checkOnSroll = true; + } + + if (!isDefined(timeIntervalInMs)) { + timeIntervalInMs = 750; + } + + enableTrackOnlyVisibleContent(checkOnSroll, timeIntervalInMs, this); + + trackCallback(function () { + trackCallbackOnLoad(function () { + // we have to wait till CSS parsed and applied + var contentNodes = content.findContentNodes(); + + var requests = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes); + sendBulkRequest(requests, configTrackerPause); + }); + }); + }, + + // it must be a node that is set to .piwikTrackContent or [data-track-content] or one of its parents nodes + trackContentImpression: function (contentName, contentPiece, contentTarget) { + if (!contentName) { + return; + } + + contentPiece = contentPiece || 'Unknown'; + + trackCallback(function () { + var request = buildContentImpressionRequest(contentName, contentPiece, contentTarget); + sendRequest(request, configTrackerPause); + }); + }, + + // it must be a node that is set to .piwikTrackContent or [data-track-content] or one of its parents nodes + // we might remove this method again + trackContentImpressionsWithinNode: function (domNode) { + trackCallback(function () { + if (isTrackOnlyVisibleContentEnabled) { + trackCallbackOnLoad(function () { + // we have to wait till CSS parsed and applied + var contentNodes = content.findContentNodesWithinNode(domNode); + + var requests = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes); + sendBulkRequest(requests, configTrackerPause); + }); + } else { + trackCallbackOnReady(function () { + // we have to wait till DOM ready + var contentNodes = content.findContentNodesWithinNode(domNode); + + var requests = getContentImpressionsRequestsFromNodes(contentNodes); + sendBulkRequest(requests, configTrackerPause); + }); + } + }); + }, + + // name and piece has to be same as previously used on an impression + trackContentInteraction: function (contentInteraction, contentName, contentPiece, contentTarget) { + if (!contentInteraction || !contentName) { + return; + } + + contentPiece = contentPiece || 'Unknown'; + + trackCallback(function () { + var request = buildContentInteractionRequest(contentInteraction, contentName, contentPiece, contentTarget); + sendRequest(request, configTrackerPause); + }); + }, + // it must be a node that is set to .piwikTrackContent or [data-track-content] or one of its parents nodes + // we might remove this method again + trackContentInteractionNode: function (domNode, contentInteraction) { + trackCallback(function () { + var request = buildContentInteractionRequestNode(domNode, contentInteraction); + sendRequest(request, configTrackerPause); + }); + }, + /** * Records an event * @@ -3083,7 +4686,6 @@ if (typeof Piwik !== 'object') { }); }, - /** * Used to record that the current page view is an item (product) page view, or a Ecommerce Category page view. * This must be called before trackPageView() on the product/category page. |