UNPKG

@atlassian/aui

Version:

Atlassian User Interface Framework

519 lines (408 loc) 16.9 kB
'use strict'; import 'skatejs-template-html'; import * as logger from './internal/log'; import debounce from './debounce'; import $ from './jquery'; import addId from './internal/add-id'; import globalize from './internal/globalize'; import isClipped from './is-clipped'; import skate from './internal/skate'; var template = window.skateTemplateHtml; var REGEX = /#.*/; var STORAGE_PREFIX = '_internal-aui-tabs-'; var RESPONSIVE_OPT_IN_SELECTOR = '.aui-tabs.horizontal-tabs[data-aui-responsive]:not([data-aui-responsive="false"]), aui-tabs[responsive]:not([responsive="false"])'; function enhanceTabLink (link) { var $thisLink = $(link); var targetPane = $thisLink.attr('href'); addId($thisLink); $thisLink.attr('role', 'tab'); $(targetPane).attr('aria-labelledby', $thisLink.attr('id')); if ($thisLink.parent().hasClass('active-tab')) { $thisLink.attr('aria-selected', 'true'); } else { $thisLink.attr('aria-selected', 'false'); } } var ResponsiveAdapter = { totalTabsWidth: function ($visibleTabs, $dropdown) { var totalVisibleTabsWidth = this.totalVisibleTabWidth($visibleTabs); var totalDropdownTabsWidth = 0; $dropdown.find('li').each(function (index, tab) { totalDropdownTabsWidth += parseInt(tab.getAttribute('data-aui-tab-width')); }); return totalVisibleTabsWidth + totalDropdownTabsWidth; }, totalVisibleTabWidth: function ($tabs) { var totalWidth = 0; $tabs.each(function (index, tab) { totalWidth += $(tab).outerWidth(); }); return totalWidth; }, removeResponsiveDropdown: function ($dropdown, $dropdownTriggerTab) { $dropdown.remove(); $dropdownTriggerTab.remove(); }, createResponsiveDropdownTrigger: function ($tabsMenu, id) { var triggerMarkup = '<li class="menu-item aui-tabs-responsive-trigger-item">' + '<a class="aui-dropdown2-trigger aui-tabs-responsive-trigger aui-dropdown2-trigger-arrowless" id="aui-tabs-responsive-trigger-' + id + '" aria-haspopup="true" aria-controls="aui-tabs-responsive-dropdown-' + id + '" href="aui-tabs-responsive-dropdown-' + id + '">...</a>' + '</li>'; $tabsMenu.append(triggerMarkup); var $trigger = $tabsMenu.find('.aui-tabs-responsive-trigger-item'); return $trigger; }, createResponsiveDropdown: function ($tabsContainer, id) { var dropdownMarkup = '<div class="aui-dropdown2 aui-style-default aui-tabs-responsive-dropdown" id="aui-tabs-responsive-dropdown-' + id + '">' + '<ul>' + '</ul>' + '</div>'; $tabsContainer.append(dropdownMarkup); var $dropdown = $tabsContainer.find('#aui-tabs-responsive-dropdown-' + id); return $dropdown; }, findNewVisibleTabs: function (tabs, parentWidth, dropdownTriggerTabWidth) { function hasMoreSpace(currentTotalTabWidth, dropdownTriggerTabWidth, parentWidth) { return currentTotalTabWidth + dropdownTriggerTabWidth <= parentWidth; } var currentTotalTabWidth = 0; for (var i = 0; hasMoreSpace(currentTotalTabWidth, dropdownTriggerTabWidth, parentWidth) && i < tabs.length; i++) { var $tab = $(tabs[i]); var tabWidth = $tab.outerWidth(true); currentTotalTabWidth += tabWidth; } // i should now be at the tab index after the last visible tab because of the loop so we minus 1 to get the new visible tabs return tabs.slice(0, i - 1); }, moveVisibleTabs: function (oldVisibleTabs, $tabsParent, $dropdownTriggerTab) { var dropdownId = $dropdownTriggerTab.find('a').attr('aria-controls'); var $dropdown = $('#' + dropdownId); var newVisibleTabs = this.findNewVisibleTabs(oldVisibleTabs, $tabsParent.outerWidth(), $dropdownTriggerTab.parent().outerWidth(true)); var lastTabIndex = newVisibleTabs.length - 1; for (var j = oldVisibleTabs.length - 1; j >= lastTabIndex; j--) { var $tab = $(oldVisibleTabs[j]); this.moveTabToResponsiveDropdown($tab, $dropdown, $dropdownTriggerTab); } return $(newVisibleTabs); }, moveTabToResponsiveDropdown: function ($tab, $dropdown, $dropdownTriggerTab) { var $tabLink = $tab.find('a'); $tab.attr('data-aui-tab-width', $tab.outerWidth(true)); $tabLink.addClass('aui-dropdown2-radio aui-tabs-responsive-item'); if ($tab.hasClass('active-tab')) { $tabLink.addClass('aui-dropdown2-checked'); $dropdownTriggerTab.addClass('active-tab'); } $dropdown.find('ul').prepend($tab); }, moveInvisibleTabs: function (tabsInDropdown, remainingSpace, $dropdownTriggerTab) { function hasMoreSpace(remainingSpace) { return remainingSpace > 0; } for (var i = 0; hasMoreSpace(remainingSpace) && i < tabsInDropdown.length; i++) { var $tab = $(tabsInDropdown[i]); var tabInDropdownWidth = parseInt($tab.attr('data-aui-tab-width'), 10); var shouldMoveTabOut = tabInDropdownWidth < remainingSpace; if (shouldMoveTabOut) { this.moveTabOutOfDropdown($tab, $dropdownTriggerTab); } remainingSpace -= tabInDropdownWidth; } }, moveTabOutOfDropdown: function ($tab, $dropdownTriggerTab) { var isTabInDropdownActive = $tab.find('a').hasClass('aui-dropdown2-checked'); if (isTabInDropdownActive){ $tab.addClass('active-tab'); $dropdownTriggerTab.removeClass('active-tab'); } $tab.children('a').removeClass('aui-dropdown2-radio aui-tabs-responsive-item aui-dropdown2-checked'); $dropdownTriggerTab.before($tab); } }; // this function is run by jquery .each() where 'this' is the current tabs container function calculateResponsiveTabs(tabsContainer, index) { var $tabsContainer = $(tabsContainer); var $tabsMenu = $tabsContainer.find('.tabs-menu').first(); var $visibleTabs = $tabsMenu.find('li:not(.aui-tabs-responsive-trigger-item)'); var $dropdownTriggerTab = $tabsMenu.find('.aui-tabs-responsive-trigger').parent(); var $dropdownTrigger = $dropdownTriggerTab.find('a'); var dropdownId = $dropdownTrigger.attr('aria-controls'); var $dropdown = $(document).find('#' + dropdownId).attr('aria-checked', false); var isResponsive = $dropdown.length > 0; var totalTabsWidth = ResponsiveAdapter.totalTabsWidth($visibleTabs, $dropdown); var needsResponsive = totalTabsWidth > $tabsContainer.outerWidth(); if (!isResponsive && needsResponsive) { $dropdownTriggerTab = ResponsiveAdapter.createResponsiveDropdownTrigger($tabsMenu, index); $dropdown = ResponsiveAdapter.createResponsiveDropdown($tabsContainer, index); } // reset id's in case tabs have changed DOM order $dropdownTrigger.attr('aria-controls', 'aui-tabs-responsive-dropdown-' + index); $dropdownTrigger.attr('id', 'aui-tabs-responsive-trigger-' + index); $dropdownTrigger.attr('href', 'aui-tabs-responsive-trigger-' + index); $dropdown.attr('id', 'aui-tabs-responsive-dropdown-' + index); if (needsResponsive) { var $newVisibleTabs = ResponsiveAdapter.moveVisibleTabs($visibleTabs.toArray(), $tabsContainer, $dropdownTriggerTab); var visibleTabWidth = ResponsiveAdapter.totalVisibleTabWidth($newVisibleTabs); var remainingSpace = $tabsContainer.outerWidth() - visibleTabWidth - $dropdownTriggerTab.outerWidth(true); var hasSpace = remainingSpace > 0; if (hasSpace) { var $tabsInDropdown = $dropdown.find('li'); ResponsiveAdapter.moveInvisibleTabs($tabsInDropdown.toArray(), remainingSpace, $dropdownTriggerTab); } $dropdown.on('click', 'a', handleTabClick); /* Workaround for bug in Edge where the dom is just not being re-rendered properly It is only triggered for certain widths. Simply taking the element out of the DOM and placing it back in causes the browser to re-render, hiding the issue. added from AUI-4098 and to be revisited in AUI-4117*/ if ($tabsMenu.is(':visible')) { $tabsMenu.hide().show(); } } if (isResponsive && !needsResponsive) { $dropdown.find('li').each(function () { ResponsiveAdapter.moveTabOutOfDropdown($(this), $dropdownTriggerTab); }); ResponsiveAdapter.removeResponsiveDropdown($dropdown, $dropdownTriggerTab); } } function switchToTab (tab) { var $tab = $(tab); // This probably isn't needed anymore. Remove once confirmed. if ($tab.hasClass('aui-tabs-responsive-trigger')) { return; } var $pane = $($tab.attr('href').match(REGEX)[0]); $pane .addClass('active-pane') .attr('aria-hidden', 'false') .siblings('.tabs-pane') .removeClass('active-pane') .attr('aria-hidden', 'true'); var $dropdownTriggerTab = $tab.parents('.aui-tabs').find('.aui-tabs-responsive-trigger-item a'); var dropdownId = $dropdownTriggerTab.attr('aria-controls'); var $dropdown = $(document).find('#' + dropdownId); $dropdown.find('li a').attr('aria-checked', false).removeClass('checked aui-dropdown2-checked'); $dropdown.find('li').removeClass('active-tab'); $tab .parent('li.menu-item') .addClass('active-tab') .siblings('.menu-item') .removeClass('active-tab'); if ($tab.hasClass('aui-tabs-responsive-item')) { var $visibleTabs = $pane.parent('.aui-tabs').find('li.menu-item:not(.aui-tabs-responsive-trigger-item)'); $visibleTabs.removeClass('active-tab'); $visibleTabs.find('a').removeClass('checked').removeAttr('aria-checked'); } if ($tab.hasClass('aui-tabs-responsive-item')) { $pane.parent('.aui-tabs').find('li.menu-item.aui-tabs-responsive-trigger-item').addClass('active-tab'); } $tab.closest('.tabs-menu').find('a').attr('aria-selected', 'false'); $tab.attr('aria-selected', 'true'); $tab.trigger('tabSelect', { tab: $tab, pane: $pane }); } function isPersistentTabGroup($tabGroup) { // Tab group persistent attribute exists and is not false return $tabGroup.attr('data-aui-persist') !== undefined && $tabGroup.attr('data-aui-persist') !== 'false'; } function createPersistentKey($tabGroup) { var tabGroupId = $tabGroup.attr('id'); var value = $tabGroup.attr('data-aui-persist'); return STORAGE_PREFIX + (tabGroupId ? tabGroupId : '') + (value && value !== 'true' ? '-' + value : ''); } /* eslint max-depth: 1 */ function updateTabsFromLocalStorage($tabGroups) { for (var i = 0, ii = $tabGroups.length; i < ii; i++) { var $tabGroup = $tabGroups.eq(i); var tabs = $tabGroups.get(i); if (isPersistentTabGroup($tabGroup) && window.localStorage) { var tabGroupId = $tabGroup.attr('id'); if (tabGroupId) { var persistentTabId = window.localStorage.getItem(createPersistentKey($tabGroup)); if (persistentTabId) { var anchor = tabs.querySelector('a[href$="' + persistentTabId + '"]'); if (anchor) { switchToTab(anchor); } } } else { logger.warn('A tab group must specify an id attribute if it specifies data-aui-persist.'); } } } } function updateLocalStorageEntry ($tab) { var $tabGroup = $tab.closest('.aui-tabs'); var tabGroupId = $tabGroup.attr('id'); if (tabGroupId){ var tabId = $tab.attr('href'); if (tabId) { window.localStorage.setItem(createPersistentKey($tabGroup),tabId); } } else { logger.warn('A tab group must specify an id attribute if it specifies data-aui-persist.'); } } function handleTabClick (e) { tabs.change($(e.target).closest('a')); if (e) { e.preventDefault(); } } function responsiveResizeHandler (tabs) { tabs.forEach(function (tab, index) { calculateResponsiveTabs(tab, index); }); } // Initialisation // -------------- function getTabs () { return $('.aui-tabs:not(.aui-tabs-disabled)'); } function getResponsiveTabs () { return $(RESPONSIVE_OPT_IN_SELECTOR).toArray(); } function initWindow () { var debounced = debounce(responsiveResizeHandler, 200); var responsive = getResponsiveTabs(); responsiveResizeHandler(responsive); $(window).resize(function () { debounced(responsive); }); } function initTab (tab) { var $tab = $(tab); tab.setAttribute('role', 'application'); if (!$tab.data('aui-tab-events-bound')) { var $tabMenu = $tab.children('ul.tabs-menu'); // ARIA setup $tabMenu.attr('role', 'tablist'); // ignore the LIs so tab count is announced correctly $tabMenu.children('li').attr('role', 'presentation'); $tabMenu.find('> .menu-item a').each(function () { enhanceTabLink(this); }); // Set up click event for tabs $tabMenu.on('click', 'a', handleTabClick); $tab.data('aui-tab-events-bound', true); initPanes(tab); } } function initTabs () { var tabs = getTabs(); tabs.each(function () { initTab(this); }); updateTabsFromLocalStorage(tabs); } function initPane (pane) { pane.setAttribute('role', 'tabpanel'); pane.setAttribute('aria-hidden', $(pane).hasClass('active-pane') ? 'false' : 'true'); } function initPanes (tab) { [].slice.call(tab.querySelectorAll('.tabs-pane')).forEach(initPane); } function initVerticalTabs () { // Vertical tab truncation setup (adds title if clipped) $('.aui-tabs.vertical-tabs').find('a').each(function () { var thisTab = $(this); // don't override existing titles if (!thisTab.attr('title')) { // if text has been truncated, add title if (isClipped(thisTab)) { thisTab.attr('title', thisTab.text()); } } }); } var tabs = { setup: function () { initWindow(); initTabs(); initVerticalTabs(); }, change: function (a) { var $a = $(a); var $tabGroup = $a.closest('.aui-tabs'); switchToTab($a); if (isPersistentTabGroup($tabGroup) && window.localStorage) { updateLocalStorageEntry($a); } } }; $(tabs.setup); // Web Components // -------------- function findComponent (element) { return $(element).closest('aui-tabs').get(0); } function findPanes (tabs) { return tabs.querySelectorAll('aui-tabs-pane'); } function findTabs (tabs) { return tabs.querySelectorAll('li[is=aui-tabs-tab]'); } skate('aui-tabs', { created: function (element) { $(element).addClass('aui-tabs horizontal-tabs'); // We must initialise here so that the old code still works since // the lifecycle of the sub-components setup the markup so that it // can be processed by the old logic. skate.init(element); // Use the old logic to initialise the tabs. initTab(element); }, template: template( '<ul class="tabs-menu">', '<content select="li[is=aui-tabs-tab]"></content>', '</ul>', '<content select="aui-tabs-pane"></content>' ), prototype: { select: function (element) { var index = $(findPanes(this)).index(element); if (index > -1) { tabs.change(findTabs(this)[index].children[0]); } return this; } } }); var Tab = skate('aui-tabs-tab', { extends: 'li', created: function (element) { $(element).addClass('menu-item'); }, template: template( '<a href="#">', '<strong>', '<content></content>', '</strong>', '</a>' ) }); skate('aui-tabs-pane', { attached: function (element) { var $component = $(findComponent(element)); var $element = $(element); var index = $component.find('aui-tabs-pane').index($element); var tab = new Tab(); var $tab = $(tab); $element.addClass('tabs-pane'); tab.firstChild.setAttribute('href', '#' + element.id); template.wrap(tab).textContent = $element.attr('title'); if (index === 0) { $element.addClass('active-pane'); } if ($element.hasClass('active-pane')) { $tab.addClass('active-tab'); } $element.siblings('ul').append(tab); }, template: template( '<content></content>' ) }); globalize('tabs', tabs); export default tabs;