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 + + + + {((args, context) => ({ + component: FileListStoryBasicComponent, + moduleMetadata: { + imports: [FileListStoryBasicModule] + }, + props: { + ...args, + onRemove: action('onRemove'), + onDownload: action('onDownload') + } + })).bind({})} + + + +We can use custom icon for files. + + + + {((args, context) => ({ + component: FileListStoryCustomIconComponent, + moduleMetadata: { + imports: [FileListStoryCustomIconModule] + }, + props: { + ...args + } + })).bind({})} + + + +We can use typography inputs in order to change text presentation. + + + + {((args, context) => ({ + component: FileListStoryTypographyComponent, + moduleMetadata: { + imports: [FileListStoryTypographyModule] + }, + props: { + ...args + } + })).bind({})} + + + +## 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