mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01:00
[CL-27] [EC-455] Ng-Select Integration (#3383)
* [CL-27] Select -> Library initial commit * [EC-455] NG-Select Integration * Prettier * [EC-455] [CL-27] Add option for removing items on close // Added loading/no item found text defaults // Fixed disabled bg color // Added templated loading icon * [EC-455] [CL-27] Removed enter override // Fixed backspace removal // Clearing search on add // Gave label select states * [EC-455] [CL-27] Added copy from text to theme * [EC-455] [CL-27] Changed SimpleItemView to type - removed creation logic to shift responsibility to hosting component * [EC-455] [CL-27] Updated custom theme to include CSS variable colors * [CL-27] [EC-455] Initial pass at form field control // initial template for custom value accessor * [EC-455] working baseItem input * [EC-455] working value accessor * [EC-455] Completed FormFieldControl implemntation // fixed badge disabled bug * [EC-455] Prettier * [EC-455] Removed obsolete variables * [EC-455] Private value accessor functions * [EC-455] Cleaned up default variables * [EC-455] Imported Shared module to access i18n pipe // cleaned up string refs * [EC-455] Adjusted padding for clear button // Changed hover color to text-main // Fixed pipe import on stories * [EC-455] FormObj factory * [EC-455] FormObj factory * [EC-455] Updated FormFieldModule import/export statements * [EC-455] Null check ngControl // added strings * [EC-455] Fixed remaining null check // Added standalone story & input * [EC-455] Actually adding the null check * [EC-455] Removed injector logic // Removed Value Accessor PROVIDER // Self-assigned value accessor * [EC-455] Fixed ID copy pasta // Forwarded desribed by to focusable input // Abstracted input for label * [EC-455] Prettier
This commit is contained in:
parent
3a298bd989
commit
9578e7b6b4
@ -5390,5 +5390,17 @@
|
|||||||
},
|
},
|
||||||
"numberOfUsers": {
|
"numberOfUsers": {
|
||||||
"message": "Number of users"
|
"message": "Number of users"
|
||||||
|
},
|
||||||
|
"multiSelectPlaceholder": {
|
||||||
|
"message": "-- Type to Filter --"
|
||||||
|
},
|
||||||
|
"multiSelectLoading": {
|
||||||
|
"message": "Retrieving options..."
|
||||||
|
},
|
||||||
|
"multiSelectNotFound": {
|
||||||
|
"message": "No items found"
|
||||||
|
},
|
||||||
|
"multiSelectClearAll": {
|
||||||
|
"message": "Clear all"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
libs/components/src/form-field/form-field-control.ts
Normal file
8
libs/components/src/form-field/form-field-control.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export abstract class BitFormFieldControl {
|
||||||
|
ariaDescribedBy: string;
|
||||||
|
id: string;
|
||||||
|
labelForId: string;
|
||||||
|
required: boolean;
|
||||||
|
hasError: boolean;
|
||||||
|
error: [string, any];
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
<label class="tw-mb-1 tw-block tw-font-semibold tw-text-main" [attr.for]="input.id">
|
<label class="tw-mb-1 tw-block tw-font-semibold tw-text-main" [attr.for]="input.labelForId">
|
||||||
<ng-content select="bit-label"></ng-content>
|
<ng-content select="bit-label"></ng-content>
|
||||||
<span *ngIf="input.required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
|
<span *ngIf="input.required" class="tw-text-xs tw-font-normal"> ({{ "required" | i18n }})</span>
|
||||||
</label>
|
</label>
|
||||||
|
@ -7,9 +7,8 @@ import {
|
|||||||
ViewChild,
|
ViewChild,
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
|
|
||||||
import { BitInputDirective } from "../input/input.directive";
|
|
||||||
|
|
||||||
import { BitErrorComponent } from "./error.component";
|
import { BitErrorComponent } from "./error.component";
|
||||||
|
import { BitFormFieldControl } from "./form-field-control";
|
||||||
import { BitHintComponent } from "./hint.component";
|
import { BitHintComponent } from "./hint.component";
|
||||||
import { BitPrefixDirective } from "./prefix.directive";
|
import { BitPrefixDirective } from "./prefix.directive";
|
||||||
import { BitSuffixDirective } from "./suffix.directive";
|
import { BitSuffixDirective } from "./suffix.directive";
|
||||||
@ -22,7 +21,7 @@ import { BitSuffixDirective } from "./suffix.directive";
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class BitFormFieldComponent implements AfterContentChecked {
|
export class BitFormFieldComponent implements AfterContentChecked {
|
||||||
@ContentChild(BitInputDirective) input: BitInputDirective;
|
@ContentChild(BitFormFieldControl) input: BitFormFieldControl;
|
||||||
@ContentChild(BitHintComponent) hint: BitHintComponent;
|
@ContentChild(BitHintComponent) hint: BitHintComponent;
|
||||||
|
|
||||||
@ViewChild(BitErrorComponent) error: BitErrorComponent;
|
@ViewChild(BitErrorComponent) error: BitErrorComponent;
|
||||||
|
@ -2,6 +2,8 @@ import { NgModule } from "@angular/core";
|
|||||||
|
|
||||||
import { BitInputDirective } from "../input/input.directive";
|
import { BitInputDirective } from "../input/input.directive";
|
||||||
import { InputModule } from "../input/input.module";
|
import { InputModule } from "../input/input.module";
|
||||||
|
import { MultiSelectComponent } from "../multi-select/multi-select.component";
|
||||||
|
import { MultiSelectModule } from "../multi-select/multi-select.module";
|
||||||
import { SharedModule } from "../shared";
|
import { SharedModule } from "../shared";
|
||||||
|
|
||||||
import { BitErrorSummary } from "./error-summary.component";
|
import { BitErrorSummary } from "./error-summary.component";
|
||||||
@ -13,16 +15,17 @@ import { BitPrefixDirective } from "./prefix.directive";
|
|||||||
import { BitSuffixDirective } from "./suffix.directive";
|
import { BitSuffixDirective } from "./suffix.directive";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, InputModule],
|
imports: [SharedModule, InputModule, MultiSelectModule],
|
||||||
exports: [
|
exports: [
|
||||||
BitErrorComponent,
|
BitErrorComponent,
|
||||||
BitErrorSummary,
|
BitErrorSummary,
|
||||||
BitFormFieldComponent,
|
BitFormFieldComponent,
|
||||||
BitHintComponent,
|
BitHintComponent,
|
||||||
BitInputDirective,
|
|
||||||
BitLabel,
|
BitLabel,
|
||||||
BitPrefixDirective,
|
BitPrefixDirective,
|
||||||
BitSuffixDirective,
|
BitSuffixDirective,
|
||||||
|
BitInputDirective,
|
||||||
|
MultiSelectComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BitErrorComponent,
|
BitErrorComponent,
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from "./form-field.module";
|
export * from "./form-field.module";
|
||||||
export * from "./form-field.component";
|
export * from "./form-field.component";
|
||||||
|
export * from "./form-field-control";
|
||||||
|
283
libs/components/src/form-field/multi-select.stories.ts
Normal file
283
libs/components/src/form-field/multi-select.stories.ts
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import {
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormBuilder,
|
||||||
|
Validators,
|
||||||
|
FormGroup,
|
||||||
|
} from "@angular/forms";
|
||||||
|
import { NgSelectModule } from "@ng-select/ng-select";
|
||||||
|
import { action } from "@storybook/addon-actions";
|
||||||
|
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { BadgeModule } from "../badge";
|
||||||
|
import { ButtonModule } from "../button";
|
||||||
|
import { InputModule } from "../input/input.module";
|
||||||
|
import { MultiSelectComponent } from "../multi-select/multi-select.component";
|
||||||
|
import { SharedModule } from "../shared";
|
||||||
|
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||||
|
|
||||||
|
import { FormFieldModule } from "./form-field.module";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Component Library/Form/Multi Select",
|
||||||
|
excludeStories: /.*Data$/,
|
||||||
|
component: MultiSelectComponent,
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [
|
||||||
|
ButtonModule,
|
||||||
|
FormsModule,
|
||||||
|
NgSelectModule,
|
||||||
|
FormFieldModule,
|
||||||
|
InputModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
BadgeModule,
|
||||||
|
SharedModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useFactory: () => {
|
||||||
|
return new I18nMockService({
|
||||||
|
multiSelectPlaceholder: "-- Type to Filter --",
|
||||||
|
multiSelectLoading: "Retrieving options...",
|
||||||
|
multiSelectNotFound: "No items found",
|
||||||
|
multiSelectClearAll: "Clear all",
|
||||||
|
required: "required",
|
||||||
|
inputRequired: "Input is required.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: "figma",
|
||||||
|
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17689",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
export const actionsData = {
|
||||||
|
onItemsConfirmed: action("onItemsConfirmed"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const fb = new FormBuilder();
|
||||||
|
const formObjFactory = () =>
|
||||||
|
fb.group({
|
||||||
|
select: [[], [Validators.required]],
|
||||||
|
});
|
||||||
|
|
||||||
|
function submit(formObj: FormGroup) {
|
||||||
|
formObj.markAllAsTouched();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSelectTemplate: Story<MultiSelectComponent> = (args: MultiSelectComponent) => ({
|
||||||
|
props: {
|
||||||
|
formObj: formObjFactory(),
|
||||||
|
submit: submit,
|
||||||
|
...args,
|
||||||
|
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<form [formGroup]="formObj" (ngSubmit)="submit(formObj)">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ name }}</bit-label>
|
||||||
|
<bit-multi-select
|
||||||
|
class="tw-w-full"
|
||||||
|
formControlName="select"
|
||||||
|
[baseItems]="baseItems"
|
||||||
|
[removeSelectedItems]="removeSelectedItems"
|
||||||
|
[loading]="loading"
|
||||||
|
[disabled]="disabled"
|
||||||
|
(onItemsConfirmed)="onItemsConfirmed($event)">
|
||||||
|
</bit-multi-select>
|
||||||
|
<bit-hint>{{ hint }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<button type="submit" bitButton buttonType="primary">Submit</button>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Loading = MultiSelectTemplate.bind({});
|
||||||
|
Loading.args = {
|
||||||
|
baseItems: [],
|
||||||
|
name: "Loading",
|
||||||
|
hint: "This is what a loading multi-select looks like",
|
||||||
|
loading: "true",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Disabled = MultiSelectTemplate.bind({});
|
||||||
|
Disabled.args = {
|
||||||
|
name: "Disabled",
|
||||||
|
disabled: "true",
|
||||||
|
hint: "This is what a disabled multi-select looks like",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Groups = MultiSelectTemplate.bind({});
|
||||||
|
Groups.args = {
|
||||||
|
name: "Select groups",
|
||||||
|
hint: "Groups will be assigned to the associated member",
|
||||||
|
baseItems: [
|
||||||
|
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||||
|
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||||
|
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||||
|
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||||
|
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||||
|
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||||
|
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Members = MultiSelectTemplate.bind({});
|
||||||
|
Members.args = {
|
||||||
|
name: "Select members",
|
||||||
|
hint: "Members will be assigned to the associated group/collection",
|
||||||
|
baseItems: [
|
||||||
|
{ id: "1", listName: "Joe Smith (jsmith@mail.me)", labelName: "Joe Smith", icon: "bwi-user" },
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
listName: "Tania Stone (tstone@mail.me)",
|
||||||
|
labelName: "Tania Stone",
|
||||||
|
icon: "bwi-user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
listName: "Matt Matters (mmatters@mail.me)",
|
||||||
|
labelName: "Matt Matters",
|
||||||
|
icon: "bwi-user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
listName: "Bob Robertson (brobertson@mail.me)",
|
||||||
|
labelName: "Bob Robertson",
|
||||||
|
icon: "bwi-user",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "5",
|
||||||
|
listName: "Ashley Fletcher (aflectcher@mail.me)",
|
||||||
|
labelName: "Ashley Fletcher",
|
||||||
|
icon: "bwi-user",
|
||||||
|
},
|
||||||
|
{ id: "6", listName: "Rita Olson (rolson@mail.me)", labelName: "Rita Olson", icon: "bwi-user" },
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
listName: "Final listName (fname@mail.me)",
|
||||||
|
labelName: "(fname@mail.me)",
|
||||||
|
icon: "bwi-user",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Collections = MultiSelectTemplate.bind({});
|
||||||
|
Collections.args = {
|
||||||
|
name: "Select collections",
|
||||||
|
hint: "Collections will be assigned to the associated member",
|
||||||
|
baseItems: [
|
||||||
|
{ id: "1", listName: "Collection 1", labelName: "Collection 1", icon: "bwi-collection" },
|
||||||
|
{ id: "2", listName: "Collection 2", labelName: "Collection 2", icon: "bwi-collection" },
|
||||||
|
{ id: "3", listName: "Collection 3", labelName: "Collection 3", icon: "bwi-collection" },
|
||||||
|
{
|
||||||
|
id: "3.5",
|
||||||
|
listName: "Child Collection 1 for Parent 1",
|
||||||
|
labelName: "Child Collection 1 for Parent 1",
|
||||||
|
icon: "bwi-collection",
|
||||||
|
parentGrouping: "Parent 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3.55",
|
||||||
|
listName: "Child Collection 2 for Parent 1",
|
||||||
|
labelName: "Child Collection 2 for Parent 1",
|
||||||
|
icon: "bwi-collection",
|
||||||
|
parentGrouping: "Parent 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3.59",
|
||||||
|
listName: "Child Collection 3 for Parent 1",
|
||||||
|
labelName: "Child Collection 3 for Parent 1",
|
||||||
|
icon: "bwi-collection",
|
||||||
|
parentGrouping: "Parent 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3.75",
|
||||||
|
listName: "Child Collection 1 for Parent 2",
|
||||||
|
labelName: "Child Collection 1 for Parent 2",
|
||||||
|
icon: "bwi-collection",
|
||||||
|
parentGrouping: "Parent 2",
|
||||||
|
},
|
||||||
|
{ id: "4", listName: "Collection 4", labelName: "Collection 4", icon: "bwi-collection" },
|
||||||
|
{ id: "5", listName: "Collection 5", labelName: "Collection 5", icon: "bwi-collection" },
|
||||||
|
{ id: "6", listName: "Collection 6", labelName: "Collection 6", icon: "bwi-collection" },
|
||||||
|
{ id: "7", listName: "Collection 7", labelName: "Collection 7", icon: "bwi-collection" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MembersAndGroups = MultiSelectTemplate.bind({});
|
||||||
|
MembersAndGroups.args = {
|
||||||
|
name: "Select groups and members",
|
||||||
|
hint: "Members/Groups will be assigned to the associated collection",
|
||||||
|
baseItems: [
|
||||||
|
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||||
|
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||||
|
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||||
|
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||||
|
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||||
|
{ id: "6", listName: "Joe Smith (jsmith@mail.me)", labelName: "Joe Smith", icon: "bwi-user" },
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
listName: "Tania Stone (tstone@mail.me)",
|
||||||
|
labelName: "(tstone@mail.me)",
|
||||||
|
icon: "bwi-user",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RemoveSelected = MultiSelectTemplate.bind({});
|
||||||
|
RemoveSelected.args = {
|
||||||
|
name: "Select groups",
|
||||||
|
hint: "Groups will be removed from the list once the dropdown is closed",
|
||||||
|
baseItems: [
|
||||||
|
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||||
|
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||||
|
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||||
|
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||||
|
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||||
|
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||||
|
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||||
|
],
|
||||||
|
removeSelectedItems: "true",
|
||||||
|
};
|
||||||
|
|
||||||
|
const StandaloneTemplate: Story<MultiSelectComponent> = (args: MultiSelectComponent) => ({
|
||||||
|
props: {
|
||||||
|
...args,
|
||||||
|
onItemsConfirmed: actionsData.onItemsConfirmed,
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<bit-multi-select
|
||||||
|
class="tw-w-full"
|
||||||
|
[baseItems]="baseItems"
|
||||||
|
[removeSelectedItems]="removeSelectedItems"
|
||||||
|
[loading]="loading"
|
||||||
|
[disabled]="disabled"
|
||||||
|
(onItemsConfirmed)="onItemsConfirmed($event)">
|
||||||
|
</bit-multi-select>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Standalone = StandaloneTemplate.bind({});
|
||||||
|
Standalone.args = {
|
||||||
|
baseItems: [
|
||||||
|
{ id: "1", listName: "Group 1", labelName: "Group 1", icon: "bwi-family" },
|
||||||
|
{ id: "2", listName: "Group 2", labelName: "Group 2", icon: "bwi-family" },
|
||||||
|
{ id: "3", listName: "Group 3", labelName: "Group 3", icon: "bwi-family" },
|
||||||
|
{ id: "4", listName: "Group 4", labelName: "Group 4", icon: "bwi-family" },
|
||||||
|
{ id: "5", listName: "Group 5", labelName: "Group 5", icon: "bwi-family" },
|
||||||
|
{ id: "6", listName: "Group 6", labelName: "Group 6", icon: "bwi-family" },
|
||||||
|
{ id: "7", listName: "Group 7", labelName: "Group 7", icon: "bwi-family" },
|
||||||
|
],
|
||||||
|
removeSelectedItems: "true",
|
||||||
|
};
|
@ -1,13 +1,16 @@
|
|||||||
import { Directive, HostBinding, Input, Optional, Self } from "@angular/core";
|
import { Directive, HostBinding, Input, Optional, Self } from "@angular/core";
|
||||||
import { NgControl, Validators } from "@angular/forms";
|
import { NgControl, Validators } from "@angular/forms";
|
||||||
|
|
||||||
|
import { BitFormFieldControl } from "../form-field/form-field-control";
|
||||||
|
|
||||||
// Increments for each instance of this component
|
// Increments for each instance of this component
|
||||||
let nextId = 0;
|
let nextId = 0;
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
|
selector: "input[bitInput], select[bitInput], textarea[bitInput]",
|
||||||
|
providers: [{ provide: BitFormFieldControl, useExisting: BitInputDirective }],
|
||||||
})
|
})
|
||||||
export class BitInputDirective {
|
export class BitInputDirective implements BitFormFieldControl {
|
||||||
@HostBinding("class") @Input() get classList() {
|
@HostBinding("class") @Input() get classList() {
|
||||||
return [
|
return [
|
||||||
"tw-block",
|
"tw-block",
|
||||||
@ -38,6 +41,10 @@ export class BitInputDirective {
|
|||||||
|
|
||||||
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
|
@HostBinding("attr.aria-describedby") ariaDescribedBy: string;
|
||||||
|
|
||||||
|
get labelForId(): string {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
@HostBinding("attr.aria-invalid") get ariaInvalid() {
|
@HostBinding("attr.aria-invalid") get ariaInvalid() {
|
||||||
return this.hasError ? true : undefined;
|
return this.hasError ? true : undefined;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
export type SelectItemView = {
|
||||||
|
id: string; // Unique ID used for comparisons
|
||||||
|
listName: string; // Default bindValue -> this is what will be displayed in list items
|
||||||
|
labelName: string; // This is what will be displayed in the selection option badge
|
||||||
|
icon: string; // Icon to display within the list
|
||||||
|
parentGrouping: string; // Used to group items by parent
|
||||||
|
};
|
55
libs/components/src/multi-select/multi-select.component.html
Normal file
55
libs/components/src/multi-select/multi-select.component.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<ng-select
|
||||||
|
[items]="baseItems"
|
||||||
|
[(ngModel)]="selectedItems"
|
||||||
|
(ngModelChange)="onChange($event)"
|
||||||
|
(blur)="onBlur()"
|
||||||
|
bindLabel="listName"
|
||||||
|
groupBy="parentGrouping"
|
||||||
|
[placeholder]="placeholder"
|
||||||
|
[loading]="loading"
|
||||||
|
[loadingText]="loadingText"
|
||||||
|
notFoundText="{{ 'multiSelectNotFound' | i18n }}"
|
||||||
|
clearAllText="{{ 'multiSelectClearAll' | i18n }}"
|
||||||
|
[multiple]="true"
|
||||||
|
[selectOnTab]="true"
|
||||||
|
[closeOnSelect]="false"
|
||||||
|
(close)="onDropdownClosed()"
|
||||||
|
[disabled]="disabled"
|
||||||
|
[clearSearchOnAdd]="true"
|
||||||
|
[labelForId]="labelForId"
|
||||||
|
>
|
||||||
|
<ng-template ng-loadingspinner-tmp>
|
||||||
|
<i class="bwi bwi-spinner bwi-spin tw-mr-1" [title]="loadingText" aria-hidden="true"></i>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ng-label-tmp let-item="item" let-clear="clear">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
bitBadge
|
||||||
|
badgeType="primary"
|
||||||
|
class="tw-mr-1 disabled:tw-border-0"
|
||||||
|
[disabled]="disabled"
|
||||||
|
(click)="clear(item)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
*ngIf="item.icon != null"
|
||||||
|
class="tw-mr-1 bwi bwi-fw {{ item.icon }}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
{{ item.labelName }}
|
||||||
|
<i class="bwi bwi-fw bwi-close bwi-sm tw-ml-1" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template ng-option-tmp let-item="item">
|
||||||
|
<div class="tw-flex">
|
||||||
|
<div class="tw-w-7 tw-flex-none">
|
||||||
|
<i *ngIf="isSelected(item)" class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="tw-mr-2 tw-flex-initial">
|
||||||
|
<i *ngIf="item.icon != null" class="bwi bwi-fw {{ item.icon }}" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="tw-flex-1">
|
||||||
|
{{ item.listName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ng-select>
|
179
libs/components/src/multi-select/multi-select.component.ts
Normal file
179
libs/components/src/multi-select/multi-select.component.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
EventEmitter,
|
||||||
|
HostBinding,
|
||||||
|
Optional,
|
||||||
|
Self,
|
||||||
|
} from "@angular/core";
|
||||||
|
import { ControlValueAccessor, NgControl, Validators } from "@angular/forms";
|
||||||
|
import { NgSelectComponent } from "@ng-select/ng-select";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { BitFormFieldControl } from "../form-field/form-field-control";
|
||||||
|
|
||||||
|
import { SelectItemView } from "./models/select-item-view";
|
||||||
|
|
||||||
|
// Increments for each instance of this component
|
||||||
|
let nextId = 0;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "bit-multi-select",
|
||||||
|
templateUrl: "./multi-select.component.html",
|
||||||
|
providers: [{ provide: BitFormFieldControl, useExisting: MultiSelectComponent }],
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This component has been implemented to only support Multi-select list events
|
||||||
|
*/
|
||||||
|
export class MultiSelectComponent implements OnInit, BitFormFieldControl, ControlValueAccessor {
|
||||||
|
@ViewChild(NgSelectComponent) select: NgSelectComponent;
|
||||||
|
|
||||||
|
// Parent component should only pass selectable items (complete list - selected items = baseItems)
|
||||||
|
@Input() baseItems: SelectItemView[];
|
||||||
|
// Defaults to native ng-select behavior - set to "true" to clear selected items on dropdown close
|
||||||
|
@Input() removeSelectedItems = false;
|
||||||
|
@Input() placeholder: string;
|
||||||
|
@Input() loading = false;
|
||||||
|
@Input() disabled = false;
|
||||||
|
|
||||||
|
// Internal tracking of selected items
|
||||||
|
@Input() selectedItems: SelectItemView[];
|
||||||
|
|
||||||
|
// Default values for our implementation
|
||||||
|
loadingText: string;
|
||||||
|
|
||||||
|
protected searchInputId = `search-input-${nextId++}`;
|
||||||
|
|
||||||
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
|
private notifyOnChange?: (value: SelectItemView[]) => void;
|
||||||
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
|
private notifyOnTouched?: () => void;
|
||||||
|
|
||||||
|
@Output() onItemsConfirmed = new EventEmitter<any[]>();
|
||||||
|
|
||||||
|
constructor(private i18nService: I18nService, @Optional() @Self() private ngControl?: NgControl) {
|
||||||
|
if (ngControl != null) {
|
||||||
|
ngControl.valueAccessor = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Default Text Values
|
||||||
|
this.placeholder = this.placeholder ?? this.i18nService.t("multiSelectPlaceholder");
|
||||||
|
this.loadingText = this.i18nService.t("multiSelectLoading");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper method for showing selected state in custom template */
|
||||||
|
isSelected(item: any): boolean {
|
||||||
|
return this.selectedItems?.find((selected) => selected.id === item.id) != undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `close` callback will act as the only trigger for signifying the user's intent of completing the selection
|
||||||
|
* of items. Selected items will be emitted to the parent component in order to allow for separate data handling.
|
||||||
|
*/
|
||||||
|
onDropdownClosed(): void {
|
||||||
|
// Early exit
|
||||||
|
if (this.selectedItems == null || this.selectedItems.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit results to parent component
|
||||||
|
this.onItemsConfirmed.emit(this.selectedItems);
|
||||||
|
|
||||||
|
// Remove selected items from base list based on input property
|
||||||
|
if (this.removeSelectedItems) {
|
||||||
|
let updatedBaseItems = this.baseItems;
|
||||||
|
this.selectedItems.forEach((selectedItem) => {
|
||||||
|
updatedBaseItems = updatedBaseItems.filter((item) => selectedItem.id !== item.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset Lists
|
||||||
|
this.selectedItems = null;
|
||||||
|
this.baseItems = updatedBaseItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
|
writeValue(obj: SelectItemView[]): void {
|
||||||
|
this.selectedItems = obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
|
registerOnChange(fn: (value: SelectItemView[]) => void): void {
|
||||||
|
this.notifyOnChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
|
registerOnTouched(fn: any): void {
|
||||||
|
this.notifyOnTouched = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
|
setDisabledState(isDisabled: boolean): void {
|
||||||
|
this.disabled = isDisabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
|
protected onChange(items: SelectItemView[]) {
|
||||||
|
if (!this.notifyOnChange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyOnChange(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of NG_VALUE_ACCESSOR */
|
||||||
|
protected onBlur() {
|
||||||
|
if (!this.notifyOnTouched) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notifyOnTouched();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of BitFormFieldControl */
|
||||||
|
@HostBinding("attr.aria-describedby")
|
||||||
|
get ariaDescribedBy() {
|
||||||
|
return this._ariaDescribedBy;
|
||||||
|
}
|
||||||
|
set ariaDescribedBy(value: string) {
|
||||||
|
this._ariaDescribedBy = value;
|
||||||
|
this.select?.searchInput.nativeElement.setAttribute("aria-describedby", value);
|
||||||
|
}
|
||||||
|
private _ariaDescribedBy: string;
|
||||||
|
|
||||||
|
/**Implemented as part of BitFormFieldControl */
|
||||||
|
get labelForId() {
|
||||||
|
return this.searchInputId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of BitFormFieldControl */
|
||||||
|
@HostBinding() @Input() id = `bit-multi-select-${nextId++}`;
|
||||||
|
|
||||||
|
/**Implemented as part of BitFormFieldControl */
|
||||||
|
@HostBinding("attr.required")
|
||||||
|
@Input()
|
||||||
|
get required() {
|
||||||
|
return this._required ?? this.ngControl?.control?.hasValidator(Validators.required) ?? false;
|
||||||
|
}
|
||||||
|
set required(value: any) {
|
||||||
|
this._required = value != null && value !== false;
|
||||||
|
}
|
||||||
|
private _required: boolean;
|
||||||
|
|
||||||
|
/**Implemented as part of BitFormFieldControl */
|
||||||
|
get hasError() {
|
||||||
|
return this.ngControl?.status === "INVALID" && this.ngControl?.touched;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**Implemented as part of BitFormFieldControl */
|
||||||
|
get error(): [string, any] {
|
||||||
|
const key = Object.keys(this.ngControl?.errors)[0];
|
||||||
|
return [key, this.ngControl?.errors[key]];
|
||||||
|
}
|
||||||
|
}
|
16
libs/components/src/multi-select/multi-select.module.ts
Normal file
16
libs/components/src/multi-select/multi-select.module.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { FormsModule } from "@angular/forms";
|
||||||
|
import { NgSelectModule } from "@ng-select/ng-select";
|
||||||
|
|
||||||
|
import { BadgeModule } from "../badge";
|
||||||
|
import { SharedModule } from "../shared";
|
||||||
|
|
||||||
|
import { MultiSelectComponent } from "./multi-select.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule, FormsModule, NgSelectModule, BadgeModule, SharedModule],
|
||||||
|
exports: [MultiSelectComponent],
|
||||||
|
declarations: [MultiSelectComponent],
|
||||||
|
})
|
||||||
|
export class MultiSelectModule {}
|
394
libs/components/src/multi-select/scss/bw.theme.scss
Normal file
394
libs/components/src/multi-select/scss/bw.theme.scss
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
// Default theme copied from https://github.com/ng-select/ng-select/blob/master/src/ng-select/themes/default.theme.scss
|
||||||
|
@mixin rtl {
|
||||||
|
@at-root [dir="rtl"] #{&} {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ng-select-highlight: rgb(var(--color-primary-700)) !default;
|
||||||
|
$ng-select-primary-text: rgb(var(--color-text-main)) !default;
|
||||||
|
$ng-select-disabled-text: rgb(var(--color-secondary-100)) !default;
|
||||||
|
$ng-select-border: rgb(var(--color-secondary-500)) !default;
|
||||||
|
$ng-select-border-radius: 4px !default;
|
||||||
|
$ng-select-bg: rgb(var(--color-background-alt)) !default;
|
||||||
|
$ng-select-selected: transparent !default;
|
||||||
|
$ng-select-selected-text: $ng-select-primary-text !default;
|
||||||
|
|
||||||
|
$ng-select-marked: rgb(var(--color-text-main) / 0.12) !default;
|
||||||
|
$ng-select-marked-text: $ng-select-primary-text !default;
|
||||||
|
|
||||||
|
$ng-select-box-shadow: none !default;
|
||||||
|
$ng-select-placeholder: rgb(var(--color-text-muted)) !default;
|
||||||
|
$ng-select-height: 36px !default;
|
||||||
|
$ng-select-value-padding-left: 10px !default;
|
||||||
|
$ng-select-value-font-size: 0.9em !default;
|
||||||
|
$ng-select-value-text: $ng-select-primary-text !default;
|
||||||
|
|
||||||
|
$ng-select-dropdown-bg: $ng-select-bg !default;
|
||||||
|
$ng-select-dropdown-border: $ng-select-border !default;
|
||||||
|
$ng-select-dropdown-optgroup-text: rgb(var(--color-text-muted)) !default;
|
||||||
|
$ng-select-dropdown-optgroup-marked: $ng-select-dropdown-optgroup-text !default;
|
||||||
|
$ng-select-dropdown-option-bg: $ng-select-dropdown-bg !default;
|
||||||
|
$ng-select-dropdown-option-text: $ng-select-primary-text !default;
|
||||||
|
$ng-select-dropdown-option-disabled: rgb(var(--color-text-muted) / 0.6) !default;
|
||||||
|
|
||||||
|
$ng-select-input-text: $ng-select-primary-text !default;
|
||||||
|
|
||||||
|
// Custom color variables
|
||||||
|
$ng-select-arrow-hover: rgb(var(--color-secondary-700)) !default;
|
||||||
|
$ng-clear-icon-hover: rgb(var(--color-text-main)) !default;
|
||||||
|
$ng-dropdown-shadow: rgb(var(--color-secondary-100)) !default;
|
||||||
|
|
||||||
|
.ng-select {
|
||||||
|
&.ng-select-opened {
|
||||||
|
> .ng-select-container {
|
||||||
|
background: $ng-select-bg;
|
||||||
|
border-color: $ng-select-border;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.ng-arrow {
|
||||||
|
top: -2px;
|
||||||
|
border-color: transparent transparent $ng-select-arrow-hover;
|
||||||
|
border-width: 0 5px 5px;
|
||||||
|
&:hover {
|
||||||
|
border-color: transparent transparent $ng-select-arrow-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-top {
|
||||||
|
> .ng-select-container {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-right {
|
||||||
|
> .ng-select-container {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-bottom {
|
||||||
|
> .ng-select-container {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-left {
|
||||||
|
> .ng-select-container {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-focused {
|
||||||
|
&:not(.ng-select-opened) > .ng-select-container {
|
||||||
|
border-color: $ng-select-highlight;
|
||||||
|
box-shadow: $ng-select-box-shadow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-disabled {
|
||||||
|
> .ng-select-container {
|
||||||
|
background-color: $ng-select-disabled-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-has-value .ng-placeholder {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ng-select-container {
|
||||||
|
color: $ng-select-primary-text;
|
||||||
|
background-color: $ng-select-bg;
|
||||||
|
border-radius: $ng-select-border-radius;
|
||||||
|
border: 1px solid $ng-select-border;
|
||||||
|
min-height: $ng-select-height;
|
||||||
|
align-items: center;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 1px 0 $ng-dropdown-shadow;
|
||||||
|
}
|
||||||
|
.ng-value-container {
|
||||||
|
align-items: center;
|
||||||
|
padding-left: $ng-select-value-padding-left;
|
||||||
|
@include rtl {
|
||||||
|
padding-right: $ng-select-value-padding-left;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.ng-placeholder {
|
||||||
|
color: $ng-select-placeholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-single {
|
||||||
|
.ng-select-container {
|
||||||
|
height: $ng-select-height;
|
||||||
|
.ng-value-container {
|
||||||
|
.ng-input {
|
||||||
|
top: 5px;
|
||||||
|
left: 0;
|
||||||
|
padding-left: $ng-select-value-padding-left;
|
||||||
|
padding-right: 50px;
|
||||||
|
@include rtl {
|
||||||
|
padding-right: $ng-select-value-padding-left;
|
||||||
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-multiple {
|
||||||
|
&.ng-select-disabled {
|
||||||
|
> .ng-select-container .ng-value-container .ng-value {
|
||||||
|
background-color: $ng-select-disabled-text;
|
||||||
|
border: 0px solid $ng-select-border; // Removing border on slected value when disabled
|
||||||
|
.ng-value-label {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-select-container {
|
||||||
|
.ng-value-container {
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-left: 7px;
|
||||||
|
@include rtl {
|
||||||
|
padding-right: 7px;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.ng-value {
|
||||||
|
font-size: $ng-select-value-font-size;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: $ng-select-value-text;
|
||||||
|
background-color: $ng-select-selected;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 5px;
|
||||||
|
@include rtl {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
&.ng-value-disabled {
|
||||||
|
background-color: $ng-select-disabled-text;
|
||||||
|
.ng-value-label {
|
||||||
|
padding-left: 5px;
|
||||||
|
@include rtl {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-value-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 5px;
|
||||||
|
}
|
||||||
|
.ng-value-icon {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 5px;
|
||||||
|
&:hover {
|
||||||
|
background-color: $ng-select-arrow-hover;
|
||||||
|
}
|
||||||
|
&.left {
|
||||||
|
border-right: 1px solid $ng-select-selected;
|
||||||
|
@include rtl {
|
||||||
|
border-left: 1px solid $ng-select-selected;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.right {
|
||||||
|
border-left: 1px solid $ng-select-selected;
|
||||||
|
@include rtl {
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 1px solid $ng-select-selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-input {
|
||||||
|
padding: 0 0 3px 3px;
|
||||||
|
@include rtl {
|
||||||
|
padding: 0 3px 3px 0;
|
||||||
|
}
|
||||||
|
> input {
|
||||||
|
color: $ng-select-input-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-placeholder {
|
||||||
|
top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
padding-left: 3px;
|
||||||
|
@include rtl {
|
||||||
|
padding-right: 3px;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-clear-wrapper {
|
||||||
|
color: $ng-select-placeholder;
|
||||||
|
padding-top: 2.5px;
|
||||||
|
&:hover .ng-clear {
|
||||||
|
color: $ng-clear-icon-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-spinner-zone {
|
||||||
|
padding: 5px 5px 0 0;
|
||||||
|
|
||||||
|
@include rtl {
|
||||||
|
padding: 5px 0 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-arrow-wrapper {
|
||||||
|
width: 25px;
|
||||||
|
padding-right: 5px;
|
||||||
|
@include rtl {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
.ng-arrow {
|
||||||
|
border-top-color: $ng-select-arrow-hover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-arrow {
|
||||||
|
border-color: $ng-select-placeholder transparent transparent;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 5px 5px 2.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ng-dropdown-panel {
|
||||||
|
background-color: $ng-select-dropdown-bg;
|
||||||
|
border: 1px solid $ng-select-dropdown-border;
|
||||||
|
box-shadow: 0 1px 0 $ng-dropdown-shadow;
|
||||||
|
left: 0;
|
||||||
|
&.ng-select-top {
|
||||||
|
bottom: 100%;
|
||||||
|
border-top-right-radius: $ng-select-border-radius;
|
||||||
|
border-top-left-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-color: $ng-select-border;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
.ng-dropdown-panel-items {
|
||||||
|
.ng-option {
|
||||||
|
&:first-child {
|
||||||
|
border-top-right-radius: $ng-select-border-radius;
|
||||||
|
border-top-left-radius: $ng-select-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-right {
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
border-top-right-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-right-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-left-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-color: $ng-select-border;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
.ng-dropdown-panel-items {
|
||||||
|
.ng-option {
|
||||||
|
&:first-child {
|
||||||
|
border-top-right-radius: $ng-select-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-bottom {
|
||||||
|
top: 100%;
|
||||||
|
border-bottom-right-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-left-radius: $ng-select-border-radius;
|
||||||
|
border-top-color: $ng-select-border;
|
||||||
|
margin-top: -1px;
|
||||||
|
.ng-dropdown-panel-items {
|
||||||
|
.ng-option {
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-right-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-left-radius: $ng-select-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-select-left {
|
||||||
|
left: -100%;
|
||||||
|
top: 0;
|
||||||
|
border-top-left-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-right-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-left-radius: $ng-select-border-radius;
|
||||||
|
border-bottom-color: $ng-select-border;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
.ng-dropdown-panel-items {
|
||||||
|
.ng-option {
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: $ng-select-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-dropdown-header {
|
||||||
|
border-bottom: 1px solid $ng-select-border;
|
||||||
|
padding: 5px 7px;
|
||||||
|
}
|
||||||
|
.ng-dropdown-footer {
|
||||||
|
border-top: 1px solid $ng-select-border;
|
||||||
|
padding: 5px 7px;
|
||||||
|
}
|
||||||
|
.ng-dropdown-panel-items {
|
||||||
|
.ng-optgroup {
|
||||||
|
user-select: none;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $ng-select-dropdown-optgroup-text;
|
||||||
|
cursor: pointer;
|
||||||
|
&.ng-option-disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
&.ng-option-marked {
|
||||||
|
background-color: $ng-select-marked;
|
||||||
|
}
|
||||||
|
&.ng-option-selected,
|
||||||
|
&.ng-option-selected.ng-option-marked {
|
||||||
|
color: $ng-select-dropdown-optgroup-marked;
|
||||||
|
background-color: $ng-select-selected;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-option {
|
||||||
|
background-color: $ng-select-dropdown-option-bg;
|
||||||
|
color: $ng-select-dropdown-option-text;
|
||||||
|
padding: 8px 10px;
|
||||||
|
&.ng-option-selected,
|
||||||
|
&.ng-option-selected.ng-option-marked {
|
||||||
|
color: $ng-select-selected-text;
|
||||||
|
background-color: $ng-select-selected;
|
||||||
|
.ng-option-label {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ng-option-marked {
|
||||||
|
background-color: $ng-select-marked;
|
||||||
|
color: $ng-select-marked-text;
|
||||||
|
}
|
||||||
|
&.ng-option-disabled {
|
||||||
|
color: $ng-select-dropdown-option-disabled;
|
||||||
|
}
|
||||||
|
&.ng-option-child {
|
||||||
|
padding-left: 22px;
|
||||||
|
@include rtl {
|
||||||
|
padding-right: 22px;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ng-tag-label {
|
||||||
|
font-size: 80%;
|
||||||
|
font-weight: 400;
|
||||||
|
padding-right: 5px;
|
||||||
|
@include rtl {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include rtl {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
@ -8,40 +8,42 @@ $card-icons-base: "../images/cards/";
|
|||||||
|
|
||||||
@import "@angular/cdk/overlay-prebuilt.css";
|
@import "@angular/cdk/overlay-prebuilt.css";
|
||||||
|
|
||||||
@import "~bootstrap/scss/_functions";
|
@import "bootstrap/scss/_functions";
|
||||||
@import "~bootstrap/scss/_variables";
|
@import "bootstrap/scss/_variables";
|
||||||
@import "~bootstrap/scss/_mixins";
|
@import "bootstrap/scss/_mixins";
|
||||||
@import "~bootstrap/scss/_root";
|
@import "bootstrap/scss/_root";
|
||||||
@import "~bootstrap/scss/_reboot";
|
@import "bootstrap/scss/_reboot";
|
||||||
@import "~bootstrap/scss/_type";
|
@import "bootstrap/scss/_type";
|
||||||
@import "~bootstrap/scss/_images";
|
@import "bootstrap/scss/_images";
|
||||||
@import "~bootstrap/scss/_code";
|
@import "bootstrap/scss/_code";
|
||||||
@import "~bootstrap/scss/_grid";
|
@import "bootstrap/scss/_grid";
|
||||||
@import "~bootstrap/scss/_tables";
|
@import "bootstrap/scss/_tables";
|
||||||
@import "~bootstrap/scss/_forms";
|
@import "bootstrap/scss/_forms";
|
||||||
@import "~bootstrap/scss/_buttons";
|
@import "bootstrap/scss/_buttons";
|
||||||
@import "~bootstrap/scss/_transitions";
|
@import "bootstrap/scss/_transitions";
|
||||||
@import "~bootstrap/scss/_dropdown";
|
@import "bootstrap/scss/_dropdown";
|
||||||
@import "~bootstrap/scss/_button-group";
|
@import "bootstrap/scss/_button-group";
|
||||||
@import "~bootstrap/scss/_input-group";
|
@import "bootstrap/scss/_input-group";
|
||||||
@import "~bootstrap/scss/_custom-forms";
|
@import "bootstrap/scss/_custom-forms";
|
||||||
@import "~bootstrap/scss/_nav";
|
@import "bootstrap/scss/_nav";
|
||||||
@import "~bootstrap/scss/_navbar";
|
@import "bootstrap/scss/_navbar";
|
||||||
@import "~bootstrap/scss/_card";
|
@import "bootstrap/scss/_card";
|
||||||
@import "~bootstrap/scss/_breadcrumb";
|
@import "bootstrap/scss/_breadcrumb";
|
||||||
@import "~bootstrap/scss/_pagination";
|
@import "bootstrap/scss/_pagination";
|
||||||
@import "~bootstrap/scss/_badge";
|
@import "bootstrap/scss/_badge";
|
||||||
@import "~bootstrap/scss/_jumbotron";
|
@import "bootstrap/scss/_jumbotron";
|
||||||
@import "~bootstrap/scss/_alert";
|
@import "bootstrap/scss/_alert";
|
||||||
@import "~bootstrap/scss/_progress";
|
@import "bootstrap/scss/_progress";
|
||||||
@import "~bootstrap/scss/_media";
|
@import "bootstrap/scss/_media";
|
||||||
@import "~bootstrap/scss/_list-group";
|
@import "bootstrap/scss/_list-group";
|
||||||
@import "~bootstrap/scss/_close";
|
@import "bootstrap/scss/_close";
|
||||||
//@import "~bootstrap/scss/_toasts";
|
//@import "bootstrap/scss/_toasts";
|
||||||
@import "~bootstrap/scss/_modal";
|
@import "bootstrap/scss/_modal";
|
||||||
@import "~bootstrap/scss/_tooltip";
|
@import "bootstrap/scss/_tooltip";
|
||||||
@import "~bootstrap/scss/_popover";
|
@import "bootstrap/scss/_popover";
|
||||||
@import "~bootstrap/scss/_carousel";
|
@import "bootstrap/scss/_carousel";
|
||||||
@import "~bootstrap/scss/_spinners";
|
@import "bootstrap/scss/_spinners";
|
||||||
@import "~bootstrap/scss/_utilities";
|
@import "bootstrap/scss/_utilities";
|
||||||
@import "~bootstrap/scss/_print";
|
@import "bootstrap/scss/_print";
|
||||||
|
|
||||||
|
@import "multi-select/scss/bw.theme.scss";
|
||||||
|
28
package-lock.json
generated
28
package-lock.json
generated
@ -27,6 +27,7 @@
|
|||||||
"@koa/router": "^10.1.1",
|
"@koa/router": "^10.1.1",
|
||||||
"@microsoft/signalr": "^6.0.7",
|
"@microsoft/signalr": "^6.0.7",
|
||||||
"@microsoft/signalr-protocol-msgpack": "^6.0.7",
|
"@microsoft/signalr-protocol-msgpack": "^6.0.7",
|
||||||
|
"@ng-select/ng-select": "^9.0.2",
|
||||||
"big-integer": "^1.6.51",
|
"big-integer": "^1.6.51",
|
||||||
"bootstrap": "4.6.0",
|
"bootstrap": "4.6.0",
|
||||||
"braintree-web-drop-in": "^1.33.1",
|
"braintree-web-drop-in": "^1.33.1",
|
||||||
@ -5216,6 +5217,23 @@
|
|||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ng-select/ng-select": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-xdNiz/kgkMWYW1qFtk/337xDk/cmfEbSVtTFxWIM2OnIX1XsQOnTlGiBYces1TsMfqS68HjAvljEkj8QIGN2Lg==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.20.0",
|
||||||
|
"npm": ">= 6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "<15.0.0",
|
||||||
|
"@angular/core": "<15.0.0",
|
||||||
|
"@angular/forms": "<15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ngtools/webpack": {
|
"node_modules/@ngtools/webpack": {
|
||||||
"version": "14.1.0",
|
"version": "14.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.1.0.tgz",
|
||||||
@ -46775,6 +46793,14 @@
|
|||||||
"integrity": "sha512-Rscrg0BO4AKqFX2mKd8C68Wh3TkSHXqF2PZp+utVoLV+PTQnGVMwHedtIHBcFoq1Ij3I4yETMgSFSdAR+lp++Q==",
|
"integrity": "sha512-Rscrg0BO4AKqFX2mKd8C68Wh3TkSHXqF2PZp+utVoLV+PTQnGVMwHedtIHBcFoq1Ij3I4yETMgSFSdAR+lp++Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@ng-select/ng-select": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-xdNiz/kgkMWYW1qFtk/337xDk/cmfEbSVtTFxWIM2OnIX1XsQOnTlGiBYces1TsMfqS68HjAvljEkj8QIGN2Lg==",
|
||||||
|
"requires": {
|
||||||
|
"tslib": "^2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@ngtools/webpack": {
|
"@ngtools/webpack": {
|
||||||
"version": "14.1.0",
|
"version": "14.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-14.1.0.tgz",
|
||||||
@ -47213,7 +47239,7 @@
|
|||||||
"postcss": "^7.0.36",
|
"postcss": "^7.0.36",
|
||||||
"postcss-loader": "^4.2.0",
|
"postcss-loader": "^4.2.0",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^18.0.0",
|
"react": "^16.14.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"read-pkg-up": "^7.0.1",
|
"read-pkg-up": "^7.0.1",
|
||||||
"regenerator-runtime": "^0.13.7",
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
@ -152,6 +152,7 @@
|
|||||||
"@koa/router": "^10.1.1",
|
"@koa/router": "^10.1.1",
|
||||||
"@microsoft/signalr": "^6.0.7",
|
"@microsoft/signalr": "^6.0.7",
|
||||||
"@microsoft/signalr-protocol-msgpack": "^6.0.7",
|
"@microsoft/signalr-protocol-msgpack": "^6.0.7",
|
||||||
|
"@ng-select/ng-select": "^9.0.2",
|
||||||
"big-integer": "^1.6.51",
|
"big-integer": "^1.6.51",
|
||||||
"bootstrap": "4.6.0",
|
"bootstrap": "4.6.0",
|
||||||
"braintree-web-drop-in": "^1.33.1",
|
"braintree-web-drop-in": "^1.33.1",
|
||||||
|
Loading…
Reference in New Issue
Block a user