/*! * Matomo - free/libre analytics platform * * @link https://matomo.org * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ function widgetsHelper() { } // a Promise for the first call to getAvailableWidgets. this should not be aborted, // so any code that aborts all ajax requests should make sure this promise is resolved // first. widgetsHelper.firstGetAvailableWidgetsCall = null; /** * Returns the available widgets fetched via AJAX (if not already done) * * @return {object} object containing available widgets */ widgetsHelper.getAvailableWidgets = function (callback) { function resetFirstGetAvailableWidgetsCall(promise) { if (widgetsHelper.firstGetAvailableWidgetsCall === promise) { widgetsHelper.firstGetAvailableWidgetsCall = null; } } if (!widgetsHelper.availableWidgets && widgetsHelper.firstGetAvailableWidgetsCall) { return widgetsHelper.firstGetAvailableWidgetsCall.then(function () { if (callback) { callback(widgetsHelper.availableWidgets); } }); } function mergeCategoriesAndSubCategories(availableWidgets) { var categorized = {}; $.each(availableWidgets, function (index, widget) { var category = widget.category.name; if (!categorized[category]) { categorized[category] = {'-': []}; } var subcategory = '-'; if (widget.subcategory && widget.subcategory.name) { subcategory = widget.subcategory.name; } if (!categorized[category][subcategory]) { categorized[category][subcategory] = []; } categorized[category][subcategory].push(widget); }); var moved = {}; $.each(categorized, function (category, widgets) { $.each(widgets, function (subcategory, subwidgets) { if (!subwidgets.length) { return; } var categoryToUse = category; if (subwidgets.length >= 3 && subcategory !== '-') { categoryToUse = category + ' - ' + subcategory; } if (!moved[categoryToUse]) { moved[categoryToUse] = []; } $.each(subwidgets, function (index, widget) { moved[categoryToUse].push(widget); }); }); }); return moved; } var promise = new Promise(function (resolve, reject) { if (!widgetsHelper.availableWidgets) { var ajaxRequest = new ajaxHelper(); ajaxRequest._mixinDefaultGetParams = function (params) { return params; }; ajaxRequest.addParams({ module: 'API', method: 'API.getWidgetMetadata', filter_limit: '-1', format: 'JSON', deep: '1', idSite: piwik.idSite || broadcast.getValueFromUrl('idSite') }, 'get'); ajaxRequest.setCallback( function (data) { widgetsHelper.availableWidgets = mergeCategoriesAndSubCategories(data); resetFirstGetAvailableWidgetsCall(promise); resolve(); } ); ajaxRequest.setErrorCallback(function (deferred, status) { resetFirstGetAvailableWidgetsCall(promise); if (status == 'abort') { reject(new Error('Loading widget metadata was aborted')); return; } if (!deferred || deferred.status < 400 || deferred.status >= 600) { reject(new Error('Loading widget metadata failed')); return; } $('#loadingError').show(); reject(new Error('Loading widget metadata failed')); }); ajaxRequest.send(); return; } resolve(); }); if (!widgetsHelper.firstGetAvailableWidgetsCall) { widgetsHelper.firstGetAvailableWidgetsCall = promise; } return promise.then(function () { if (callback) { callback(widgetsHelper.availableWidgets); } }); }; /** * Clear the current collection of availableWidgets so that getAvailableWidgets() will load a fresh * collection from the API. This is useful when we added/deleted a report and need a refresh. */ widgetsHelper.clearAvailableWidgets = function () { delete widgetsHelper.availableWidgets; widgetsHelper.firstGetAvailableWidgetsCall = null; }; /** * Determines the complete widget object by its unique id and sends it to callback method * * @param {string} uniqueId * @param {function} callback */ widgetsHelper.getWidgetObjectFromUniqueId = function (uniqueId, callback) { widgetsHelper.getAvailableWidgets(function(widgets){ callback(widgetsHelper.findWidgetObjectFromUniqueId(uniqueId, widgets)); }); }; widgetsHelper.findWidgetObjectFromUniqueId = function (uniqueId, widgets) { if (!widgets) { return false; } for (var widgetCategory in widgets) { var widgetInCategory = widgets[widgetCategory]; for (var i in widgetInCategory) { if (widgetInCategory[i]["uniqueId"] == uniqueId) { return widgetInCategory[i]; } } } return false; }; /** * Determines the name of a widget by its unique id and sends it to the callback * * @param {string} uniqueId unique id of the widget * @param {function} callback * @return {string} */ widgetsHelper.getWidgetNameFromUniqueId = function (uniqueId, callback) { this.getWidgetObjectFromUniqueId(uniqueId, function(widget) { if (widget == false) { callback(false); } callback(widget["name"]); }); }; /** * Sends an ajax request to query for the widget's html * * @param {string} widgetUniqueId unique id of the widget * @param {object} widgetParameters parameters to be used for loading the widget * @param {function} onWidgetLoadedCallback callback to be executed after widget is loaded * @return {object} */ widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWidgetLoadedCallback, onWidgetErrorCallback) { var disableLink = broadcast.getValueFromUrl('disableLink'); widgetParameters['disableLink'] = disableLink.length || $('body#standalone').length; widgetParameters['widget'] = 1; var widgetRequest = { aborted: false, ajaxRequest: null, abort: function () { this.aborted = true; if (this.ajaxRequest) { this.ajaxRequest.abort(); } } }; function loadLegacyWidget() { if (widgetRequest.aborted) { return; } var ajaxRequest = new ajaxHelper(); ajaxRequest.addParams(widgetParameters, 'get'); ajaxRequest.setCallback(function () { if (widgetRequest.aborted) { return; } onWidgetLoadedCallback.apply(this, arguments); }); if (onWidgetErrorCallback) { ajaxRequest.setErrorCallback(function () { if (widgetRequest.aborted) { return; } onWidgetErrorCallback.apply(this, arguments); }); } ajaxRequest.setFormat('html'); ajaxRequest.send(); widgetRequest.ajaxRequest = ajaxRequest; } var metadataReady = widgetsHelper.availableWidgets ? Promise.resolve() : widgetsHelper.getAvailableWidgets(); metadataReady.then(function () { if (widgetRequest.aborted) { return; } var clientWidget = widgetsHelper.findWidgetObjectFromUniqueId( widgetUniqueId, widgetsHelper.availableWidgets ); if (clientWidget && clientWidget.clientComponent) { clientWidget = $.extend(true, {}, clientWidget); clientWidget.parameters = $.extend({}, clientWidget.parameters, widgetParameters); var html = '
'; onWidgetLoadedCallback(html); return; } loadLegacyWidget(); }).catch(function () { loadLegacyWidget(); }); return widgetRequest; }; (function ($, require) { var exports = require('piwik/UI/Dashboard'); /** * Singleton instance that creates widget elements. Normally not needed even * when embedding/re-using dashboard widgets, but it can be useful when creating * elements with the same look and feel as dashboard widgets, but different * behavior (such as the widget preview in the dashboard manager control). * * @constructor */ var WidgetFactory = function () { // empty }; /** * Creates an HTML element for displaying a widget. * * @param {string} uniqueId unique id of the widget * @param {string} widgetName name of the widget * @return {Element} the empty widget */ WidgetFactory.prototype.make = function (uniqueId, widgetName) { var $result = this.getWidgetTemplate().clone(); $result.attr('id', uniqueId).find('.widgetName').append(widgetName); return $result; }; /** * Returns the base widget template element. The template is stored in the * element with id == 'widgetTemplate'. * * @return {Element} the widget template */ WidgetFactory.prototype.getWidgetTemplate = function () { if (!this.widgetTemplate) { this.widgetTemplate = $('#widgetTemplate').find('>.widget').detach(); } return this.widgetTemplate; }; exports.WidgetFactory = new WidgetFactory(); })(jQuery, require); /** * widgetPreview jQuery Extension * * Converts a dom element to a widget preview * Widget preview contains a categorylist, a widgetlist and a preview */ (function ($) { $.extend({ widgetPreview: new function () { /** * Default settings for widgetPreview * @type {object} */ var defaultSettings = { /** * handler called after a widget preview is loaded in preview element * @type {function} */ onPreviewLoaded: function () {}, /** * handler called on click on element in widgetlist or widget header * @type {function} */ onSelect: function () {}, /** * callback used to determine if a widget is available or not * unavailable widgets aren't selectable in widgetlist * @type {function} */ isWidgetAvailable: function (widgetUniqueId) { return true; }, /** * should the lists and preview be reset on widget selection? * @type {boolean} */ resetOnSelect: false, /** * css classes for various elements * @type {string} */ baseClass: 'widgetpreview-base', categorylistClass: 'widgetpreview-categorylist', widgetlistClass: 'widgetpreview-widgetlist', widgetpreviewClass: 'widgetpreview-preview', choosenClass: 'widgetpreview-choosen', unavailableClass: 'widgetpreview-unavailable' }; /** * Returns the div to show category list in * - if element doesn't exist, it will be created and added * - if element already exists, its content will be removed * * @return {$} category list element */ function createWidgetCategoryList(widgetPreview, availableWidgets) { var settings = widgetPreview.settings; if (!$('.' + settings.categorylistClass, widgetPreview).length) { $(widgetPreview).append('