Skip to content
This repository has been archived by the owner on Nov 22, 2021. It is now read-only.

Commit

Permalink
feat(tagsInput): Added addOnBlur option
Browse files Browse the repository at this point in the history
Added an option for the user to set if a tag should be created when the
input field loses focus and there is some text left in it. This feature
is important because it prevents a tag from being accidentally lost when
it's not explicitly added.

Closes #29.
  • Loading branch information
mbenford committed Dec 5, 2013
1 parent c2b43c6 commit 69415a2
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 57 deletions.
4 changes: 4 additions & 0 deletions build/ng-tags-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
position: relative;
}

.ngTagsInput:active {
outline: none;
}

.ngTagsInput .tags {
-moz-appearance: textfield;
-webkit-appearance: textfield;
Expand Down
69 changes: 42 additions & 27 deletions build/ng-tags-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ angular.module('tags-input', []);
* @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key.
* @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key.
* @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key.
* @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus.
* @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes.
* @param {string=} [allowedTagsPattern=^[a-zA-Z0-9\s]+$*] Regular expression that determines whether a new tag is valid.
* @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into
Expand All @@ -39,7 +40,7 @@ angular.module('tags-input', []);
* @param {expression} onTagAdded Expression to evaluate upon adding a new tag. The new tag is available as $tag.
* @param {expression} onTagRemoved Expression to evaluate upon removing an existing tag. The removed tag is available as $tag.
*/
angular.module('tags-input').directive('tagsInput', ["configuration", function(configuration) {
angular.module('tags-input').directive('tagsInput', ["$timeout","$document","configuration", function($timeout, $document, configuration) {
function SimplePubSub() {
var events = {};

Expand Down Expand Up @@ -67,7 +68,7 @@ angular.module('tags-input').directive('tagsInput', ["configuration", function(c
},
replace: false,
transclude: true,
template: '<div class="ngTagsInput" ng-class="options.customClass" transclude-append>' +
template: '<div class="ngTagsInput" tabindex="-1" ng-class="options.customClass" transclude-append>' +
' <div class="tags">' +
' <ul>' +
' <li ng-repeat="tag in tags" ng-class="getCssClass($index)">' +
Expand Down Expand Up @@ -99,6 +100,7 @@ angular.module('tags-input').directive('tagsInput', ["configuration", function(c
addOnEnter: { type: Boolean, defaultValue: true },
addOnSpace: { type: Boolean, defaultValue: false },
addOnComma: { type: Boolean, defaultValue: true },
addOnBlur: { type: Boolean, defaultValue: true },
allowedTagsPattern: { type: RegExp, defaultValue: /^[a-zA-Z0-9\s]+$/ },
enableEditingLastTag: { type: Boolean, defaultValue: false }
});
Expand Down Expand Up @@ -201,39 +203,52 @@ angular.module('tags-input').directive('tagsInput', ["configuration", function(c
var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace];
var input = element.find('input');

input.on('keydown', function(e) {
var key;
input
.on('keydown', function(e) {
var key;

// This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
// I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
// https://github.com/angular/angular.js/pull/4833
if (e.isImmediatePropagationStopped && e.isImmediatePropagationStopped()) {
return;
}
// This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
// I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
// https://github.com/angular/angular.js/pull/4833
if (e.isImmediatePropagationStopped && e.isImmediatePropagationStopped()) {
return;
}

if (hotkeys.indexOf(e.keyCode) === -1) {
return;
}
if (hotkeys.indexOf(e.keyCode) === -1) {
return;
}

key = e.keyCode;
key = e.keyCode;

if (key === KEYS.enter && scope.options.addOnEnter ||
key === KEYS.comma && scope.options.addOnComma ||
key === KEYS.space && scope.options.addOnSpace) {
if (key === KEYS.enter && scope.options.addOnEnter ||
key === KEYS.comma && scope.options.addOnComma ||
key === KEYS.space && scope.options.addOnSpace) {

if (scope.tryAdd()) {
scope.$apply();
if (scope.tryAdd()) {
scope.$apply();
}
e.preventDefault();
}
e.preventDefault();
}
else if (key === KEYS.backspace && this.value.length === 0) {
if (scope.tryRemoveLast()) {
scope.$apply();
else if (key === KEYS.backspace && this.value.length === 0) {
if (scope.tryRemoveLast()) {
scope.$apply();

e.preventDefault();
e.preventDefault();
}
}
}
});
})
.on('blur', function() {
if (!scope.options.addOnBlur) {
return;
}

$timeout(function() {
var parentElement = angular.element($document[0].activeElement).parent();
if (parentElement[0] !== element[0] && scope.tryAdd()) {
scope.$apply();
}
}, 0);
});

element.find('div').on('click', function() {
input[0].focus();
Expand Down
Binary file modified build/ng-tags-input.min.zip
Binary file not shown.
Binary file modified build/ng-tags-input.zip
Binary file not shown.
4 changes: 4 additions & 0 deletions css/tags-input.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
position: relative;
}

.ngTagsInput:active {
outline: none;
}

.ngTagsInput .tags {
-moz-appearance: textfield;
-webkit-appearance: textfield;
Expand Down
69 changes: 42 additions & 27 deletions src/tags-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ angular.module('tags-input', []);
* @param {boolean=} [addOnEnter=true] Flag indicating that a new tag will be added on pressing the ENTER key.
* @param {boolean=} [addOnSpace=false] Flag indicating that a new tag will be added on pressing the SPACE key.
* @param {boolean=} [addOnComma=true] Flag indicating that a new tag will be added on pressing the COMMA key.
* @param {boolean=} [addOnBlur=true] Flag indicating that a new tag will be added when the input field loses focus.
* @param {boolean=} [replaceSpacesWithDashes=true] Flag indicating that spaces will be replaced with dashes.
* @param {string=} [allowedTagsPattern=^[a-zA-Z0-9\s]+$*] Regular expression that determines whether a new tag is valid.
* @param {boolean=} [enableEditingLastTag=false] Flag indicating that the last tag will be moved back into
Expand All @@ -28,7 +29,7 @@ angular.module('tags-input', []);
* @param {expression} onTagAdded Expression to evaluate upon adding a new tag. The new tag is available as $tag.
* @param {expression} onTagRemoved Expression to evaluate upon removing an existing tag. The removed tag is available as $tag.
*/
angular.module('tags-input').directive('tagsInput', function(configuration) {
angular.module('tags-input').directive('tagsInput', function($timeout, $document, configuration) {
function SimplePubSub() {
var events = {};

Expand Down Expand Up @@ -56,7 +57,7 @@ angular.module('tags-input').directive('tagsInput', function(configuration) {
},
replace: false,
transclude: true,
template: '<div class="ngTagsInput" ng-class="options.customClass" transclude-append>' +
template: '<div class="ngTagsInput" tabindex="-1" ng-class="options.customClass" transclude-append>' +
' <div class="tags">' +
' <ul>' +
' <li ng-repeat="tag in tags" ng-class="getCssClass($index)">' +
Expand Down Expand Up @@ -88,6 +89,7 @@ angular.module('tags-input').directive('tagsInput', function(configuration) {
addOnEnter: { type: Boolean, defaultValue: true },
addOnSpace: { type: Boolean, defaultValue: false },
addOnComma: { type: Boolean, defaultValue: true },
addOnBlur: { type: Boolean, defaultValue: true },
allowedTagsPattern: { type: RegExp, defaultValue: /^[a-zA-Z0-9\s]+$/ },
enableEditingLastTag: { type: Boolean, defaultValue: false }
});
Expand Down Expand Up @@ -190,39 +192,52 @@ angular.module('tags-input').directive('tagsInput', function(configuration) {
var hotkeys = [KEYS.enter, KEYS.comma, KEYS.space, KEYS.backspace];
var input = element.find('input');

input.on('keydown', function(e) {
var key;
input
.on('keydown', function(e) {
var key;

// This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
// I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
// https://github.com/angular/angular.js/pull/4833
if (e.isImmediatePropagationStopped && e.isImmediatePropagationStopped()) {
return;
}
// This hack is needed because jqLite doesn't implement stopImmediatePropagation properly.
// I've sent a PR to Angular addressing this issue and hopefully it'll be fixed soon.
// https://github.com/angular/angular.js/pull/4833
if (e.isImmediatePropagationStopped && e.isImmediatePropagationStopped()) {
return;
}

if (hotkeys.indexOf(e.keyCode) === -1) {
return;
}
if (hotkeys.indexOf(e.keyCode) === -1) {
return;
}

key = e.keyCode;
key = e.keyCode;

if (key === KEYS.enter && scope.options.addOnEnter ||
key === KEYS.comma && scope.options.addOnComma ||
key === KEYS.space && scope.options.addOnSpace) {
if (key === KEYS.enter && scope.options.addOnEnter ||
key === KEYS.comma && scope.options.addOnComma ||
key === KEYS.space && scope.options.addOnSpace) {

if (scope.tryAdd()) {
scope.$apply();
if (scope.tryAdd()) {
scope.$apply();
}
e.preventDefault();
}
e.preventDefault();
}
else if (key === KEYS.backspace && this.value.length === 0) {
if (scope.tryRemoveLast()) {
scope.$apply();
else if (key === KEYS.backspace && this.value.length === 0) {
if (scope.tryRemoveLast()) {
scope.$apply();

e.preventDefault();
e.preventDefault();
}
}
}
});
})
.on('blur', function() {
if (!scope.options.addOnBlur) {
return;
}

$timeout(function() {
var parentElement = angular.element($document[0].activeElement).parent();
if (parentElement[0] !== element[0] && scope.tryAdd()) {
scope.$apply();
}
}, 0);
});

element.find('div').on('click', function() {
input[0].focus();
Expand Down
96 changes: 94 additions & 2 deletions test/tags-input.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
'use strict';

describe('tags-input-directive', function() {
var $compile, $scope,
var $compile, $scope, $timeout, $document,
isolateScope, element;

beforeEach(function() {
module('tags-input');

inject(function(_$compile_, _$rootScope_) {
inject(function(_$compile_, _$rootScope_, _$document_, _$timeout_) {
$compile = _$compile_;
$scope = _$rootScope_;
$document = _$document_;
$timeout = _$timeout_;
});
});

Expand Down Expand Up @@ -332,6 +334,96 @@ describe('tags-input-directive', function() {
});
});

describe('add-on-blur option', function() {
it('initializes the option to true', function() {
// Arrange/Act
compile();

// Assert
expect(isolateScope.options.addOnBlur).toBe(true);
});

it('sets the option given a static string', function() {
// Arrange/Act
compile('add-on-blur="false"');

// Assert
expect(isolateScope.options.addOnBlur).toBe(false);
});

it('sets the option given an interpolated string', function() {
// Arrange
$scope.value = false;

// Act
compile('add-on-blur="{{ value }}"');

// Assert
expect(isolateScope.options.addOnBlur).toBe(false);
});

it('ensures the outermost div element has a tabindex attribute set to -1', function() {
// Arrange/Act
compile();

// Assert
expect(element.find('div').attr('tabindex')).toBe('-1');
});

describe('option is true', function() {
var anotherElement;

beforeEach(function() {
compile('add-on-blur="true"');
anotherElement = angular.element('<div></div>');

$document.find('body')
.append(element)
.append(anotherElement);
});

it('adds a tag when the input field loses focus to any element on the page but the directive itself', function() {
// Arrange
isolateScope.newTag = 'foo';
anotherElement[0].focus();

// Act
getInput().trigger('blur');
$timeout.flush();

// Assert
expect($scope.tags).toEqual(['foo']);
});

it('does not add a tag when the input field loses focus to the directive itself', function() {
// Arrange
isolateScope.newTag = 'foo';
element.find('div')[0].focus();

// Act
getInput().trigger('blur');
$timeout.flush();

// Assert
expect($scope.tags).toEqual([]);
});
});

describe('option is off', function() {
it('does not add a new tag when the input field loses focus', function() {
// Arrange
compile('add-on-blur="false"');
isolateScope.newTag = 'foo';

// Act
getInput().trigger('blur');

// Assert
expect($scope.tags).toEqual([]);
});
});
});

describe('placeholder option', function() {
it('sets the input field placeholder text', function() {
// Arrange/Act
Expand Down
5 changes: 4 additions & 1 deletion test/test-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
<body ng-controller="Ctrl">
<tags-input ng-model="tags"
placeholder="{{ placeholder.value }}"
replace-spaces-with-dashes="false">
replace-spaces-with-dashes="false"
add-on-blur="false">
<auto-complete source="loadItems($query)"
debounce-delay="0"
min-length="1"
Expand All @@ -18,6 +19,8 @@
</auto-complete>
</tags-input>

<input type="text"/>

<script type="text/javascript">
angular.module('app', ['tags-input'])
.controller('Ctrl', function($scope, $q) {
Expand Down

0 comments on commit 69415a2

Please sign in to comment.