diff options
author | Benaka Moorthi <benaka.moorthi@gmail.com> | 2013-09-12 03:39:59 +0400 |
---|---|---|
committer | Benaka Moorthi <benaka.moorthi@gmail.com> | 2013-09-12 03:39:59 +0400 |
commit | 6d884e19f5d870c8c0647ec2bac68fa546115578 (patch) | |
tree | 1e1dbda09d046cf06e1644dba48e47159fc52722 /plugins | |
parent | 94eff35d47c5be86d88b51e933bec3fb8ee450cd (diff) |
Refs #3089, #4116, display visitor profile popup when visit in realtime map is clicked.
Notes:
- Includes new UIControl base type.
- Fixes cleanup bugs in realtime map and popover closing.
- Refactored realtime map so as little JavaScript as possible is included in HTML fragments.
- Allow more than one realtime map to exist on a single page.
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/CoreHome/CoreHome.php | 1 | ||||
-rw-r--r-- | plugins/CoreHome/javascripts/broadcast.js | 3 | ||||
-rw-r--r-- | plugins/CoreHome/javascripts/popover.js | 1 | ||||
-rw-r--r-- | plugins/CoreHome/javascripts/uiControl.js | 75 | ||||
-rwxr-xr-x | plugins/Dashboard/javascripts/dashboardWidget.js | 1 | ||||
-rw-r--r-- | plugins/Live/javascripts/visitorProfile.js | 12 | ||||
-rw-r--r-- | plugins/UserCountryMap/Controller.php | 67 | ||||
-rw-r--r-- | plugins/UserCountryMap/javascripts/realtime-map.js | 116 | ||||
-rw-r--r-- | plugins/UserCountryMap/stylesheets/realtime-map.less | 4 | ||||
-rw-r--r-- | plugins/UserCountryMap/templates/realtimeMap.twig | 61 |
10 files changed, 236 insertions, 105 deletions
diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php index 64fe4b56dd..5ed00a718a 100644 --- a/plugins/CoreHome/CoreHome.php +++ b/plugins/CoreHome/CoreHome.php @@ -72,6 +72,7 @@ class CoreHome extends \Piwik\Plugin $jsFiles[] = "plugins/Zeitgeist/javascripts/piwikHelper.js"; $jsFiles[] = "plugins/Zeitgeist/javascripts/ajaxHelper.js"; $jsFiles[] = "plugins/CoreHome/javascripts/require.js"; + $jsFiles[] = "plugins/CoreHome/javascripts/uiControl.js"; $jsFiles[] = "plugins/CoreHome/javascripts/dataTable.js"; $jsFiles[] = "plugins/CoreHome/javascripts/dataTable_rowactions.js"; $jsFiles[] = "plugins/CoreHome/javascripts/popover.js"; diff --git a/plugins/CoreHome/javascripts/broadcast.js b/plugins/CoreHome/javascripts/broadcast.js index fe30534120..19aeb74bbc 100644 --- a/plugins/CoreHome/javascripts/broadcast.js +++ b/plugins/CoreHome/javascripts/broadcast.js @@ -130,6 +130,9 @@ var broadcast = { // make sure the "Widgets & Dashboard" is deleted on reload $('#dashboardSettings').remove(); $('#dashboardWidgetsArea').dashboard('destroy'); + + // remove unused controls + require('piwik/UI').UIControl.cleanupUnusedControls(); } } diff --git a/plugins/CoreHome/javascripts/popover.js b/plugins/CoreHome/javascripts/popover.js index e3ff6d9326..22b23eab38 100644 --- a/plugins/CoreHome/javascripts/popover.js +++ b/plugins/CoreHome/javascripts/popover.js @@ -45,6 +45,7 @@ var Piwik_Popover = (function () { $('.ui-widget-overlay').off('click.popover'); isOpen = false; broadcast.propagateNewPopoverParameter(false); + require('piwik/UI').UIControl.cleanupUnusedControls(); if (typeof closeCallback == 'function') { closeCallback(); closeCallback = false; diff --git a/plugins/CoreHome/javascripts/uiControl.js b/plugins/CoreHome/javascripts/uiControl.js new file mode 100644 index 0000000000..58d9662acb --- /dev/null +++ b/plugins/CoreHome/javascripts/uiControl.js @@ -0,0 +1,75 @@ +/** + * Piwik - Web Analytics + * + * Visitor profile popup control. + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +(function ($, require) { + + var exports = require('piwik/UI'); + + /** + * Base type for Piwik UI controls. Provides functionality that all controls need (such as + * cleanup on destruction). + * + * @param {Element} element The root element of the control. + */ + var UIControl = function (element) { + this._controlIndex = UIControl._controls.length; + UIControl._controls.push(this); + + var $element = this.$element = $(element); + $element.data('uiControlObject', this); + }; + + /** + * Contains all active control instances. + */ + UIControl._controls = []; + + /** + * Utility method that will clean up all piwik UI controls whose elements are not attached + * to the DOM. + * + * TODO: instead of having other pieces of the UI manually calling cleanupUnusedControls, + * MutationObservers should be called + */ + UIControl.cleanupUnusedControls = function () { + var controls = UIControl._controls; + + for (var i = 0; i != controls.length; ++i) { + var control = controls[i]; + if (control.$element + && !$.contains(document.documentElement, control.$element[0]) + ) { + controls[i] = null; + control._destroy(); + + if (!control._baseDestroyCalled) { + throw new Error("Error: " + control.constructor.name + "'s destroy method does not call " + + "UIControl.destroy. You may have a memory leak."); + } + } + } + }; + + UIControl.prototype = { + + /** + * Perform cleanup. Called when the control has been removed from the DOM. Derived + * classes should overload this function to perform their own cleanup. + */ + _destroy: function () { + this.$element.removeData('uiControlObject'); + delete this.$element; + + this._baseDestroyCalled = true; + }, + }; + + exports.UIControl = UIControl; + +})(jQuery, require);
\ No newline at end of file diff --git a/plugins/Dashboard/javascripts/dashboardWidget.js b/plugins/Dashboard/javascripts/dashboardWidget.js index ccbf68ba9c..a59ed325c4 100755 --- a/plugins/Dashboard/javascripts/dashboardWidget.js +++ b/plugins/Dashboard/javascripts/dashboardWidget.js @@ -68,6 +68,7 @@ } $('*', this.element).off('.dashboardWidget'); // unbind all events $('.widgetContent', this.element).trigger('widget:destroy'); + require('piwik/UI').UIControl.cleanupUnusedControls(); return this; }, diff --git a/plugins/Live/javascripts/visitorProfile.js b/plugins/Live/javascripts/visitorProfile.js index 5b85211b0c..112f2c0781 100644 --- a/plugins/Live/javascripts/visitorProfile.js +++ b/plugins/Live/javascripts/visitorProfile.js @@ -10,7 +10,8 @@ (function ($, require) { var piwik = require('piwik'), - exports = require('piwik/UI'); + exports = require('piwik/UI'), + UIControl = exports.UIControl; /** * Sets up and handles events for the visitor profile popup. @@ -20,7 +21,7 @@ * @constructor */ var VisitorProfileControl = function (element) { - this.$element = $(element).focus(); + UIControl.call(this, element); this._setupControl(); this._bindEventCallbacks(); }; @@ -53,9 +54,12 @@ Piwik_Popover.createPopupAndLoadUrl(url, '', 'visitor-profile-popup'); }; - VisitorProfileControl.prototype = { + $.extend(VisitorProfileControl.prototype, UIControl.prototype, { _setupControl: function () { + // focus the popup so it will accept key events + this.$element.focus(); + // highlight the first visit $('.visitor-profile-visits>li:first-child', this.$element).addClass('visitor-profile-current-visit'); }, @@ -232,7 +236,7 @@ _inWidget: function () { return !! this.$element.closest('.widget').length; } - }; + }); exports.VisitorProfileControl = VisitorProfileControl; diff --git a/plugins/UserCountryMap/Controller.php b/plugins/UserCountryMap/Controller.php index 539e3dadb0..bf0dffd95d 100644 --- a/plugins/UserCountryMap/Controller.php +++ b/plugins/UserCountryMap/Controller.php @@ -131,40 +131,50 @@ class Controller extends \Piwik\Controller $view->metrics = $this->getMetrics($idSite, 'range', self::REAL_TIME_WINDOW, $token_auth); $view->defaultMetric = 'nb_visits'; - $view->liveRefreshAfterMs = (int)Config::getInstance()->General['live_widget_refresh_after_seconds'] * 1000; + $liveRefreshAfterMs = (int)Config::getInstance()->General['live_widget_refresh_after_seconds'] * 1000; $goals = API::getInstance()->getGoals($idSite); $site = new Site($idSite); - $view->hasGoals = !empty($goals) || $site->isEcommerceEnabled() ? 'true' : 'false'; + $hasGoals = !empty($goals) || $site->isEcommerceEnabled(); // maximum number of visits to be displayed in the map - $view->maxVisits = Common::getRequestVar('format_limit', 100, 'int'); + $maxVisits = Common::getRequestVar('format_limit', 100, 'int'); // some translations - $view->localeJSON = json_encode(array( - 'nb_actions' => Piwik_Translate('VisitsSummary_NbActionsDescription'), - 'local_time' => Piwik_Translate('VisitTime_ColumnLocalTime'), - 'from' => Piwik_Translate('General_FromReferrer'), - 'seconds' => Piwik_Translate('UserCountryMap_Seconds'), - 'seconds_ago' => Piwik_Translate('UserCountryMap_SecondsAgo'), - 'minutes' => Piwik_Translate('UserCountryMap_Minutes'), - 'minutes_ago' => Piwik_Translate('UserCountryMap_MinutesAgo'), - 'hours' => Piwik_Translate('UserCountryMap_Hours'), - 'hours_ago' => Piwik_Translate('UserCountryMap_HoursAgo'), - 'days_ago' => Piwik_Translate('UserCountryMap_DaysAgo'), - 'actions' => Piwik_Translate('VisitsSummary_NbPageviewsDescription'), - 'searches' => Piwik_Translate('UserCountryMap_Searches'), - 'goal_conversions' => Piwik_Translate('UserCountryMap_GoalConversions'), - )); + $locale = array( + 'nb_actions' => Piwik_Translate('VisitsSummary_NbActionsDescription'), + 'local_time' => Piwik_Translate('VisitTime_ColumnLocalTime'), + 'from' => Piwik_Translate('General_FromReferrer'), + 'seconds' => Piwik_Translate('UserCountryMap_Seconds'), + 'seconds_ago' => Piwik_Translate('UserCountryMap_SecondsAgo'), + 'minutes' => Piwik_Translate('UserCountryMap_Minutes'), + 'minutes_ago' => Piwik_Translate('UserCountryMap_MinutesAgo'), + 'hours' => Piwik_Translate('UserCountryMap_Hours'), + 'hours_ago' => Piwik_Translate('UserCountryMap_HoursAgo'), + 'days_ago' => Piwik_Translate('UserCountryMap_DaysAgo'), + 'actions' => Piwik_Translate('VisitsSummary_NbPageviewsDescription'), + 'searches' => Piwik_Translate('UserCountryMap_Searches'), + 'goal_conversions' => Piwik_Translate('UserCountryMap_GoalConversions'), + ); $segment = $segmentOverride ?: Request::getRawSegmentFromRequest() ?: ''; - $view->reqParamsJSON = $this->getEnrichedRequest(array( - 'period' => 'range', - 'idSite' => $idSite, - 'date' => self::REAL_TIME_WINDOW, - 'segment' => $segment, - 'token_auth' => $token_auth, - )); + $reqParams = $this->getEnrichedRequest(array( + 'period' => 'range', + 'idSite' => $idSite, + 'date' => self::REAL_TIME_WINDOW, + 'segment' => $segment, + 'token_auth' => $token_auth, + ), $encode = false); + + $view->config = array( + 'metrics' => array(), + 'svgBasePath' => $view->piwikUrl . 'plugins/UserCountryMap/svg/', + 'liveRefreshAfterMs' => $liveRefreshAfterMs, + '_' => $locale, + 'reqParams' => $reqParams, + 'siteHasGoals' => $hasGoals, + 'maxVisits' => $maxVisits + ); if ($fetch) { return $view->render(); @@ -173,7 +183,7 @@ class Controller extends \Piwik\Controller } } - private function getEnrichedRequest($params) + private function getEnrichedRequest($params, $encode = true) { $params['format'] = 'json'; $params['showRawMetrics'] = 1; @@ -184,7 +194,10 @@ class Controller extends \Piwik\Controller } } - return Common::json_encode($params); + if ($encode) { + $params = Common::json_encode($params); + } + return $params; } private function checkUserCountryPluginEnabled() diff --git a/plugins/UserCountryMap/javascripts/realtime-map.js b/plugins/UserCountryMap/javascripts/realtime-map.js index c851485a6b..e4e4eef09f 100644 --- a/plugins/UserCountryMap/javascripts/realtime-map.js +++ b/plugins/UserCountryMap/javascripts/realtime-map.js @@ -10,13 +10,65 @@ (function () { - var RealtimeMap = window.UserCountryMap.RealtimeMap = function (config, theWidget) { - this.config = config; - this.theWidget = theWidget || false; + var UIControl = require('piwik/UI').UIControl; + + var RealtimeMap = window.UserCountryMap.RealtimeMap = function (element) { + UIControl.call(this, element); + this._init(); this.run(); }; - $.extend(RealtimeMap.prototype, { + RealtimeMap.initElements = function () { + $('.RealTimeMap').each(function () { + if (!$(this).attr('data-inited')) { + var control = new RealtimeMap(this); + + $(this).data('uiControlObject', control); + $(this).attr('data-inited', 1); + } + }); + }; + + $.extend(RealtimeMap.prototype, UIControl.prototype, { + + _init: function () { + var $element = this.$element; + + this.config = JSON.parse($element.attr('data-config')); + + // If the map is loaded from the menu, do a few tweaks to clean up the display + if ($element.attr('data-standalone') == 1) { + this._initStandaloneMap(); + } + + // handle widgetry + if ($('#dashboardWidgetsArea').length) { + var $widgetContent = $element.closest('.widgetContent'); + + var self = this; + $widgetContent.on('widget:maximise', function () { + self.resize(); + }).on('widget:minimise', function () { + self.resize(); + }); + } + + // set unique ID for kartograph map div + this.uniqueId = 'RealTimeMap_map-' + this._controlIndex; + $('.RealTimeMap_map', $element).attr('id', this.uniqueId); + }, + + _initStandaloneMap: function () { + $('.top_controls').hide(); + $('.nav').on('piwikSwitchPage', function (event, item) { + var clickedMenuIsNotMap = ($(item).text() != "{{ 'UserCountryMap_RealTimeMap'|translate|e('js') }}"); + if (clickedMenuIsNotMap) { + $('.top_controls').show(); + } + }); + $('.realTimeMap_overlay').css('top', '0px'); + $('.realTimeMap_datetime').css('top', '20px'); + }, run: function () { var debug = 0; @@ -24,8 +76,8 @@ var self = this, config = self.config, _ = config._, - map = self.map = Kartograph.map('#RealTimeMap_map'), - main = $('#RealTimeMap_container'), + map = self.map = Kartograph.map('#' + this.uniqueId), + main = $('.RealTimeMap_container', this.$element), worldTotalVisits = 0, maxVisits = config.maxVisits || 100, width = main.width(), @@ -75,7 +127,7 @@ 'visitLocalTime', 'city', 'country', 'referrerType', 'referrerName', 'referrerTypeName', 'browserIcon', 'operatingSystemIcon', 'countryFlag', 'idVisit', 'actionDetails', 'continentCode', - 'actions', 'searches', 'goalConversions'].join(','), + 'actions', 'searches', 'goalConversions', 'visitorId'].join(','), minTimestamp: firstRun ? -1 : lastTimestamp }); } @@ -247,6 +299,25 @@ } + // default click behavior. if a visit is clicked, the visitor profile is launched, + // otherwise zoom in or out. + // TODO: visitor profile launching logic should probably be contained in + // visitorProfile.js. not sure how to do that, though... + this.$element.on('mapClick', function (e, visit, mapPath) { + var VisitorProfileControl = require('piwik/UI').VisitorProfileControl; + if (visit + && VisitorProfileControl + && !self.$element.closest('.visitor-profile').length + ) { + VisitorProfileControl.showPopover(visit.visitorId); + } else { + var cont = UserCountryMap.cont2cont[mapPath.data.continentCode]; + if (cont && cont != currentMap) { + updateMap(cont); + } + } + }); + /* * this function requests new data from Live.getLastVisitsDetails * and updates the symbols on the map. Then, it sets a timeout @@ -259,6 +330,11 @@ * this is called after new visit reports came in */ function gotNewReport(report) { + // if the map has been destroyed, do nothing + if (!self.map) { + return; + } + // successful request, so set timeout for next API call nextReqTimer = setTimeout(refreshVisits, config.liveRefreshAfterMs); @@ -290,12 +366,9 @@ tooltip: visitTooltip, mouseenter: highlightVisit, mouseleave: unhighlightVisit, - click: function (r, s, evt) { + click: function (visit, mapPath, evt) { evt.stopPropagation(); - var cont = UserCountryMap.cont2cont[s.data.continentCode]; - if (cont && cont != currentMap) { - updateMap(cont); - } + self.$element.trigger('mapClick', [visit, mapPath]); } }); @@ -387,7 +460,7 @@ * the zoom behaviour is initialized. */ function initMap() { - $('#widgetRealTimeMapliveMap .loadingPiwik, #RealTimeMap .loadingPiwik').hide(); + $('#widgetRealTimeMapliveMap .loadingPiwik, .RealTimeMap .loadingPiwik').hide(); map.addLayer('countries', { styles: { fill: colorTheme[currentTheme].fill, @@ -440,7 +513,7 @@ updateMap(location.hash && (location.hash == '#world' || location.hash.match(/^#[A-Z][A-Z]$/)) ? location.hash.substr(1) : 'world'); // TODO: restore last state // clicking on map background zooms out - $('#RealTimeMap_map').off('click').click(function () { + $('.RealTimeMap_map', this.$element).off('click').click(function () { if (currentMap != 'world') updateMap('world'); }); @@ -456,7 +529,7 @@ } function switchTheme() { - $('#RealTimeMap').css({ background: colorTheme[currentTheme].bg }); + self.$element.css({ background: colorTheme[currentTheme].bg }); if (isFullscreenWidget) { $('body').css({ background: colorTheme[currentTheme].bg }); $('.widget').css({ 'border-width': 1 }); @@ -489,7 +562,7 @@ } // setup automatic tooltip updates - setInterval(function () { + this._tooltipUpdateInterval = setInterval(function () { $('.qtip .rel-time').each(function (i, el) { el = $(el); var ds = new Date().getTime() / 1000 - el.data('actiontime'); @@ -523,11 +596,18 @@ else $('.tableIcon span').show(); }, - destroy: function () { + _destroy: function () { + UIControl.prototype._destroy.call(this); + + if (this._tooltipUpdateInterval) { + clearInterval(this._tooltipUpdateInterval); + } + this.map.clear(); $(this.map.container).html(''); + delete this.map; } }); -}()); +}());
\ No newline at end of file diff --git a/plugins/UserCountryMap/stylesheets/realtime-map.less b/plugins/UserCountryMap/stylesheets/realtime-map.less index c9d3aa47b0..2e1e57edaa 100644 --- a/plugins/UserCountryMap/stylesheets/realtime-map.less +++ b/plugins/UserCountryMap/stylesheets/realtime-map.less @@ -13,7 +13,7 @@ filter: alpha(opacity=3); } -#RealTimeMap-black { +.RealTimeMap-black { position: absolute; right: 0; left: 0; @@ -23,7 +23,7 @@ background: #D5D3C8; } -#RealTimeMap .loadingPiwik { +.RealTimeMap .loadingPiwik { position: absolute !important; top: 42% !important; right: 10px !important; diff --git a/plugins/UserCountryMap/templates/realtimeMap.twig b/plugins/UserCountryMap/templates/realtimeMap.twig index dbc0f96c5d..8e7d90e51a 100644 --- a/plugins/UserCountryMap/templates/realtimeMap.twig +++ b/plugins/UserCountryMap/templates/realtimeMap.twig @@ -1,7 +1,9 @@ -<div id="RealTimeMap" style="position:relative; overflow:hidden;"> +<div class="RealTimeMap" style="position:relative; overflow:hidden;" + data-standalone="{{ mapIsStandaloneNotWidget|default(0) }}" + data-config="{{ config|json_encode }}"> - <div id="RealTimeMap_container"> - <div id="RealTimeMap_map" style="overflow:hidden;"></div> + <div class="RealTimeMap_container"> + <div class="RealTimeMap_map" style="overflow:hidden;"></div> <div class="realTimeMap_overlay"> <span class="showing_visits_of" style="display:none;">{{ 'UserCountryMap_ShowingVisits'|translate }} <span class="realTimeMap_timeSpan" style="font-weight:bold;"></span> @@ -12,60 +14,11 @@ </div> <div class="realTimeMap_overlay realTimeMap_datetime"></div> </div> - <div id="RealTimeMap_meta"> + <div class="RealTimeMap_meta"> <span class="loadingPiwik"> <img src="{{ piwikUrl }}plugins/Zeitgeist/images/loading-blue.gif"> {{ 'General_LoadingData'|translate }}... </span> </div> </div> - -<!-- configure some piwik vars --> -<script type="text/javascript"> - {# If the map is loaded from the menu, do a few tweaks to clean up the display #} - {% if mapIsStandaloneNotWidget %} - function initStandaloneMap() { - $('.top_controls').hide(); - $('.nav').on('piwikSwitchPage', function (event, item) { - var clickedMenuIsNotMap = ($(item).text() != "{{ 'UserCountryMap_RealTimeMap'|translate|e('js') }}"); - if (clickedMenuIsNotMap) { - $('.top_controls').show(); - } - }); - $('.realTimeMap_overlay').css('top', '0px'); - $('.realTimeMap_datetime').css('top', '20px'); - } - - initStandaloneMap(); - {% endif %} - - var config = { metrics: {} }; - - config.svgBasePath = "{{ piwikUrl }}plugins/UserCountryMap/svg/"; - config.liveRefreshAfterMs = {{ liveRefreshAfterMs }}; - - config._ = JSON.parse('{{ localeJSON|e('js') }}'); - config.reqParams = JSON.parse('{{ reqParamsJSON|e('js') }}'); - config.siteHasGoals = {{ hasGoals }}; - config.maxVisits = {{ maxVisits }}; - - var realtimeMap; - - if ($('#dashboardWidgetsArea').length) { - // dashboard mode - var $widgetContent = $('#RealTimeMap').parents('.widgetContent'); - - $widgetContent.on('widget:create',function (evt, widget) { - realtimeMap = new UserCountryMap.RealtimeMap(config, widget); - }).on('widget:maximise',function (evt) { - realtimeMap.resize(); - }).on('widget:minimise',function (evt) { - realtimeMap.resize(); - }).on('widget:destroy', function (evt) { - realtimeMap.destroy(); - }); - } else { - // stand-alone mode - realtimeMap = new UserCountryMap.RealtimeMap(config); - } -</script> +<script type="text/javascript">UserCountryMap.RealtimeMap.initElements();</script> |