diff --git a/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html b/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html new file mode 100644 index 00000000000..ea0012411d1 --- /dev/null +++ b/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html @@ -0,0 +1,37 @@ + + +
+

Autocomplete Dialog Example

+ + + + +
+
+ + +
+
+

Use md-autocomplete to search for matches from local or remote data sources.

+ + + {{item.display}} + + + No states matching "{{ctrl.searchText}}" were found. + + +
+
+
+ +
+ Finished +
+
\ No newline at end of file diff --git a/src/components/autocomplete/demoInsideDialog/index.html b/src/components/autocomplete/demoInsideDialog/index.html new file mode 100644 index 00000000000..6e292869183 --- /dev/null +++ b/src/components/autocomplete/demoInsideDialog/index.html @@ -0,0 +1,9 @@ +
+ +

+ Click the button below to open the dialog with an autocomplete. +

+ + Open Dialog +
+
diff --git a/src/components/autocomplete/demoInsideDialog/script.js b/src/components/autocomplete/demoInsideDialog/script.js new file mode 100644 index 00000000000..d794b650a2a --- /dev/null +++ b/src/components/autocomplete/demoInsideDialog/script.js @@ -0,0 +1,84 @@ +(function () { + 'use strict'; + angular + .module('autocompleteDemoInsideDialog', ['ngMaterial']) + .controller('DemoCtrl', DemoCtrl); + + function DemoCtrl($mdDialog) { + var self = this; + + self.openDialog = function($event) { + $mdDialog.show({ + controller: DialogCtrl, + controllerAs: 'ctrl', + templateUrl: 'dialog.tmpl.html', + parent: angular.element(document.body), + targetEvent: $event, + clickOutsideToClose:true + }) + } + } + + function DialogCtrl ($timeout, $q, $scope, $mdDialog) { + var self = this; + + // list of `state` value/display objects + self.states = loadAll(); + self.querySearch = querySearch; + + // ****************************** + // Template methods + // ****************************** + + self.cancel = function($event) { + $mdDialog.cancel(); + }; + self.finish = function($event) { + $mdDialog.hide(); + }; + + // ****************************** + // Internal methods + // ****************************** + + /** + * Search for states... use $timeout to simulate + * remote dataservice call. + */ + function querySearch (query) { + return query ? self.states.filter( createFilterFor(query) ) : self.states; + } + + /** + * Build `states` list of key/value pairs + */ + function loadAll() { + var allStates = 'Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware,\ + Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana,\ + Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana,\ + Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina,\ + North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina,\ + South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia,\ + Wisconsin, Wyoming'; + + return allStates.split(/, +/g).map( function (state) { + return { + value: state.toLowerCase(), + display: state + }; + }); + } + + /** + * Create filter function for a query string + */ + function createFilterFor(query) { + var lowercaseQuery = angular.lowercase(query); + + return function filterFn(state) { + return (state.value.indexOf(lowercaseQuery) === 0); + }; + + } + } +})(); diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js index 6a1e3a6baef..d6ee34a7871 100644 --- a/src/components/autocomplete/js/autocompleteController.js +++ b/src/components/autocomplete/js/autocompleteController.js @@ -412,9 +412,9 @@ function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, select(ctrl.index); break; case $mdConstant.KEY_CODE.ENTER: + if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return; event.stopPropagation(); event.preventDefault(); - if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return; select(ctrl.index); break; case $mdConstant.KEY_CODE.ESCAPE: diff --git a/src/components/chips/chips.spec.js b/src/components/chips/chips.spec.js index 3a1761baf06..455c439f6c4 100644 --- a/src/components/chips/chips.spec.js +++ b/src/components/chips/chips.spec.js @@ -4,8 +4,12 @@ describe('', function() { var BASIC_CHIP_TEMPLATE = ''; + var CHIP_TRANSFORM_TEMPLATE = + ''; var CHIP_APPEND_TEMPLATE = ''; + var CHIP_ADD_TEMPLATE = + ''; var CHIP_REMOVE_TEMPLATE = ''; var CHIP_SELECT_TEMPLATE = @@ -109,7 +113,15 @@ describe('', function() { expect(chips[1].innerHTML).toContain('Orange'); }); - it('should call the append method when adding a chip', function() { + // TODO: Remove in 1.0 release after deprecation + it('should warn of deprecation when using md-on-append', inject(function($log) { + spyOn($log, 'warn'); + buildChips(CHIP_APPEND_TEMPLATE); + expect($log.warn).toHaveBeenCalled(); + })); + + // TODO: Remove in 1.0 release after deprecation + it('should retain the deprecated md-on-append functionality until removed', function() { var element = buildChips(CHIP_APPEND_TEMPLATE); var ctrl = element.controller('mdChips'); @@ -129,6 +141,62 @@ describe('', function() { expect(scope.items[3]).toBe('GrapeGrape'); }); + it('should call the transform method when adding a chip', function() { + var element = buildChips(CHIP_TRANSFORM_TEMPLATE); + var ctrl = element.controller('mdChips'); + + var doubleText = function(text) { + return "" + text + text; + }; + scope.transformChip = jasmine.createSpy('transformChip').and.callFake(doubleText); + + element.scope().$apply(function() { + ctrl.chipBuffer = 'Grape'; + simulateInputEnterKey(ctrl); + }); + + expect(scope.transformChip).toHaveBeenCalled(); + expect(scope.transformChip.calls.mostRecent().args[0]).toBe('Grape'); + expect(scope.items.length).toBe(4); + expect(scope.items[3]).toBe('GrapeGrape'); + }); + + it('should not add the chip if md-transform-chip returns null', function() { + var element = buildChips(CHIP_TRANSFORM_TEMPLATE); + var ctrl = element.controller('mdChips'); + + var nullChip = function(text) { + return null; + }; + scope.transformChip = jasmine.createSpy('transformChip').and.callFake(nullChip); + + element.scope().$apply(function() { + ctrl.chipBuffer = 'Grape'; + simulateInputEnterKey(ctrl); + }); + + expect(scope.transformChip).toHaveBeenCalled(); + expect(scope.transformChip.calls.mostRecent().args[0]).toBe('Grape'); + expect(scope.items.length).toBe(3); + }); + + it('should call the add method when adding a chip', function() { + var element = buildChips(CHIP_ADD_TEMPLATE); + var ctrl = element.controller('mdChips'); + + scope.addChip = jasmine.createSpy('addChip'); + + element.scope().$apply(function() { + ctrl.chipBuffer = 'Grape'; + simulateInputEnterKey(ctrl); + }); + + expect(scope.addChip).toHaveBeenCalled(); + expect(scope.addChip.calls.mostRecent().args[0]).toBe('Grape'); // Chip + expect(scope.addChip.calls.mostRecent().args[1]).toBe(4); // Index + }); + + it('should call the remove method when removing a chip', function() { var element = buildChips(CHIP_REMOVE_TEMPLATE); var ctrl = element.controller('mdChips'); @@ -328,6 +396,80 @@ describe('', function() { expect(scope.items[3]).toBe('Kiwi'); expect(element.find('input').val()).toBe(''); })); + + it('simultaneously allows selecting an existing chip AND adding a new one', inject(function($mdConstant) { + // Setup our scope and function + setupScopeForAutocomplete(); + scope.transformChip = jasmine.createSpy('transformChip'); + + // Modify the base template to add md-transform-chip + var modifiedTemplate = AUTOCOMPLETE_CHIPS_TEMPLATE + .replace('Display an ordered set of objects as chips (with custom tem

Note: the variables $chip and $index are available in custom chip templates.

+ md-transform-chip="ctrl.newVeg($chip)"> [{{$index}}] {{$chip.name}} diff --git a/src/components/chips/demoContactChips/style.scss b/src/components/chips/demoContactChips/style.scss index 17e40fc44fd..f221e2c0fd2 100644 --- a/src/components/chips/demoContactChips/style.scss +++ b/src/components/chips/demoContactChips/style.scss @@ -1,5 +1,11 @@ md-content.autocomplete { min-height: 250px; + + // NOTE: Due to a bug with the virtual repeat sizing, we must manually set the width of + // the input so that the autocomplete popup will be properly sized. See issue #4450. + input { + min-width: 400px; + } } .md-item-text.compact { padding-top: 8px; @@ -15,6 +21,7 @@ md-content.autocomplete { } .md-list-item-text { padding: 14px 0; + max-width: 190px; h3 { margin: 0 !important; padding: 0; diff --git a/src/components/chips/demoCustomInputs/index.html b/src/components/chips/demoCustomInputs/index.html index df6ed4b3642..488108dd65b 100644 --- a/src/components/chips/demoCustomInputs/index.html +++ b/src/components/chips/demoCustomInputs/index.html @@ -15,7 +15,9 @@

Use an input element to build an ordered set

Use md-autocomplete to build an ordered set of chips.

- + Use md-autocomplete to build an ordered set o
+ + Tell the autocomplete to require a match (when enabled you cannot create new chips) + +

Vegetable Options

diff --git a/src/components/chips/demoCustomInputs/script.js b/src/components/chips/demoCustomInputs/script.js index dcd4f265876..53f0fa48a1e 100644 --- a/src/components/chips/demoCustomInputs/script.js +++ b/src/components/chips/demoCustomInputs/script.js @@ -16,6 +16,21 @@ self.numberChips = []; self.numberChips2 = []; self.numberBuffer = ''; + self.autocompleteDemoRequireMatch = true; + self.transformChip = transformChip; + + /** + * Return the proper object when the append is called. + */ + function transformChip(chip) { + // If it is an object, it's already a known chip + if (angular.isObject(chip)) { + return chip; + } + + // Otherwise, create a new one + return { name: chip, type: 'new' } + } /** * Search for vegetables. diff --git a/src/components/chips/js/chipsController.js b/src/components/chips/js/chipsController.js index 8da8b75eb21..f0d73e40c30 100644 --- a/src/components/chips/js/chipsController.js +++ b/src/components/chips/js/chipsController.js @@ -73,9 +73,31 @@ function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout) { * Whether to use the onAppend expression to transform the chip buffer * before appending it to the list. * @type {boolean} + * + * + * @deprecated Will remove in 1.0. */ this.useOnAppend = false; + /** + * Whether to use the transformChip expression to transform the chip buffer + * before appending it to the list. + * @type {boolean} + */ + this.useTransformChip = false; + + /** + * Whether to use the onAdd expression to notify of chip additions. + * @type {boolean} + */ + this.useOnAdd = false; + + /** + * Whether to use the onRemove expression to notify of chip removals. + * @type {boolean} + */ + this.useOnRemove = false; + /** * Whether to use the onSelect expression to notify the component's user * after selecting a chip from the list. @@ -93,6 +115,11 @@ function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout) { MdChipsCtrl.prototype.inputKeydown = function(event) { var chipBuffer = this.getChipBuffer(); + // If we have an autocomplete, and it handled the event, we have nothing to do + if (this.hasAutocomplete && event.isDefaultPrevented && event.isDefaultPrevented()) { + return; + } + switch (event.keyCode) { case this.$mdConstant.KEY_CODE.ENTER: if ((this.hasAutocomplete && this.requireMatch) || !chipBuffer) break; @@ -189,30 +216,40 @@ MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) { /** * Append the contents of the buffer to the chip list. This method will first - * call out to the md-on-append method, if provided + * call out to the md-transform-chip method, if provided. + * * @param newChip */ - MdChipsCtrl.prototype.appendChip = function(newChip) { +MdChipsCtrl.prototype.appendChip = function(newChip) { + if (this.useTransformChip && this.transformChip) { + var transformedChip = this.transformChip({'$chip': newChip}); + + // Check to make sure the chip is defined before assigning it, otherwise, we'll just assume + // they want the string version. + if (angular.isDefined(transformedChip)) { + newChip = transformedChip; + } + } - // If useOnAppend and onAppend function is provided call it. - if (this.useOnAppend && this.onAppend) { - newChip = this.onAppend({'$chip': newChip}); - } + // If items contains an identical object to newChip, do not append + if (angular.isObject(newChip)){ + var identical = this.items.some(function(item){ + return angular.equals(newChip, item); + }); + if(identical) return; + } - // If items contains identical object to newChip do not append - if(angular.isObject(newChip)){ - var identical = this.items.some(function(item){ - return angular.equals(newChip, item); - }); - if(identical) return; - } + // Check for a null (but not undefined), or existing chip and cancel appending + if (newChip == null || this.items.indexOf(newChip) + 1) return; - // If items contains newChip do not append - if (this.items.indexOf(newChip) + 1) return; + // Append the new chip onto our list + var index = this.items.push(newChip); - //add newChip to items - this.items.push(newChip); - }; + // If they provide the md-on-add attribute, notify them of the chip addition + if (this.useOnAdd && this.onAdd) { + this.onAdd({ '$chip': newChip, '$index': index }); + } +}; /** * Sets whether to use the md-on-append expression. This expression is @@ -220,9 +257,39 @@ MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) { * {@code onAppend}. Due to the nature of directive scope bindings, the * controller cannot know on its own/from the scope whether an expression was * actually provided. + * + * @deprecated + * + * TODO: Remove deprecated md-on-append functionality in 1.0 */ MdChipsCtrl.prototype.useOnAppendExpression = function() { - this.useOnAppend = true; + this.$log.warn("md-on-append is deprecated; please use md-transform-chip or md-on-add instead"); + if (!this.useTransformChip || !this.transformChip) { + this.useTransformChip = true; + this.transformChip = this.onAppend; + } +}; + +/** + * Sets whether to use the md-transform-chip expression. This expression is + * bound to scope and controller in {@code MdChipsDirective} as + * {@code transformChip}. Due to the nature of directive scope bindings, the + * controller cannot know on its own/from the scope whether an expression was + * actually provided. + */ +MdChipsCtrl.prototype.useTransformChipExpression = function() { + this.useTransformChip = true; +}; + +/** + * Sets whether to use the md-on-add expression. This expression is + * bound to scope and controller in {@code MdChipsDirective} as + * {@code onAdd}. Due to the nature of directive scope bindings, the + * controller cannot know on its own/from the scope whether an expression was + * actually provided. + */ +MdChipsCtrl.prototype.useOnAddExpression = function() { + this.useOnAdd = true; }; /** diff --git a/src/components/chips/js/chipsDirective.js b/src/components/chips/js/chipsDirective.js index 1c5f7f7ee11..9549a764e4e 100644 --- a/src/components/chips/js/chipsDirective.js +++ b/src/components/chips/js/chipsDirective.js @@ -67,11 +67,18 @@ * displayed when there is at least on item in the list * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding * the input and delete buttons - * @param {expression} md-on-append An expression that when called expects you to return an object - * representation of the chip input string. + * @param {expression} md-transform-chip An expression of form `myFunction($chip)` that when called + * expects one of the following return values: + * - an object representing the `$chip` input string + * - `undefined` to simply add the `$chip` input string, or + * - `null` to prevent the chip from being appended + * @param {expression=} md-on-add An expression which will be called when a chip has been + * added. * @param {expression=} md-on-remove An expression which will be called when a chip has been * removed. * @param {expression=} md-on-select An expression which will be called when a chip is selected. + * @param {boolean} md-require-match If true, and the chips template contains an autocomplete, + * only allow selection of pre-defined chips (i.e. you cannot add new ones). * @param {string=} delete-hint A string read by screen readers instructing users that pressing * the delete key will remove the chip. * @param {string=} delete-button-label A label for the delete button. Also hidden and read by @@ -167,7 +174,9 @@ readonly: '=readonly', placeholder: '@', secondaryPlaceholder: '@', + transformChip: '&mdTransformChip', onAppend: '&mdOnAppend', + onAdd: '&mdOnAdd', onRemove: '&mdOnRemove', onSelect: '&mdOnSelect', deleteHint: '@', @@ -248,10 +257,20 @@ if (attr.ngModel) { mdChipsCtrl.configureNgModel(element.controller('ngModel')); + // If an `md-transform-chip` attribute was set, tell the controller to use the expression + // before appending chips. + if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression(); + // If an `md-on-append` attribute was set, tell the controller to use the expression // when appending chips. + // + // DEPRECATED: Will remove in official 1.0 release if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression(); + // If an `md-on-add` attribute was set, tell the controller to use the expression + // when adding chips. + if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression(); + // If an `md-on-remove` attribute was set, tell the controller to use the expression // when removing chips. if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();