/*! * Piwik - Web Analytics * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ //----------------------------------------------------------------------------- // DataTable //----------------------------------------------------------------------------- /** * DataTable * @constructor */ function dataTable() { this.param = {}; } /** * This class contains the client side logic for viewing and interacting with * Piwik datatables. * * The id attribute for DataTables is set dynamically by the DataTableManager * class, and this class instance is stored using the jQuery $.data function * with the 'piwikDataTable' key. * * To find a datatable element by report (ie, 'UserSettings.getBrowser'), * use piwik.DataTableManager.getDataTableByReport. * * To get the dataTable JS instance (an instance of this class) for a * datatable HTML element, use $(element).data('piwikDataTable'). */ dataTable.prototype = { //initialisation function init: function (workingDivId, domElem) { if (typeof domElem == "undefined") { domElem = $('#' + workingDivId); } this.workingDivId = workingDivId; this.loadedSubDataTable = {}; this.isEmpty = $('.pk-emptyDataTable', domElem).length > 0; this.bindEventsAndApplyStyle(domElem); this.initialized = true; domElem.data('piwikDataTable', this); }, //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; self.reloadAjaxDataTable(); }, setGraphedColumn: function (columnName) { this.param.columns = columnName; }, //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', 'include_aggregate_rows', 'totalRows' ]; for (var key in filters) { 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) { 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'); } // when switching to display graphs, reset limit if (self.param.viewDataTable && self.param.viewDataTable.indexOf('graph') === 0) { delete self.param.filter_offset; delete self.param.filter_limit; } var container = $('#' + self.workingDivId + ' .piwik-graph'); var params = {}; for (var key in self.param) { if (typeof self.param[key] != "undefined" && self.param[key] != '') params[key] = self.param[key]; } var ajaxRequest = new ajaxHelper(); ajaxRequest.addParams(params, 'get'); ajaxRequest.setCallback( function (response) { container.trigger('piwikDestroyPlot'); container.off('piwikDestroyPlot'); callbackSuccess(response); } ); ajaxRequest.setFormat('html'); ajaxRequest.send(false); }, // 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) { var content = $(response); var idToReplace = workingDivId || $(content).attr('id'); var dataTableSel = $('#' + idToReplace); // keep the original list of related reports var oldReportsElem = $('.datatableRelatedReports', dataTableSel); $('.datatableRelatedReports', content).replaceWith(oldReportsElem); // 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); } piwikHelper.lazyScrollTo(content[0], 400); 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 or truncate the labels when they are too big - 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.handleSort(domElem); self.handleLimit(domElem); self.handleSearchBox(domElem); self.handleOffsetInformation(domElem); self.handleAnnotationsButton(domElem); self.handleEvolutionAnnotations(domElem); self.handleExportBox(domElem); self.applyCosmetics(domElem); self.handleSubDataTable(domElem); self.handleConfigurationBox(domElem); self.handleColumnDocumentation(domElem); self.handleReportDocumentation(domElem); self.handleRowActions(domElem); self.handleCellTooltips(domElem); self.handleRelatedReports(domElem); self.handleTriggeredEvents(domElem); }, handleLimit: function (domElem) { var tableRowLimits = [5, 10, 25, 50, 100, 250, 500], evolutionLimits = { day: [30, 60, 90, 180, 365, 500], week: [4, 12, 26, 52, 104, 500], month: [3, 6, 12, 24, 36, 120], year: [3, 5, 10] }; var self = this; if (typeof self.parentId != "undefined" && self.parentId != '') { // no limit selector for subtables $('.limitSelection', domElem).remove(); return; } // 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; }; } // setup limit control $('.limitSelection', domElem).append('
' + self.param[limitParamName] + '
'); if (self.param.viewDataTable == 'table' || self.param.viewDataTable == 'tableAllColumns' || self.param.viewDataTable == 'tableGoals' || self.param.viewDataTable == 'ecommerceOrder' || self.param.viewDataTable == 'ecommerceAbandonedCart' || self.param.viewDataTable == 'graphEvolution') { $('.limitSelection ul', domElem).hide(); for (var i = 0; i < numbers.length; i++) { $('.limitSelection ul', domElem).append('
  • ' + numbers[i] + '
  • '); } $('.limitSelection ul li:last', domElem).addClass('last'); if (!self.isEmpty) { var show = function() { $('.limitSelection ul', domElem).show(); $('.limitSelection', domElem).addClass('visible'); $(document).on('mouseup.limitSelection', function(e) { if (!$(e.target).closest('.limitSelection').length) { hide(); } }); }; var hide = function () { $('.limitSelection ul', domElem).hide(); $('.limitSelection', domElem).removeClass('visible'); $(document).off('mouseup.limitSelection'); }; $('.limitSelection div', domElem).on('click', function () { $('.limitSelection', domElem).is('.visible') ? hide() : show(); }); $('.limitSelection ul li', domElem).on('click', function (event) { var limit = parseInt($(event.target).text()); hide(); if (limit != self.param[limitParamName]) { setLimitValue(self.param, limit); $('.limitSelection>div>span', domElem).text(limit); self.reloadAjaxDataTable(); var data = {}; data[limitParamName] = self.param[limitParamName]; self.notifyWidgetParametersChange(domElem, data); } }); } else { $('.limitSelection', domElem).toggleClass('disabled'); } } else { $('.limitSelection', domElem).hide(); } }, // 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; function getSortImageSrc() { var imageSortSrc = false; if (currentIsSubDataTable) { if (self.param.filter_sort_order == 'asc') { imageSortSrc = 'plugins/Zeitgeist/images/sort_subtable_asc.png'; } else { imageSortSrc = 'plugins/Zeitgeist/images/sort_subtable_desc.png'; } } else { if (self.param.filter_sort_order == 'asc') { imageSortSrc = 'plugins/Zeitgeist/images/sortasc.png'; } else { imageSortSrc = 'plugins/Zeitgeist/images/sortdesc.png'; } } return imageSortSrc; } 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 imageSortSrc = getSortImageSrc(); var imageSortWidth = 16; var imageSortHeight = 16; var sortOrder = self.param.filter_sort_order; var ImageSortClass = sortOrder.charAt(0).toUpperCase() + sortOrder.substr(1); // we change the style of the column currently used as sort column // adding an image and the class columnSorted to the TD $(".sortable#" + self.param.filter_sort_column + ' #thDIV', domElem).parent() .addClass('columnSorted') .prepend('
    '); } } }, //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); $('.dataTableSearchPattern', domElem) .css({display: 'block'}) .each(function () { // when enter is pressed in the input field we submit the form $('.searchInput', this) .on("keyup", function (e) { if (isEnterKey(e)) { $(this).siblings(':submit').submit(); } } ) .val(currentPattern) ; $(':submit', this).submit( function () { var keyword = $(this).siblings('.searchInput').val(); self.param.filter_offset = 0; 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); } ); $(':submit', this) .click(function () { $(this).submit(); }) ; // in the case there is a searched keyword we display the RESET image if (currentPattern) { var target = this; var clearImg = $('\ \ ') .click(function () { $('.searchInput', target).val(''); $(':submit', target).submit(); }); $('.searchInput', this).after(clearImg); } } ); }, //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.props.keep_summary_row == 1) --totalRows; if (offsetEnd > totalRows) offsetEndDisp = totalRows; // only show this string if there is some rows in the datatable if (totalRows != 0) { var str = sprintf(_pk_translate('CoreHome_PageOf'), offset + '-' + offsetEndDisp, totalRows); $(this).text(str); } } ); // Display the next link if the total Rows is greater than the current end row $('.dataTableNext', domElem) .each(function () { var offsetEnd = Number(self.param.filter_offset) + Number(self.param.filter_limit); var totalRows = Number(self.param.totalRows); if (self.props.keep_summary_row == 1) --totalRows; if (offsetEnd < totalRows) { $(this).css('display', 'inline'); } }) // bind the click event to trigger the ajax request with the new offset .click(function () { $(this).off('click'); self.param.filter_offset = Number(self.param.filter_offset) + Number(self.param.filter_limit); self.reloadAjaxDataTable(); }) ; // Display the previous link if the current offset is not zero $('.dataTablePrevious', domElem) .each(function () { var offset = 1 + Number(self.param.filter_offset); if (offset != 1) { $(this).css('display', 'inline'); } } ) // bind the click event to trigger the ajax request with the new offset // take care of the negative offset, we setup 0 .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) 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 ; // set position of evolution annotation icons annotations.css({ top: -datatableFeatures.height() - annotationAxisHeight + noteSize / 2, left: 6 // padding-left of .jqplot-graph element (in _dataTableViz_jqplotGraph.tpl) }); piwik.annotations.placeEvolutionIcons(annotations, domElem); // add new section under axis datatableFeatures.append(annotations); // 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 $('span', annotations).hover( function () { $(this).css('opacity', 1); }, 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 $('span', annotations).click(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 img', 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', viewAndAdd.replace("%s", oldDate)); return false; } }); } // change the tooltip of the clicked evolution icon if (manager.is(':hidden')) { spanSelf.attr('title', viewAndAdd.replace("%s", date)); } else { spanSelf.attr('title', hideNotes.replace("%s", 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; }); } ); } }, 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 $('img', this).attr('title', _pk_translate('Annotations_IconDescHideNotes')); } else { annotationManager.slideUp('slow'); // hiding $('img', 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 $('img', 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; } // When the (+) image is hovered, the export buttons are displayed $('.dataTableFooterIconsShow', domElem) .show() .hover(function () { $(this).fadeOut('slow'); $('.exportToFormatIcons', $(this).parent()).show('slow'); }, function () {} ); //footer arrow position element name self.jsViewDataTable = $('.dataTableFooterWrap', domElem).attr('var'); $('.tableAllColumnsSwitch a', domElem) .show() .click( function () { // we only reset the limit filter, in case switch to table view from cloud view where limit is custom set to 30 // this value is stored in config file General->datatable_default_limit but this is more an edge case so ok to set it to 10 self.param.viewDataTable = $(this).attr('format'); //self.resetAllFilters(); // when switching to display simple table, do not exclude low pop by default delete self.param.enable_filter_excludelowpop; delete self.param.filter_sort_column; delete self.param.filter_sort_order; delete columns; self.reloadAjaxDataTable(); self.notifyWidgetParametersChange($(this), {viewDataTable: self.param.viewDataTable}); } ); //handle Graph View icons $('.tableGraphViews a', domElem) .click(function () { var viewDataTable = $(this).attr('format'); var filters = self.resetAllFilters(); self.param.flat = filters.flat; self.param.columns = filters.columns; self.param.viewDataTable = viewDataTable; self.reloadAjaxDataTable(); self.notifyWidgetParametersChange($(this), {viewDataTable: self.param.viewDataTable}); }); //Graph icon Collapsed functionality self.currentGraphViewIcon = 0; self.graphViewEnabled = 0; self.graphViewStartingThreads = 0; self.graphViewStartingKeep = false; //show keep flag //define collapsed icons $('.tableGraphCollapsed a', domElem) .each(function (i) { if (self.jsViewDataTable == $(this).attr('var')) { self.currentGraphViewIcon = i; self.graphViewEnabled = true; } }) .each(function (i) { if (self.currentGraphViewIcon != i) $(this).hide(); }); $('.tableGraphCollapsed', domElem).hover( function () { //Graph icon onmouseover if (self.graphViewStartingThreads > 0) return self.graphViewStartingKeep = true; //exit if animation is not finished $(this).addClass('tableIconsGroupActive'); $('a', this).each(function (i) { if (self.currentGraphViewIcon != i || self.graphViewEnabled) { self.graphViewStartingThreads++; } if (self.currentGraphViewIcon != i) { //show other icons $(this).show('fast', function () {self.graphViewStartingThreads--}); } else if (self.graphViewEnabled) { self.graphViewStartingThreads--; } }); self.exportToFormatHide(domElem); }, function() { //Graph icon onmouseout if (self.graphViewStartingKeep) return self.graphViewStartingKeep = false; //exit while icons animate $('a', this).each(function (i) { if (self.currentGraphViewIcon != i) { //hide other icons $(this).hide('fast'); } }); $(this).removeClass('tableIconsGroupActive'); } ); //handle exportToFormat icons self.exportToFormat = null; $('.exportToFormatIcons a', domElem).click(function () { self.exportToFormat = {}; self.exportToFormat.lastActiveIcon = this; self.exportToFormat.target = $(this).parent().siblings('.exportToFormatItems').show('fast'); self.exportToFormat.obj = $(this).hide(); }); //close exportToFormat onClickOutside $('body').on('mouseup', function (e) { if (self.exportToFormat) { self.exportToFormatHide(domElem); } }); $('.exportToFormatItems a', domElem) // prevent click jacking attacks by dynamically adding the token auth when the link is clicked .click(function () { $(this).attr('href', function () { var url = $(this).attr('href') + '&token_auth=' + piwik.token_auth; var limit = $('.limitSelection>div>span', domElem).text(); url += '&filter_limit=' + (limit || $(this).attr('filter_limit')); return url; }) }) .attr('href', function () { var format = $(this).attr('format'); var method = $(this).attr('methodToCall'); var segment = self.param.segment; var label = self.param.label; var idGoal = self.param.idGoal; var param_date = self.param.date; var date = $(this).attr('date'); if (typeof date != 'undefined') { param_date = date; } if (typeof self.param.dateUsedInGraph != 'undefined') { param_date = self.param.dateUsedInGraph; } var period = self.param.period; // RSS does not work for period=range if (format == 'RSS' && self.param.period == 'range') { period = 'day'; } var str = 'index.php?module=API' + '&method=' + method + '&format=' + format + '&idSite=' + self.param.idSite + '&period=' + period + '&date=' + param_date + ( typeof self.param.filter_pattern != "undefined" ? '&filter_pattern=' + self.param.filter_pattern : '') + ( typeof self.param.filter_pattern_recursive != "undefined" ? '&filter_pattern_recursive=' + self.param.filter_pattern_recursive : ''); if (typeof self.param.flat != "undefined") { str += '&flat=' + (self.param.flat == 0 ? '0' : '1'); if (typeof self.param.include_aggregate_rows != "undefined" && self.param.include_aggregate_rows) { str += '&include_aggregate_rows=1'; } } else { str += '&expanded=1'; } if (format == 'CSV' || format == 'TSV' || format == 'RSS') { str += '&translateColumnNames=1&language=' + piwik.language; } if (typeof segment != 'undefined') { str += '&segment=' + segment; } // Export Goals specific reports if (typeof idGoal != 'undefined' && idGoal != '-1') { str += '&idGoal=' + idGoal; } if (label) { label = label.split(','); if (label.length > 1) { for (var i = 0; i != label.length; ++i) { str += '&label[]=' + encodeURIComponent(label[i]); } } else { str += '&label=' + encodeURIComponent(label[0]); } } return str; } ); }, exportToFormatHide: function (domElem, noAnimation) { var self = this; if (self.exportToFormat) { var animationSpeed = noAnimation ? 0 : 'fast'; self.exportToFormat.target.hide(animationSpeed); self.exportToFormat.obj.show(animationSpeed); self.exportToFormat = null; } }, 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 = $('div.tableConfiguration ul', domElem); function hideConfigurationIcon() { // hide the icon when there are no actions available or we're not in a table view $('div.tableConfiguration', domElem).remove(); } if (ul.find('li').size() == 0) { hideConfigurationIcon(); return; } var icon = $('a.tableConfigurationIcon', domElem); icon.click(function () { return false; }); var iconHighlighted = false; ul.find('li:first').addClass('first'); ul.find('li:last').addClass('last'); ul.prepend('
  • '); // open and close the box var open = function () { self.exportToFormatHide(domElem, true); ul.addClass('open'); icon.css('opacity', 1); }; var close = function () { ul.removeClass('open'); icon.css('opacity', icon.hasClass('highlighted') ? .85 : .6); }; $('div.tableConfiguration', domElem).hover(open, close); var generateClickCallback = function (paramName, callbackAfterToggle) { return function () { close(); self.param[paramName] = 1 - self.param[paramName]; self.param.filter_offset = 0; delete self.param.totalRows; if (callbackAfterToggle) callbackAfterToggle(); self.reloadAjaxDataTable(true, callbackSuccess); var data = {}; data[paramName] = self.param[paramName]; self.notifyWidgetParametersChange(domElem, data); }; }; var getText = function (text, addDefault) { text = _pk_translate(text); if (text.indexOf('%s') > 0) { text = text.replace('%s', '
    » '); if (addDefault) text += ' (' + _pk_translate('CoreHome_Default') + ')'; text += ''; } return text; }; var setText = function (el, paramName, textA, textB) { if (typeof self.param[paramName] != 'undefined' && self.param[paramName] == 1) { $(el).html(getText(textA, true)); iconHighlighted = true; } else { self.param[paramName] = 0; $(el).html(getText(textB)); } }; // handle low population $('.dataTableExcludeLowPopulation', domElem) .each(function () { // Set the text, either "Exclude low pop" or "Include all" if (typeof self.param.enable_filter_excludelowpop == 'undefined') { self.param.enable_filter_excludelowpop = 0; } if (Number(self.param.enable_filter_excludelowpop) != 0) { var string = getText('CoreHome_IncludeRowsWithLowPopulation', true); self.param.enable_filter_excludelowpop = 1; iconHighlighted = true; } else { var string = getText('CoreHome_ExcludeRowsWithLowPopulation'); self.param.enable_filter_excludelowpop = 0; } $(this).html(string); }) .click(generateClickCallback('enable_filter_excludelowpop')); // handle flatten $('.dataTableFlatten', domElem) .each(function () { setText(this, 'flat', 'CoreHome_UnFlattenDataTable', 'CoreHome_FlattenDataTable'); }) .click(generateClickCallback('flat')); $('.dataTableIncludeAggregateRows', domElem) .each(function () { setText(this, 'include_aggregate_rows', 'CoreHome_DataTableExcludeAggregateRows', 'CoreHome_DataTableIncludeAggregateRows'); }) .click(generateClickCallback('include_aggregate_rows', function () { if (self.param.include_aggregate_rows == 1) { // when including aggregate rows is enabled, we remove the sorting // this way, the aggregate rows appear directly before their children self.param.filter_sort_column = ''; self.notifyWidgetParametersChange(domElem, {filter_sort_column: ''}); } })); // handle highlighted icon if (iconHighlighted) { icon.addClass('highlighted'); } close(); if (!iconHighlighted && !(self.param.viewDataTable == 'table' || self.param.viewDataTable == 'tableAllColumns' || self.param.viewDataTable == 'tableGoals')) { hideConfigurationIcon(); return; } // fix a css bug of ie7 if (document.all && !window.opera && window.XMLHttpRequest) { window.setTimeout(function () { open(); var width = 0; ul.find('li').each(function () { width = Math.max(width, $(this).width()); }).width(width); close(); }, 400); } }, // Tell parent widget that the parameters of this table was updated, notifyWidgetParametersChange: function (domWidget, parameters) { var widget = $(domWidget).parents('[widgetId]'); // trigger setParameters event on base element widget.trigger('setParameters', parameters); }, truncate: function (domElemToTruncate, truncationOffset) { var self = this; domElemToTruncate = $(domElemToTruncate); if (typeof domElemToTruncate.data('originalText') != 'undefined') { // truncate only once. otherwise, the tooltip will show the truncated text as well. return; } // make the original text (before truncation) available for others. // the .truncate plugins adds a title to the dom element but the .tooltip // plugin removes that again. domElemToTruncate.data('originalText', domElemToTruncate.text()); if (typeof truncationOffset == 'undefined') { truncationOffset = 0; } var truncationLimit = 50; if (typeof self.param.idSubtable == 'undefined' && self.param.viewDataTable == 'tableAllColumns') { // when showing all columns in a subtable, space is restricted truncationLimit = 25; } truncationLimit += truncationOffset; domElemToTruncate.truncate(truncationLimit); var tooltipElem = $('.truncated', domElemToTruncate), customToolTipText = domElemToTruncate.attr('title'); // if there's a title on the dom element, use this as the tooltip instead of // the one set by the truncate plugin if (customToolTipText) { // make sure browser doesn't add its own tooltip for the truncated element if (tooltipElem[0]) { tooltipElem.removeAttr('title'); } tooltipElem = domElemToTruncate; tooltipElem.attr('title', customToolTipText); } // use tooltip (tooltip text determined by the 'title' attribute) tooltipElem.tooltip({ track: true, show: false, hide: false }); }, //Apply some miscelleaneous style to the DataTable applyCosmetics: function (domElem) { var self = this; // Add some styles on the cells even/odd // label (first column of a data row) or not $("th:first-child", domElem).addClass('label'); $("td:first-child:odd", domElem).addClass('label labeleven'); $("td:first-child:even", domElem).addClass('label labelodd'); $("tr:odd td", domElem).slice(1).addClass('column columnodd'); $("tr:even td", domElem).slice(1).addClass('column columneven'); $('td span.label', domElem).each(function () { self.truncate($(this)); }); }, //behaviour for 'nested DataTable' (DataTable loaded on a click on a row) handleSubDataTable: function (domElem) { var self = this; // When the TR has a subDataTable class it means that this row has a link to a subDataTable this.numberOfSubtables = $('tr.subDataTable', domElem) .click( function () { // get the idSubTable var idSubTable = $(this).attr('id'); var divIdToReplaceWithSubTable = 'subDataTable_' + idSubTable; // if the subDataTable is not already loaded if (typeof self.loadedSubDataTable[divIdToReplaceWithSubTable] == "undefined") { var numberOfColumns = $(this).children().length; // at the end of the query it will replace the ID matching the new HTML table #ID // we need to create this ID first $(this).after( '' + '' + '
    ' + '' + _pk_translate('General_Loading') + '' + '
    ' + '' + '' ); var savedActionVariable = self.param.action; // reset all the filters from the Parent table var filtersToRestore = self.resetAllFilters(); // do not ignore the exclude low population click self.param.enable_filter_excludelowpop = filtersToRestore.enable_filter_excludelowpop; self.param.idSubtable = idSubTable; self.param.action = self.props.subtable_controller_action; delete self.param.totalRows; self.reloadAjaxDataTable(false, function(response) { self.dataTableLoaded(response, divIdToReplaceWithSubTable); }); self.param.action = savedActionVariable; delete self.param.idSubtable; self.restoreAllFilters(filtersToRestore); self.loadedSubDataTable[divIdToReplaceWithSubTable] = true; $(this).next().toggle(); // when "loading..." is displayed, hide actions // repositioning after loading is not easily possible $(this).find('div.dataTableRowActions').hide(); } $(this).next().toggle(); self.repositionRowActions($(this)); } ).size(); }, // tooltip for column documentation handleColumnDocumentation: function (domElem) { if ($('#dashboard').size() > 0) { // don't display column documentation in dashboard // it causes trouble in full screen view return; } $('th:has(.columnDocumentation)', domElem).each(function () { var th = $(this); var tooltip = th.find('.columnDocumentation'); tooltip.next().hover(function () { var left = (-1 * tooltip.outerWidth() / 2) + th.width() / 2; var top = -1 * (tooltip.outerHeight() + 10); if (th.next().size() == 0) { left = (-1 * tooltip.outerWidth()) + th.width() + parseInt(th.css('padding-right'), 10); } tooltip.css({ marginLeft: left, marginTop: top }); tooltip.stop(true, true).fadeIn(250); }, function () { $(this).prev().stop(true, true).fadeOut(400); }); }); }, // documentation for report handleReportDocumentation: function (domElem) { // don't display report documentation in dashboard if ($('#dashboard').size() > 0 // or in Widgetize screen || $('.widgetContent').size() > 0 // or in Widget export || $('.widget').size() > 0 ) { return; } domElem = $(domElem); var doc = domElem.find('.reportDocumentation'); var h2 = this._findReportHeader(domElem); if (doc.size() == 0 || doc.children().size() == 0) // if we can't find the element, or the element is empty { if (h2 && h2.size() > 0) { h2.find('a.reportDocumentationIcon').addClass('hidden'); } return; } var icon = $(''); var docShown = false; icon.click(function () { if (docShown) { doc.stop(true, true).fadeOut(250); } else { var widthOrientation = domElem.find('table, canvas, object').eq(0); if (widthOrientation.size() > 0) { var width = Math.min(widthOrientation.width(), doc.parent().innerWidth()); doc.css('width', (width - 2) + 'px'); } doc.stop(true, true).fadeIn(250); } docShown = !docShown; return false; }); icon.addClass('reportDocumentationIcon'); if (h2 && h2.size() > 0) { // handle previously added icon var existingIcon = h2.find('a.reportDocumentationIcon'); if (existingIcon.size() > 0) { existingIcon.replaceWith(icon); } else { // add icon h2.append('   '); h2.append(icon); h2.hover(function () { $(this).find('a.reportDocumentationIcon').show(); }, function () { $(this).find('a.reportDocumentationIcon').hide(); }) .click( function () { $(this).find('a.reportDocumentationIcon').click(); }) .css('cursor', 'pointer'); } } else { //domElem.prepend(icon); } }, handleRowActions: function (domElem) { this.doHandleRowActions(domElem.find('table > tbody > tr')); }, handleCellTooltips: function(domElem) { domElem.find('span.cell-tooltip').tooltip({ track: true, items: 'span', content: function() { return $(this).data('tooltip'); }, show: false, hide: false, tooltipClass: 'small' }); }, handleRelatedReports: function (domElem) { var self = this, hideShowRelatedReports = function (thisReport) { $('span', $(thisReport).parent().parent()).each(function () { if (thisReport == this) $(this).hide(); else $(this).show(); }); }, // 'this' report must be hidden in datatable output thisReport = $('.datatableRelatedReports span:hidden', domElem)[0]; hideShowRelatedReports(thisReport); $('.datatableRelatedReports span', domElem).each(function () { var clicked = this; $(this).unbind('click').click(function (e) { var url = $(this).attr('href'); // if this url is also the url of a menu item, better to click that menu item instead of // doing AJAX request var menuItem = null; $("#root").find(">.nav a").each(function () { if ($(this).attr('href') == url) { menuItem = this; return false } }); if (menuItem) { $(menuItem).click(); return; } // modify parameters self.resetAllFilters(); var newParams = broadcast.getValuesFromUrl(url); for (var key in newParams) { self.param[key] = decodeURIComponent(newParams[key]); } // do ajax request self.reloadAjaxDataTable(true, function (newReport) { var newDomElem = self.dataTableLoaded(newReport, self.workingDivId); hideShowRelatedReports(clicked); // update header, if we can find it var h2 = self._findReportHeader(newDomElem); if (h2) h2.text($(clicked).text()); }); }); }); }, /** * Handle events that other code triggers on this table. * * You can trigger one of these events to get the datatable to do things, * such as reload its data. * * Events handled: * - reload: Triggering 'reload' on a datatable DOM element will * reload the datatable's data. You can pass in an object mapping * parameters to set before reloading data. * * $(datatableDomElem).trigger('reload', {columns: 'nb_visits,nb_actions', idSite: 2}); */ handleTriggeredEvents: function (domElem) { var self = this; // reload datatable w/ new params if desired (NOTE: must use 'bind', not 'on') $(domElem).bind('reload', function (e, paramOverride) { paramOverride = paramOverride || {}; for (var name in paramOverride) { self.param[name] = paramOverride[name]; } self.reloadAjaxDataTable(true); }); }, // also used in action data table doHandleRowActions: function (trs) { var self = this; var merged = $.extend({}, self.param, self.props); var availableActionsForReport = DataTable_RowActions_Registry.getAvailableActionsForReport(merged); if (availableActionsForReport.length == 0) { return; } var actionInstances = {}; for (var i = 0; i < availableActionsForReport.length; i++) { var action = availableActionsForReport[i]; actionInstances[action.name] = action.createInstance(self); } trs.each(function () { var tr = $(this); var td = tr.find('td:first'); // call initTr on all actions that are available for the report for (var i = 0; i < availableActionsForReport.length; i++) { var action = availableActionsForReport[i]; actionInstances[action.name].initTr(tr); } // if there are row actions, make sure the first column is not too narrow td.css('minWidth', '145px'); // show actions that are available for the row on hover var actionsDom = null; tr.hover(function () { if (actionsDom === null) { // create dom nodes on the fly actionsDom = self.createRowActions(availableActionsForReport, tr, actionInstances); td.prepend(actionsDom); } // reposition and show the actions self.repositionRowActions(tr); if ($(window).width() >= 600) { actionsDom.show(); } }, function () { if (actionsDom !== null) { actionsDom.hide(); } }); }); }, createRowActions: function (availableActionsForReport, tr, actionInstances) { var container = $(document.createElement('div')).addClass('dataTableRowActions'); for (var i = availableActionsForReport.length - 1; i >= 0; i--) { var action = availableActionsForReport[i]; if (!action.isAvailableOnRow(this.param, tr)) { continue; } var actionEl = $(document.createElement('a')).attr({href: '#'}).addClass('action' + action.name); actionEl.append($(document.createElement('img')).attr({src: action.dataTableIcon})); container.append(actionEl); if (i == availableActionsForReport.length - 1) { actionEl.addClass('leftmost'); } if (i == 0) { actionEl.addClass('rightmost'); } actionEl.click((function (action, el) { return function (e) { $(this).blur().tooltip('close'); container.hide(); if (typeof actionInstances[action.name].onClick == 'function') { return actionInstances[action.name].onClick(el, tr, e); } actionInstances[action.name].trigger(tr, e); return false; } })(action, actionEl)); if (typeof action.dataTableIconHover != 'undefined') { actionEl.append($(document.createElement('img')).attr({src: action.dataTableIconHover}).hide()); actionEl.hover(function () { var img = $(this).find('img'); img.eq(0).hide(); img.eq(1).show(); }, function () { var img = $(this).find('img'); img.eq(1).hide(); img.eq(0).show(); }); } if (typeof action.dataTableIconTooltip != 'undefined') { actionEl.tooltip({ track: true, items: 'a', content: '

    '+action.dataTableIconTooltip[0]+'

    '+action.dataTableIconTooltip[1], tooltipClass: 'rowActionTooltip', show: false, hide: false }); } } return container; }, repositionRowActions: function (tr) { var td = tr.find('td:first'); var actions = tr.find('div.dataTableRowActions'); actions.height(tr.innerHeight() - 2); actions.css('marginLeft', (td.width() + 5 - actions.outerWidth()) + 'px'); }, _findReportHeader: function (domElem) { var h2 = false; if (domElem.prev().is('h2')) { h2 = domElem.prev(); } else if (this.param.viewDataTable == 'tableGoals') { h2 = $('#titleGoalsByDimension'); } else if ($('h2', domElem)) { h2 = $('h2', domElem); } return h2; } };