From 5e47b52bd6a8817b2624ca6797b8267a162f84af Mon Sep 17 00:00:00 2001 From: Robert Messerle Date: Fri, 29 May 2015 11:29:23 -0700 Subject: [PATCH] fix(tabs): select/deselect events will now fire when a tab is removed but the index does not change Closes #2837 --- src/components/tabs/js/tabsController.js | 345 ++++++++++++----------- 1 file changed, 180 insertions(+), 165 deletions(-) diff --git a/src/components/tabs/js/tabsController.js b/src/components/tabs/js/tabsController.js index 187b4f79921..d1a7474bddd 100644 --- a/src/components/tabs/js/tabsController.js +++ b/src/components/tabs/js/tabsController.js @@ -54,28 +54,60 @@ function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $md $timeout(adjustOffset); } + //-- Change handlers + function handleHasContent (hasContent) { $element[hasContent ? 'removeClass' : 'addClass']('md-no-tab-content'); } - function getElements () { - var elements = {}; + function handleOffsetChange (left) { + var newValue = shouldCenterTabs() ? '' : '-' + left + 'px'; + angular.element(elements.paging).css('transform', 'translate3d(' + newValue + ', 0, 0)'); + $scope.$broadcast('$mdTabsPaginationChanged'); + } - //-- gather tab bar elements - elements.wrapper = $element[0].getElementsByTagName('md-tabs-wrapper')[0]; - elements.canvas = elements.wrapper.getElementsByTagName('md-tabs-canvas')[0]; - elements.paging = elements.canvas.getElementsByTagName('md-pagination-wrapper')[0]; - elements.tabs = elements.paging.getElementsByTagName('md-tab-item'); - elements.dummies = elements.canvas.getElementsByTagName('md-dummy-tab'); - elements.inkBar = elements.paging.getElementsByTagName('md-ink-bar')[0]; + function handleFocusIndexChange (newIndex, oldIndex) { + if (newIndex === oldIndex) return; + if (!elements.tabs[newIndex]) return; + adjustOffset(); + redirectFocus(); + } - //-- gather tab content elements - elements.contentsWrapper = $element[0].getElementsByTagName('md-tabs-content-wrapper')[0]; - elements.contents = elements.contentsWrapper.getElementsByTagName('md-tab-content'); + function handleSelectedIndexChange (newValue, oldValue) { + if (newValue === oldValue) return; - return elements; + $scope.selectedIndex = getNearestSafeIndex(newValue); + ctrl.lastSelectedIndex = oldValue; + updateInkBarStyles(); + updateHeightFromContent(); + $scope.$broadcast('$mdTabsChanged'); + ctrl.tabs[oldValue] && ctrl.tabs[oldValue].scope.deselect(); + ctrl.tabs[newValue] && ctrl.tabs[newValue].scope.select(); + } + + function handleResizeWhenVisible () { + //-- if there is already a watcher waiting for resize, do nothing + if (handleResizeWhenVisible.watcher) return; + //-- otherwise, we will abuse the $watch function to check for visible + handleResizeWhenVisible.watcher = $scope.$watch(function () { + //-- since we are checking for DOM size, we use $timeout to wait for after the DOM updates + $timeout(function () { + //-- if the watcher has already run (ie. multiple digests in one cycle), do nothing + if (!handleResizeWhenVisible.watcher) return; + + if ($element.prop('offsetParent')) { + handleResizeWhenVisible.watcher(); + handleResizeWhenVisible.watcher = null; + + //-- we have to trigger our own $apply so that the DOM bindings will update + $scope.$apply(handleWindowResize); + } + }, 0, false); + }); } + //-- Event handlers / actions + function keydown (event) { switch (event.keyCode) { case $mdConstant.KEY_CODE.LEFT_ARROW: @@ -95,6 +127,141 @@ function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $md ctrl.lastClick = false; } + function select (index) { + if (!locked) ctrl.focusIndex = $scope.selectedIndex = index; + ctrl.lastClick = true; + } + + function scroll (event) { + if (!shouldPaginate()) return; + event.preventDefault(); + ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta); + } + + function nextPage () { + var viewportWidth = elements.canvas.clientWidth, + totalWidth = viewportWidth + ctrl.offsetLeft, + i, tab; + for (i = 0; i < elements.tabs.length; i++) { + tab = elements.tabs[i]; + if (tab.offsetLeft + tab.offsetWidth > totalWidth) break; + } + ctrl.offsetLeft = fixOffset(tab.offsetLeft); + } + + function previousPage () { + var i, tab; + for (i = 0; i < elements.tabs.length; i++) { + tab = elements.tabs[i]; + if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break; + } + ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth); + } + + function handleWindowResize () { + ctrl.lastSelectedIndex = $scope.selectedIndex; + ctrl.offsetLeft = fixOffset(ctrl.offsetLeft); + $timeout(updateInkBarStyles, 0, false); + } + + function removeTab (tabData) { + var selectedIndex = $scope.selectedIndex, + tab = ctrl.tabs.splice(tabData.getIndex(), 1)[0]; + refreshIndex(); + //-- when removing a tab, if the selected index did not change, we have to manually trigger the + // tab select/deselect events + if ($scope.selectedIndex === selectedIndex) { + tab.scope.deselect(); + ctrl.tabs[$scope.selectedIndex] && ctrl.tabs[$scope.selectedIndex].scope.select(); + } + $timeout(function () { + updateInkBarStyles(); + ctrl.offsetLeft = fixOffset(ctrl.offsetLeft); + }); + } + + function insertTab (tabData, index) { + var proto = { + getIndex: function () { return ctrl.tabs.indexOf(tab); }, + isActive: function () { return this.getIndex() === $scope.selectedIndex; }, + isLeft: function () { return this.getIndex() < $scope.selectedIndex; }, + isRight: function () { return this.getIndex() > $scope.selectedIndex; }, + hasFocus: function () { return !ctrl.lastClick && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; }, + id: $mdUtil.nextUid() + }, + tab = angular.extend(proto, tabData); + if (angular.isDefined(index)) { + ctrl.tabs.splice(index, 0, tab); + } else { + ctrl.tabs.push(tab); + } + processQueue(); + updateHasContent(); + return tab; + } + + //-- Getter methods + + function getElements () { + var elements = {}; + + //-- gather tab bar elements + elements.wrapper = $element[0].getElementsByTagName('md-tabs-wrapper')[0]; + elements.canvas = elements.wrapper.getElementsByTagName('md-tabs-canvas')[0]; + elements.paging = elements.canvas.getElementsByTagName('md-pagination-wrapper')[0]; + elements.tabs = elements.paging.getElementsByTagName('md-tab-item'); + elements.dummies = elements.canvas.getElementsByTagName('md-dummy-tab'); + elements.inkBar = elements.paging.getElementsByTagName('md-ink-bar')[0]; + + //-- gather tab content elements + elements.contentsWrapper = $element[0].getElementsByTagName('md-tabs-content-wrapper')[0]; + elements.contents = elements.contentsWrapper.getElementsByTagName('md-tab-content'); + + return elements; + } + + function canPageBack () { + return ctrl.offsetLeft > 0; + } + + function canPageForward () { + var lastTab = elements.tabs[elements.tabs.length - 1]; + return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + ctrl.offsetLeft; + } + + function shouldStretchTabs () { + switch ($scope.stretchTabs) { + case 'always': return true; + case 'never': return false; + default: return !shouldPaginate() && $window.matchMedia('(max-width: 600px)').matches; + } + } + + function shouldCenterTabs () { + return $scope.centerTabs && !shouldPaginate(); + } + + function shouldPaginate () { + if ($scope.noPagination) return false; + var canvasWidth = $element.prop('clientWidth'); + angular.forEach(elements.tabs, function (tab) { canvasWidth -= tab.offsetWidth; }); + return canvasWidth < 0; + } + + function getNearestSafeIndex(newIndex) { + var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex), + i, tab; + for (i = 0; i <= maxOffset; i++) { + tab = ctrl.tabs[newIndex + i]; + if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); + tab = ctrl.tabs[newIndex - i]; + if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); + } + return newIndex; + } + + //-- Utility methods + function updateTabOrder () { var selectedItem = ctrl.tabs[$scope.selectedIndex], focusItem = ctrl.tabs[ctrl.focusIndex]; @@ -118,19 +285,6 @@ function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $md } } - function handleOffsetChange (left) { - var newValue = shouldCenterTabs() ? '' : '-' + left + 'px'; - angular.element(elements.paging).css('transform', 'translate3d(' + newValue + ', 0, 0)'); - $scope.$broadcast('$mdTabsPaginationChanged'); - } - - function handleFocusIndexChange (newIndex, oldIndex) { - if (newIndex === oldIndex) return; - if (!elements.tabs[newIndex]) return; - adjustOffset(); - redirectFocus(); - } - function redirectFocus () { elements.dummies[ctrl.focusIndex].focus(); } @@ -144,37 +298,11 @@ function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $md ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left)); } - function handleWindowResize () { - ctrl.lastSelectedIndex = $scope.selectedIndex; - ctrl.offsetLeft = fixOffset(ctrl.offsetLeft); - $timeout(updateInkBarStyles, 0, false); - } - function processQueue () { queue.forEach(function (func) { $timeout(func); }); queue = []; } - function insertTab (tabData, index) { - var proto = { - getIndex: function () { return ctrl.tabs.indexOf(tab); }, - isActive: function () { return this.getIndex() === $scope.selectedIndex; }, - isLeft: function () { return this.getIndex() < $scope.selectedIndex; }, - isRight: function () { return this.getIndex() > $scope.selectedIndex; }, - hasFocus: function () { return !ctrl.lastClick && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex; }, - id: $mdUtil.nextUid() - }, - tab = angular.extend(proto, tabData); - if (angular.isDefined(index)) { - ctrl.tabs.splice(index, 0, tab); - } else { - ctrl.tabs.push(tab); - } - processQueue(); - updateHasContent(); - return tab; - } - function updateHasContent () { var hasContent = false; angular.forEach(ctrl.tabs, function (tab) { @@ -183,53 +311,11 @@ function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $md ctrl.hasContent = hasContent; } - function removeTab (tabData) { - ctrl.tabs.splice(tabData.getIndex(), 1); - refreshIndex(); - $timeout(function () { - updateInkBarStyles(); - ctrl.offsetLeft = fixOffset(ctrl.offsetLeft); - }); - } - function refreshIndex () { $scope.selectedIndex = getNearestSafeIndex($scope.selectedIndex); ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex); } - function handleSelectedIndexChange (newValue, oldValue) { - if (newValue === oldValue) return; - - $scope.selectedIndex = getNearestSafeIndex(newValue); - ctrl.lastSelectedIndex = oldValue; - updateInkBarStyles(); - updateHeightFromContent(); - $scope.$broadcast('$mdTabsChanged'); - ctrl.tabs[oldValue] && ctrl.tabs[oldValue].scope.deselect(); - ctrl.tabs[newValue] && ctrl.tabs[newValue].scope.select(); - } - - function handleResizeWhenVisible () { - //-- if there is already a watcher waiting for resize, do nothing - if (handleResizeWhenVisible.watcher) return; - //-- otherwise, we will abuse the $watch function to check for visible - handleResizeWhenVisible.watcher = $scope.$watch(function () { - //-- since we are checking for DOM size, we use $timeout to wait for after the DOM updates - $timeout(function () { - //-- if the watcher has already run (ie. multiple digests in one cycle), do nothing - if (!handleResizeWhenVisible.watcher) return; - - if ($element.prop('offsetParent')) { - handleResizeWhenVisible.watcher(); - handleResizeWhenVisible.watcher = null; - - //-- we have to trigger our own $apply so that the DOM bindings will update - $scope.$apply(handleWindowResize); - } - }, 0, false); - }); - } - function updateHeightFromContent () { if (!$scope.dynamicHeight) return $element.css('height', ''); if (!ctrl.tabs.length) return queue.push(updateHeightFromContent); @@ -279,48 +365,6 @@ function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $md } } - function getNearestSafeIndex(newIndex) { - var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex), - i, tab; - for (i = 0; i <= maxOffset; i++) { - tab = ctrl.tabs[newIndex + i]; - if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); - tab = ctrl.tabs[newIndex - i]; - if (tab && (tab.scope.disabled !== true)) return tab.getIndex(); - } - return newIndex; - } - - function shouldStretchTabs () { - switch ($scope.stretchTabs) { - case 'always': return true; - case 'never': return false; - default: return !shouldPaginate() && $window.matchMedia('(max-width: 600px)').matches; - } - } - - function shouldCenterTabs () { - return $scope.centerTabs && !shouldPaginate(); - } - - function shouldPaginate () { - if ($scope.noPagination) return false; - var canvasWidth = $element.prop('clientWidth'); - angular.forEach(elements.tabs, function (tab) { canvasWidth -= tab.offsetWidth; }); - return canvasWidth < 0; - } - - function select (index) { - if (!locked) ctrl.focusIndex = $scope.selectedIndex = index; - ctrl.lastClick = true; - } - - function scroll (event) { - if (!shouldPaginate()) return; - event.preventDefault(); - ctrl.offsetLeft = fixOffset(ctrl.offsetLeft - event.wheelDelta); - } - function fixOffset (value) { if (!elements.tabs.length || !shouldPaginate()) return 0; var lastTab = elements.tabs[elements.tabs.length - 1], @@ -330,35 +374,6 @@ function MdTabsController ($scope, $element, $window, $timeout, $mdConstant, $md return value; } - function nextPage () { - var viewportWidth = elements.canvas.clientWidth, - totalWidth = viewportWidth + ctrl.offsetLeft, - i, tab; - for (i = 0; i < elements.tabs.length; i++) { - tab = elements.tabs[i]; - if (tab.offsetLeft + tab.offsetWidth > totalWidth) break; - } - ctrl.offsetLeft = fixOffset(tab.offsetLeft); - } - - function previousPage () { - var i, tab; - for (i = 0; i < elements.tabs.length; i++) { - tab = elements.tabs[i]; - if (tab.offsetLeft + tab.offsetWidth >= ctrl.offsetLeft) break; - } - ctrl.offsetLeft = fixOffset(tab.offsetLeft + tab.offsetWidth - elements.canvas.clientWidth); - } - - function canPageBack () { - return ctrl.offsetLeft > 0; - } - - function canPageForward () { - var lastTab = elements.tabs[elements.tabs.length - 1]; - return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth + ctrl.offsetLeft; - } - function attachRipple (scope, element) { var options = { colorElement: angular.element(elements.inkBar) }; $mdTabInkRipple.attach(scope, element, options);