From 00d2f345e8fb4a1ae8d8b038d685e20e5b46e738 Mon Sep 17 00:00:00 2001 From: Mert Degirmenci Date: Thu, 3 Mar 2016 10:07:30 -0800 Subject: [PATCH] feat(chips): Make chips editable --- src/components/chips/chips-theme.scss | 5 + src/components/chips/chips.scss | 6 + .../chips/demoBasicUsage/index.html | 6 + src/components/chips/demoBasicUsage/script.js | 2 + src/components/chips/js/chipController.js | 192 ++++++++++++++++++ src/components/chips/js/chipDirective.js | 25 ++- src/components/chips/js/chipsController.js | 30 ++- src/components/chips/js/chipsDirective.js | 4 + 8 files changed, 260 insertions(+), 10 deletions(-) create mode 100644 src/components/chips/js/chipController.js mode change 100755 => 100644 src/components/chips/js/chipsController.js mode change 100755 => 100644 src/components/chips/js/chipsDirective.js diff --git a/src/components/chips/chips-theme.scss b/src/components/chips/chips-theme.scss index 1d8d62f7ae..fcbd92c74a 100644 --- a/src/components/chips/chips-theme.scss +++ b/src/components/chips/chips-theme.scss @@ -31,6 +31,11 @@ md-chips.md-THEME_NAME-theme { color: '{{primary-contrast}}'; } } + + &._md-chip-editing { + background: transparent; + color: '{{background-800}}'; + } } md-chip-remove { .md-button { diff --git a/src/components/chips/chips.scss b/src/components/chips/chips.scss index 90888665bb..1f367593cb 100644 --- a/src/components/chips/chips.scss +++ b/src/components/chips/chips.scss @@ -100,6 +100,12 @@ $contact-chip-name-width: rem(12) !default; outline: none; } } + &._md-chip-content-edit-is-enabled { + -webkit-user-select: none; /* webkit (safari, chrome) browsers */ + -moz-user-select: none; /* mozilla browsers */ + -khtml-user-select: none; /* webkit (konqueror) browsers */ + -ms-user-select: none; /* IE10+ */ + } ._md-chip-remove-container { position: absolute; @include rtl-prop(right, left, 0); diff --git a/src/components/chips/demoBasicUsage/index.html b/src/components/chips/demoBasicUsage/index.html index 9b1119008c..43be5e8232 100644 --- a/src/components/chips/demoBasicUsage/index.html +++ b/src/components/chips/demoBasicUsage/index.html @@ -22,6 +22,12 @@

Use the default chip template.

+ +
+

Make chips editable.

+ + +

Use Placeholders and override hint texts.

diff --git a/src/components/chips/demoBasicUsage/script.js b/src/components/chips/demoBasicUsage/script.js index f4e960ef2d..d86a107ecd 100644 --- a/src/components/chips/demoBasicUsage/script.js +++ b/src/components/chips/demoBasicUsage/script.js @@ -12,6 +12,8 @@ // Lists of fruit names and Vegetable objects self.fruitNames = ['Apple', 'Banana', 'Orange']; self.roFruitNames = angular.copy(self.fruitNames); + self.editableFruitNames = angular.copy(self.fruitNames); + self.tags = []; self.vegObjs = [ { diff --git a/src/components/chips/js/chipController.js b/src/components/chips/js/chipController.js new file mode 100644 index 0000000000..c6b4d9b055 --- /dev/null +++ b/src/components/chips/js/chipController.js @@ -0,0 +1,192 @@ +angular + .module('material.components.chips') + .controller('MdChipCtrl', MdChipCtrl); + +/** + * Controller for the MdChip component. Responsible for handling keyboard + * events and editting the chip if needed. + * + * @param $scope + * @param $element + * @param $mdConstant + * @param $timeout + * @param $mdUtil + * @constructor + */ +function MdChipCtrl ($scope, $element, $mdConstant, $timeout, $mdUtil) { + /** + * @type {$scope} + */ + this.$scope = $scope; + + /** + * @type {$element} + */ + this.$element = $element; + + /** + * @type {$mdConstant} + */ + this.$mdConstant = $mdConstant; + + /** + * @type {$timeout} + */ + this.$timeout = $timeout; + + /** + * @type {$mdUtil} + */ + this.$mdUtil = $mdUtil; + + /** + * @type {boolean} + */ + this.isEditting = false; + + /** + * @type {MdChipsCtrl} + */ + this.parentController = undefined; + + /** + * @type {boolean} + */ + this.enableChipEdit = false; +} + + +/** + * @param {MdChipsCtrl} controller + */ +MdChipCtrl.prototype.init = function(controller) { + this.parentController = controller; + this.enableChipEdit = this.parentController.enableChipEdit; + + if (this.enableChipEdit) { + this.$element.on('keydown', this.chipKeyDown.bind(this)); + this.$element.on('mousedown', this.chipMouseDown.bind(this)); + this.getChipContent().addClass('_md-chip-content-edit-is-enabled'); + } +}; + + +/** + * @return {Object} + */ +MdChipCtrl.prototype.getChipContent = function() { + var chipContents = this.$element[0].getElementsByClassName('_md-chip-content'); + return angular.element(chipContents[0]); +}; + + +/** + * @return {Object} + */ +MdChipCtrl.prototype.getContentElement = function() { + return angular.element(this.getChipContent().children()[0]); +}; + + +/** + * @return {number} + */ +MdChipCtrl.prototype.getChipIndex = function() { + return parseInt(this.$element.attr('index')); +}; + + +/** + * Presents an input element to edit the contents of the chip. + */ +MdChipCtrl.prototype.goOutOfEditMode = function() { + if (!this.isEditting) return; + + this.isEditting = false; + this.$element.removeClass('_md-chip-editing'); + this.getChipContent()[0].contentEditable = 'false'; + var chipIndex = this.getChipIndex(); + + var content = this.getContentElement().text(); + if (content) { + this.parentController.updateChipContents( + chipIndex, + this.getContentElement().text() + ); + + this.$mdUtil.nextTick(function() { + if (this.parentController.selectedChip === chipIndex) { + this.parentController.focusChip(chipIndex); + } + }.bind(this)); + } else { + this.parentController.removeChipAndFocusInput(chipIndex); + } +}; + + +/** + * Given an HTML element. Selects contents of it. + * @param node + */ +MdChipCtrl.prototype.selectNodeContents = function(node) { + var range, selection; + if (document.body.createTextRange) { + range = document.body.createTextRange(); + range.moveToElementText(node); + range.select(); + } else if (window.getSelection) { + selection = window.getSelection(); + range = document.createRange(); + range.selectNodeContents(node); + selection.removeAllRanges(); + selection.addRange(range); + } +}; + + +/** + * Presents an input element to edit the contents of the chip. + */ +MdChipCtrl.prototype.goInEditMode = function() { + this.isEditting = true; + this.$element.addClass('_md-chip-editing'); + this.getChipContent()[0].contentEditable = 'true'; + this.getChipContent().on('blur', function() { + this.goOutOfEditMode(); + }.bind(this)); + + this.selectNodeContents(this.getChipContent()[0]); +}; + + +/** + * Handles the keydown event on the chip element. If enable-chip-edit attribute is + * set to true, space or enter keys can trigger going into edit mode. Enter can also + * trigger submitting if the chip is already being edited. + * @param event + */ +MdChipCtrl.prototype.chipKeyDown = function(event) { + if (!this.isEditting && + (event.keyCode === this.$mdConstant.KEY_CODE.ENTER || + event.keyCode === this.$mdConstant.KEY_CODE.SPACE)) { + event.preventDefault(); + this.goInEditMode(); + } else if (this.isEditting && + event.keyCode === this.$mdConstant.KEY_CODE.ENTER) { + event.preventDefault(); + this.goOutOfEditMode(); + } +}; + + +/** + * Handles the double click event + */ +MdChipCtrl.prototype.chipMouseDown = function() { + if(this.getChipIndex() == this.parentController.selectedChip && + this.enableChipEdit && + !this.isEditting) { + this.goInEditMode(); + } +}; diff --git a/src/components/chips/js/chipDirective.js b/src/components/chips/js/chipDirective.js index b934886ba2..607e7f18a1 100644 --- a/src/components/chips/js/chipDirective.js +++ b/src/components/chips/js/chipDirective.js @@ -30,7 +30,7 @@ var DELETE_HINT_TEMPLATE = '\ * MDChip Directive Definition * * @param $mdTheming - * @param $mdInkRipple + * @param $mdUtil * @ngInject */ function MdChip($mdTheming, $mdUtil) { @@ -38,22 +38,29 @@ function MdChip($mdTheming, $mdUtil) { return { restrict: 'E', - require: '^?mdChips', - compile: compile + require: ['^?mdChips', 'mdChip'], + compile: compile, + controller: 'MdChipCtrl' }; function compile(element, attr) { // Append the delete template element.append($mdUtil.processTemplate(hintTemplate)); - return function postLink(scope, element, attr, ctrl) { + return function postLink(scope, element, attr, ctrls) { + var chipsController = ctrls.shift(); + var chipController = ctrls.shift(); $mdTheming(element); - if (ctrl) angular.element(element[0].querySelector('._md-chip-content')) - .on('blur', function () { - ctrl.resetSelectedChip(); - ctrl.$scope.$applyAsync(); - }); + if (chipsController) { + chipController.init(chipsController); + + angular.element(element[0].querySelector('._md-chip-content')) + .on('blur', function () { + chipsController.selectedChip = -1; + chipsController.$scope.$applyAsync(); + }); + } }; } } diff --git a/src/components/chips/js/chipsController.js b/src/components/chips/js/chipsController.js old mode 100755 new mode 100644 index d19558e38b..bf055a1e9d --- a/src/components/chips/js/chipsController.js +++ b/src/components/chips/js/chipsController.js @@ -11,9 +11,10 @@ angular * @param $mdConstant * @param $log * @param $element + * @param $mdUtil * @constructor */ -function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout) { +function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout, $mdUtil) { /** @type {$timeout} **/ this.$timeout = $timeout; @@ -50,6 +51,8 @@ function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout) { /** @type {boolean} */ this.hasAutocomplete = false; + /** @type {string} */ + this.enableChipEdit = $mdUtil.parseAttributeBoolean(this.mdEnableChipEdit); /** * Hidden hint text for how to delete a chip. Used to give context to screen readers. @@ -146,6 +149,29 @@ MdChipsCtrl.prototype.inputKeydown = function(event) { } }; + +/** + * Updates the content of the chip at given index + * @param chipIndex + * @param chipContents + */ +MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){ + if(chipIndex >= 0 && chipIndex < this.items.length) { + this.items[chipIndex] = chipContents; + this.ngModelCtrl.$setDirty(); + } +}; + + +/** + * Returns true if a chip is currently being edited. False otherwise. + * @return {boolean} + */ +MdChipsCtrl.prototype.isEditingChip = function(){ + return !!this.$element[0].getElementsByClassName('_md-chip-editing').length; +}; + + /** * Handles the keydown event on the chip elements: backspace removes the selected chip, arrow * keys switch which chips is active @@ -153,6 +179,8 @@ MdChipsCtrl.prototype.inputKeydown = function(event) { */ MdChipsCtrl.prototype.chipKeydown = function (event) { if (this.getChipBuffer()) return; + if (this.isEditingChip()) return; + switch (event.keyCode) { case this.$mdConstant.KEY_CODE.BACKSPACE: case this.$mdConstant.KEY_CODE.DELETE: diff --git a/src/components/chips/js/chipsDirective.js b/src/components/chips/js/chipsDirective.js old mode 100755 new mode 100644 index 5260fc6d09..4a1987fd60 --- a/src/components/chips/js/chipsDirective.js +++ b/src/components/chips/js/chipsDirective.js @@ -68,6 +68,9 @@ * @param {boolean=} readonly Disables list manipulation (deleting or adding list items), hiding * the input and delete buttons. If no `ng-model` is provided, the chips will automatically be * marked as readonly. + * @param {string=} md-enable-chip-edit Set this to "true" to enable editing of chip contents. The user can + * go into edit mode with pressing "space", "enter", or double clicking on the chip. Chip edit is only + * supported for chips with basic template. * @param {number=} md-max-chips The maximum number of chips allowed to add through user input. *

The validation property `md-max-chips` can be used when the max chips * amount is reached. @@ -195,6 +198,7 @@ scope: { readonly: '=readonly', placeholder: '@', + mdEnableChipEdit: '@', secondaryPlaceholder: '@', maxChips: '@mdMaxChips', transformChip: '&mdTransformChip',