/*! * Piwik - free/libre analytics platform * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ function _pk_translate(translationStringId, values) { if( typeof(piwik_translations[translationStringId]) != 'undefined' ){ var translation = piwik_translations[translationStringId]; if (typeof values != 'undefined' && values && values.length) { values.unshift(translation); return sprintf.apply(null, values); } return translation; } return "The string "+translationStringId+" was not loaded in javascript. Make sure it is added in the Translate.getClientSideTranslationKeys hook."; } var piwikHelper = { htmlDecode: function(value) { var textArea = document.createElement('textarea'); textArea.innerHTML = value; return textArea.value; }, sendContentAsDownload: function (filename, content, mimeType) { if (!mimeType) { mimeType = 'text/plain'; } function downloadFile(content) { var node = document.createElement('a'); node.style.display = 'none'; if ('string' === typeof content) { node.setAttribute('href', 'data:' + mimeType + ';charset=utf-8,' + encodeURIComponent(content)); } else { node.href = window.URL.createObjectURL(blob); } node.setAttribute('download', filename); document.body.appendChild(node); node.click(); document.body.removeChild(node); } var node; if ('function' === typeof Blob) { // browser supports blob try { var blob = new Blob([content], {type: mimeType}); if (window.navigator.msSaveOrOpenBlob) { window.navigator.msSaveBlob(blob, filename); return; } else { downloadFile(blob); return; } } catch (e) { downloadFile(content); } } downloadFile(content); }, /** * a nice cross-browser logging function */ log: function() { try { console.log.apply(console, arguments); // Firefox, Chrome } catch (e) { try { opera.postError.apply(opera, arguments); // Opera } catch (f) { // don't alert as log is not considered to be important enough // (as opposed to piwikHelper.error) //alert(Array.prototype.join.call(arguments, ' ')); // MSIE } } }, error: function() { try { console.error.apply(console, arguments); // Firefox, Chrome } catch (e) { try { opera.postError.apply(opera, arguments); // Opera } catch (f) { alert(Array.prototype.join.call(arguments, ' ')); // MSIE } } }, htmlEntities: function(value) { if (!value) { return value; } var findReplace = [[/&/g, "&"], [//g, ">"], [/"/g, """], [/{{/g, '{⁣{']]; for(var item in findReplace) { value = value.replace(findReplace[item][0], findReplace[item][1]); } return value; }, escape: function (value) { var escape = angular.element(document).injector().get('$sanitize'); return escape(value); }, /** * Add break points to a string so that it can be displayed more compactly */ addBreakpoints: function(text, breakpointMarkup) { return text.replace(/([\/&=?\.%#:_-])/g, '$1' + (typeof breakpointMarkup == 'undefined' ? '​' : breakpointMarkup)); // ​ is for internet explorer }, /** * Add breakpoints to a URL * urldecodes and encodes htmlentities to display utf8 urls without XSS vulnerabilities */ addBreakpointsToUrl: function(url) { try { url = decodeURIComponent(url); } catch (e) { // might throw "URI malformed" } url = piwikHelper.addBreakpoints(url, '|||'); url = $(document.createElement('p')).text(url).html(); url = url.replace(/\|\|\|/g, '​'); // ​ is for internet explorer return url; }, getAngularDependency: function (dependency) { return angular.element(document).injector().get(dependency); }, /** * As we still have a lot of old jQuery code and copy html from node to node we sometimes have to trigger the * compiling of angular components manually. * * @param selector * @param {object} options * @param {object} options.scope if supplied, the given scope will be used when compiling the template. Shouldn't * be a plain object but an actual angular scope. * @param {object} options.params if supplied, the properties in this object are * added to the new scope. */ compileAngularComponents: function (selector, options) { options = options || {}; var $element = $(selector); if (!$element.length) { return; } angular.element(document).injector().invoke(function($compile, $rootScope) { var scope = null; if (options.scope) { scope = options.scope; } else if (!options.forceNewScope) { // TODO: docs scope = angular.element($element).scope(); } if (!scope) { scope = $rootScope.$new(true); } if (options.params) { $.extend(scope, options.params); } $compile($element)(scope); }); }, /** * Detection works currently only for directives defining an isolated scope. Functionality might need to be * extended if needed. Under circumstances you might call this method before calling compileAngularComponents() * to avoid compiling the same element twice. * @param selector */ isAlreadyCompiledAngularComponent: function (selector) { var $element = $(selector); return ($element.length && $element.hasClass('ng-isolate-scope')); }, /** * Detects whether angular is rendering the page. If so, the page will be reloaded automatically * via angular as soon as it detects a $locationChange * * @returns {number|jQuery} */ isAngularRenderingThePage: function () { return $('[piwik-reporting-page]').length; }, /** * Moves an element further to the left by changing the left margin to make sure as much as possible of an element * is visible in the current viewport. The top position keeps unchanged. * @param elementToPosition */ setMarginLeftToBeInViewport: function (elementToPosition) { var availableWidth = $(window).width(); $(elementToPosition).css('marginLeft', '0px'); var offset = $(elementToPosition).offset(); if (!offset) { return; } var leftPos = offset.left; if (leftPos < 0) { leftPos = 0; } var widthSegmentForm = $(elementToPosition).outerWidth(); if (leftPos + widthSegmentForm > availableWidth) { var extraSpaceForMoreBeauty = 16; var newLeft = availableWidth - widthSegmentForm - extraSpaceForMoreBeauty; if (newLeft < extraSpaceForMoreBeauty) { newLeft = extraSpaceForMoreBeauty; } var marginLeft = Math.abs(leftPos - newLeft); if (marginLeft > extraSpaceForMoreBeauty) { // we only move it further to the left if it is actually more than 16px to the left. // otherwise it is not really worth it and doesn't look as good. $(elementToPosition).css('marginLeft', (parseInt(marginLeft, 10) * -1) + 'px'); } } }, /** * Displays a Modal dialog. Text will be taken from the DOM node domSelector. * Given callback handles will be mapped to the buttons having a role attribute * * Dialog will be closed when a button is clicked and callback handle will be * called, if one was given for the clicked role * * @param {string} domSelector domSelector for modal window * @param {object} handles callback functions for available roles * @param {object} options options for modal * @return {void} */ modalConfirm: function(domSelector, handles, options) { if (!options) { options = {}; } var domElem = $(domSelector); var buttons = []; var content = ''; var $content = $(content).hide(); var $footer = $content.find('.modal-footer'); $('[role]', domElem).each(function(){ var $button = $(this); var role = $button.attr('role'); var title = $button.attr('title'); var text = $button.val(); $button.hide(); var button = $(''); button.text(text); if (title) { button.attr('title', title); } if (typeof handles !== 'undefined' && typeof handles[role] == 'function') { button.on('click', function(){ handles[role].apply() }); } if (typeof $button.data('href') !== 'undefined') { button.on('click', function () { window.location.href = $button.data('href'); }) } $footer.append(button); }); $('body').append($content); $content.find('.modal-content').append(domElem); if (options && options.fixedFooter) { $content.addClass('modal-fixed-footer'); delete options.fixedFooter; } if (options && options.extraWide) { // if given, the modal will be shown larger than usual and almost consume the full width. $content.addClass('modal-extra-wide'); delete options.extraWide; } if (options && !options.ready) { options.ready = function () { $(".modal.open a").focus(); var modalContent = $(".modal.open"); if (modalContent && modalContent[0]) { // make sure to scroll to the top of the content modalContent[0].scrollTop = 0; } }; } domElem.show(); $content.openModal(options); }, getQueryStringWithParametersModified: function (queryString, newParameters) { if (queryString != '') { var r, i, keyvalue, keysvalues = newParameters.split('&'); var appendUrl = ''; for (i = 0; i < keysvalues.length; i++) { keyvalue = keysvalues[i].split('='); r = new RegExp('(^|[?&])' + keyvalue[0] + '=[^&]*'); queryString = queryString.replace(r, ''); // empty value, eg. &segment=, we remove the parameter from URL entirely if (keyvalue[1].length == 0) { continue; } appendUrl += '&' + keyvalue[0] + '=' + keyvalue[1]; } queryString += appendUrl; if (queryString[0] == '&') { queryString = '?' + queryString.substring(1); } } else { queryString = '?' + newParameters; } return queryString; }, /** * Returns the current query string with the given parameters modified * @param {String} newparams parameters to be modified * @return {String} */ getCurrentQueryStringWithParametersModified: function(newparams) { var queryString = String(window.location.search); if (newparams) { queryString = this.getQueryStringWithParametersModified(queryString, newparams); } return String(window.location.pathname) + queryString; }, /** * Given param1=v1¶m2=k2 * returns: { "param1": "v1", "param2": "v2" } * * @param query string * @return {Object} */ getArrayFromQueryString: function (query) { var params = {}; var vars = query.split("&"); for (var i=0;i $(window).scrollTop()+$(window).height() || forceScroll ) { // scroll the page smoothly to the graph $.scrollTo(elem, time); } }, /** * Returns the filtered/converted content of a textarea to be used for api requests * @param {string} textareaContent * @return {string} */ getApiFormatTextarea: function (textareaContent) { if (typeof textareaContent == 'undefined') { return ''; } return textareaContent.trim().split("\n").join(','); }, shortcuts: {}, /** * Register a shortcut * * @param {string} key key-stroke to be registered for this shortcut * @param {string } description description to be shown in summary * @param callback method called when pressing the key */ registerShortcut: function(key, description, callback) { piwikHelper.shortcuts[key] = description; Mousetrap.bind(key, callback); }, calculateEvolution: function (currentValue, pastValue) { var dividend = currentValue - pastValue; var divisor = pastValue; if (dividend == 0) { return 0; } else if (divisor == 0) { return 1; } else { return Math.round((dividend / divisor) * 1000) / 1000; } } }; if (typeof String.prototype.trim !== 'function') { String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g,""); }; } /** * Returns true if the event keypress passed in parameter is the ENTER key * @param {Event} e current window event * @return {boolean} */ function isEnterKey(e) { return (window.event?window.event.keyCode:e.which)==13; } /** * Returns true if the event keypress passed in parameter is the ESCAPE key * @param {Event} e current window event * @return {boolean} */ function isEscapeKey(e) { return (window.event?window.event.keyCode:e.which)==27; } // workarounds (function($){ try { // this code is not vital, so we make sure any errors are ignored //-------------------------------------- // // monkey patch that works around bug in arc function of some browsers where // nothing gets drawn if angles are 2 * PI apart and in counter-clockwise direction. // affects some versions of chrome & IE 8 // //-------------------------------------- var oldArc = CanvasRenderingContext2D.prototype.arc; CanvasRenderingContext2D.prototype.arc = function(x, y, r, sAngle, eAngle, clockwise) { if (Math.abs(eAngle - sAngle - Math.PI * 2) < 0.000001 && !clockwise) eAngle -= 0.000001; oldArc.call(this, x, y, r, sAngle, eAngle, clockwise); }; // Fix jQuery UI dialogs scrolling when click on links with tooltips jQuery.ui.dialog.prototype._focusTabbable = $.noop; // Fix jQuery UI tooltip displaying when dialog is closed by Esc key jQuery(document).keyup(function(e) { if (e.keyCode == 27) { $('.ui-tooltip').hide(); } }); } catch (e) {} }(jQuery));