diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php index 5d9a523a59681442bb9e3c3da12d7aeba02f0e53..5e24324539b07d49adf3f1eda657802353b8937e 100644 --- a/plugins/CoreHome/CoreHome.php +++ b/plugins/CoreHome/CoreHome.php @@ -68,6 +68,7 @@ class CoreHome extends \Piwik\Plugin $jsFiles[] = "libs/jquery/jquery.mousewheel.js"; $jsFiles[] = "libs/jquery/mwheelIntent.js"; $jsFiles[] = "libs/javascript/sprintf.js"; + $jsFiles[] = "libs/angularjs/angular.js"; $jsFiles[] = "plugins/Zeitgeist/javascripts/piwikHelper.js"; $jsFiles[] = "plugins/Zeitgeist/javascripts/ajaxHelper.js"; $jsFiles[] = "plugins/CoreHome/javascripts/require.js"; @@ -89,6 +90,11 @@ class CoreHome extends \Piwik\Plugin $jsFiles[] = "plugins/CoreHome/javascripts/color_manager.js"; $jsFiles[] = "plugins/CoreHome/javascripts/notification.js"; $jsFiles[] = "plugins/CoreHome/javascripts/notification_parser.js"; + $jsFiles[] = "plugins/CoreHome/javascripts/piwikApp.js"; + $jsFiles[] = "plugins/CoreHome/javascripts/filters/filters.js"; + $jsFiles[] = "plugins/CoreHome/javascripts/dependencies/PiwikApi.js"; + $jsFiles[] = "plugins/CoreHome/javascripts/directives/directives.js"; + $jsFiles[] = "plugins/CoreHome/javascripts/siteselector/siteSelectorController.js"; } public function getClientSideTranslationKeys(&$translationKeys) @@ -98,6 +104,7 @@ class CoreHome extends \Piwik\Plugin $translationKeys[] = 'General_Show'; $translationKeys[] = 'General_Hide'; $translationKeys[] = 'General_YearShort'; + $translationKeys[] = 'General_MultiSitesSummary'; $translationKeys[] = 'CoreHome_YouAreUsingTheLatestVersion'; $translationKeys[] = 'CoreHome_IncludeRowsWithLowPopulation'; $translationKeys[] = 'CoreHome_ExcludeRowsWithLowPopulation'; @@ -107,6 +114,7 @@ class CoreHome extends \Piwik\Plugin $translationKeys[] = 'CoreHome_PageOf'; $translationKeys[] = 'CoreHome_FlattenDataTable'; $translationKeys[] = 'CoreHome_UnFlattenDataTable'; + $translationKeys[] = 'SitesManager_NotFound'; $translationKeys[] = 'Annotations_ViewAndAddAnnotations'; $translationKeys[] = 'General_RowEvolutionRowActionTooltipTitle'; $translationKeys[] = 'General_RowEvolutionRowActionTooltip'; diff --git a/plugins/CoreHome/javascripts/dependencies/PiwikApi.js b/plugins/CoreHome/javascripts/dependencies/PiwikApi.js new file mode 100644 index 0000000000000000000000000000000000000000..87328e6239d086a86ef1f5f9f7763258881edef3 --- /dev/null +++ b/plugins/CoreHome/javascripts/dependencies/PiwikApi.js @@ -0,0 +1,202 @@ +/*! + * Piwik - Web Analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +piwikApp.factory('piwikApi', function ($http, $q, $rootScope) { + + var url = 'index.php'; + var format = 'json'; + var getParams = {}; + var postParams = {}; + + var piwikApi = {}; + + /** + * Adds params to the request. + * If params are given more then once, the latest given value is used for the request + * + * @param {object} params + * @param {string} type type of given parameters (POST or GET) + * @return {void} + */ + piwikApi.addParams = function (params, type) { + if (typeof params == 'string') { + params = broadcast.getValuesFromUrl(params); + } + + for (var key in params) { + if(type.toLowerCase() == 'get') { + getParams[key] = params[key]; + } else if(type.toLowerCase() == 'post') { + postParams[key] = params[key]; + } + } + }; + + /** + * Sets the base URL to use in the AJAX request. + * + * @param {string} url + */ + piwikApi.setUrl = function (url) { + this.addParams(broadcast.getValuesFromUrl(url), 'GET'); + }; + + /** + * Gets this helper instance ready to send a bulk request. Each argument to this + * function is a single request to use. + */ + piwikApi.setBulkRequests = function () { + var urls = []; + for (var i = 0; i != arguments.length; ++i) { + urls.push($.param(arguments[i])); + } + + this.addParams({ + module: 'API', + method: 'API.getBulkRequest', + urls: urls, + format: 'json' + }, 'post'); + }; + + /** + * Sets the response format for the request + * + * @param {string} theFormat response format (e.g. json, html, ...) + * @return {void} + */ + piwikApi.setFormat = function (theFormat) { + format = theFormat; + }; + + /** + * Send the request + * @param {Boolean} [sync] indicates if the request should be synchronous (defaults to false) + * @return {void} + */ + piwikApi.send = function () { + + var deferred = $q.defer(); + + var onError = function (message) { + deferred.reject(message); + }; + + var onSuccess = function (response) { + if (response && response.result == 'error') { + + if (response.message) { + onError(response.message); + + var UI = require('piwik/UI'); + var notification = new UI.Notification(); + notification.show(response.message, { + context: 'error', + type: 'toast', + id: 'ajaxHelper' + }); + notification.scrollToNotification(); + } else { + onError(null); + } + + } else { + deferred.resolve(response); + } + }; + + var ajaxCall = { + method: 'POST', + url: url, + responseType: format, + params: _mixinDefaultGetParams(getParams), + data: $.param(_mixinDefaultPostParams(postParams)), + timeout: deferred.promise, + headers: {'Content-Type': 'application/x-www-form-urlencoded'} + }; + + $http(ajaxCall).success(onSuccess).error(onError); + + return deferred.promise; + }; + + /** + * Mixin the default parameters to send as POST + * + * @param {object} params parameter object + * @return {object} + * @private + */ + function _mixinDefaultPostParams (params) { + + var defaultParams = { + token_auth: piwik.token_auth + }; + + for (var index in defaultParams) { + if (!params[index]) { + params[index] = defaultParams[index]; + } + } + + return params; + }; + + /** + * Mixin the default parameters to send as GET + * + * @param {object} getParamsToMixin parameter object + * @return {object} + * @private + */ + function _mixinDefaultGetParams (getParamsToMixin) { + + var defaultParams = { + idSite: piwik.idSite || broadcast.getValueFromUrl('idSite'), + period: piwik.period || broadcast.getValueFromUrl('period'), + segment: broadcast.getValueFromHash('segment', window.location.href.split('#')[1]) + }; + + // never append token_auth to url + if (getParamsToMixin.token_auth) { + getParamsToMixin.token_auth = null; + delete getParamsToMixin.token_auth; + } + + for (var key in defaultParams) { + if (!getParamsToMixin[key] && !postParams[key] && defaultParams[key]) { + getParamsToMixin[key] = defaultParams[key]; + } + } + + // handle default date & period if not already set + if (!getParamsToMixin.date && !postParams.date) { + getParamsToMixin.date = piwik.currentDateString || broadcast.getValueFromUrl('date'); + if (getParamsToMixin.period == 'range' && piwik.currentDateString) { + getParamsToMixin.date = piwik.startDateString + ',' + getParamsToMixin.date; + } + } + + return getParamsToMixin; + }; + + /** + * Convenient method for making an API request + * @param getParams + */ + piwikApi.fetch = function (getParams) { + + getParams.module = 'API'; + getParams.format = 'JSON'; + + this.addParams(getParams, 'GET'); + + return this.send(); + }; + + return piwikApi; +}); diff --git a/plugins/CoreHome/javascripts/directives/directives.js b/plugins/CoreHome/javascripts/directives/directives.js new file mode 100644 index 0000000000000000000000000000000000000000..028a54e8f65927069c0d7fe7fe7947018a561a62 --- /dev/null +++ b/plugins/CoreHome/javascripts/directives/directives.js @@ -0,0 +1,57 @@ +/*! + * Piwik - Web Analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +piwikApp.directive('piwikFocusAnywhereButHere', function($document){ + return { + restrict: 'A', + link: function(scope, element, attr, ctrl) { + + function onClickOutsideElement (event) { + if (element.has(event.target).length === 0) { + scope.$apply(attr.piwikFocusAnywhereButHere); + } + } + + function onEscapeHandler (event) { + if (event.which === 27) { + scope.$apply(attr.piwikFocusAnywhereButHere); + } + } + + $document.on('keyup', onEscapeHandler); + $document.on('mouseup', onClickOutsideElement); + scope.$on('$destroy', function() { + $document.off('mouseup', onClickOutsideElement); + $document.off('keyup', onEscapeHandler); + }); + } + } +}); + +piwikApp.directive('piwikAutocompleteMatched', function() { + return function(scope, element, attrs) { + var searchTerm; + + scope.$watch(attrs.autocompleteMatched, function(value) { + searchTerm = value; + updateText(); + }); + + function updateText () { + if (!searchTerm) { + return; + } + var content = element.text(); + var startTerm = content.toLowerCase().indexOf(searchTerm); + if (-1 !== startTerm) { + var word = content.substr(startTerm, searchTerm.length); + content = content.replace(word, '<span class="autocompleteMatched">' + word + '</span>'); + element.html(content); + }; + } + }; +}); \ No newline at end of file diff --git a/plugins/CoreHome/javascripts/filters/filters.js b/plugins/CoreHome/javascripts/filters/filters.js new file mode 100644 index 0000000000000000000000000000000000000000..8e8002bf785f1c2ab4cbbd6b597b97fb0571f78a --- /dev/null +++ b/plugins/CoreHome/javascripts/filters/filters.js @@ -0,0 +1,21 @@ +/*! + * Piwik - Web Analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +piwikApp.filter('translate', function() { + return function(key) { + return _pk_translate(key); + } +}); + +piwikApp.filter('default', function() { + return function(value, defaultValue) { + if (!value) { + return defaultValue; + } + return value; + } +}); diff --git a/plugins/CoreHome/javascripts/piwikApp.js b/plugins/CoreHome/javascripts/piwikApp.js new file mode 100644 index 0000000000000000000000000000000000000000..36dc2c55c4cf2043b1576c5c2dcb655436a77d10 --- /dev/null +++ b/plugins/CoreHome/javascripts/piwikApp.js @@ -0,0 +1,2 @@ +var piwikApp = angular.module('piwikApp', []); +var customApp = angular.module('app', []); diff --git a/plugins/CoreHome/javascripts/siteselector/siteSelector.tpl.html b/plugins/CoreHome/javascripts/siteselector/siteSelector.tpl.html new file mode 100644 index 0000000000000000000000000000000000000000..2a01bf5f131622f4c0e8f03e02f3aca81ada2b6a --- /dev/null +++ b/plugins/CoreHome/javascripts/siteselector/siteSelector.tpl.html @@ -0,0 +1,49 @@ +<div ng-class="{'sites_autocomplete--dropdown': (hasMultipleWebsites || showAllSitesItem)}" + id="{{ siteSelectorId }}"> + <div piwik-focus-anywhere-but-here="showSitesList=false" class="custom_select"> + + <input ng-if="inputName" ng-model="selectedSiteId" type="hidden" name="{{ inputName }}" value=""/> + + <a ng-click="showSitesList=!showSitesList; showSitesList && loadInitialSites()" href="javascript:void(0)" + class="custom_select_main_link" ng-class="{'loading': isLoading}" + data-siteid="{{ selectedSiteId }}"> + <span>{{ siteName }}</span> + </a> + + <div ng-show="showSitesList" class="custom_select_block"> + + <div ng-click="siteName='Ddd'" ng-show="allWebsitesLinkLocation=='top' && showAllSitesItem" class="custom_select_all" style="clear: both;"> + <a href="#"> + {{ allSitesItemText | default:'General_MultiSitesSummary'|translate }} + </a> + </div> + + <div class="custom_select_container"> + <ul class="custom_select_ul_list"> + <li ng-focus="searchTerm=site.name" ng-click="$parent.showSitesList=false; switchSite(site)" ng-repeat="site in sites" ng-hide="!showSelectedSite && piwik.idSite==site.idsite"> + <a href="javascript:void(0)" piwik-autocomplete-matched="searchTerm">{{ site.name }}</a> + </li> + </ul> + <ul ng-show="!sites.length" class="ui-autocomplete ui-front ui-menu ui-widget ui-widget-content ui-corner-all siteSelect"> + <li class="ui-menu-item" style="float:none;position:static"> + <a class="ui-corner-all" style="float:none;position:static" tabindex="-1">{{ ('SitesManager_NotFound' | translate) + ' ' + searchTerm }}</a> + </li> + </ul> + </div> + + <div ng-click="siteName='Ddd'" ng-show="allWebsitesLinkLocation=='bottom' && showAllSitesItem" class="custom_select_all" style="clear: both;"> + <a href="#"> + {{ allSitesItemText | default:'General_MultiSitesSummary'|translate }} + </a> + </div> + + <div class="custom_select_search" ng-show="show_autocompleter"> + <input type="text" ng-focus="showSitesList" ng-click="searchTerm=''" ng-model="searchTerm" ng-change="searchSite(searchTerm)" length="15" class="websiteSearch inp"/> + <input type="submit" value="Search" class="but"/> + <img title="Clear" ng-show="searchTerm" ng-click="searchTerm=''; loadInitialSites()" class="reset" style="position: relative; top: 4px; left: -44px; cursor: pointer;" + src="plugins/CoreHome/images/reset_search.png"/> + </div> + </div> + </div> + +</div> diff --git a/plugins/CoreHome/javascripts/siteselector/siteSelectorController.js b/plugins/CoreHome/javascripts/siteselector/siteSelectorController.js new file mode 100644 index 0000000000000000000000000000000000000000..b099cdede4e7eb69a8ba53f43864f5b2a3aa305c --- /dev/null +++ b/plugins/CoreHome/javascripts/siteselector/siteSelectorController.js @@ -0,0 +1,91 @@ +/*! + * Piwik - Web Analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +piwikApp.controller('SiteSelectorCtrl', ['$scope', 'piwikApi', function($scope, piwikApi){ + var filterLimit = 10; + + $scope.templateUrl = 'plugins/CoreHome/javascripts/siteselector//siteSelector.tpl.html'; + $scope.allWebsitesLinkLocation = 'bottom'; + $scope.sites = []; + $scope.showSelectedSite = false; + $scope.show_autocompleter = true; + $scope.siteSelectorId = ''; + $scope.switchSiteOnSelect = false; + $scope.hasMultipleWebsites = false; + $scope.isLoading = false; + $scope.showAllSitesItem = true; + $scope.selectedSiteId = 0; + $scope.searchTerm = ''; + $scope.max_sitename_width = 130; // can be removed? + + $scope.switchSite = function (site) { + if (!$scope.switchSiteOnSelect || piwik.idSite == site.idsite) { + $scope.selectedSiteId = site.idsite; + $scope.siteName = site.name; + return; + } + + if (site.idsite == 'all' && !$scope.showAllSitesItem) { + broadcast.propagateNewPage('module=MultiSites&action=index'); + } else { + broadcast.propagateNewPage($scope.getUrlForWebsiteId(site.idsite), false); + } + }; + + $scope.getUrlForWebsiteId = function (idSite) { + var idSiteParam = 'idSite=' + idSite; + var newParameters = 'segment=&' + idSiteParam; + var hash = broadcast.isHashExists() ? broadcast.getHashFromUrl() : ""; + return piwikHelper.getCurrentQueryStringWithParametersModified(newParameters) + + '#' + piwikHelper.getQueryStringWithParametersModified(hash.substring(1), newParameters); + }; + + $scope.updateWebsitesList = function (websites) { + angular.forEach(websites, function (website) { + website.name = piwikHelper.htmlDecode(website.name); + }); + + $scope.sites = websites; + + if (!$scope.siteName) { + $scope.siteName = websites[0].name; + } + + $scope.hasMultipleWebsites = websites.length > 1; + }; + + $scope.searchSite = function (term) { + if (!term) { + $scope.loadInitialSites(); + return; + } + $scope.isLoading = true; + piwikApi.fetch({ + method: 'SitesManager.getPatternMatchSites', + filter_limit: filterLimit, + pattern: term + }).then(function (response) { + $scope.updateWebsitesList(response); + }).finally(function () { + $scope.isLoading = false; + }); + }; + + $scope.loadInitialSites = function () { + $scope.isLoading = true; + piwikApi.fetch({ + method: 'SitesManager.getSitesWithAtLeastViewAccess', + filter_limit: filterLimit, + showColumns: 'name,idsite' + }).then(function (response) { + $scope.updateWebsitesList(response); + }).finally(function () { + $scope.isLoading = false; + }); + } + +}]); \ No newline at end of file diff --git a/plugins/CoreHome/javascripts/siteselector/tests.js b/plugins/CoreHome/javascripts/siteselector/tests.js new file mode 100644 index 0000000000000000000000000000000000000000..92b3089007dd8b0064c69665503ccce8920f565e --- /dev/null +++ b/plugins/CoreHome/javascripts/siteselector/tests.js @@ -0,0 +1,17 @@ +/*! + * Piwik - Web Analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +describe('SiteSelector Tests', function() { + beforeEach(module(function() { + return function(inject1, inject2) { + } + })); + + it('should fetch all websites', function() { + expect(1).toBe(1); + }); +}); \ No newline at end of file diff --git a/plugins/CoreHome/templates/_siteSelect.twig b/plugins/CoreHome/templates/_siteSelect.twig index 071673c1e073e69b30ff3e62aebf0a6cda6c7319..84092ac04e87c06008fc555aa2581a222ffbba08 100644 --- a/plugins/CoreHome/templates/_siteSelect.twig +++ b/plugins/CoreHome/templates/_siteSelect.twig @@ -1,67 +1 @@ -{# The following parameters can be used to customize this widget when 'include'-ing: - * - * - showAllSitesItem true if the 'All Websites' option should be shown, false if otherwise. Default = true. - * - allSitesItemText The text to use for the 'All Websites' option. Default = 'All Websites'. - * - allWebsitesLinkLocation The location of the 'All Websites' option. Can be 'top' or 'bottom'. Default = 'bottom'. - * - showSelectedSite false if the currently selected site should be excluded from the list of sites. Default = false. - * - sites The list of sites to use. By default, the first N sites are used. They are retrieved in View. - * - show_autocompleter Whether to show the autocompleter or not. Default true. - * - switchSiteOnSelect Whether to change the page w/ a new idSite value when a site is selected, or not. - * Default = true. - * - inputName If set, a hidden <input> w/ name == $inputName is created which will hold the selected site's ID. For - * use with <form> elements. - * - siteName The currently selected site name. Defaults to the first name in $sites set by View. - * - idSite The currently selected idSite. Defaults to the first id in $sites set by View. - #} -{% set sitesSelector_allWebsitesLink %} - <div class="custom_select_all" style="clear: both;"> - <a href="#" {% if showAllSitesItem is defined and showAllSitesItem == false %}style="display:none;"{% endif %}> - {% if allSitesItemText is defined %} - {{ allSitesItemText }} - {% else %} - {{ 'General_MultiSitesSummary'|translate }} - {% endif %} - </a> - </div> -{% endset %} -<div class="sites_autocomplete {% if sites|length > 1 %}sites_autocomplete--dropdown{% endif %}" - {% if siteSelectorId is defined %}id="{{ siteSelectorId }}"{% endif %} - {% if switchSiteOnSelect is not defined or switchSiteOnSelect %}data-switch-site-on-select="1"{% endif %}> - <div class="custom_select"> - - <a href="#" onclick="return false" class="custom_select_main_link" data-loading="0" data-siteid="{% if idSite is defined %}{{ idSite }}{% else %}{{ sites[0].idsite }}{% endif %}"> - <span>{% if siteName is defined %}{{ siteName|raw }}{% else %}{{ sites[0].name|raw }}{% endif %}</span> - </a> - - <div class="custom_select_block"> - {% if allWebsitesLinkLocation is defined and allWebsitesLinkLocation == 'top' %} - {{ sitesSelector_allWebsitesLink }} - {% endif %} - <div class="custom_select_container"> - <ul class="custom_select_ul_list"> - {% for info in sites %} - <li {% if (showSelectedSite is not defined or showSelectedSite == false) and idSite == info.idsite %} style="display: none;"{% endif %}> - <a href="#" data-siteid="{{ info.idsite }}">{{ info.name|raw }}</a> - </li> - {% endfor %} - </ul> - </div> - {% if allWebsitesLinkLocation is not defined or allWebsitesLinkLocation == 'bottom' %} - {{ sitesSelector_allWebsitesLink }} - {% endif %} - <div class="custom_select_search" {% if show_autocompleter == false %}style="display:none;"{% endif %}> - <input type="text" length="15" class="websiteSearch inp"/> - <input type="hidden" class="max_sitename_width" value="130"/> - <input type="submit" value="Search" class="but"/> - <img title="Clear" class="reset" style="position: relative; top: 4px; left: -44px; cursor: pointer; display: none;" - src="plugins/CoreHome/images/reset_search.png"/> - </div> - </div> - </div> - {% if inputName is defined %} - <input type="hidden" name="{{ inputName }}" value="{% if idSite is defined %}{{ idSite }}{% else %}{{ sites[0].idsite }}{% endif %}"/> - {% endif %} -</div> -<script type="text/javascript"> - $(document).ready(function () { piwik.initSiteSelectors(); }); -</script> +<div ng-include src="templateUrl" ng-init="siteName='test';inputName='test2'" ng-controller="SiteSelectorCtrl" class="sites_autocomplete"></div> \ No newline at end of file diff --git a/plugins/Morpheus/stylesheets/forms.less b/plugins/Morpheus/stylesheets/forms.less index 95f79d2e39a6aab8f87f673b38b338b9e65d38ff..842df5164d0b5595fca5942b80d7fb59fa25ae20 100644 --- a/plugins/Morpheus/stylesheets/forms.less +++ b/plugins/Morpheus/stylesheets/forms.less @@ -79,7 +79,7 @@ button[type="button"], .sites_autocomplete--dropdown { - .custom_select_main_link[data-loading="0"]:before { + .custom_select_main_link:not(.loading):before { color: @brand-red; font-size: 0.7em; top: 2px; diff --git a/plugins/Zeitgeist/stylesheets/ui/_siteSelect.less b/plugins/Zeitgeist/stylesheets/ui/_siteSelect.less index 80dcf97d1c45764d74efbbe26cf8a2e46088802b..cd6dd28ff5d075b0fd2aa248372900fbacc005f2 100644 --- a/plugins/Zeitgeist/stylesheets/ui/_siteSelect.less +++ b/plugins/Zeitgeist/stylesheets/ui/_siteSelect.less @@ -64,7 +64,7 @@ padding: 0 20px 0 4px; } -.sites_autocomplete--dropdown .custom_select_main_link[data-loading="0"]:before { +.sites_autocomplete--dropdown .custom_select_main_link:not(.loading):before { content: " \25BC"; position: absolute; right: 0; @@ -78,7 +78,7 @@ position: relative; } -.sites_autocomplete .custom_select_main_link[data-loading="1"] { +.sites_autocomplete .custom_select_main_link.loading { background: url(plugins/Zeitgeist/images/loading-blue.gif) no-repeat right 3px; } @@ -144,9 +144,8 @@ } .custom_select_block { - height: 0; overflow: hidden; - max-width:140px; + max-width: inherit } .custom_select_block_show { diff --git a/plugins/Zeitgeist/templates/dashboard.twig b/plugins/Zeitgeist/templates/dashboard.twig index 282f51f6c8c9d469de1d422d6942b3d9e6d2d8e6..d2778327a45ced9f83953f49358ef430aded6921 100644 --- a/plugins/Zeitgeist/templates/dashboard.twig +++ b/plugins/Zeitgeist/templates/dashboard.twig @@ -1,8 +1,8 @@ <!DOCTYPE html> <!--[if lt IE 9 ]> -<html class="old-ie"> <![endif]--> +<html class="old-ie" id="ng-app" ng-app="piwikApp"> <![endif]--> <!--[if (gte IE 9)|!(IE)]><!--> -<html><!--<![endif]--> +<html id="ng-app" ng-app="piwikApp"><!--<![endif]--> <head> {% block head %} <meta charset="utf-8"> @@ -23,7 +23,7 @@ <![endif]--> {% endblock %} </head> - <body> + <body ng-app="app"> {% include "_iframeBuster.twig" %} {% include "@CoreHome/_javaScriptDisabled.twig" %}