source: trip-planner-front/node_modules/angular-material/modules/js/chips/chips.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 74.2 KB
Line 
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.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 editing 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.isEditing = 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('dblclick', this.chipMouseDoubleClick.bind(this));
91 this.getChipContent().addClass('_md-chip-content-edit-is-enabled');
92 }
93};
94
95
96/**
97 * @return {Object} first element with the md-chip-content class
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 * When editing the chip, if the user modifies the existing contents, we'll get a span back and
107 * need to ignore text elements as they only contain blank space.
108 * `children()` ignores text elements.
109 *
110 * When editing the chip, if the user deletes the contents and then enters some new content
111 * we'll only get a text element back.
112 * @return {Object} jQuery object representing the content element of the chip
113 */
114MdChipCtrl.prototype.getContentElement = function() {
115 var contentElement = angular.element(this.getChipContent().children()[0]);
116 if (!contentElement || contentElement.length === 0) {
117 contentElement = angular.element(this.getChipContent().contents()[0]);
118 }
119 return contentElement;
120};
121
122
123/**
124 * @return {number} index of this chip
125 */
126MdChipCtrl.prototype.getChipIndex = function() {
127 return parseInt(this.$element.attr('index'));
128};
129
130
131/**
132 * Update the chip's contents, focus the chip if it's selected, and exit edit mode.
133 * If the contents were updated to be empty, remove the chip and re-focus the input element.
134 */
135MdChipCtrl.prototype.goOutOfEditMode = function() {
136 if (!this.isEditing) {
137 return;
138 }
139
140 this.isEditing = false;
141 this.$element.removeClass('_md-chip-editing');
142 this.getChipContent()[0].contentEditable = 'false';
143 var chipIndex = this.getChipIndex();
144
145 var content = this.getContentElement().text();
146 if (content) {
147 this.parentController.updateChipContents(chipIndex, content);
148
149 this.$mdUtil.nextTick(function() {
150 if (this.parentController.selectedChip === chipIndex) {
151 this.parentController.focusChip(chipIndex);
152 }
153 }.bind(this));
154 } else {
155 this.parentController.removeChipAndFocusInput(chipIndex);
156 }
157};
158
159
160/**
161 * Given an HTML element. Selects contents of it.
162 * @param {Element} node
163 */
164MdChipCtrl.prototype.selectNodeContents = function(node) {
165 var range, selection;
166 if (document.body.createTextRange) {
167 range = document.body.createTextRange();
168 range.moveToElementText(node);
169 range.select();
170 } else if (window.getSelection) {
171 selection = window.getSelection();
172 range = document.createRange();
173 range.selectNodeContents(node);
174 selection.removeAllRanges();
175 selection.addRange(range);
176 }
177};
178
179
180/**
181 * Presents an input element to edit the contents of the chip.
182 */
183MdChipCtrl.prototype.goInEditMode = function() {
184 this.isEditing = true;
185 this.$element.addClass('_md-chip-editing');
186 this.getChipContent()[0].contentEditable = 'true';
187 this.getChipContent().on('blur', function() {
188 this.goOutOfEditMode();
189 }.bind(this));
190
191 this.selectNodeContents(this.getChipContent()[0]);
192};
193
194
195/**
196 * Handles the keydown event on the chip element. If enable-chip-edit attribute is
197 * set to true, space or enter keys can trigger going into edit mode. Enter can also
198 * trigger submitting if the chip is already being edited.
199 * @param {KeyboardEvent} event
200 */
201MdChipCtrl.prototype.chipKeyDown = function(event) {
202 if (!this.isEditing &&
203 (event.keyCode === this.$mdConstant.KEY_CODE.ENTER ||
204 event.keyCode === this.$mdConstant.KEY_CODE.SPACE)) {
205 event.preventDefault();
206 this.goInEditMode();
207 } else if (this.isEditing && event.keyCode === this.$mdConstant.KEY_CODE.ENTER) {
208 event.preventDefault();
209 this.goOutOfEditMode();
210 }
211};
212
213
214/**
215 * Enter edit mode if we're not already editing and the enable-chip-edit attribute is enabled.
216 */
217MdChipCtrl.prototype.chipMouseDoubleClick = function() {
218 if (this.enableChipEdit && !this.isEditing) {
219 this.goInEditMode();
220 }
221};
222
223
224MdChip['$inject'] = ["$mdTheming", "$mdUtil", "$compile", "$timeout"];angular
225 .module('material.components.chips')
226 .directive('mdChip', MdChip);
227
228/**
229 * @ngdoc directive
230 * @name mdChip
231 * @module material.components.chips
232 *
233 * @description
234 * `<md-chip>` is a component used within `<md-chips>`. It is responsible for rendering an
235 * individual chip.
236 *
237 *
238 * @usage
239 * <hljs lang="html">
240 * <md-chips>
241 * <md-chip>{{$chip}}</md-chip>
242 * </md-chips>
243 * </hljs>
244 *
245 */
246
247/**
248 * MDChip Directive Definition
249 *
250 * @param $mdTheming
251 * @param $mdUtil
252 * @param $compile
253 * @param $timeout
254 * ngInject
255 */
256function MdChip($mdTheming, $mdUtil, $compile, $timeout) {
257 return {
258 restrict: 'E',
259 require: ['^?mdChips', 'mdChip'],
260 link: postLink,
261 controller: 'MdChipCtrl'
262 };
263
264 function postLink(scope, element, attr, ctrls) {
265 var chipsController = ctrls.shift();
266 var chipController = ctrls.shift();
267 var chipContentElement = angular.element(element[0].querySelector('.md-chip-content'));
268
269 $mdTheming(element);
270
271 if (chipsController) {
272 chipController.init(chipsController);
273
274 // When a chip is blurred, make sure to unset (or reset) the selected chip so that tabbing
275 // through elements works properly
276 chipContentElement.on('blur', function() {
277 chipsController.resetSelectedChip();
278 chipsController.$scope.$applyAsync();
279 });
280 }
281
282 // Use $timeout to ensure we run AFTER the element has been added to the DOM so we can focus it.
283 $timeout(function() {
284 if (!chipsController) {
285 return;
286 }
287
288 if (chipsController.shouldFocusLastChip) {
289 chipsController.focusLastChipThenInput();
290 }
291 });
292 }
293}
294
295
296MdChipRemove['$inject'] = ["$timeout"];angular
297 .module('material.components.chips')
298 .directive('mdChipRemove', MdChipRemove);
299
300/**
301 * @ngdoc directive
302 * @name mdChipRemove
303 * @restrict A
304 * @module material.components.chips
305 *
306 * @description
307 * Indicates that the associated element should be used as the delete button template for all chips.
308 * The associated element must be a child of `md-chips`.
309 *
310 * The provided button template will be appended to each chip and will remove the associated chip
311 * on click.
312 *
313 * The button is not styled or themed based on the theme set on the `md-chips` component. A theme
314 * class and custom icon can be specified in your template.
315 *
316 * You can also specify the `type` of the button in your template.
317 *
318 * @usage
319 * ### With Standard Chips
320 * <hljs lang="html">
321 * <md-chips ...>
322 * <button md-chip-remove type="button" aria-label="Remove {{$chip}}">
323 * <md-icon md-svg-icon="md-cancel"></md-icon>
324 * </button>
325 * </md-chips>
326 * </hljs>
327 *
328 * ### With Object Chips
329 * <hljs lang="html">
330 * <md-chips ...>
331 * <button md-chip-remove type="button" aria-label="Remove {{$chip.name}}">
332 * <md-icon md-svg-icon="md-cancel"></md-icon>
333 * </button>
334 * </md-chips>
335 * </hljs>
336 */
337
338
339/**
340 * MdChipRemove Directive Definition.
341 *
342 * @param $timeout
343 * @returns {{restrict: string, require: string[], link: Function, scope: boolean}}
344 * @constructor
345 */
346function MdChipRemove ($timeout) {
347 return {
348 restrict: 'A',
349 require: '^mdChips',
350 scope: false,
351 link: postLink
352 };
353
354 function postLink(scope, element, attr, ctrl) {
355 element.on('click', function() {
356 scope.$apply(function() {
357 ctrl.removeChip(scope.$$replacedScope.$index);
358 });
359 });
360
361 // Child elements aren't available until after a $timeout tick as they are hidden by an
362 // `ng-if`. see http://goo.gl/zIWfuw
363 $timeout(function() {
364 element.attr({ 'tabindex': '-1', 'aria-hidden': 'true' });
365 element.find('button').attr('tabindex', '-1');
366 });
367 }
368}
369
370
371MdChipTransclude['$inject'] = ["$compile"];angular
372 .module('material.components.chips')
373 .directive('mdChipTransclude', MdChipTransclude);
374
375function MdChipTransclude ($compile) {
376 return {
377 restrict: 'EA',
378 terminal: true,
379 link: link,
380 scope: false
381 };
382 function link (scope, element, attr) {
383 var ctrl = scope.$parent.$mdChipsCtrl,
384 newScope = ctrl.parent.$new(false, ctrl.parent);
385 newScope.$$replacedScope = scope;
386 newScope.$chip = scope.$chip;
387 newScope.$index = scope.$index;
388 newScope.$mdChipsCtrl = ctrl;
389
390 var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude);
391
392 element.html(newHtml);
393 $compile(element.contents())(newScope);
394 }
395}
396
397/**
398 * The default chip append delay.
399 *
400 * @type {number}
401 */
402MdChipsCtrl['$inject'] = ["$scope", "$attrs", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil", "$mdLiveAnnouncer", "$exceptionHandler"];
403var DEFAULT_CHIP_APPEND_DELAY = 300;
404
405angular
406 .module('material.components.chips')
407 .controller('MdChipsCtrl', MdChipsCtrl);
408
409/**
410 * Controller for the MdChips component. Responsible for adding to and
411 * removing from the list of chips, marking chips as selected, and binding to
412 * the models of various input components.
413 *
414 * @param $scope
415 * @param $attrs
416 * @param $mdConstant
417 * @param $log
418 * @param $element
419 * @param $timeout
420 * @param $mdUtil
421 * @param $mdLiveAnnouncer
422 * @param $exceptionHandler
423 * @constructor
424 */
425function MdChipsCtrl ($scope, $attrs, $mdConstant, $log, $element, $timeout, $mdUtil,
426 $mdLiveAnnouncer, $exceptionHandler) {
427 /** @type {Function} **/
428 this.$timeout = $timeout;
429
430 /** @type {Object} */
431 this.$mdConstant = $mdConstant;
432
433 /** @type {angular.$scope} */
434 this.$scope = $scope;
435
436 /** @type {angular.$scope} */
437 this.parent = $scope.$parent;
438
439 /** @type {$mdUtil} */
440 this.$mdUtil = $mdUtil;
441
442 /** @type {$log} */
443 this.$log = $log;
444
445 /** @type {$mdLiveAnnouncer} */
446 this.$mdLiveAnnouncer = $mdLiveAnnouncer;
447
448 /** @type {$exceptionHandler} */
449 this.$exceptionHandler = $exceptionHandler;
450
451 /** @type {$element} */
452 this.$element = $element;
453
454 /** @type {$attrs} */
455 this.$attrs = $attrs;
456
457 /** @type {angular.NgModelController} */
458 this.ngModelCtrl = null;
459
460 /** @type {angular.NgModelController} */
461 this.userInputNgModelCtrl = null;
462
463 /** @type {MdAutocompleteCtrl} */
464 this.autocompleteCtrl = null;
465
466 /** @type {Element} */
467 this.userInputElement = null;
468
469 /** @type {Array.<Object>} */
470 this.items = [];
471
472 /** @type {number} */
473 this.selectedChip = -1;
474
475 /** @type {string} */
476 this.enableChipEdit = $mdUtil.parseAttributeBoolean($attrs.mdEnableChipEdit);
477
478 /** @type {string} */
479 this.addOnBlur = $mdUtil.parseAttributeBoolean($attrs.mdAddOnBlur);
480
481 /**
482 * The class names to apply to the autocomplete or input.
483 * @type {string}
484 */
485 this.inputClass = '';
486
487 /**
488 * The text to be used as the aria-label for the input.
489 * @type {string}
490 */
491 this.inputAriaLabel = 'Chips input.';
492
493 /**
494 * Label text to describe the chips container. Used to give context and instructions to screen
495 * reader users when the chips container is selected.
496 * @type {string}
497 */
498 this.containerHint = 'Chips container. Use arrow keys to select chips.';
499
500 /**
501 * Label text to describe the chips container when it is empty. Used to give context and
502 * instructions to screen reader users when the chips container is selected and it contains
503 * no chips.
504 * @type {string}
505 */
506 this.containerEmptyHint =
507 'Chips container. Enter the text area, then type text, and press enter to add a chip.';
508
509 /**
510 * Hidden hint text for how to delete a chip. Used to give context to screen readers.
511 * @type {string}
512 */
513 this.deleteHint = 'Press delete to remove this chip.';
514
515 /**
516 * Hidden label for the delete button. Used to give context to screen readers.
517 * @type {string}
518 */
519 this.deleteButtonLabel = 'Remove';
520
521 /**
522 * Model used by the input element.
523 * @type {string}
524 */
525 this.chipBuffer = '';
526
527 /**
528 * Whether to use the transformChip expression to transform the chip buffer
529 * before appending it to the list.
530 * @type {boolean}
531 */
532 this.useTransformChip = false;
533
534 /**
535 * Whether to use the onAdd expression to notify of chip additions.
536 * @type {boolean}
537 */
538 this.useOnAdd = false;
539
540 /**
541 * Whether to use the onRemove expression to notify of chip removals.
542 * @type {boolean}
543 */
544 this.useOnRemove = false;
545
546 /**
547 * The ID of the chips wrapper which is used to build unique IDs for the chips and the aria-owns
548 * attribute.
549 *
550 * Defaults to '_md-chips-wrapper-' plus a unique number.
551 *
552 * @type {string}
553 */
554 this.wrapperId = '';
555
556 /**
557 * Array of unique numbers which will be auto-generated any time the items change, and is used to
558 * create unique IDs for the aria-owns attribute.
559 *
560 * @type {Array<number>}
561 */
562 this.contentIds = [];
563
564 /**
565 * The index of the chip that should have it's `tabindex` property set to `0` so it is selectable
566 * via the keyboard.
567 *
568 * @type {number|null}
569 */
570 this.ariaTabIndex = null;
571
572 /**
573 * After appending a chip, the chip will be focused for this number of milliseconds before the
574 * input is refocused.
575 *
576 * **Note:** This is **required** for compatibility with certain screen readers in order for
577 * them to properly allow keyboard access.
578 *
579 * @type {number}
580 */
581 this.chipAppendDelay = DEFAULT_CHIP_APPEND_DELAY;
582
583 /**
584 * Collection of functions to call to un-register watchers
585 *
586 * @type {Array}
587 */
588 this.deRegister = [];
589
590 /**
591 * The screen reader will announce the chip content followed by this message when a chip is added.
592 * @type {string}
593 */
594 this.addedMessage = 'added';
595
596 /**
597 * The screen reader will announce the chip content followed by this message when a chip is
598 * removed.
599 * @type {string}
600 */
601 this.removedMessage = 'removed';
602
603 this.init();
604}
605
606/**
607 * Initializes variables and sets up watchers
608 */
609MdChipsCtrl.prototype.init = function() {
610 var ctrl = this;
611
612 // Set the wrapper ID
613 this.wrapperId = '_md-chips-wrapper-' + this.$mdUtil.nextUid();
614
615 // If we're using static chips, then we need to initialize a few things.
616 if (!this.$element.attr('ng-model')) {
617 this.setupStaticChips();
618 }
619
620 // Setup a watcher which manages the role and aria-owns attributes.
621 // This is never called for static chips since items is not defined.
622 this.deRegister.push(
623 this.$scope.$watchCollection('$mdChipsCtrl.items', function() {
624 // Make sure our input and wrapper have the correct ARIA attributes
625 ctrl.setupInputAria();
626 ctrl.setupWrapperAria();
627 })
628 );
629
630 this.deRegister.push(
631 this.$attrs.$observe('mdChipAppendDelay', function(newValue) {
632 ctrl.chipAppendDelay = parseInt(newValue) || DEFAULT_CHIP_APPEND_DELAY;
633 })
634 );
635};
636
637/**
638 * Destructor for cleanup
639 */
640MdChipsCtrl.prototype.$onDestroy = function $onDestroy() {
641 var $destroyFn;
642 while (($destroyFn = this.deRegister.pop())) {
643 $destroyFn.call(this);
644 }
645};
646
647/**
648 * If we have an input, ensure it has the appropriate ARIA attributes.
649 */
650MdChipsCtrl.prototype.setupInputAria = function() {
651 var input = this.$element.find('input');
652
653 // If we have no input, just return
654 if (!input) {
655 return;
656 }
657
658 input.attr('role', 'textbox');
659 input.attr('aria-multiline', true);
660 if (this.inputAriaDescribedBy) {
661 input.attr('aria-describedby', this.inputAriaDescribedBy);
662 }
663 if (this.inputAriaLabelledBy) {
664 input.attr('aria-labelledby', this.inputAriaLabelledBy);
665 input.removeAttr('aria-label');
666 } else {
667 input.attr('aria-label', this.inputAriaLabel);
668 }
669};
670
671/**
672 * Ensure our wrapper has the appropriate ARIA attributes.
673 */
674MdChipsCtrl.prototype.setupWrapperAria = function() {
675 var ctrl = this,
676 wrapper = this.$element.find('md-chips-wrap');
677
678 if (this.items && this.items.length) {
679 // Dynamically add the listbox role on every change because it must be removed when there are
680 // no items.
681 wrapper.attr('role', 'listbox');
682
683 // Generate some random (but unique) IDs for each chip
684 this.contentIds = this.items.map(function() {
685 return ctrl.wrapperId + '-chip-' + ctrl.$mdUtil.nextUid();
686 });
687
688 // Use the contentIDs above to generate the aria-owns attribute
689 wrapper.attr('aria-owns', this.contentIds.join(' '));
690 wrapper.attr('aria-label', this.containerHint);
691 } else {
692 // If we have no items, then the role and aria-owns attributes MUST be removed
693 wrapper.removeAttr('role');
694 wrapper.removeAttr('aria-owns');
695 wrapper.attr('aria-label', this.containerEmptyHint);
696 }
697};
698
699/**
700 * Apply specific roles and aria attributes for static chips
701 */
702MdChipsCtrl.prototype.setupStaticChips = function() {
703 var ctrl = this, i, staticChips;
704 var wrapper = this.$element.find('md-chips-wrap');
705
706 this.$timeout(function() {
707 wrapper.attr('role', 'list');
708 staticChips = wrapper[0].children;
709 for (i = 0; i < staticChips.length; i++) {
710 staticChips[i].setAttribute('role', 'listitem');
711 staticChips[i].setAttribute('aria-setsize', staticChips.length);
712 }
713 if (ctrl.inputAriaDescribedBy) {
714 wrapper.attr('aria-describedby', ctrl.inputAriaDescribedBy);
715 }
716 if (ctrl.inputAriaLabelledBy) {
717 wrapper.attr('aria-labelledby', ctrl.inputAriaLabelledBy);
718 wrapper.removeAttr('aria-label');
719 } else {
720 wrapper.attr('aria-label', ctrl.inputAriaLabel);
721 }
722 }, 10);
723};
724
725/**
726 * Handles the keydown event on the input element: by default <enter> appends
727 * the buffer to the chip list, while backspace removes the last chip in the
728 * list if the current buffer is empty.
729 * @param {jQuery.Event|KeyboardEvent} event
730 */
731MdChipsCtrl.prototype.inputKeydown = function(event) {
732 var chipBuffer = this.getChipBuffer();
733
734 // If we have an autocomplete, and it handled the event, we have nothing to do
735 if (this.autocompleteCtrl && event.isDefaultPrevented && event.isDefaultPrevented()) {
736 return;
737 }
738
739 if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) {
740 // Only select and focus the previous chip, if the current caret position of the
741 // input element is at the beginning.
742 if (this.getCursorPosition(event.target) !== 0) {
743 return;
744 }
745
746 event.preventDefault();
747 event.stopPropagation();
748
749 if (this.items.length) {
750 this.selectAndFocusChipSafe(this.items.length - 1);
751 }
752
753 return;
754 }
755
756 // By default <enter> appends the buffer to the chip list.
757 if (!this.separatorKeys || this.separatorKeys.length < 1) {
758 this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER];
759 }
760
761 // Support additional separator key codes in an array of `md-separator-keys`.
762 if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
763 if ((this.autocompleteCtrl && this.requireMatch) || !chipBuffer) return;
764 event.preventDefault();
765
766 // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
767 if (this.hasMaxChipsReached()) return;
768
769 this.appendChip(chipBuffer.trim());
770 this.resetChipBuffer();
771
772 return false;
773 }
774};
775
776/**
777 * Returns the cursor position of the specified input element.
778 * @param {HTMLInputElement} element relevant input element
779 * @returns {Number} Cursor Position of the input.
780 */
781MdChipsCtrl.prototype.getCursorPosition = function(element) {
782 /*
783 * Figure out whether the current input for the chips buffer is valid for using
784 * the selectionStart / end property to retrieve the cursor position.
785 * Some browsers do not allow the use of those attributes, on different input types.
786 */
787 try {
788 if (element.selectionStart === element.selectionEnd) {
789 return element.selectionStart;
790 }
791 } catch (e) {
792 if (!element.value) {
793 return 0;
794 }
795 }
796};
797
798
799/**
800 * Updates the content of the chip at given index
801 * @param {number} chipIndex
802 * @param {string} chipContents
803 */
804MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents) {
805 if (chipIndex >= 0 && chipIndex < this.items.length) {
806 this.items[chipIndex] = chipContents;
807 this.updateNgModel(true);
808 }
809};
810
811
812/**
813 * @return {boolean} true if a chip is currently being edited. False otherwise.
814 */
815MdChipsCtrl.prototype.isEditingChip = function() {
816 return !!this.$element[0].querySelector('._md-chip-editing');
817};
818
819/**
820 * @param {string|Object} chip contents of a single chip
821 * @returns {boolean} true if the chip is an Object, false otherwise.
822 * @private
823 */
824MdChipsCtrl.prototype._isChipObject = function(chip) {
825 return angular.isObject(chip);
826};
827
828/**
829 * @returns {boolean} true if chips can be removed, false otherwise.
830 */
831MdChipsCtrl.prototype.isRemovable = function() {
832 // Return false if we have static chips
833 if (!this.ngModelCtrl) {
834 return false;
835 }
836
837 return this.readonly ? this.removable :
838 angular.isDefined(this.removable) ? this.removable : true;
839};
840
841/**
842 * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow
843 * keys switch which chip is active.
844 * @param {KeyboardEvent} event
845 */
846MdChipsCtrl.prototype.chipKeydown = function (event) {
847 if (this.getChipBuffer()) return;
848 if (this.isEditingChip()) return;
849
850 switch (event.keyCode) {
851 case this.$mdConstant.KEY_CODE.BACKSPACE:
852 case this.$mdConstant.KEY_CODE.DELETE:
853 if (this.selectedChip < 0) return;
854 event.preventDefault();
855 // Cancel the delete action only after the event cancel. Otherwise the page will go back.
856 if (!this.isRemovable()) return;
857 this.removeAndSelectAdjacentChip(this.selectedChip, event);
858 break;
859 case this.$mdConstant.KEY_CODE.LEFT_ARROW:
860 event.preventDefault();
861 // By default, allow selection of -1 which will focus the input; if we're readonly, don't go
862 // below 0.
863 if (this.selectedChip < 0 || (this.readonly && this.selectedChip === 0)) {
864 this.selectedChip = this.items.length;
865 }
866 if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1);
867 break;
868 case this.$mdConstant.KEY_CODE.RIGHT_ARROW:
869 event.preventDefault();
870 this.selectAndFocusChipSafe(this.selectedChip + 1);
871 break;
872 case this.$mdConstant.KEY_CODE.ESCAPE:
873 case this.$mdConstant.KEY_CODE.TAB:
874 if (this.selectedChip < 0) return;
875 event.preventDefault();
876 this.onFocus();
877 break;
878 }
879};
880
881/**
882 * Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder`
883 * when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used
884 * always.
885 * @returns {string}
886 */
887MdChipsCtrl.prototype.getPlaceholder = function() {
888 // Allow `secondary-placeholder` to be blank.
889 var useSecondary = (this.items && this.items.length &&
890 (this.secondaryPlaceholder === '' || this.secondaryPlaceholder));
891 return useSecondary ? this.secondaryPlaceholder : this.placeholder;
892};
893
894/**
895 * Removes chip at {@code index} and selects the adjacent chip.
896 * @param {number} index adjacent chip to select
897 * @param {Event=} event
898 */
899MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index, event) {
900 var self = this;
901 var selIndex = self.getAdjacentChipIndex(index);
902 var wrap = this.$element[0].querySelector('md-chips-wrap');
903 var chip = this.$element[0].querySelector('md-chip[index="' + index + '"]');
904
905 self.removeChip(index, event);
906
907 // The double-timeout is currently necessary to ensure that the DOM has finalized and the select()
908 // will find the proper chip since the selection is index-based.
909 //
910 // TODO: Investigate calling from within chip $scope.$on('$destroy') to reduce/remove timeouts
911 self.$timeout(function() {
912 self.$timeout(function() {
913 self.selectAndFocusChipSafe(selIndex);
914 });
915 });
916};
917
918/**
919 * Sets the selected chip index to -1.
920 */
921MdChipsCtrl.prototype.resetSelectedChip = function() {
922 this.selectedChip = -1;
923 this.ariaTabIndex = null;
924};
925
926/**
927 * Gets the index of an adjacent chip to select after deletion. Adjacency is
928 * determined as the next chip in the list, unless the target chip is the
929 * last in the list, then it is the chip immediately preceding the target. If
930 * there is only one item in the list, -1 is returned (select none).
931 * The number returned is the index to select AFTER the target has been removed.
932 * If the current chip is not selected, then -1 is returned to select none.
933 * @param {number} index
934 * @returns {number}
935 */
936MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
937 var len = this.items.length - 1;
938 return (len === 0) ? -1 :
939 (index === len) ? index - 1 : index;
940};
941
942/**
943 * Append the contents of the buffer to the chip list. This method will first
944 * call out to the md-transform-chip method, if provided.
945 * @param {string} newChip chip buffer contents that will be used to create the new chip
946 */
947MdChipsCtrl.prototype.appendChip = function(newChip) {
948 this.shouldFocusLastChip = !this.addOnBlur;
949 if (this.useTransformChip && this.transformChip) {
950 var transformedChip = this.transformChip({'$chip': newChip});
951
952 // Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
953 // they want the string version.
954 if (angular.isDefined(transformedChip)) {
955 newChip = transformedChip;
956 }
957 }
958
959 // If items contains an identical object to newChip, do not append
960 if (angular.isObject(newChip)) {
961 var identical = this.items.some(function(item) {
962 return angular.equals(newChip, item);
963 });
964 if (identical) return;
965 }
966
967 // Check for a null (but not undefined), or existing chip and cancel appending
968 if (newChip == null || this.items.indexOf(newChip) + 1) return;
969
970 // Append the new chip onto our list
971 var length = this.items.push(newChip);
972 var index = length - 1;
973
974 this.updateNgModel();
975
976 // Tell screen reader users that the chip was successfully added.
977 // TODO add a way for developers to specify which field of the object should be announced here.
978 var chipContent = angular.isObject(newChip) ? '' : newChip;
979 this.$mdLiveAnnouncer.announce(chipContent + ' ' + this.addedMessage, 'assertive');
980
981 // If the md-on-add attribute is specified, send a chip addition event
982 if (this.useOnAdd && this.onAdd) {
983 this.onAdd({ '$chip': newChip, '$index': index });
984 }
985};
986
987/**
988 * Sets whether to use the md-transform-chip expression. This expression is
989 * bound to scope and controller in {@code MdChipsDirective} as
990 * {@code transformChip}. Due to the nature of directive scope bindings, the
991 * controller cannot know on its own/from the scope whether an expression was
992 * actually provided.
993 */
994MdChipsCtrl.prototype.useTransformChipExpression = function() {
995 this.useTransformChip = true;
996};
997
998/**
999 * Sets whether to use the md-on-add expression. This expression is
1000 * bound to scope and controller in {@code MdChipsDirective} as
1001 * {@code onAdd}. Due to the nature of directive scope bindings, the
1002 * controller cannot know on its own/from the scope whether an expression was
1003 * actually provided.
1004 */
1005MdChipsCtrl.prototype.useOnAddExpression = function() {
1006 this.useOnAdd = true;
1007};
1008
1009/**
1010 * Sets whether to use the md-on-remove expression. This expression is
1011 * bound to scope and controller in {@code MdChipsDirective} as
1012 * {@code onRemove}. Due to the nature of directive scope bindings, the
1013 * controller cannot know on its own/from the scope whether an expression was
1014 * actually provided.
1015 */
1016MdChipsCtrl.prototype.useOnRemoveExpression = function() {
1017 this.useOnRemove = true;
1018};
1019
1020/**
1021 * Sets whether to use the md-on-select expression. This expression is
1022 * bound to scope and controller in {@code MdChipsDirective} as
1023 * {@code onSelect}. Due to the nature of directive scope bindings, the
1024 * controller cannot know on its own/from the scope whether an expression was
1025 * actually provided.
1026 */
1027MdChipsCtrl.prototype.useOnSelectExpression = function() {
1028 this.useOnSelect = true;
1029};
1030
1031/**
1032 * Gets the input buffer. The input buffer can be the model bound to the
1033 * default input item {@code this.chipBuffer}, the {@code selectedItem}
1034 * model of an {@code md-autocomplete}, or, through some magic, the model
1035 * bound to any input or text area element found within a
1036 * {@code md-input-container} element.
1037 * @return {string} the input buffer
1038 */
1039MdChipsCtrl.prototype.getChipBuffer = function() {
1040 var chipBuffer = !this.userInputElement ? this.chipBuffer :
1041 this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
1042 this.userInputElement[0].value;
1043
1044 // Ensure that the chip buffer is always a string. For example, the input element buffer
1045 // might be falsy.
1046 return angular.isString(chipBuffer) ? chipBuffer : '';
1047};
1048
1049/**
1050 * Resets the input buffer for either the internal input or user provided input element.
1051 */
1052MdChipsCtrl.prototype.resetChipBuffer = function() {
1053 if (this.userInputElement) {
1054 if (this.userInputNgModelCtrl) {
1055 this.userInputNgModelCtrl.$setViewValue('');
1056 this.userInputNgModelCtrl.$render();
1057 } else {
1058 this.userInputElement[0].value = '';
1059 }
1060 } else {
1061 this.chipBuffer = '';
1062 }
1063};
1064
1065/**
1066 * @returns {boolean} true if the max chips limit has been reached, false otherwise.
1067 */
1068MdChipsCtrl.prototype.hasMaxChipsReached = function() {
1069 if (angular.isString(this.maxChips)) {
1070 this.maxChips = parseInt(this.maxChips, 10) || 0;
1071 }
1072
1073 return this.maxChips > 0 && this.items.length >= this.maxChips;
1074};
1075
1076/**
1077 * Updates the validity properties for the ngModel.
1078 *
1079 * TODO add the md-max-chips validator to this.ngModelCtrl.validators so that the validation will
1080 * be performed automatically.
1081 */
1082MdChipsCtrl.prototype.validateModel = function() {
1083 this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
1084 this.ngModelCtrl.$validate(); // rerun any registered validators
1085};
1086
1087/**
1088 * Function to handle updating the model, validation, and change notification when a chip
1089 * is added, removed, or changed.
1090 * @param {boolean=} skipValidation true to skip calling validateModel()
1091 */
1092MdChipsCtrl.prototype.updateNgModel = function(skipValidation) {
1093 if (!skipValidation) {
1094 this.validateModel();
1095 }
1096 // This will trigger ng-change to fire, even in cases where $setViewValue() would not.
1097 angular.forEach(this.ngModelCtrl.$viewChangeListeners, function(listener) {
1098 try {
1099 listener();
1100 } catch (e) {
1101 this.$exceptionHandler(e);
1102 }
1103 });
1104};
1105
1106/**
1107 * Removes the chip at the given index.
1108 * @param {number} index of chip to remove
1109 * @param {Event=} event optionally passed to the onRemove callback
1110 */
1111MdChipsCtrl.prototype.removeChip = function(index, event) {
1112 var removed = this.items.splice(index, 1);
1113
1114 this.updateNgModel();
1115 this.ngModelCtrl.$setDirty();
1116
1117 // Tell screen reader users that the chip was successfully removed.
1118 // TODO add a way for developers to specify which field of the object should be announced here.
1119 var chipContent = angular.isObject(removed[0]) ? '' : removed[0];
1120 this.$mdLiveAnnouncer.announce(chipContent + ' ' + this.removedMessage, 'assertive');
1121
1122 if (removed && removed.length && this.useOnRemove && this.onRemove) {
1123 this.onRemove({ '$chip': removed[0], '$index': index, '$event': event });
1124 }
1125};
1126
1127/**
1128 * @param {number} index location of chip to remove
1129 * @param {Event=} $event
1130 */
1131MdChipsCtrl.prototype.removeChipAndFocusInput = function (index, $event) {
1132 this.removeChip(index, $event);
1133
1134 if (this.autocompleteCtrl) {
1135 // Always hide the autocomplete dropdown before focusing the autocomplete input.
1136 // Wait for the input to move horizontally, because the chip was removed.
1137 // This can lead to an incorrect dropdown position.
1138 this.autocompleteCtrl.hidden = true;
1139 this.$mdUtil.nextTick(this.onFocus.bind(this));
1140 } else {
1141 this.onFocus();
1142 }
1143
1144};
1145/**
1146 * Selects the chip at `index`,
1147 * @param {number} index location of chip to select and focus
1148 */
1149MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) {
1150 // If we have no chips, or are asked to select a chip before the first, just focus the input
1151 if (!this.items.length || index === -1) {
1152 return this.focusInput();
1153 }
1154
1155 // If we are asked to select a chip greater than the number of chips...
1156 if (index >= this.items.length) {
1157 if (this.readonly) {
1158 // If we are readonly, jump back to the start (because we have no input)
1159 index = 0;
1160 } else {
1161 // If we are not readonly, we should attempt to focus the input
1162 return this.onFocus();
1163 }
1164 }
1165
1166 index = Math.max(index, 0);
1167 index = Math.min(index, this.items.length - 1);
1168
1169 this.selectChip(index);
1170 this.focusChip(index);
1171};
1172
1173/**
1174 * Focus last chip, then focus the input. This is needed for screen reader support.
1175 */
1176MdChipsCtrl.prototype.focusLastChipThenInput = function() {
1177 var ctrl = this;
1178
1179 ctrl.shouldFocusLastChip = false;
1180
1181 ctrl.focusChip(this.items.length - 1);
1182
1183 ctrl.$timeout(function() {
1184 ctrl.focusInput();
1185 }, ctrl.chipAppendDelay);
1186};
1187
1188/**
1189 * Focus the input element.
1190 */
1191MdChipsCtrl.prototype.focusInput = function() {
1192 this.selectChip(-1);
1193 this.onFocus();
1194};
1195
1196/**
1197 * Marks the chip at the given index as selected.
1198 * @param {number} index location of chip to select
1199 */
1200MdChipsCtrl.prototype.selectChip = function(index) {
1201 if (index >= -1 && index <= this.items.length) {
1202 this.selectedChip = index;
1203
1204 // Fire the onSelect if provided
1205 if (this.useOnSelect && this.onSelect) {
1206 this.onSelect({'$chip': this.items[index] });
1207 }
1208 } else {
1209 this.$log.warn('Selected Chip index out of bounds; ignoring.');
1210 }
1211};
1212
1213/**
1214 * Call {@code focus()} on the chip at {@code index}
1215 * @param {number} index location of chip to focus
1216 */
1217MdChipsCtrl.prototype.focusChip = function(index) {
1218 var chipContent = this.$element[0].querySelector(
1219 'md-chip[index="' + index + '"] .md-chip-content'
1220 );
1221
1222 this.ariaTabIndex = index;
1223
1224 chipContent.focus();
1225};
1226
1227/**
1228 * Configures the required interactions with the ngModel Controller.
1229 * Specifically, set {@code this.items} to the {@code NgModelController#$viewValue}.
1230 * @param {NgModelController} ngModelCtrl
1231 */
1232MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
1233 this.ngModelCtrl = ngModelCtrl;
1234
1235 var self = this;
1236
1237 // in chips the meaning of $isEmpty changes
1238 ngModelCtrl.$isEmpty = function(value) {
1239 return !value || value.length === 0;
1240 };
1241
1242 ngModelCtrl.$render = function() {
1243 // model is updated. do something.
1244 self.items = self.ngModelCtrl.$viewValue;
1245 };
1246};
1247
1248MdChipsCtrl.prototype.onFocus = function () {
1249 var input = this.$element[0].querySelector('input');
1250 input && input.focus();
1251 this.resetSelectedChip();
1252};
1253
1254MdChipsCtrl.prototype.onInputFocus = function () {
1255 this.inputHasFocus = true;
1256
1257 // Make sure we have the appropriate ARIA attributes
1258 this.setupInputAria();
1259
1260 // Make sure we don't have any chips selected
1261 this.resetSelectedChip();
1262};
1263
1264MdChipsCtrl.prototype.onInputBlur = function () {
1265 this.inputHasFocus = false;
1266
1267 if (this.shouldAddOnBlur()) {
1268 this.appendChip(this.getChipBuffer().trim());
1269 this.resetChipBuffer();
1270 }
1271};
1272
1273/**
1274 * Configure event bindings on input element.
1275 * @param {angular.element} inputElement
1276 */
1277MdChipsCtrl.prototype.configureInput = function configureInput(inputElement) {
1278 // Find the NgModelCtrl for the input element
1279 var ngModelCtrl = inputElement.controller('ngModel');
1280 var ctrl = this;
1281
1282 if (ngModelCtrl) {
1283
1284 // sync touched-state from inner input to chips-element
1285 this.deRegister.push(
1286 this.$scope.$watch(
1287 function() {
1288 return ngModelCtrl.$touched;
1289 },
1290 function(isTouched) {
1291 isTouched && ctrl.ngModelCtrl.$setTouched();
1292 }
1293 )
1294 );
1295
1296 // sync dirty-state from inner input to chips-element
1297 this.deRegister.push(
1298 this.$scope.$watch(
1299 function() {
1300 return ngModelCtrl.$dirty;
1301 },
1302 function(isDirty) {
1303 isDirty && ctrl.ngModelCtrl.$setDirty();
1304 }
1305 )
1306 );
1307 }
1308};
1309
1310/**
1311 * Configure event bindings on a user-provided input element.
1312 * @param {angular.element} inputElement
1313 */
1314MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
1315 this.userInputElement = inputElement;
1316
1317 // Find the NgModelCtrl for the input element
1318 var ngModelCtrl = inputElement.controller('ngModel');
1319 // `.controller` will look in the parent as well.
1320 if (ngModelCtrl !== this.ngModelCtrl) {
1321 this.userInputNgModelCtrl = ngModelCtrl;
1322 }
1323
1324 var scope = this.$scope;
1325 var ctrl = this;
1326
1327 // Run all of the events using evalAsync because a focus may fire a blur in the same digest loop
1328 var scopeApplyFn = function(event, fn) {
1329 scope.$evalAsync(angular.bind(ctrl, fn, event));
1330 };
1331
1332 // Bind to keydown and focus events of input
1333 inputElement
1334 .attr({ tabindex: 0 })
1335 .on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown); })
1336 .on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus); })
1337 .on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur); });
1338};
1339
1340/**
1341 * @param {MdAutocompleteCtrl} ctrl controller from the autocomplete component
1342 */
1343MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
1344 if (ctrl) {
1345 this.autocompleteCtrl = ctrl;
1346 // Update the default container empty hint when we're inside of an autocomplete.
1347 if (!this.$element.attr('container-empty-hint')) {
1348 this.containerEmptyHint = 'Chips container with autocompletion. Enter the text area, ' +
1349 'type text to search, and then use the up and down arrow keys to select an option. ' +
1350 'Press enter to add the selected option as a chip.';
1351 this.setupWrapperAria();
1352 }
1353
1354 ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
1355 if (item) {
1356 // Only append the chip and reset the chip buffer if the max chips limit isn't reached.
1357 if (this.hasMaxChipsReached()) return;
1358
1359 this.appendChip(item);
1360 this.resetChipBuffer();
1361 }
1362 }));
1363
1364 this.$element.find('input')
1365 .on('focus',angular.bind(this, this.onInputFocus))
1366 .on('blur', angular.bind(this, this.onInputBlur));
1367 }
1368};
1369
1370/**
1371 * @returns {boolean} Whether the current chip buffer should be added on input blur or not.
1372 */
1373MdChipsCtrl.prototype.shouldAddOnBlur = function() {
1374
1375 // Update the custom ngModel validators from the chips component.
1376 this.validateModel();
1377
1378 var chipBuffer = this.getChipBuffer().trim();
1379 // If the model value is empty and required is set on the element, then the model will be invalid.
1380 // In that case, we still want to allow adding the chip. The main (but not only) case we want
1381 // to disallow is adding a chip on blur when md-max-chips validation fails.
1382 var isModelValid = this.ngModelCtrl.$isEmpty(this.ngModelCtrl.$modelValue) ||
1383 this.ngModelCtrl.$valid;
1384 var isAutocompleteShowing = this.autocompleteCtrl && !this.autocompleteCtrl.hidden;
1385
1386 if (this.userInputNgModelCtrl) {
1387 isModelValid = isModelValid && this.userInputNgModelCtrl.$valid;
1388 }
1389
1390 return this.addOnBlur && !this.requireMatch && chipBuffer && isModelValid &&
1391 !isAutocompleteShowing;
1392};
1393
1394/**
1395 * @returns {boolean} true if the input or a chip is focused. False otherwise.
1396 */
1397MdChipsCtrl.prototype.hasFocus = function () {
1398 return this.inputHasFocus || this.selectedChip >= 0;
1399};
1400
1401/**
1402 * @param {number} index location of content id
1403 * @returns {number} unique id for the aria-owns attribute
1404 */
1405MdChipsCtrl.prototype.contentIdFor = function(index) {
1406 return this.contentIds[index];
1407};
1408
1409
1410 MdChips['$inject'] = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout", "$$mdSvgRegistry"];angular
1411 .module('material.components.chips')
1412 .directive('mdChips', MdChips);
1413
1414 /**
1415 * @ngdoc directive
1416 * @name mdChips
1417 * @module material.components.chips
1418 *
1419 * @description
1420 * `<md-chips>` is an input component for building lists of strings or objects. The list items are
1421 * displayed as 'chips'. This component can make use of an `<input>` element or an
1422 * `<md-autocomplete>` element.
1423 *
1424 * ### Custom templates
1425 * A custom template may be provided to render the content of each chip. This is achieved by
1426 * specifying an `<md-chip-template>` element containing the custom content as a child of
1427 * `<md-chips>`.
1428 *
1429 * Note: Any attributes on
1430 * `<md-chip-template>` will be dropped as only the innerHTML is used for the chip template. The
1431 * variables `$chip` and `$index` are available in the scope of `<md-chip-template>`, representing
1432 * the chip object and its index in the list of chips, respectively.
1433 * To override the chip delete control, include an element (ideally a button) with the attribute
1434 * `md-chip-remove`. A click listener to remove the chip will be added automatically. The element
1435 * is also placed as a sibling to the chip content (on which there are also click listeners) to
1436 * avoid a nested ng-click situation.
1437 *
1438 * <!-- Note: We no longer want to include this in the site docs; but it should remain here for
1439 * future developers and those looking at the documentation.
1440 *
1441 * <h3> Pending Features </h3>
1442 * <ul style="padding-left:20px;">
1443 *
1444 * <ul>Style
1445 * <li>Colors for hover, press states (ripple?).</li>
1446 * </ul>
1447 *
1448 * <ul>Validation
1449 * <li>allow a validation callback</li>
1450 * <li>highlighting style for invalid chips</li>
1451 * </ul>
1452 *
1453 * <ul>Item mutation
1454 * <li>Support `
1455 * <md-chip-edit>` template, show/hide the edit element on tap/click? double tap/double
1456 * click?
1457 * </li>
1458 * </ul>
1459 *
1460 * <ul>Truncation and Disambiguation (?)
1461 * <li>Truncate chip text where possible, but do not truncate entries such that two are
1462 * indistinguishable.</li>
1463 * </ul>
1464 *
1465 * <ul>Drag and Drop
1466 * <li>Drag and drop chips between related `<md-chips>` elements.
1467 * </li>
1468 * </ul>
1469 * </ul>
1470 *
1471 * //-->
1472 *
1473 * Sometimes developers want to limit the amount of possible chips.<br/>
1474 * You can specify the maximum amount of chips by using the following markup.
1475 *
1476 * <hljs lang="html">
1477 * <md-chips
1478 * ng-model="myItems"
1479 * placeholder="Add an item"
1480 * md-max-chips="5">
1481 * </md-chips>
1482 * </hljs>
1483 *
1484 * In some cases, you have an autocomplete inside of the `md-chips`.<br/>
1485 * When the maximum amount of chips has been reached, you can also disable the autocomplete
1486 * selection.<br/>
1487 * Here is an example markup.
1488 *
1489 * <hljs lang="html">
1490 * <md-chips ng-model="myItems" md-max-chips="5">
1491 * <md-autocomplete ng-hide="myItems.length > 5" ...></md-autocomplete>
1492 * </md-chips>
1493 * </hljs>
1494 *
1495 * ### Accessibility
1496 *
1497 * The `md-chips` component supports keyboard and screen reader users since Version 1.1.2. In
1498 * order to achieve this, we modified the chips behavior to select newly appended chips for
1499 * `300ms` before re-focusing the input and allowing the user to type.
1500 *
1501 * For most users, this delay is small enough that it will not be noticeable but allows certain
1502 * screen readers to function properly (JAWS and NVDA in particular).
1503 *
1504 * We introduced a new `md-chip-append-delay` option to allow developers to better control this
1505 * behavior.
1506 *
1507 * Please refer to the documentation of this option (below) for more information.
1508 *
1509 * @param {expression} ng-model Assignable AngularJS expression to be data-bound to the list of
1510 * chips. The expression should evaluate to a `string` or `Object` Array. The type of this
1511 * array should align with the return value of `md-transform-chip`.
1512 * @param {expression=} ng-change AngularJS expression to be executed on chip addition, removal,
1513 * or content change.
1514 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
1515 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
1516 * displayed when there is at least one item in the list
1517 * @param {boolean=} md-removable Enables or disables the deletion of chips through the
1518 * removal icon or the Delete/Backspace key. Defaults to true.
1519 * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding
1520 * the input and delete buttons. If no `ng-model` is provided, the chips will automatically be
1521 * marked as readonly.<br/><br/>
1522 * When `md-removable` is not defined, the `md-remove` behavior will be overwritten and
1523 * disabled.
1524 * @param {boolean=} md-enable-chip-edit Set this to `"true"` to enable editing of chip contents.
1525 * The user can go into edit mode by pressing the `space` or `enter` keys, or by double
1526 * clicking on the chip. Chip editing is only supported for chips using the basic template.
1527 * **Note:** This attribute is only evaluated once; it is not watched.
1528 * @param {boolean=} ng-required Whether ng-model is allowed to be empty or not.
1529 * @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
1530 * <br/><br/>The validation property `md-max-chips` can be used when the max chips
1531 * amount is reached.
1532 * @param {boolean=} md-add-on-blur When set to `"true"`, the remaining text inside of the input
1533 * will be converted into a new chip on blur.
1534 * **Note:** This attribute is only evaluated once; it is not watched.
1535 * @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when
1536 * called expects one of the following return values:
1537 * - an object representing the `$chip` input string
1538 * - `undefined` to simply add the `$chip` input string, or
1539 * - `null` to prevent the chip from being appended
1540 * @param {expression=} md-on-add An expression which will be called when a chip has been
1541 * added with `$chip` and `$index` available as parameters.
1542 * @param {expression=} md-on-remove An expression which will be called when a chip has been
1543 * removed with `$chip`, `$index`, and `$event` available as parameters.
1544 * @param {expression=} md-on-select An expression which will be called when a chip is selected.
1545 * @param {boolean=} md-require-match If true, and the chips template contains an autocomplete,
1546 * only allow selection of pre-defined chips (i.e. you cannot add new ones).
1547 * @param {string=} md-input-class This class will be applied to the child input for custom
1548 * styling. If you are using an `md-autocomplete`, then you need to put this attribute on the
1549 * `md-autocomplete` rather than the `md-chips`.
1550 * @param {string=} input-aria-describedby A space-separated list of element IDs. This should
1551 * contain the IDs of any elements that describe this autocomplete. Screen readers will read
1552 * the content of these elements at the end of announcing that the chips input has been
1553 * selected and describing its current state. The descriptive elements do not need to be
1554 * visible on the page.
1555 * @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use
1556 * case is that this would contain the ID of a `<label>` element that is associated with these
1557 * chips.<br><br>
1558 * For `<label id="state">US State</label>`, you would set this to
1559 * `input-aria-labelledby="state"`.
1560 * @param {string=} input-aria-label A string read by screen readers to identify the input.
1561 * For static chips, this will be applied to the chips container.
1562 * @param {string=} container-hint A string read by screen readers informing users of how to
1563 * navigate the chips when there are chips. Only applies when `ng-model` is defined.
1564 * @param {string=} container-empty-hint A string read by screen readers informing users of how to
1565 * add chips when there are no chips. You will want to use this to override the default when
1566 * in a non-English locale. Only applies when `ng-model` is defined.
1567 * @param {string=} delete-hint A string read by screen readers instructing users that pressing
1568 * the delete key will remove the chip. You will want to use this to override the default when
1569 * in a non-English locale.
1570 * @param {string=} delete-button-label Text for the `aria-label` of the button with the
1571 * `md-chip-remove` class. If the chip is an Object, then this will be the only text in the
1572 * label. Otherwise, this is prepended to the string representation of the chip. Defaults to
1573 * "Remove", which would be "Remove Apple" for a chip that contained the string "Apple".
1574 * You will want to use this to override the default when in a non-English locale.
1575 * @param {string=} md-removed-message Screen readers will announce this message following the
1576 * chips contents. The default is `"removed"`. If a chip with the content of "Apple" was
1577 * removed, the screen reader would read "Apple removed". You will want to use this to override
1578 * the default when in a non-English locale.
1579 * @param {string=} md-added-message Screen readers will announce this message following the
1580 * chips contents. The default is `"added"`. If a chip with the content of "Apple" was
1581 * created, the screen reader would read "Apple added". You will want to use this to override
1582 * the default when in a non-English locale.
1583 * @param {expression=} md-separator-keys An array of key codes used to separate chips.
1584 * @param {string=} md-chip-append-delay The number of milliseconds that the component will select
1585 * a newly appended chip before allowing a user to type into the input. This is **necessary**
1586 * for keyboard accessibility for screen readers. It defaults to 300ms and any number less than
1587 * 300 can cause issues with screen readers (particularly JAWS and sometimes NVDA).
1588 *
1589 * _Available since Version 1.1.2._
1590 *
1591 * **Note:** You can safely set this to `0` in one of the following two instances:
1592 *
1593 * 1. You are targeting an iOS or Safari-only application (where users would use VoiceOver) or
1594 * only ChromeVox users.
1595 *
1596 * 2. If you have utilized the `md-separator-keys` to disable the `enter` keystroke in
1597 * favor of another one (such as `,` or `;`).
1598 *
1599 * @usage
1600 * <hljs lang="html">
1601 * <md-chips
1602 * ng-model="myItems"
1603 * placeholder="Add an item"
1604 * readonly="isReadOnly">
1605 * </md-chips>
1606 * </hljs>
1607 *
1608 * <h3>Validation</h3>
1609 * When using [ngMessages](https://docs.angularjs.org/api/ngMessages), you can show errors based
1610 * on our custom validators.
1611 * <hljs lang="html">
1612 * <form name="userForm">
1613 * <md-chips
1614 * name="fruits"
1615 * ng-model="myItems"
1616 * placeholder="Add an item"
1617 * md-max-chips="5">
1618 * </md-chips>
1619 * <div ng-messages="userForm.fruits.$error" ng-if="userForm.$dirty">
1620 * <div ng-message="md-max-chips">You reached the maximum amount of chips</div>
1621 * </div>
1622 * </form>
1623 * </hljs>
1624 *
1625 */
1626
1627 // TODO add a way for developers to specify which field of the object should used in the
1628 // aria-label.
1629 var MD_CHIPS_TEMPLATE = '\
1630 <md-chips-wrap\
1631 id="{{$mdChipsCtrl.wrapperId}}"\
1632 tabindex="{{$mdChipsCtrl.readonly ? 0 : -1}}"\
1633 ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
1634 ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \
1635 \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly,\
1636 \'md-removable\': $mdChipsCtrl.isRemovable() }"\
1637 class="md-chips">\
1638 <md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
1639 index="{{$index}}" \
1640 ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly}">\
1641 <div class="md-chip-content"\
1642 tabindex="{{$mdChipsCtrl.ariaTabIndex === $index ? 0 : -1}}"\
1643 id="{{$mdChipsCtrl.contentIdFor($index)}}"\
1644 role="option"\
1645 aria-selected="{{$mdChipsCtrl.selectedChip === $index}}"\
1646 aria-setsize="{{$mdChipsCtrl.items.length}}"\
1647 aria-posinset="{{$index+1}}"\
1648 ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\
1649 aria-label="{{$mdChipsCtrl._isChipObject($chip) ? \'\' : $chip + \'. \'}}{{$mdChipsCtrl.isRemovable() ? \'\' + $mdChipsCtrl.deleteHint : \'\'}}" \
1650 ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\
1651 md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\
1652 <div ng-if="$mdChipsCtrl.isRemovable()"\
1653 class="md-chip-remove-container"\
1654 tabindex="-1"\
1655 md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\
1656 </md-chip>\
1657 <div class="md-chip-input-container" ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl">\
1658 <div md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\
1659 </div>\
1660 </md-chips-wrap>';
1661
1662 var CHIP_INPUT_TEMPLATE = '\
1663 <input\
1664 class="md-input{{ $mdChipsCtrl.inputClass ? \' \' + $mdChipsCtrl.inputClass: \'\'}}"\
1665 tabindex="0"\
1666 aria-label="{{$mdChipsCtrl.inputAriaLabel}}"\
1667 placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
1668 ng-model="$mdChipsCtrl.chipBuffer"\
1669 ng-focus="$mdChipsCtrl.onInputFocus()"\
1670 ng-blur="$mdChipsCtrl.onInputBlur()"\
1671 ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
1672
1673 var CHIP_DEFAULT_TEMPLATE = '\
1674 <span>{{$chip}}</span>';
1675
1676 var CHIP_REMOVE_TEMPLATE = '\
1677 <button\
1678 class="md-chip-remove"\
1679 ng-if="$mdChipsCtrl.isRemovable()"\
1680 ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index, $event)"\
1681 type="button"\
1682 tabindex="-1"\
1683 aria-label="{{$mdChipsCtrl.deleteButtonLabel}}{{$mdChipsCtrl._isChipObject($chip) ? \'\' : \' \' + $chip}}">\
1684 <md-icon md-svg-src="{{$mdChipsCtrl.mdCloseIcon}}" aria-hidden="true"></md-icon>\
1685 </button>';
1686
1687 /**
1688 * MDChips Directive Definition
1689 */
1690 function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout, $$mdSvgRegistry) {
1691 // Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
1692 var templates = getTemplates();
1693
1694 return {
1695 template: function(element, attrs) {
1696 // Clone the element into an attribute. By prepending the attribute
1697 // name with '$', AngularJS won't write it into the DOM. The cloned
1698 // element propagates to the link function via the attrs argument,
1699 // where various contained-elements can be consumed.
1700 attrs['$mdUserTemplate'] = element.clone();
1701 return templates.chips;
1702 },
1703 require: ['mdChips'],
1704 restrict: 'E',
1705 controller: 'MdChipsCtrl',
1706 controllerAs: '$mdChipsCtrl',
1707 bindToController: true,
1708 compile: compile,
1709 scope: {
1710 readonly: '=?readonly',
1711 removable: '=?mdRemovable',
1712 placeholder: '@?',
1713 secondaryPlaceholder: '@?',
1714 maxChips: '@?mdMaxChips',
1715 transformChip: '&mdTransformChip',
1716 onAdd: '&?mdOnAdd',
1717 onRemove: '&?mdOnRemove',
1718 addedMessage: '@?mdAddedMessage',
1719 removedMessage: '@?mdRemovedMessage',
1720 onSelect: '&?mdOnSelect',
1721 inputClass: '@?mdInputClass',
1722 inputAriaDescribedBy: '@?inputAriaDescribedby',
1723 inputAriaLabelledBy: '@?inputAriaLabelledby',
1724 inputAriaLabel: '@?',
1725 containerHint: '@?',
1726 containerEmptyHint: '@?',
1727 deleteHint: '@?',
1728 deleteButtonLabel: '@?',
1729 separatorKeys: '=?mdSeparatorKeys',
1730 requireMatch: '=?mdRequireMatch',
1731 chipAppendDelayString: '@?mdChipAppendDelay',
1732 ngChange: '&?'
1733 }
1734 };
1735
1736 /**
1737 * Builds the final template for `md-chips` and returns the postLink function.
1738 *
1739 * Building the template involves 3 key components:
1740 * static chips
1741 * chip template
1742 * input control
1743 *
1744 * If no `ng-model` is provided, only the static chip work needs to be done.
1745 *
1746 * If no user-passed `md-chip-template` exists, the default template is used. This resulting
1747 * template is appended to the chip content element.
1748 *
1749 * The remove button may be overridden by passing an element with an md-chip-remove attribute.
1750 *
1751 * If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
1752 * transclusion later. The transclusion happens in `postLink` as the parent scope is required.
1753 * If no user input is provided, a default one is appended to the input container node in the
1754 * template.
1755 *
1756 * Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
1757 * transclusion in the `postLink` function.
1758 *
1759 *
1760 * @param element
1761 * @param attr
1762 * @returns {Function}
1763 */
1764 function compile(element, attr) {
1765 // Grab the user template from attr and reset the attribute to null.
1766 var userTemplate = attr['$mdUserTemplate'];
1767 attr['$mdUserTemplate'] = null;
1768
1769 var chipTemplate = getTemplateByQuery('md-chips>md-chip-template');
1770
1771 var chipRemoveSelector = $mdUtil
1772 .prefixer()
1773 .buildList('md-chip-remove')
1774 .map(function(attr) {
1775 return 'md-chips>*[' + attr + ']';
1776 })
1777 .join(',');
1778
1779 // Set the chip remove, chip contents and chip input templates. The link function will put
1780 // them on the scope for transclusion later.
1781 var chipRemoveTemplate = getTemplateByQuery(chipRemoveSelector) || templates.remove,
1782 chipContentsTemplate = chipTemplate || templates.default,
1783 chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete')
1784 || getTemplateByQuery('md-chips>input')
1785 || templates.input,
1786 staticChips = userTemplate.find('md-chip');
1787
1788 // Warn of malformed template. See #2545
1789 if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
1790 $log.warn('invalid placement of md-chip-remove within md-chip-template.');
1791 }
1792
1793 function getTemplateByQuery (query) {
1794 if (!attr.ngModel) return;
1795 var element = userTemplate[0].querySelector(query);
1796 return element && element.outerHTML;
1797 }
1798
1799 /**
1800 * Configures controller and transcludes.
1801 */
1802 return function postLink(scope, element, attrs, controllers) {
1803 $mdUtil.initOptionalProperties(scope, attr);
1804
1805 $mdTheming(element);
1806 var mdChipsCtrl = controllers[0];
1807 if (chipTemplate) {
1808 // Chip editing functionality assumes we are using the default chip template.
1809 mdChipsCtrl.enableChipEdit = false;
1810 }
1811
1812 mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
1813 mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate;
1814 mdChipsCtrl.chipInputTemplate = chipInputTemplate;
1815
1816 mdChipsCtrl.mdCloseIcon = $$mdSvgRegistry.mdCancel;
1817
1818 element
1819 .attr({ tabindex: -1 })
1820 .on('focus', function () { mdChipsCtrl.onFocus(); })
1821 .on('click', function () {
1822 if (!mdChipsCtrl.readonly && mdChipsCtrl.selectedChip === -1) {
1823 mdChipsCtrl.onFocus();
1824 }
1825 });
1826
1827 if (attr.ngModel) {
1828 mdChipsCtrl.configureNgModel(element.controller('ngModel'));
1829
1830 // If an `md-transform-chip` attribute was set, tell the controller to use the expression
1831 // before appending chips.
1832 if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression();
1833
1834 // If an `md-on-add` attribute was set, tell the controller to use the expression
1835 // when adding chips.
1836 if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression();
1837
1838 // If an `md-on-remove` attribute was set, tell the controller to use the expression
1839 // when removing chips.
1840 if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();
1841
1842 // If an `md-on-select` attribute was set, tell the controller to use the expression
1843 // when selecting chips.
1844 if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();
1845
1846 // The md-autocomplete and input elements won't be compiled until after this directive
1847 // is complete (due to their nested nature). Wait a tick before looking for them to
1848 // configure the controller.
1849 if (chipInputTemplate !== templates.input) {
1850 // The autocomplete will not appear until the readonly attribute is not true (i.e.
1851 // false or undefined), so we have to watch the readonly and then on the next tick
1852 // after the chip transclusion has run, we can configure the autocomplete and user
1853 // input.
1854 scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
1855 if (!readonly) {
1856
1857 $mdUtil.nextTick(function(){
1858
1859 if (chipInputTemplate.indexOf('<md-autocomplete') === 0) {
1860 var autocompleteEl = element.find('md-autocomplete');
1861 mdChipsCtrl.configureAutocomplete(autocompleteEl.controller('mdAutocomplete'));
1862 }
1863
1864 mdChipsCtrl.configureUserInput(element.find('input'));
1865 });
1866 }
1867 });
1868 }
1869
1870 // At the next tick, if we find an input, make sure it has the md-input class
1871 $mdUtil.nextTick(function() {
1872 var input = element.find('input');
1873
1874 if (input) {
1875 mdChipsCtrl.configureInput(input);
1876 input.toggleClass('md-input', true);
1877 }
1878 });
1879 }
1880
1881 // Compile with the parent's scope and prepend any static chips to the wrapper.
1882 if (staticChips.length > 0) {
1883 var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
1884 $timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
1885 }
1886 };
1887 }
1888
1889 function getTemplates() {
1890 return {
1891 chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE),
1892 input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE),
1893 default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE),
1894 remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE)
1895 };
1896 }
1897 }
1898
1899
1900MdContactChipsCtrl['$inject'] = ["$attrs", "$element", "$timeout"];angular
1901 .module('material.components.chips')
1902 .controller('MdContactChipsCtrl', MdContactChipsCtrl);
1903
1904/**
1905 * Controller for the MdContactChips component
1906 * @constructor
1907 */
1908function MdContactChipsCtrl ($attrs, $element, $timeout) {
1909 /** @type {$element} */
1910 this.$element = $element;
1911
1912 /** @type {$attrs} */
1913 this.$attrs = $attrs;
1914
1915 /** @type {Function} */
1916 this.$timeout = $timeout;
1917
1918 /** @type {Object} */
1919 this.selectedItem = null;
1920
1921 /** @type {string} */
1922 this.searchText = '';
1923
1924 /**
1925 * Collection of functions to call to un-register watchers
1926 * @type {Array}
1927 */
1928 this.deRegister = [];
1929
1930 this.init();
1931}
1932
1933MdContactChipsCtrl.prototype.init = function() {
1934 var ctrl = this;
1935 var deRegister = this.deRegister;
1936 var element = this.$element;
1937
1938 // Setup a watcher which manages chips a11y messages and autocomplete aria.
1939 // Timeout required to allow the child elements to be compiled.
1940 this.$timeout(function() {
1941 deRegister.push(
1942 element.find('md-chips').controller('mdChips').$scope.$watchCollection('$mdChipsCtrl.items', function() {
1943 // Make sure our input and wrapper have the correct ARIA attributes
1944 ctrl.setupChipsAria();
1945 ctrl.setupAutocompleteAria();
1946 })
1947 );
1948 });
1949};
1950
1951MdContactChipsCtrl.prototype.setupChipsAria = function() {
1952 var chips = this.$element.find('md-chips');
1953 var chipsCtrl = chips.controller('mdChips');
1954
1955 // Configure MdChipsCtrl
1956 if (this.removedMessage) {
1957 chipsCtrl.removedMessage = this.removedMessage;
1958 }
1959 if (this.containerHint) {
1960 chipsCtrl.containerHint = this.containerHint;
1961 }
1962 if (this.containerEmptyHint) {
1963 // Apply attribute to avoid the hint being overridden by MdChipsCtrl.configureAutocomplete()
1964 chips.attr('container-empty-hint', this.containerEmptyHint);
1965 chipsCtrl.containerEmptyHint = this.containerEmptyHint;
1966 }
1967 if (this.deleteHint) {
1968 chipsCtrl.deleteHint = this.deleteHint;
1969 }
1970 if (this.inputAriaLabel) {
1971 chipsCtrl.inputAriaLabel = this.inputAriaLabel;
1972 }
1973 if (this.inputClass) {
1974 chipsCtrl.inputClass = this.inputClass;
1975 }
1976};
1977
1978MdContactChipsCtrl.prototype.setupAutocompleteAria = function() {
1979 var autocompleteInput = this.$element.find('md-chips-wrap').find('md-autocomplete').find('input');
1980
1981 // Set attributes on the input of the md-autocomplete
1982 if (this.inputAriaDescribedBy) {
1983 autocompleteInput.attr('aria-describedby', this.inputAriaDescribedBy);
1984 }
1985 if (this.inputAriaLabelledBy) {
1986 autocompleteInput.removeAttr('aria-label');
1987 autocompleteInput.attr('aria-labelledby', this.inputAriaLabelledBy);
1988 }
1989};
1990
1991MdContactChipsCtrl.prototype.queryContact = function(searchText) {
1992 return this.contactQuery({'$query': searchText});
1993};
1994
1995MdContactChipsCtrl.prototype.inputKeydown = function(event) {
1996 if (!this.separatorKeys || this.separatorKeys.indexOf(event.keyCode) < 0) {
1997 return;
1998 }
1999
2000 event.stopPropagation();
2001 event.preventDefault();
2002
2003 var autocompleteCtrl = angular.element(event.target).controller('mdAutocomplete');
2004 autocompleteCtrl.select(autocompleteCtrl.index);
2005};
2006
2007MdContactChipsCtrl.prototype.itemName = function(item) {
2008 return item[this.contactName];
2009};
2010
2011/**
2012 * Destructor for cleanup
2013 */
2014MdContactChipsCtrl.prototype.$onDestroy = function $onDestroy() {
2015 var $destroyFn;
2016 while (($destroyFn = this.deRegister.pop())) {
2017 $destroyFn.call(this);
2018 }
2019};
2020
2021
2022MdContactChips['$inject'] = ["$mdTheming", "$mdUtil"];angular
2023 .module('material.components.chips')
2024 .directive('mdContactChips', MdContactChips);
2025
2026/**
2027 * @ngdoc directive
2028 * @name mdContactChips
2029 * @module material.components.chips
2030 *
2031 * @description
2032 * `<md-contact-chips>` is an input component based on `md-chips` and makes use of an
2033 * `md-autocomplete` element. The component allows the caller to supply a query expression which
2034 * returns a list of possible contacts. The user can select one of these and add it to the list of
2035 * chips.
2036 *
2037 * You may also use the <a ng-href="api/directive/mdHighlightText">md-highlight-flags</a> attribute
2038 * along with its parameters to control the appearance of the matched text inside of the contacts'
2039 * autocomplete popup.
2040 *
2041 * @param {expression} ng-model Assignable AngularJS expression to be data-bound to the list of
2042 * contact chips. The expression should evaluate to an `Object` Array.
2043 * @param {expression=} ng-change AngularJS expression to be executed on chip addition, removal,
2044 * or content change.
2045 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
2046 * @param {string=} secondary-placeholder Placeholder text that will be forwarded to the input,
2047 * displayed when there is at least on item in the list
2048 * @param {expression} md-contacts An expression expected to return contacts matching the search
2049 * test, `$query`. If this expression involves a promise, a loading bar is displayed while
2050 * waiting for it to resolve.
2051 * @param {string} md-contact-name The field name of the contact object representing the
2052 * contact's name.
2053 * @param {string} md-contact-email The field name of the contact object representing the
2054 * contact's email address.
2055 * @param {string} md-contact-image The field name of the contact object representing the
2056 * contact's image.
2057 * @param {number=} md-max-chips The maximum number of chips allowed to add through user input.
2058 * <br/><br/>The validation property `md-max-chips` can be used when the max chips
2059 * amount is reached.
2060 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
2061 * make suggestions
2062 * @param {string=} md-input-class This class will be applied to the child `md-autocomplete` for
2063 * custom styling.
2064 * @param {string=} input-aria-describedby A space-separated list of element IDs. This should
2065 * contain the IDs of any elements that describe this autocomplete. Screen readers will read
2066 * the content of these elements at the end of announcing that the chips input has been
2067 * selected and describing its current state. The descriptive elements do not need to be
2068 * visible on the page.
2069 * @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use
2070 * case is that this would contain the ID of a `<label>` element that is associated with these
2071 * chips.<br><br>
2072 * For `<label id="state">US State</label>`, you would set this to
2073 * `input-aria-labelledby="state"`.
2074 * @param {string=} input-aria-label A string read by screen readers to identify the input.
2075 * For static chips, this will be applied to the chips container.
2076 * @param {string=} container-hint A string read by screen readers informing users of how to
2077 * navigate the chips when there are chips.
2078 * @param {string=} container-empty-hint A string read by screen readers informing users of how to
2079 * add chips when there are no chips. You will want to use this to override the default when
2080 * in a non-English locale.
2081 * @param {string=} delete-hint A string read by screen readers instructing users that pressing
2082 * the delete key will remove the chip. You will want to use this to override the default when
2083 * in a non-English locale.
2084 * @param {string=} md-removed-message Screen readers will announce this message following the
2085 * chips contents. The default is `"removed"`. If a chip with the content of "Apple" was
2086 * removed, the screen reader would read "Apple removed". You will want to use this to override
2087 * the default when in a non-English locale.
2088 *
2089 *
2090 * @usage
2091 * <hljs lang="html">
2092 * <md-contact-chips
2093 * ng-model="ctrl.contacts"
2094 * md-contacts="ctrl.querySearch($query)"
2095 * md-contact-name="name"
2096 * md-contact-image="image"
2097 * md-contact-email="email"
2098 * placeholder="To">
2099 * </md-contact-chips>
2100 * </hljs>
2101 *
2102 */
2103
2104
2105var MD_CONTACT_CHIPS_TEMPLATE = '\
2106 <md-chips class="md-contact-chips"\
2107 ng-model="$mdContactChipsCtrl.contacts"\
2108 ng-change="$mdContactChipsCtrl.ngChange($mdContactChipsCtrl.contacts)"\
2109 md-require-match="$mdContactChipsCtrl.requireMatch"\
2110 md-max-chips="{{$mdContactChipsCtrl.maxChips}}"\
2111 md-chip-append-delay="{{$mdContactChipsCtrl.chipAppendDelay}}"\
2112 md-separator-keys="$mdContactChipsCtrl.separatorKeys"\
2113 md-autocomplete-snap>\
2114 <md-autocomplete\
2115 md-menu-class="md-contact-chips-suggestions"\
2116 md-selected-item="$mdContactChipsCtrl.selectedItem"\
2117 md-search-text="$mdContactChipsCtrl.searchText"\
2118 md-items="item in $mdContactChipsCtrl.queryContact($mdContactChipsCtrl.searchText)"\
2119 md-item-text="$mdContactChipsCtrl.itemName(item)"\
2120 md-no-cache="true"\
2121 md-min-length="$mdContactChipsCtrl.minLength"\
2122 md-autoselect\
2123 ng-attr-md-input-class="{{$mdContactChipsCtrl.inputClass}}"\
2124 ng-keydown="$mdContactChipsCtrl.inputKeydown($event)"\
2125 placeholder="{{$mdContactChipsCtrl.contacts.length === 0 ?\
2126 $mdContactChipsCtrl.placeholder : $mdContactChipsCtrl.secondaryPlaceholder}}">\
2127 <div class="md-contact-suggestion">\
2128 <img \
2129 ng-src="{{item[$mdContactChipsCtrl.contactImage]}}"\
2130 alt="{{item[$mdContactChipsCtrl.contactName]}}"\
2131 ng-if="item[$mdContactChipsCtrl.contactImage]" />\
2132 <span class="md-contact-name" md-highlight-text="$mdContactChipsCtrl.searchText"\
2133 md-highlight-flags="{{$mdContactChipsCtrl.highlightFlags}}">\
2134 {{item[$mdContactChipsCtrl.contactName]}}\
2135 </span>\
2136 <span class="md-contact-email" >{{item[$mdContactChipsCtrl.contactEmail]}}</span>\
2137 </div>\
2138 </md-autocomplete>\
2139 <md-chip-template>\
2140 <div class="md-contact-avatar">\
2141 <img \
2142 ng-src="{{$chip[$mdContactChipsCtrl.contactImage]}}"\
2143 alt="{{$chip[$mdContactChipsCtrl.contactName]}}"\
2144 ng-if="$chip[$mdContactChipsCtrl.contactImage]" />\
2145 </div>\
2146 <div class="md-contact-name">\
2147 {{$chip[$mdContactChipsCtrl.contactName]}}\
2148 </div>\
2149 </md-chip-template>\
2150 </md-chips>';
2151
2152
2153/**
2154 * MDContactChips Directive Definition
2155 *
2156 * @param $mdTheming
2157 * @param $mdUtil
2158 * @returns {*}
2159 * ngInject
2160 */
2161function MdContactChips($mdTheming, $mdUtil) {
2162 return {
2163 template: function(element, attrs) {
2164 return MD_CONTACT_CHIPS_TEMPLATE;
2165 },
2166 restrict: 'E',
2167 controller: 'MdContactChipsCtrl',
2168 controllerAs: '$mdContactChipsCtrl',
2169 bindToController: true,
2170 compile: compile,
2171 scope: {
2172 contactQuery: '&mdContacts',
2173 placeholder: '@?',
2174 secondaryPlaceholder: '@?',
2175 contactName: '@mdContactName',
2176 contactImage: '@mdContactImage',
2177 contactEmail: '@mdContactEmail',
2178 contacts: '=ngModel',
2179 ngChange: '&?',
2180 requireMatch: '=?mdRequireMatch',
2181 minLength: '=?mdMinLength',
2182 maxChips: '=?mdMaxChips',
2183 highlightFlags: '@?mdHighlightFlags',
2184 chipAppendDelay: '@?mdChipAppendDelay',
2185 separatorKeys: '=?mdSeparatorKeys',
2186 removedMessage: '@?mdRemovedMessage',
2187 inputClass: '@?mdInputClass',
2188 inputAriaDescribedBy: '@?inputAriaDescribedby',
2189 inputAriaLabelledBy: '@?inputAriaLabelledby',
2190 inputAriaLabel: '@?',
2191 containerHint: '@?',
2192 containerEmptyHint: '@?',
2193 deleteHint: '@?'
2194 }
2195 };
2196
2197 function compile(element, attr) {
2198 return function postLink(scope, element, attrs, controllers) {
2199 var contactChipsController = controllers;
2200
2201 $mdUtil.initOptionalProperties(scope, attr);
2202 $mdTheming(element);
2203
2204 element.attr('tabindex', '-1');
2205
2206 attrs.$observe('mdChipAppendDelay', function(newValue) {
2207 contactChipsController.chipAppendDelay = newValue;
2208 });
2209 };
2210 }
2211}
2212
2213})(window, window.angular);
Note: See TracBrowser for help on using the repository browser.