Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions modules/components/tag-input/tag-input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ describe('TagInputComponent', () => {
component = getComponent(fixture);
component.removeItem(tagName, 0);

expect(component.selectedTag).toBe(undefined);
expect(component.selectedTags.length).toBe(0);
}));
});

Expand Down Expand Up @@ -328,33 +328,33 @@ describe('TagInputComponent', () => {
component = getComponent(fixture);

// selected tag is undefined
expect(component.selectedTag).toEqual(undefined);
expect(component.selectedTags.length).toEqual(0);

keyDown['keyCode'] = 8;
// press backspace
component.inputForm.input.nativeElement.dispatchEvent(keyDown);

// selected tag is the last one
expect(component.selectedTag).toEqual('Typescript');
expect(component.selectedTags[0]).toEqual('Typescript');

// press tab and focus input again
keyDown['keyCode'] = 9;
component.tags.last.element.nativeElement.dispatchEvent(keyDown);

expect(component.selectedTag).toEqual(undefined);
expect(component.selectedTags.length).toEqual(0);
expect(component.inputForm.isInputFocused()).toEqual(true);

keyDown['keyCode'] = 8;
// then starts from back again
component.inputForm.input.nativeElement.dispatchEvent(keyDown);

expect(component.selectedTag).toEqual('Typescript');
expect(component.selectedTags[0]).toEqual('Typescript');

// it removes current selected tag when pressing delete
component.tags.last.element.nativeElement.dispatchEvent(keyDown);

expect(component.items.length).toEqual(1);
expect(component.selectedTag).toBe(undefined);
expect(component.selectedTags.length).toBe(0);

discardPeriodicTasks();
}));
Expand All @@ -367,22 +367,22 @@ describe('TagInputComponent', () => {
component.inputForm.input.nativeElement.dispatchEvent(keyDown);

// selected tag is the last one
expect(component.selectedTag).toEqual('Typescript');
expect(component.selectedTags[0]).toEqual('Typescript');

// press left arrow
component.tags.last.element.nativeElement.dispatchEvent(keyDown);
expect(component.selectedTag).toEqual('Javascript');
expect(component.selectedTags[0]).toEqual('Javascript');

// press right arrow
keyDown['keyCode'] = 39;
component.tags.first.element.nativeElement.dispatchEvent(keyDown);
expect(component.selectedTag).toEqual('Typescript');
expect(component.selectedTags[0]).toEqual('Typescript');

// press tab -> focuses input
keyDown['keyCode'] = 9;
component.tags.last.element.nativeElement.dispatchEvent(keyDown);

expect(component.selectedTag).toEqual(undefined);
expect(component.selectedTags.length).toEqual(0);
expect(component.inputForm.isInputFocused()).toEqual(true);

discardPeriodicTasks();
Expand All @@ -396,12 +396,12 @@ describe('TagInputComponent', () => {
component.inputForm.input.nativeElement.dispatchEvent(keyDown);

// selected tag is the last one
expect(component.selectedTag).toEqual('Typescript');
expect(component.selectedTags[0]).toEqual('Typescript');

keyDown['keyCode'] = 9;
// press tab -> focuses input
component.tags.last.element.nativeElement.dispatchEvent(keyDown);
expect(component.selectedTag).toEqual(undefined);
expect(component.selectedTags.length).toEqual(0);

expect(component.inputForm.isInputFocused()).toEqual(true);

Expand Down Expand Up @@ -668,7 +668,7 @@ describe('TagInputComponent', () => {
component = getComponent(fixture);

// selected tag is undefined
expect(component.selectedTag).toEqual(undefined);
expect(component.selectedTags.length).toEqual(0);

// enable editing mode
component.tags.first.toggleEditMode();
Expand Down Expand Up @@ -701,7 +701,7 @@ describe('TagInputComponent', () => {
component = getComponent(fixture);

// selected tag is undefined
expect(component.selectedTag).toEqual(undefined);
expect(component.selectedTags.length).toEqual(0);

// enable editing mode
component.tags.first.toggleEditMode();
Expand Down
2 changes: 2 additions & 0 deletions modules/components/tag-input/tag-input.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<div class="ng2-tags-container">
<tag
*ngFor="let item of items; let i = index; trackBy: trackBy"
class="ng2-tag"
(onSelect)="selectItem(item)"
(onRemove)="onRemoveRequested(item, i)"
(onKeyDown)="handleKeydown($event)"
Expand All @@ -31,6 +32,7 @@
(dragleave)="dragZone ? dragProvider.onDragEnd() : undefined"
[canAddTag]="isTagValid"
[attr.tabindex]="0"
[class.tag--selected]="isAnySelectedFocused() && isInSelection(item)"
[disabled]="disable"
[@animation]="animationMetadata"
[hasRipple]="ripple"
Expand Down
152 changes: 138 additions & 14 deletions modules/components/tag-input/tag-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ContentChildren,
ContentChild,
OnInit,
OnDestroy,
TemplateRef,
QueryList,
AfterViewInit
Expand All @@ -25,7 +26,8 @@ import {
} from '@angular/forms';

// rx
import { Observable, debounceTime, filter, map, first } from 'rxjs';
import { Observable } from 'rxjs';
import { map, debounceTime, filter, first } from 'rxjs/operators';

// ng2-tag-input
import { TagInputAccessor } from '../../core/accessor';
Expand Down Expand Up @@ -56,7 +58,7 @@ const CUSTOM_ACCESSOR = {
templateUrl: './tag-input.template.html',
animations
})
export class TagInputComponent extends TagInputAccessor implements OnInit, AfterViewInit {
export class TagInputComponent extends TagInputAccessor implements OnInit, AfterViewInit, OnDestroy {
/**
* @name separatorKeys
* @desc keyboard keys with which a user can separate items
Expand Down Expand Up @@ -174,6 +176,21 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
*/
@Input() public editable: boolean = defaults.tagInput.editable;

/**
* @name copyable
*/
@Input() public copyable: boolean = defaults.tagInput.copyable;

/**
* @name joinSeparator
*/
@Input() public joinSeparator: string = defaults.tagInput.joinSeparator;

/**
* @name multipleSelect
*/
@Input() public multipleSelect: boolean = defaults.tagInput.multipleSelect;

/**
* @name allowDupes
*/
Expand Down Expand Up @@ -249,7 +266,7 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
* @name onSelect
* @desc event emitted when selecting an item
*/
@Output() public onSelect = new EventEmitter<TagModel>();
@Output() public onSelect = new EventEmitter<TagModel[]>();

/**
* @name onFocus
Expand Down Expand Up @@ -305,9 +322,9 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After

/**
* @name selectedTag
* @desc reference to the current selected tag
* @desc reference to the current selected tags
*/
public selectedTag: TagModel | undefined;
public selectedTags: TagModel[] | undefined = [];

/**
* @name isLoading
Expand Down Expand Up @@ -338,6 +355,8 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
[constants.KEYUP]: <{ (fun): any }[]>[]
};

private copyListener: ((e: ClipboardEvent) => void) | undefined;

/**
* @description emitter for the 2-way data binding inputText value
* @name inputTextChange
Expand Down Expand Up @@ -397,6 +416,10 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
this.setUpOnPasteListener();
}

if (this.copyable) {
this.setUpCopyListeners();
}

const statusChanges$ = this.inputForm.form.statusChanges;

statusChanges$.pipe(
Expand Down Expand Up @@ -440,6 +463,17 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
this.setAnimationMetadata();
}

/**
* @name ngOnDestroy
*/
public ngOnDestroy(): void {
// remove copy listeners if defined
if (this.copyListener) {
document.removeEventListener('copy', this.copyListener);
this.copyListener = undefined;
}
}

/**
* @name onRemoveRequested
* @param tag
Expand Down Expand Up @@ -498,6 +532,15 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
];
}

/**
* @name isInSelection
* @desc checks if the item is in current selection
* @param item
*/
public isInSelection(item: TagModel | undefined): boolean {
return !!(this.selectedTags.length && this.selectedTags.includes(item));
}

/**
* @name createTag
* @param model
Expand All @@ -523,14 +566,55 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
public selectItem(item: TagModel | undefined, emit = true): void {
const isReadonly = item && typeof item !== 'string' && item.readonly;

if (isReadonly || this.selectedTag === item) {
if (isReadonly) {
return;
}

this.selectedTag = item;

if (emit) {
this.onSelect.emit(item);
if (!!item) {
if (!this.multipleSelect) {
if (!this.selectedTags.includes(item)) {
this.selectedTags = [item];
if (emit) {
this.onSelect.emit(this.selectedTags);
}
}
} else {
const event = window.event as MouseEvent;
if (this.isInSelection(item)) {
if (event && (event.ctrlKey || event.metaKey)) {
if (this.selectedTags.length > 1) {
// remove from selection
this.selectedTags = this.selectedTags.filter(tag => tag != item);

// then select the first tag in the selection
const tagComponent = this.tags.filter(tag => tag.model == this.selectedTags[0])[0];
tagComponent.focus();
} else {
// select only this
this.selectedTags = [item];
}
} else if (event && event.shiftKey) {
// select range
this.selectedTags = this.selectRange(item);
} else {
// select only this
this.selectedTags = [item];
}
} else {
if (event && (event.ctrlKey || event.metaKey)) {
// add to selection
this.selectedTags.push(item);
} else if (event && event.shiftKey) {
// select range
this.selectedTags = this.selectRange(item);
} else {
// select only this
this.selectedTags = [item];
}
}
}
} else {
this.selectedTags = [];
}
}

Expand All @@ -556,9 +640,9 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After

switch (constants.KEY_PRESS_ACTIONS[key]) {
case constants.ACTIONS_KEYS.DELETE:
if (this.selectedTag && this.removable) {
const index = this.items.indexOf(this.selectedTag);
this.onRemoveRequested(this.selectedTag, index);
if (this.selectedTags.length === 1 && this.removable) {
const index = this.items.indexOf(this.selectedTags[0]);
this.onRemoveRequested(this.selectedTags[0], index);
}
break;

Expand Down Expand Up @@ -813,6 +897,32 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
return assertions.filter(Boolean).length === assertions.length;
}

public isAnySelectedFocused(): boolean {
if (!this.tags || !this.tags.length || !this.selectedTags || !this.selectedTags.length) {
return false;
}

const tagComponents = this.tags.filter(tag => this.selectedTags.includes(tag.model));
return !!(tagComponents.length && tagComponents.some(tag => tag.isFocused()));
}

private selectRange(item: TagModel): TagModel[] {
const indices = this.selectedTags.map(tag => this.items.indexOf(tag));
const min = Math.min(...indices);
const max = Math.max(...indices);
const selectedIndex = this.items.indexOf(item);

let start = min;
let end = selectedIndex;
if (selectedIndex < min) {
start = selectedIndex;
end = max;
}

const slice = this.items.slice(start, (end + 1));
return slice;
}

/**
* @name moveToTag
* @param item
Expand Down Expand Up @@ -882,7 +992,7 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
this.items = this.getItemsWithout(index);

// if the removed tag was selected, set it as undefined
if (this.selectedTag === tag) {
if (this.selectedTags.length === 1 && this.selectedTags[0] === tag) {
this.selectItem(undefined, false);
}

Expand Down Expand Up @@ -1049,6 +1159,20 @@ export class TagInputComponent extends TagInputAccessor implements OnInit, After
});
}

private setUpCopyListeners() {
this.copyListener = (event: ClipboardEvent) => {
if (this.isAnySelectedFocused()) {
const text = this.selectedTags.map(tag => this.getItemDisplay(tag)).join(this.joinSeparator);
event.clipboardData.setData('text/plain', text);

event.preventDefault();
}
};

document.removeEventListener('copy', this.copyListener);
document.addEventListener('copy', this.copyListener);
}

/**
* @name setUpTextChangeSubscriber
*/
Expand Down
3 changes: 1 addition & 2 deletions modules/components/tag-input/tests/testing-helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import {
FormControl
} from '@angular/forms';

import { Observable } from 'rxjs';
import { of } from 'rxjs';
import { Observable, of } from 'rxjs';

import { TagInputModule } from '../../../tag-input.module';

Expand Down
Loading