socketioMEANseed/www/bower_components/angular-material/modules/closure/chips/chips.js
www/bower_components/angular-material/modules/closure/chips/chips.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.3
6 */
7goog.provide('ngmaterial.components.chips');
8goog.require('ngmaterial.components.autocomplete');
9goog.require('ngmaterial.core');
10/**
11 * @ngdoc module
12 * @name material.components.chips
13 */
14/*
15 * @see js folder for chips implementation
16 */
17angular.module('material.components.chips', [
18 'material.core',
19 'material.components.autocomplete'
20]);
21
22
23MdChipCtrl['$inject'] = ["$scope", "$element", "$mdConstant", "$timeout", "$mdUtil"];angular
24 .module('material.components.chips')
25 .controller('MdChipCtrl', MdChipCtrl);
26
27/**
28 * Controller for the MdChip component. Responsible for handling keyboard
29 * events and editting the chip if needed.
30 *
31 * @param $scope
32 * @param $element
33 * @param $mdConstant
34 * @param $timeout
35 * @param $mdUtil
36 * @constructor
37 */
38function MdChipCtrl ($scope, $element, $mdConstant, $timeout, $mdUtil) {
39 /**
40 * @type {$scope}
41 */
42 this.$scope = $scope;
43
44 /**
45 * @type {$element}
46 */
47 this.$element = $element;
48
49 /**
50 * @type {$mdConstant}
51 */
52 this.$mdConstant = $mdConstant;
53
54 /**
55 * @type {$timeout}
56 */
57 this.$timeout = $timeout;
58
59 /**
60 * @type {$mdUtil}
61 */
62 this.$mdUtil = $mdUtil;
63
64 /**
65 * @type {boolean}
66 */
67 this.isEditting = false;
68
69 /**
70 * @type {MdChipsCtrl}
71 */
72 this.parentController = undefined;
73
74 /**
75 * @type {boolean}
76 */
77 this.enableChipEdit = false;
78}
79
80
81/**
82 * @param {MdChipsCtrl} controller
83 */
84MdChipCtrl.prototype.init = function(controller) {
85 this.parentController = controller;
86 this.enableChipEdit = this.parentController.enableChipEdit;
87
88 if (this.enableChipEdit) {
89 this.$element.on('keydown', this.chipKeyDown.bind(this));
90 this.$element.on('mousedown', this.chipMouseDown.bind(this));
91 this.getChipContent().addClass('_md-chip-content-edit-is-enabled');
92 }
93};
94
95
96/**
97 * @return {Object}
98 */
99MdChipCtrl.prototype.getChipContent = function() {
100 var chipContents = this.$element[0].getElementsByClassName('md-chip-content');
101 return angular.element(chipContents[0]);
102};
103
104
105/**
106 * @return {Object}
107 */
108MdChipCtrl.prototype.getContentElement = function() {
109 return angular.element(this.getChipContent().children()[0]);
110};
111
112
113/**
114 * @return {number}
115 */
116MdChipCtrl.prototype.getChipIndex = function() {
117 return parseInt(this.$element.attr('index'));
118};
119
120
121/**
122 * Presents an input element to edit the contents of the chip.
123 */
124MdChipCtrl.prototype.goOutOfEditMode = function() {
125 if (!this.isEditting) return;
126
127 this.isEditting = false;
128 this.$element.removeClass('_md-chip-editing');
129 this.getChipContent()[0].contentEditable = 'false';
130 var chipIndex = this.getChipIndex();
131
132 var content = this.getContentElement().text();
133 if (content) {
134 this.parentController.updateChipContents(
135 chipIndex,
136 this.getContentElement().text()
137 );
138
139 this.$mdUtil.nextTick(function() {
140 if (this.parentController.selectedChip === chipIndex) {
141 this.parentController.focusChip(chipIndex);
142 }
143 }.bind(this));
144 } else {
145 this.parentController.removeChipAndFocusInput(chipIndex);
146 }
147};
148
149
150/**
151 * Given an HTML element. Selects contents of it.
152 * @param node
153 */
154MdChipCtrl.prototype.selectNodeContents = function(node) {
155 var range, selection;
156 if (document.body.createTextRange) {
157 range = document.body.createTextRange();
158 range.moveToElementText(node);
159 range.select();
160 } else if (window.getSelection) {
161 selection = window.getSelection();
162 range = document.createRange();
163 range.selectNodeContents(node);
164 selection.removeAllRanges();
165 selection.addRange(range);
166 }
167};
168
169
170/**
171 * Presents an input element to edit the contents of the chip.
172 */
173MdChipCtrl.prototype.goInEditMode = function() {
174 this.isEditting = true;
175 this.$element.addClass('_md-chip-editing');
176 this.getChipContent()[0].contentEditable = 'true';
177 this.getChipContent().on('blur', function() {
178 this.goOutOfEditMode();
179 }.bind(this));
180
181 this.selectNodeContents(this.getChipContent()[0]);
182};
183
184
185/**
186 * Handles the keydown event on the chip element. If enable-chip-edit attribute is
187 * set to true, space or enter keys can trigger going into edit mode. Enter can also
188 * trigger submitting if the chip is already being edited.
189 * @param event
190 */
191MdChipCtrl.prototype.chipKeyDown = function(event) {
192 if (!this.isEditting &&
193 (event.keyCode === this.$mdConstant.KEY_CODE.ENTER ||
194 event.keyCode === this.$mdConstant.KEY_CODE.SPACE)) {
195 event.preventDefault();
196 this.goInEditMode();
197 } else if (this.isEditting &&
198 event.keyCode === this.$mdConstant.KEY_CODE.ENTER) {
199 event.preventDefault();
200 this.goOutOfEditMode();
201 }
202};
203
204
205/**
206 * Handles the double click event
207 */
208MdChipCtrl.prototype.chipMouseDown = function() {
209 if(this.getChipIndex() == this.parentController.selectedChip &&
210 this.enableChipEdit &&
211 !this.isEditting) {
212 this.goInEditMode();
213 }
214};
215
216
217MdChip['$inject'] = ["$mdTheming", "$mdUtil", "$compile", "$timeout"];angular
218 .module('material.components.chips')
219 .directive('mdChip', MdChip);
220
221/**
222 * @ngdoc directive
223 * @name mdChip
224 * @module material.components.chips
225 *
226 * @description
227 * `<md-chip>` is a component used within `<md-chips>` and is responsible for rendering individual
228 * chips.
229 *
230 *
231 * @usage
232 * <hljs lang="html">
233 * <md-chip>{{$chip}}</md-chip>
234 * </hljs>
235 *
236 */
237
238// This hint text is hidden within a chip but used by screen readers to
239// inform the user how they can interact with a chip.
240var DELETE_HINT_TEMPLATE = '\
241 <span ng-if="!$mdChipsCtrl.readonly" class="md-visually-hidden">\
242 {{$mdChipsCtrl.deleteHint}}\
243 </span>';
244
245/**
246 * MDChip Directive Definition
247 *
248 * @param $mdTheming
249 * @param $mdUtil
250 * ngInject
251 */
252function MdChip($mdTheming, $mdUtil, $compile, $timeout) {
253 var deleteHintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);
254
255 return {
256 restrict: 'E',
257 require: ['^?mdChips', 'mdChip'],
258 link: postLink,
259 controller: 'MdChipCtrl'
260 };
261
262 function postLink(scope, element, attr, ctrls) {
263 var chipsController = ctrls.shift();
264 var chipController = ctrls.shift();
265 var chipContentElement = angular.element(element[0].querySelector('.md-chip-content'));
266
267 $mdTheming(element);
268
269 if (chipsController) {
270 chipController.init(chipsController);
271
272 // Append our delete hint to the div.md-chip-content (which does not exist at compile time)
273 chipContentElement.append($compile(deleteHintTemplate)(scope));
274
275 // When a chip is blurred, make sure to unset (or reset) the selected chip so that tabbing
276 // through elements works properly
277 chipContentElement.on('blur', function() {
278 chipsController.resetSelectedChip();
279 chipsController.$scope.$applyAsync();
280 });
281 }
282
283 // Use $timeout to ensure we run AFTER the element has been added to the DOM so we can focus it.
284 $timeout(function() {
285 if (!chipsController) {
286 return;
287 }
288
289 if (chipsController.shouldFocusLastChip) {
290 chipsController.focusLastChipThenInput();
291 }
292 });
293 }
294}
295
296
297MdChipRemove['$inject'] = ["$timeout"];angular
298 .module('material.components.chips')
299 .directive('mdChipRemove', MdChipRemove);
300
301/**
302 * @ngdoc directive
303 * @name mdChipRemove
304 * @restrict A
305 * @module material.components.chips
306 *
307 * @description
308 * Designates an element to be used as the delete button for a chip. <br/>
309 * This element is passed as a child of the `md-chips` element.
310 *
311 * The designated button will be just appended to the chip and removes the given chip on click.<br/>
312 * By default the button is not being styled by the `md-chips` component.
313 *
314 * @usage
315 * <hljs lang="html">
316 * <md-chips>
317 * <button md-chip-remove="">
318 * <md-icon md-svg-icon="md-close"></md-icon>
319 * </button>
320 * </md-chips>
321 * </hljs>
322 */
323
324
325/**
326 * MdChipRemove Directive Definition.
327 *
328 * @param $timeout
329 * @returns {{restrict: string, require: string[], link: Function, scope: boolean}}
330 * @constructor
331 */
332function MdChipRemove ($timeout) {
333 return {
334 restrict: 'A',
335 require: '^mdChips',
336 scope: false,
337 link: postLink
338 };
339
340 function postLink(scope, element, attr, ctrl) {
341 element.on('click', function(event) {
342 scope.$apply(function() {
343 ctrl.removeChip(scope.$$replacedScope.$index);
344 });
345 });
346
347 // Child elements aren't available until after a $timeout tick as they are hidden by an
348 // `ng-if`. see http://goo.gl/zIWfuw
349 $timeout(function() {
350 element.attr({ tabindex: -1, 'aria-hidden': true });
351 element.find('button').attr('tabindex', '-1');
352 });
353 }
354}
355
356
357MdChipTransclude['$inject'] = ["$compile"];angular
358 .module('material.components.chips')
359 .directive('mdChipTransclude', MdChipTransclude);
360
361function MdChipTransclude ($compile) {
362 return {
363 restrict: 'EA',
364 terminal: true,
365 link: link,
366 scope: false
367 };
368 function link (scope, element, attr) {
369 var ctrl = scope.$parent.$mdChipsCtrl,
370 newScope = ctrl.parent.$new(false, ctrl.parent);
371 newScope.$$replacedScope = scope;
372 newScope.$chip = scope.$chip;
373 newScope.$index = scope.$index;
374 newScope.$mdChipsCtrl = ctrl;
375
376 var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude);
377
378 element.html(newHtml);
379 $compile(element.contents())(newScope);
380 }
381}
382
383/**
384 * The default chip append delay.
385 *
386 * @type {number}
387 */
388MdChipsCtrl['$inject'] = ["$scope", "$attrs", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil"];
389var DEFAULT_CHIP_APPEND_DELAY = 300;
390
391angular
392 .module('material.components.chips')
393 .controller('MdChipsCtrl', MdChipsCtrl);
394
395/**
396 * Controller for the MdChips component. Responsible for adding to and
397 * removing from the list of chips, marking chips as selected, and binding to
398 * the models of various input components.
399 *
400 * @param $scope
401 * @param $attrs
402 * @param $mdConstant
403 * @param $log
404 * @param $element
405 * @param $timeout
406 * @param $mdUtil
407 * @constructor
408 */
409function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $mdUtil) {
410 /** @type {$timeout} **/
411 this.$timeout = $timeout;
412
413 /** @type {Object} */
414 this.$mdConstant = $mdConstant;
415
416 /** @type {angular.$scope} */
417 this.$scope = $scope;
418
419 /** @type {angular.$scope} */
420 this.parent = $scope.$parent;
421
422 /** @type {$mdUtil} */
423 this.$mdUtil = $mdUtil;
424
425 /** @type {$log} */
426 this.$log = $log;
427
428 /** @type {$element} */
429 this.$element = $element;
430
431 /** @type {$attrs} */
432 this.$attrs = $attrs;
433
434 /** @type {angular.NgModelController} */
435 this.ngModelCtrl = null;
436
437 /** @type {angular.NgModelController} */
438 this.userInputNgModelCtrl = null;
439
440 /** @type {MdAutocompleteCtrl} */
441 this.autocompleteCtrl = null;
442
443 /** @type {Element} */
444 this.userInputElement = null;
445
446 /** @type {Array.<Object>} */
447 this.items = [];
448
449 /** @type {number} */
450 this.selectedChip = -1;
451
452 /** @type {string} */
453 this.enableChipEdit = $mdUtil.parseAttributeBoolean($attrs.mdEnableChipEdit);
454
455 /** @type {string} */
456 this.addOnBlur = $mdUtil.parseAttributeBoolean($attrs.mdAddOnBlur);
457
458 /**
459 * The text to be used as the aria-label for the input.
460 * @type {string}
461 */
462 this.inputAriaLabel = 'Chips input.';
463
464 /**
465 * Hidden hint text to describe the chips container. Used to give context to screen readers when
466 * the chips are readonly and the input cannot be selected.
467 *
468 * @type {string}
469 */
470 this.containerHint = 'Chips container. Use arrow keys to select chips.';
471
472 /**
473 * Hidden hint text for how to delete a chip. Used to give context to screen readers.
474 * @type {string}
475 */
476 this.deleteHint = 'Press delete to remove this chip.';
477
478 /**
479 * Hidden label for the delete button. Used to give context to screen readers.
480 * @type {string}
481 */
482 this.deleteButtonLabel = 'Remove';
483
484 /**
485 * Model used by the input element.
486 * @type {string}
487 */
488 this.chipBuffer = '';
489
490 /**
491 * Whether to use the transformChip expression to transform the chip buffer
492 * before appending it to the list.
493 * @type {boolean}
494 */
495 this.useTransformChip = false;
496
497 /**
498 * Whether to use the onAdd expression to notify of chip additions.
499 * @type {boolean}
500 */
501 this.useOnAdd = false;
502
503 /**
504 * Whether to use the onRemove expression to notify of chip removals.
505 * @type {boolean}
506 */
507 this.useOnRemove = false;
508
509 /**
510 * The ID of the chips wrapper which is used to build unique IDs for the chips and the aria-owns
511 * attribute.
512 *
513 * Defaults to '_md-chips-wrapper-' plus a unique number.
514 *
515 * @type {string}
516 */
517 this.wrapperId = '';
518
519 /**
520 * Array of unique numbers which will be auto-generated any time the items change, and is used to
521 * create unique IDs for the aria-owns attribute.
522 *
523 * @type {Array}
524 */
525 this.contentIds = [];
526
527 /**
528 * The index of the chip that should have it's tabindex property set to 0 so it is selectable
529 * via the keyboard.
530 *
531 * @type {int}
532 */
533 this.ariaTabIndex = null;
534
535 /**
536 * After appending a chip, the chip will be focused for this number of milliseconds before the
537 * input is refocused.
538 *
539 * **Note:** This is **required** for compatibility with certain screen readers in order for
540 * them to properly allow keyboard access.
541 *
542 * @type {number}
543 */
544 this.chipAppendDelay = DEFAULT_CHIP_APPEND_DELAY;
545
546 this.init();
547}
548
549/**
550 * Initializes variables and sets up watchers
551 */
552MdChipsCtrl.prototype.init = function() {
553 var ctrl = this;
554
555 // Set the wrapper ID
556 ctrl.wrapperId = '_md-chips-wrapper-' + ctrl.$mdUtil.nextUid();
557
558 // Setup a watcher which manages the role and aria-owns attributes
559 ctrl.$scope.$watchCollection('$mdChipsCtrl.items', function() {
560 // Make sure our input and wrapper have the correct ARIA attributes
561 ctrl.setupInputAria();
562 ctrl.setupWrapperAria();
563 });
564
565 ctrl.$attrs.$observe('mdChipAppendDelay', function(newValue) {
566 ctrl.chipAppendDelay = parseInt(newValue) || DEFAULT_CHIP_APPEND_DELAY;
567 });
568};
569
570/**
571 * If we have an input, ensure it has the appropriate ARIA attributes.
572 */
573MdChipsCtrl.prototype.setupInputAria = function() {
574 var input = this.$element.find('input');
575
576 // If we have no input, just return
577 if (!input) {
578 return;
579 }
580
581 input.attr('role', 'textbox');
582 input.attr('aria-multiline', true);
583};
584
585/**
586 * Ensure our wrapper has the appropriate ARIA attributes.
587 */
588MdChipsCtrl.prototype.setupWrapperAria = function() {
589 var ctrl = this,
590 wrapper = this.$element.find('md-chips-wrap');
591
592 if (this.items && this.items.length) {
593 // Dynamically add the listbox role on every change because it must be removed when there are
594 // no items.
595 wrapper.attr('role', 'listbox');
596
597 // Generate some random (but unique) IDs for each chip
598 this.contentIds = this.items.map(function() {
599 return ctrl.wrapperId + '-chip-' + ctrl.$mdUtil.nextUid();
600 });
601
602 // Use the contentIDs above to generate the aria-owns attribute
603 wrapper.attr('aria-owns', this.contentIds.join(' '));
604 } else {
605 // If we have no items, then the role and aria-owns attributes MUST be removed
606 wrapper.removeAttr('role');
607 wrapper.removeAttr('aria-owns');
608 }
609};
610
611/**
612 * Handles the keydown event on the input element: by default <enter> appends
613 * the buffer to the chip list, while backspace removes the last chip in the
614 * list if the current buffer is empty.
615 * @param event
616 */
617MdChipsCtrl.prototype.inputKeydown = function(event) {
618 var chipBuffer = this.getChipBuffer();
619
620 // If we have an autocomplete, and it handled the event, we have nothing to do
621 if (this.autocompleteCtrl && event.isDefaultPrevented && event.isDefaultPrevented()) {
622 return;
623 }
624
625 if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) {
626 // Only select and focus the previous chip, if the current caret position of the
627 // input element is at the beginning.
628 if (this.getCursorPosition(event.target) !== 0) {
629 return;
630 }
631
632 event.preventDefault();
633 event.stopPropagation();
634
635 if (this.items.length) {
636 this.selectAndFocusChipSafe(this.items.length - 1);
637 }
638
639 return;
640 }
641
642 // By default <enter> appends the buffer to the chip list.
643 if (!this.separatorKeys || this.separatorKeys.length < 1) {
644 this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER];
645 }
646
647 // Support additional separator key codes in an array of `md-separator-keys`.
648 if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
649 if ((this.autocompleteCtrl && this.requireMatch) || !chipBuffer) return;
650 event.preventDefault();
651
652 // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
653 if (this.hasMaxChipsReached()) return;
654
655 this.appendChip(chipBuffer.trim());
656 this.resetChipBuffer();
657
658 return false;
659 }
660};
661
662/**
663 * Returns the cursor position of the specified input element.
664 * @param element HTMLInputElement
665 * @returns {Number} Cursor Position of the input.
666 */
667MdChipsCtrl.prototype.getCursorPosition = function(element) {
668 /*
669 * Figure out whether the current input for the chips buffer is valid for using
670 * the selectionStart / end property to retrieve the cursor position.
671 * Some browsers do not allow the use of those attributes, on different input types.
672 */
673 try {
674 if (element.selectionStart === element.selectionEnd) {
675 return element.selectionStart;
676 }
677 } catch (e) {
678 if (!element.value) {
679 return 0;
680 }
681 }
682};
683
684
685/**
686 * Updates the content of the chip at given index
687 * @param chipIndex
688 * @param chipContents
689 */
690MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){
691 if(chipIndex >= 0 && chipIndex < this.items.length) {
692 this.items[chipIndex] = chipContents;
693 this.ngModelCtrl.$setDirty();
694 }
695};
696
697
698/**
699 * Returns true if a chip is currently being edited. False otherwise.
700 * @return {boolean}
701 */
702MdChipsCtrl.prototype.isEditingChip = function() {
703 return !!this.$element[0].querySelector('._md-chip-editing');
704};
705
706
707MdChipsCtrl.prototype.isRemovable = function() {
708 // Return false if we have static chips
709 if (!this.ngModelCtrl) {
710 return false;
711 }
712
713 return this.readonly ? this.removable :
714 angular.isDefined(this.removable) ? this.removable : true;
715};
716
717/**
718 * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow
719 * keys switch which chips is active
720 * @param event
721 */
722MdChipsCtrl.prototype.chipKeydown = function (event) {
723 if (this.getChipBuffer()) return;
724 if (this.isEditingChip()) return;
725
726 switch (event.keyCode) {
727 case this.$mdConstant.KEY_CODE.BACKSPACE:
728 case this.$mdConstant.KEY_CODE.DELETE:
729 if (this.selectedChip < 0) return;
730 event.preventDefault();
731 // Cancel the delete action only after the event cancel. Otherwise the page will go back.
732 if (!this.isRemovable()) return;
733 this.removeAndSelectAdjacentChip(this.selectedChip);
734 break;
735 case this.$mdConstant.KEY_CODE.LEFT_ARROW:
736 event.preventDefault();
737 // By default, allow selection of -1 which will focus the input; if we're readonly, don't go
738 // below 0
739 if (this.selectedChip < 0 || (this.readonly && this.selectedChip == 0)) {
740 this.selectedChip = this.items.length;
741 }
742 if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1);
743 break;
744 case this.$mdConstant.KEY_CODE.RIGHT_ARROW:
745 event.preventDefault();
746 this.selectAndFocusChipSafe(this.selectedChip + 1);
747 break;
748 case this.$mdConstant.KEY_CODE.ESCAPE:
749 case this.$mdConstant.KEY_CODE.TAB:
750 if (this.selectedChip < 0) return;
751 event.preventDefault();
752 this.onFocus();
753 break;
754 }
755};
756
757/**
758 * Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder`
759 * when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used
760 * always.
761 */
762MdChipsCtrl.prototype.getPlaceholder = function() {
763 // Allow `secondary-placeholder` to be blank.
764 var useSecondary = (this.items && this.items.length &&
765 (this.secondaryPlaceholder == '' || this.secondaryPlaceholder));
766 return useSecondary ? this.secondaryPlaceholder : this.placeholder;
767};
768
769/**
770 * Removes chip at {@code index} and selects the adjacent chip.
771 * @param index
772 */
773MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index) {
774 var self = this;
775 var selIndex = self.getAdjacentChipIndex(index);
776 var wrap = this.$element[0].querySelector('md-chips-wrap');
777 var chip = this.$element[0].querySelector('md-chip[index="' + index + '"]');
778
779 self.removeChip(index);
780
781 // The dobule-timeout is currently necessary to ensure that the DOM has finalized and the select()
782 // will find the proper chip since the selection is index-based.
783 //
784 // TODO: Investigate calling from within chip $scope.$on('$destroy') to reduce/remove timeouts
785 self.$timeout(function() {
786 self.$timeout(function() {
787 self.selectAndFocusChipSafe(selIndex);
788 });
789 });
790};
791
792/**
793 * Sets the selected chip index to -1.
794 */
795MdChipsCtrl.prototype.resetSelectedChip = function() {
796 this.selectedChip = -1;
797 this.ariaTabIndex = null;
798};
799
800/**
801 * Gets the index of an adjacent chip to select after deletion. Adjacency is
802 * determined as the next chip in the list, unless the target chip is the
803 * last in the list, then it is the chip immediately preceding the target. If
804 * there is only one item in the list, -1 is returned (select none).
805 * The number returned is the index to select AFTER the target has been
806 * removed.
807 * If the current chip is not selected, then -1 is returned to select none.
808 */
809MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
810 var len = this.items.length - 1;
811 return (len == 0) ? -1 :
812 (index == len) ? index -1 : index;
813};
814
815/**
816 * Append the contents of the buffer to the chip list. This method will first
817 * call out to the md-transform-chip method, if provided.
818 *
819 * @param newChip
820 */
821MdChipsCtrl.prototype.appendChip = function(newChip) {
822 this.shouldFocusLastChip = true;
823 if (this.useTransformChip && this.transformChip) {
824 var transformedChip = this.transformChip({'$chip': newChip});
825
826 // Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
827 // they want the string version.
828 if (angular.isDefined(transformedChip)) {
829 newChip = transformedChip;
830 }
831 }
832
833 // If items contains an identical object to newChip, do not append
834 if (angular.isObject(newChip)){
835 var identical = this.items.some(function(item){
836 return angular.equals(newChip, item);
837 });
838 if (identical) return;
839 }
840
841 // Check for a null (but not undefined), or existing chip and cancel appending
842 if (newChip == null || this.items.indexOf(newChip) + 1) return;
843
844 // Append the new chip onto our list
845 var length = this.items.push(newChip);
846 var index = length - 1;
847
848 // Update model validation
849 this.ngModelCtrl.$setDirty();
850 this.validateModel();
851
852 // If they provide the md-on-add attribute, notify them of the chip addition
853 if (this.useOnAdd && this.onAdd) {
854 this.onAdd({ '$chip': newChip, '$index': index });
855 }
856};
857
858/**
859 * Sets whether to use the md-transform-chip expression. This expression is
860 * bound to scope and controller in {@code MdChipsDirective} as
861 * {@code transformChip}. Due to the nature of directive scope bindings, the
862 * controller cannot know on its own/from the scope whether an expression was
863 * actually provided.
864 */
865MdChipsCtrl.prototype.useTransformChipExpression = function() {
866 this.useTransformChip = true;
867};
868
869/**
870 * Sets whether to use the md-on-add expression. This expression is
871 * bound to scope and controller in {@code MdChipsDirective} as
872 * {@code onAdd}. Due to the nature of directive scope bindings, the
873 * controller cannot know on its own/from the scope whether an expression was
874 * actually provided.
875 */
876MdChipsCtrl.prototype.useOnAddExpression = function() {
877 this.useOnAdd = true;
878};
879
880/**
881 * Sets whether to use the md-on-remove expression. This expression is
882 * bound to scope and controller in {@code MdChipsDirective} as
883 * {@code onRemove}. Due to the nature of directive scope bindings, the
884 * controller cannot know on its own/from the scope whether an expression was
885 * actually provided.
886 */
887MdChipsCtrl.prototype.useOnRemoveExpression = function() {
888 this.useOnRemove = true;
889};
890
891/*
892 * Sets whether to use the md-on-select expression. This expression is
893 * bound to scope and controller in {@code MdChipsDirective} as
894 * {@code onSelect}. Due to the nature of directive scope bindings, the
895 * controller cannot know on its own/from the scope whether an expression was
896 * actually provided.
897 */
898MdChipsCtrl.prototype.useOnSelectExpression = function() {
899 this.useOnSelect = true;
900};
901
902/**
903 * Gets the input buffer. The input buffer can be the model bound to the
904 * default input item {@code this.chipBuffer}, the {@code selectedItem}
905 * model of an {@code md-autocomplete}, or, through some magic, the model
906 * bound to any inpput or text area element found within a
907 * {@code md-input-container} element.
908 * @return {string}
909 */
910MdChipsCtrl.prototype.getChipBuffer = function() {
911 var chipBuffer = !this.userInputElement ? this.chipBuffer :
912 this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
913 this.userInputElement[0].value;
914
915 // Ensure that the chip buffer is always a string. For example, the input element buffer might be falsy.
916 return angular.isString(chipBuffer) ? chipBuffer : '';
917};
918
919/**
920 * Resets the input buffer for either the internal input or user provided input element.
921 */
922MdChipsCtrl.prototype.resetChipBuffer = function() {
923 if (this.userInputElement) {
924 if (this.userInputNgModelCtrl) {
925 this.userInputNgModelCtrl.$setViewValue('');
926 this.userInputNgModelCtrl.$render();
927 } else {
928 this.userInputElement[0].value = '';
929 }
930 } else {
931 this.chipBuffer = '';
932 }
933};
934
935MdChipsCtrl.prototype.hasMaxChipsReached = function() {
936 if (angular.isString(this.maxChips)) this.maxChips = parseInt(this.maxChips, 10) || 0;
937
938 return this.maxChips > 0 && this.items.length >= this.maxChips;
939};
940
941/**
942 * Updates the validity properties for the ngModel.
943 */
944MdChipsCtrl.prototype.validateModel = function() {
945 this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
946};
947
948/**
949 * Removes the chip at the given index.
950 * @param index
951 */
952MdChipsCtrl.prototype.removeChip = function(index) {
953 var removed = this.items.splice(index, 1);
954
955 // Update model validation
956 this.ngModelCtrl.$setDirty();
957 this.validateModel();
958
959 if (removed && removed.length && this.useOnRemove && this.onRemove) {
960 this.onRemove({ '$chip': removed[0], '$index': index });
961 }
962};
963
964MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) {
965 this.removeChip(index);
966
967 if (this.autocompleteCtrl) {
968 // Always hide the autocomplete dropdown before focusing the autocomplete input.
969 // Wait for the input to move horizontally, because the chip was removed.
970 // This can lead to an incorrect dropdown position.
971 this.autocompleteCtrl.hidden = true;
972 this.$mdUtil.nextTick(this.onFocus.bind(this));
973 } else {
974 this.onFocus();
975 }
976
977};
978/**
979 * Selects the chip at `index`,
980 * @param index
981 */
982MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) {
983 // If we have no chips, or are asked to select a chip before the first, just focus the input
984 if (!this.items.length || index === -1) {
985 return this.focusInput();
986 }
987
988 // If we are asked to select a chip greater than the number of chips...
989 if (index >= this.items.length) {
990 if (this.readonly) {
991 // If we are readonly, jump back to the start (because we have no input)
992 index = 0;
993 } else {
994 // If we are not readonly, we should attempt to focus the input
995 return this.onFocus();
996 }
997 }
998
999 index = Math.max(index, 0);
1000 index = Math.min(index, this.items.length - 1);
1001
1002 this.selectChip(index);
1003 this.focusChip(index);
1004};
1005
1006MdChipsCtrl.prototype.focusLastChipThenInput = function() {
1007 var ctrl = this;
1008
1009 ctrl.shouldFocusLastChip = false;
1010
1011 ctrl.focusChip(this.items.length - 1);
1012
1013 ctrl.$timeout(function() {
1014 ctrl.focusInput();
1015 }, ctrl.chipAppendDelay);
1016};
1017
1018MdChipsCtrl.prototype.focusInput = function() {
1019 this.selectChip(-1);
1020 this.onFocus();
1021};
1022
1023/**
1024 * Marks the chip at the given index as selected.
1025 * @param index
1026 */
1027MdChipsCtrl.prototype.selectChip = function(index) {
1028 if (index >= -1 && index <= this.items.length) {
1029 this.selectedChip = index;
1030
1031 // Fire the onSelect if provided
1032 if (this.useOnSelect && this.onSelect) {
1033 this.onSelect({'$chip': this.items[index] });
1034 }
1035 } else {
1036 this.$log.warn('Selected Chip index out of bounds; ignoring.');
1037 }
1038};
1039
1040/**
1041 * Selects the chip at `index` and gives it focus.
1042 * @param index
1043 */
1044MdChipsCtrl.prototype.selectAndFocusChip = function(index) {
1045 this.selectChip(index);
1046 if (index != -1) {
1047 this.focusChip(index);
1048 }
1049};
1050
1051/**
1052 * Call `focus()` on the chip at `index`
1053 */
1054MdChipsCtrl.prototype.focusChip = function(index) {
1055 var chipContent = this.$element[0].querySelector('md-chip[index="' + index + '"] .md-chip-content');
1056
1057 this.ariaTabIndex = index;
1058
1059 chipContent.focus();
1060};
1061
1062/**
1063 * Configures the required interactions with the ngModel Controller.
1064 * Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}.
1065 * @param ngModelCtrl
1066 */
1067MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
1068 this.ngModelCtrl = ngModelCtrl;
1069
1070 var self = this;
1071 ngModelCtrl.$render = function() {
1072 // model is updated. do something.
1073 self.items = self.ngModelCtrl.$viewValue;
1074 };
1075};
1076
1077MdChipsCtrl.prototype.onFocus = function () {
1078 var input = this.$element[0].querySelector('input');
1079 input && input.focus();
1080 this.resetSelectedChip();
1081};
1082
1083MdChipsCtrl.prototype.onInputFocus = function () {
1084 this.inputHasFocus = true;
1085
1086 // Make sure we have the appropriate ARIA attributes
1087 this.setupInputAria();
1088
1089 // Make sure we don't have any chips selected
1090 this.resetSelectedChip();
1091};
1092
1093MdChipsCtrl.prototype.onInputBlur = function () {
1094 this.inputHasFocus = false;
1095
1096 if (this.shouldAddOnBlur()) {
1097 this.appendChip(this.getChipBuffer().trim());
1098 this.resetChipBuffer();
1099 }
1100};
1101
1102/**
1103 * Configure event bindings on a user-provided input element.
1104 * @param inputElement
1105 */
1106MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
1107 this.userInputElement = inputElement;
1108
1109 // Find the NgModelCtrl for the input element
1110 var ngModelCtrl = inputElement.controller('ngModel');
1111 // `.controller` will look in the parent as well.
1112 if (ngModelCtrl != this.ngModelCtrl) {
1113 this.userInputNgModelCtrl = ngModelCtrl;
1114 }
1115
1116 var scope = this.$scope;
1117 var ctrl = this;
1118
1119 // Run all of the events using evalAsync because a focus may fire a blur in the same digest loop
1120 var scopeApplyFn = function(event, fn) {
1121 scope.$evalAsync(angular.bind(ctrl, fn, event));
1122 };
1123
1124 // Bind to keydown and focus events of input
1125 inputElement
1126 .attr({ tabindex: 0 })
1127 .on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) })
1128 .on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) })
1129 .on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) })
1130};
1131
1132MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
1133 if (ctrl) {
1134 this.autocompleteCtrl = ctrl;
1135
1136 ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
1137 if (item) {
1138 // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
1139 if (this.hasMaxChipsReached()) return;
1140
1141 this.appendChip(item);
1142 this.resetChipBuffer();
1143 }
1144 }));
1145
1146 this.$element.find('input')
1147 .on('focus',angular.bind(this, this.onInputFocus) )
1148 .on('blur', angular.bind(this, this.onInputBlur) );
1149 }
1150};
1151
1152/**
1153 * Whether the current chip buffer should be added on input blur or not.
1154 * @returns {boolean}
1155 */
1156MdChipsCtrl.prototype.shouldAddOnBlur = function() {
1157
1158 // Update the custom ngModel validators from the chips component.
1159 this.validateModel();
1160
1161 var chipBuffer = this.getChipBuffer().trim();
1162 var isModelValid = this.ngModelCtrl.$valid;
1163 var isAutocompleteShowing = this.autocompleteCtrl && !this.autocompleteCtrl.hidden;
1164
1165 if (this.userInputNgModelCtrl) {
1166 isModelValid = isModelValid && this.userInputNgModelCtrl.$valid;
1167 }
1168
1169 return this.addOnBlur && !this.requireMatch && chipBuffer && isModelValid && !isAutocompleteShowing;
1170};
1171
1172MdChipsCtrl.prototype.hasFocus = function () {
1173 return this.inputHasFocus || this.selectedChip >= 0;
1174};
1175
1176MdChipsCtrl.prototype.contentIdFor = function(index) {
1177 return this.contentIds[index];
1178};
1179
1180
1181 MdChips['$inject'] = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout", "$$mdSvgRegistry"];angular
1182 .module('material.components.chips')
1183 .directive('mdChips', MdChips);
1184
1185 /**
1186 * @ngdoc directive
1187 * @name mdChips
1188 * @module material.components.chips
1189 *
1190 * @description
1191 * `<md-chips>` is an input component for building lists of strings or objects. The list items are
1192 * displayed as 'chips'. This component can make use of an `<input>` element or an
1193 * `<md-autocomplete>` element.
1194 *
1195 * ### Custom templates
1196 * A custom template may be provided to render the content of each chip. This is achieved by
1197 * specifying an `<md-chip-template>` element containing the custom content as a child of
1198 * `<md-chips>`.
1199 *
1200 * Note: Any attributes on
1201 * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
1202 * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
1203 * the chip object and its index in the list of chips, respectively.
1204 * To override the chip delete control, include an element (ideally a button) with the attribute
1205 * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
1206 * is also placed as a sibling to the chip content (on which there are also click listeners) to
1207 * avoid a nested ng-click situation.
1208 *
1209 * <!-- Note: We no longer want to include this in the site docs; but it should remain here for
1210 * future developers and those looking at the documentation.
1211 *
1212 * <h3> Pending Features </h3>
1213 * <ul style="padding-left:20px;">
1214 *
1215 * <ul>Style
1216 * <li>Colours for hover, press states (ripple?).</li>
1217 * </ul>
1218 *
1219 * <ul>Validation
1220 * <li>allow a validation callback</li>
1221 * <li>hilighting style for invalid chips</li>
1222 * </ul>
1223 *
1224 * <ul>Item mutation
1225 * <li>Support `
1226 * <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
1227 * click?
1228 * </li>
1229 * </ul>
1230 *
1231 * <ul>Truncation and Disambiguation (?)
1232 * <li>Truncate chip text where possible, but do not truncate entries such that two are
1233 * indistinguishable.</li>
1234 * </ul>
1235 *
1236 * <ul>Drag and Drop
1237 * <li>Drag and drop chips between related `<md-chips>` elements.
1238 * </li>
1239 * </ul>
1240 * </ul>
1241 *
1242 * //-->
1243 *
1244 * Sometimes developers want to limit the amount of possible chips.<br/>
1245 * You can specify the maximum amount of chips by using the following markup.
1246 *
1247 * <hljs lang="html">
1248 * <md-chips
1249 * ng-model="myItems"
1250 * placeholder="Add an item"
1251 * md-max-chips="5">
1252 * </md-chips>
1253 * </hljs>
1254 *
1255 * In some cases, you have an autocomplete inside of the `md-chips`.<br/>
1256 * When the maximum amount of chips has been reached, you can also disable the autocomplete selection.<br/>
1257 * Here is an example markup.
1258 *
1259 * <hljs lang="html">
1260 * <md-chips ng-model="myItems" md-max-chips="5">
1261 * <md-autocomplete ng-hide="myItems.length > 5" ...></md-autocomplete>
1262 * </md-chips>
1263 * </hljs>
1264 *
1265 * ### Accessibility
1266 *
1267 * The `md-chips` component supports keyboard and screen reader users since Version 1.1.2. In
1268 * order to achieve this, we modified the chips behavior to select newly appended chips for
1269 * `300ms` before re-focusing the input and allowing the user to type.
1270 *
1271 * For most users, this delay is small enough that it will not be noticeable but allows certain
1272 * screen readers to function properly (JAWS and NVDA in particular).
1273 *
1274 * We introduced a new `md-chip-append-delay` option to allow developers to better control this
1275 * behavior.
1276 *
1277 * Please refer to the documentation of this option (below) for more information.
1278 *
1279 * @param {string=|object=} ng-model A model to which the list of items will be bound.
1280 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
1281 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
1282 * displayed when there is at least one item in the list
1283 * @param {boolean=} md-removable Enables or disables the deletion of chips through the
1284 * removal icon or the Delete/Backspace key. Defaults to true.
1285 * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
1286 * the input and delete buttons. If no `ng-model` is provided, the chips will automatically be
1287 * marked as readonly.<br/><br/>
1288 * When `md-removable` is not defined, the `md-remove` behavior will be overwritten and disabled.
1289 * @param {string=} md-enable-chip-edit Set this to "true" to enable editing of chip contents. The user can
1290 * go into edit mode with pressing "space", "enter", or double clicking on the chip. Chip edit is only
1291 * supported for chips with basic template.
1292 * @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
1293 * <br/><br/>The validation property `md-max-chips` can be used when the max chips
1294 * amount is reached.
1295 * @param {boolean=} md-add-on-blur When set to true, remaining text inside of the input will
1296 * be converted into a new chip on blur.
1297 * @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when called
1298 * expects one of the following return values:
1299 * - an object representing the `$chip` input string
1300 * - `undefined` to simply add the `$chip` input string, or
1301 * - `null` to prevent the chip from being appended
1302 * @param {expression=} md-on-add An expression which will be called when a chip has been
1303 * added.
1304 * @param {expression=} md-on-remove An expression which will be called when a chip has been
1305 * removed.
1306 * @param {expression=} md-on-select An expression which will be called when a chip is selected.
1307 * @param {boolean} md-require-match If true, and the chips template contains an autocomplete,
1308 * only allow selection of pre-defined chips (i.e. you cannot add new ones).
1309 * @param {string=} input-aria-label A string read by screen readers to identify the input.
1310 * @param {string=} container-hint A string read by screen readers informing users of how to
1311 * navigate the chips. Used in readonly mode.
1312 * @param {string=} delete-hint A string read by screen readers instructing users that pressing
1313 * the delete key will remove the chip.
1314 * @param {string=} delete-button-label A label for the delete button. Also hidden and read by
1315 * screen readers.
1316 * @param {expression=} md-separator-keys An array of key codes used to separate chips.
1317 * @param {string=} md-chip-append-delay The number of milliseconds that the component will select
1318 * a newly appended chip before allowing a user to type into the input. This is **necessary**
1319 * for keyboard accessibility for screen readers. It defaults to 300ms and any number less than
1320 * 300 can cause issues with screen readers (particularly JAWS and sometimes NVDA).
1321 *
1322 * _Available since Version 1.1.2._
1323 *
1324 * **Note:** You can safely set this to `0` in one of the following two instances:
1325 *
1326 * 1. You are targeting an iOS or Safari-only application (where users would use VoiceOver) or
1327 * only ChromeVox users.
1328 *
1329 * 2. If you have utilized the `md-separator-keys` to disable the `enter` keystroke in
1330 * favor of another one (such as `,` or `;`).
1331 *
1332 * @usage
1333 * <hljs lang="html">
1334 * <md-chips
1335 * ng-model="myItems"
1336 * placeholder="Add an item"
1337 * readonly="isReadOnly">
1338 * </md-chips>
1339 * </hljs>
1340 *
1341 * <h3>Validation</h3>
1342 * When using [ngMessages](https://docs.angularjs.org/api/ngMessages), you can show errors based
1343 * on our custom validators.
1344 * <hljs lang="html">
1345 * <form name="userForm">
1346 * <md-chips
1347 * name="fruits"
1348 * ng-model="myItems"
1349 * placeholder="Add an item"
1350 * md-max-chips="5">
1351 * </md-chips>
1352 * <div ng-messages="userForm.fruits.$error" ng-if="userForm.$dirty">
1353 * <div ng-message="md-max-chips">You reached the maximum amount of chips</div>
1354 * </div>
1355 * </form>
1356 * </hljs>
1357 *
1358 */
1359
1360 var MD_CHIPS_TEMPLATE = '\
1361 <md-chips-wrap\
1362 id="{{$mdChipsCtrl.wrapperId}}"\
1363 tabindex="{{$mdChipsCtrl.readonly ? 0 : -1}}"\
1364 ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
1365 ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \
1366 \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly,\
1367 \'md-removable\': $mdChipsCtrl.isRemovable() }"\
1368 aria-setsize="{{$mdChipsCtrl.items.length}}"\
1369 class="md-chips">\
1370 <span ng-if="$mdChipsCtrl.readonly" class="md-visually-hidden">\
1371 {{$mdChipsCtrl.containerHint}}\
1372 </span>\
1373 <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
1374 index="{{$index}}"\
1375 ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly}">\
1376 <div class="md-chip-content"\
1377 tabindex="{{$mdChipsCtrl.ariaTabIndex == $index ? 0 : -1}}"\
1378 id="{{$mdChipsCtrl.contentIdFor($index)}}"\
1379 role="option"\
1380 aria-selected="{{$mdChipsCtrl.selectedChip == $index}}" \
1381 aria-posinset="{{$index}}"\
1382 ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\
1383 ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\
1384 md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\
1385 <div ng-if="$mdChipsCtrl.isRemovable()"\
1386 class="md-chip-remove-container"\
1387 tabindex="-1"\
1388 md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\
1389 </md-chip>\
1390 <div class="md-chip-input-container" ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl">\
1391 <div md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\
1392 </div>\
1393 </md-chips-wrap>';
1394
1395 var CHIP_INPUT_TEMPLATE = '\
1396 <input\
1397 class="md-input"\
1398 tabindex="0"\
1399 aria-label="{{$mdChipsCtrl.inputAriaLabel}}" \
1400 placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
1401 ng-model="$mdChipsCtrl.chipBuffer"\
1402 ng-focus="$mdChipsCtrl.onInputFocus()"\
1403 ng-blur="$mdChipsCtrl.onInputBlur()"\
1404 ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
1405
1406 var CHIP_DEFAULT_TEMPLATE = '\
1407 <span>{{$chip}}</span>';
1408
1409 var CHIP_REMOVE_TEMPLATE = '\
1410 <button\
1411 class="md-chip-remove"\
1412 ng-if="$mdChipsCtrl.isRemovable()"\
1413 ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\
1414 type="button"\
1415 tabindex="-1">\
1416 <md-icon md-svg-src="{{ $mdChipsCtrl.mdCloseIcon }}"></md-icon>\
1417 <span class="md-visually-hidden">\
1418 {{$mdChipsCtrl.deleteButtonLabel}}\
1419 </span>\
1420 </button>';
1421
1422 /**
1423 * MDChips Directive Definition
1424 */
1425 function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout, $$mdSvgRegistry) {
1426 // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
1427 var templates = getTemplates();
1428
1429 return {
1430 template: function(element, attrs) {
1431 // Clone the element into an attribute. By prepending the attribute
1432 // name with '$', Angular won't write it into the DOM. The cloned
1433 // element propagates to the link function via the attrs argument,
1434 // where various contained-elements can be consumed.
1435 attrs['$mdUserTemplate'] = element.clone();
1436 return templates.chips;
1437 },
1438 require: ['mdChips'],
1439 restrict: 'E',
1440 controller: 'MdChipsCtrl',
1441 controllerAs: '$mdChipsCtrl',
1442 bindToController: true,
1443 compile: compile,
1444 scope: {
1445 readonly: '=readonly',
1446 removable: '=mdRemovable',
1447 placeholder: '@',
1448 secondaryPlaceholder: '@',
1449 maxChips: '@mdMaxChips',
1450 transformChip: '&mdTransformChip',
1451 onAppend: '&mdOnAppend',
1452 onAdd: '&mdOnAdd',
1453 onRemove: '&mdOnRemove',
1454 onSelect: '&mdOnSelect',
1455 inputAriaLabel: '@',
1456 containerHint: '@',
1457 deleteHint: '@',
1458 deleteButtonLabel: '@',
1459 separatorKeys: '=?mdSeparatorKeys',
1460 requireMatch: '=?mdRequireMatch',
1461 chipAppendDelayString: '@?mdChipAppendDelay'
1462 }
1463 };
1464
1465 /**
1466 * Builds the final template for `md-chips` and returns the postLink function.
1467 *
1468 * Building the template involves 3 key components:
1469 * static chips
1470 * chip template
1471 * input control
1472 *
1473 * If no `ng-model` is provided, only the static chip work needs to be done.
1474 *
1475 * If no user-passed `md-chip-template` exists, the default template is used. This resulting
1476 * template is appended to the chip content element.
1477 *
1478 * The remove button may be overridden by passing an element with an md-chip-remove attribute.
1479 *
1480 * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
1481 * transclusion later. The transclusion happens in `postLink` as the parent scope is required.
1482 * If no user input is provided, a default one is appended to the input container node in the
1483 * template.
1484 *
1485 * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
1486 * transclusion in the `postLink` function.
1487 *
1488 *
1489 * @param element
1490 * @param attr
1491 * @returns {Function}
1492 */
1493 function compile(element, attr) {
1494 // Grab the user template from attr and reset the attribute to null.
1495 var userTemplate = attr['$mdUserTemplate'];
1496 attr['$mdUserTemplate'] = null;
1497
1498 var chipTemplate = getTemplateByQuery('md-chips>md-chip-template');
1499
1500 var chipRemoveSelector = $mdUtil
1501 .prefixer()
1502 .buildList('md-chip-remove')
1503 .map(function(attr) {
1504 return 'md-chips>*[' + attr + ']';
1505 })
1506 .join(',');
1507
1508 // Set the chip remove, chip contents and chip input templates. The link function will put
1509 // them on the scope for transclusion later.
1510 var chipRemoveTemplate = getTemplateByQuery(chipRemoveSelector) || templates.remove,
1511 chipContentsTemplate = chipTemplate || templates.default,
1512 chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete')
1513 || getTemplateByQuery('md-chips>input')
1514 || templates.input,
1515 staticChips = userTemplate.find('md-chip');
1516
1517 // Warn of malformed template. See #2545
1518 if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
1519 $log.warn('invalid placement of md-chip-remove within md-chip-template.');
1520 }
1521
1522 function getTemplateByQuery (query) {
1523 if (!attr.ngModel) return;
1524 var element = userTemplate[0].querySelector(query);
1525 return element && element.outerHTML;
1526 }
1527
1528 /**
1529 * Configures controller and transcludes.
1530 */
1531 return function postLink(scope, element, attrs, controllers) {
1532 $mdUtil.initOptionalProperties(scope, attr);
1533
1534 $mdTheming(element);
1535 var mdChipsCtrl = controllers[0];
1536 if(chipTemplate) {
1537 // Chip editing functionality assumes we are using the default chip template.
1538 mdChipsCtrl.enableChipEdit = false;
1539 }
1540
1541 mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
1542 mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate;
1543 mdChipsCtrl.chipInputTemplate = chipInputTemplate;
1544
1545 mdChipsCtrl.mdCloseIcon = $$mdSvgRegistry.mdClose;
1546
1547 element
1548 .attr({ tabindex: -1 })
1549 .on('focus', function () { mdChipsCtrl.onFocus(); });
1550
1551 if (attr.ngModel) {
1552 mdChipsCtrl.configureNgModel(element.controller('ngModel'));
1553
1554 // If an `md-transform-chip` attribute was set, tell the controller to use the expression
1555 // before appending chips.
1556 if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression();
1557
1558 // If an `md-on-append` attribute was set, tell the controller to use the expression
1559 // when appending chips.
1560 //
1561 // DEPRECATED: Will remove in official 1.0 release
1562 if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression();
1563
1564 // If an `md-on-add` attribute was set, tell the controller to use the expression
1565 // when adding chips.
1566 if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression();
1567
1568 // If an `md-on-remove` attribute was set, tell the controller to use the expression
1569 // when removing chips.
1570 if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();
1571
1572 // If an `md-on-select` attribute was set, tell the controller to use the expression
1573 // when selecting chips.
1574 if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();
1575
1576 // The md-autocomplete and input elements won't be compiled until after this directive
1577 // is complete (due to their nested nature). Wait a tick before looking for them to
1578 // configure the controller.
1579 if (chipInputTemplate != templates.input) {
1580 // The autocomplete will not appear until the readonly attribute is not true (i.e.
1581 // false or undefined), so we have to watch the readonly and then on the next tick
1582 // after the chip transclusion has run, we can configure the autocomplete and user
1583 // input.
1584 scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
1585 if (!readonly) {
1586
1587 $mdUtil.nextTick(function(){
1588
1589 if (chipInputTemplate.indexOf('<md-autocomplete') === 0) {
1590 var autocompleteEl = element.find('md-autocomplete');
1591 mdChipsCtrl.configureAutocomplete(autocompleteEl.controller('mdAutocomplete'));
1592 }
1593
1594 mdChipsCtrl.configureUserInput(element.find('input'));
1595 });
1596 }
1597 });
1598 }
1599
1600 // At the next tick, if we find an input, make sure it has the md-input class
1601 $mdUtil.nextTick(function() {
1602 var input = element.find('input');
1603
1604 input && input.toggleClass('md-input', true);
1605 });
1606 }
1607
1608 // Compile with the parent's scope and prepend any static chips to the wrapper.
1609 if (staticChips.length > 0) {
1610 var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
1611 $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
1612 }
1613 };
1614 }
1615
1616 function getTemplates() {
1617 return {
1618 chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE),
1619 input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE),
1620 default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE),
1621 remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE)
1622 };
1623 }
1624 }
1625
1626angular
1627 .module('material.components.chips')
1628 .controller('MdContactChipsCtrl', MdContactChipsCtrl);
1629
1630
1631
1632/**
1633 * Controller for the MdContactChips component
1634 * @constructor
1635 */
1636function MdContactChipsCtrl () {
1637 /** @type {Object} */
1638 this.selectedItem = null;
1639
1640 /** @type {string} */
1641 this.searchText = '';
1642}
1643
1644
1645MdContactChipsCtrl.prototype.queryContact = function(searchText) {
1646 return this.contactQuery({'$query': searchText});
1647};
1648
1649
1650MdContactChipsCtrl.prototype.itemName = function(item) {
1651 return item[this.contactName];
1652};
1653
1654
1655MdContactChips['$inject'] = ["$mdTheming", "$mdUtil"];angular
1656 .module('material.components.chips')
1657 .directive('mdContactChips', MdContactChips);
1658
1659/**
1660 * @ngdoc directive
1661 * @name mdContactChips
1662 * @module material.components.chips
1663 *
1664 * @description
1665 * `<md-contact-chips>` is an input component based on `md-chips` and makes use of an
1666 * `md-autocomplete` element. The component allows the caller to supply a query expression which
1667 * returns a list of possible contacts. The user can select one of these and add it to the list of
1668 * chips.
1669 *
1670 * You may also use the `md-highlight-text` directive along with its parameters to control the
1671 * appearance of the matched text inside of the contacts' autocomplete popup.
1672 *
1673 * @param {string=|object=} ng-model A model to bind the list of items to
1674 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
1675 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
1676 * displayed when there is at least on item in the list
1677 * @param {expression} md-contacts An expression expected to return contacts matching the search
1678 * test, `$query`. If this expression involves a promise, a loading bar is displayed while
1679 * waiting for it to resolve.
1680 * @param {string} md-contact-name The field name of the contact object representing the
1681 * contact's name.
1682 * @param {string} md-contact-email The field name of the contact object representing the
1683 * contact's email address.
1684 * @param {string} md-contact-image The field name of the contact object representing the
1685 * contact's image.
1686 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
1687 * make suggestions
1688 *
1689 * @param {expression=} filter-selected Whether to filter selected contacts from the list of
1690 * suggestions shown in the autocomplete.
1691 *
1692 * ***Note:** This attribute has been removed but may come back.*
1693 *
1694 *
1695 *
1696 * @usage
1697 * <hljs lang="html">
1698 * <md-contact-chips
1699 * ng-model="ctrl.contacts"
1700 * md-contacts="ctrl.querySearch($query)"
1701 * md-contact-name="name"
1702 * md-contact-image="image"
1703 * md-contact-email="email"
1704 * placeholder="To">
1705 * </md-contact-chips>
1706 * </hljs>
1707 *
1708 */
1709
1710
1711var MD_CONTACT_CHIPS_TEMPLATE = '\
1712 <md-chips class="md-contact-chips"\
1713 ng-model="$mdContactChipsCtrl.contacts"\
1714 md-require-match="$mdContactChipsCtrl.requireMatch"\
1715 md-chip-append-delay="{{$mdContactChipsCtrl.chipAppendDelay}}" \
1716 md-autocomplete-snap>\
1717 <md-autocomplete\
1718 md-menu-class="md-contact-chips-suggestions"\
1719 md-selected-item="$mdContactChipsCtrl.selectedItem"\
1720 md-search-text="$mdContactChipsCtrl.searchText"\
1721 md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\
1722 md-item-text="$mdContactChipsCtrl.itemName(item)"\
1723 md-no-cache="true"\
1724 md-min-length="$mdContactChipsCtrl.minLength"\
1725 md-autoselect\
1726 placeholder="{{$mdContactChipsCtrl.contacts.length == 0 ?\
1727 $mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\
1728 <div class="md-contact-suggestion">\
1729 <img \
1730 ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\
1731 alt="{{item[$mdContactChipsCtrl.contactName]}}"\
1732 ng-if="item[$mdContactChipsCtrl.contactImage]" />\
1733 <span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText"\
1734 md-highlight-flags="{{$mdContactChipsCtrl.highlightFlags}}">\
1735 {{item[$mdContactChipsCtrl.contactName]}}\
1736 </span>\
1737 <span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\
1738 </div>\
1739 </md-autocomplete>\
1740 <md-chip-template>\
1741 <div class="md-contact-avatar">\
1742 <img \
1743 ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\
1744 alt="{{$chip[$mdContactChipsCtrl.contactName]}}"\
1745 ng-if="$chip[$mdContactChipsCtrl.contactImage]" />\
1746 </div>\
1747 <div class="md-contact-name">\
1748 {{$chip[$mdContactChipsCtrl.contactName]}}\
1749 </div>\
1750 </md-chip-template>\
1751 </md-chips>';
1752
1753
1754/**
1755 * MDContactChips Directive Definition
1756 *
1757 * @param $mdTheming
1758 * @returns {*}
1759 * ngInject
1760 */
1761function MdContactChips($mdTheming, $mdUtil) {
1762 return {
1763 template: function(element, attrs) {
1764 return MD_CONTACT_CHIPS_TEMPLATE;
1765 },
1766 restrict: 'E',
1767 controller: 'MdContactChipsCtrl',
1768 controllerAs: '$mdContactChipsCtrl',
1769 bindToController: true,
1770 compile: compile,
1771 scope: {
1772 contactQuery: '&mdContacts',
1773 placeholder: '@',
1774 secondaryPlaceholder: '@',
1775 contactName: '@mdContactName',
1776 contactImage: '@mdContactImage',
1777 contactEmail: '@mdContactEmail',
1778 contacts: '=ngModel',
1779 requireMatch: '=?mdRequireMatch',
1780 minLength: '=?mdMinLength',
1781 highlightFlags: '@?mdHighlightFlags',
1782 chipAppendDelay: '@?mdChipAppendDelay'
1783 }
1784 };
1785
1786 function compile(element, attr) {
1787 return function postLink(scope, element, attrs, controllers) {
1788 var contactChipsController = controllers;
1789
1790 $mdUtil.initOptionalProperties(scope, attr);
1791 $mdTheming(element);
1792
1793 element.attr('tabindex', '-1');
1794
1795 attrs.$observe('mdChipAppendDelay', function(newValue) {
1796 contactChipsController.chipAppendDelay = newValue;
1797 });
1798 };
1799 }
1800}
1801
1802ngmaterial.components.chips = angular.module("material.components.chips");