/*! * Matomo - free/libre analytics platform * * @link https://matomo.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ //----------------------------------------------------------------------------- // DataTable //----------------------------------------------------------------------------- (function ($, require) { var exports = require('piwik/UI'), UIControl = exports.UIControl; /** * This class contains the client side logic for viewing and interacting with * Piwik datatables. * * The id attribute for DataTables is set dynamically by the initNewDataTables * method, and this class instance is stored using the jQuery $.data function * with the 'uiControlObject' key. * * To find a datatable element by report (ie, 'DevicesDetection.getBrowsers'), * use piwik.DataTable.getDataTableByReport. * * To get the dataTable JS instance (an instance of this class) for a * datatable HTML element, use $(element).data('uiControlObject'). * * @constructor */ function DataTable(element) { UIControl.call(this, element); this.init(); } DataTable._footerIconHandlers = {}; DataTable.initNewDataTables = function () { $('div.dataTable').each(function () { if (!$(this).attr('id')) { var tableType = $(this).attr('data-table-type') || 'DataTable', klass = require('piwik/UI')[tableType] || require(tableType); if (klass && $.isFunction(klass)) { var table = new klass(this); } } }); }; DataTable.registerFooterIconHandler = function (id, handler) { var handlers = DataTable._footerIconHandlers; if (handlers[id]) { setTimeout(function () { // fail gracefully throw new Exception("DataTable footer icon handler '" + id + "' is already being used.") }, 1); return; } handlers[id] = handler; }; /** * Returns the first datatable div displaying a specific report. * * @param {string} report The report, eg, UserLanguage.getLanguage * @return {Element} The datatable div displaying the report, or undefined if * it cannot be found. */ DataTable.getDataTableByReport = function (report) { var result = undefined; $('div.dataTable').each(function () { if ($(this).attr('data-report') == report) { result = this; return false; } }); return result; }; $.extend(DataTable.prototype, UIControl.prototype, { _init: function (domElem) { // initialize your dataTable in your plugin }, _destroy: function() { UIControl.prototype._destroy.call(this); // remove handlers to avoid memory leaks if (this.windowResizeTableAttached) { $(window).off('resize', this._resizeDataTable); } if (this._bodyMouseUp) { $('body').off('mouseup', this._bodyMouseUp); } }, //initialisation function init: function () { var domElem = this.$element; this.workingDivId = this._createDivId(); domElem.attr('id', this.workingDivId); this.loadedSubDataTable = {}; this.isEmpty = $('.pk-emptyDataTable', domElem).length > 0; this.bindEventsAndApplyStyle(domElem); this._init(domElem); this.initialized = true; }, //function triggered when user click on column sort onClickSort: function (domElem) { var self = this; var newColumnToSort = $(domElem).attr('id'); // we lookup if the column to sort was already this one, if it is the case then we switch from desc <-> asc if (self.param.filter_sort_column == newColumnToSort) { // toggle the sorted order if (this.param.filter_sort_order == 'asc') { self.param.filter_sort_order = 'desc'; } else { self.param.filter_sort_order = 'asc'; } } self.param.filter_offset = 0; self.param.filter_sort_column = newColumnToSort; if (!self.isDashboard()) { self.notifyWidgetParametersChange(domElem, { filter_sort_column: newColumnToSort, filter_sort_order: self.param.filter_sort_order }); } self.reloadAjaxDataTable(); }, setGraphedColumn: function (columnName) { this.param.columns = columnName; }, isWithinDialog: function (domElem) { return !!$(domElem).parents('.ui-dialog').length; }, isDashboard: function () { return !!$('#dashboardWidgetsArea').length; }, getReportMetadata: function () { return JSON.parse(this.$element.attr('data-report-metadata') || '{}'); }, //Reset DataTable filters (used before a reload or view change) resetAllFilters: function () { var self = this; var FiltersToRestore = {}; var filters = [ 'filter_column', 'filter_pattern', 'filter_column_recursive', 'filter_pattern_recursive', 'enable_filter_excludelowpop', 'filter_offset', 'filter_limit', 'filter_sort_column', 'filter_sort_order', 'disable_generic_filters', 'columns', 'flat', 'totals', 'include_aggregate_rows', 'totalRows', 'pivotBy', 'pivotByColumn' ]; for (var key = 0; key < filters.length; key++) { var value = filters[key]; FiltersToRestore[value] = self.param[value]; delete self.param[value]; } return FiltersToRestore; }, //Restores the filters to the values given in the array in parameters restoreAllFilters: function (FiltersToRestore) { var self = this; for (var key in FiltersToRestore) { self.param[key] = FiltersToRestore[key]; } }, //Translate string parameters to javascript builtins //'true' -> true, 'false' -> false //it simplifies condition tests in the code cleanParams: function () { var self = this; for (var key in self.param) { if (self.param[key] == 'true') self.param[key] = true; if (self.param[key] == 'false') self.param[key] = false; } }, // Function called to trigger the AJAX request // The ajax request contains the function callback to trigger if the request is successful or failed // displayLoading = false When we don't want to display the Loading... DIV .loadingPiwik // for example when the script add a Loading... it self and doesn't want to display the generic Loading reloadAjaxDataTable: function (displayLoading, callbackSuccess, extraParams) { var self = this; if (typeof displayLoading == "undefined") { displayLoading = true; } if (typeof callbackSuccess == "undefined") { callbackSuccess = function (response) { self.dataTableLoaded(response, self.workingDivId); }; } if (displayLoading) { $('#' + self.workingDivId + ' .loadingPiwik').last().css('display', 'block'); } $('#loadingError').hide(); // when switching to display graphs, reset limit if (self && self.param && self.param.viewDataTable && String(self.param.viewDataTable).indexOf('graph') === 0) { delete self.param.filter_offset; delete self.param.filter_limit; } delete self.param.showtitle; var container = $('#' + self.workingDivId + ' .piwik-graph'); var ajaxRequest = new ajaxHelper(); if (self.param.totalRows) { ajaxRequest.addParams({'totalRows': self.param.totalRows}, 'post'); delete self.param.totalRows; } var params = {}; for (var key in self.param) { if (typeof self.param[key] != "undefined" && self.param[key] !== null && self.param[key] !== '') { if (key == 'filter_column' || key == 'filter_column_recursive' ) { // search in (metadata) `combinedLabel` when dimensions are shown separately in flattened tables // needs to be overwritten for each request as switching a searched table might return no results // otherwise, as search column doesn't fit anymore if (self.param.flat == "1" && self.param.show_dimensions == "1") { params[key] = 'combinedLabel'; } else { params[key] = 'label'; } continue; } params[key] = self.param[key]; } } ajaxRequest.addParams(params, 'get'); if (extraParams) { ajaxRequest.addParams(extraParams, 'post'); } ajaxRequest.withTokenInUrl(); ajaxRequest.setCallback( function (response) { container.trigger('piwikDestroyPlot'); container.off('piwikDestroyPlot'); callbackSuccess(response); } ); ajaxRequest.setErrorCallback(function (deferred, status) { if (status == 'abort' || !deferred || deferred.status < 400 || deferred.status >= 600) { return; } $('#' + self.workingDivId + ' .loadingPiwik').last().css('display', 'none'); $('#loadingError').show(); }); ajaxRequest.setFormat('html'); ajaxRequest.send(); }, // Function called when the AJAX request is successful // it looks for the ID of the response and replace the very same ID // in the current page with the AJAX response dataTableLoaded: function (response, workingDivId, doScroll) { var content = $(response); if ($.trim($('.dataTableControls', content).html()) === '') { $('.dataTableControls', content).append(' '); // fix table controls are not visible because there is no content. prevents limit selection being displayed // in the middle } var idToReplace = workingDivId || $(content).attr('id'); var dataTableSel = $('#' + idToReplace); // if the current dataTable is located inside another datatable table = $(content).parents('table.dataTable'); if (dataTableSel.parents('.dataTable').is('table')) { // we add class to the table so that we can give a different style to the subtable $(content).find('table.dataTable').addClass('subDataTable'); $(content).find('.dataTableFeatures').addClass('subDataTable'); //we force the initialisation of subdatatables dataTableSel.replaceWith(content); } else { dataTableSel.find('object').remove(); dataTableSel.replaceWith(content); } content.trigger('piwik:dataTableLoaded'); if (doScroll || 'undefined' === typeof doScroll) { piwikHelper.lazyScrollTo(content[0], 400); } piwikHelper.compileAngularComponents(content); return content; }, /* This method is triggered when a new DIV is loaded, which happens - at the first loading of the page - after any AJAX loading of a DataTable This method basically add features to the DataTable, - such as column sorting, searching in the rows, displaying Next / Previous links, etc. - add styles to the cells and rows (odd / even styles) - modify some rows to add images if a span img is found, or add a link if a span urlLink is found - bind new events onclick / hover / etc. to trigger AJAX requests, nice hovertip boxes for truncated cells */ bindEventsAndApplyStyle: function (domElem) { var self = this; self.cleanParams(); self.preBindEventsAndApplyStyleHook(domElem); self.handleSort(domElem); self.handleLimit(domElem); self.handlePeriod(domElem); self.handleOffsetInformation(domElem); self.handleAnnotationsButton(domElem); self.handleEvolutionAnnotations(domElem); self.handleExportBox(domElem); self.applyCosmetics(domElem); self.handleSubDataTable(domElem); self.handleConfigurationBox(domElem); self.handleSearchBox(domElem); self.handleColumnDocumentation(domElem); self.handleRowActions(domElem); self.handleCellTooltips(domElem); self.handleRelatedReports(domElem); self.handleTriggeredEvents(domElem); self.handleColumnHighlighting(domElem); self.setFixWidthToMakeEllipsisWork(domElem); self.handleSummaryRow(domElem); self.postBindEventsAndApplyStyleHook(domElem); }, preBindEventsAndApplyStyleHook: function (domElem) { }, postBindEventsAndApplyStyleHook: function (domElem) { }, isWidgetized: function () { return -1 !== location.search.indexOf('module=Widgetize'); }, setFixWidthToMakeEllipsisWork: function (domElem) { var self = this; function getTableWidth(domElem) { var totalWidth = $(domElem).width(); var totalWidthTable = $('table.dataTable', domElem).width(); // fixes tables in dbstats, referrers, ... if (totalWidthTable < totalWidth) { totalWidth = totalWidthTable; } if (!totalWidth) { totalWidth = 0; } return parseInt(totalWidth, 10); } function setMaxTableWidthIfNeeded (domElem, maxTableWidth) { var $domElem = $(domElem); var dataTableInCard = $domElem.parents('.card').first(); var parentDataTable = $domElem.parent('.dataTable'); dataTableInCard.width(''); $domElem.width(''); parentDataTable.width(''); var $table = $('table.dataTable', domElem); if ($table.closest('.reportsByDimensionView').length) { var requiredTableWidth = $table.width() - 40; // 40 is padding on card content if (domElem.width() > requiredTableWidth) { domElem.css('max-width', requiredTableWidth + 'px'); } } var tableWidth = getTableWidth(domElem); if (tableWidth <= maxTableWidth) { return; } if (self.isWidgetized() || self.isDashboard()) { return; } if (dataTableInCard && dataTableInCard.length) { // makes sure card has the same width dataTableInCard.width(maxTableWidth); } else { $domElem.width(maxTableWidth); } if (parentDataTable && parentDataTable.length) { // makes sure dataTableWrapper and DataTable has same size => makes sure maxLabelWidth does not get // applied in getLabelWidth() since they will have the same size. if (dataTableInCard.length) { dataTableInCard.width(maxTableWidth); } else { parentDataTable.width(maxTableWidth); } } } function getLabelWidth(domElem, tableWidth, minLabelWidth, maxLabelWidth) { var labelWidth = minLabelWidth; var columnsInFirstRow = $('tbody tr:not(.parentComparisonRow):not(.comparePeriod):eq(0) td:not(.label)', domElem); var widthOfAllColumns = 0; columnsInFirstRow.each(function (index, column) { widthOfAllColumns += $(column).outerWidth(); }); if (tableWidth - widthOfAllColumns >= minLabelWidth) { labelWidth = tableWidth - widthOfAllColumns; } else if (widthOfAllColumns >= tableWidth) { labelWidth = tableWidth * 0.5; } var innerWidth = 0; var innerWrapper = domElem.find('.dataTableWrapper'); if (innerWrapper && innerWrapper.length) { innerWidth = innerWrapper.width(); } if (labelWidth > maxLabelWidth && !self.isWidgetized() && innerWidth !== domElem.width() && !self.isDashboard() ) { labelWidth = maxLabelWidth; // prevent for instance table in Actions-Pages is not too wide } return parseInt(labelWidth / $('tr:nth-child(1) td.label', domElem).length, 10); } function getLabelColumnMinWidth(domElem) { var minWidth = 0; var minWidthHead = $('thead .first.label', domElem).css('minWidth'); if (minWidthHead) { minWidth = parseInt(minWidthHead, 10); } var minWidthBody = $('tbody tr:nth-child(1) td.label', domElem).css('minWidth'); if (minWidthBody) { minWidthBody = parseInt(minWidthBody, 10); if (minWidthBody && minWidthBody > minWidth) { minWidth = minWidthBody; } } return parseInt(minWidth, 10); } function getLabelColumnMaxWidth(domElem) { var maxWidth = 0; var maxWidthHead = $('thead .first.label', domElem).css('maxWidth'); if (maxWidthHead) { maxWidthHead = parseInt(maxWidthHead, 10); if (maxWidthHead > 0) { maxWidth = parseInt(maxWidthHead, 10); } } var maxWidthBody = $('tbody tr:nth-child(1) td.label', domElem).css('maxWidth'); if (maxWidthBody) { maxWidthBody = parseInt(maxWidthBody, 10); if (maxWidthBody && maxWidthBody > 0 && (maxWidth === 0 || maxWidthBody < maxWidth)) { maxWidth = maxWidthBody; } } return parseInt(maxWidth, 10); } function removePaddingFromWidth(elem, labelWidth) { var paddingLeft = elem.css('paddingLeft'); paddingLeft = paddingLeft ? Math.round(parseFloat(paddingLeft)) : 0; var paddingRight = elem.css('paddingRight'); paddingRight = paddingRight ? Math.round(parseFloat(paddingLeft)) : 0; labelWidth = labelWidth - paddingLeft - paddingRight; return labelWidth; } setMaxTableWidthIfNeeded(domElem, 1200); var isTableVisualization = this.jsViewDataTable && typeof this.jsViewDataTable === 'string' && typeof this.jsViewDataTable.indexOf === 'function' && this.jsViewDataTable.indexOf('table') !== -1; if (isTableVisualization) { // we do this only for html tables var tableWidth = getTableWidth(domElem); var labelColumnMinWidth = getLabelColumnMinWidth(domElem); var labelColumnMaxWidth = getLabelColumnMaxWidth(domElem); var labelColumnWidth = getLabelWidth(domElem, tableWidth, 125, 440); if (labelColumnMinWidth > labelColumnWidth) { labelColumnWidth = labelColumnMinWidth; } if (labelColumnMaxWidth && labelColumnMaxWidth < labelColumnWidth) { labelColumnWidth = labelColumnMaxWidth; } if (labelColumnWidth) { $('td.label', domElem).each(function() { $(this).width(removePaddingFromWidth($(this), labelColumnWidth)); }); } $('td span.label', domElem).each(function () { self.tooltip($(this)); }); } if (!self.windowResizeTableAttached) { self.windowResizeTableAttached = true; // on resize of the window we re-calculate everything. var timeout = null; var resizeDataTable = function() { if (timeout) { clearTimeout(timeout); } timeout = setTimeout(function () { var isInDom = domElem && domElem[0] && document && document.body && document.body.contains(domElem[0]); if (isInDom) { // as domElem might have been removed by now we check whether domElem actually still is in dom // and do this expensive operation only if needed. if (isTableVisualization) { $('td.label', domElem).width(''); } self.setFixWidthToMakeEllipsisWork(domElem); } else { $(window).off('resize', resizeDataTable); } timeout = null; }, Math.floor((Math.random() * 80) + 220)); // we randomize it just a little to not process all dataTables at similar time but to have a little // delay in between for smoother resizing. we want to do it between 300 and 400ms } $(window).on('resize', resizeDataTable); self._resizeDataTable = resizeDataTable; } }, handleLimit: function (domElem) { var tableRowLimits = this.props.datatable_row_limits || piwik.config.datatable_row_limits, evolutionLimits = { day: [8, 30, 60, 90, 180], week: [4, 12, 26, 52, 104], month: [3, 6, 12, 24, 36, 120], year: [3, 5, 10] }; // only allow big evolution limits for non flattened reports if (!parseInt(this.param.flat)) { evolutionLimits.day.push(365, 500); evolutionLimits.week.push(500); } var self = this; if (typeof self.parentId != "undefined" && self.parentId != '') { // no limit selector for subtables $('.limitSelection', domElem).remove(); return; } if (self.props.disable_all_rows_filter_limit) { // remove the -1 value from the limits array var tempTableRowLimits = []; tableRowLimits.forEach(function (limit) { if (limit != -1) { tempTableRowLimits.push(limit); } }); tableRowLimits = tempTableRowLimits; } // configure limit control var setLimitValue, numbers, limitParamName; if (self.param.viewDataTable == 'graphEvolution') { limitParamName = 'evolution_' + self.param.period + '_last_n'; numbers = evolutionLimits[self.param.period] || tableRowLimits; setLimitValue = function (params, limit) { params[limitParamName] = limit; }; } else { numbers = tableRowLimits; limitParamName = 'filter_limit'; setLimitValue = function (params, value) { params.filter_limit = value; params.filter_offset = 0; }; } function getFilterLimitAsString(limit) { if (limit == '-1') { return _pk_translate('General_All').toLowerCase(); } return limit; } // setup limit control var selectionMarkup = '
'; $('.limitSelection', domElem).append(selectionMarkup); var $limitSelect = $('.limitSelection select', domElem); if (!self.isEmpty) { $limitSelect.on('change', function (event) { var limit = $(this).val(); if (limit != self.param[limitParamName]) { setLimitValue(self.param, limit); self.reloadAjaxDataTable(); var data = {}; data[limitParamName] = self.param[limitParamName]; self.notifyWidgetParametersChange(domElem, data); } }); } else { $limitSelect.toggleClass('disabled'); } $limitSelect.material_select(); $('.limitSelection input', domElem).attr('title', _pk_translate('General_RowsToDisplay')); } else { $('.limitSelection', domElem).hide(); } }, handlePeriod: function (domElem) { var $periodSelect = $('.dataTablePeriods .tableIcon', domElem); var self = this; $periodSelect.click(function () { var period = $(this).attr('data-period'); if (!period || period == self.param['period']) { return; } var piwikPeriods = piwikHelper.getAngularDependency('piwikPeriods'); var currentPeriod = piwikPeriods.parse(self.param['period'], self.param['date']); var endDateOfPeriod = currentPeriod.getDateRange()[1]; endDateOfPeriod = piwikPeriods.format(endDateOfPeriod); self.param['period'] = period; self.param['date'] = endDateOfPeriod; self.reloadAjaxDataTable(); }); }, // if sorting the columns is enabled, when clicking on a column, // - if this column was already the one used for sorting, we revert the order desc<->asc // - we send the ajax request with the new sorting information handleSort: function (domElem) { var self = this; if (self.props.enable_sort) { $('.sortable', domElem).off('click.dataTableSort').on('click.dataTableSort', function () { $(this).off('click.dataTableSort'); self.onClickSort(this); } ); } if (self.param.filter_sort_column) { // are we in a subdatatable? var currentIsSubDataTable = $(domElem).parent().hasClass('cellSubDataTable'); var imageSortClassType = currentIsSubDataTable ? 'sortSubtable' : '' var imageSortWidth = 16; var imageSortHeight = 16; var sortOrder = self.param.filter_sort_order || 'desc'; // we change the style of the column currently used as sort column // adding an image and the class columnSorted to the TD var head = $('th', domElem).filter(function () { return $(this).attr('id') == self.param.filter_sort_column; }).addClass('columnSorted'); var sortIconHtml = ''; var div = head.find('.thDIV'); if (head.hasClass('first') || head.attr('id') == 'label') { div.append(sortIconHtml); } else { div.prepend(sortIconHtml); } } }, //behaviour for the DataTable 'search box' handleSearchBox: function (domElem, callbackSuccess) { var self = this; var currentPattern = self.param.filter_pattern; if (typeof self.param.filter_pattern != "undefined" && self.param.filter_pattern.length > 0) { currentPattern = self.param.filter_pattern; } else if (typeof self.param.filter_pattern_recursive != "undefined" && self.param.filter_pattern_recursive.length > 0) { currentPattern = self.param.filter_pattern_recursive; } else { currentPattern = ''; } currentPattern = piwikHelper.htmlDecode(currentPattern); var patternsToReplace = [{from: '?', to: '\\?'}, {from: '+', to: '\\+'}, {from: '*', to: '\\*'}] $.each(patternsToReplace, function (index, pattern) { if (0 === currentPattern.indexOf(pattern.to)) { currentPattern = pattern.from + currentPattern.substr(2); } }); var $searchAction = $('.dataTableAction.searchAction', domElem); if (!$searchAction.length) { return; } $searchAction.on('click', showSearch); $searchAction.find('.icon-close').on('click', hideSearch); var $searchInput = $('.dataTableSearchInput', domElem); function getOptimalWidthForSearchField() { var controlBarWidth = $('.dataTableControls', domElem).width(); var spaceLeft = controlBarWidth - $searchAction.position().left; var idealWidthForSearchBar = 250; var minimalWidthForSearchBar = 150; // if it's only 150 pixel we still show it on same line var width = idealWidthForSearchBar; if (spaceLeft > minimalWidthForSearchBar && spaceLeft < idealWidthForSearchBar) { width = spaceLeft; } if (width > controlBarWidth) { width = controlBarWidth; } return width; } function hideSearch(event) { event.preventDefault(); event.stopPropagation(); var $searchAction = $(this).parents('.searchAction').first(); $searchAction.removeClass('searchActive active forceActionVisible'); $searchAction.css('width', ''); $searchAction.on('click', showSearch); $searchAction.find('.icon-search').off('click', searchForPattern); $searchInput.val(''); if (currentPattern) { // we search for this pattern so if there was a search term before, and someone closes the search // we show all results again searchForPattern(); } } function showSearch(event) { event.preventDefault(); event.stopPropagation(); var $searchAction = $(this); $searchAction.addClass('searchActive forceActionVisible'); var width = getOptimalWidthForSearchField(); $searchAction.css('width', width + 'px'); $searchAction.find('.dataTableSearchInput').focus(); $searchAction.find('.icon-search').on('click', searchForPattern); $searchAction.off('click', showSearch); } function searchForPattern() { var keyword = $searchInput.val(); if (!keyword && !currentPattern) { // we search only if a keyword is actually given, or if no keyword is given and a search was performed // before (in this case we want to clear the search basically.) return; } self.param.filter_offset = 0; $.each(patternsToReplace, function (index, pattern) { if (0 === keyword.indexOf(pattern.from)) { keyword = pattern.to + keyword.substr(1); } }); if (self.param.search_recursive) { self.param.filter_column_recursive = 'label'; self.param.filter_pattern_recursive = keyword; } else { self.param.filter_column = 'label'; self.param.filter_pattern = keyword; } delete self.param.totalRows; self.reloadAjaxDataTable(true, callbackSuccess); } $searchInput.on("keyup", function (e) { if (isEnterKey(e)) { searchForPattern(); } else if (isEscapeKey(e)) { $searchAction.find('.icon-close').click(); } }); if (currentPattern) { $searchInput.val(currentPattern); $searchAction.click(); } if (this.isEmpty && !currentPattern) { $searchAction.css({display: 'none'}); } }, //behaviour for '< prev' 'next >' links and page count handleOffsetInformation: function (domElem) { var self = this; $('.dataTablePages', domElem).each( function () { var offset = 1 + Number(self.param.filter_offset); var offsetEnd = Number(self.param.filter_offset) + Number(self.param.filter_limit); var totalRows = Number(self.param.totalRows); var offsetEndDisp = offsetEnd; if (self.param.keep_summary_row == 1) --totalRows; if (offsetEnd > totalRows || Number(self.param.filter_limit) == -1) offsetEndDisp = totalRows; // only show this string if there is some rows in the datatable if (totalRows != 0) { var str = sprintf(_pk_translate('General_Pagination'), offset, offsetEndDisp, totalRows); $(this).text(str); } else { $(this).hide(); } } ); var $next = $('.dataTableNext', domElem); // Display the next link if the total Rows is greater than the current end row $next.each(function () { var offsetEnd = Number(self.param.filter_offset) + Number(self.param.filter_limit); var totalRows = Number(self.param.totalRows); if (self.param.keep_summary_row == 1) --totalRows; if (offsetEnd < totalRows) { $(this).css('visibility', 'visible'); } }); // bind the click event to trigger the ajax request with the new offset $next.off('click'); $next.click(function () { $(this).off('click'); self.param.filter_offset = Number(self.param.filter_offset) + Number(self.param.filter_limit); self.reloadAjaxDataTable(); }); var $prev = $('.dataTablePrevious', domElem); // Display the previous link if the current offset is not zero $prev.each(function () { var offset = 1 + Number(self.param.filter_offset); if (offset != 1) { $(this).css('visibility', 'visible'); } }); // bind the click event to trigger the ajax request with the new offset // take care of the negative offset, we setup 0 $prev.off('click'); $prev.click(function () { $(this).off('click'); var offset = Number(self.param.filter_offset) - Number(self.param.filter_limit); if (offset < 0) { offset = 0; } self.param.filter_offset = offset; self.param.previous = 1; self.reloadAjaxDataTable(); }); }, handleEvolutionAnnotations: function (domElem) { var self = this; if (self.param.viewDataTable == 'graphEvolution' && $('.annotationView', domElem).length > 0) { // get dates w/ annotations across evolution period (have to do it through AJAX since we // determine placement using the elements created by jqplot) $('.dataTableFeatures', domElem).addClass('hasEvolution'); piwik.annotations.api.getEvolutionIcons( self.param.idSite, self.param.date, self.param.period, self.param['evolution_' + self.param.period + '_last_n'], function (response) { var annotations = $(response), datatableFeatures = $('.dataTableFeatures', domElem), noteSize = 16, annotationAxisHeight = 30 // css height + padding + margin ; var annotationsCss = {left: 6}; // padding-left of .jqplot-graph element (in _dataTableViz_jqplotGraph.tpl) // set position of evolution annotation icons annotations.css(annotationsCss); piwik.annotations.placeEvolutionIcons(annotations, domElem); // add new section under axis annotations.insertBefore($('.dataTableFooterNavigation', domElem)); // reposition annotation icons every time the graph is resized $('.piwik-graph', domElem).on('resizeGraph', function () { piwik.annotations.placeEvolutionIcons(annotations, domElem); }); // on hover of x-axis, show note icon over correct part of x-axis datatableFeatures.on('mouseenter', '.evolution-annotations>span', function () { $(this).css('opacity', 1); }); datatableFeatures.on('mouseleave', '.evolution-annotations>span', function () { if ($(this).attr('data-count') == 0) // only hide if there are no annotations for this note { $(this).css('opacity', 0); } }); // when clicking an annotation, show the annotation viewer for that period datatableFeatures.on('click', '.evolution-annotations>span', function () { var spanSelf = $(this), date = spanSelf.attr('data-date'), oldDate = $('.annotation-manager', domElem).attr('data-date'); if (date) { var period = self.param.period; if (period == 'range') { period = 'day'; } piwik.annotations.showAnnotationViewer( domElem, self.param.idSite, date, period, undefined, // lastN function (manager) { manager.attr('data-is-range', 0); $('.annotationView', domElem) .attr('title', _pk_translate('Annotations_IconDesc')); var viewAndAdd = _pk_translate('Annotations_ViewAndAddAnnotations'), hideNotes = _pk_translate('Annotations_HideAnnotationsFor'); // change the tooltip of the previously clicked evolution icon (if any) if (oldDate) { $('span', annotations).each(function () { if ($(this).attr('data-date') == oldDate) { $(this).attr('title', sprintf(viewAndAdd, oldDate)); return false; } }); } // change the tooltip of the clicked evolution icon if (manager.is(':hidden')) { spanSelf.attr('title', sprintf(viewAndAdd, date)); } else { spanSelf.attr('title', sprintf(hideNotes, date)); } } ); } }); // when hover over annotation in annotation manager, highlight the annotation // icon var runningAnimation = null; domElem.on('mouseenter', '.annotation', function (e) { var date = $(this).attr('data-date'); // find the icon for this annotation var icon = $(); $('span', annotations).each(function () { if ($(this).attr('data-date') == date) { icon = $('img', this); return false; } }); if (icon[0] == runningAnimation) // if the animation is already running, do nothing { return; } // stop ongoing animations $('span', annotations).each(function () { $('img', this).removeAttr('style'); }); // start a bounce animation icon.effect("bounce", {times: 1, distance: 10}, 1000); runningAnimation = icon[0]; }); // reset running animation item when leaving annotations list domElem.on('mouseleave', '.annotations', function (e) { runningAnimation = null; }); self.$element.trigger('piwik:annotationsLoaded'); } ); } }, handleAnnotationsButton: function (domElem) { var self = this; if (self.param.idSubtable) // no annotations for subtables, just whole reports { return; } // show the annotations view on click $('.annotationView', domElem).click(function () { var annotationManager = $('.annotation-manager', domElem); if (annotationManager.length > 0 && annotationManager.attr('data-is-range') == 1) { if (annotationManager.is(':hidden')) { annotationManager.slideDown('slow'); // showing $(this).attr('title', _pk_translate('Annotations_IconDescHideNotes')); } else { annotationManager.slideUp('slow'); // hiding $(this).attr('title', _pk_translate('Annotations_IconDesc')); } } else { // show the annotation viewer for the whole date range var lastN = self.param['evolution_' + self.param.period + '_last_n']; piwik.annotations.showAnnotationViewer( domElem, self.param.idSite, self.param.date, self.param.period, lastN, function (manager) { manager.attr('data-is-range', 1); } ); // change the tooltip of the view annotation icon $(this).attr('title', _pk_translate('Annotations_IconDescHideNotes')); } }); }, // DataTable view box (simple table, all columns table, Goals table, pie graph, tag cloud, graph, ...) handleExportBox: function (domElem) { var self = this; if (self.param.idSubtable) { // no view box for subtables return; } //footer arrow position element name self.jsViewDataTable = self.param.viewDataTable; $('.tableAllColumnsSwitch a', domElem).show(); $('.dataTableFooterIcons .tableIcon', domElem).click(function () { var id = $(this).attr('data-footer-icon-id'); if (!id) { return; } var handler = DataTable._footerIconHandlers[id]; if (!handler) { handler = DataTable._footerIconHandlers['table']; } handler(self, id); }); //Graph icon Collapsed functionality self.currentGraphViewIcon = 0; self.graphViewEnabled = 0; self.graphViewStartingThreads = 0; self.graphViewStartingKeep = false; //show keep flag }, handleConfigurationBox: function (domElem, callbackSuccess) { var self = this; if (typeof self.parentId != "undefined" && self.parentId != '') { // no manipulation when loading subtables return; } if ((typeof self.numberOfSubtables == 'undefined' || self.numberOfSubtables == 0) && (typeof self.param.flat == 'undefined' || self.param.flat != 1)) { // if there are no subtables, remove the flatten action $('.dataTableFlatten', domElem).parent().remove(); } var ul = $('ul.tableConfiguration', domElem); function hideConfigurationIcon() { // hide the icon when there are no actions available or we're not in a table view $('.dropdownConfigureIcon', domElem).remove(); } if (!ul.find('li').length) { hideConfigurationIcon(); return; } var icon = $('a.dropdownConfigureIcon', domElem); var iconHighlighted = false; var generateClickCallback = function (paramName, callbackAfterToggle, setParamCallback) { return function () { if (setParamCallback) { var data = setParamCallback(); } else { self.param[paramName] = (1 - self.param[paramName]) + ''; var data = {}; } self.param.filter_offset = 0; delete self.param.totalRows; if (callbackAfterToggle) callbackAfterToggle(); self.reloadAjaxDataTable(true, callbackSuccess); data[paramName] = self.param[paramName]; self.notifyWidgetParametersChange(domElem, data); }; }; var getText = function (text, addDefault, replacement) { if (/(%(.\$)?s+)/g.test(_pk_translate(text))) { var values = ['