diff --git a/projects/elonkit/src/assets/elonkit/file-list/file.svg b/projects/elonkit/src/assets/elonkit/file-list/file.svg
new file mode 100644
index 00000000..e9ba2dce
--- /dev/null
+++ b/projects/elonkit/src/assets/elonkit/file-list/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/elonkit/src/assets/elonkit/file-list/file_download.svg b/projects/elonkit/src/assets/elonkit/file-list/file_download.svg
new file mode 100644
index 00000000..d9408366
--- /dev/null
+++ b/projects/elonkit/src/assets/elonkit/file-list/file_download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/projects/elonkit/src/public-api.ts b/projects/elonkit/src/public-api.ts
index 99ebc1a5..09f0859a 100644
--- a/projects/elonkit/src/public-api.ts
+++ b/projects/elonkit/src/public-api.ts
@@ -1,4 +1,5 @@
export * from './ui/breadcrumbs';
+export * from './ui/file-list';
export * from './ui/empty-state';
export * from './ui/dropzone';
export * from './ui/inline-form-field';
diff --git a/projects/elonkit/src/ui/file-list/__specs__/file-list.spec.ts b/projects/elonkit/src/ui/file-list/__specs__/file-list.spec.ts
new file mode 100644
index 00000000..368f7006
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__specs__/file-list.spec.ts
@@ -0,0 +1,138 @@
+import { render } from '@testing-library/angular';
+import { MatIconTestingModule } from '@angular/material/icon/testing';
+
+import { ESFileListModule } from '../file-list.module';
+import { ESFileListComponent } from '../file-list.component';
+import { filesFixture } from '../fixtures/files.fixture';
+import { ESLocaleService, en, ru } from '../../locale';
+
+describe('File List', () => {
+ it('Should render all files', async () => {
+ const component = await render(ESFileListComponent, {
+ componentProperties: {
+ files: filesFixture
+ },
+ imports: [ESFileListModule, MatIconTestingModule],
+ excludeComponentDeclaration: true
+ });
+ expect(component.getAllByTestId('file')).toHaveLength(filesFixture.length);
+ });
+
+ it('Should accept typography classes', async () => {
+ const file = filesFixture[0];
+ const component = await render(ESFileListComponent, {
+ componentProperties: {
+ files: [file],
+ fileNameTypography: 'app-body-1',
+ fileSizeTypography: 'app-caption'
+ },
+ imports: [ESFileListModule, MatIconTestingModule],
+ excludeComponentDeclaration: true
+ });
+ expect(component.getByText(file.name)).toHaveClass('app-body-1');
+ expect(component.getByText(en.fileList.labelKB, { exact: false })).toHaveClass('app-caption');
+ });
+
+ it('Should render remove button on canRemove input', async () => {
+ const component = await render(ESFileListComponent, {
+ componentProperties: {
+ files: filesFixture,
+ canRemove: true
+ },
+ imports: [ESFileListModule, MatIconTestingModule],
+ excludeComponentDeclaration: true
+ });
+ expect(component.getAllByLabelText(en.fileList.labelRemove)).toHaveLength(filesFixture.length);
+ });
+
+ it('Should remove file on remove button click', async () => {
+ const onRemove = jest.fn();
+ const component = await render(ESFileListComponent, {
+ componentProperties: {
+ files: filesFixture,
+ canRemove: true,
+ remove: {
+ emit: onRemove
+ } as any
+ },
+ imports: [ESFileListModule, MatIconTestingModule],
+ excludeComponentDeclaration: true
+ });
+ const removeButtons = component.getAllByLabelText(en.fileList.labelRemove);
+ removeButtons.forEach((btn) => {
+ component.click(btn);
+ });
+ expect(onRemove).toHaveBeenCalledTimes(filesFixture.length);
+ });
+
+ it('Should render download icon on canDownload input', async () => {
+ const component = await render(ESFileListComponent, {
+ componentProperties: {
+ files: filesFixture,
+ canDownload: true
+ },
+ imports: [ESFileListModule, MatIconTestingModule],
+ excludeComponentDeclaration: true
+ });
+ expect(component.getAllByLabelText(en.fileList.labelDownload)).toHaveLength(
+ filesFixture.length
+ );
+ });
+
+ it('Should download file on download icon click', async () => {
+ const onDownload = jest.fn();
+ const component = await render(ESFileListComponent, {
+ componentProperties: {
+ files: filesFixture,
+ canDownload: true,
+ download: {
+ emit: onDownload
+ } as any
+ },
+ imports: [ESFileListModule, MatIconTestingModule],
+ excludeComponentDeclaration: true
+ });
+ const downloadButtons = component.getAllByLabelText(en.fileList.labelDownload);
+ downloadButtons.forEach((btn) => {
+ component.click(btn);
+ });
+ expect(onDownload).toHaveBeenCalledTimes(filesFixture.length);
+ });
+
+ it('Should not render image files on hideImages input', async () => {
+ const component = await render(ESFileListComponent, {
+ componentProperties: {
+ files: filesFixture,
+ hideImages: true
+ },
+ imports: [ESFileListModule, MatIconTestingModule],
+ excludeComponentDeclaration: true
+ });
+ const nonImageFixture = filesFixture.filter((file) => !file.type.startsWith('image'));
+ expect(component.getAllByTestId('file')).toHaveLength(nonImageFixture.length);
+ });
+
+ it('Should change locale', async () => {
+ const localeService = new ESLocaleService();
+ localeService.register('ru', ru);
+ localeService.use('ru');
+
+ const component = await render(ESFileListComponent, {
+ componentProperties: {
+ files: filesFixture,
+ canDownload: true,
+ canRemove: true
+ },
+ imports: [ESFileListModule, MatIconTestingModule],
+ providers: [{ provide: ESLocaleService, useValue: localeService }],
+ excludeComponentDeclaration: true
+ });
+ expect(component.getAllByLabelText(ru.fileList.labelDownload)).toHaveLength(
+ filesFixture.length
+ );
+ expect(component.getAllByLabelText(ru.fileList.labelRemove)).toHaveLength(filesFixture.length);
+ expect(component.getAllByText(ru.fileList.labelKB, { exact: false })).toHaveLength(
+ filesFixture.length
+ );
+ });
+});
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.component.html b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.component.html
new file mode 100644
index 00000000..b2900f6b
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.component.html
@@ -0,0 +1,9 @@
+
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.component.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.component.ts
new file mode 100644
index 00000000..6ef70549
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.component.ts
@@ -0,0 +1,22 @@
+import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
+
+import { ESFileListFile } from '../../file-list.types';
+import { filesFixture } from '../../fixtures/files.fixture';
+
+@Component({
+ selector: 'es-file-list-basic',
+ templateUrl: './file-list-story-basic.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class FileListStoryBasicComponent {
+ @Input()
+ public canRemove: boolean;
+ @Input()
+ public canDownload: boolean;
+ @Input()
+ public hideImages: boolean;
+ @Input()
+ public imageTypes: string;
+
+ public files: ESFileListFile[] = filesFixture;
+}
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.module.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.module.ts
new file mode 100644
index 00000000..bbf9d3f8
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { FileListStoryBasicComponent } from './file-list-story-basic.component';
+import { ESFileListModule } from '../../file-list.module';
+
+@NgModule({
+ declarations: [FileListStoryBasicComponent],
+ imports: [CommonModule, ESFileListModule],
+ exports: [FileListStoryBasicComponent]
+})
+export class FileListStoryBasicModule {}
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.source.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.source.ts
new file mode 100644
index 00000000..32079c8e
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/file-list-story-basic.source.ts
@@ -0,0 +1,54 @@
+export const FILE_LIST_STORY_BASIC_SOURCE = {
+ ts: `
+ @Component({
+ ...
+ })
+ export class AppComponent {
+ public files: ESFileListFile[] = [
+ {
+ id: 1,
+ type: 'image/jpg',
+ file: 'https://dummyimage.com/400x400/405ed6/fff.jpg&text=ES',
+ name: 'FileName1.jpg',
+ size: 45678,
+ content: null
+ },
+ {
+ id: 2,
+ type: 'image/jpg',
+ file: 'https://dummyimage.com/400x400/228a0f/fff.jpg&text=ES',
+ name: 'FileName2.jpg',
+ size: 456789,
+ content: null
+ },
+ {
+ id: 3,
+ type: 'application/pdf',
+ file: 'https://dummyimage.com/400x400/d6761c/fff.jpg&text=ES',
+ name: 'FileName3.pdf',
+ size: 4567,
+ content: null
+ },
+ {
+ id: 4,
+ type: 'image/jpg',
+ file: 'https://dummyimage.com/400x400/2dbdb8/fff.jpg&text=ES',
+ name: 'FileName4.jpg',
+ size: 456,
+ content: null
+ }
+ ];
+ }
+ `,
+ html: `
+
+ `
+};
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/index.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/index.ts
new file mode 100644
index 00000000..95ffa3a3
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-basic/index.ts
@@ -0,0 +1,3 @@
+export { FileListStoryBasicComponent } from './file-list-story-basic.component';
+export { FileListStoryBasicModule } from './file-list-story-basic.module';
+export { FILE_LIST_STORY_BASIC_SOURCE } from './file-list-story-basic.source';
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.component.html b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.component.html
new file mode 100644
index 00000000..0c468fab
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.component.html
@@ -0,0 +1,8 @@
+
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.component.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.component.ts
new file mode 100644
index 00000000..02df1d48
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.component.ts
@@ -0,0 +1,23 @@
+import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
+
+import { ESFileListFile } from '../../file-list.types';
+import { filesFixture } from '../../fixtures/files.fixture';
+
+@Component({
+ selector: 'es-file-list-custom-icon',
+ templateUrl: './file-list-story-custom-icon.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class FileListStoryCustomIconComponent {
+ @Input()
+ public canRemove: boolean;
+ @Input()
+ public canDownload: boolean;
+ @Input()
+ public hideImages: boolean;
+ @Input()
+ public imageTypes: string;
+
+ public files: ESFileListFile[] = filesFixture;
+ public customIcon = '/icons/file-list/file.svg';
+}
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.module.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.module.ts
new file mode 100644
index 00000000..649724d2
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { FileListStoryCustomIconComponent } from './file-list-story-custom-icon.component';
+import { ESFileListModule } from '../../file-list.module';
+
+@NgModule({
+ declarations: [FileListStoryCustomIconComponent],
+ imports: [CommonModule, ESFileListModule],
+ exports: [FileListStoryCustomIconComponent]
+})
+export class FileListStoryCustomIconModule {}
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.source.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.source.ts
new file mode 100644
index 00000000..95cae37b
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/file-list-story-custom-icon.source.ts
@@ -0,0 +1,4 @@
+export const FILE_LIST_STORY_CUSTOM_ICON_SOURCE = {
+ html: `
+ `
+};
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/index.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/index.ts
new file mode 100644
index 00000000..0abaceec
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-custom-icon/index.ts
@@ -0,0 +1,3 @@
+export { FileListStoryCustomIconComponent } from './file-list-story-custom-icon.component';
+export { FileListStoryCustomIconModule } from './file-list-story-custom-icon.module';
+export { FILE_LIST_STORY_CUSTOM_ICON_SOURCE } from './file-list-story-custom-icon.source';
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.html b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.html
new file mode 100644
index 00000000..18be56c3
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.html
@@ -0,0 +1,9 @@
+
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.scss b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.scss
new file mode 100644
index 00000000..53e1952c
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.scss
@@ -0,0 +1,15 @@
+.typography {
+ &-body-1 {
+ color: rgba(0, 0, 0, 0.88);
+ font-family: 'Roboto', sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ }
+
+ &-caption {
+ color: rgba(0, 0, 0, 0.54);
+ font-family: 'Roboto', sans-serif;
+ font-size: 12px;
+ line-height: 16px;
+ }
+}
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.ts
new file mode 100644
index 00000000..dadff680
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.component.ts
@@ -0,0 +1,24 @@
+import { Component, Input, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+
+import { ESFileListFile } from '../../file-list.types';
+import { filesFixture } from '../../fixtures/files.fixture';
+
+@Component({
+ selector: 'es-file-list-typography',
+ templateUrl: './file-list-story-typography.component.html',
+ styleUrls: ['./file-list-story-typography.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None
+})
+export class FileListStoryTypographyComponent {
+ @Input()
+ public canRemove: boolean;
+ @Input()
+ public canDownload: boolean;
+ @Input()
+ public hideImages: boolean;
+ @Input()
+ public imageTypes: string;
+
+ public files: ESFileListFile[] = filesFixture;
+}
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.module.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.module.ts
new file mode 100644
index 00000000..bc7f6981
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { FileListStoryTypographyComponent } from './file-list-story-typography.component';
+import { ESFileListModule } from '../../file-list.module';
+
+@NgModule({
+ declarations: [FileListStoryTypographyComponent],
+ imports: [CommonModule, ESFileListModule],
+ exports: [FileListStoryTypographyComponent]
+})
+export class FileListStoryTypographyModule {}
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.source.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.source.ts
new file mode 100644
index 00000000..fba7a500
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/file-list-story-typography.source.ts
@@ -0,0 +1,33 @@
+export const FILE_LIST_STORY_TYPOGRAPHY_SOURCE = {
+ html: `
+
+ `,
+ ts: `
+ @Component({
+ ...
+ encapsulation: ViewEncapsulation.None
+ })
+ export class AppComponent {
+ }
+ `,
+ scss: `
+ .typography {
+ &-body-1 {
+ color: rgba(0, 0, 0, 0.88);
+ font-family: 'Roboto', sans-serif;
+ font-size: 16px;
+ line-height: 24px;
+ }
+
+ &-caption {
+ color: rgba(0, 0, 0, 0.54);
+ font-family: 'Roboto', sans-serif;
+ font-size: 12px;
+ line-height: 16px;
+ }
+ }`
+};
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/index.ts b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/index.ts
new file mode 100644
index 00000000..8915c7f0
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list-story-typography/index.ts
@@ -0,0 +1,3 @@
+export { FileListStoryTypographyComponent } from './file-list-story-typography.component';
+export { FileListStoryTypographyModule } from './file-list-story-typography.module';
+export { FILE_LIST_STORY_TYPOGRAPHY_SOURCE } from './file-list-story-typography.source';
diff --git a/projects/elonkit/src/ui/file-list/__stories__/file-list.stories.mdx b/projects/elonkit/src/ui/file-list/__stories__/file-list.stories.mdx
new file mode 100644
index 00000000..288b26ad
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/__stories__/file-list.stories.mdx
@@ -0,0 +1,147 @@
+import { Meta, Story, ArgsTable } from '@storybook/addon-docs/blocks';
+import { Canvas } from '~storybook/components';
+
+import { action } from '@storybook/addon-actions';
+
+import { ESFileListComponent } from '..';
+
+import {
+ FileListStoryBasicComponent,
+ FileListStoryBasicModule,
+ FILE_LIST_STORY_BASIC_SOURCE
+} from './file-list-story-basic';
+
+import {
+ FileListStoryCustomIconComponent,
+ FileListStoryCustomIconModule,
+ FILE_LIST_STORY_CUSTOM_ICON_SOURCE
+} from './file-list-story-custom-icon';
+
+import {
+ FileListStoryTypographyComponent,
+ FileListStoryTypographyModule,
+ FILE_LIST_STORY_TYPOGRAPHY_SOURCE
+} from './file-list-story-typography';
+
+
+
+# File List
+
+This component displays a list of files.
+
+## Demos
+
+
+
+We can use custom icon for files.
+
+
+
+We can use typography inputs in order to change text presentation.
+
+
+
+## API
+
+
+
+## Interfaces
+
+```ts
+interface ESFileListFile {
+ id?: number;
+ deleted?: boolean;
+ type?: string;
+ base64?: string;
+ file?: string;
+ updatedAt?: string;
+ name: string;
+ size: number;
+ content: File | string;
+}
+```
+
+```ts
+interface ESFileListRemoveAction {
+ file: IESFileListFile;
+ index: number;
+}
+```
+
+Image types string should contain types separated by a comma, e.g. `image/png,image/jpg,image/jpeg`
+
+```ts
+interface ESFileListDefaultOptions {
+ imageTypes?: string;
+ hideImages?: boolean;
+ canRemove?: boolean;
+ canDownload?: boolean;
+ fileNameTypography?: string;
+ fileSizeTypography?: string;
+}
+```
+
+## Constants
+
+Injection token that can be used to configure the default options for all components within an app.
+
+```ts
+import { ES_FILE_LIST_DEFAULT_OPTIONS } from '@elonsoft/elonkit/ui/file-list';
+@NgModule({
+ providers: [
+ {
+ provide: ES_FILE_LIST_DEFAULT_OPTIONS,
+ useValue: {
+ imageTypes: 'image/png',
+ hideImages: true
+ }
+ }
+ ]
+})
+```
diff --git a/projects/elonkit/src/ui/file-list/file-list.component.html b/projects/elonkit/src/ui/file-list/file-list.component.html
new file mode 100644
index 00000000..a90ee8ef
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/file-list.component.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
{{ file.name }}
+
+
+
+
+ {{ getFileSize(file) | async }}
+
+
+ •
+
+ {{ file.updatedAt | date: 'd MMM yyyy' | lowercase }} {{ locale.labelAt }}
+ {{ file.updatedAt | date: 'HH:mm:ss' }}
+
+
+
+
+
+
+
+
diff --git a/projects/elonkit/src/ui/file-list/file-list.component.scss b/projects/elonkit/src/ui/file-list/file-list.component.scss
new file mode 100644
index 00000000..8a56bb1c
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/file-list.component.scss
@@ -0,0 +1,71 @@
+.es-file-list {
+ margin-top: 24px;
+
+ &__file {
+ align-items: center;
+ display: flex;
+
+ &:not(:last-child) {
+ margin-bottom: 16px;
+ }
+ }
+
+ &__icon-wrapper {
+ display: flex;
+ margin-right: 12px;
+ position: relative;
+ }
+
+ &__icon-btn.mat-icon-button {
+ display: flex;
+ height: 24px;
+ justify-content: center;
+ left: 50%;
+ line-height: 24px;
+ position: absolute;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 24px;
+ }
+
+ &__icon-btn.mat-icon-button &__icon {
+ height: 14px;
+ width: 14px;
+ }
+
+ &__icon {
+ height: 100%;
+ width: 100%;
+ }
+
+ &__title {
+ align-items: center;
+ display: flex;
+ min-height: 24px;
+ }
+
+ &__remove.mat-icon-button {
+ height: 24px;
+ line-height: 22px;
+ width: 24px;
+ }
+
+ &__remove &__remove-icon.mat-icon {
+ color: rgba(0, 0, 0, 0.38);
+ font-size: 24px;
+ }
+
+ &__name {
+ margin-right: 8px;
+ }
+
+ &__subtitle {
+ align-items: center;
+ color: rgba(0, 0, 0, 0.38);
+ display: flex;
+
+ &-point {
+ margin: 0 8px;
+ }
+ }
+}
diff --git a/projects/elonkit/src/ui/file-list/file-list.component.ts b/projects/elonkit/src/ui/file-list/file-list.component.ts
new file mode 100644
index 00000000..ce68a751
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/file-list.component.ts
@@ -0,0 +1,214 @@
+import {
+ Component,
+ Input,
+ Output,
+ EventEmitter,
+ ChangeDetectionStrategy,
+ ViewEncapsulation,
+ InjectionToken,
+ Optional,
+ Inject
+} from '@angular/core';
+
+import { validateFileType } from '~utils/validate-file-type';
+import {
+ ESFileListFile,
+ ESFileListRemoveAction,
+ ESFileListDefaultOptions
+} from './file-list.types';
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+import { Observable } from 'rxjs';
+import { ESLocale, ESLocaleService } from '../locale';
+import { map } from 'rxjs/operators';
+
+export const ES_FILE_LIST_DEFAULT_OPTIONS = new InjectionToken(
+ 'ES_FILE_LIST_DEFAULT_OPTIONS'
+);
+
+@Component({
+ selector: 'es-file-list',
+ templateUrl: './file-list.component.html',
+ styleUrls: ['./file-list.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None
+})
+export class ESFileListComponent {
+ /**
+ * File types to be considered as image separated by a comma, e.g. `image/png,image/jpg,image/jpeg`.
+ * Defaults to `image/*`
+ */
+ @Input()
+ public get imageTypes(): string {
+ return this._imageTypes;
+ }
+ public set imageTypes(value: string) {
+ this._imageTypes = value || this.defaultOptions?.imageTypes || 'image/*';
+ }
+ private _imageTypes: string;
+
+ /**
+ * Defines whether component should render images in a list.
+ */
+ @Input()
+ public get hideImages(): boolean {
+ return this._hideImages;
+ }
+ public set hideImages(value: boolean) {
+ this._hideImages = coerceBooleanProperty(value);
+ }
+ private _hideImages: boolean;
+
+ /**
+ * Defines whether remove buttons should be rendered for files.
+ */
+ @Input()
+ public get canRemove(): boolean {
+ return this._canRemove;
+ }
+ public set canRemove(value: boolean) {
+ this._canRemove = coerceBooleanProperty(value);
+ }
+ private _canRemove: boolean;
+
+ /**
+ * Defines whether download file icon should be rendered for files.
+ */
+ @Input()
+ public get canDownload(): boolean {
+ return this._canDownload;
+ }
+ public set canDownload(value: boolean) {
+ this._canDownload = coerceBooleanProperty(value);
+ }
+ private _canDownload: boolean;
+
+ /**
+ * Class applied to file name text.
+ */
+ @Input()
+ public get fileNameTypography(): string {
+ return this._fileNameTypography;
+ }
+ public set fileNameTypography(value: string) {
+ this._fileNameTypography = value || this.defaultOptions?.fileNameTypography || 'mat-body-1';
+ }
+ private _fileNameTypography: string;
+
+ /**
+ * Class applied to file size text.
+ */
+ @Input()
+ public get fileSizeTypography(): string {
+ return this._fileSizeTypography;
+ }
+ public set fileSizeTypography(value: string) {
+ this._fileSizeTypography = value || this.defaultOptions?.fileSizeTypography || 'mat-caption';
+ }
+ private _fileSizeTypography: string;
+
+ /**
+ * Array of files to display.
+ */
+ @Input()
+ public files: ESFileListFile[];
+
+ /**
+ * Path to image to display as file icon instead of the prebuilt icon.
+ */
+ @Input()
+ public fileIconSrc?: string;
+
+ /**
+ * Object with removed file and its index is emitted.
+ */
+ @Output()
+ public remove: EventEmitter = new EventEmitter();
+
+ /**
+ * File is emitted on download.
+ */
+ @Output()
+ public download: EventEmitter = new EventEmitter();
+
+ /**
+ * @internal
+ * @ignore
+ */
+ public locale$: Observable;
+
+ /**
+ * @internal
+ * @ignore
+ */
+ constructor(
+ private localeService: ESLocaleService,
+ @Optional()
+ @Inject(ES_FILE_LIST_DEFAULT_OPTIONS)
+ private defaultOptions: ESFileListDefaultOptions
+ ) {
+ this.locale$ = this.localeService.locale();
+ this.imageTypes = this.defaultOptions?.imageTypes;
+ this.hideImages = this.defaultOptions?.hideImages;
+ this.canDownload = this.defaultOptions?.canDownload;
+ this.canRemove = this.defaultOptions?.canRemove;
+ this.fileNameTypography = this.defaultOptions?.fileNameTypography;
+ this.fileSizeTypography = this.defaultOptions?.fileSizeTypography;
+ }
+
+ /**
+ * @internal
+ * @ignore
+ */
+ public getFileSize(file: ESFileListFile): Observable {
+ const sizeKB = file.size / 1024;
+ const sizeMB = file.size / 1024 / 1024;
+ return this.locale$.pipe(
+ map((translation) =>
+ sizeKB < 1024
+ ? `${sizeKB.toFixed(1)} ${translation.fileList.labelKB}`
+ : `${sizeMB.toFixed(1)} ${translation.fileList.labelMB}`
+ )
+ );
+ }
+
+ /**
+ * @internal
+ * @ignore
+ */
+ public fileTypeValid(file: ESFileListFile): boolean {
+ return validateFileType(file, this.imageTypes);
+ }
+
+ /**
+ * @internal
+ * @ignore
+ */
+ public removeFile(file: ESFileListRemoveAction): void {
+ this.remove.emit(file);
+ }
+
+ /**
+ * @internal
+ * @ignore
+ */
+ public downloadFile(e: MouseEvent, file: ESFileListFile): void {
+ e.preventDefault();
+ this.download.emit(file);
+ }
+
+ /**
+ * @internal
+ * @ignore
+ */
+ public get src(): string {
+ return this.fileIconSrc || './assets/elonkit/file-list/file.svg';
+ }
+
+ /**
+ * @internal
+ * @ignore
+ */
+ public get srcDownload(): string {
+ return './assets/elonkit/file-list/file_download.svg';
+ }
+}
diff --git a/projects/elonkit/src/ui/file-list/file-list.module.ts b/projects/elonkit/src/ui/file-list/file-list.module.ts
new file mode 100644
index 00000000..7edba140
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/file-list.module.ts
@@ -0,0 +1,13 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+
+import { ESFileListComponent } from './file-list.component';
+
+@NgModule({
+ declarations: [ESFileListComponent],
+ imports: [CommonModule, MatButtonModule, MatIconModule],
+ exports: [ESFileListComponent]
+})
+export class ESFileListModule {}
diff --git a/projects/elonkit/src/ui/file-list/file-list.types.ts b/projects/elonkit/src/ui/file-list/file-list.types.ts
new file mode 100644
index 00000000..2ea19931
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/file-list.types.ts
@@ -0,0 +1,25 @@
+export interface ESFileListFile {
+ id?: number;
+ deleted?: boolean;
+ type?: string;
+ base64?: string;
+ file?: string;
+ updatedAt?: string;
+ name: string;
+ size: number;
+ content: File | string;
+}
+
+export interface ESFileListRemoveAction {
+ file: ESFileListFile;
+ index: number;
+}
+
+export interface ESFileListDefaultOptions {
+ imageTypes?: string;
+ hideImages?: boolean;
+ canRemove?: boolean;
+ canDownload?: boolean;
+ fileNameTypography?: string;
+ fileSizeTypography?: string;
+}
diff --git a/projects/elonkit/src/ui/file-list/fixtures/files.fixture.ts b/projects/elonkit/src/ui/file-list/fixtures/files.fixture.ts
new file mode 100644
index 00000000..23dfa408
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/fixtures/files.fixture.ts
@@ -0,0 +1,39 @@
+import { ESFileListFile } from '../file-list.types';
+
+export const filesFixture: ESFileListFile[] = [
+ {
+ id: 1,
+ type: 'image/jpg',
+ file: 'https://dummyimage.com/400x400/405ed6/fff.jpg&text=ES',
+ name: 'FileName1.jpg',
+ size: 45678,
+ content: null,
+ updatedAt: '2014-09-08T08:02:17-05:00'
+ },
+ {
+ id: 2,
+ type: 'image/jpg',
+ file: 'https://dummyimage.com/400x400/228a0f/fff.jpg&text=ES',
+ name: 'Ochen_Dlinnoe_nazvanie_faila.jpg',
+ size: 456789,
+ content: null,
+ updatedAt: '2020-10-11T08:12:17-05:00'
+ },
+ {
+ id: 3,
+ type: 'application/pdf',
+ file: 'https://dummyimage.com/400x400/d6761c/fff.jpg&text=ES',
+ name: 'Nazvanie_faila.pdf',
+ size: 4567,
+ content: null,
+ updatedAt: '2009-04-02T08:08:12-05:00'
+ },
+ {
+ id: 4,
+ type: 'image/jpg',
+ file: 'https://dummyimage.com/400x400/2dbdb8/fff.jpg&text=ES',
+ name: 'Vtoroe_nazvanie_faila.jpg',
+ size: 456,
+ content: null
+ }
+];
diff --git a/projects/elonkit/src/ui/file-list/index.ts b/projects/elonkit/src/ui/file-list/index.ts
new file mode 100644
index 00000000..7e1a213e
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/index.ts
@@ -0,0 +1 @@
+export * from './public-api';
diff --git a/projects/elonkit/src/ui/file-list/ng-package.json b/projects/elonkit/src/ui/file-list/ng-package.json
new file mode 100644
index 00000000..789c95e4
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/ng-package.json
@@ -0,0 +1,5 @@
+{
+ "lib": {
+ "entryFile": "public-api.ts"
+ }
+}
diff --git a/projects/elonkit/src/ui/file-list/public-api.ts b/projects/elonkit/src/ui/file-list/public-api.ts
new file mode 100644
index 00000000..af2b770d
--- /dev/null
+++ b/projects/elonkit/src/ui/file-list/public-api.ts
@@ -0,0 +1,7 @@
+export { ESFileListModule } from './file-list.module';
+export { ESFileListComponent, ES_FILE_LIST_DEFAULT_OPTIONS } from './file-list.component';
+export {
+ ESFileListFile,
+ ESFileListDefaultOptions,
+ ESFileListRemoveAction
+} from './file-list.types';
diff --git a/projects/elonkit/src/ui/locale/locales/en.ts b/projects/elonkit/src/ui/locale/locales/en.ts
index ba3b5730..19624191 100644
--- a/projects/elonkit/src/ui/locale/locales/en.ts
+++ b/projects/elonkit/src/ui/locale/locales/en.ts
@@ -22,5 +22,12 @@ export const en = {
labelHH: 'HH',
labelMM: 'MM',
labelSS: 'SS'
+ },
+ fileList: {
+ labelDownload: 'Download',
+ labelRemove: 'Remove',
+ labelKB: 'KB',
+ labelMB: 'MB',
+ labelAt: 'at'
}
};
diff --git a/projects/elonkit/src/ui/locale/locales/ru.ts b/projects/elonkit/src/ui/locale/locales/ru.ts
index f7aa4a18..75402ede 100644
--- a/projects/elonkit/src/ui/locale/locales/ru.ts
+++ b/projects/elonkit/src/ui/locale/locales/ru.ts
@@ -22,5 +22,12 @@ export const ru = {
labelHH: 'ЧЧ',
labelMM: 'ММ',
labelSS: 'СС'
+ },
+ fileList: {
+ labelDownload: 'Скачать',
+ labelRemove: 'Удалить',
+ labelKB: 'КБ',
+ labelMB: 'МБ',
+ labelAt: 'в'
}
};
diff --git a/projects/elonkit/storybook/assets/icons/file-list/file.svg b/projects/elonkit/storybook/assets/icons/file-list/file.svg
new file mode 100644
index 00000000..a1a6d2b7
--- /dev/null
+++ b/projects/elonkit/storybook/assets/icons/file-list/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file