d030e158ec6e879d0dfa81fd4c2c39cd0ad71b83493b74e8605eb99d67cb0f3f8fb85f7278bb27ae83438cfec1d6d7f502a28110a34a21bba61206ffcc93a1 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <script>
  2. import TabBar from './tab-bar';
  3. import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
  4. function noop() {}
  5. const firstUpperCase = str => {
  6. return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
  7. };
  8. export default {
  9. name: 'TabNav',
  10. components: {
  11. TabBar
  12. },
  13. inject: ['rootTabs'],
  14. props: {
  15. panes: Array,
  16. currentName: String,
  17. editable: Boolean,
  18. onTabClick: {
  19. type: Function,
  20. default: noop
  21. },
  22. onTabRemove: {
  23. type: Function,
  24. default: noop
  25. },
  26. type: String,
  27. stretch: Boolean
  28. },
  29. data() {
  30. return {
  31. scrollable: false,
  32. navOffset: 0,
  33. isFocus: false,
  34. focusable: true
  35. };
  36. },
  37. computed: {
  38. navStyle() {
  39. const dir = ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'X' : 'Y';
  40. return {
  41. transform: `translate${dir}(-${this.navOffset}px)`
  42. };
  43. },
  44. sizeName() {
  45. return ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'width' : 'height';
  46. }
  47. },
  48. methods: {
  49. scrollPrev() {
  50. const containerSize = this.$refs.navScroll[`offset${firstUpperCase(this.sizeName)}`];
  51. const currentOffset = this.navOffset;
  52. if (!currentOffset) return;
  53. let newOffset = currentOffset > containerSize
  54. ? currentOffset - containerSize
  55. : 0;
  56. this.navOffset = newOffset;
  57. },
  58. scrollNext() {
  59. const navSize = this.$refs.nav[`offset${firstUpperCase(this.sizeName)}`];
  60. const containerSize = this.$refs.navScroll[`offset${firstUpperCase(this.sizeName)}`];
  61. const currentOffset = this.navOffset;
  62. if (navSize - currentOffset <= containerSize) return;
  63. let newOffset = navSize - currentOffset > containerSize * 2
  64. ? currentOffset + containerSize
  65. : (navSize - containerSize);
  66. this.navOffset = newOffset;
  67. },
  68. scrollToActiveTab() {
  69. if (!this.scrollable) return;
  70. const nav = this.$refs.nav;
  71. const activeTab = this.$el.querySelector('.is-active');
  72. if (!activeTab) return;
  73. const navScroll = this.$refs.navScroll;
  74. const isHorizontal = ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1;
  75. const activeTabBounding = activeTab.getBoundingClientRect();
  76. const navScrollBounding = navScroll.getBoundingClientRect();
  77. const maxOffset = isHorizontal
  78. ? nav.offsetWidth - navScrollBounding.width
  79. : nav.offsetHeight - navScrollBounding.height;
  80. const currentOffset = this.navOffset;
  81. let newOffset = currentOffset;
  82. if (isHorizontal) {
  83. if (activeTabBounding.left < navScrollBounding.left) {
  84. newOffset = currentOffset - (navScrollBounding.left - activeTabBounding.left);
  85. }
  86. if (activeTabBounding.right > navScrollBounding.right) {
  87. newOffset = currentOffset + activeTabBounding.right - navScrollBounding.right;
  88. }
  89. } else {
  90. if (activeTabBounding.top < navScrollBounding.top) {
  91. newOffset = currentOffset - (navScrollBounding.top - activeTabBounding.top);
  92. }
  93. if (activeTabBounding.bottom > navScrollBounding.bottom) {
  94. newOffset = currentOffset + (activeTabBounding.bottom - navScrollBounding.bottom);
  95. }
  96. }
  97. newOffset = Math.max(newOffset, 0);
  98. this.navOffset = Math.min(newOffset, maxOffset);
  99. },
  100. update() {
  101. if (!this.$refs.nav) return;
  102. const sizeName = this.sizeName;
  103. const navSize = this.$refs.nav[`offset${firstUpperCase(sizeName)}`];
  104. const containerSize = this.$refs.navScroll[`offset${firstUpperCase(sizeName)}`];
  105. const currentOffset = this.navOffset;
  106. if (containerSize < navSize) {
  107. const currentOffset = this.navOffset;
  108. this.scrollable = this.scrollable || {};
  109. this.scrollable.prev = currentOffset;
  110. this.scrollable.next = currentOffset + containerSize < navSize;
  111. if (navSize - currentOffset < containerSize) {
  112. this.navOffset = navSize - containerSize;
  113. }
  114. } else {
  115. this.scrollable = false;
  116. if (currentOffset > 0) {
  117. this.navOffset = 0;
  118. }
  119. }
  120. },
  121. changeTab(e) {
  122. const keyCode = e.keyCode;
  123. let nextIndex;
  124. let currentIndex, tabList;
  125. if ([37, 38, 39, 40].indexOf(keyCode) !== -1) { // 左右上下键更换tab
  126. tabList = e.currentTarget.querySelectorAll('[role=tab]');
  127. currentIndex = Array.prototype.indexOf.call(tabList, e.target);
  128. } else {
  129. return;
  130. }
  131. if (keyCode === 37 || keyCode === 38) { // left
  132. if (currentIndex === 0) { // first
  133. nextIndex = tabList.length - 1;
  134. } else {
  135. nextIndex = currentIndex - 1;
  136. }
  137. } else { // right
  138. if (currentIndex < tabList.length - 1) { // not last
  139. nextIndex = currentIndex + 1;
  140. } else {
  141. nextIndex = 0;
  142. }
  143. }
  144. tabList[nextIndex].focus(); // 改变焦点元素
  145. tabList[nextIndex].click(); // 选中下一个tab
  146. this.setFocus();
  147. },
  148. setFocus() {
  149. if (this.focusable) {
  150. this.isFocus = true;
  151. }
  152. },
  153. removeFocus() {
  154. this.isFocus = false;
  155. },
  156. visibilityChangeHandler() {
  157. const visibility = document.visibilityState;
  158. if (visibility === 'hidden') {
  159. this.focusable = false;
  160. } else if (visibility === 'visible') {
  161. setTimeout(() => {
  162. this.focusable = true;
  163. }, 50);
  164. }
  165. },
  166. windowBlurHandler() {
  167. this.focusable = false;
  168. },
  169. windowFocusHandler() {
  170. setTimeout(() => {
  171. this.focusable = true;
  172. }, 50);
  173. }
  174. },
  175. updated() {
  176. this.update();
  177. },
  178. render(h) {
  179. const {
  180. type,
  181. panes,
  182. editable,
  183. stretch,
  184. onTabClick,
  185. onTabRemove,
  186. navStyle,
  187. scrollable,
  188. scrollNext,
  189. scrollPrev,
  190. changeTab,
  191. setFocus,
  192. removeFocus
  193. } = this;
  194. const scrollBtn = scrollable
  195. ? [
  196. <span class={['el-tabs__nav-prev', scrollable.prev ? '' : 'is-disabled']} on-click={scrollPrev}><i class="el-icon-arrow-left"></i></span>,
  197. <span class={['el-tabs__nav-next', scrollable.next ? '' : 'is-disabled']} on-click={scrollNext}><i class="el-icon-arrow-right"></i></span>
  198. ] : null;
  199. const tabs = this._l(panes, (pane, index) => {
  200. let tabName = pane.name || pane.index || index;
  201. const closable = pane.isClosable || editable;
  202. pane.index = `${index}`;
  203. const btnClose = closable
  204. ? <span class="el-icon-close" on-click={(ev) => { onTabRemove(pane, ev); }}></span>
  205. : null;
  206. const tabLabelContent = pane.$slots.label || pane.label;
  207. const tabindex = pane.active ? 0 : -1;
  208. return (
  209. <div
  210. class={{
  211. 'el-tabs__item': true,
  212. [`is-${ this.rootTabs.tabPosition }`]: true,
  213. 'is-active': pane.active,
  214. 'is-disabled': pane.disabled,
  215. 'is-closable': closable,
  216. 'is-focus': this.isFocus
  217. }}
  218. id={`tab-${tabName}`}
  219. key={`tab-${tabName}`}
  220. aria-controls={`pane-${tabName}`}
  221. role="tab"
  222. aria-selected={ pane.active }
  223. ref="tabs"
  224. tabindex={tabindex}
  225. refInFor
  226. on-focus={ ()=> { setFocus(); }}
  227. on-blur ={ ()=> { removeFocus(); }}
  228. on-click={(ev) => { removeFocus(); onTabClick(pane, tabName, ev); }}
  229. on-keydown={(ev) => { if (closable && (ev.keyCode === 46 || ev.keyCode === 8)) { onTabRemove(pane, ev);} }}
  230. >
  231. {tabLabelContent}
  232. {btnClose}
  233. </div>
  234. );
  235. });
  236. return (
  237. <div class={['el-tabs__nav-wrap', scrollable ? 'is-scrollable' : '', `is-${ this.rootTabs.tabPosition }`]}>
  238. {scrollBtn}
  239. <div class={['el-tabs__nav-scroll']} ref="navScroll">
  240. <div
  241. class={['el-tabs__nav', `is-${ this.rootTabs.tabPosition }`, stretch && ['top', 'bottom'].indexOf(this.rootTabs.tabPosition) !== -1 ? 'is-stretch' : '']}
  242. ref="nav"
  243. style={navStyle}
  244. role="tablist"
  245. on-keydown={ changeTab }
  246. >
  247. {!type ? <tab-bar tabs={panes}></tab-bar> : null}
  248. {tabs}
  249. </div>
  250. </div>
  251. </div>
  252. );
  253. },
  254. mounted() {
  255. addResizeListener(this.$el, this.update);
  256. document.addEventListener('visibilitychange', this.visibilityChangeHandler);
  257. window.addEventListener('blur', this.windowBlurHandler);
  258. window.addEventListener('focus', this.windowFocusHandler);
  259. setTimeout(() => {
  260. this.scrollToActiveTab();
  261. }, 0);
  262. },
  263. beforeDestroy() {
  264. if (this.$el && this.update) removeResizeListener(this.$el, this.update);
  265. document.removeEventListener('visibilitychange', this.visibilityChangeHandler);
  266. window.removeEventListener('blur', this.windowBlurHandler);
  267. window.removeEventListener('focus', this.windowFocusHandler);
  268. }
  269. };
  270. </script>