1 | /*!
|
---|
2 | * AngularJS Material Design
|
---|
3 | * https://github.com/angular/material
|
---|
4 | * @license MIT
|
---|
5 | * v1.2.3
|
---|
6 | */
|
---|
7 | (function( window, angular, undefined ){
|
---|
8 | "use strict";
|
---|
9 |
|
---|
10 | /**
|
---|
11 | * @ngdoc module
|
---|
12 | * @name material.components.tabs
|
---|
13 | * @description
|
---|
14 | *
|
---|
15 | * Tabs, created with the `<md-tabs>` directive provide *tabbed* navigation with different styles.
|
---|
16 | * The Tabs component consists of clickable tabs that are aligned horizontally side-by-side.
|
---|
17 | *
|
---|
18 | * Features include support for:
|
---|
19 | *
|
---|
20 | * - static or dynamic tabs,
|
---|
21 | * - responsive designs,
|
---|
22 | * - accessibility support (ARIA),
|
---|
23 | * - tab pagination,
|
---|
24 | * - external or internal tab content,
|
---|
25 | * - focus indicators and arrow-key navigations,
|
---|
26 | * - programmatic lookup and access to tab controllers, and
|
---|
27 | * - dynamic transitions through different tab contents.
|
---|
28 | *
|
---|
29 | */
|
---|
30 | /*
|
---|
31 | * @see js folder for tabs implementation
|
---|
32 | */
|
---|
33 | angular.module('material.components.tabs', [
|
---|
34 | 'material.core',
|
---|
35 | 'material.components.icon'
|
---|
36 | ]);
|
---|
37 |
|
---|
38 | angular
|
---|
39 | .module('material.components.tabs')
|
---|
40 | .service('MdTabsPaginationService', MdTabsPaginationService);
|
---|
41 |
|
---|
42 | /**
|
---|
43 | * @private
|
---|
44 | * @module material.components.tabs
|
---|
45 | * @name MdTabsPaginationService
|
---|
46 | * @description Provides many standalone functions to ease in pagination calculations.
|
---|
47 | *
|
---|
48 | * Most functions accept the elements and the current offset.
|
---|
49 | *
|
---|
50 | * The `elements` parameter is typically the value returned from the `getElements()` function of the
|
---|
51 | * tabsController.
|
---|
52 | *
|
---|
53 | * The `offset` parameter is always positive regardless of LTR or RTL (we simply make the LTR one
|
---|
54 | * negative when we apply our transform). This is typically the `ctrl.leftOffset` variable in the
|
---|
55 | * tabsController.
|
---|
56 | *
|
---|
57 | * @returns MdTabsPaginationService
|
---|
58 | * @constructor
|
---|
59 | */
|
---|
60 | function MdTabsPaginationService() {
|
---|
61 | return {
|
---|
62 | decreasePageOffset: decreasePageOffset,
|
---|
63 | increasePageOffset: increasePageOffset,
|
---|
64 | getTabOffsets: getTabOffsets,
|
---|
65 | getTotalTabsWidth: getTotalTabsWidth
|
---|
66 | };
|
---|
67 |
|
---|
68 | /**
|
---|
69 | * Returns the offset for the next decreasing page.
|
---|
70 | *
|
---|
71 | * @param elements
|
---|
72 | * @param currentOffset
|
---|
73 | * @returns {number}
|
---|
74 | */
|
---|
75 | function decreasePageOffset(elements, currentOffset) {
|
---|
76 | var canvas = elements.canvas,
|
---|
77 | tabOffsets = getTabOffsets(elements),
|
---|
78 | i, firstVisibleTabOffset;
|
---|
79 |
|
---|
80 | // Find the first fully visible tab in offset range
|
---|
81 | for (i = 0; i < tabOffsets.length; i++) {
|
---|
82 | if (tabOffsets[i] >= currentOffset) {
|
---|
83 | firstVisibleTabOffset = tabOffsets[i];
|
---|
84 | break;
|
---|
85 | }
|
---|
86 | }
|
---|
87 |
|
---|
88 | // Return (the first visible tab offset - the tabs container width) without going negative
|
---|
89 | return Math.max(0, firstVisibleTabOffset - canvas.clientWidth);
|
---|
90 | }
|
---|
91 |
|
---|
92 | /**
|
---|
93 | * Returns the offset for the next increasing page.
|
---|
94 | *
|
---|
95 | * @param elements
|
---|
96 | * @param currentOffset
|
---|
97 | * @returns {number}
|
---|
98 | */
|
---|
99 | function increasePageOffset(elements, currentOffset) {
|
---|
100 | var canvas = elements.canvas,
|
---|
101 | maxOffset = getTotalTabsWidth(elements) - canvas.clientWidth,
|
---|
102 | tabOffsets = getTabOffsets(elements),
|
---|
103 | i, firstHiddenTabOffset;
|
---|
104 |
|
---|
105 | // Find the first partially (or fully) invisible tab
|
---|
106 | for (i = 0; i < tabOffsets.length, tabOffsets[i] <= currentOffset + canvas.clientWidth; i++) {
|
---|
107 | firstHiddenTabOffset = tabOffsets[i];
|
---|
108 | }
|
---|
109 |
|
---|
110 | // Return the offset of the first hidden tab, or the maximum offset (whichever is smaller)
|
---|
111 | return Math.min(maxOffset, firstHiddenTabOffset);
|
---|
112 | }
|
---|
113 |
|
---|
114 | /**
|
---|
115 | * Returns the offsets of all of the tabs based on their widths.
|
---|
116 | *
|
---|
117 | * @param elements
|
---|
118 | * @returns {number[]}
|
---|
119 | */
|
---|
120 | function getTabOffsets(elements) {
|
---|
121 | var i, tab, currentOffset = 0, offsets = [];
|
---|
122 |
|
---|
123 | for (i = 0; i < elements.tabs.length; i++) {
|
---|
124 | tab = elements.tabs[i];
|
---|
125 | offsets.push(currentOffset);
|
---|
126 | currentOffset += tab.offsetWidth;
|
---|
127 | }
|
---|
128 |
|
---|
129 | return offsets;
|
---|
130 | }
|
---|
131 |
|
---|
132 | /**
|
---|
133 | * Sum the width of all tabs.
|
---|
134 | *
|
---|
135 | * @param elements
|
---|
136 | * @returns {number}
|
---|
137 | */
|
---|
138 | function getTotalTabsWidth(elements) {
|
---|
139 | var sum = 0, i, tab;
|
---|
140 |
|
---|
141 | for (i = 0; i < elements.tabs.length; i++) {
|
---|
142 | tab = elements.tabs[i];
|
---|
143 | sum += tab.offsetWidth;
|
---|
144 | }
|
---|
145 |
|
---|
146 | return sum;
|
---|
147 | }
|
---|
148 |
|
---|
149 | }
|
---|
150 |
|
---|
151 | /**
|
---|
152 | * @ngdoc directive
|
---|
153 | * @name mdTab
|
---|
154 | * @module material.components.tabs
|
---|
155 | *
|
---|
156 | * @restrict E
|
---|
157 | *
|
---|
158 | * @description
|
---|
159 | * The `<md-tab>` is a nested directive used within `<md-tabs>` to specify a tab with a **label**
|
---|
160 | * and optional *view content*.
|
---|
161 | *
|
---|
162 | * If the `label` attribute is not specified, then an optional `<md-tab-label>` tag can be used to
|
---|
163 | * specify more complex tab header markup. If neither the **label** nor the **md-tab-label** are
|
---|
164 | * specified, then the nested markup of the `<md-tab>` is used as the tab header markup.
|
---|
165 | *
|
---|
166 | * Please note that if you use `<md-tab-label>`, your content **MUST** be wrapped in the
|
---|
167 | * `<md-tab-body>` tag. This is to define a clear separation between the tab content and the tab
|
---|
168 | * label.
|
---|
169 | *
|
---|
170 | * This container is used by the TabsController to show/hide the active tab's content view. This
|
---|
171 | * synchronization is automatically managed by the internal TabsController whenever the tab
|
---|
172 | * selection changes. Selection changes can be initiated via data binding changes, programmatic
|
---|
173 | * invocation, or user gestures.
|
---|
174 | *
|
---|
175 | * @param {string=} label Optional attribute to specify a simple string as the tab label
|
---|
176 | * @param {boolean=} ng-disabled If present and expression evaluates to truthy, disabled tab
|
---|
177 | * selection.
|
---|
178 | * @param {string=} md-tab-class Optional attribute to specify a class that will be applied to the
|
---|
179 | * tab's button
|
---|
180 | * @param {expression=} md-on-deselect Expression to be evaluated after the tab has been
|
---|
181 | * de-selected.
|
---|
182 | * @param {expression=} md-on-select Expression to be evaluated after the tab has been selected.
|
---|
183 | * @param {boolean=} md-active When true, sets the active tab. Note: There can only be one active
|
---|
184 | * tab at a time.
|
---|
185 | *
|
---|
186 | *
|
---|
187 | * @usage
|
---|
188 | *
|
---|
189 | * <hljs lang="html">
|
---|
190 | * <md-tab label="My Tab" md-tab-class="my-content-tab" ng-disabled md-on-select="onSelect()"
|
---|
191 | * md-on-deselect="onDeselect()">
|
---|
192 | * <h3>My Tab content</h3>
|
---|
193 | * </md-tab>
|
---|
194 | *
|
---|
195 | * <md-tab>
|
---|
196 | * <md-tab-label>
|
---|
197 | * <h3>My Tab</h3>
|
---|
198 | * </md-tab-label>
|
---|
199 | * <md-tab-body>
|
---|
200 | * <p>
|
---|
201 | * Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
|
---|
202 | * laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
|
---|
203 | * architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit
|
---|
204 | * aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione
|
---|
205 | * voluptatem sequi nesciunt.
|
---|
206 | * </p>
|
---|
207 | * </md-tab-body>
|
---|
208 | * </md-tab>
|
---|
209 | * </hljs>
|
---|
210 | *
|
---|
211 | */
|
---|
212 | angular
|
---|
213 | .module('material.components.tabs')
|
---|
214 | .directive('mdTab', MdTab);
|
---|
215 |
|
---|
216 | function MdTab () {
|
---|
217 | return {
|
---|
218 | require: '^?mdTabs',
|
---|
219 | terminal: true,
|
---|
220 | compile: function (element, attr) {
|
---|
221 | var label = firstChild(element, 'md-tab-label'),
|
---|
222 | body = firstChild(element, 'md-tab-body');
|
---|
223 |
|
---|
224 | if (label.length === 0) {
|
---|
225 | label = angular.element('<md-tab-label></md-tab-label>');
|
---|
226 | if (attr.label) label.text(attr.label);
|
---|
227 | else label.append(element.contents());
|
---|
228 |
|
---|
229 | if (body.length === 0) {
|
---|
230 | var contents = element.contents().detach();
|
---|
231 | body = angular.element('<md-tab-body></md-tab-body>');
|
---|
232 | body.append(contents);
|
---|
233 | }
|
---|
234 | }
|
---|
235 |
|
---|
236 | element.append(label);
|
---|
237 | if (body.html()) element.append(body);
|
---|
238 |
|
---|
239 | return postLink;
|
---|
240 | },
|
---|
241 | scope: {
|
---|
242 | active: '=?mdActive',
|
---|
243 | disabled: '=?ngDisabled',
|
---|
244 | select: '&?mdOnSelect',
|
---|
245 | deselect: '&?mdOnDeselect',
|
---|
246 | tabClass: '@mdTabClass'
|
---|
247 | }
|
---|
248 | };
|
---|
249 |
|
---|
250 | function postLink (scope, element, attr, ctrl) {
|
---|
251 | if (!ctrl) return;
|
---|
252 | var index = ctrl.getTabElementIndex(element),
|
---|
253 | body = firstChild(element, 'md-tab-body').remove(),
|
---|
254 | label = firstChild(element, 'md-tab-label').remove(),
|
---|
255 | data = ctrl.insertTab({
|
---|
256 | scope: scope,
|
---|
257 | parent: scope.$parent,
|
---|
258 | index: index,
|
---|
259 | element: element,
|
---|
260 | template: body.html(),
|
---|
261 | label: label.html()
|
---|
262 | }, index);
|
---|
263 |
|
---|
264 | scope.select = scope.select || angular.noop;
|
---|
265 | scope.deselect = scope.deselect || angular.noop;
|
---|
266 |
|
---|
267 | scope.$watch('active', function (active) { if (active) ctrl.select(data.getIndex(), true); });
|
---|
268 | scope.$watch('disabled', function () { ctrl.refreshIndex(); });
|
---|
269 | scope.$watch(
|
---|
270 | function () {
|
---|
271 | return ctrl.getTabElementIndex(element);
|
---|
272 | },
|
---|
273 | function (newIndex) {
|
---|
274 | data.index = newIndex;
|
---|
275 | ctrl.updateTabOrder();
|
---|
276 | }
|
---|
277 | );
|
---|
278 | scope.$on('$destroy', function () { ctrl.removeTab(data); });
|
---|
279 | }
|
---|
280 |
|
---|
281 | function firstChild (element, tagName) {
|
---|
282 | var children = element[0].children;
|
---|
283 | for (var i = 0, len = children.length; i < len; i++) {
|
---|
284 | var child = children[i];
|
---|
285 | if (child.tagName === tagName.toUpperCase()) return angular.element(child);
|
---|
286 | }
|
---|
287 | return angular.element();
|
---|
288 | }
|
---|
289 | }
|
---|
290 |
|
---|
291 | angular
|
---|
292 | .module('material.components.tabs')
|
---|
293 | .directive('mdTabItem', MdTabItem);
|
---|
294 |
|
---|
295 | function MdTabItem () {
|
---|
296 | return {
|
---|
297 | require: '^?mdTabs',
|
---|
298 | link: function link (scope, element, attr, ctrl) {
|
---|
299 | if (!ctrl) return;
|
---|
300 | ctrl.attachRipple(scope, element);
|
---|
301 | }
|
---|
302 | };
|
---|
303 | }
|
---|
304 |
|
---|
305 | angular
|
---|
306 | .module('material.components.tabs')
|
---|
307 | .directive('mdTabLabel', MdTabLabel);
|
---|
308 |
|
---|
309 | function MdTabLabel () {
|
---|
310 | return { terminal: true };
|
---|
311 | }
|
---|
312 |
|
---|
313 |
|
---|
314 |
|
---|
315 | MdTabScroll['$inject'] = ["$parse"];angular.module('material.components.tabs')
|
---|
316 | .directive('mdTabScroll', MdTabScroll);
|
---|
317 |
|
---|
318 | function MdTabScroll ($parse) {
|
---|
319 | return {
|
---|
320 | restrict: 'A',
|
---|
321 | compile: function ($element, attr) {
|
---|
322 | var fn = $parse(attr.mdTabScroll, null, true);
|
---|
323 | return function ngEventHandler (scope, element) {
|
---|
324 | element.on('wheel', function (event) {
|
---|
325 | scope.$apply(function () { fn(scope, { $event: event }); });
|
---|
326 | });
|
---|
327 | };
|
---|
328 | }
|
---|
329 | };
|
---|
330 | }
|
---|
331 |
|
---|
332 |
|
---|
333 | MdTabsController['$inject'] = ["$scope", "$element", "$window", "$mdConstant", "$mdTabInkRipple", "$mdUtil", "$animateCss", "$attrs", "$compile", "$mdTheming", "$mdInteraction", "$timeout", "MdTabsPaginationService"];angular
|
---|
334 | .module('material.components.tabs')
|
---|
335 | .controller('MdTabsController', MdTabsController);
|
---|
336 |
|
---|
337 | /**
|
---|
338 | * ngInject
|
---|
339 | */
|
---|
340 | function MdTabsController ($scope, $element, $window, $mdConstant, $mdTabInkRipple, $mdUtil,
|
---|
341 | $animateCss, $attrs, $compile, $mdTheming, $mdInteraction, $timeout,
|
---|
342 | MdTabsPaginationService) {
|
---|
343 | // define private properties
|
---|
344 | var ctrl = this,
|
---|
345 | locked = false,
|
---|
346 | queue = [],
|
---|
347 | destroyed = false,
|
---|
348 | loaded = false;
|
---|
349 |
|
---|
350 | // Define public methods
|
---|
351 | ctrl.$onInit = $onInit;
|
---|
352 | ctrl.updatePagination = $mdUtil.debounce(updatePagination, 100);
|
---|
353 | ctrl.redirectFocus = redirectFocus;
|
---|
354 | ctrl.attachRipple = attachRipple;
|
---|
355 | ctrl.insertTab = insertTab;
|
---|
356 | ctrl.removeTab = removeTab;
|
---|
357 | ctrl.select = select;
|
---|
358 | ctrl.scroll = scroll;
|
---|
359 | ctrl.nextPage = nextPage;
|
---|
360 | ctrl.previousPage = previousPage;
|
---|
361 | ctrl.keydown = keydown;
|
---|
362 | ctrl.canPageForward = canPageForward;
|
---|
363 | ctrl.canPageBack = canPageBack;
|
---|
364 | ctrl.refreshIndex = refreshIndex;
|
---|
365 | ctrl.incrementIndex = incrementIndex;
|
---|
366 | ctrl.getTabElementIndex = getTabElementIndex;
|
---|
367 | ctrl.updateInkBarStyles = $mdUtil.debounce(updateInkBarStyles, 100);
|
---|
368 | ctrl.updateTabOrder = $mdUtil.debounce(updateTabOrder, 100);
|
---|
369 | ctrl.getFocusedTabId = getFocusedTabId;
|
---|
370 |
|
---|
371 | // For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
|
---|
372 | // manually call the $onInit hook.
|
---|
373 | if (angular.version.major === 1 && angular.version.minor <= 4) {
|
---|
374 | this.$onInit();
|
---|
375 | }
|
---|
376 |
|
---|
377 | /**
|
---|
378 | * AngularJS Lifecycle hook for newer AngularJS versions.
|
---|
379 | * Bindings are not guaranteed to have been assigned in the controller, but they are in the
|
---|
380 | * $onInit hook.
|
---|
381 | */
|
---|
382 | function $onInit() {
|
---|
383 | // Define one-way bindings
|
---|
384 | defineOneWayBinding('stretchTabs', handleStretchTabs);
|
---|
385 |
|
---|
386 | // Define public properties with change handlers
|
---|
387 | defineProperty('focusIndex', handleFocusIndexChange, ctrl.selectedIndex || 0);
|
---|
388 | defineProperty('offsetLeft', handleOffsetChange, 0);
|
---|
389 | defineProperty('hasContent', handleHasContent, false);
|
---|
390 | defineProperty('maxTabWidth', handleMaxTabWidth, getMaxTabWidth());
|
---|
391 | defineProperty('shouldPaginate', handleShouldPaginate, false);
|
---|
392 |
|
---|
393 | // Define boolean attributes
|
---|
394 | defineBooleanAttribute('noInkBar', handleInkBar);
|
---|
395 | defineBooleanAttribute('dynamicHeight', handleDynamicHeight);
|
---|
396 | defineBooleanAttribute('noPagination');
|
---|
397 | defineBooleanAttribute('swipeContent');
|
---|
398 | defineBooleanAttribute('autoselect');
|
---|
399 | defineBooleanAttribute('noSelectClick');
|
---|
400 | defineBooleanAttribute('centerTabs', handleCenterTabs);
|
---|
401 | defineBooleanAttribute('enableDisconnect');
|
---|
402 |
|
---|
403 | // Define public properties
|
---|
404 | ctrl.scope = $scope;
|
---|
405 | ctrl.parent = $scope.$parent;
|
---|
406 | ctrl.tabs = [];
|
---|
407 | ctrl.lastSelectedIndex = null;
|
---|
408 | ctrl.hasFocus = false;
|
---|
409 | ctrl.styleTabItemFocus = false;
|
---|
410 | ctrl.shouldCenterTabs = shouldCenterTabs();
|
---|
411 | ctrl.tabContentPrefix = 'tab-content-';
|
---|
412 | ctrl.navigationHint = 'Use the left and right arrow keys to navigate between tabs';
|
---|
413 |
|
---|
414 | // Setup the tabs controller after all bindings are available.
|
---|
415 | setupTabsController();
|
---|
416 | }
|
---|
417 |
|
---|
418 | /**
|
---|
419 | * Perform setup for the controller, setup events and watcher(s)
|
---|
420 | */
|
---|
421 | function setupTabsController () {
|
---|
422 | ctrl.selectedIndex = ctrl.selectedIndex || 0;
|
---|
423 | compileTemplate();
|
---|
424 | configureWatchers();
|
---|
425 | bindEvents();
|
---|
426 | $mdTheming($element);
|
---|
427 | $mdUtil.nextTick(function () {
|
---|
428 | updateHeightFromContent();
|
---|
429 | adjustOffset();
|
---|
430 | updateInkBarStyles();
|
---|
431 | ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
|
---|
432 | loaded = true;
|
---|
433 | updatePagination();
|
---|
434 | });
|
---|
435 | }
|
---|
436 |
|
---|
437 | /**
|
---|
438 | * Compiles the template provided by the user. This is passed as an attribute from the tabs
|
---|
439 | * directive's template function.
|
---|
440 | */
|
---|
441 | function compileTemplate () {
|
---|
442 | var template = $attrs.$mdTabsTemplate,
|
---|
443 | element = angular.element($element[0].querySelector('md-tab-data'));
|
---|
444 |
|
---|
445 | element.html(template);
|
---|
446 | $compile(element.contents())(ctrl.parent);
|
---|
447 | delete $attrs.$mdTabsTemplate;
|
---|
448 | }
|
---|
449 |
|
---|
450 | /**
|
---|
451 | * Binds events used by the tabs component.
|
---|
452 | */
|
---|
453 | function bindEvents () {
|
---|
454 | angular.element($window).on('resize', handleWindowResize);
|
---|
455 | $scope.$on('$destroy', cleanup);
|
---|
456 | }
|
---|
457 |
|
---|
458 | /**
|
---|
459 | * Configure watcher(s) used by Tabs
|
---|
460 | */
|
---|
461 | function configureWatchers () {
|
---|
462 | $scope.$watch('$mdTabsCtrl.selectedIndex', handleSelectedIndexChange);
|
---|
463 | }
|
---|
464 |
|
---|
465 | /**
|
---|
466 | * Creates a one-way binding manually rather than relying on AngularJS's isolated scope
|
---|
467 | * @param key
|
---|
468 | * @param handler
|
---|
469 | */
|
---|
470 | function defineOneWayBinding (key, handler) {
|
---|
471 | var attr = $attrs.$normalize('md-' + key);
|
---|
472 | if (handler) defineProperty(key, handler);
|
---|
473 | $attrs.$observe(attr, function (newValue) { ctrl[ key ] = newValue; });
|
---|
474 | }
|
---|
475 |
|
---|
476 | /**
|
---|
477 | * Defines boolean attributes with default value set to true. I.e. md-stretch-tabs with no value
|
---|
478 | * will be treated as being truthy.
|
---|
479 | * @param {string} key
|
---|
480 | * @param {Function=} handler
|
---|
481 | */
|
---|
482 | function defineBooleanAttribute (key, handler) {
|
---|
483 | var attr = $attrs.$normalize('md-' + key);
|
---|
484 | if (handler) defineProperty(key, handler, undefined);
|
---|
485 | if ($attrs.hasOwnProperty(attr)) updateValue($attrs[attr]);
|
---|
486 | $attrs.$observe(attr, updateValue);
|
---|
487 | function updateValue (newValue) {
|
---|
488 | ctrl[ key ] = newValue !== 'false';
|
---|
489 | }
|
---|
490 | }
|
---|
491 |
|
---|
492 | /**
|
---|
493 | * Remove any events defined by this controller
|
---|
494 | */
|
---|
495 | function cleanup () {
|
---|
496 | destroyed = true;
|
---|
497 | angular.element($window).off('resize', handleWindowResize);
|
---|
498 | }
|
---|
499 |
|
---|
500 | // Change handlers
|
---|
501 |
|
---|
502 | /**
|
---|
503 | * Toggles stretch tabs class and updates inkbar when tab stretching changes.
|
---|
504 | */
|
---|
505 | function handleStretchTabs () {
|
---|
506 | var elements = getElements();
|
---|
507 | angular.element(elements.wrapper).toggleClass('md-stretch-tabs', shouldStretchTabs());
|
---|
508 | updateInkBarStyles();
|
---|
509 | }
|
---|
510 |
|
---|
511 | /**
|
---|
512 | * Update the value of ctrl.shouldCenterTabs.
|
---|
513 | */
|
---|
514 | function handleCenterTabs () {
|
---|
515 | ctrl.shouldCenterTabs = shouldCenterTabs();
|
---|
516 | }
|
---|
517 |
|
---|
518 | /**
|
---|
519 | * @param {number} newWidth new max tab width in pixels
|
---|
520 | * @param {number} oldWidth previous max tab width in pixels
|
---|
521 | */
|
---|
522 | function handleMaxTabWidth (newWidth, oldWidth) {
|
---|
523 | if (newWidth !== oldWidth) {
|
---|
524 | var elements = getElements();
|
---|
525 |
|
---|
526 | // Set the max width for the real tabs
|
---|
527 | angular.forEach(elements.tabs, function(tab) {
|
---|
528 | tab.style.maxWidth = newWidth + 'px';
|
---|
529 | });
|
---|
530 |
|
---|
531 | // Set the max width for the dummy tabs too
|
---|
532 | angular.forEach(elements.dummies, function(tab) {
|
---|
533 | tab.style.maxWidth = newWidth + 'px';
|
---|
534 | });
|
---|
535 |
|
---|
536 | $mdUtil.nextTick(ctrl.updateInkBarStyles);
|
---|
537 | }
|
---|
538 | }
|
---|
539 |
|
---|
540 | function handleShouldPaginate (newValue, oldValue) {
|
---|
541 | if (newValue !== oldValue) {
|
---|
542 | ctrl.maxTabWidth = getMaxTabWidth();
|
---|
543 | ctrl.shouldCenterTabs = shouldCenterTabs();
|
---|
544 | $mdUtil.nextTick(function () {
|
---|
545 | ctrl.maxTabWidth = getMaxTabWidth();
|
---|
546 | adjustOffset(ctrl.selectedIndex);
|
---|
547 | });
|
---|
548 | }
|
---|
549 | }
|
---|
550 |
|
---|
551 | /**
|
---|
552 | * Add/remove the `md-no-tab-content` class depending on `ctrl.hasContent`
|
---|
553 | * @param {boolean} hasContent
|
---|
554 | */
|
---|
555 | function handleHasContent (hasContent) {
|
---|
556 | $element[ hasContent ? 'removeClass' : 'addClass' ]('md-no-tab-content');
|
---|
557 | }
|
---|
558 |
|
---|
559 | /**
|
---|
560 | * Apply ctrl.offsetLeft to the paging element when it changes
|
---|
561 | * @param {string|number} left
|
---|
562 | */
|
---|
563 | function handleOffsetChange (left) {
|
---|
564 | var newValue = ((ctrl.shouldCenterTabs || isRtl() ? '' : '-') + left + 'px');
|
---|
565 |
|
---|
566 | // Fix double-negative which can happen with RTL support
|
---|
567 | newValue = newValue.replace('--', '');
|
---|
568 |
|
---|
569 | angular.element(getElements().paging).css($mdConstant.CSS.TRANSFORM,
|
---|
570 | 'translate(' + newValue + ', 0)');
|
---|
571 | $scope.$broadcast('$mdTabsPaginationChanged');
|
---|
572 | }
|
---|
573 |
|
---|
574 | /**
|
---|
575 | * Update the UI whenever `ctrl.focusIndex` is updated
|
---|
576 | * @param {number} newIndex
|
---|
577 | * @param {number} oldIndex
|
---|
578 | */
|
---|
579 | function handleFocusIndexChange (newIndex, oldIndex) {
|
---|
580 | if (newIndex === oldIndex) return;
|
---|
581 | if (!getElements().tabs[ newIndex ]) return;
|
---|
582 | adjustOffset();
|
---|
583 | redirectFocus();
|
---|
584 | }
|
---|
585 |
|
---|
586 | /**
|
---|
587 | * Update the UI whenever the selected index changes. Calls user-defined select/deselect methods.
|
---|
588 | * @param {number} newValue selected index's new value
|
---|
589 | * @param {number} oldValue selected index's previous value
|
---|
590 | */
|
---|
591 | function handleSelectedIndexChange (newValue, oldValue) {
|
---|
592 | if (newValue === oldValue) return;
|
---|
593 |
|
---|
594 | ctrl.selectedIndex = getNearestSafeIndex(newValue);
|
---|
595 | ctrl.lastSelectedIndex = oldValue;
|
---|
596 | ctrl.updateInkBarStyles();
|
---|
597 | updateHeightFromContent();
|
---|
598 | adjustOffset(newValue);
|
---|
599 | $scope.$broadcast('$mdTabsChanged');
|
---|
600 | ctrl.tabs[ oldValue ] && ctrl.tabs[ oldValue ].scope.deselect();
|
---|
601 | ctrl.tabs[ newValue ] && ctrl.tabs[ newValue ].scope.select();
|
---|
602 | }
|
---|
603 |
|
---|
604 | function getTabElementIndex(tabEl){
|
---|
605 | var tabs = $element[0].getElementsByTagName('md-tab');
|
---|
606 | return Array.prototype.indexOf.call(tabs, tabEl[0]);
|
---|
607 | }
|
---|
608 |
|
---|
609 | /**
|
---|
610 | * Queues up a call to `handleWindowResize` when a resize occurs while the tabs component is
|
---|
611 | * hidden.
|
---|
612 | */
|
---|
613 | function handleResizeWhenVisible () {
|
---|
614 | // if there is already a watcher waiting for resize, do nothing
|
---|
615 | if (handleResizeWhenVisible.watcher) return;
|
---|
616 | // otherwise, we will abuse the $watch function to check for visible
|
---|
617 | handleResizeWhenVisible.watcher = $scope.$watch(function () {
|
---|
618 | // since we are checking for DOM size, we use $mdUtil.nextTick() to wait for after the DOM updates
|
---|
619 | $mdUtil.nextTick(function () {
|
---|
620 | // if the watcher has already run (ie. multiple digests in one cycle), do nothing
|
---|
621 | if (!handleResizeWhenVisible.watcher) return;
|
---|
622 |
|
---|
623 | if ($element.prop('offsetParent')) {
|
---|
624 | handleResizeWhenVisible.watcher();
|
---|
625 | handleResizeWhenVisible.watcher = null;
|
---|
626 |
|
---|
627 | handleWindowResize();
|
---|
628 | }
|
---|
629 | }, false);
|
---|
630 | });
|
---|
631 | }
|
---|
632 |
|
---|
633 | // Event handlers / actions
|
---|
634 |
|
---|
635 | /**
|
---|
636 | * Handle user keyboard interactions
|
---|
637 | * @param {KeyboardEvent} event keydown event
|
---|
638 | */
|
---|
639 | function keydown (event) {
|
---|
640 | switch (event.keyCode) {
|
---|
641 | case $mdConstant.KEY_CODE.LEFT_ARROW:
|
---|
642 | event.preventDefault();
|
---|
643 | incrementIndex(-1, true);
|
---|
644 | break;
|
---|
645 | case $mdConstant.KEY_CODE.RIGHT_ARROW:
|
---|
646 | event.preventDefault();
|
---|
647 | incrementIndex(1, true);
|
---|
648 | break;
|
---|
649 | case $mdConstant.KEY_CODE.SPACE:
|
---|
650 | case $mdConstant.KEY_CODE.ENTER:
|
---|
651 | event.preventDefault();
|
---|
652 | if (!locked) select(ctrl.focusIndex);
|
---|
653 | break;
|
---|
654 | case $mdConstant.KEY_CODE.TAB:
|
---|
655 | // On tabbing out of the tablist, reset hasFocus to reset ng-focused and
|
---|
656 | // its md-focused class if the focused tab is not the active tab.
|
---|
657 | if (ctrl.focusIndex !== ctrl.selectedIndex) {
|
---|
658 | ctrl.focusIndex = ctrl.selectedIndex;
|
---|
659 | }
|
---|
660 | break;
|
---|
661 | }
|
---|
662 | }
|
---|
663 |
|
---|
664 | /**
|
---|
665 | * Update the selected index. Triggers a click event on the original `md-tab` element in order
|
---|
666 | * to fire user-added click events if canSkipClick or `md-no-select-click` are false.
|
---|
667 | * @param index
|
---|
668 | * @param canSkipClick Optionally allow not firing the click event if `md-no-select-click` is also true.
|
---|
669 | */
|
---|
670 | function select (index, canSkipClick) {
|
---|
671 | if (!locked) ctrl.focusIndex = ctrl.selectedIndex = index;
|
---|
672 | // skip the click event if noSelectClick is enabled
|
---|
673 | if (canSkipClick && ctrl.noSelectClick) return;
|
---|
674 | // nextTick is required to prevent errors in user-defined click events
|
---|
675 | $mdUtil.nextTick(function () {
|
---|
676 | ctrl.tabs[ index ].element.triggerHandler('click');
|
---|
677 | }, false);
|
---|
678 | }
|
---|
679 |
|
---|
680 | /**
|
---|
681 | * When pagination is on, this makes sure the selected index is in view.
|
---|
682 | * @param {WheelEvent} event
|
---|
683 | */
|
---|
684 | function scroll (event) {
|
---|
685 | if (!ctrl.shouldPaginate) return;
|
---|
686 | event.preventDefault();
|
---|
687 | if (event.deltaY) {
|
---|
688 | ctrl.offsetLeft = fixOffset(ctrl.offsetLeft + event.deltaY);
|
---|
689 | } else if (event.deltaX) {
|
---|
690 | ctrl.offsetLeft = fixOffset(ctrl.offsetLeft + event.deltaX);
|
---|
691 | }
|
---|
692 | }
|
---|
693 |
|
---|
694 | /**
|
---|
695 | * Slides the tabs over approximately one page forward.
|
---|
696 | */
|
---|
697 | function nextPage () {
|
---|
698 | if (!ctrl.canPageForward()) { return; }
|
---|
699 |
|
---|
700 | var newOffset = MdTabsPaginationService.increasePageOffset(getElements(), ctrl.offsetLeft);
|
---|
701 |
|
---|
702 | ctrl.offsetLeft = fixOffset(newOffset);
|
---|
703 | }
|
---|
704 |
|
---|
705 | /**
|
---|
706 | * Slides the tabs over approximately one page backward.
|
---|
707 | */
|
---|
708 | function previousPage () {
|
---|
709 | if (!ctrl.canPageBack()) { return; }
|
---|
710 |
|
---|
711 | var newOffset = MdTabsPaginationService.decreasePageOffset(getElements(), ctrl.offsetLeft);
|
---|
712 |
|
---|
713 | // Set the new offset
|
---|
714 | ctrl.offsetLeft = fixOffset(newOffset);
|
---|
715 | }
|
---|
716 |
|
---|
717 | /**
|
---|
718 | * Update size calculations when the window is resized.
|
---|
719 | */
|
---|
720 | function handleWindowResize () {
|
---|
721 | ctrl.lastSelectedIndex = ctrl.selectedIndex;
|
---|
722 | ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
|
---|
723 |
|
---|
724 | $mdUtil.nextTick(function () {
|
---|
725 | ctrl.updateInkBarStyles();
|
---|
726 | updatePagination();
|
---|
727 | });
|
---|
728 | }
|
---|
729 |
|
---|
730 | /**
|
---|
731 | * Hides or shows the tabs ink bar.
|
---|
732 | * @param {boolean} hide A Boolean (not just truthy/falsy) value to determine whether the class
|
---|
733 | * should be added or removed.
|
---|
734 | */
|
---|
735 | function handleInkBar (hide) {
|
---|
736 | angular.element(getElements().inkBar).toggleClass('ng-hide', hide);
|
---|
737 | }
|
---|
738 |
|
---|
739 | /**
|
---|
740 | * Enables or disables tabs dynamic height.
|
---|
741 | * @param {boolean} value A Boolean (not just truthy/falsy) value to determine whether the class
|
---|
742 | * should be added or removed.
|
---|
743 | */
|
---|
744 | function handleDynamicHeight (value) {
|
---|
745 | $element.toggleClass('md-dynamic-height', value);
|
---|
746 | }
|
---|
747 |
|
---|
748 | /**
|
---|
749 | * Remove a tab from the data and select the nearest valid tab.
|
---|
750 | * @param {Object} tabData tab to remove
|
---|
751 | */
|
---|
752 | function removeTab (tabData) {
|
---|
753 | if (destroyed) return;
|
---|
754 | var selectedIndex = ctrl.selectedIndex,
|
---|
755 | tab = ctrl.tabs.splice(tabData.getIndex(), 1)[ 0 ];
|
---|
756 | refreshIndex();
|
---|
757 | // when removing a tab, if the selected index did not change, we have to manually trigger the
|
---|
758 | // tab select/deselect events
|
---|
759 | if (ctrl.selectedIndex === selectedIndex) {
|
---|
760 | tab.scope.deselect();
|
---|
761 | ctrl.tabs[ ctrl.selectedIndex ] && ctrl.tabs[ ctrl.selectedIndex ].scope.select();
|
---|
762 | }
|
---|
763 | $mdUtil.nextTick(function () {
|
---|
764 | updatePagination();
|
---|
765 | ctrl.offsetLeft = fixOffset(ctrl.offsetLeft);
|
---|
766 | });
|
---|
767 | }
|
---|
768 |
|
---|
769 | /**
|
---|
770 | * Create an entry in the tabs array for a new tab at the specified index.
|
---|
771 | * @param {Object} tabData tab to insert
|
---|
772 | * @param {number} index location to insert the new tab
|
---|
773 | * @returns {Object} the inserted tab
|
---|
774 | */
|
---|
775 | function insertTab (tabData, index) {
|
---|
776 | var hasLoaded = loaded;
|
---|
777 | var proto = {
|
---|
778 | getIndex: function () { return ctrl.tabs.indexOf(tab); },
|
---|
779 | isActive: function () { return this.getIndex() === ctrl.selectedIndex; },
|
---|
780 | isLeft: function () { return this.getIndex() < ctrl.selectedIndex; },
|
---|
781 | isRight: function () { return this.getIndex() > ctrl.selectedIndex; },
|
---|
782 | shouldRender: function () { return ctrl.dynamicHeight || this.isActive(); },
|
---|
783 | hasFocus: function () {
|
---|
784 | return ctrl.styleTabItemFocus
|
---|
785 | && ctrl.hasFocus && this.getIndex() === ctrl.focusIndex;
|
---|
786 | },
|
---|
787 | id: $mdUtil.nextUid(),
|
---|
788 | hasContent: !!(tabData.template && tabData.template.trim())
|
---|
789 | };
|
---|
790 | var tab = angular.extend(proto, tabData);
|
---|
791 |
|
---|
792 | if (angular.isDefined(index)) {
|
---|
793 | ctrl.tabs.splice(index, 0, tab);
|
---|
794 | } else {
|
---|
795 | ctrl.tabs.push(tab);
|
---|
796 | }
|
---|
797 | processQueue();
|
---|
798 | updateHasContent();
|
---|
799 |
|
---|
800 | $mdUtil.nextTick(function () {
|
---|
801 | updatePagination();
|
---|
802 | setAriaControls(tab);
|
---|
803 |
|
---|
804 | // if autoselect is enabled, select the newly added tab
|
---|
805 | if (hasLoaded && ctrl.autoselect) {
|
---|
806 | $mdUtil.nextTick(function () {
|
---|
807 | $mdUtil.nextTick(function () { select(ctrl.tabs.indexOf(tab)); });
|
---|
808 | });
|
---|
809 | }
|
---|
810 | });
|
---|
811 | return tab;
|
---|
812 | }
|
---|
813 |
|
---|
814 | // Getter methods
|
---|
815 |
|
---|
816 | /**
|
---|
817 | * Gathers references to all of the DOM elements used by this controller.
|
---|
818 | * @returns {Object}
|
---|
819 | */
|
---|
820 | function getElements () {
|
---|
821 | var elements = {};
|
---|
822 | var node = $element[0];
|
---|
823 |
|
---|
824 | // gather tab bar elements
|
---|
825 | elements.wrapper = node.querySelector('md-tabs-wrapper');
|
---|
826 | elements.canvas = elements.wrapper.querySelector('md-tabs-canvas');
|
---|
827 | elements.paging = elements.canvas.querySelector('md-pagination-wrapper');
|
---|
828 | elements.inkBar = elements.paging.querySelector('md-ink-bar');
|
---|
829 | elements.nextButton = node.querySelector('md-next-button');
|
---|
830 | elements.prevButton = node.querySelector('md-prev-button');
|
---|
831 |
|
---|
832 | elements.contents = node.querySelectorAll('md-tabs-content-wrapper > md-tab-content');
|
---|
833 | elements.tabs = elements.paging.querySelectorAll('md-tab-item');
|
---|
834 | elements.dummies = elements.canvas.querySelectorAll('md-dummy-tab');
|
---|
835 |
|
---|
836 | return elements;
|
---|
837 | }
|
---|
838 |
|
---|
839 | /**
|
---|
840 | * Determines whether or not the left pagination arrow should be enabled.
|
---|
841 | * @returns {boolean}
|
---|
842 | */
|
---|
843 | function canPageBack () {
|
---|
844 | // This works for both LTR and RTL
|
---|
845 | return ctrl.offsetLeft > 0;
|
---|
846 | }
|
---|
847 |
|
---|
848 | /**
|
---|
849 | * Determines whether or not the right pagination arrow should be enabled.
|
---|
850 | * @returns {*|boolean}
|
---|
851 | */
|
---|
852 | function canPageForward () {
|
---|
853 | var elements = getElements();
|
---|
854 | var lastTab = elements.tabs[ elements.tabs.length - 1 ];
|
---|
855 |
|
---|
856 | if (isRtl()) {
|
---|
857 | return ctrl.offsetLeft < elements.paging.offsetWidth - elements.canvas.offsetWidth;
|
---|
858 | }
|
---|
859 |
|
---|
860 | return lastTab && lastTab.offsetLeft + lastTab.offsetWidth > elements.canvas.clientWidth +
|
---|
861 | ctrl.offsetLeft;
|
---|
862 | }
|
---|
863 |
|
---|
864 | /**
|
---|
865 | * Returns currently focused tab item's element ID
|
---|
866 | */
|
---|
867 | function getFocusedTabId() {
|
---|
868 | var focusedTab = ctrl.tabs[ctrl.focusIndex];
|
---|
869 | if (!focusedTab || !focusedTab.id) {
|
---|
870 | return null;
|
---|
871 | }
|
---|
872 | return 'tab-item-' + focusedTab.id;
|
---|
873 | }
|
---|
874 |
|
---|
875 | /**
|
---|
876 | * Determines if the UI should stretch the tabs to fill the available space.
|
---|
877 | * @returns {*}
|
---|
878 | */
|
---|
879 | function shouldStretchTabs () {
|
---|
880 | switch (ctrl.stretchTabs) {
|
---|
881 | case 'always':
|
---|
882 | return true;
|
---|
883 | case 'never':
|
---|
884 | return false;
|
---|
885 | default:
|
---|
886 | return !ctrl.shouldPaginate
|
---|
887 | && $window.matchMedia('(max-width: 600px)').matches;
|
---|
888 | }
|
---|
889 | }
|
---|
890 |
|
---|
891 | /**
|
---|
892 | * Determines if the tabs should appear centered.
|
---|
893 | * @returns {boolean}
|
---|
894 | */
|
---|
895 | function shouldCenterTabs () {
|
---|
896 | return ctrl.centerTabs && !ctrl.shouldPaginate;
|
---|
897 | }
|
---|
898 |
|
---|
899 | /**
|
---|
900 | * Determines if pagination is necessary to display the tabs within the available space.
|
---|
901 | * @returns {boolean} true if pagination is necessary, false otherwise
|
---|
902 | */
|
---|
903 | function shouldPaginate () {
|
---|
904 | var shouldPaginate;
|
---|
905 | if (ctrl.noPagination || !loaded) return false;
|
---|
906 | var canvasWidth = $element.prop('clientWidth');
|
---|
907 |
|
---|
908 | angular.forEach(getElements().tabs, function (tab) {
|
---|
909 | canvasWidth -= tab.offsetWidth;
|
---|
910 | });
|
---|
911 |
|
---|
912 | shouldPaginate = canvasWidth < 0;
|
---|
913 | // Work around width calculation issues on IE11 when pagination is enabled.
|
---|
914 | // Don't do this on other browsers because it breaks scroll to new tab animation.
|
---|
915 | if ($mdUtil.msie) {
|
---|
916 | if (shouldPaginate) {
|
---|
917 | getElements().paging.style.width = '999999px';
|
---|
918 | } else {
|
---|
919 | getElements().paging.style.width = undefined;
|
---|
920 | }
|
---|
921 | }
|
---|
922 | return shouldPaginate;
|
---|
923 | }
|
---|
924 |
|
---|
925 | /**
|
---|
926 | * Finds the nearest tab index that is available. This is primarily used for when the active
|
---|
927 | * tab is removed.
|
---|
928 | * @param newIndex
|
---|
929 | * @returns {*}
|
---|
930 | */
|
---|
931 | function getNearestSafeIndex (newIndex) {
|
---|
932 | if (newIndex === -1) return -1;
|
---|
933 | var maxOffset = Math.max(ctrl.tabs.length - newIndex, newIndex),
|
---|
934 | i, tab;
|
---|
935 | for (i = 0; i <= maxOffset; i++) {
|
---|
936 | tab = ctrl.tabs[ newIndex + i ];
|
---|
937 | if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
|
---|
938 | tab = ctrl.tabs[ newIndex - i ];
|
---|
939 | if (tab && (tab.scope.disabled !== true)) return tab.getIndex();
|
---|
940 | }
|
---|
941 | return newIndex;
|
---|
942 | }
|
---|
943 |
|
---|
944 | // Utility methods
|
---|
945 |
|
---|
946 | /**
|
---|
947 | * Defines a property using a getter and setter in order to trigger a change handler without
|
---|
948 | * using `$watch` to observe changes.
|
---|
949 | * @param {PropertyKey} key
|
---|
950 | * @param {Function} handler
|
---|
951 | * @param {any} value
|
---|
952 | */
|
---|
953 | function defineProperty (key, handler, value) {
|
---|
954 | Object.defineProperty(ctrl, key, {
|
---|
955 | get: function () { return value; },
|
---|
956 | set: function (newValue) {
|
---|
957 | var oldValue = value;
|
---|
958 | value = newValue;
|
---|
959 | handler && handler(newValue, oldValue);
|
---|
960 | }
|
---|
961 | });
|
---|
962 | }
|
---|
963 |
|
---|
964 | /**
|
---|
965 | * Updates whether or not pagination should be displayed.
|
---|
966 | */
|
---|
967 | function updatePagination () {
|
---|
968 | ctrl.maxTabWidth = getMaxTabWidth();
|
---|
969 | ctrl.shouldPaginate = shouldPaginate();
|
---|
970 | }
|
---|
971 |
|
---|
972 | /**
|
---|
973 | * @param {Array<HTMLElement>} tabs tab item elements for use in computing total width
|
---|
974 | * @returns {number} the width of the tabs in the specified array in pixels
|
---|
975 | */
|
---|
976 | function calcTabsWidth(tabs) {
|
---|
977 | var width = 0;
|
---|
978 |
|
---|
979 | angular.forEach(tabs, function (tab) {
|
---|
980 | // Uses the larger value between `getBoundingClientRect().width` and `offsetWidth`. This
|
---|
981 | // prevents `offsetWidth` value from being rounded down and causing wrapping issues, but
|
---|
982 | // also handles scenarios where `getBoundingClientRect()` is inaccurate (ie. tabs inside
|
---|
983 | // of a dialog).
|
---|
984 | width += Math.max(tab.offsetWidth, tab.getBoundingClientRect().width);
|
---|
985 | });
|
---|
986 |
|
---|
987 | return Math.ceil(width);
|
---|
988 | }
|
---|
989 |
|
---|
990 | /**
|
---|
991 | * @returns {number} either the max width as constrained by the container or the max width from
|
---|
992 | * the 2017 version of the Material Design spec.
|
---|
993 | */
|
---|
994 | function getMaxTabWidth() {
|
---|
995 | var elements = getElements(),
|
---|
996 | containerWidth = elements.canvas.clientWidth,
|
---|
997 |
|
---|
998 | // See https://material.io/archive/guidelines/components/tabs.html#tabs-specs
|
---|
999 | specMax = 264;
|
---|
1000 |
|
---|
1001 | // Do the spec maximum, or the canvas width; whichever is *smaller* (tabs larger than the canvas
|
---|
1002 | // width can break the pagination) but not less than 0
|
---|
1003 | return Math.max(0, Math.min(containerWidth - 1, specMax));
|
---|
1004 | }
|
---|
1005 |
|
---|
1006 | /**
|
---|
1007 | * Re-orders the tabs and updates the selected and focus indexes to their new positions.
|
---|
1008 | * This is triggered by `tabDirective.js` when the user's tabs have been re-ordered.
|
---|
1009 | */
|
---|
1010 | function updateTabOrder () {
|
---|
1011 | var selectedItem = ctrl.tabs[ ctrl.selectedIndex ],
|
---|
1012 | focusItem = ctrl.tabs[ ctrl.focusIndex ];
|
---|
1013 | ctrl.tabs = ctrl.tabs.sort(function (a, b) {
|
---|
1014 | return a.index - b.index;
|
---|
1015 | });
|
---|
1016 | ctrl.selectedIndex = ctrl.tabs.indexOf(selectedItem);
|
---|
1017 | ctrl.focusIndex = ctrl.tabs.indexOf(focusItem);
|
---|
1018 | }
|
---|
1019 |
|
---|
1020 | /**
|
---|
1021 | * This moves the selected or focus index left or right. This is used by the keydown handler.
|
---|
1022 | * @param {number} inc amount to increment
|
---|
1023 | * @param {boolean} focus true to increment the focus index, false to increment the selected index
|
---|
1024 | */
|
---|
1025 | function incrementIndex (inc, focus) {
|
---|
1026 | var newIndex,
|
---|
1027 | key = focus ? 'focusIndex' : 'selectedIndex',
|
---|
1028 | index = ctrl[ key ];
|
---|
1029 | for (newIndex = index + inc;
|
---|
1030 | ctrl.tabs[ newIndex ] && ctrl.tabs[ newIndex ].scope.disabled;
|
---|
1031 | newIndex += inc) { /* do nothing */ }
|
---|
1032 |
|
---|
1033 | newIndex = (index + inc + ctrl.tabs.length) % ctrl.tabs.length;
|
---|
1034 |
|
---|
1035 | if (ctrl.tabs[ newIndex ]) {
|
---|
1036 | ctrl[ key ] = newIndex;
|
---|
1037 | }
|
---|
1038 | }
|
---|
1039 |
|
---|
1040 | /**
|
---|
1041 | * This is used to forward focus to tab container elements. This method is necessary to avoid
|
---|
1042 | * animation issues when attempting to focus an item that is out of view.
|
---|
1043 | */
|
---|
1044 | function redirectFocus () {
|
---|
1045 | ctrl.styleTabItemFocus = ($mdInteraction.getLastInteractionType() === 'keyboard');
|
---|
1046 | var tabToFocus = getElements().tabs[ctrl.focusIndex];
|
---|
1047 | if (tabToFocus) {
|
---|
1048 | tabToFocus.focus();
|
---|
1049 | }
|
---|
1050 | }
|
---|
1051 |
|
---|
1052 | /**
|
---|
1053 | * Forces the pagination to move the focused tab into view.
|
---|
1054 | * @param {number=} index of tab to have its offset adjusted
|
---|
1055 | */
|
---|
1056 | function adjustOffset (index) {
|
---|
1057 | var elements = getElements();
|
---|
1058 |
|
---|
1059 | if (!angular.isNumber(index)) index = ctrl.focusIndex;
|
---|
1060 | if (!elements.tabs[ index ]) return;
|
---|
1061 | if (ctrl.shouldCenterTabs) return;
|
---|
1062 | var tab = elements.tabs[ index ],
|
---|
1063 | left = tab.offsetLeft,
|
---|
1064 | right = tab.offsetWidth + left,
|
---|
1065 | extraOffset = 32;
|
---|
1066 |
|
---|
1067 | // If we are selecting the first tab (in LTR and RTL), always set the offset to 0
|
---|
1068 | if (index === 0) {
|
---|
1069 | ctrl.offsetLeft = 0;
|
---|
1070 | return;
|
---|
1071 | }
|
---|
1072 |
|
---|
1073 | if (isRtl()) {
|
---|
1074 | var tabWidthsBefore = calcTabsWidth(Array.prototype.slice.call(elements.tabs, 0, index));
|
---|
1075 | var tabWidthsIncluding = calcTabsWidth(Array.prototype.slice.call(elements.tabs, 0, index + 1));
|
---|
1076 |
|
---|
1077 | ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(tabWidthsBefore));
|
---|
1078 | ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(tabWidthsIncluding - elements.canvas.clientWidth));
|
---|
1079 | } else {
|
---|
1080 | ctrl.offsetLeft = Math.max(ctrl.offsetLeft, fixOffset(right - elements.canvas.clientWidth + extraOffset));
|
---|
1081 | ctrl.offsetLeft = Math.min(ctrl.offsetLeft, fixOffset(left));
|
---|
1082 | }
|
---|
1083 | }
|
---|
1084 |
|
---|
1085 | /**
|
---|
1086 | * Iterates through all queued functions and clears the queue. This is used for functions that
|
---|
1087 | * are called before the UI is ready, such as size calculations.
|
---|
1088 | */
|
---|
1089 | function processQueue () {
|
---|
1090 | queue.forEach(function (func) { $mdUtil.nextTick(func); });
|
---|
1091 | queue = [];
|
---|
1092 | }
|
---|
1093 |
|
---|
1094 | /**
|
---|
1095 | * Determines if the tab content area is needed.
|
---|
1096 | */
|
---|
1097 | function updateHasContent () {
|
---|
1098 | var hasContent = false;
|
---|
1099 | var i;
|
---|
1100 |
|
---|
1101 | for (i = 0; i < ctrl.tabs.length; i++) {
|
---|
1102 | if (ctrl.tabs[i].hasContent) {
|
---|
1103 | hasContent = true;
|
---|
1104 | break;
|
---|
1105 | }
|
---|
1106 | }
|
---|
1107 |
|
---|
1108 | ctrl.hasContent = hasContent;
|
---|
1109 | }
|
---|
1110 |
|
---|
1111 | /**
|
---|
1112 | * Moves the indexes to their nearest valid values.
|
---|
1113 | */
|
---|
1114 | function refreshIndex () {
|
---|
1115 | ctrl.selectedIndex = getNearestSafeIndex(ctrl.selectedIndex);
|
---|
1116 | ctrl.focusIndex = getNearestSafeIndex(ctrl.focusIndex);
|
---|
1117 | }
|
---|
1118 |
|
---|
1119 | /**
|
---|
1120 | * Calculates the content height of the current tab.
|
---|
1121 | * @returns {*}
|
---|
1122 | */
|
---|
1123 | function updateHeightFromContent () {
|
---|
1124 | if (!ctrl.dynamicHeight) return $element.css('height', '');
|
---|
1125 | if (!ctrl.tabs.length) return queue.push(updateHeightFromContent);
|
---|
1126 |
|
---|
1127 | var elements = getElements();
|
---|
1128 |
|
---|
1129 | var tabContent = elements.contents[ ctrl.selectedIndex ],
|
---|
1130 | contentHeight = tabContent ? tabContent.offsetHeight : 0,
|
---|
1131 | tabsHeight = elements.wrapper.offsetHeight,
|
---|
1132 | newHeight = contentHeight + tabsHeight,
|
---|
1133 | currentHeight = $element.prop('clientHeight');
|
---|
1134 |
|
---|
1135 | if (currentHeight === newHeight) return;
|
---|
1136 |
|
---|
1137 | // Adjusts calculations for when the buttons are bottom-aligned since this relies on absolute
|
---|
1138 | // positioning. This should probably be cleaned up if a cleaner solution is possible.
|
---|
1139 | if ($element.attr('md-align-tabs') === 'bottom') {
|
---|
1140 | currentHeight -= tabsHeight;
|
---|
1141 | newHeight -= tabsHeight;
|
---|
1142 | // Need to include bottom border in these calculations
|
---|
1143 | if ($element.attr('md-border-bottom') !== undefined) {
|
---|
1144 | ++currentHeight;
|
---|
1145 | }
|
---|
1146 | }
|
---|
1147 |
|
---|
1148 | // Lock during animation so the user can't change tabs
|
---|
1149 | locked = true;
|
---|
1150 |
|
---|
1151 | var fromHeight = { height: currentHeight + 'px' },
|
---|
1152 | toHeight = { height: newHeight + 'px' };
|
---|
1153 |
|
---|
1154 | // Set the height to the current, specific pixel height to fix a bug on iOS where the height
|
---|
1155 | // first animates to 0, then back to the proper height causing a visual glitch
|
---|
1156 | $element.css(fromHeight);
|
---|
1157 |
|
---|
1158 | // Animate the height from the old to the new
|
---|
1159 | $animateCss($element, {
|
---|
1160 | from: fromHeight,
|
---|
1161 | to: toHeight,
|
---|
1162 | easing: 'cubic-bezier(0.35, 0, 0.25, 1)',
|
---|
1163 | duration: 0.5
|
---|
1164 | }).start().done(function () {
|
---|
1165 | // Then (to fix the same iOS issue as above), disable transitions and remove the specific
|
---|
1166 | // pixel height so the height can size with browser width/content changes, etc.
|
---|
1167 | $element.css({
|
---|
1168 | transition: 'none',
|
---|
1169 | height: ''
|
---|
1170 | });
|
---|
1171 |
|
---|
1172 | // In the next tick, re-allow transitions (if we do it all at once, $element.css is "smart"
|
---|
1173 | // enough to batch it for us instead of doing it immediately, which undoes the original
|
---|
1174 | // transition: none)
|
---|
1175 | $mdUtil.nextTick(function() {
|
---|
1176 | $element.css('transition', '');
|
---|
1177 | });
|
---|
1178 |
|
---|
1179 | // And unlock so tab changes can occur
|
---|
1180 | locked = false;
|
---|
1181 | });
|
---|
1182 | }
|
---|
1183 |
|
---|
1184 | /**
|
---|
1185 | * Repositions the ink bar to the selected tab.
|
---|
1186 | * Parameters are used when calling itself recursively when md-center-tabs is used as we need to
|
---|
1187 | * run two passes to properly center the tabs. These parameters ensure that we only run two passes
|
---|
1188 | * and that we don't run indefinitely.
|
---|
1189 | * @param {number=} previousTotalWidth previous width of pagination wrapper
|
---|
1190 | * @param {number=} previousWidthOfTabItems previous width of all tab items
|
---|
1191 | */
|
---|
1192 | function updateInkBarStyles (previousTotalWidth, previousWidthOfTabItems) {
|
---|
1193 | if (ctrl.noInkBar) {
|
---|
1194 | return;
|
---|
1195 | }
|
---|
1196 | var elements = getElements();
|
---|
1197 |
|
---|
1198 | if (!elements.tabs[ ctrl.selectedIndex ]) {
|
---|
1199 | angular.element(elements.inkBar).css({ left: 'auto', right: 'auto' });
|
---|
1200 | return;
|
---|
1201 | }
|
---|
1202 |
|
---|
1203 | if (!ctrl.tabs.length) {
|
---|
1204 | queue.push(ctrl.updateInkBarStyles);
|
---|
1205 | return;
|
---|
1206 | }
|
---|
1207 | // If the element is not visible, we will not be able to calculate sizes until it becomes
|
---|
1208 | // visible. We should treat that as a resize event rather than just updating the ink bar.
|
---|
1209 | if (!$element.prop('offsetParent')) {
|
---|
1210 | handleResizeWhenVisible();
|
---|
1211 | return;
|
---|
1212 | }
|
---|
1213 |
|
---|
1214 | var index = ctrl.selectedIndex,
|
---|
1215 | totalWidth = elements.paging.offsetWidth,
|
---|
1216 | tab = elements.tabs[ index ],
|
---|
1217 | left = tab.offsetLeft,
|
---|
1218 | right = totalWidth - left - tab.offsetWidth;
|
---|
1219 |
|
---|
1220 | if (ctrl.shouldCenterTabs) {
|
---|
1221 | // We need to use the same calculate process as in the pagination wrapper, to avoid rounding
|
---|
1222 | // deviations.
|
---|
1223 | var totalWidthOfTabItems = calcTabsWidth(elements.tabs);
|
---|
1224 |
|
---|
1225 | if (totalWidth > totalWidthOfTabItems &&
|
---|
1226 | previousTotalWidth !== totalWidth &&
|
---|
1227 | previousWidthOfTabItems !== totalWidthOfTabItems) {
|
---|
1228 | $timeout(updateInkBarStyles, 0, true, totalWidth, totalWidthOfTabItems);
|
---|
1229 | }
|
---|
1230 | }
|
---|
1231 | updateInkBarClassName();
|
---|
1232 | angular.element(elements.inkBar).css({ left: left + 'px', right: right + 'px' });
|
---|
1233 | }
|
---|
1234 |
|
---|
1235 | /**
|
---|
1236 | * Adds left/right classes so that the ink bar will animate properly.
|
---|
1237 | */
|
---|
1238 | function updateInkBarClassName () {
|
---|
1239 | var elements = getElements();
|
---|
1240 | var newIndex = ctrl.selectedIndex,
|
---|
1241 | oldIndex = ctrl.lastSelectedIndex,
|
---|
1242 | ink = angular.element(elements.inkBar);
|
---|
1243 | if (!angular.isNumber(oldIndex)) return;
|
---|
1244 | ink
|
---|
1245 | .toggleClass('md-left', newIndex < oldIndex)
|
---|
1246 | .toggleClass('md-right', newIndex > oldIndex);
|
---|
1247 | }
|
---|
1248 |
|
---|
1249 | /**
|
---|
1250 | * Takes an offset value and makes sure that it is within the min/max allowed values.
|
---|
1251 | * @param {number} value
|
---|
1252 | * @returns {number}
|
---|
1253 | */
|
---|
1254 | function fixOffset (value) {
|
---|
1255 | var elements = getElements();
|
---|
1256 |
|
---|
1257 | if (!elements.tabs.length || !ctrl.shouldPaginate) return 0;
|
---|
1258 |
|
---|
1259 | var lastTab = elements.tabs[ elements.tabs.length - 1 ],
|
---|
1260 | totalWidth = lastTab.offsetLeft + lastTab.offsetWidth;
|
---|
1261 |
|
---|
1262 | if (isRtl()) {
|
---|
1263 | value = Math.min(elements.paging.offsetWidth - elements.canvas.clientWidth, value);
|
---|
1264 | value = Math.max(0, value);
|
---|
1265 | } else {
|
---|
1266 | value = Math.max(0, value);
|
---|
1267 | value = Math.min(totalWidth - elements.canvas.clientWidth, value);
|
---|
1268 | }
|
---|
1269 |
|
---|
1270 | return value;
|
---|
1271 | }
|
---|
1272 |
|
---|
1273 | /**
|
---|
1274 | * Attaches a ripple to the tab item element.
|
---|
1275 | * @param scope
|
---|
1276 | * @param element
|
---|
1277 | */
|
---|
1278 | function attachRipple (scope, element) {
|
---|
1279 | var elements = getElements();
|
---|
1280 | var options = { colorElement: angular.element(elements.inkBar) };
|
---|
1281 | $mdTabInkRipple.attach(scope, element, options);
|
---|
1282 | }
|
---|
1283 |
|
---|
1284 | /**
|
---|
1285 | * Sets the `aria-controls` attribute to the elements that correspond to the passed-in tab.
|
---|
1286 | * @param tab
|
---|
1287 | */
|
---|
1288 | function setAriaControls (tab) {
|
---|
1289 | if (tab.hasContent) {
|
---|
1290 | var nodes = $element[0].querySelectorAll('[md-tab-id="' + tab.id + '"]');
|
---|
1291 | angular.element(nodes).attr('aria-controls', ctrl.tabContentPrefix + tab.id);
|
---|
1292 | }
|
---|
1293 | }
|
---|
1294 |
|
---|
1295 | function isRtl() {
|
---|
1296 | return $mdUtil.isRtl($attrs);
|
---|
1297 | }
|
---|
1298 | }
|
---|
1299 |
|
---|
1300 | /**
|
---|
1301 | * @ngdoc directive
|
---|
1302 | * @name mdTabs
|
---|
1303 | * @module material.components.tabs
|
---|
1304 | *
|
---|
1305 | * @restrict E
|
---|
1306 | *
|
---|
1307 | * @description
|
---|
1308 | * The `<md-tabs>` directive serves as the container for 1..n
|
---|
1309 | * <a ng-href="api/directive/mdTab">`<md-tab>`</a> child directives.
|
---|
1310 | * In turn, the nested `<md-tab>` directive is used to specify a tab label for the
|
---|
1311 | * **header button** and <i>optional</i> tab view content that will be associated with each tab
|
---|
1312 | * button.
|
---|
1313 | *
|
---|
1314 | * Below is the markup for its simplest usage:
|
---|
1315 | *
|
---|
1316 | * <hljs lang="html">
|
---|
1317 | * <md-tabs>
|
---|
1318 | * <md-tab label="Tab #1"></md-tab>
|
---|
1319 | * <md-tab label="Tab #2"></md-tab>
|
---|
1320 | * <md-tab label="Tab #3"></md-tab>
|
---|
1321 | * </md-tabs>
|
---|
1322 | * </hljs>
|
---|
1323 | *
|
---|
1324 | * Tabs support three (3) usage scenarios:
|
---|
1325 | *
|
---|
1326 | * 1. Tabs (buttons only)
|
---|
1327 | * 2. Tabs with internal view content
|
---|
1328 | * 3. Tabs with external view content
|
---|
1329 | *
|
---|
1330 | * **Tabs-only** support is useful when tab buttons are used for custom navigation regardless of any
|
---|
1331 | * other components, content, or views.
|
---|
1332 | *
|
---|
1333 | * <blockquote><b>Note:</b> If you are using the Tabs component for page-level navigation, please
|
---|
1334 | * use the <a ng-href="./api/directive/mdNavBar">NavBar component</a> instead. It handles this
|
---|
1335 | * case a more natively and more performantly.</blockquote>
|
---|
1336 | *
|
---|
1337 | * **Tabs with internal views** are the traditional usage where each tab has associated view
|
---|
1338 | * content and the view switching is managed internally by the Tabs component.
|
---|
1339 | *
|
---|
1340 | * **Tabs with external view content** is often useful when content associated with each tab is
|
---|
1341 | * independently managed and data-binding notifications announce tab selection changes.
|
---|
1342 | *
|
---|
1343 | * Additional features include:
|
---|
1344 | *
|
---|
1345 | * * Content can include any markup.
|
---|
1346 | * * If a tab is disabled while active/selected, then the next tab will be auto-selected.
|
---|
1347 | *
|
---|
1348 | * ### Theming
|
---|
1349 | *
|
---|
1350 | * By default, tabs use your app's accent color for the selected tab's text and ink bar.
|
---|
1351 | *
|
---|
1352 | * You can use the theming classes to change the color of the `md-tabs` background:
|
---|
1353 | * * Applying `class="md-primary"` will use your app's primary color for the background, your
|
---|
1354 | * accent color for the ink bar, and your primary palette's contrast color for the text of the
|
---|
1355 | * selected tab.
|
---|
1356 | * * When using the `md-primary` class, you can add the `md-no-ink-bar-color` class to make the
|
---|
1357 | * ink bar use your theme's primary contrast color instead of the accent color.
|
---|
1358 | * * Applying `class="md-accent"` will use your app's accent color for the background and your
|
---|
1359 | * accent palette's contrast color for the text and ink bar of the selected tab.
|
---|
1360 | * * Applying `class="md-warn"` will use your app's warn color for the background and your
|
---|
1361 | * warn palette's contrast color for the text and ink bar of the selected tab.
|
---|
1362 | *
|
---|
1363 | * ### Explanation of tab stretching
|
---|
1364 | *
|
---|
1365 | * Initially, tabs will have an inherent size. This size will either be defined by how much space
|
---|
1366 | * is needed to accommodate their text or set by the user through CSS.
|
---|
1367 | * Calculations will be based on this size.
|
---|
1368 | *
|
---|
1369 | * On mobile devices, tabs will be expanded to fill the available horizontal space.
|
---|
1370 | * When this happens, all tabs will become the same size.
|
---|
1371 | *
|
---|
1372 | * On desktops, by default, stretching will never occur.
|
---|
1373 | *
|
---|
1374 | * This default behavior can be overridden through the `md-stretch-tabs` attribute.
|
---|
1375 | * Here is a table showing when stretching will occur:
|
---|
1376 | *
|
---|
1377 | * `md-stretch-tabs` | mobile | desktop
|
---|
1378 | * ------------------|-----------|--------
|
---|
1379 | * `auto` | stretched | ---
|
---|
1380 | * `always` | stretched | stretched
|
---|
1381 | * `never` | --- | ---
|
---|
1382 | *
|
---|
1383 | * @param {number=} md-selected Index of the active/selected tab.
|
---|
1384 | * @param {expression=} md-no-ink-bar If `true` or no value, disables the selection ink bar.
|
---|
1385 | * @param {string=} md-align-tabs Attribute to indicate position of tab buttons: `bottom` or `top`;
|
---|
1386 | * Default is `top`.
|
---|
1387 | * @param {string=} md-stretch-tabs Attribute to indicate whether or not to stretch tabs: `auto`,
|
---|
1388 | * `always`, or `never`; Default is `auto`.
|
---|
1389 | * @param {expression=} md-dynamic-height If `true` or no value, the tab wrapper will resize based
|
---|
1390 | * on the contents of the selected tab.
|
---|
1391 | * @param {boolean=} md-border-bottom If the attribute is present, shows a solid `1px` border
|
---|
1392 | * between the tabs and their content.
|
---|
1393 | * @param {boolean=} md-center-tabs If the attribute is present, tabs will be centered provided
|
---|
1394 | * there is no need for pagination.
|
---|
1395 | * @param {boolean=} md-no-pagination If the attribute is present, pagination will remain off.
|
---|
1396 | * @param {expression=} md-swipe-content When enabled, swipe gestures will be enabled for the content
|
---|
1397 | * area to allow swiping between tabs.
|
---|
1398 | * @param {boolean=} md-enable-disconnect When enabled, scopes will be disconnected for tabs that
|
---|
1399 | * are not being displayed. This provides a performance boost, but may also cause unexpected
|
---|
1400 | * issues. It is not recommended for most users.
|
---|
1401 | * @param {boolean=} md-autoselect If the attribute is present, any tabs added after the initial
|
---|
1402 | * load will be automatically selected.
|
---|
1403 | * @param {boolean=} md-no-select-click When true, click events will not be fired when the value of
|
---|
1404 | * `md-active` on an `md-tab` changes. This is useful when using tabs with UI-Router's child
|
---|
1405 | * states, as triggering a click event in that case can cause an extra tab change to occur.
|
---|
1406 | * @param {string=} md-navigation-hint Attribute to override the default `tablist` navigation hint
|
---|
1407 | * that screen readers will announce to provide instructions for navigating between tabs. This is
|
---|
1408 | * desirable when you want the hint to be in a different language. Default is "Use the left and
|
---|
1409 | * right arrow keys to navigate between tabs".
|
---|
1410 | *
|
---|
1411 | * @usage
|
---|
1412 | * <hljs lang="html">
|
---|
1413 | * <md-tabs md-selected="selectedIndex">
|
---|
1414 | * <img ng-src="img/angular.png" class="centered" alt="Angular icon">
|
---|
1415 | * <md-tab
|
---|
1416 | * ng-repeat="tab in tabs | orderBy:predicate:reversed"
|
---|
1417 | * md-on-select="onTabSelected(tab)"
|
---|
1418 | * md-on-deselect="announceDeselected(tab)"
|
---|
1419 | * ng-disabled="tab.disabled">
|
---|
1420 | * <md-tab-label>
|
---|
1421 | * {{tab.title}}
|
---|
1422 | * <img src="img/removeTab.png" ng-click="removeTab(tab)" class="delete" alt="Remove tab">
|
---|
1423 | * </md-tab-label>
|
---|
1424 | * <md-tab-body>
|
---|
1425 | * {{tab.content}}
|
---|
1426 | * </md-tab-body>
|
---|
1427 | * </md-tab>
|
---|
1428 | * </md-tabs>
|
---|
1429 | * </hljs>
|
---|
1430 | *
|
---|
1431 | */
|
---|
1432 | MdTabs['$inject'] = ["$$mdSvgRegistry"];
|
---|
1433 | angular
|
---|
1434 | .module('material.components.tabs')
|
---|
1435 | .directive('mdTabs', MdTabs);
|
---|
1436 |
|
---|
1437 | function MdTabs ($$mdSvgRegistry) {
|
---|
1438 | return {
|
---|
1439 | scope: {
|
---|
1440 | navigationHint: '@?mdNavigationHint',
|
---|
1441 | selectedIndex: '=?mdSelected'
|
---|
1442 | },
|
---|
1443 | template: function (element, attr) {
|
---|
1444 | attr.$mdTabsTemplate = element.html();
|
---|
1445 | return '' +
|
---|
1446 | '<md-tabs-wrapper> ' +
|
---|
1447 | '<md-tab-data></md-tab-data> ' +
|
---|
1448 | '<md-prev-button ' +
|
---|
1449 | 'tabindex="-1" ' +
|
---|
1450 | 'role="button" ' +
|
---|
1451 | 'aria-label="Previous Page" ' +
|
---|
1452 | 'aria-disabled="{{!$mdTabsCtrl.canPageBack()}}" ' +
|
---|
1453 | 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageBack() }" ' +
|
---|
1454 | 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
|
---|
1455 | 'ng-click="$mdTabsCtrl.previousPage()"> ' +
|
---|
1456 | '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
|
---|
1457 | '</md-prev-button> ' +
|
---|
1458 | '<md-next-button ' +
|
---|
1459 | 'tabindex="-1" ' +
|
---|
1460 | 'role="button" ' +
|
---|
1461 | 'aria-label="Next Page" ' +
|
---|
1462 | 'aria-disabled="{{!$mdTabsCtrl.canPageForward()}}" ' +
|
---|
1463 | 'ng-class="{ \'md-disabled\': !$mdTabsCtrl.canPageForward() }" ' +
|
---|
1464 | 'ng-if="$mdTabsCtrl.shouldPaginate" ' +
|
---|
1465 | 'ng-click="$mdTabsCtrl.nextPage()"> ' +
|
---|
1466 | '<md-icon md-svg-src="'+ $$mdSvgRegistry.mdTabsArrow +'"></md-icon> ' +
|
---|
1467 | '</md-next-button> ' +
|
---|
1468 | '<md-tabs-canvas ' +
|
---|
1469 | 'tabindex="{{ $mdTabsCtrl.hasFocus ? -1 : 0 }}" ' +
|
---|
1470 | 'ng-focus="$mdTabsCtrl.redirectFocus()" ' +
|
---|
1471 | 'ng-class="{ ' +
|
---|
1472 | '\'md-paginated\': $mdTabsCtrl.shouldPaginate, ' +
|
---|
1473 | '\'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs ' +
|
---|
1474 | '}" ' +
|
---|
1475 | 'ng-keydown="$mdTabsCtrl.keydown($event)"> ' +
|
---|
1476 | '<md-pagination-wrapper ' +
|
---|
1477 | 'ng-class="{ \'md-center-tabs\': $mdTabsCtrl.shouldCenterTabs }" ' +
|
---|
1478 | 'md-tab-scroll="$mdTabsCtrl.scroll($event)" ' +
|
---|
1479 | 'role="tablist" ' +
|
---|
1480 | 'aria-label="{{::$mdTabsCtrl.navigationHint}}">' +
|
---|
1481 | '<md-tab-item ' +
|
---|
1482 | 'tabindex="{{ tab.isActive() ? 0 : -1 }}" ' +
|
---|
1483 | 'class="md-tab {{::tab.scope.tabClass}}" ' +
|
---|
1484 | 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
|
---|
1485 | 'role="tab" ' +
|
---|
1486 | 'id="tab-item-{{::tab.id}}" ' +
|
---|
1487 | 'md-tab-id="{{::tab.id}}" ' +
|
---|
1488 | 'aria-selected="{{tab.isActive()}}" ' +
|
---|
1489 | 'aria-disabled="{{tab.scope.disabled || \'false\'}}" ' +
|
---|
1490 | 'ng-click="$mdTabsCtrl.select(tab.getIndex())" ' +
|
---|
1491 | 'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
|
---|
1492 | 'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
|
---|
1493 | 'ng-class="{ ' +
|
---|
1494 | '\'md-active\': tab.isActive(), ' +
|
---|
1495 | '\'md-focused\': tab.hasFocus(), ' +
|
---|
1496 | '\'md-disabled\': tab.scope.disabled ' +
|
---|
1497 | '}" ' +
|
---|
1498 | 'ng-disabled="tab.scope.disabled" ' +
|
---|
1499 | 'md-swipe-left="$mdTabsCtrl.nextPage()" ' +
|
---|
1500 | 'md-swipe-right="$mdTabsCtrl.previousPage()" ' +
|
---|
1501 | 'md-tabs-template="::tab.label" ' +
|
---|
1502 | 'md-scope="::tab.parent"></md-tab-item> ' +
|
---|
1503 | '<md-ink-bar></md-ink-bar> ' +
|
---|
1504 | '</md-pagination-wrapper> ' +
|
---|
1505 | '<md-tabs-dummy-wrapper aria-hidden="true" class="md-visually-hidden md-dummy-wrapper"> ' +
|
---|
1506 | '<md-dummy-tab ' +
|
---|
1507 | 'class="md-tab" ' +
|
---|
1508 | 'tabindex="-1" ' +
|
---|
1509 | 'ng-focus="$mdTabsCtrl.hasFocus = true" ' +
|
---|
1510 | 'ng-blur="$mdTabsCtrl.hasFocus = false" ' +
|
---|
1511 | 'ng-repeat="tab in $mdTabsCtrl.tabs" ' +
|
---|
1512 | 'md-tabs-template="::tab.label" ' +
|
---|
1513 | 'md-scope="::tab.parent"></md-dummy-tab> ' +
|
---|
1514 | '</md-tabs-dummy-wrapper> ' +
|
---|
1515 | '</md-tabs-canvas> ' +
|
---|
1516 | '</md-tabs-wrapper> ' +
|
---|
1517 | '<md-tabs-content-wrapper ng-show="$mdTabsCtrl.hasContent && $mdTabsCtrl.selectedIndex >= 0" class="_md"> ' +
|
---|
1518 | '<md-tab-content ' +
|
---|
1519 | 'id="{{:: $mdTabsCtrl.tabContentPrefix + tab.id}}" ' +
|
---|
1520 | 'class="_md" ' +
|
---|
1521 | 'role="tabpanel" ' +
|
---|
1522 | 'aria-labelledby="tab-item-{{::tab.id}}" ' +
|
---|
1523 | 'md-swipe-left="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(1)" ' +
|
---|
1524 | 'md-swipe-right="$mdTabsCtrl.swipeContent && $mdTabsCtrl.incrementIndex(-1)" ' +
|
---|
1525 | 'ng-if="tab.hasContent" ' +
|
---|
1526 | 'ng-repeat="(index, tab) in $mdTabsCtrl.tabs" ' +
|
---|
1527 | 'ng-class="{ ' +
|
---|
1528 | '\'md-no-transition\': $mdTabsCtrl.lastSelectedIndex == null, ' +
|
---|
1529 | '\'md-active\': tab.isActive(), ' +
|
---|
1530 | '\'md-left\': tab.isLeft(), ' +
|
---|
1531 | '\'md-right\': tab.isRight(), ' +
|
---|
1532 | '\'md-no-scroll\': $mdTabsCtrl.dynamicHeight ' +
|
---|
1533 | '}"> ' +
|
---|
1534 | '<div ' +
|
---|
1535 | 'md-tabs-template="::tab.template" ' +
|
---|
1536 | 'md-connected-if="tab.isActive()" ' +
|
---|
1537 | 'md-scope="::tab.parent" ' +
|
---|
1538 | 'ng-if="$mdTabsCtrl.enableDisconnect || tab.shouldRender()"></div> ' +
|
---|
1539 | '</md-tab-content> ' +
|
---|
1540 | '</md-tabs-content-wrapper>';
|
---|
1541 | },
|
---|
1542 | controller: 'MdTabsController',
|
---|
1543 | controllerAs: '$mdTabsCtrl',
|
---|
1544 | bindToController: true
|
---|
1545 | };
|
---|
1546 | }
|
---|
1547 |
|
---|
1548 |
|
---|
1549 | MdTabsDummyWrapper['$inject'] = ["$mdUtil", "$window"];angular
|
---|
1550 | .module('material.components.tabs')
|
---|
1551 | .directive('mdTabsDummyWrapper', MdTabsDummyWrapper);
|
---|
1552 |
|
---|
1553 | /**
|
---|
1554 | * @private
|
---|
1555 | *
|
---|
1556 | * @param $mdUtil
|
---|
1557 | * @param $window
|
---|
1558 | * @returns {{require: string, link: link}}
|
---|
1559 | * @constructor
|
---|
1560 | *
|
---|
1561 | * ngInject
|
---|
1562 | */
|
---|
1563 | function MdTabsDummyWrapper ($mdUtil, $window) {
|
---|
1564 | return {
|
---|
1565 | require: '^?mdTabs',
|
---|
1566 | link: function link (scope, element, attr, ctrl) {
|
---|
1567 | if (!ctrl) return;
|
---|
1568 |
|
---|
1569 | var observer;
|
---|
1570 | var disconnect;
|
---|
1571 |
|
---|
1572 | var mutationCallback = function() {
|
---|
1573 | ctrl.updatePagination();
|
---|
1574 | ctrl.updateInkBarStyles();
|
---|
1575 | };
|
---|
1576 |
|
---|
1577 | if ('MutationObserver' in $window) {
|
---|
1578 | var config = {
|
---|
1579 | childList: true,
|
---|
1580 | subtree: true,
|
---|
1581 | // Per https://bugzilla.mozilla.org/show_bug.cgi?id=1138368, browsers will not fire
|
---|
1582 | // the childList mutation, once a <span> element's innerText changes.
|
---|
1583 | // The characterData of the <span> element will change.
|
---|
1584 | characterData: true
|
---|
1585 | };
|
---|
1586 |
|
---|
1587 | observer = new MutationObserver(mutationCallback);
|
---|
1588 | observer.observe(element[0], config);
|
---|
1589 | disconnect = observer.disconnect.bind(observer);
|
---|
1590 | } else {
|
---|
1591 | var debounced = $mdUtil.debounce(mutationCallback, 15, null, false);
|
---|
1592 |
|
---|
1593 | element.on('DOMSubtreeModified', debounced);
|
---|
1594 | disconnect = element.off.bind(element, 'DOMSubtreeModified', debounced);
|
---|
1595 | }
|
---|
1596 |
|
---|
1597 | // Disconnect the observer
|
---|
1598 | scope.$on('$destroy', function() {
|
---|
1599 | disconnect();
|
---|
1600 | });
|
---|
1601 | }
|
---|
1602 | };
|
---|
1603 | }
|
---|
1604 |
|
---|
1605 |
|
---|
1606 | MdTabsTemplate['$inject'] = ["$compile", "$mdUtil"];angular
|
---|
1607 | .module('material.components.tabs')
|
---|
1608 | .directive('mdTabsTemplate', MdTabsTemplate);
|
---|
1609 |
|
---|
1610 | function MdTabsTemplate ($compile, $mdUtil) {
|
---|
1611 | return {
|
---|
1612 | restrict: 'A',
|
---|
1613 | link: link,
|
---|
1614 | scope: {
|
---|
1615 | template: '=mdTabsTemplate',
|
---|
1616 | connected: '=?mdConnectedIf',
|
---|
1617 | compileScope: '=mdScope'
|
---|
1618 | },
|
---|
1619 | require: '^?mdTabs'
|
---|
1620 | };
|
---|
1621 | function link (scope, element, attr, ctrl) {
|
---|
1622 | if (!ctrl) return;
|
---|
1623 |
|
---|
1624 | var compileScope = ctrl.enableDisconnect ? scope.compileScope.$new() : scope.compileScope;
|
---|
1625 |
|
---|
1626 | element.html(scope.template);
|
---|
1627 | $compile(element.contents())(compileScope);
|
---|
1628 |
|
---|
1629 | return $mdUtil.nextTick(handleScope);
|
---|
1630 |
|
---|
1631 | function handleScope () {
|
---|
1632 | scope.$watch('connected', function (value) { value === false ? disconnect() : reconnect(); });
|
---|
1633 | scope.$on('$destroy', reconnect);
|
---|
1634 | }
|
---|
1635 |
|
---|
1636 | function disconnect () {
|
---|
1637 | if (ctrl.enableDisconnect) $mdUtil.disconnectScope(compileScope);
|
---|
1638 | }
|
---|
1639 |
|
---|
1640 | function reconnect () {
|
---|
1641 | if (ctrl.enableDisconnect) $mdUtil.reconnectScope(compileScope);
|
---|
1642 | }
|
---|
1643 | }
|
---|
1644 | }
|
---|
1645 |
|
---|
1646 | })(window, window.angular); |
---|