+
- @if (!hasTemplate(group, 'group')) {
-
- } @else {
-
- }
-
+ -
+
+ @if (!hasTemplate(group, 'group')) {
+
+ } @else {
+
+ }
+
+
@for (option of group.options; track option; let j = $index) {
-
+
+
+
+
diff --git a/projects/ng-select2-component/src/lib/select2.component.scss b/projects/ng-select2-component/src/lib/select2.component.scss
index ed03ecd..f32ba66 100644
--- a/projects/ng-select2-component/src/lib/select2.component.scss
+++ b/projects/ng-select2-component/src/lib/select2.component.scss
@@ -258,20 +258,35 @@
height: 0;
content: ' ';
}
+ }
- .select2-selection__reset {
- color: var(--select2-reset-color, #999);
- }
+ .select2-selection__reset {
+ color: var(--select2-reset-color, #999);
+ background: var(--select2-reset-background, transparent);
+ border: var(--select2-reset-border, none);
+ border-radius: var(--select2-reset-border-radius, 4px);
+ height: fit-content;
+ align-self: center;
}
+ &.select2-container--disabled,
+ &.select2-container--readonly {
+ .select2-selection--single .select2-selection__clear,
+ .select2-selection__choice__remove {
+ display: none;
+ }
+ }
&.select2-container--disabled {
- .select2-selection--single {
+ .select2-selection--single,
+ .select2-selection--multiple {
cursor: default;
background: var(--select2-selection-disabled-background, #eee);
-
- .select2-selection__clear {
- display: none;
- }
+ }
+ }
+ &.select2-container--readonly {
+ .select2-selection--single,
+ .select2-selection--multiple {
+ background: var(--select2-selection-readonly-background, #eee);
}
}
@@ -372,17 +387,6 @@
}
}
- &.select2-container--disabled {
- .select2-selection--multiple {
- cursor: default;
- background: var(--select2-selection-disabled-background, #eee);
- }
-
- .select2-selection__choice__remove {
- display: none;
- }
- }
-
&.select2-container--open.select2-container--above {
.select2-selection--single,
.select2-selection--multiple {
@@ -422,7 +426,7 @@
}
.select2-results__option {
- &[role='group'] {
+ &.select2-results__group {
grid-column: col-start / col-end;
padding: 0;
}
@@ -717,18 +721,35 @@
color: var(--select2-material-option-selected-text-color, #ff5722);
}
+ &.select2-container--disabled,
+ &.select2-container--readonly {
+ .select2-selection--single,
+ .select2-selection--multiple {
+ background: transparent;
+ }
+ }
&.select2-container--disabled .select2-selection--single,
&.select2-container--disabled .select2-selection--multiple {
- background: transparent;
-
&::before {
background: var(
--select2-material-underline-disabled,
linear-gradient(to right, rgba(0, 0, 0, 0.26) 0, rgba(0, 0, 0, 0.26) 33%, transparent 0)
);
+ background-size: 4px 1px;
+ background-repeat: repeat-x;
background-position: 0 bottom;
+ }
+ }
+ &.select2-container--readonly .select2-selection--single,
+ &.select2-container--readonly .select2-selection--multiple {
+ &::before {
+ background: var(
+ --select2-material-underline-readonly,
+ linear-gradient(to right, rgba(0, 0, 0, 0.26) 0, rgba(0, 0, 0, 0.26) 33%, transparent 0)
+ );
background-size: 4px 1px;
background-repeat: repeat-x;
+ background-position: 0 bottom;
}
}
}
diff --git a/projects/ng-select2-component/src/lib/select2.component.ts b/projects/ng-select2-component/src/lib/select2.component.ts
index ded94d6..3da4738 100644
--- a/projects/ng-select2-component/src/lib/select2.component.ts
+++ b/projects/ng-select2-component/src/lib/select2.component.ts
@@ -22,6 +22,7 @@ import {
Self,
TemplateRef,
booleanAttribute,
+ computed,
input,
numberAttribute,
output,
@@ -53,7 +54,14 @@ import { Select2Utils } from './select2-utils';
let nextUniqueId = 0;
-const displaySearchStatusList = ['default', 'hidden', 'always'];
+interface KeyInfo {
+ key: string;
+ altKey: boolean;
+}
+
+const OPEN_KEYS: (string | KeyInfo)[] = ['ArrowDown', 'ArrowUp', 'Enter', ' ', 'Home', 'End', 'PageUp', 'PageDown'];
+const ON_OPEN_KEYS: (string | KeyInfo)[] = ['Home', 'End', 'PageUp', 'PageDown'];
+const CLOSE_KEYS: (string | KeyInfo)[] = ['Escape', 'Tab', { key: 'ArrowUp', altKey: true }];
@Component({
selector: 'select2',
@@ -61,12 +69,13 @@ const displaySearchStatusList = ['default', 'hidden', 'always'];
styleUrls: ['./select2.component.scss'],
imports: [CdkOverlayOrigin, NgTemplateOutlet, CdkConnectedOverlay, InfiniteScrollDirective],
host: {
- '[id]': '_id',
+ '[id]': 'id()',
'[class.select2-selection-nowrap]': 'selectionNoWrap()',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterViewInit, OnDestroy {
+ readonly _uid = `select2-${nextUniqueId++}`;
// ----------------------- signal-input
/** data of options & option groups */
@@ -133,7 +142,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
readonly minCountForSearch = input(undefined, { transform: numberAttribute });
/** Unique id of the element. */
- readonly id = input
();
+ readonly id = input(this._uid);
+ readonly idLabel = computed(() => `${this.id()}-label`);
+ readonly idCombo = computed(() => `${this.id()}-combo`);
+ readonly idOptions = computed(() => `${this.id()}-options`);
+ readonly idOverlay = computed(() => `${this.id()}-overlay`);
/** Whether the element is required. */
readonly required = input(false, { transform: booleanAttribute });
@@ -185,6 +198,18 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
/** Text for select all options */
readonly selectAllText = input('Select all');
+ // WAI related inputs
+ /** title attribute applied to the input */
+ readonly title = input();
+ /** aria-labelledby attribute applied to the input, to specify en external label */
+ readonly ariaLabelledby = input();
+ /** aria-describedby attribute applied to the input */
+ readonly ariaDescribedby = input();
+ /** aria-invalid attribute applied to the input, to force error state */
+ readonly ariaInvalid = input(false, { transform: booleanAttribute });
+ /** description of the reset button when using 'resettable'. Default value : 'Reset' */
+ readonly ariaResetButtonDescription = input('Reset');
+
// ----------------------- signal-output
readonly update = output>();
@@ -208,11 +233,6 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
// ----------------------- HostBinding
- @HostBinding('attr.aria-invalid')
- get ariaInvalid(): boolean {
- return this._isErrorState();
- }
-
@HostBinding('class.material')
get classMaterial(): boolean {
return this.styleMode() === 'material';
@@ -244,11 +264,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
filteredData = signal(undefined);
- get select2Options() {
- return this.multiple() ? (this.selectedOption as Select2Option[]) : null;
+ get select2Options(): Select2Option[] {
+ return this.multiple() ? (this.selectedOption as Select2Option[]) ?? [] : [];
}
- get select2Option() {
+ get select2Option(): Select2Option | null {
return this.multiple() ? null : (this.selectedOption as Select2Option);
}
@@ -302,9 +322,11 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
protected maxResultsExceeded: boolean | undefined;
- private hoveringValue: Select2Value | null | undefined = null;
+ private hoveringOption = signal(null);
+ hoveringOptionId = computed(() => this.getElementId(this.hoveringOption()));
+
private innerSearchText = '';
- private isSearchboxHidden: boolean | undefined;
+ protected isSearchboxHidden: boolean | undefined;
private selectionElement: HTMLElement | undefined;
@@ -321,13 +343,8 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
private _data: Select2Data = [];
- get _id(): string {
- return this.id() || this._uid;
- }
-
private _disabled = false;
- private _uid = `select2-${nextUniqueId++}`;
protected _value: Select2UpdateValue | null = null;
private _previousNativeValue: Select2UpdateValue | undefined;
private _overlayPosition: VerticalConnectionPos | undefined;
@@ -341,9 +358,6 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
@Self() @Optional() public _control: NgControl,
@Attribute('tabindex') tabIndex: string,
) {
- // eslint-disable-next-line no-self-assign
- this.id = this.id;
-
if (this._control) {
this._control.valueAccessor = this;
}
@@ -389,10 +403,10 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
if (!this.ifParentContainsClass(target, 'select2-dropdown')) {
this.toggleOpenAndClose();
}
- if (!this.overlay() && !this.ifParentContainsId(target, this._id)) {
+ if (!this.overlay() && !this.ifParentContainsId(target, this.id())) {
this.clickExit();
}
- } else if (!this.ifParentContainsId(target, this._id)) {
+ } else if (!this.ifParentContainsId(target, this.id())) {
this.toggleOpenAndClose();
this.clickExit();
}
@@ -428,7 +442,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
this.selectedOption = option ?? null;
}
if (!Array.isArray(option)) {
- this.hoveringValue = this.value();
+ this.hoveringOption.set(Select2Utils.getOptionByValue(this._data, this.value));
}
this.updateSearchBox();
}
@@ -474,37 +488,29 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
}
updateSearchBox() {
- const hidden = this.customSearchEnabled()
- ? false
- : Select2Utils.isSearchboxHidden(this._data, this.minCountForSearch());
+ const hidden =
+ this.displaySearchStatus() === 'hidden' ||
+ (this.displaySearchStatus() !== 'always' &&
+ !this.customSearchEnabled() &&
+ Select2Utils.isSearchboxHidden(this._data, this.minCountForSearch()));
+
if (this.isSearchboxHidden !== hidden) {
this.isSearchboxHidden = hidden;
}
}
- hideSearch(): boolean {
- if (this.autoCreate() && !this.multiple()) {
- return false;
- }
- const displaySearchStatus =
- this.displaySearchStatus() && displaySearchStatusList.indexOf(this.displaySearchStatus()!) > -1
- ? this.displaySearchStatus()
- : 'default';
- return (displaySearchStatus === 'default' && this.isSearchboxHidden) || displaySearchStatus === 'hidden';
- }
-
getOptionStyle(option: Select2Option) {
return (
'select2-results__option ' +
(option.hide ? 'select2-results__option--hide ' : '') +
- (option.value === this.hoveringValue ? 'select2-results__option--highlighted ' : '') +
+ (option === this.hoveringOption() ? 'select2-results__option--highlighted ' : '') +
(option.classes || '')
);
}
mouseenter(option: Select2Option) {
if (!option.disabled) {
- this.hoveringValue = option.value;
+ this.hoveringOption.set(option);
}
}
@@ -525,6 +531,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
if (event) {
this.stopEvent(event);
}
+ this._focus(true);
}
prevChange(event: Event) {
@@ -537,18 +544,19 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
}
toggleOpenAndClose(focus = true, open?: boolean, event?: KeyboardEvent) {
- if (this.disabledState) {
+ if (this.disabledState || this.readonly()) {
return;
}
this._focus(focus);
+ const onOpenAction = event && this._testKey(event, ON_OPEN_KEYS);
const changeEmit = this.isOpen !== (open ?? !this.isOpen);
this.isOpen = open ?? !this.isOpen;
if (this.isOpen) {
if (!this.isSearchboxHidden) {
this.innerSearchText = '';
this.updateFilteredData();
- this._focusSearchboxOrResultsElement(focus);
+ this._focusSearchbox(focus);
}
if (this.isSearchboxHidden && !changeEmit && event) {
@@ -560,6 +568,9 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
} else if (this.resultsElement) {
this.resultsElement.scrollTop = 0;
}
+ if (onOpenAction) {
+ this.keyDown(event);
+ }
this._changeDetectorRef.detectChanges();
this.triggerRect();
@@ -625,14 +636,14 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
if (!this.selectAllTest()) {
const options: Select2Option[] = [];
this._data.forEach(e => {
- if ((e as Select2Group).options) {
- (e as Select2Group).options.forEach(f => {
+ if (Select2Utils.isGroup(e)) {
+ e.options.forEach(f => {
if (!f.disabled && !f.hide) {
options.push(f);
}
});
- } else if (!(e as Select2Option).disabled && !(e as Select2Option).hide) {
- options.push(e as Select2Option);
+ } else if (!e.disabled && !e.hide) {
+ options.push(e);
}
});
this.selectedOption = options;
@@ -651,13 +662,13 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
if (this.multiple() && Array.isArray(this.selectedOption) && this.selectedOption.length) {
let options = 0;
this._data.forEach(e => {
- if ((e as Select2Group).options) {
- (e as Select2Group).options.forEach(f => {
+ if (Select2Utils.isGroup(e)) {
+ e.options.forEach(f => {
if (!f.disabled && !f.hide) {
options++;
}
});
- } else if (!(e as Select2Option).disabled && !(e as Select2Option).hide) {
+ } else if (!e.disabled && !e.hide) {
options++;
}
});
@@ -718,8 +729,8 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
this.maxResultsExceeded = false;
}
- if (Select2Utils.valueIsNotInFilteredData(result, this.hoveringValue)) {
- this.hoveringValue = Select2Utils.getFirstAvailableOption(result);
+ if (Select2Utils.optionIsNotInFilteredData(result, this.hoveringOption())) {
+ this.hoveringOption.set(Select2Utils.getFirstAvailableOption(result));
}
this.filteredData.set(result);
@@ -730,14 +741,14 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
const options: Select2Option[] = [];
const value = this.selectedOption.map(e => e.value);
this._data.forEach(e => {
- if ((e as Select2Group).options) {
- (e as Select2Group).options.forEach(f => {
+ if (Select2Utils.isGroup(e)) {
+ e.options.forEach(f => {
if (value.includes(f.value)) {
options.push(f);
}
});
- } else if (value.includes((e as Select2Option).value)) {
- options.push(e as Select2Option);
+ } else if (value.includes(e.value)) {
+ options.push(e);
}
});
// preserve selection order
@@ -745,14 +756,14 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
} else if (!Array.isArray(this.selectedOption) && this.selectedOption) {
let option: Select2Option | null = null;
this._data.forEach(e => {
- if ((e as Select2Group).options) {
- (e as Select2Group).options.forEach(f => {
+ if (Select2Utils.isGroup(e)) {
+ e.options.forEach(f => {
if ((this.selectedOption as Select2Option).value === f.value) {
option = f;
}
});
- } else if ((this.selectedOption as Select2Option).value === (e as Select2Option).value) {
- option = e as Select2Option;
+ } else if ((this.selectedOption as Select2Option).value === e.value) {
+ option = e;
}
});
this.selectedOption = option;
@@ -764,15 +775,19 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
this._focus(false);
}
- private ifParentContainsClass(element: HTMLElement, cssClass: string): boolean {
+ private isInSelect(elt: Element): boolean {
+ return this.ifParentContainsId(elt, this.id()) || this.ifParentContainsId(elt, this.idOverlay());
+ }
+
+ private ifParentContainsClass(element: Element, cssClass: string): boolean {
return this.getParentElementByClass(element, cssClass) !== null;
}
- private ifParentContainsId(element: HTMLElement, id: string): boolean {
+ private ifParentContainsId(element: Element, id: string): boolean {
return this.getParentElementById(element, id) !== null;
}
- private getParentElementByClass(element: HTMLElement, cssClass: string): HTMLElement | null {
+ private getParentElementByClass(element: Element, cssClass: string): Element | null {
return this.containClasses(element, cssClass.trim().split(/\s+/))
? element
: element.parentElement
@@ -780,7 +795,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
: null;
}
- private getParentElementById(element: HTMLElement, id: string): HTMLElement | null {
+ private getParentElementById(element: Element, id: string): Element | null {
return element.id === id
? element
: element.parentElement
@@ -788,7 +803,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
: null;
}
- private containClasses(element: HTMLElement, cssClasses: string[]): boolean {
+ private containClasses(element: Element, cssClasses: string[]): boolean {
if (!element.classList) {
return false;
}
@@ -826,14 +841,14 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
]);
}
- focusin() {
+ focusin(options?: FocusOptions) {
if (!this.disabledState) {
- this._focus(true);
+ this._focus(true, options);
}
}
- focusout() {
- if (this.selectionElement && !this.selectionElement.classList.contains('select2-focused')) {
+ focusout(event: FocusEvent) {
+ if (!event.relatedTarget || !this.isInSelect(event.relatedTarget as Element)) {
this._focus(false);
this._onTouched();
}
@@ -907,34 +922,43 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
keyDown(event: KeyboardEvent, create = false) {
if (create && this._testKey(event, ['Enter'])) {
this.createAndAdd(event);
- } else if (this._testKey(event, ['ArrowDown'])) {
+ } else if (this._testKey(event, [{ key: 'ArrowDown', altKey: false }])) {
this.moveDown();
event.preventDefault();
- } else if (this._testKey(event, ['ArrowUp'])) {
+ } else if (this._testKey(event, [{ key: 'ArrowUp', altKey: false }])) {
this.moveUp();
event.preventDefault();
- } else if (this._testKey(event, ['Enter'])) {
+ } else if (this._testKey(event, ['Home'])) {
+ this.moveStart();
+ event.preventDefault();
+ } else if (this._testKey(event, ['End'])) {
+ this.moveEnd();
+ event.preventDefault();
+ } else if (this._testKey(event, ['PageUp'])) {
+ this.moveUp(10);
+ event.preventDefault();
+ } else if (this._testKey(event, ['PageDown'])) {
+ this.moveDown(10);
+ event.preventDefault();
+ } else if (this._testKey(event, ['Enter']) || (this.isSearchboxHidden && this._testKey(event, [' ']))) {
this.selectByEnter();
event.preventDefault();
- } else if (this._testKey(event, ['Escape', 'Tab']) && this.isOpen) {
+ } else if (this._testKey(event, CLOSE_KEYS) && this.isOpen) {
this.toggleOpenAndClose();
- this._focus(false);
+ this._focus(true);
}
}
openKey(event: KeyboardEvent, create = false) {
if (create && this._testKey(event, ['Enter'])) {
this.createAndAdd(event);
- } else if (this._testKey(event, ['ArrowDown', 'ArrowUp', 'Enter'])) {
+ } else if (this._testKey(event, OPEN_KEYS)) {
this.toggleOpenAndClose(true, true, event);
event.preventDefault();
- } else if (this._testKey(event, ['Escape', 'Tab'])) {
+ } else if (this._testKey(event, CLOSE_KEYS)) {
if (this.isOpen) {
- this.toggleOpenAndClose(false);
+ this.toggleOpenAndClose();
this._onTouched();
- event.preventDefault();
- } else {
- this._focus(false);
}
}
}
@@ -966,6 +990,10 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
}
removeSelection(e: MouseEvent | KeyboardEvent | Event, option: Select2Option) {
+ if (this.readonly() || this.disabledState) {
+ return;
+ }
+
Select2Utils.removeSelection(this.selectedOption, option);
if (this.multiple() && this.hideSelectedItems()) {
@@ -999,7 +1027,9 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
e.stopPropagation();
if (this.isOpen) {
- this._focusSearchboxOrResultsElement();
+ this._focusSearchbox();
+ } else {
+ this._focus(true);
}
}
@@ -1079,6 +1109,33 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
return undefined;
}
+ getElementId(elt: Select2Group | Select2Option | null): string | null {
+ if (!elt) {
+ return null;
+ }
+
+ const [i, j] = this._getElementPath(elt);
+ const toSuffix = (index: number) => (index !== undefined ? `-${index}` : '');
+ return (elt as Select2Option).id ?? `${this.id()}-option${toSuffix(i)}${toSuffix(j)}`;
+ }
+
+ _getElementPath(elt: Select2Group | Select2Option): number[] {
+ for (let i = 0; i < this._data.length; i++) {
+ const optionOrGroup = this._data[i];
+
+ if (optionOrGroup === elt) {
+ return [i];
+ } else if (Select2Utils.isGroup(optionOrGroup)) {
+ const j = optionOrGroup.options.findIndex(o => o === elt);
+ if (j >= 0) {
+ return [i, j];
+ }
+ }
+ }
+
+ return [];
+ }
+
_toGroup(group: Select2Option | Select2Group) {
return group as Select2Group;
}
@@ -1125,17 +1182,29 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
this.stopEvent(e);
}
- private moveUp() {
- this.updateScrollFromOption(Select2Utils.getPreviousOption(this.filteredData()!, this.hoveringValue));
+ private moveUp(times = 1) {
+ for (let i = 0; i < times; i++) {
+ this.updateScrollFromOption(Select2Utils.getPreviousOption(this.filteredData()!, this.hoveringOption()));
+ }
+ }
+
+ private moveDown(times = 1) {
+ for (let i = 0; i < times; i++) {
+ this.updateScrollFromOption(Select2Utils.getNextOption(this.filteredData()!, this.hoveringOption()));
+ }
+ }
+
+ private moveStart() {
+ this.updateScrollFromOption(Select2Utils.getFirstOption(this.filteredData()!));
}
- private moveDown() {
- this.updateScrollFromOption(Select2Utils.getNextOption(this.filteredData()!, this.hoveringValue));
+ private moveEnd() {
+ this.updateScrollFromOption(Select2Utils.getLastOption(this.filteredData()!));
}
private updateScrollFromOption(option: Select2Option | null) {
if (option) {
- this.hoveringValue = option.value;
+ this.hoveringOption.set(option);
const domElement = this.results().find(r => r.nativeElement.innerText.trim() === option.label);
if (domElement && this.resultsElement) {
this.resultsElement.scrollTop = 0;
@@ -1147,34 +1216,20 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
}
private selectByEnter() {
- if (this.hoveringValue) {
- const option = Select2Utils.getOptionByValue(this._data, this.hoveringValue) ?? null;
- this.select(option);
- }
- }
-
- private _testKey(event: KeyboardEvent, refs: (number | string)[] = []): boolean {
- return this._isKey(this._getKey(event), refs);
- }
-
- private _getKey(event: KeyboardEvent): number | string {
- let code: number | string | undefined;
-
- if (event.key !== undefined) {
- code = event.key;
- } else if ((event as any)['keyIdentifier'] !== undefined) {
- code = (event as any)['keyIdentifier'];
- } else if ((event as any)['keyCode'] !== undefined) {
- code = (event as any)['keyCode'];
- } else {
- event.preventDefault();
+ if (this.hoveringOption()) {
+ this.select(this.hoveringOption());
}
-
- return code ?? '';
}
- private _isKey(code: number | string, refs: (number | string)[] = []): boolean {
- return refs && refs.length > 0 ? refs.indexOf(code) !== -1 : false;
+ private _testKey(event: KeyboardEvent, refs: (string | KeyInfo)[] = []): boolean {
+ const { key, altKey } = event;
+ return refs.some(ref => {
+ if (typeof ref === 'string') {
+ return ref === key;
+ } else {
+ return key === ref.key && altKey === ref.altKey;
+ }
+ });
}
/**
@@ -1250,7 +1305,7 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
}
}
- private _focusSearchboxOrResultsElement(focus = true) {
+ private _focusSearchbox(focus = true) {
if (!this.isSearchboxHidden) {
setTimeout(() => {
const searchInput = this.searchInput();
@@ -1258,20 +1313,24 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
searchInput.nativeElement.focus();
}
});
- if (this.resultsElement && focus) {
- this.resultsElement.focus();
- }
}
}
- private _focus(state: boolean) {
- if (!state && this.focused) {
- this.focused = state;
- this.blur.emit(this);
- } else if (state && !this.focused) {
- this.focused = state;
- this.focus.emit(this);
+ private _focus(state: boolean, options?: FocusOptions) {
+ if (state) {
+ const eltToFocus =
+ !this.isSearchboxHidden && this.isOpen ? this.searchInput()!.nativeElement : this.selection().nativeElement;
+ if (document.activeElement !== eltToFocus) {
+ eltToFocus.focus(options);
+ }
+ } else if (
+ document.activeElement === this.selection()?.nativeElement ||
+ document.activeElement === this.searchInput()?.nativeElement
+ ) {
+ (document.activeElement as HTMLElement).blur();
}
+
+ this._updateFocusState(state);
}
private _isAbobeOverlay(): boolean {
@@ -1280,4 +1339,14 @@ export class Select2 implements ControlValueAccessor, OnInit, DoCheck, AfterView
? this._overlayPosition === 'top'
: listPosition === 'above';
}
-}
\ No newline at end of file
+
+ protected _updateFocusState(state: boolean) {
+ if (!state && this.focused) {
+ this.focused = state;
+ this.blur.emit(this);
+ } else if (state && !this.focused) {
+ this.focused = state;
+ this.focus.emit(this);
+ }
+ }
+}
diff --git a/src/app/app-examples.component.html b/src/app/app-examples.component.html
index a93d1fd..1706a61 100644
--- a/src/app/app-examples.component.html
+++ b/src/app/app-examples.component.html
@@ -4,6 +4,49 @@ Examples
+
0. WAI
+
+ Label
+
+
+
External label
+
External description
+
+
+
+ Invalid field
+
+
+
+ Required field
+
+
+
+ Readonly field
+
+
+
+ Disabled field
+
+
+
+ Resettable field
+
+
1. options in group ({{ value1 }})
1. options in group ({{ value1 }})
(search)="search('search1', $event)"
resettable
customSearchEnabled
- id="selec2-1"
+ id="select2-1"
>
2. options ({{ value2 }})
-
+
3. less options ({{ value3 }})
-
+
4. disabled ({{ value4 }})
-
+
5. search box (infinity) ({{ value5 }})
@@ -61,7 +104,7 @@
[minCountForSearch]="limit.value"
[displaySearchStatus]="$any(status.value)"
(update)="update('value6', $event)"
- id="selec2-6"
+ id="select2-6"
>
7. placeholder ({{ value7 }})
@@ -71,7 +114,7 @@ 7. placeholder ({{ value7 }})
placeholder="select an item"
(update)="update('value7', $event)"
resettable
- id="selec2-7"
+ id="select2-7"
>
8. open, close and search event ({{ value8 }})
@@ -84,7 +127,7 @@
8. open, close and search event ({{ value8 }})
(close)="close8()"
(search)="search8($event)"
(update)="update('value8', $event)"
- id="selec2-8"
+ id="select2-8"
>
value : {{ value8 }}
@@ -102,7 +145,7 @@
9. multiple + limite
10. multiple + hide selected items ({{ value10 | json }})
@@ -116,7 +159,7 @@ 10. multiple + hide selected items ({{ value10 | json }})
multiple="true"
(update)="update('value10', $event)"
hideSelectedItems="true"
- id="selec2-10"
+ id="select2-10"
[selectionNoWrap]="selectionNoWrap"
>
@@ -129,7 +172,7 @@ 11. material style and form binding ({{ value11 }})
formControlName="test11"
placeholder="Select a state"
styleMode="material"
- id="selec2-11"
+ id="select2-11"
>
@@ -142,11 +185,11 @@ 12. material style ({{ value12 }})
[value]="value12"
(update)="update('value12', $event)"
styleMode="material"
- id="selec2-12"
+ id="select2-12"
>
13. boolean value ({{ value13 }})
-
+
14. FormControl ({{ fg.get('state')!.value }})
15. with label ({{ value15 }})
-
+
Select a state
16. required with label ({{ value16 }})
@@ -173,7 +216,7 @@ 16. required with label ({{ value16 }})
[value]="value16"
(update)="update('value16', $event)"
required
- id="selec2-16"
+ id="select2-16"
>
Select a state
@@ -187,7 +230,7 @@ 18. search starts with 3 chars
[value]="value18"
(update)="update('value18', $event)"
minCharForSearch="3"
- id="selec2-18"
+ id="select2-18"
>
19. dropdown list position above ({{ value19 }})
@@ -197,7 +240,7 @@ 19. dropdown list position above ({{ value19 }})
[value]="value19"
(update)="update('value19', $event)"
listPosition="above"
- id="selec2-19"
+ id="select2-19"
>
@@ -215,7 +258,7 @@
20. nostyle ({{ value20 }})
(update)="update('value20', $event)"
listPosition="above"
[styleMode]="$any(select20?.value) || 'noStyle'"
- id="selec2-20"
+ id="select2-20"
>
@@ -226,7 +269,7 @@ 21. update to empty/null/undefined ({{ value21 }})
[value]="value21"
(update)="update('value21', $event)"
listPosition="above"
- id="selec2-21"
+ id="select2-21"
>
@@ -239,7 +282,7 @@ 22. with item classes and id ({{ value22 }})
[value]="value22"
(update)="update('value22', $event)"
listPosition="auto"
- id="selec2-22"
+ id="select2-22"
class="flower-list"
>
@@ -254,7 +297,7 @@ 23. with template ({{ value23 }})
listPosition="above"
[templates]="template"
[templateSelection]="templateSelection"
- id="selec2-23"
+ id="select2-23"
class="flower-list"
>
24. with template (option / group) ({{ value24 }})
(update)="update('value24', $event)"
listPosition="above"
[templates]="{ option: templateOption, group: templateGroup }"
- id="selec2-24"
+ id="select2-24"
class="flower-list"
>
25. with templates Ids ({{ value25 }})
(update)="update('value25', $event)"
listPosition="above"
[templates]="{ template1: template1, template2: template2, template3: template3 }"
- id="selec2-25"
+ id="select2-25"
class="flower-list"
>
26. infiniteScroll({{ value26 }})
infiniteScrollDistance="1.5"
infiniteScrollThrottle="150"
(scroll)="scroll26($event)"
- id="selec2-26"
+ id="select2-26"
>
27. position auto (overlay only) ({{ value2 }})
27. position auto (overlay only) ({{ value2 }})
[value]="value2"
listPosition="auto"
(update)="update('value2', $event)"
- id="selec2-27"
+ id="select2-27"
>
28. max results 50 ({{ value28 }})
28. max results 50 ({{ value28 }})
[data]="data28"
[value]="value28"
listPosition="auto"
- id="selec2-28"
+ id="select2-28"
(update)="update('value28', $event)"
maxResults="50"
maxResultsMessage="Too much results in this list."
@@ -349,7 +392,7 @@ 29. option autocreate ({{ value29 }})
[value]="value29"
multiple
autoCreate
- id="selec2-29"
+ id="select2-29"
(update)="update('value29', $event)"
>
@@ -361,7 +404,7 @@ 29b. option autocreate in search box ({{ value29b }})
[value]="value29b"
multiple
autoCreate
- id="selec2-29b"
+ id="select2-29b"
(update)="update('value29b', $event)"
>
@@ -373,7 +416,7 @@ 30. selected option when × is clicked (resettable) ({{ value30 }
[value]="value30"
resettable
resetSelectedValue="CA"
- id="selec2-30"
+ id="select2-30"
(update)="update('value30', $event)"
>
@@ -386,7 +429,7 @@ 31. change list ({{ value31 }})
>
Value :
-
+
@@ -395,7 +438,7 @@ 31. change list ({{ value31 }})
[overlay]="overlay"
[data]="data31"
[value]="value31m"
- id="selec2-31-m"
+ id="select2-31-m"
(update)="update('value31m', $event)"
multiple
>
@@ -407,7 +450,7 @@ 31. change list ({{ value31 }})
[overlay]="overlay"
[data]="data31"
[(ngModel)]="value31b"
- id="selec2-31b"
+ id="select2-31b"
(update)="update('value31b', $event)"
>
@@ -419,7 +462,7 @@ 32. auto create when / resettable ({{ value32 }})
resettable
autoCreate
resetSelectedValue="CA"
- id="selec2-32"
+ id="select2-32"
(autoCreateItem)="autoCreateItem('value32', $event)"
(update)="update('value32', $event)"
>
@@ -433,7 +476,7 @@ 33. reset form multiple({{ ctrlForm3.get('test33')?.value }})
@@ -444,7 +487,7 @@ 34. grid ({{ value34 }})
[overlay]="overlay"
[data]="data34"
[value]="value34"
- id="selec2-34"
+ id="select2-34"
grid="4"
(autoCreateItem)="autoCreateItem('value34', $event)"
(update)="update('value34', $event)"
@@ -456,7 +499,7 @@ 34b. grid sub-group ({{ value34b }})
[data]="data34b"
[value]="value34b"
grid="4"
- id="selec2-34b"
+ id="select2-34b"
(autoCreateItem)="autoCreateItem('value34b', $event)"
(update)="update('value34b', $event)"
>
@@ -466,7 +509,7 @@ 35. grid-auto ({{ value35 }})
[overlay]="overlay"
[data]="data35"
[value]="value35"
- id="selec2-35"
+ id="select2-35"
grid="35px"
(autoCreateItem)="autoCreateItem('value35', $event)"
(update)="update('value35', $event)"
@@ -478,7 +521,7 @@ 35b. grid-auto sub-group + multiple ({{ value35b | json }})
[data]="data35b"
[value]="value35b"
multiple
- id="selec2-35b"
+ id="select2-35b"
grid="35px"
(autoCreateItem)="autoCreateItem('value35b', $event)"
(update)="update('value35b', $event)"
@@ -491,20 +534,21 @@ 36. selectionOverride / resettable ({{ value36 }})
[overlay]="overlay"
[data]="data36"
[value]="value36"
- id="selec2-36"
+ id="select2-36"
(update)="update('value36', $event)"
>
- 36b. selectionOverride multiple ({{ value36m | json }})
+ 36b. selectionOverride multiple / resettable ({{ value36m | json }})
@@ -520,7 +564,7 @@ 36c. selectionOverride multiple / function ({{ value36mf | json
[overlay]="overlay"
[data]="data36mf"
[value]="value36mf"
- id="selec2-36-mf"
+ id="select2-36-mf"
(update)="update('value36mf', $event)"
multiple
>
@@ -533,7 +577,7 @@ 37. select all option ({{ value37 | json }})
[overlay]="overlay"
[data]="data37"
[value]="value37"
- id="selec2-37"
+ id="select2-37"
(update)="update('value37', $event)"
multiple
>
diff --git a/src/app/app-examples.component.ts b/src/app/app-examples.component.ts
index ef8e625..1e2a196 100644
--- a/src/app/app-examples.component.ts
+++ b/src/app/app-examples.component.ts
@@ -8,7 +8,7 @@ import { Select2AutoCreateEvent, Select2Data, Select2ScrollEvent, Select2SearchE
-import { data1, data2, data3, data5, data6, data8, data13, data17, data18, data19, data22, data23, data24, data26, data28, data31en, data31fr, data31ja, data35, data35b } from './app.data';
+import { data1, data13, data17, data18, data19, data2, data22, data23, data24, data26, data28, data3, data31en, data31fr, data31ja, data35, data35b, data5, data6, data8 } from './app.data';
@@ -224,12 +224,12 @@ export class AppExamplesComponent {
}
update(key: string, event: Select2UpdateEvent) {
- console.log('update', key, event.component._id, event.value);
+ console.log('update', key, event.component.id(), event.value);
(this as any)[key] = event.value;
}
autoCreateItem(key: string, event: Select2AutoCreateEvent) {
- console.log('autoCreateItem', key, event.component._id, event.value);
+ console.log('autoCreateItem', key, event.component.id(), event.value);
}
resetForm() {
@@ -259,4 +259,4 @@ export class AppExamplesComponent {
break;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/app/app-gen.component.html b/src/app/app-gen.component.html
index f8d7828..e6fbddf 100644
--- a/src/app/app-gen.component.html
+++ b/src/app/app-gen.component.html
@@ -22,6 +22,10 @@ parameters
+
+
+
+
@@ -196,7 +200,7 @@
Templates
Events
-
+
@@ -237,6 +241,7 @@
HTML render
[data]="data"
[overlay]="value?.overlay"
[disabled]="value?.disabled"
+ [readonly]="value?.readonly"
[minCharForSearch]="value?.minCharForSearch || 0"
[minCountForSearch]="value?.minCountForSearch"
[displaySearchStatus]="value?.displaySearchStatus"
diff --git a/src/app/app-gen.component.ts b/src/app/app-gen.component.ts
index e7214ee..7e1992a 100644
--- a/src/app/app-gen.component.ts
+++ b/src/app/app-gen.component.ts
@@ -39,6 +39,7 @@ export class AppGenComponent implements AfterContentInit {
hint: new UntypedFormControl(),
// parameters
disabled: new UntypedFormControl(),
+ readonly: new UntypedFormControl(),
overlay: new UntypedFormControl(),
minCharForSearch: new UntypedFormControl(),
minCountForSearch: new UntypedFormControl(),
@@ -95,7 +96,7 @@ export class AppGenComponent implements AfterContentInit {
}
selectionOverride: Select2SelectionOverride = params => {
- return `Selection (${params.size}${params.options!.length > 0 ? ': ' + params.options!.map(e => e.label).join(', ') : ''}) `;
+ return `Selection (${params.size}${(params.options?.length ?? 0) > 0 ? ': ' + params.options!.map(e => e.label).join(', ') : ''}) `;
};
getTemplate(
@@ -197,6 +198,9 @@ export class AppGenComponent implements AfterContentInit {
if (value.disabled) {
attrs['disabled'] = this._testBoolean(value.disabled);
}
+ if (value.readonly) {
+ attrs['readonly'] = this._testBoolean(value.readonly);
+ }
if (value.overlay) {
attrs['overlay'] = this._testBoolean(value.overlay);
}
@@ -468,4 +472,4 @@ export class AppGenComponent implements AfterContentInit {
private _testBoolean(value: any): null | undefined {
return value ? null : undefined;
}
-}
\ No newline at end of file
+}