PageSourceSearch

https://www.californiacolleges.edu/lib/js/vendor/angularjs-dropdown-multiselect.js

js californiacolleges.edu collected 2026-10-01 11:43:49 UTC 22,840 bytes, 623 lines download raw bytes

1(function(angular) {
2	'use strict';
3
4	var directiveModule = angular.module('angularjs-dropdown-multiselect', []);
5
6	directiveModule.directive('dmDropdownStaticInclude', ['$compile', function($compile) {
7		return function(scope, element, attrs) {
8			var template = attrs.dmDropdownStaticInclude;
9			var contents = element.html(template).contents();
10			$compile(contents)(scope);
11		};
12	}]);
13
14	directiveModule.directive('ngDropdownMultiselect', ['$filter', '$document', '$compile', '$parse', function($filter, $document, $compile, $parse) {
15		return {
16			restrict: 'AE',
17			scope: {
18				selectedModel: '=',
19				options: '=',
20				extraSettings: '=',
21				events: '=',
22				searchFilter: '=?',
23				translationTexts: '=',
24				groupBy: '@',
25				disabled: "=",
26				buttonDefaultText : '@'
27			},
28			template: function(element, attrs) {
29				var checkboxes = attrs.checkboxes ? true : false;
30				var groups = attrs.groupBy ? true : false;
31
32				var template = '<div class="multiselect-parent btn-group dropdown-multiselect" ng-class="{open: open}">';
33				template += '<button ng-disabled="disabled" type="button" class="dropdown-toggle" ng-class="settings.buttonClasses" ng-click="toggleDropdown()">{{getButtonText()}}&nbsp;<span class="caret"></span></button>';
34				template += '<ul class="dropdown-menu dropdown-menu-form" ng-if="open" ng-style="{display: open ? \'block\' : \'none\', height : settings.scrollable ? settings.scrollableHeight : \'auto\', overflow: \'auto\' }" >';
35				template += '<li ng-if="settings.showCheckAll && settings.selectionLimit !== 1"><a ng-keydown="keyDownLink($event)" data-ng-click="selectAll()" tabindex="-1" id="selectAll"><span class="glyphicon glyphicon-ok"></span>  {{texts.checkAll}}</a>';
36				template += '<li ng-if="settings.showUncheckAll"><a ng-keydown="keyDownLink($event)" data-ng-click="deselectAll();" tabindex="-1" id="deselectAll"><span class="glyphicon glyphicon-remove"></span>   {{texts.uncheckAll}}</a></li>';
37				template += '<li ng-if="settings.selectByGroups && ((settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll)" class="divider"></li>';
38				template += '<li ng-if="settings.selectByGroups && ((settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll)" class="divider"></li>';
39				template += '<li ng-repeat="currentGroup in settings.selectByGroups track by $index" ng-click="selectCurrentGroup(currentGroup)"><a ng-class="{\'dropdown-selected-group\': selectedGroup === currentGroup}" tabindex="-1">{{::texts.selectGroup}} {{::getGroupLabel(currentGroup)}}</a></li>';
40				template += '<li ng-if="settings.selectByGroups && settings.showEnableSearchButton" class="divider"></li>';
41				template += '<li ng-if="settings.showEnableSearchButton && settings.enableSearch"><a ng-keydown="keyDownLink($event); keyDownToggleSearch();" ng-click="toggleSearch($event);" tabindex="-1">{{texts.disableSearch}}</a></li>';
42				template += '<li ng-if="settings.showEnableSearchButton && !settings.enableSearch"><a ng-keydown="keyDownLink($event); keyDownToggleSearch();" ng-click="toggleSearch($event);" tabindex="-1">{{texts.enableSearch}}</a></li>';
43				template += '<li ng-if="(settings.showCheckAll && settings.selectionLimit > 0) || settings.showUncheckAll || settings.showEnableSearchButton" class="divider"></li>';
44				template += '<li ng-if="settings.enableSearch"><div class="dropdown-header"><input type="text" class="form-control searchField" ng-keydown="keyDownSearchDefault($event); keyDownSearch($event, input.searchFilter);" ng-style="{width: \'100%\'}" ng-model="input.searchFilter" placeholder="{{texts.searchPlaceholder}}" /></li>';
45				template += '<li ng-if="settings.enableSearch" class="divider"></li>';
46
47				if (groups) {
48					template += '<li ng-repeat-start="option in orderedItems | filter:getFilter(input.searchFilter)" ng-show="getPropertyForObject(option, settings.groupBy) !== getPropertyForObject(orderedItems[$index - 1], settings.groupBy)" role="presentation" class="dropdown-header">{{ getGroupLabel(getPropertyForObject(option, settings.groupBy)) }}</li>';
49					template += '<li ng-class="{\'active\': isChecked(getPropertyForObject(option,settings.idProp)) && settings.styleActive}" ng-repeat-end role="presentation">';
50				} else {
51					template += '<li ng-class="{\'active\': isChecked(getPropertyForObject(option,settings.idProp)) && settings.styleActive}" role="presentation" ng-repeat="option in options | filter:getFilter(input.searchFilter)">';
52				}
53
54				template += '<a ng-keydown="option.disabled || keyDownLink($event)" role="menuitem" class="option" tabindex="-1" ng-click="option.disabled || setSelectedItem(getPropertyForObject(option,settings.idProp), false, true)" ng-disabled="option.disabled">';
55
56				if (checkboxes) {
57					template += '<div class="checkbox"><label><input class="checkboxInput" type="checkbox" ng-click="checkboxClick($event, getPropertyForObject(option,settings.idProp))" ng-checked="isChecked(getPropertyForObject(option,settings.idProp))" /> <span dm-dropdown-static-include="{{settings.template}}"></div></label></span></a>';
58				} else {
59					template += '<span data-ng-class="{\'glyphicon glyphicon-ok\': isChecked(getPropertyForObject(option,settings.idProp))}"> </span> <span dm-dropdown-static-include="{{settings.template}}"></span></a>';
60				}
61
62				template += '</li>';
63
64				template += '<li class="divider" ng-show="settings.selectionLimit > 1"></li>';
65				template += '<li role="presentation" ng-show="settings.selectionLimit > 1"><a role="menuitem">{{selectedModel.length}} {{texts.selectionOf}} {{settings.selectionLimit}} {{texts.selectionCount}}</a></li>';
66
67				template += '</ul>';
68				template += '</div>';
69
70				return template;
71			},
72			link: function($scope, $element, $attrs) {
73				var $dropdownTrigger = $element.children()[0];
74
75				$scope.toggleDropdown = function() {
76					if ($scope.open) {
77						$scope.close()
78					} else { $scope.open = true }
79					if ($scope.settings.keyboardControls) {
80						if ($scope.open) {
81							if ($scope.settings.selectionLimit === 1 && $scope.settings.enableSearch) {
82								setTimeout(function() {
83									angular.element($element)[0].querySelector('.searchField').focus();
84								}, 0);
85							} else {
86								focusFirstOption();
87							}
88						}
89					}
90					if ($scope.settings.enableSearch) {
91						if ($scope.open) {
92							setTimeout(function () {
93								angular.element($element)[0].querySelector('.searchField').focus();
94							}, 0);
95						}
96					}
97				};
98
99				$scope.checkboxClick = function($event, id) {
100					$scope.setSelectedItem(id, false, true);
101					$event.stopImmediatePropagation();
102				};
103
104				$scope.externalEvents = {
105					onItemSelect: angular.noop,
106					onItemDeselect: angular.noop,
107					onSelectAll: angular.noop,
108					onDeselectAll: angular.noop,
109					onInitDone: angular.noop,
110					onMaxSelectionReached: angular.noop,
111					onSelectionChanged: angular.noop,
112					onClose: angular.noop
113				};
114
115				$scope.settings = {
116					dynamicTitle: true,
117					scrollable: false,
118					scrollableHeight: '300px',
119					closeOnBlur: true,
120					displayProp: 'label',
121					idProp: 'id',
122					externalIdProp: 'id',
123					enableSearch: false,
124					selectionLimit: 0,
125					showCheckAll: true,
126					showUncheckAll: true,
127					showEnableSearchButton: false,
128					closeOnSelect: false,
129					buttonClasses: 'btn btn-default',
130					closeOnDeselect: false,
131					groupBy: $attrs.groupBy || undefined,
132					groupByTextProvider: null,
133					smartButtonMaxItems: 0,
134					smartButtonTextConverter: angular.noop,
135					styleActive: false,
136					keyboardControls: false,
137					template: '{{getPropertyForObject(option, settings.displayProp)}}',
138					searchField: '$',
139					showAllSelectedText: false
140				};
141
142				$scope.texts = {
143					checkAll: 'Check All',
144					uncheckAll: 'Uncheck All',
145					selectionCount: 'checked',
146					selectionOf: '/',
147					searchPlaceholder: 'Search...',
148					buttonDefaultText: $scope.buttonDefaultText ? $scope.buttonDefaultText : 'Select',
149					dynamicButtonTextSuffix: 'checked',
150					disableSearch: 'Disable search',
151					enableSearch: 'Enable search',
152					selectGroup: 'Select all:',
153					allSelectedText: 'All'
154				};
155
156				$scope.input = {
157					searchFilter: $scope.searchFilter || ''
158				};
159
160				if (angular.isDefined($scope.settings.groupBy)) {
161					$scope.$watch('options', function(newValue) {
162						if (angular.isDefined(newValue)) {
163							$scope.orderedItems = $filter('orderBy')(newValue, $scope.settings.groupBy);
164						}
165					});
166				}
167
168				$scope.$watch('selectedModel', function(newValue) {
169					if (!Array.isArray(newValue)) {
170						$scope.singleSelection = true;
171					} else {
172						$scope.singleSelection = false;
173					}
174				});
175
176				$scope.close = function() {
177					$scope.open = false;
178					$scope.externalEvents.onClose();
179				}
180
181				$scope.selectCurrentGroup = function(currentGroup) {
182					$scope.selectedModel.splice(0, $scope.selectedModel.length);
183					if ($scope.orderedItems) {
184						$scope.orderedItems.forEach(function(item) {
185							if (item[$scope.groupBy] === currentGroup) {
186								$scope.setSelectedItem($scope.getPropertyForObject(item, $scope.settings.idProp), false, false)
187							}
188						});
189					}
190					$scope.externalEvents.onSelectionChanged();
191				};
192
193				angular.extend($scope.settings, $scope.extraSettings || []);
194				angular.extend($scope.externalEvents, $scope.events || []);
195				angular.extend($scope.texts, $scope.translationTexts);
196
197				$scope.singleSelection = $scope.settings.selectionLimit === 1;
198
199				function getFindObj(id) {
200					var findObj = {};
201
202					if ($scope.settings.externalIdProp === '') {
203						findObj[$scope.settings.idProp] = id;
204					} else {
205						findObj[$scope.settings.externalIdProp] = id;
206					}
207
208					return findObj;
209				}
210
211				function clearObject(object) {
212					for (var prop in object) {
213						delete object[prop];
214					}
215				}
216
217				if ($scope.singleSelection) {
218					if (angular.isArray($scope.selectedModel) && $scope.selectedModel.length === 0) {
219						clearObject($scope.selectedModel);
220					}
221				}
222
223				if ($scope.settings.closeOnBlur) {
224					$document.on('click', function(e) {
225						if ($scope.open) {
226							var target = e.target.parentElement;
227							var parentFound = false;
228
229							while (angular.isDefined(target) && target !== null && !parentFound) {
230								if (!!target.className.split && contains(target.className.split(' '), 'multiselect-parent') && !parentFound) {
231									if (target === $dropdownTrigger) {
232										parentFound = true;
233									}
234								}
235								target = target.parentElement;
236							}
237
238							if (!parentFound) {
239								$scope.$apply(function() {
240									$scope.close();
241								});
242							}
243						}
244					});
245				}
246
247				$scope.getGroupLabel = function(groupValue) {
248					if ($scope.settings.groupByTextProvider !== null) {
249						return $scope.settings.groupByTextProvider(groupValue);
250					}
251
252					return groupValue;
253				};
254
255				function textWidth(text) {
256					var $btn = $element.find('button');
257					var canvas = document.createElement('canvas');
258					var ctx = canvas.getContext('2d');
259					ctx.font = $btn.css('font-size') + $btn.css('font-family');
260					// http://stackoverflow.com/questions/38823353/chrome-canvas-2d-context-measuretext-giving-me-weird-results
261					ctx.originalFont = $btn.css('font-size') + $btn.css('font-family');
262					ctx.fillStyle = '#000000';
263					return ctx.measureText(text).width;
264				}
265
266				$scope.getButtonText = function() {
267					if ($scope.settings.dynamicTitle && $scope.selectedModel && ($scope.selectedModel.length > 0 || (angular.isObject($scope.selectedModel) && Object.keys($scope.selectedModel).length > 0))) {
268						if ($scope.settings.smartButtonMaxItems > 0) {
269
270							var paddingWidth = 12 * 2,
271									borderWidth = 1 * 2,
272									dropdownIconWidth = 8;
273							var widthLimit = $element[0].offsetWidth - paddingWidth - borderWidth - dropdownIconWidth;
274
275							var itemsText = [];
276
277							angular.forEach($scope.options, function(optionItem) {
278								if ($scope.isChecked($scope.getPropertyForObject(optionItem, $scope.settings.idProp))) {
279									var displayText = $scope.getPropertyForObject(optionItem, $scope.settings.displayProp);
280									var converterResponse = $scope.settings.smartButtonTextConverter(displayText, optionItem);
281
282									itemsText.push(converterResponse ? converterResponse : displayText);
283								}
284							});
285
286							if ($scope.selectedModel.length > $scope.settings.smartButtonMaxItems) {
287								itemsText = itemsText.slice(0, $scope.settings.smartButtonMaxItems);
288								itemsText.push('...');
289							}
290
291							var result = itemsText.join(', ');
292							var index = result.length - 4;
293							if ($element[0].offsetWidth === 0)
294								return result;
295							while (textWidth(result) > widthLimit && index > 0) {
296								if (itemsText[itemsText.length - 1] !== "...") {
297									itemsText.push('...');
298									result = result + "...";
299								}
300								result = result.slice(0, index) + result.slice(index + 1);
301								index--;
302							}
303
304							return result;
305						} else {
306							var totalSelected;
307
308							if ($scope.options.length === $scope.selectedModel.length){
309							itemsText = "All Selected";
310							return itemsText;
311							}
312
313							if ($scope.singleSelection) {
314								totalSelected = ($scope.selectedModel !== null && angular.isDefined($scope.selectedModel[$scope.settings.idProp])) ? 1 : 0;
315							} else {
316								totalSelected = angular.isDefined($scope.selectedModel) ? $scope.selectedModel.length : 0;
317							}
318
319							if (totalSelected === 0) {
320								return $scope.texts.buttonDefaultText;
321							}
322
323							if ($scope.settings.showAllSelectedText && totalSelected === $scope.options.length) {
324								return $scope.texts.allSelectedText;
325							}
326
327							return totalSelected + ' ' + $scope.texts.dynamicButtonTextSuffix;
328						}
329					} else {
330						return $scope.texts.buttonDefaultText;
331					}
332				};
333
334				$scope.getPropertyForObject = function(object, property) {
335					if (angular.isDefined(object) && object.hasOwnProperty(property)) {
336						return object[property];
337					}
338
339					return undefined;
340				};
341
342				$scope.selectAll = function() {
343					var searchResult;
344					$scope.deselectAll(true);
345					$scope.externalEvents.onSelectAll();
346
347					searchResult = $filter('filter')($scope.options, $scope.getFilter($scope.input.searchFilter));
348					angular.forEach(searchResult, function(value) {
349						$scope.setSelectedItem(value[$scope.settings.idProp], true, false);
350					});
351					$scope.externalEvents.onSelectionChanged();
352					$scope.selectedGroup = null;
353				};
354
355				$scope.deselectAll = function(dontSendEvent) {
356					dontSendEvent = dontSendEvent || false;
357
358					if (!dontSendEvent) {
359						$scope.externalEvents.onDeselectAll();
360					}
361
362					if ($scope.singleSelection) {
363						clearObject($scope.selectedModel);
364					} else {
365						$scope.selectedModel.splice(0, $scope.selectedModel.length);
366					}
367					if (!dontSendEvent) {
368						$scope.externalEvents.onSelectionChanged();
369					}
370					$scope.selectedGroup = null;
371				};
372
373				$scope.setSelectedItem = function(id, dontRemove, fireSelectionChange) {
374					var findObj = getFindObj(id);
375					var finalObj = null;
376
377					if ($scope.settings.externalIdProp === '') {
378						finalObj = find($scope.options, findObj);
379					} else {
380						finalObj = findObj;
381					}
382
383					if ($scope.singleSelection) {
384						clearObject($scope.selectedModel);
385						angular.extend($scope.selectedModel, finalObj);
386						if (fireSelectionChange) {
387							$scope.externalEvents.onItemSelect(finalObj);
388						}
389						if ($scope.settings.closeOnSelect || $scope.settings.closeOnDeselect) $scope.close();
390					} else {
391						dontRemove = dontRemove || false;
392
393						var exists = findIndex($scope.selectedModel, findObj) !== -1;
394
395						if (!dontRemove && exists) {
396							$scope.selectedModel.splice(findIndex($scope.selectedModel, findObj), 1);
397							$scope.externalEvents.onItemDeselect(findObj);
398							if ($scope.settings.closeOnDeselect) $scope.close();
399						} else if (!exists && ($scope.settings.selectionLimit === 0 || $scope.selectedModel.length < $scope.settings.selectionLimit)) {
400							$scope.selectedModel.push(finalObj);
401							if (fireSelectionChange) {
402								$scope.externalEvents.onItemSelect(finalObj);
403							}
404							if ($scope.settings.closeOnSelect) $scope.close();
405							if ($scope.settings.selectionLimit > 0 && $scope.selectedModel.length === $scope.settings.selectionLimit) {
406								$scope.externalEvents.onMaxSelectionReached();
407							}
408						}
409					}
410					if (fireSelectionChange) {
411						$scope.externalEvents.onSelectionChanged();
412					}
413					$scope.selectedGroup = null;
414				};
415
416				$scope.isChecked = function(id) {
417					if ($scope.singleSelection) {
418						if ($scope.settings.externalIdProp === '') {
419							return $scope.selectedModel !== null && angular.isDefined($scope.selectedModel[$scope.settings.idProp]) && $scope.selectedModel[$scope.settings.idProp] === getFindObj(id)[$scope.settings.idProp];
420						}
421						return $scope.selectedModel !== null && angular.isDefined($scope.selectedModel[$scope.settings.externalIdProp]) && $scope.selectedModel[$scope.settings.externalIdProp] === getFindObj(id)[$scope.settings.externalIdProp];
422					}
423
424					return findIndex($scope.selectedModel, getFindObj(id)) !== -1;
425				};
426
427				$scope.externalEvents.onInitDone();
428
429				$scope.keyDownLink = function(event) {
430					var sourceScope = angular.element(event.target).scope();
431					var nextOption;
432					var parent = event.target.parentNode;
433					if (!$scope.settings.keyboardControls) {
434						return;
435					}
436					if (event.keyCode === 13 || event.keyCode === 32) { // enter
437						event.preventDefault();
438						if (!!sourceScope.option) {
439							$scope.setSelectedItem($scope.getPropertyForObject(sourceScope.option, $scope.settings.idProp), false, true);
440						} else if (event.target.id === 'deselectAll') {
441							$scope.deselectAll();
442						} else if (event.target.id === 'selectAll') {
443							$scope.selectAll();
444						}
445					} else if (event.keyCode === 38) { // up arrow
446						event.preventDefault();
447						if (!!parent.previousElementSibling) {
448							nextOption = parent.previousElementSibling.querySelector('a') || parent.previousElementSibling.querySelector('input');
449						}
450						while (!nextOption && !!parent) {
451							parent = parent.previousElementSibling;
452							if (!!parent) {
453								nextOption = parent.querySelector('a') || parent.querySelector('input');
454							}
455						}
456						if (!!nextOption) {
457							nextOption.focus();
458						}
459					} else if (event.keyCode === 40) { // down arrow
460						event.preventDefault();
461						if (!!parent.nextElementSibling) {
462							nextOption = parent.nextElementSibling.querySelector('a') || parent.nextElementSibling.querySelector('input');
463						}
464						while (!nextOption && !!parent) {
465							parent = parent.nextElementSibling;
466							if (!!parent) {
467								nextOption = parent.querySelector('a') || parent.querySelector('input');
468							}
469						}
470						if (!!nextOption) {
471							nextOption.focus();
472						}
473					} else if (event.keyCode === 27) {
474						event.preventDefault();
475
476						$scope.toggleDropdown();
477					}
478				};
479
480				$scope.keyDownSearchDefault = function(event) {
481					var parent = event.target.parentNode.parentNode;
482					var nextOption;
483					if (!$scope.settings.keyboardControls) {
484						return;
485					}
486					if (event.keyCode === 9 || event.keyCode === 40) { //tab
487						event.preventDefault();
488						focusFirstOption();
489					} else if (event.keyCode === 38) {
490						event.preventDefault();
491						if (!!parent.previousElementSibling) {
492							nextOption = parent.previousElementSibling.querySelector('a') || parent.previousElementSibling.querySelector('input');
493						}
494						while (!nextOption && !!parent) {
495							parent = parent.previousElementSibling;
496							if (!!parent) {
497								nextOption = parent.querySelector('a') || parent.querySelector('input');
498							}
499						}
500						if (!!nextOption) {
501							nextOption.focus();
502						}
503					} else if (event.keyCode === 27) {
504						event.preventDefault();
505
506						$scope.toggleDropdown();
507					}
508				};
509
510				$scope.keyDownSearch = function(event, searchFilter) {
511					var searchResult;
512					if (!$scope.settings.keyboardControls) {
513						return;
514					}
515					if (event.keyCode === 13) {
516						if ($scope.settings.selectionLimit === 1 && $scope.settings.enableSearch) {
517							searchResult = $filter('filter')($scope.options, $scope.getFilter(searchFilter));
518							if (searchResult.length === 1) {
519								$scope.setSelectedItem($scope.getPropertyForObject(searchResult[0], $scope.settings.idProp), false, true);
520							}
521						} else if ($scope.settings.enableSearch) {
522							$scope.selectAll();
523						}
524					}
525				};
526
527				$scope.getFilter = function(searchFilter) {
528					var filter = {};
529					filter[$scope.settings.searchField] = searchFilter;
530					return filter;
531				};
532
533				$scope.toggleSearch = function($event) {
534					if ($event) {
535						$event.stopPropagation();
536					}
537					$scope.settings.enableSearch = !$scope.settings.enableSearch;
538					if (!$scope.settings.enableSearch) {
539						$scope.input.searchFilter = '';
540					}
541				};
542
543				$scope.keyDownToggleSearch = function() {
544					if (!$scope.settings.keyboardControls) {
545						return;
546					}
547					if (event.keyCode === 13) {
548						$scope.toggleSearch();
549						if ($scope.settings.enableSearch) {
550							setTimeout(
551								function() {
552									angular.element($element)[0].querySelector('.searchField').focus();
553								}, 0
554							);
555						} else {
556							focusFirstOption();
557						}
558					}
559				};
560
561				function focusFirstOption() {
562					setTimeout(function() {
563						var elementToFocus = angular.element($element)[0].querySelector('.option');
564						if (angular.isDefined(elementToFocus) && elementToFocus != null) {
565							elementToFocus.focus();
566						}
567					}, 0);
568				}
569			}
570		};
571	}]);
572
573	function contains(collection, target) {
574		var containsTarget = false;
575		collection.some(function(object) {
576			if (object === target) {
577				containsTarget = true;
578				return true;
579			}
580		});
581		return containsTarget;
582	}
583
584	function find(collection, properties) {
585		var target;
586
587		collection.some(function(object) {
588			var hasAllSameProperties = true;
589			Object.keys(properties).forEach(function(key) {
590				if (object[key] !== properties[key]) {
591					hasAllSameProperties = false;
592				}
593			});
594			if (hasAllSameProperties) {
595				target = object;
596				return true
597			}
598		});
599
600		return target;
601	}
602
603	function findIndex(collection, properties) {
604		var index = -1;
605		var counter = -1;
606
607		collection.some(function(object) {
608			var hasAllSameProperties = true;
609			counter += 1;
610			Object.keys(properties).forEach(function(key) {
611				if (object[key] !== properties[key]) {
612					hasAllSameProperties = false;
613				}
614			});
615			if (hasAllSameProperties) {
616				index = counter;
617				return true
618			}
619		});
620
621		return index;
622	}
623})(angular);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.