Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Steur <thomas.steur@gmail.com>2015-07-03 03:54:27 +0300
committersgiehl <stefan@piwik.org>2015-10-06 18:25:13 +0300
commit9ba8f216fd7856ce5fef06bf82ecb8f8a2e7e630 (patch)
tree6ce07d18a85d00b39ab720abe042361c0775aead /plugins/CoreHome/angularjs
parent8ccc9dc05da021325cdbf141a548637fa52f16b2 (diff)
generate pages instead of implementing them in each controller
Diffstat (limited to 'plugins/CoreHome/angularjs')
-rw-r--r--plugins/CoreHome/angularjs/activity-indicator/activityindicator.directive.js31
-rw-r--r--plugins/CoreHome/angularjs/activity-indicator/activityindicator.html3
-rw-r--r--plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js4
-rw-r--r--plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js8
-rw-r--r--plugins/CoreHome/angularjs/common/filters/escape.js16
-rw-r--r--plugins/CoreHome/angularjs/common/services/global-ajax-queue.js14
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik-url.js54
-rw-r--r--plugins/CoreHome/angularjs/common/services/report-metadata-model.js52
-rw-r--r--plugins/CoreHome/angularjs/common/services/reporting-pages-model.js58
-rw-r--r--plugins/CoreHome/angularjs/history/history.service.js21
-rw-r--r--plugins/CoreHome/angularjs/http404check.js6
-rw-r--r--plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.html2
-rw-r--r--plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.js13
-rw-r--r--plugins/CoreHome/angularjs/popover-handler/popover-handler.directive.js74
-rw-r--r--plugins/CoreHome/angularjs/reporting-menu/reportingmenu-model.js153
-rw-r--r--plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js125
-rw-r--r--plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.html40
-rw-r--r--plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.js31
-rw-r--r--plugins/CoreHome/angularjs/reporting-page/reportingpage-model.js196
-rw-r--r--plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js56
-rw-r--r--plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.html25
-rw-r--r--plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.js31
-rw-r--r--plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.html25
-rw-r--r--plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.js67
-rw-r--r--plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.html10
-rw-r--r--plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.js32
-rw-r--r--plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.html13
-rw-r--r--plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js134
-rw-r--r--plugins/CoreHome/angularjs/widget/widget.directive.html23
-rw-r--r--plugins/CoreHome/angularjs/widget/widget.directive.js93
30 files changed, 1391 insertions, 19 deletions
diff --git a/plugins/CoreHome/angularjs/activity-indicator/activityindicator.directive.js b/plugins/CoreHome/angularjs/activity-indicator/activityindicator.directive.js
new file mode 100644
index 0000000000..4417cea7ac
--- /dev/null
+++ b/plugins/CoreHome/angularjs/activity-indicator/activityindicator.directive.js
@@ -0,0 +1,31 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Shows a general loading message while [loading] is set to true.
+ *
+ * @param {Boolean} loading If true, the activity indicator is shown, otherwise the indicator is hidden.
+ *
+ * Example:
+ * <div piwik-activity-indicator loading="true|false"></div>
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikActivityIndicator', piwikActivityIndicator);
+
+ piwikActivityIndicator.$inject = ['piwik'];
+
+ function piwikActivityIndicator(piwik){
+ return {
+ restrict: 'A',
+ transclude: true,
+ scope: {
+ loading: '='
+ },
+ templateUrl: 'plugins/CoreHome/angularjs/activity-indicator/activityindicator.html?cb=' + piwik.cacheBuster
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/activity-indicator/activityindicator.html b/plugins/CoreHome/angularjs/activity-indicator/activityindicator.html
new file mode 100644
index 0000000000..f6080ae817
--- /dev/null
+++ b/plugins/CoreHome/angularjs/activity-indicator/activityindicator.html
@@ -0,0 +1,3 @@
+<div ng-show="loading" class="loadingPiwik">
+ <img src="plugins/Morpheus/images/loading-blue.gif" alt=""/>{{ 'General_LoadingData'|translate }}
+</div> \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js b/plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js
index 89d572cedf..2eef346ec6 100644
--- a/plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js
+++ b/plugins/CoreHome/angularjs/ajax-form/ajax-form.directive.js
@@ -132,7 +132,9 @@
scope.ajaxForm.data[name] = val;
if (!skipScopeApply) {
- scope.$apply();
+ setTimeout(function () {
+ scope.$apply();
+ }, 0);
}
}
};
diff --git a/plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js b/plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js
index 0617c5df88..ee1a2cf219 100644
--- a/plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js
+++ b/plugins/CoreHome/angularjs/common/directives/focus-anywhere-but-here.js
@@ -24,13 +24,17 @@
function onClickOutsideElement (event) {
if (element.has(event.target).length === 0) {
- scope.$apply(attr.piwikFocusAnywhereButHere);
+ setTimeout(function () {
+ scope.$apply(attr.piwikFocusAnywhereButHere);
+ }, 0);
}
}
function onEscapeHandler (event) {
if (event.which === 27) {
- scope.$apply(attr.piwikFocusAnywhereButHere);
+ setTimeout(function () {
+ scope.$apply(attr.piwikFocusAnywhereButHere);
+ }, 0);
}
}
diff --git a/plugins/CoreHome/angularjs/common/filters/escape.js b/plugins/CoreHome/angularjs/common/filters/escape.js
new file mode 100644
index 0000000000..382e84b5ac
--- /dev/null
+++ b/plugins/CoreHome/angularjs/common/filters/escape.js
@@ -0,0 +1,16 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp.filter').filter('escape', escape);
+
+ function escape() {
+
+ return function(value) {
+ return piwikHelper.escape(piwikHelper.htmlEntities(value));
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/common/services/global-ajax-queue.js b/plugins/CoreHome/angularjs/common/services/global-ajax-queue.js
new file mode 100644
index 0000000000..f831995810
--- /dev/null
+++ b/plugins/CoreHome/angularjs/common/services/global-ajax-queue.js
@@ -0,0 +1,14 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp.service').service('globalAjaxQueue', ajaxQueue);
+
+ function ajaxQueue() {
+
+ return globalAjaxQueue;
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/common/services/piwik-url.js b/plugins/CoreHome/angularjs/common/services/piwik-url.js
new file mode 100644
index 0000000000..f2d6e9bb0c
--- /dev/null
+++ b/plugins/CoreHome/angularjs/common/services/piwik-url.js
@@ -0,0 +1,54 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp.service').service('piwikUrl', piwikUrl);
+
+ piwikUrl.$inject = ['$location', 'piwik'];
+
+ /**
+ * Similar to angulars $location but works around some limitation. Use it if you need to access search params
+ */
+ function piwikUrl($location, piwik) {
+
+ var model = {
+ getSearchParam: getSearchParam
+ }
+
+ return model;
+
+ function getSearchParam(paramName)
+ {
+ if (paramName === 'segment') {
+ var hash = window.location.href.split('#');
+ if (hash && hash[1]) {
+ return piwik.broadcast.getValueFromHash(paramName, hash[1]);
+ }
+
+ return broadcast.getValueFromUrl(paramName);
+ }
+
+ // available in global scope
+ var search = $location.search();
+
+ if (!search[paramName]) {
+ // see https://github.com/angular/angular.js/issues/7239 (issue is resolved but problem still exists)
+ search[paramName] = piwik.broadcast.getValueFromUrl(paramName);
+ }
+
+ if (search[paramName]) {
+ var value = search[paramName];
+
+ if (angular.isArray(search[paramName])) {
+ // use last one. Eg when having period=day&period=year angular would otherwise return ['day', 'year']
+ return value[value.length - 1];
+ }
+
+ return value;
+ }
+ }
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/common/services/report-metadata-model.js b/plugins/CoreHome/angularjs/common/services/report-metadata-model.js
new file mode 100644
index 0000000000..f158861423
--- /dev/null
+++ b/plugins/CoreHome/angularjs/common/services/report-metadata-model.js
@@ -0,0 +1,52 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp.service').factory('reportMetadataModel', reportMetadataModel);
+
+ reportMetadataModel.$inject = ['piwik', 'piwikApi'];
+
+ function reportMetadataModel (piwik, piwikApi) {
+
+ var reportsPromise = null;
+
+ var model = {
+ reports: [],
+ fetchReportMetadata: fetchReportMetadata,
+ findReport: findReport
+ };
+
+ return model;
+
+ function findReport(module, action)
+ {
+ var found = [];
+
+ angular.forEach(model.reports, function (report) {
+ if (report.module === module && report.action === action) {
+ found = report;
+ }
+ });
+
+ return found;
+ }
+
+ function fetchReportMetadata()
+ {
+ if (!reportsPromise) {
+ reportsPromise = piwikApi.fetch({
+ method: 'API.getReportMetadata',
+ idSites: piwik.idSite || piwik.broadcast.getValueFromUrl('idSite'),
+ }).then(function (response) {
+ model.reports = response;
+ return response;
+ });
+ }
+
+ return reportsPromise;
+ }
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/common/services/reporting-pages-model.js b/plugins/CoreHome/angularjs/common/services/reporting-pages-model.js
new file mode 100644
index 0000000000..1b1e406c95
--- /dev/null
+++ b/plugins/CoreHome/angularjs/common/services/reporting-pages-model.js
@@ -0,0 +1,58 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp.service').factory('reportingPagesModel', reportingPagesModelService);
+
+ reportingPagesModelService.$inject = ['piwikApi'];
+
+ function reportingPagesModelService (piwikApi) {
+ var fetchAllPagesPromise = false;
+
+ // those sites are going to be displayed
+ var model = {
+ pages : [],
+ findPage: findPage,
+ reloadAllPages : reloadAllPages,
+ getAllPages : getAllPages
+ };
+
+ return model;
+
+ function findPage(categoryId, subcategoryId)
+ {
+ var found = null;
+
+ angular.forEach(model.pages, function (page) {
+ if (page &&
+ page.category && page.subcategory &&
+ page.category.id === categoryId && ('' + page.subcategory.id) === subcategoryId) {
+ found = page;
+ }
+ });
+
+ return found;
+ }
+
+ function reloadAllPages()
+ {
+ fetchAllPagesPromise = null;
+ return getAllPages();
+ }
+
+ function getAllPages()
+ {
+ if (!fetchAllPagesPromise) {
+ fetchAllPagesPromise = piwikApi.fetch({method: 'API.getReportPagesMetadata'}).then(function (response) {
+ model.pages = response;
+ return response;
+ });
+ }
+
+ return fetchAllPagesPromise;
+ }
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/history/history.service.js b/plugins/CoreHome/angularjs/history/history.service.js
index 2c6486a517..7dadf35ad0 100644
--- a/plugins/CoreHome/angularjs/history/history.service.js
+++ b/plugins/CoreHome/angularjs/history/history.service.js
@@ -36,6 +36,19 @@
loadCurrentPage();
}
+ function cleanHash(hash)
+ {
+ var chars = ['#', '/', '?'];
+ for (var i = 0; i != chars.length; ++i) {
+ var charToRemove = chars[i];
+ if (hash.charAt(0) == charToRemove) {
+ hash = hash.substring(1);
+ }
+ }
+
+ return hash;
+ }
+
// currently, the AJAX content URL is stored in $location.search(), but before it was stored in $location.path().
// this function makes sure URLs like http://piwik.net/?...#/module=Whatever&action=whatever still work.
function changePathToSearch() {
@@ -82,13 +95,7 @@
function load(hash) {
// make sure the hash is just the query parameter values, w/o a starting #, / or ? char. broadcast.pageload & $location.path should get neither
- var chars = ['#', '/', '?'];
- for (var i = 0; i != chars.length; ++i) {
- var charToRemove = chars[i];
- if (hash.charAt(0) == charToRemove) {
- hash = hash.substring(1);
- }
- }
+ hash = cleanHash(hash);
if (location.hash === '#?' + hash) {
loadCurrentPage(); // it would not trigger a location change success event as URL is the same, call it manually
diff --git a/plugins/CoreHome/angularjs/http404check.js b/plugins/CoreHome/angularjs/http404check.js
index f2e2a86edf..5b720c9472 100644
--- a/plugins/CoreHome/angularjs/http404check.js
+++ b/plugins/CoreHome/angularjs/http404check.js
@@ -1,9 +1,9 @@
(function () {
angular.module('piwikApp').factory('http404CheckInterceptor', http404CheckInterceptor);
- http404CheckInterceptor.$inject = ['$q'];
+ http404CheckInterceptor.$inject = ['$q', 'globalAjaxQueue'];
- function http404CheckInterceptor($q) {
+ function http404CheckInterceptor($q, globalAjaxQueue) {
function isClientError(rejection)
{
@@ -15,8 +15,8 @@
}
return {
-
'responseError': function(rejection) {
+
if (rejection &&
isClientError(rejection) &&
rejection.config &&
diff --git a/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.html b/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.html
index d1964d1ac4..14191ac20e 100644
--- a/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.html
+++ b/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.html
@@ -23,7 +23,7 @@
class="reset"
src="plugins/CoreHome/images/reset_search.png"/>
</div>
- <div ng-transclude>
+ <div ng-transclude ng-click="selectItem($event)">
</div>
</div>
diff --git a/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.js b/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.js
index 00fbf87d7b..313ef00d6c 100644
--- a/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.js
+++ b/plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.js
@@ -34,10 +34,10 @@
templateUrl: 'plugins/CoreHome/angularjs/menudropdown/menudropdown.directive.html?cb=' + piwik.cacheBuster,
link: function(scope, element, attrs) {
- element.find('.item').on('click', function () {
- var $self = angular.element(this);
+ scope.selectItem = function (event) {
+ var $self = angular.element(event.target);
- if ($self.hasClass('disabled') || $self.hasClass('separator')) {
+ if (!$self.hasClass('item') || $self.hasClass('disabled') || $self.hasClass('separator')) {
return;
}
@@ -47,11 +47,14 @@
});
}
scope.$eval('view.showItems = false');
- scope.$apply();
+
+ setTimeout(function () {
+ scope.$apply();
+ }, 0);
element.find('.item').removeClass('active');
$self.addClass('active');
- });
+ };
scope.searchItems = function (searchTerm)
{
diff --git a/plugins/CoreHome/angularjs/popover-handler/popover-handler.directive.js b/plugins/CoreHome/angularjs/popover-handler/popover-handler.directive.js
new file mode 100644
index 0000000000..ea15f025b0
--- /dev/null
+++ b/plugins/CoreHome/angularjs/popover-handler/popover-handler.directive.js
@@ -0,0 +1,74 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * When present in the page it listens to a popover URL parameter.
+ *
+ * If present it will try to load the related content in a popover or if the URL is empty it will close an
+ * opened popover.
+ *
+ * Example:
+ * <div piwik-popover-handler></div>
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikPopoverHandler', piwikPopoverHandler);
+
+ piwikPopoverHandler.$inject = ['$location', '$rootScope', 'piwik'];
+
+ function piwikPopoverHandler($location, $rootScope, piwik){
+
+ return {
+ restrict: 'A',
+ scope: {},
+ controller: function () {
+
+ function close()
+ {
+ Piwik_Popover.close();
+ }
+
+ function open(popoverParam)
+ {
+ // in case the $ was encoded (e.g. when using copy&paste on urls in some browsers)
+ popoverParam = decodeURIComponent(popoverParam);
+ // revert special encoding from broadcast.propagateNewPopoverParameter()
+ popoverParam = popoverParam.replace(/\$/g, '%');
+ popoverParam = decodeURIComponent(popoverParam);
+
+ var popoverParamParts = popoverParam.split(':');
+ var handlerName = popoverParamParts[0];
+ popoverParamParts.shift();
+ var param = popoverParamParts.join(':');
+ if (typeof piwik.broadcast.popoverHandlers[handlerName] != 'undefined'
+ && !piwik.broadcast.isLoginPage()) {
+ piwik.broadcast.popoverHandlers[handlerName](param);
+ }
+ }
+
+ function openOrClose()
+ {
+ // should be rather done by routing
+ var popoverParam = $location.search().popover;
+ if (popoverParam) {
+ open(popoverParam);
+ } else {
+ close();
+ }
+ }
+
+ $rootScope.$on('$locationChangeSuccess', function () {
+ // should be rather done by routing
+ $(function () {
+ // make sure all popover handles were registered
+ openOrClose();
+ });
+ });
+
+ }
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/reporting-menu/reportingmenu-model.js b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu-model.js
new file mode 100644
index 0000000000..adc1b97936
--- /dev/null
+++ b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu-model.js
@@ -0,0 +1,153 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp').factory('reportingMenuModel', reportingMenuModelService);
+
+ reportingMenuModelService.$inject = ['$filter', '$q', 'piwikApi', 'reportingPagesModel', '$location'];
+
+ function reportingMenuModelService ($filter, $q, piwikApi, reportingPagesModel, $location) {
+
+ // those sites are going to be displayed
+ var model = {
+ menu: [],
+ selected: [],
+ fetchMenuItems: fetchMenuItems,
+ reloadMenuItems: reloadMenuItems,
+ findSubcategory: findSubcategory
+ };
+
+ return model;
+
+ function isNumeric(text) {
+ return !isNaN(parseFloat(text)) && isFinite(text);
+ }
+
+ function findSubcategory(categoryId, subcategoryId)
+ {
+ var foundCategory = null;
+ var foundSubcategory = null;
+
+ angular.forEach(model.menu, function (category) {
+ if (category.id !== categoryId) {
+ return;
+ }
+ angular.forEach(category.subcategories, function (subcategory) {
+ if (subcategory.id === subcategoryId) {
+ foundCategory = category;
+ foundSubcategory = subcategory;
+ }
+ });
+ });
+
+ return {category: foundCategory, subcategory: foundSubcategory};
+ }
+
+ function buildMenuFromPages(pages)
+ {
+ var menu = [];
+
+ var activeCategory = $location.search().category;
+ var activeSubcategory = $location.search().subcategory;
+
+ var categoriesHandled = {};
+ angular.forEach(pages, function (page, key) {
+ var category = page.category;
+ var categoryId = category.id;
+
+ if (categoriesHandled[categoryId]) {
+ return;
+ }
+
+ categoriesHandled[categoryId] = true;
+
+ if (activeCategory && category.id === activeCategory) {
+ // this doesn't really belong here but placed it here for convenience
+ category.active = true;
+ category.hover = true;
+ }
+
+ category.subcategories = [];
+
+ var goalsGroup = false;
+
+ angular.forEach(pages, function (page, key) {
+ if (page.category.id === categoryId) {
+ var subcategory = page.subcategory;
+
+ if (subcategory.id === activeSubcategory) {
+ subcategory.active = true;
+ }
+
+ if (page.widgets && page.widgets[0] && page.category.id === 'Goals_Goals' && isNumeric(page.subcategory.id)) {
+ // we handle a goal
+ if (!goalsGroup) {
+ goalsGroup = angular.copy(subcategory);
+ goalsGroup.name = $filter('translate')('Goals_ChooseGoal');
+ goalsGroup.isGroup = true;
+ goalsGroup.subcategories = [];
+ goalsGroup.order = 10;
+ }
+
+ if (subcategory.active) {
+ goalsGroup.name = subcategory.name;
+ }
+
+ var goalId = page.subcategory.id;
+ subcategory.tooltip = subcategory.name + ' (id = ' + goalId + ' )';
+
+ goalsGroup.subcategories.push(subcategory);
+ return;
+ }
+
+ category.subcategories.push(subcategory);
+ }
+ });
+
+ if (goalsGroup && goalsGroup.subcategories && goalsGroup.subcategories.length <= 3) {
+ angular.forEach(goalsGroup.subcategories, function (subcategory) {
+ category.subcategories.push(subcategory);
+ });
+ } else if(goalsGroup) {
+ category.subcategories.push(goalsGroup);
+ }
+
+ category.subcategories = sortMenuItems(category.subcategories);
+
+ menu.push(category);
+
+ return menu;
+ });
+
+ menu = sortMenuItems(menu);
+
+ return menu;
+ }
+
+ function sortMenuItems(menu) {
+ return $filter('orderBy')(menu, 'order');
+ };
+
+ function reloadMenuItems()
+ {
+ var pagesPromise = reportingPagesModel.reloadAllPages();
+ return pagesPromise.then(function (pages) {
+ model.menu = buildMenuFromPages(pages);
+ });
+ }
+
+ function fetchMenuItems()
+ {
+ var pagesPromise = reportingPagesModel.getAllPages();
+
+ return pagesPromise.then(function (pages) {
+ model.menu = buildMenuFromPages(pages);
+
+ return model.menu;
+ });
+ }
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js
new file mode 100644
index 0000000000..85dc0f17c5
--- /dev/null
+++ b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js
@@ -0,0 +1,125 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp').controller('ReportingMenuController', ReportingMenuController);
+
+ ReportingMenuController.$inject = ['$scope', 'piwik', '$location', '$timeout', 'reportingMenuModel', '$rootScope'];
+
+ function ReportingMenuController($scope, piwik, $location, $timeout, menuModel, $rootScope) {
+
+ function markAllCategoriesAsInactive()
+ {
+ angular.forEach(menuModel.menu, function (cat) {
+ cat.active = false;
+ cat.hover = false;
+ angular.forEach(cat.subcategories, function (subcat) {
+ subcat.active = false;
+ });
+ });
+ }
+
+ function getUrlParam(param)
+ {
+ var value = piwik.broadcast.getValueFromHash(param);
+ if (!value) {
+ value = piwik.broadcast.getValueFromUrl(param);
+ }
+ return value;
+ }
+
+ $scope.menuModel = menuModel;
+
+ var timeoutPromise = null;
+
+ // show subcategories of the currently hovered category
+ $scope.enterCategory = function (category) {
+
+ if (timeoutPromise) {
+ $timeout.cancel(timeoutPromise);
+ }
+
+ angular.forEach(menuModel.menu, function (cat) {
+ cat.hover = false;
+ });
+
+ category.hover = true;
+ };
+
+ // show subcategories of the current active category again (after 2 sec max)
+ $scope.leaveCategory = function (category) {
+
+ if (timeoutPromise) {
+ $timeout.cancel(timeoutPromise);
+ }
+
+ timeoutPromise = $timeout(function () {
+ angular.forEach(menuModel.menu, function (cat) {
+ if (cat.active) {
+ cat.hover = true;
+ } else {
+ cat.hover = false;
+ }
+ });
+ }, 2000);
+ };
+
+ // highlight the currently hovered subcategory (and category)
+ $scope.enterSubcategory = function (category, subcategory) {
+ if (!category || !subcategory) {
+ return;
+ }
+
+ markAllCategoriesAsInactive();
+
+ category.active = true;
+ category.hover = true;
+ subcategory.active = true;
+ };
+
+ var idSite = getUrlParam('idSite');
+ var period = getUrlParam('period');
+ var date = getUrlParam('date');
+ var segment = getUrlParam('segment');
+
+ $scope.makeUrl = function (category, subcategory) {
+ var url = 'idSite=' + idSite + '&period=' + period + '&date=' + date + '&category=' + category.id + '&subcategory=' + subcategory.id;
+ if (segment) {
+ url+= '&segment='+ segment;
+ }
+ return url;
+ }
+
+ $scope.loadSubcategory = function (category, subcategory) {
+ if (subcategory && subcategory.active) {
+ // this menu item is already active, a location change success would not be triggered,
+ // instead trigger an event
+ $rootScope.$emit('loadPage', category.id, subcategory.id);
+ }
+ };
+
+ menuModel.fetchMenuItems().then(function (menu) {
+ if (!$location.search().subcategory) {
+ // load first, initial page if no subcategory is present
+ $scope.enterSubcategory(menu[0], menu[0].subcategories[0]);
+ $location.search($scope.makeUrl(menu[0], menu[0].subcategories[0]));
+ }
+ });
+
+ $rootScope.$on('$locationChangeSuccess', function () {
+ var category = $location.search().category;
+ var subcategory = $location.search().subcategory;
+
+ if (!category || !subcategory) {
+ return;
+ }
+
+ var found = menuModel.findSubcategory(category, subcategory);
+ $scope.enterSubcategory(found.category, found.subcategory);
+ });
+
+ }
+})();
diff --git a/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.html b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.html
new file mode 100644
index 0000000000..bbfd687517
--- /dev/null
+++ b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.html
@@ -0,0 +1,40 @@
+<div class="Menu--dashboard">
+ <ul class="Menu-tabList">
+ <li ng-repeat="category in menuModel.menu"
+ ng-class="{'sfActive': category.active, 'sfHover': category.hover}"
+ ng-mouseenter="enterCategory(category)"
+ ng-mouseleave="leaveCategory(category)">
+ <a class="menuItem"
+ ng-href="#?{{ makeUrl(category,category.subcategories[0]) }}"
+ ng-click="loadSubcategory(category, category.subcategories[0])">
+ {{ category.name }}
+ </a>
+ <ul ng-show="(category.subcategories|length) > 1">
+ <li ng-repeat="subcategory in category.subcategories"
+ ng-class="{'sfHover': subcategory.active}">
+ <div ng-if="subcategory.isGroup" piwik-menudropdown show-search="true" menu-title="{{ subcategory.name|escape }}">
+ <a class="item"
+ ng-repeat="subcat in subcategory.subcategories"
+ title="{{ subcat.tooltip }}"
+ ng-class="{'active': subcat.active}"
+ ng-href="#?{{ makeUrl(category,subcat) }}"
+ ng-click='loadSubcategory(category, subcat)'>
+ {{ subcat.name }}
+ </a>
+ </div>
+
+ <a ng-if="!subcategory.isGroup"
+ ng-href="#?{{ makeUrl(category,subcategory) }}"
+ class="menuItem"
+ ng-click='loadSubcategory(category, subcategory)'>
+ {{ subcategory.name }}
+ </a>
+ </li>
+ </ul>
+ </li>
+ <li id="Searchmenu">
+ <span piwik-quick-access></span>
+ </li>
+ </ul>
+
+</div> \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.js b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.js
new file mode 100644
index 0000000000..7cdd9e951b
--- /dev/null
+++ b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.js
@@ -0,0 +1,31 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Shows the Piwik reporting menu.
+ *
+ * It automatically calls the API to fetch all data.
+ *
+ * Example:
+ * <div piwik-reporting-menu></div>
+ */
+
+(function () {
+ angular.module('piwikApp').directive('piwikReportingMenu', piwikReportingMenu);
+
+ piwikReportingMenu.$inject = ['piwik'];
+
+ function piwikReportingMenu(piwik){
+
+ return {
+ restrict: 'A',
+ scope: {},
+ templateUrl: 'plugins/CoreHome/angularjs/reporting-menu/reportingmenu.directive.html?cb=' + piwik.cacheBuster,
+ controller: 'ReportingMenuController'
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/reporting-page/reportingpage-model.js b/plugins/CoreHome/angularjs/reporting-page/reportingpage-model.js
new file mode 100644
index 0000000000..4018fcee8c
--- /dev/null
+++ b/plugins/CoreHome/angularjs/reporting-page/reportingpage-model.js
@@ -0,0 +1,196 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp').factory('reportingPageModel', reportingPageModelService);
+
+ reportingPageModelService.$inject = ['$filter', 'piwikApi', 'reportingPagesModel', 'reportMetadataModel'];
+
+ function reportingPageModelService ($filter, piwikApi, reportingPagesModel, reportMetadataModel) {
+ var init = false;
+
+ // those sites are going to be displayed
+ var model = {
+ fetchPage: fetchPage,
+ resetPage: resetPage,
+ widgets: [],
+ page: null,
+ pageContentUrl: '',
+ evolutionReports: [],
+ sparklineReports: []
+ };
+
+ return model;
+
+ function resetPage()
+ {
+ model.page = null;
+ model.widgets = [];
+ model.pageContentUrl = '';
+ model.evolutionReports = [];
+ model.sparklineReports = [];
+ }
+
+ function sortWidgets(widgets)
+ {
+ return $filter('orderBy')(widgets, 'order');
+ }
+
+ function shouldBeRenderedWithFullWidth(widget)
+ {
+ // rather controller logic
+ if ((widget.isContainer && widget.layout && widget.layout === 'ByDimension')
+ || widget.viewDataTable === 'bydimension') {
+ return true;
+ }
+
+ return widget.viewDataTable && widget.viewDataTable === 'tableAllColumns';
+ }
+
+ function buildPage(page)
+ {
+ if (!page) {
+ return;
+ }
+
+ var widgets = [];
+ var evolutionReports = [];
+ var sparklineReports = [];
+ var reportsToIgnore = [];
+
+ angular.forEach(page.widgets, function (widget) {
+
+ if (isIgnoredReport(reportsToIgnore, widget)) {
+ return;
+ }
+
+ reportsToIgnore = reportsToIgnore.concat(getRelatedReports(widget));
+
+ if (widget.viewDataTable && widget.viewDataTable === 'graphEvolution') {
+ evolutionReports.push(widget);
+ } else if (widget.viewDataTable && widget.viewDataTable === 'sparklines') {
+ sparklineReports.push(widget);
+ } else {
+ widgets.push(widget);
+ }
+ });
+
+ widgets = sortWidgets(widgets);
+
+ var groupedWidgets = [];
+
+ if (widgets.length === 1) {
+ // if there is only one widget, we always display it full width
+ groupedWidgets = widgets;
+ } else {
+ for (var i = 0; i < widgets.length; i++) {
+ var widget = widgets[i];
+
+ if (shouldBeRenderedWithFullWidth(widget) || (widgets[i+1] && shouldBeRenderedWithFullWidth(widgets[i+1]))) {
+ widget.widgets = sortWidgets(widget.widgets);
+
+ groupedWidgets.push(widget);
+ } else {
+
+ var counter = 0;
+ var left = [widget];
+ var right = [];
+
+ while (widgets[i+1] && !shouldBeRenderedWithFullWidth(widgets[i+1])) {
+ i++;
+ counter++;
+ if (counter % 2 === 0) {
+ left.push(widgets[i]);
+ } else {
+ right.push(widgets[i]);
+ }
+ }
+
+ groupedWidgets.push({group: true, left: left, right: right});
+ }
+ }
+ }
+
+ var copyWidgets = angular.copy(groupedWidgets);
+ var copyEvolution = angular.copy(evolutionReports);
+ var copySparklines = angular.copy(sparklineReports);
+
+ if (copyEvolution.length) {
+ copyEvolution = markWidgetsInFirstRowOfPage(copyEvolution);
+ } else if (copySparklines.length) {
+ copySparklines = markWidgetsInFirstRowOfPage(copySparklines);
+ } else {
+ copyWidgets = markWidgetsInFirstRowOfPage(copyWidgets);
+ }
+
+ // angular.copy forces the page to re-render. Otherwise it won't reload some pages
+ model.evolutionReports = copyEvolution;
+ model.sparklineReports = copySparklines;
+ model.widgets = copyWidgets;
+ }
+
+ function markWidgetsInFirstRowOfPage(widgets)
+ {
+ if (widgets && widgets[0]) {
+ if (widgets[0].group) {
+ markWidgetsInFirstRowOfPage(widgets[0].left);
+ markWidgetsInFirstRowOfPage(widgets[0].right);
+ } else {
+ widgets[0].isFirstInPage = true;
+ }
+ }
+
+ return widgets;
+ }
+
+ function getRelatedReports(widget)
+ {
+ if (widget.isReport) {
+ var report = reportMetadataModel.findReport(widget.module, widget.action);
+
+ if (report && report.relatedReports) {
+ return report.relatedReports;
+ }
+ }
+
+ return [];
+ }
+
+ function isIgnoredReport(reportsToIgnore, widget)
+ {
+ var found = false;
+
+ if (widget.isReport) {
+ angular.forEach(reportsToIgnore, function (report) {
+ if (report.module === widget.module &&
+ report.action === widget.action) {
+ found = true;
+ }
+ });
+ }
+
+ return found;
+ }
+
+ function fetchPage(category, subcategory)
+ {
+ resetPage();
+
+ var pagesPromise = reportingPagesModel.getAllPages();
+ var reportsPromise = reportMetadataModel.fetchReportMetadata();
+
+ return pagesPromise.then(function () {
+ model.page = reportingPagesModel.findPage(category, subcategory);
+
+ reportsPromise.then(function () {
+ buildPage(model.page);
+ });
+
+ return model.page;
+ });
+ }
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js b/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js
new file mode 100644
index 0000000000..cf0d424b55
--- /dev/null
+++ b/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js
@@ -0,0 +1,56 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+(function () {
+ angular.module('piwikApp').controller('ReportingPageController', ReportingPageController);
+
+ ReportingPageController.$inject = ['$scope', 'piwik', '$rootScope', '$location', 'reportingPageModel'];
+
+ function ReportingPageController($scope, piwik, $rootScope, $location, pageModel) {
+ pageModel.resetPage();
+ $scope.pageModel = pageModel;
+
+ var currentCategory = null;
+ var currentSubcategory = null;
+
+ $scope.renderPage = function (category, subcategory) {
+ if (!category || !subcategory) {
+ pageModel.resetPage();
+ $scope.loading = false;
+ return;
+ }
+
+ currentCategory = category;
+ currentSubcategory = subcategory;
+
+ pageModel.fetchPage(category, subcategory).then(function () {
+ $scope.hasNoPage = !pageModel.page;
+ $scope.loading = false;
+ });
+ }
+
+ $scope.loading = true; // we only set loading on initial load
+
+ $scope.renderPage($location.search().category, $location.search().subcategory);
+
+ $rootScope.$on('$locationChangeSuccess', function () {
+ // should be handled by $route
+ var category = $location.search().category;
+ var subcategory = $location.search().subcategory;
+
+ if (category === currentCategory && subcategory === currentSubcategory) {
+ // this page is already loaded
+ return;
+ }
+
+ $scope.renderPage(category, subcategory);
+ });
+
+ $rootScope.$on('loadPage', function (event, category, subcategory) {
+ $scope.renderPage(category, subcategory);
+ });
+ }
+})();
diff --git a/plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.html b/plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.html
new file mode 100644
index 0000000000..9a8974e313
--- /dev/null
+++ b/plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.html
@@ -0,0 +1,25 @@
+<div class="reporting-page">
+
+ <div piwik-activity-indicator loading="loading"/>
+
+ <div ng-show="hasNoPage">{{ 'CoreHome_NoSuchPage'|translate }}</div>
+
+ <div class="row" ng-repeat="evolutionReport in pageModel.evolutionReports">
+ <div class="col-md-12" piwik-widget="evolutionReport"></div>
+ </div>
+
+ <div class="row" ng-repeat="sparklineReport in pageModel.sparklineReports">
+ <div class="col-md-12" piwik-widget="sparklineReport"></div>
+ </div>
+
+ <div class="row" ng-repeat="widget in pageModel.widgets">
+ <div class="col-md-12" ng-if="!widget.group" piwik-widget="widget"></div>
+
+ <div ng-if="widget.group" class="col-md-6">
+ <div ng-repeat="widgetInGroup in widget.left" piwik-widget="widgetInGroup"></div>
+ </div>
+ <div ng-if="widget.group" class="col-md-6">
+ <div ng-repeat="widgetInGroup in widget.right" piwik-widget="widgetInGroup"></div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.js b/plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.js
new file mode 100644
index 0000000000..a146ed3112
--- /dev/null
+++ b/plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.js
@@ -0,0 +1,31 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Shows a piwik reporting page.
+ *
+ * The content to be displayed is automatically loaded via API based on the current URL. The URL parameters
+ * 'category' and 'subcategory' need to be present in the URL in order to see something in the reporting page.
+ *
+ * Example:
+ * <div piwik-reporting-page></div>
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikReportingPage', piwikReportingPage);
+
+ piwikReportingPage.$inject = ['piwik'];
+
+ function piwikReportingPage(piwik){
+
+ return {
+ restrict: 'A',
+ scope: {},
+ templateUrl: 'plugins/CoreHome/angularjs/reporting-page/reportingpage.directive.html?cb=' + piwik.cacheBuster,
+ controller: 'ReportingPageController'
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.html b/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.html
new file mode 100644
index 0000000000..db98dee895
--- /dev/null
+++ b/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.html
@@ -0,0 +1,25 @@
+<div class="reportsByDimensionView">
+
+ <div class="entityList">
+ <div class='dimensionCategory' ng-repeat="category in widgetsByCategory">
+ {{ category.name }}
+ <ul class='listCircle'>
+ <li ng-repeat="widget in category.widgets"
+ class="reportDimension"
+ ng-class="{activeDimension: (selectedWidget.uniqueId===widget.uniqueId)}"
+ ng-click="selectWidget(widget)">
+ <span class='dimension'>{{widget.name}}</span>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div style="float:left;max-width:900px;">
+ <h2 ng-if="selectedWidget.name" class="noTopMargin">{{ selectedWidget.name }}</h2>
+
+ <div ng-if="selectedWidget.parameters" class="dimensionReport"
+ piwik-widget-loader="selectedWidget.parameters"></div>
+ </div>
+ <div class="clear"></div>
+
+</div> \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.js b/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.js
new file mode 100644
index 0000000000..1620be2a9d
--- /dev/null
+++ b/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.js
@@ -0,0 +1,67 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Renders a widget that is a container widget having the layout "ByDimension".
+ *
+ * The "ByDimension" layout shows a menu on the left letting you choose any widgets within this container. The
+ * currently selected widget is shown on the right.
+ *
+ * @param {Object} piwikWidgetByDimensionContainer a widget object as returned by the WidgetMetadata API.
+ *
+ * Example:
+ * <div piwik-widget-by-dimension-container="containerWidget"></div>
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikWidgetByDimensionContainer', piwikWidgetContainer);
+
+ piwikWidgetContainer.$inject = ['piwik', '$filter'];
+
+ function piwikWidgetContainer(piwik, $filter){
+ return {
+ restrict: 'A',
+ scope: {
+ container: '=piwikWidgetByDimensionContainer'
+ },
+ templateUrl: 'plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.html?cb=' + piwik.cacheBuster,
+ compile: function (element, attrs) {
+
+ return function (scope, element, attrs, ngModel) {
+
+ var widgetsSorted = $filter('orderBy')(scope.container.widgets, 'order');
+ var widgetsByCategory = {};
+
+ angular.forEach(widgetsSorted, function (widget) {
+ var category = widget.subcategory.name;
+
+ if (!widgetsByCategory[category]) {
+ widgetsByCategory[category] = {name: category, order: widget.order, widgets: []};
+ }
+
+ widgetsByCategory[category].widgets.push(widget);
+ });
+
+ // only an array can be sorted
+ var finalWidgetsByCategory = [];
+ angular.forEach(widgetsByCategory, function (category) {
+ finalWidgetsByCategory.push(category);
+ });
+
+ scope.widgetsByCategory = $filter('orderBy')(finalWidgetsByCategory, 'order');
+
+ scope.selectWidget = function (widget) {
+ scope.selectedWidget = widget;
+ }
+
+ if (widgetsSorted && widgetsSorted.length) {
+ scope.selectWidget(widgetsSorted[0]);
+ }
+ };
+ }
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.html b/plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.html
new file mode 100644
index 0000000000..21ebd3d0cd
--- /dev/null
+++ b/plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.html
@@ -0,0 +1,10 @@
+<div>
+ <!-- custom template prevents recursion -->
+ <script id="mywidget.html" type="text/ng-template">
+ <div piwik-widget="widget"></div>
+ </script>
+
+ <div ng-repeat="widget in container.widgets">
+ <div ng-include src="'mywidget.html'"/>
+ </div>
+</div> \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.js b/plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.js
new file mode 100644
index 0000000000..c378637c71
--- /dev/null
+++ b/plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.js
@@ -0,0 +1,32 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Renders a widget that is a container widget having no specific layout (which is the default).
+ *
+ * It shows all widgets vertically aligned one widget after another.
+ *
+ * @param {Object} piwikWidgetContainer a widget object as returned by the WidgetMetadata API.
+ *
+ * Example:
+ * <div piwik-widget-container="containerWidget"></div>
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikWidgetContainer', piwikWidgetContainer);
+
+ piwikWidgetContainer.$inject = ['piwik'];
+
+ function piwikWidgetContainer(piwik){
+ return {
+ restrict: 'A',
+ scope: {
+ container: '=piwikWidgetContainer'
+ },
+ templateUrl: 'plugins/CoreHome/angularjs/widget-container/widgetcontainer.directive.html?cb=' + piwik.cacheBuster
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.html b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.html
new file mode 100644
index 0000000000..d1f0cb51f1
--- /dev/null
+++ b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.html
@@ -0,0 +1,13 @@
+<div>
+
+ <div piwik-activity-indicator loading="loading"/>
+
+ <div ng-show="loadingFailed">
+ <div class="notification system notification-error">
+ {{ 'General_ErrorRequest'|translate:(''):('') }}
+ </div>
+ </div>
+
+ <div class="theWidgetContent"></div>
+
+</div> \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js
new file mode 100644
index 0000000000..4e1859e789
--- /dev/null
+++ b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js
@@ -0,0 +1,134 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Loads any custom widget or URL based on the given parameters.
+ *
+ * The currently active idSite, period, date and segment (if needed) is automatically appended to the parameters. If
+ * this widget is removed from the DOM and requests are in progress, these requests will be aborted. A loading message
+ * or an error message on failure is shown as well. It's kinda similar to ng-include but there it is not possible to
+ * listen to HTTP errors etc.
+ *
+ * Example:
+ * <div piwik-widget-loader="{module: '', action: '', ...}"></div>
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikWidgetLoader', piwikWidgetLoader);
+
+ piwikWidgetLoader.$inject = ['piwik', 'piwikUrl', '$http', '$compile', '$q'];
+
+ function piwikWidgetLoader(piwik, piwikUrl, $http, $compile, $q){
+ return {
+ restrict: 'A',
+ transclude: true,
+ scope: {
+ piwikWidgetLoader: '='
+ },
+ templateUrl: 'plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.html?cb=' + piwik.cacheBuster,
+ compile: function (element, attrs) {
+
+ return function (scope, element, attrs, ngModel) {
+ var changeCounter = 0,
+ currentScope,
+ currentElement,
+ httpCanceler,
+ contentNode = element.find('.theWidgetContent');
+
+ var cleanupLastWidgetContent = function() {
+ if (currentElement) {
+ currentElement.remove();
+ currentElement = null;
+ }
+ if (currentScope) {
+ currentScope.$destroy();
+ currentScope = null;
+ }
+ };
+
+ var abortHttpRequestIfNeeded = function () {
+ if (httpCanceler) {
+ httpCanceler.resolve();
+ httpCanceler = null;
+ }
+ }
+
+ function getFullWidgetUrl(parameters) {
+
+ var url = $.param(parameters);
+
+ var idSite = piwikUrl.getSearchParam('idSite');
+ var period = piwikUrl.getSearchParam('period');
+ var date = piwikUrl.getSearchParam('date');
+ var segment = piwikUrl.getSearchParam('segment');
+
+ url += '&idSite=' + idSite + '&period=' + period;
+ url += '&date=' + date + '&random=' + parseInt(Math.random() * 10000);
+
+ if (segment) {
+ url += '&segment=' + segment;
+ }
+
+ return '?' + url;
+ }
+
+ function loadWidgetUrl(parameters, thisChangeId)
+ {
+ scope.loading = true;
+
+ var url = getFullWidgetUrl(parameters);
+
+ abortHttpRequestIfNeeded();
+ cleanupLastWidgetContent();
+
+ httpCanceler = $q.defer();
+
+ $http.get(url, {timeout: httpCanceler.promise}).success(function(response) {
+ if (thisChangeId !== changeCounter || !response) {
+ // another widget was requested meanwhile, ignore this response
+ return;
+ }
+
+ httpCanceler = null;
+
+ var newScope = scope.$new();
+ currentScope = newScope;
+
+ scope.loading = false;
+ scope.loadingFailed = false;
+
+ currentElement = contentNode.html(response).children();
+ $compile(currentElement)(newScope);
+
+ }).error(function () {
+ if (thisChangeId !== changeCounter) {
+ // another widget was requested meanwhile, ignore this response
+ return;
+ }
+
+ httpCanceler = null;
+
+ cleanupLastWidgetContent();
+
+ scope.loading = false;
+ scope.loadingFailed = true;
+ });
+ }
+
+ scope.$watch('piwikWidgetLoader', function (parameters, oldUrl) {
+ if (parameters) {
+ loadWidgetUrl(parameters, ++changeCounter);
+ }
+ });
+
+ element.on('$destroy', function() {
+ abortHttpRequestIfNeeded();
+ });
+ };
+ }
+ };
+ }
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/widget/widget.directive.html b/plugins/CoreHome/angularjs/widget/widget.directive.html
new file mode 100644
index 0000000000..86cdc91291
--- /dev/null
+++ b/plugins/CoreHome/angularjs/widget/widget.directive.html
@@ -0,0 +1,23 @@
+<div id="{{ widget.uniqueId }}"
+ ng-show="view.showWidget"
+ class="{{widget.viewDataTable}}"
+ ng-class="{'smallTopMargin': (!widget.isFirstInPage && (!widget.name || widgetized))}"
+ >
+ <!--smallTopMargin: we display small margin if it's not the first widget on the page and if there's no headline -->
+ <h2 ng-if="!widget.parameters.widget && widget.name && !widgetized"
+ piwik-enriched-headline
+ ng-class="{'noTopMargin': widget.isFirstInPage}"
+ feature-name="{{widget.name}}">{{widget.name}}</h2>
+ <h2 ng-if="widget.parameters.widget && widget.name && !widgetized">{{widget.name}}</h2>
+
+ <div ng-if="!widget.isContainer && widget.parameters"
+ piwik-widget-loader="widget.parameters"></div>
+
+ <div ng-if="widget.isContainer && widget.layout!='ByDimension'">
+ <div piwik-widget-container="widget"></div>
+ </div>
+
+ <div ng-if="widget.isContainer && widget.layout=='ByDimension'">
+ <div piwik-widget-by-dimension-container="widget"></div>
+ </div>
+</div> \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/widget/widget.directive.js b/plugins/CoreHome/angularjs/widget/widget.directive.js
new file mode 100644
index 0000000000..86574870f2
--- /dev/null
+++ b/plugins/CoreHome/angularjs/widget/widget.directive.js
@@ -0,0 +1,93 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Renders any kind of widget. If you have a widget and you want to have it rendered, use this directive. It will
+ * display a name on top and the actual widget below. It can handle any kind of widget, no matter whether it is a
+ * regular widget or a container.
+ *
+ * @param {Object} piwikWidget A widget object as returned by the WidgetMetadata API.
+ * @param {Object} piwikWidget.middlewareParameters If present, we will request a URL using the given parameters and
+ * only if this URL returns a JSON `true` the widget will be shown.
+ * Otherwise the widget won't be shown.
+ * @param {String} containerId If you do not have a widget object but a containerId we will find the correct widget
+ * object based on the given containerId. Be aware that we might not find the widget if
+ * it is for example not available for the current user or period/date.
+ * @param {Boolean} widgetized true if the widget is widgetized (eg in Dashboard or exported). In this case we will add
+ * a URL parameter widget=1 to all widgets. Eg sparklines will be then displayed one after
+ * another (vertically aligned) instead of two next to each other.
+ *
+ * Example:
+ * <div piwik-widget="widget"></div>
+ * <div piwik-widget containerid="widgetGoalsOverview"></div> // in this case we will find the correct widget automatically
+ * <div piwik-widget="widget" widetized="true"></div> // disables rating feature, no initial headline
+ */
+(function () {
+ angular.module('piwikApp').directive('piwikWidget', piwikWidget);
+
+ piwikWidget.$inject = ['piwik', 'piwikApi'];
+
+ function piwikWidget(piwik, piwikApi){
+
+ function findContainerWidget(containerId, scope) {
+ widgetsHelper.getAvailableWidgets(function (categorizedWidgets) {
+
+ angular.forEach(categorizedWidgets, function (widgets) {
+ angular.forEach(widgets, function (widget) {
+
+ if (widget && widget.isContainer && widget.parameters.containerId === containerId) {
+ widget = angular.copy(widget);
+ if (scope.widgetized) {
+ widget.isFirstInPage = '1';
+ widget.parameters.widget = '1';
+ angular.forEach(widget.widgets, function (widget) {
+ widget.parameters.widget = '1';
+ });
+ }
+ scope.widget = widget;
+ applyMiddleware(scope);
+ }
+ });
+ });
+
+ });
+ }
+
+ function applyMiddleware(scope)
+ {
+ if (!scope.widget.middlewareParameters) {
+ scope.$eval('view.showWidget = true');
+ } else {
+ var params = angular.copy(scope.widget.middlewareParameters);
+ piwikApi.fetch(params).then(function (response) {
+ var enabled = response ? 'true' : 'false';
+ scope.$eval('view.showWidget = ' + enabled);
+ });
+ }
+ }
+
+ return {
+ restrict: 'A',
+ scope: {
+ widget: '=?piwikWidget',
+ widgetized: '=?',
+ containerid: '='
+ },
+ templateUrl: 'plugins/CoreHome/angularjs/widget/widget.directive.html?cb=' + piwik.cacheBuster,
+ compile: function (element, attrs) {
+
+ return function (scope, element, attrs, ngModel) {
+ if (scope.widget) {
+ applyMiddleware(scope);
+ } else if (attrs.containerid) {
+ findContainerWidget(attrs.containerid, scope);
+ }
+ }
+ }
+ };
+ }
+})(); \ No newline at end of file