From d1bed54cf1d77e2ea7d4e5cfe072d158d4cc7988 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 4 Aug 2015 11:59:57 +0200 Subject: [PATCH] Fixes tabdrop memory leak U4-6907 Bootstrap tab drop has memory leaks, fixes tab/header to use the correct bootstrap tab api, simplifies the header code dramatically and speeds up processing. Fixes mem leak with delayedMouseLeave.directive --- src/Umbraco.Web.UI.Client/bower.json | 1 - .../lib/bootstrap-tabdrop/README.md | 62 ++++++++ .../bootstrap-tabdrop/bootstrap-tabdrop.js | 132 ++++++++++++++++++ .../directives/html/umbheader.directive.js | 69 +++------ .../util/delayedMouseLeave.directive.js | 24 +++- .../src/views/directives/umb-header.html | 2 +- src/Umbraco.Web/UI/JavaScript/JsInitialize.js | 2 +- 7 files changed, 233 insertions(+), 59 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/lib/bootstrap-tabdrop/README.md create mode 100644 src/Umbraco.Web.UI.Client/lib/bootstrap-tabdrop/bootstrap-tabdrop.js diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index a1def1a7ff..9a6b26aeaf 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -25,7 +25,6 @@ "jquery-ui": "1.10.3", "angular-dynamic-locale": "~0.1.27", "tinymce": "~4.1.10", - "bootstrap-tabdrop": "~1.0.0", "codemirror": "~5.3.0" } } diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap-tabdrop/README.md b/src/Umbraco.Web.UI.Client/lib/bootstrap-tabdrop/README.md new file mode 100644 index 0000000000..e8d266fb24 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap-tabdrop/README.md @@ -0,0 +1,62 @@ +bootstrap-tabdrop +================= + +***************************************************************** +NOTE: THIS IS A CUSTOM FIXED VERSION!!!!!!!!!!!!!!!!!!!!!! +- THE ORIGINAL HAS A MEMORY LEAK, SO WE'VE HAD TO EMBED THIS + INTO THE CORE WITH THE FIX + +--- UMBRACO CORE TEAM +***************************************************************** + +A dropdown tab tool for @twitter bootstrap forked from Stefan Petre's (of eyecon.ro), + +The dropdown tab appears when your tabs do not all fit in the same row. + +Original site and examples: http://www.eyecon.ro/bootstrap-tabdrop/ + +Added functionality: Displays the text of an active tab selected from the dropdown list instead of the text option on the dropdown tab. + + +## Requirements + +* [Bootstrap](http://twitter.github.com/bootstrap/) 2.0.4+ +* [jQuery](http://jquery.com/) 1.7.1+ + +## Example + +No additional HTML needed - the script adds it when the dropdown tab is needed. + +Using bootstrap-tabdrop.js +Call the tab drop via javascript on .nav-tabs and .nav-pills: +```js +$('.nav-pills, .nav-tabs').tabdrop() +``` + +### Options + +#### text +Type: string +Default: icon +```html + +``` +To change the default value, call +```javascript +.tabdrop({text: "your text here"}); +``` +when initalizing the tabdrop. The displayed value will change when a tab is selected from the dropdown list. + +### Methods + +```js +.tabdrop(options) +``` + +Initializes an tab drop. + +```js +.tabdrop('layout') +``` + +Checks if the tabs fit in one single row. diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap-tabdrop/bootstrap-tabdrop.js b/src/Umbraco.Web.UI.Client/lib/bootstrap-tabdrop/bootstrap-tabdrop.js new file mode 100644 index 0000000000..ede04bfc6b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap-tabdrop/bootstrap-tabdrop.js @@ -0,0 +1,132 @@ +/* ========================================================= + * bootstrap-tabdrop.js + * http://www.eyecon.ro/bootstrap-tabdrop + * ========================================================= + * Copyright 2012 Stefan Petre + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + +/***************************************************************** + * NOTE: THIS IS A CUSTOM FIXED VERSION!!!!!!!!!!!!!!!!!!!!!! + * - THE ORIGINAL HAS A MEMORY LEAK, SO WE'VE HAD TO EMBED THIS + * INTO THE CORE WITH THE FIX + * + * --- UMBRACO CORE TEAM + *****************************************************************/ + +!function( $ ) { + + var WinReszier = (function(){ + var registered = []; + var inited = false; + var timer; + var resize = function(ev) { + clearTimeout(timer); + timer = setTimeout(notify, 100); + }; + var notify = function() { + for(var i=0, cnt=registered.length; i -1) { + registered.splice(index, 1); + } + } + }; + }()); + + var TabDrop = function(element, options) { + this.element = $(element); + this.dropdown = $('').prependTo(this.element); + if (this.element.parent().is('.tabs-below')) { + this.dropdown.addClass('dropup'); + } + this.resizeCallback = $.proxy(this.layout, this); + WinReszier.register(this.resizeCallback); + this.layout(); + }; + + TabDrop.prototype = { + constructor: TabDrop, + + layout: function() { + var collection = []; + this.dropdown.removeClass('hide'); + this.element + .append(this.dropdown.find('li')) + .find('>li') + .not('.tabdrop') + .each(function(){ + if(this.offsetTop > 0) { + collection.push(this); + } + }); + if (collection.length > 0) { + collection = $(collection); + this.dropdown + .find('ul') + .empty() + .append(collection); + if (this.dropdown.find('.active').length == 1) { + this.dropdown.addClass('active'); + } else { + this.dropdown.removeClass('active'); + } + } else { + this.dropdown.addClass('hide'); + } + }, + + destroy: function() { + this.dropdown.html(); + WinReszier.unregister(this.resizeCallback); + } + }; + + $.fn.tabdrop = function ( option ) { + return this.each(function () { + var $this = $(this), + data = $this.data('tabdrop'), + options = typeof option === 'object' && option; + if (!data) { + $this.data('tabdrop', (data = new TabDrop(this, $.extend({}, + $.fn.tabdrop.defaults,options)))); + } + if (typeof option == 'string') { + data[option](); + } + }); + }; + + $.fn.tabdrop.defaults = { + text: '' + }; + + $.fn.tabdrop.Constructor = TabDrop; + +}(window.jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js index 439f27e5ee..8a13a3c714 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js @@ -19,51 +19,13 @@ angular.module("umbraco.directives") tabs: "=" }, link: function (scope, iElement, iAttrs) { - - var maxTabs = 4; - - function collectFromDom(activeTab){ - var $panes = $('div.tab-content'); - - angular.forEach($panes.find('.tab-pane'), function (pane, index) { - var $this = angular.element(pane); - - var id = $this.attr("rel"); - var label = $this.attr("label"); - var tab = {id: id, label: label, active: false}; - if(!activeTab){ - tab.active = true; - activeTab = tab; - } - - if ($this.attr("rel") === String(activeTab.id)) { - $this.addClass('active'); - } - else { - $this.removeClass('active'); - } - - if(label){ - scope.visibleTabs.push(tab); - } - - }); - - //TODO: We'll need to destroy this I'm assuming! - iElement.find('.nav-pills, .nav-tabs').tabdrop(); - } - + scope.showTabs = iAttrs.tabs ? true : false; scope.visibleTabs = []; - scope.overflownTabs = []; - $timeout(function () { - collectFromDom(undefined); - }, 500); - - //when the tabs change, we need to hack the planet a bit and force the first tab content to be active, - //unfortunately twitter bootstrap tabs is not playing perfectly with angular. - scope.$watch("tabs", function (newValue, oldValue) { + //since tabs are loaded async, we need to put a watch on them to determine + // when they are loaded, then we can close the watch + var tabWatch = scope.$watch("tabs", function (newValue, oldValue) { angular.forEach(newValue, function(val, index){ var tab = {id: val.id, label: val.label}; @@ -73,16 +35,25 @@ angular.module("umbraco.directives") //don't process if we cannot or have already done so if (!newValue) {return;} if (!newValue.length || newValue.length === 0){return;} - - var activeTab = _.find(newValue, function (item) { - return item.active; - }); - + //we need to do a timeout here so that the current sync operation can complete // and update the UI, then this will fire and the UI elements will be available. $timeout(function () { - collectFromDom(activeTab); - }, 500); + + //use bootstrap tabs API to show the first one + iElement.find(".nav-tabs a:first").tab('show'); + + //enable the tab drop + iElement.find('.nav-pills, .nav-tabs').tabdrop(); + + //ensure to destroy tabdrop (unbinds window resize listeners) + scope.$on('$destroy', function () { + iElement.find('.nav-pills, .nav-tabs').tabdrop("destroy"); + }); + + //stop watching now + tabWatch(); + }); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/delayedMouseLeave.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/delayedMouseLeave.directive.js index 94d5925f2c..28a90f9041 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/delayedMouseLeave.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/delayedMouseLeave.directive.js @@ -5,21 +5,31 @@ angular.module("umbraco.directives") link: function (scope, element, attrs, ctrl) { var active = false; var fn = $parse(attrs.delayedMouseleave); - element.on("mouseleave", function(event) { - var callback = function() { - fn(scope, {$event:event}); + + function mouseLeave(event) { + var callback = function () { + fn(scope, { $event: event }); }; active = false; - $timeout(function(){ - if(active === false){ + $timeout(function () { + if (active === false) { scope.$apply(callback); } }, 650); - }); + } - element.on("mouseenter", function(event, args){ + function mouseEnter(event, args){ active = true; + } + + element.on("mouseleave", mouseLeave); + element.on("mouseenter", mouseEnter); + + //unbind!! + scope.$on('$destroy', function () { + element.off("mouseleave", mouseLeave); + element.off("mouseenter", mouseEnter); }); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-header.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-header.html index f7798bba09..016055b84a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/directives/umb-header.html @@ -5,7 +5,7 @@