Feat/input file (#1881)
* feat(input-file): create input-file component
* chore: improvements after review
* chore: update after review
* chore: update translations
* chore: - fixed demo with form submit, submit was not prevented
- fixed checking allowed file extensions
- fixed clicking on select file button in drag and drop area
* chore: since the input-file does not upload files itself but enables user to select files, I replaced "upload" and "upload" with "select" and "selected" where applicable
* chore: - removed unused properties allowedFileTypes and allowedFileExtensions from lion-input-file
- cleaned up docs
* chore: - changed type Array.<type> to Array<type>
- removed redundant type definition
* fix: - FocusMixin: moved registering events for from connectedCallback to firstUpdated since _focusableNode is sometimes not available yet
- SlotMixin: changed updated to update in since slots were rendered too late (related to previous fix in FocusMixin.js)
* fix: renamed lion-uploaded-file-list.js to lion-selected-file-list.js
* fix: fixed test for lion-selected-file-list
* fix: fixed typ
* wip
* fix: - fixed issue with multiple file selection where element would not select valid files after invalid ones
- added getMessage method to FileValidation that returns empty string to prevent message being shown that error message must be configured
- fixed tests
* chore: replaced `uploadOnFormSubmit` with `uploadOnSelect` and flipped the default value to false. When `uploadOnSelect` is set to true, the file will be uploaded as soon as it is selected.
* fix: - replaced `uploadOnFormSubmit` with `uploadOnSelect` and flipped the default value to false. When `uploadOnSelect` is set to true, the file will be uploaded as soon as it is selected.
- fixed issue where a valid file was not selected and added to the file list if it was preceded by an invalid file
* chore: removed redundant README.md
* fix: fixed failing test
* chore: added missing type annotation
* chore: annotated event param as optional
---------
Co-authored-by: Danny Moerkerke <danny.moerkerke@ing.com>
Co-authored-by: Thijs Louisse <Thijs.Louisse@ing.com>
This commit is contained in:
parent
649bcc6320
commit
d2de984f0b
64 changed files with 3807 additions and 73 deletions
5
.changeset/serious-trees-thank.md
Normal file
5
.changeset/serious-trees-thank.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@lion/ui': minor
|
||||
---
|
||||
|
||||
[input-file] Create input-file component
|
||||
|
|
@ -51,5 +51,5 @@ Support for this `exports` field was added to TypeScript 4.7 so for that reason,
|
|||
|
||||
### A new changelog
|
||||
|
||||
`@lion/ui` has [a new single CHANGELOG.md]( https://github.com/ing-bank/lion/blob/master/packages/ui/CHANGELOG.md) in `/packages/ui` for the whole package. The older individual changelogs can be
|
||||
`@lion/ui` has [a new single CHANGELOG.md](https://github.com/ing-bank/lion/blob/master/packages/ui/CHANGELOG.md) in `/packages/ui` for the whole package. The older individual changelogs can be
|
||||
found in the `/packages/ui/_legacy-changelogs` folder.
|
||||
|
|
|
|||
3
docs/components/input-file/index.md
Normal file
3
docs/components/input-file/index.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Input File ||20
|
||||
|
||||
-> go to Overview
|
||||
32
docs/components/input-file/overview.md
Normal file
32
docs/components/input-file/overview.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Input File >> Overview ||10
|
||||
|
||||
A web component based on the file input field.
|
||||
|
||||
```js script
|
||||
import { html } from '@mdjs/mdjs-preview';
|
||||
import '@lion/ui/define/lion-input-file.js';
|
||||
```
|
||||
|
||||
```js preview-story
|
||||
export const main = () => {
|
||||
return html` <lion-input-file label="Upload" name="upload"></lion-input-file> `;
|
||||
};
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Based on our [input](../input/overview.md)
|
||||
- Default labels and validation messages in different languages
|
||||
- Options for multi file upload and drop-zone.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm i --save @lion/ui
|
||||
```
|
||||
|
||||
```js
|
||||
import { LionInputFile } from '@lion/ui/input-file.js';
|
||||
// or
|
||||
import '@lion/ui/define/lion-input-file.js';
|
||||
```
|
||||
425
docs/components/input-file/use-cases.md
Normal file
425
docs/components/input-file/use-cases.md
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
# Input File >> Use Cases ||20
|
||||
|
||||
```js script
|
||||
import { Required, Validator } from '@lion/ui/form-core.js';
|
||||
import { loadDefaultFeedbackMessages } from '@lion/ui/validate-messages.js';
|
||||
import { html } from '@mdjs/mdjs-preview';
|
||||
import { ScopedElementsMixin } from '@open-wc/scoped-elements';
|
||||
import { LitElement } from 'lit';
|
||||
import '@lion/ui/define/lion-input-file.js';
|
||||
loadDefaultFeedbackMessages();
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
API calls to upload the selected files can be done in below two ways which is driven by `uploadOnSelect` property
|
||||
|
||||
- On `form` submit
|
||||
- On file selection
|
||||
|
||||
## Parameters
|
||||
|
||||
```html
|
||||
<lion-input-file
|
||||
multiple
|
||||
label="Attachments"
|
||||
help-text="Signature scan file"
|
||||
max-file-size="1024000"
|
||||
accept="application/PDF"
|
||||
upload-on-form-submit
|
||||
.uploadResponse="${uploadResponse}"
|
||||
>
|
||||
</lion-input-file>
|
||||
```
|
||||
|
||||
### modelValue
|
||||
|
||||
Array of [File](https://developer.mozilla.org/en-US/docs/Web/API/File); Contains all the uploaded files
|
||||
|
||||
### multiple
|
||||
|
||||
Boolean; setting to `true` allows selecting multiple files.
|
||||
|
||||
### EnableDragAndDrop
|
||||
|
||||
Boolean; setting to `true` allows file upload through drag and drop.
|
||||
|
||||
### uploadOnSelect
|
||||
|
||||
Boolean;
|
||||
|
||||
- Set to `true` when API calls for file upload needs to be done on file selection
|
||||
- Set to `false` when API calls for file upload needs to be done on form submit.
|
||||
- Default is `false`.
|
||||
|
||||
### uploadResponse
|
||||
|
||||
An array of the file-specific data to display the feedback of fileUpload
|
||||
|
||||
Data structure for uploadResponse array:
|
||||
|
||||
```js
|
||||
[
|
||||
{
|
||||
name: 'file1.txt', // name of uploaded file
|
||||
status: '', // SUCCESS | FAIL | LOADING
|
||||
errorMessage: '', // custom error message to be displayed
|
||||
id: '', // unique id for future execution reference
|
||||
},
|
||||
{
|
||||
name: 'file2.txt', // name of uploaded file
|
||||
status: '', // SUCCESS | FAIL | LOADING
|
||||
errorMessage: '', // custom error message to be displayed
|
||||
id: '', // unique id for future execution reference
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
### model-value-changed
|
||||
|
||||
Fired when modelValue property changes.
|
||||
|
||||
### file-list-changed
|
||||
|
||||
Fired when files are uploaded. Event `detail` gives list of newly added files in `newFiles` key
|
||||
|
||||
```js
|
||||
ev.detail.newFiles;
|
||||
```
|
||||
|
||||
### file-removed
|
||||
|
||||
Fired when a file is removed. Event `detail` gives
|
||||
|
||||
- File objected for removed file in `ev.detail.removedFile`
|
||||
- Status of this file can be used as `ev.detail.status`
|
||||
- uploadResponse for this file as set using `uploadResponse` property. Can use used as `ev.detail.uploadResponse`
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic File upload
|
||||
|
||||
When file has to be uploaded as soon as it is selected by the user. Use `file-list-changed` event to get the newly added file, upload it to your server and set the response back to component via `uploadResponse` property.
|
||||
|
||||
```js preview-story
|
||||
export const basicFileUpload = () => {
|
||||
return html`
|
||||
<lion-input-file
|
||||
label="Label"
|
||||
max-file-size="1024000"
|
||||
accept=".jpg,.svg,.xml,image/svg+xml"
|
||||
@file-list-changed="${ev => {
|
||||
console.log('fileList', ev.detail.newFiles);
|
||||
}}"
|
||||
>
|
||||
</lion-input-file>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
#### Accept
|
||||
|
||||
The `accept` attribute value is a string that defines the file types the file input should accept. This string is a comma-separated list of unique file type specifiers.
|
||||
|
||||
For more info please consult the [MDN documentation for "accept"](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept).
|
||||
|
||||
```js preview-story
|
||||
export const acceptValidator = () => {
|
||||
return html`
|
||||
<lion-input-file
|
||||
accept=".jpg,.svg,.xml,image/svg+xml"
|
||||
label="Upload"
|
||||
enable-drop-zone
|
||||
@file-list-changed="${ev => {
|
||||
console.log(ev.detail.newFiles);
|
||||
}}"
|
||||
>
|
||||
</lion-input-file>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
#### Maximum File Size
|
||||
|
||||
The `max-file-size` attribute sets the maximum file size in bytes.
|
||||
|
||||
```js preview-story
|
||||
export const sizeValidator = () => {
|
||||
return html`
|
||||
<lion-input-file
|
||||
max-file-size="2048"
|
||||
label="Upload"
|
||||
@file-list-changed="${ev => {
|
||||
console.log(ev.detail.newFiles);
|
||||
}}"
|
||||
>
|
||||
</lion-input-file>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Multiple file upload
|
||||
|
||||
When file has to be uploaded as soon as it is selected by the user. Use `file-list-changed` event to get the newly added files, upload it to your server and set the response back to component via `uploadResponse` property for each file.
|
||||
|
||||
```js preview-story
|
||||
export const multipleFileUpload = () => {
|
||||
return html`
|
||||
<lion-input-file
|
||||
label="Upload"
|
||||
name="upload"
|
||||
multiple
|
||||
max-file-size="1024000"
|
||||
@file-removed="${ev => {
|
||||
console.log('removed file details', ev.detail);
|
||||
}}"
|
||||
@file-list-changed="${ev => {
|
||||
console.log('file-list-changed', ev.detail.newFiles);
|
||||
}}"
|
||||
@model-value-changed="${ev => {
|
||||
console.log('model-value-changed', ev);
|
||||
}}"
|
||||
>
|
||||
</lion-input-file>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Pre-filled state
|
||||
|
||||
The pre-filled state of the file upload component shows the files that were being assigned by the user initially.
|
||||
|
||||
On every reload of page, the file upload component reverts to the initial state and using the `uploadResponse` property, shows the names(not links) of previously assigned files.
|
||||
|
||||
**Note:**
|
||||
|
||||
- the list of prefilled files will not be available in `modelValue` or `serializedValue`. This is because value cannot be assigned to native `input` with type `file`
|
||||
- Only `Required` validation (if set on `lion-file-input`) is triggered for files which are prefilled.
|
||||
|
||||
```js preview-story
|
||||
export const prefilledState = () => {
|
||||
return html`
|
||||
<lion-input-file
|
||||
label="Upload"
|
||||
name="myFiles"
|
||||
multiple
|
||||
.validators="${[new Required()]}"
|
||||
.uploadResponse="${[
|
||||
{
|
||||
name: 'file1.txt',
|
||||
status: 'SUCCESS',
|
||||
errorMessage: '',
|
||||
id: '132',
|
||||
},
|
||||
{
|
||||
name: 'file2.txt',
|
||||
status: 'SUCCESS',
|
||||
errorMessage: '',
|
||||
id: 'abcd',
|
||||
},
|
||||
]}"
|
||||
>
|
||||
</lion-input-file>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### With form submit
|
||||
|
||||
**Note:** When `lion-input-file` is prefilled, the list of prefilled files will not be available in `modelValue` or `serializedValue`. This is because value cannot be assigned to native `input` with type `file`.
|
||||
|
||||
```js preview-story
|
||||
const myFormReset = ev => {
|
||||
const input = ev.target.querySelector('lion-input-file');
|
||||
input.reset();
|
||||
console.log(input.modelValue);
|
||||
};
|
||||
|
||||
const myFormSubmit = ev => {
|
||||
ev.preventDefault();
|
||||
const input = ev.target.querySelector('lion-input-file');
|
||||
console.log(input.hasFeedbackFor);
|
||||
console.log(input.serializedValue);
|
||||
return false;
|
||||
};
|
||||
|
||||
export const withIngForm = () => {
|
||||
class FilenameLengthValidator extends Validator {
|
||||
static get validatorName() {
|
||||
return 'FilenameLengthValidator';
|
||||
}
|
||||
|
||||
static getMessage(data) {
|
||||
return `Filename length should not exceed ${data.params.maxFilenameLength} characters`;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
checkFilenameLength(val, allowedFileNameLength) {
|
||||
return val <= allowedFileNameLength;
|
||||
}
|
||||
|
||||
execute(modelVal, { maxFilenameLength }) {
|
||||
const invalidFileIndex = modelVal.findIndex(
|
||||
file => !this.checkFilenameLength(file.name.length, maxFilenameLength),
|
||||
);
|
||||
return invalidFileIndex > -1;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
<form @submit="${myFormSubmit}" @reset="${myFormReset}">
|
||||
<lion-input-file
|
||||
label="Upload"
|
||||
name="upload"
|
||||
multiple
|
||||
.validators="${[new Required(), new FilenameLengthValidator({ maxFilenameLength: 20 })]}"
|
||||
.uploadResponse="${[
|
||||
{
|
||||
name: 'file1.zip',
|
||||
status: 'SUCCESS',
|
||||
id: '132',
|
||||
},
|
||||
{
|
||||
name: 'file2.zip',
|
||||
status: 'SUCCESS',
|
||||
id: 'abcd',
|
||||
},
|
||||
]}"
|
||||
>
|
||||
</lion-input-file>
|
||||
<button type="reset">Reset</button>
|
||||
<button>Upload</button>
|
||||
</form>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Without form submit
|
||||
|
||||
Set `uploadOnSelect` property to `true`. This option can be used when Server API calls are needed as soon as it is selected by the user.
|
||||
|
||||
For this scenario, the list of files is displayed based on the `uploadResponse` property which needs to maintained by the consumers of this component
|
||||
|
||||
Below is the flow:
|
||||
|
||||
#### When user uploads new file(s)
|
||||
|
||||
1. `file-list-changed` event is fired from component with the newly added files
|
||||
2. Initiate the request to your backend API and set the status of the relevant files to `LOADING` in `uploadResponse` property
|
||||
3. Once the API request in completed, set the status of relevant files to `SUCCESS` or `FAIL` in `uploadResponse` property
|
||||
|
||||
#### When user deletes a file
|
||||
|
||||
1. `file-removed` event is fired from component with the deleted file
|
||||
2. Initiate the delete request to your backend API and set the status of the relevant files as `LOADING` in `uploadResponse` property
|
||||
3. Once the API request in completed, delete the file object from `uploadResponse` property
|
||||
|
||||
```js preview-story
|
||||
export const uploadWithoutFormSubmit = () => {
|
||||
return html`
|
||||
<lion-input-file
|
||||
label="Upload"
|
||||
name="upload"
|
||||
.multiple="${true}"
|
||||
.uploadOnSelect="${true}"
|
||||
@file-removed="${ev => {
|
||||
ev.target.uploadResponse[
|
||||
ev.target.uploadResponse.findIndex(file => file.name === ev.detail.uploadResponse.name)
|
||||
].status = 'LOADING';
|
||||
ev.target.uploadResponse = [...ev.target.uploadResponse]; // update uploadResponse after API calls are completed
|
||||
setTimeout(() => {
|
||||
ev.target.uploadResponse[
|
||||
ev.target.uploadResponse.findIndex(file => file.name === ev.detail.uploadResponse.name)
|
||||
] = {};
|
||||
ev.target.uploadResponse = [...ev.target.uploadResponse]; // update uploadResponse after API calls are completed
|
||||
}, 1000);
|
||||
}}"
|
||||
@file-list-changed="${ev => {
|
||||
if (!ev.detail.newFiles[0]) {
|
||||
return;
|
||||
}
|
||||
ev.target.uploadResponse[
|
||||
ev.target.uploadResponse.findIndex(file => file.name === ev.detail.newFiles[0].name)
|
||||
].status = 'LOADING';
|
||||
ev.target.uploadResponse = [...ev.target.uploadResponse]; // update uploadResponse after API calls are completed
|
||||
|
||||
if (ev.detail.newFiles[1]) {
|
||||
ev.target.uploadResponse[
|
||||
ev.target.uploadResponse.findIndex(file => file.name === ev.detail.newFiles[1].name)
|
||||
].status = 'LOADING';
|
||||
ev.target.uploadResponse = [...ev.target.uploadResponse]; // update uploadResponse after API calls are completed
|
||||
}
|
||||
setTimeout(() => {
|
||||
ev.target.uploadResponse[
|
||||
ev.target.uploadResponse.findIndex(file => file.name === ev.detail.newFiles[0].name)
|
||||
].status = 'SUCCESS';
|
||||
ev.target.uploadResponse = [...ev.target.uploadResponse]; // update uploadResponse after API calls are completed
|
||||
}, 3000);
|
||||
|
||||
setTimeout(() => {
|
||||
if (ev.detail.newFiles[1]) {
|
||||
const file1Status = {
|
||||
name: ev.detail.newFiles[1].name,
|
||||
status: 'FAIL',
|
||||
errorMessage: 'error from server',
|
||||
};
|
||||
ev.target.uploadResponse[
|
||||
ev.target.uploadResponse.findIndex(file => file.name === ev.detail.newFiles[1].name)
|
||||
] = {
|
||||
name: ev.detail.newFiles[1].name,
|
||||
status: 'FAIL',
|
||||
errorMessage: 'error from server',
|
||||
};
|
||||
ev.target.uploadResponse = [...ev.target.uploadResponse];
|
||||
}
|
||||
}, 3000);
|
||||
}}"
|
||||
>
|
||||
</lion-input-file>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Drag and Drop
|
||||
|
||||
Set the `enableDropZone` parameter to `true` to use the drag and drop functionality in the component.
|
||||
|
||||
Drag and drop the files to be uploaded to the server.
|
||||
|
||||
```js preview-story
|
||||
export const dragAndDrop = () => {
|
||||
return html`
|
||||
<lion-input-file
|
||||
label="Upload"
|
||||
name="myFiles"
|
||||
accept=".png"
|
||||
max-file-size="1024000"
|
||||
enable-drop-zone
|
||||
.multiple="${true}"
|
||||
.validators="${[new Required()]}"
|
||||
>
|
||||
</lion-input-file>
|
||||
`;
|
||||
};
|
||||
```
|
||||
|
||||
### Posting file to API using axios
|
||||
|
||||
You can retrieve the uploaded files in `file-list-changed` event or from `modelValue` property of this component
|
||||
|
||||
To submit files you can refer to the following code snippet:
|
||||
|
||||
```js
|
||||
const formData = new FormData();
|
||||
const inputFile = document.querySelector('lion-input-file').modelValue;
|
||||
formData.append('file', inputFile.querySelector('input').files);
|
||||
axios.post('upload_file', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
|
@ -16,6 +16,7 @@ import '@lion/ui/define/lion-input-amount.js';
|
|||
import '@lion/ui/define/lion-input-date.js';
|
||||
import '@lion/ui/define/lion-input-datepicker.js';
|
||||
import '@lion/ui/define/lion-input-email.js';
|
||||
import '@lion/ui/define/lion-input-file.js';
|
||||
import '@lion/ui/define/lion-input-tel.js';
|
||||
import '@lion/ui/define/lion-input-tel-dropdown.js';
|
||||
import '@lion/ui/define/lion-input-iban.js';
|
||||
|
|
@ -75,6 +76,7 @@ export const main = () => {
|
|||
<lion-input-amount name="money" label="Money"></lion-input-amount>
|
||||
<lion-input-iban name="iban" label="Iban"></lion-input-iban>
|
||||
<lion-input-email name="email" label="Email"></lion-input-email>
|
||||
<lion-input-file name="file" label="File"></lion-input-file>
|
||||
<lion-input-tel name="tel" label="Telephone number"></lion-input-tel>
|
||||
<lion-checkbox-group
|
||||
label="What do you like?"
|
||||
|
|
|
|||
|
|
@ -90,8 +90,8 @@ const SlotMixinImplementation = superclass =>
|
|||
* Here we rerender slots defined with a `SlotRerenderObject`
|
||||
* @param {import('lit-element').PropertyValues } changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
update(changedProperties) {
|
||||
super.update(changedProperties);
|
||||
|
||||
if (this.__slotsThatNeedRerender.size) {
|
||||
for (const slotName of Array.from(this.__slotsThatNeedRerender)) {
|
||||
|
|
|
|||
|
|
@ -51,8 +51,11 @@ const FocusMixinImplementation = superclass =>
|
|||
this.autofocus = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
/**
|
||||
* @param {import('lit').PropertyValues } changedProperties
|
||||
*/
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.__registerEventsForFocusMixin();
|
||||
this.__syncAutofocusToFocusableElement();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,9 +72,11 @@ export class LionField extends FormControlMixin(
|
|||
|
||||
/**
|
||||
* Dispatches custom bubble event
|
||||
* @param {Event=} ev
|
||||
* @protected
|
||||
*/
|
||||
_onChange() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_onChange(ev) {
|
||||
/** @protectedEvent user-input-changed */
|
||||
this.dispatchEvent(new Event('user-input-changed', { bubbles: true }));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ import { html, LitElement } from 'lit';
|
|||
/**
|
||||
* @typedef {import('./Validator.js').Validator} Validator
|
||||
* @typedef {import('lit').TemplateResult} TemplateResult
|
||||
* @typedef {Object} messageMap
|
||||
* @property {string | Node} message
|
||||
* @property {string} type
|
||||
* @property {Validator} [validator]
|
||||
* @typedef {import('../../types/validate/ValidateMixinTypes.js').FeedbackMessage} FeedbackMessage
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
@ -47,7 +44,7 @@ export class LionValidationFeedback extends LitElement {
|
|||
if (this.currentType === 'success') {
|
||||
this.removeMessage = window.setTimeout(() => {
|
||||
this.removeAttribute('type');
|
||||
/** @type {messageMap[]} */
|
||||
/** @type {FeedbackMessage[]} */
|
||||
this.feedbackData = [];
|
||||
}, 3000);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Validator } from './Validator.js';
|
||||
|
||||
/**
|
||||
* @desc Instead of evaluating the result of a regular validator, a ResultValidator looks
|
||||
* Instead of evaluating the result of a regular Validator, a ResultValidator looks
|
||||
* at the total result of regular Validators. Instead of an execute function, it uses a
|
||||
* 'executeOnResults' Validator.
|
||||
* ResultValidators cannot be async, and should not contain an execute method.
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import { FormControlHost } from '../FormControlMixinTypes.js';
|
|||
import { SyncUpdatableHost } from '../utils/SyncUpdatableMixinTypes.js';
|
||||
import { Validator } from '../../src/validate/Validator.js';
|
||||
|
||||
type FeedbackMessage = {
|
||||
export type FeedbackMessage = {
|
||||
message: string | Node;
|
||||
type: string;
|
||||
type: ValidationType | string;
|
||||
validator?: Validator;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ describe('Form inside dialog Integrations', () => {
|
|||
'lion-input-amount',
|
||||
'lion-input-iban',
|
||||
'lion-input-email',
|
||||
'lion-input-file',
|
||||
'lion-input-tel',
|
||||
'lion-input-tel-dropdown',
|
||||
'lion-checkbox-group',
|
||||
|
|
|
|||
|
|
@ -1,49 +1,20 @@
|
|||
import { elementUpdated, expect, fixture } from '@open-wc/testing';
|
||||
import { html } from 'lit';
|
||||
import './helpers/umbrella-form.js';
|
||||
import { getAllFieldsAndFormGroups } from './helpers/helpers.js';
|
||||
|
||||
import '@lion/ui/define/lion-button.js';
|
||||
import '@lion/ui/define/lion-button-submit.js';
|
||||
import '@lion/ui/define/lion-button-reset.js';
|
||||
|
||||
import '@lion/ui/define/lion-checkbox.js';
|
||||
import '@lion/ui/define/lion-checkbox-group.js';
|
||||
import '@lion/ui/define/lion-combobox.js';
|
||||
|
||||
import '@lion/ui/define/lion-dialog.js';
|
||||
|
||||
import '@lion/ui/define/lion-form.js';
|
||||
import '@lion/ui/define/lion-fieldset.js';
|
||||
import '@lion/ui/define/lion-select-rich.js';
|
||||
import '@lion/ui/define/lion-select.js';
|
||||
|
||||
import '@lion/ui/define/lion-input-stepper.js';
|
||||
import '@lion/ui/define/lion-input-range.js';
|
||||
import '@lion/ui/define/lion-input-amount.js';
|
||||
import '@lion/ui/define/lion-input.js';
|
||||
import '@lion/ui/define/lion-input-date.js';
|
||||
import '@lion/ui/define/lion-input-iban.js';
|
||||
import '@lion/ui/define/lion-input-email.js';
|
||||
import '@lion/ui/define/lion-input-tel-dropdown.js';
|
||||
import '@lion/ui/define/lion-input-datepicker.js';
|
||||
|
||||
import '@lion/ui/define/lion-textarea.js';
|
||||
|
||||
import '@lion/ui/define/lion-radio.js';
|
||||
import '@lion/ui/define/lion-radio-group.js';
|
||||
|
||||
import '@lion/ui/define/lion-listbox.js';
|
||||
import '@lion/ui/define/lion-option.js';
|
||||
import '@lion/ui/define/lion-options.js';
|
||||
|
||||
import '@lion/ui/define/lion-validation-feedback.js';
|
||||
import './helpers/umbrella-form.js';
|
||||
/**
|
||||
* @typedef {import('../../form-core/src/LionField.js').LionField} LionField
|
||||
* @typedef {import('../../input-file/types/input-file.js').InputFile} InputFile
|
||||
* @typedef {import('../../button/src/LionButton.js').LionButton} LionButton
|
||||
* @typedef {import('./helpers/umbrella-form.js').UmbrellaForm} UmbrellaForm
|
||||
*/
|
||||
|
||||
const file = /** @type {InputFile} */ (
|
||||
new File(['foo'], 'foo.txt', {
|
||||
type: 'text/plain',
|
||||
})
|
||||
);
|
||||
|
||||
const fullyPrefilledSerializedValue = {
|
||||
fullName: { firstName: 'Lorem', lastName: 'Ipsum' },
|
||||
date: '2000-12-12',
|
||||
|
|
@ -57,6 +28,7 @@ const fullyPrefilledSerializedValue = {
|
|||
favoriteFruit: 'Banana',
|
||||
favoriteMovie: 'Rocky',
|
||||
favoriteColor: 'hotpink',
|
||||
file: [],
|
||||
lyrics: '1',
|
||||
notifications: {
|
||||
checked: true,
|
||||
|
|
@ -81,6 +53,7 @@ const fullyChangedSerializedValue = {
|
|||
favoriteFruit: '',
|
||||
favoriteMovie: '',
|
||||
favoriteColor: '',
|
||||
file: [file],
|
||||
lyrics: '2',
|
||||
notifications: {
|
||||
checked: false,
|
||||
|
|
@ -189,6 +162,7 @@ describe(`Submitting/Resetting/Clearing Form`, async () => {
|
|||
favoriteFruit: '',
|
||||
favoriteMovie: '',
|
||||
favoriteColor: '',
|
||||
file: [],
|
||||
lyrics: '',
|
||||
notifications: {
|
||||
checked: false,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ describe('Form Integrations', () => {
|
|||
favoriteFruit: 'Banana',
|
||||
favoriteMovie: 'Rocky',
|
||||
favoriteColor: 'hotpink',
|
||||
file: [],
|
||||
lyrics: '1',
|
||||
range: 2.3,
|
||||
terms: [],
|
||||
|
|
@ -74,6 +75,7 @@ describe('Form Integrations', () => {
|
|||
favoriteFruit: 'Banana',
|
||||
favoriteMovie: 'Rocky',
|
||||
favoriteColor: 'hotpink',
|
||||
file: '',
|
||||
lyrics: 'Fire up that loud',
|
||||
range: 2.3,
|
||||
terms: [],
|
||||
|
|
@ -98,6 +100,7 @@ describe('Form Integrations', () => {
|
|||
money: '',
|
||||
iban: '',
|
||||
email: '',
|
||||
file: [],
|
||||
checkers: ['foo', 'bar'],
|
||||
dinosaurs: 'brontosaurus',
|
||||
favoriteFruit: 'Banana',
|
||||
|
|
@ -129,6 +132,7 @@ describe('Form Integrations', () => {
|
|||
'lion-input-amount',
|
||||
'lion-input-iban',
|
||||
'lion-input-email',
|
||||
'lion-input-file',
|
||||
'lion-input-tel',
|
||||
'lion-input-tel-dropdown',
|
||||
'lion-checkbox-group',
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
import { LitElement, html } from 'lit';
|
||||
import { Required, MinLength } from '@lion/ui/form-core.js';
|
||||
import '@lion/ui/define/lion-form.js';
|
||||
import '@lion/ui/define/lion-fieldset.js';
|
||||
import '@lion/ui/define/lion-input.js';
|
||||
import '@lion/ui/define/lion-input-date.js';
|
||||
import '@lion/ui/define/lion-input-datepicker.js';
|
||||
import '@lion/ui/define/lion-input-amount.js';
|
||||
import '@lion/ui/define/lion-input-iban.js';
|
||||
import '@lion/ui/define/lion-input-email.js';
|
||||
import '@lion/ui/define/lion-input-tel.js';
|
||||
import '@lion/ui/define/lion-input-tel-dropdown.js';
|
||||
import '@lion/ui/define/lion-button-reset.js';
|
||||
import '@lion/ui/define/lion-button-submit.js';
|
||||
import '@lion/ui/define/lion-checkbox-group.js';
|
||||
import '@lion/ui/define/lion-checkbox.js';
|
||||
import '@lion/ui/define/lion-radio-group.js';
|
||||
import '@lion/ui/define/lion-radio.js';
|
||||
import '@lion/ui/define/lion-select.js';
|
||||
import '@lion/ui/define/lion-select-rich.js';
|
||||
import '@lion/ui/define/lion-combobox.js';
|
||||
import '@lion/ui/define/lion-fieldset.js';
|
||||
import '@lion/ui/define/lion-form.js';
|
||||
import '@lion/ui/define/lion-input-amount.js';
|
||||
import '@lion/ui/define/lion-input-date.js';
|
||||
import '@lion/ui/define/lion-input-datepicker.js';
|
||||
import '@lion/ui/define/lion-input-email.js';
|
||||
import '@lion/ui/define/lion-input-file.js';
|
||||
import '@lion/ui/define/lion-input-iban.js';
|
||||
import '@lion/ui/define/lion-input-range.js';
|
||||
import '@lion/ui/define/lion-input-stepper.js';
|
||||
import '@lion/ui/define/lion-input-tel-dropdown.js';
|
||||
import '@lion/ui/define/lion-input-tel.js';
|
||||
import '@lion/ui/define/lion-input.js';
|
||||
import '@lion/ui/define/lion-listbox.js';
|
||||
import '@lion/ui/define/lion-option.js';
|
||||
import '@lion/ui/define/lion-combobox.js';
|
||||
import '@lion/ui/define/lion-input-range.js';
|
||||
import '@lion/ui/define/lion-textarea.js';
|
||||
import '@lion/ui/define/lion-button.js';
|
||||
import '@lion/ui/define/lion-radio-group.js';
|
||||
import '@lion/ui/define/lion-radio.js';
|
||||
import '@lion/ui/define/lion-select-rich.js';
|
||||
import '@lion/ui/define/lion-select.js';
|
||||
import '@lion/ui/define/lion-switch.js';
|
||||
import '@lion/ui/define/lion-input-stepper.js';
|
||||
import '@lion/ui/define/lion-textarea.js';
|
||||
import { MinLength, Required } from '@lion/ui/form-core.js';
|
||||
import { html, LitElement } from 'lit';
|
||||
|
||||
export class UmbrellaForm extends LitElement {
|
||||
get _lionFormNode() {
|
||||
|
|
@ -83,6 +85,7 @@ export class UmbrellaForm extends LitElement {
|
|||
<lion-input-amount name="money" label="Money"></lion-input-amount>
|
||||
<lion-input-iban name="iban" label="Iban"></lion-input-iban>
|
||||
<lion-input-email name="email" label="Email"></lion-input-email>
|
||||
<lion-input-file name="file" label="File"></lion-input-file>
|
||||
<lion-input-tel name="tel" label="Telephone Number"></lion-input-tel>
|
||||
<lion-input-tel-dropdown
|
||||
name="tel-dropdown"
|
||||
|
|
@ -151,7 +154,7 @@ export class UmbrellaForm extends LitElement {
|
|||
.validators="${[new Required()]}"
|
||||
>
|
||||
<lion-checkbox
|
||||
.choiceValue="agreed"
|
||||
.choiceValue="${'agreed'}"
|
||||
label="I blindly accept all terms and conditions"
|
||||
></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
|
|
|
|||
98
packages/ui/components/input-file/src/FileHandle.js
Normal file
98
packages/ui/components/input-file/src/FileHandle.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { IsAcceptedFile } from './validators.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../types/input-file.js').SystemFile} SystemFile
|
||||
*/
|
||||
|
||||
// Do these global constants add value? They are only used in this file
|
||||
// Typing filehandle would be enough
|
||||
|
||||
/**
|
||||
* 500MB in bytes
|
||||
*/
|
||||
export const MAX_FILE_SIZE = 524288000;
|
||||
|
||||
/**
|
||||
* @typedef {Object} property
|
||||
* @property {string} type
|
||||
* @property {string} size
|
||||
*/
|
||||
export const FILE_FAILED_PROP = {
|
||||
type: 'FILE_TYPE',
|
||||
size: 'FILE_SIZE',
|
||||
};
|
||||
|
||||
export const UPLOAD_FILE_STATUS = {
|
||||
fail: 'FAIL',
|
||||
pass: 'SUCCESS',
|
||||
};
|
||||
|
||||
export class FileHandle {
|
||||
/**
|
||||
* @param {SystemFile} systemFile
|
||||
* @param {{ allowedFileTypes: Array<string>; allowedFileExtensions: Array<string>; maxFileSize: number; }} _acceptCriteria
|
||||
*/
|
||||
constructor(systemFile, _acceptCriteria) {
|
||||
/**
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
this.failedProp = [];
|
||||
this.systemFile = systemFile;
|
||||
this._acceptCriteria = _acceptCriteria;
|
||||
this.uploadFileStatus();
|
||||
if (this.failedProp.length === 0) {
|
||||
this.createDownloadUrl(systemFile);
|
||||
}
|
||||
}
|
||||
|
||||
// TDOO: same util as in validators.js
|
||||
/**
|
||||
* @param {string} fileName
|
||||
* @return {string}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_getFileNameExtension(fileName) {
|
||||
return fileName.slice(fileName.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
// TODO: seems to suggest upload is going on...
|
||||
// checks the file size and type to set failedProp property
|
||||
uploadFileStatus() {
|
||||
if (this._acceptCriteria.allowedFileExtensions.length) {
|
||||
const fileExtension = this._getFileNameExtension(this.systemFile.name);
|
||||
if (
|
||||
!IsAcceptedFile.isExtensionAllowed(
|
||||
fileExtension,
|
||||
this._acceptCriteria.allowedFileExtensions,
|
||||
)
|
||||
) {
|
||||
this.status = UPLOAD_FILE_STATUS.fail;
|
||||
this.failedProp.push(FILE_FAILED_PROP.type);
|
||||
}
|
||||
} else if (this._acceptCriteria.allowedFileTypes.length) {
|
||||
const fileType = this.systemFile.type;
|
||||
if (!IsAcceptedFile.isFileTypeAllowed(fileType, this._acceptCriteria.allowedFileTypes)) {
|
||||
this.status = UPLOAD_FILE_STATUS.fail;
|
||||
this.failedProp.push(FILE_FAILED_PROP.type);
|
||||
}
|
||||
}
|
||||
|
||||
if (IsAcceptedFile.checkFileSize(this.systemFile.size, this._acceptCriteria.maxFileSize)) {
|
||||
if (this.status !== UPLOAD_FILE_STATUS.fail) {
|
||||
this.status = UPLOAD_FILE_STATUS.pass;
|
||||
}
|
||||
} else {
|
||||
this.status = UPLOAD_FILE_STATUS.fail;
|
||||
this.failedProp.push(FILE_FAILED_PROP.size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SystemFile} file
|
||||
*/
|
||||
createDownloadUrl(file) {
|
||||
// @ts-ignore
|
||||
this.downloadUrl = window.URL.createObjectURL(file);
|
||||
}
|
||||
}
|
||||
892
packages/ui/components/input-file/src/LionInputFile.js
Normal file
892
packages/ui/components/input-file/src/LionInputFile.js
Normal file
|
|
@ -0,0 +1,892 @@
|
|||
import { LionField } from '@lion/ui/form-core.js';
|
||||
import { LocalizeMixin } from '@lion/ui/localize.js';
|
||||
import { ScopedElementsMixin } from '@open-wc/scoped-elements';
|
||||
import { css, html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { FileHandle, MAX_FILE_SIZE } from './FileHandle.js';
|
||||
import { LionSelectedFileList } from './LionSelectedFileList.js';
|
||||
import { localizeNamespaceLoader } from './localizeNamespaceLoader.js';
|
||||
import { DuplicateFileNames, IsAcceptedFile } from './validators.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('lit').TemplateResult} TemplateResult
|
||||
* @typedef {import('lit').RenderOptions} RenderOptions
|
||||
* @typedef {import('../types/input-file.js').InputFile} InputFile
|
||||
* @typedef {import('../types/input-file.js').SystemFile} SystemFile
|
||||
* @typedef {import('../types/input-file.js').FileSelectResponse} FileSelectResponse
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number} bytes
|
||||
* @param {number} decimals
|
||||
*/
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (!+bytes) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = [' bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / k ** i).toFixed(dm))}${sizes[i]}`;
|
||||
}
|
||||
|
||||
export class LionInputFile extends ScopedElementsMixin(LocalizeMixin(LionField)) {
|
||||
static get scopedElements() {
|
||||
return {
|
||||
...super.scopedElements,
|
||||
'lion-selected-file-list': LionSelectedFileList,
|
||||
};
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
accept: { type: String },
|
||||
multiple: { type: Boolean, reflect: true },
|
||||
buttonLabel: { type: String, attribute: 'button-label' },
|
||||
maxFileSize: { type: Number, attribute: 'max-file-size' },
|
||||
enableDropZone: { type: Boolean, attribute: 'enable-drop-zone' },
|
||||
uploadOnSelect: { type: Boolean, attribute: 'upload-on-select' },
|
||||
_fileSelectResponse: { type: Array, state: false },
|
||||
_selectedFilesMetaData: { type: Array, state: true },
|
||||
};
|
||||
}
|
||||
|
||||
static localizeNamespaces = [
|
||||
{ 'lion-input-file': localizeNamespaceLoader },
|
||||
...super.localizeNamespaces,
|
||||
];
|
||||
|
||||
static get validationTypes() {
|
||||
return ['error', 'info'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @configure SlotMixin
|
||||
*/
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
input: () => html`<input .value="${ifDefined(this.getAttribute('value'))}" />`,
|
||||
'file-select-button': () =>
|
||||
html`<button
|
||||
type="button"
|
||||
id="select-button-${this._inputId}"
|
||||
@click="${this.__openDialogOnBtnClick}"
|
||||
>
|
||||
${this.buttonLabel}
|
||||
</button>`,
|
||||
'selected-file-list': () => ({
|
||||
template: html`
|
||||
<lion-selected-file-list
|
||||
.fileList=${this._selectedFilesMetaData}
|
||||
.multiple=${this.multiple}
|
||||
></lion-selected-file-list>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {HTMLInputElement}
|
||||
* @protected
|
||||
*/
|
||||
get _inputNode() {
|
||||
return /** @type {HTMLInputElement} */ (super._inputNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
get _buttonNode() {
|
||||
return this.querySelector(`#select-button-${this._inputId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The helpt text for the input node.
|
||||
* When no light dom defined via [slot=help-text], this value will be used
|
||||
* @type {string}
|
||||
*/
|
||||
get buttonLabel() {
|
||||
return this.__buttonLabel || this._buttonNode?.textContent || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} newValue
|
||||
*/
|
||||
set buttonLabel(newValue) {
|
||||
const oldValue = this.buttonLabel;
|
||||
/** @type {string} */
|
||||
this.__buttonLabel = newValue;
|
||||
this.requestUpdate('buttonLabel', oldValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @configure FocusMixin
|
||||
*/
|
||||
// @ts-ignore
|
||||
get _focusableNode() {
|
||||
return this._buttonNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
// TODO: no need to check anymore? https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get _isDragAndDropSupported() {
|
||||
return 'draggable' in document.createElement('div');
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.type = 'file';
|
||||
/**
|
||||
* @protected
|
||||
* @type {InputFile[]}
|
||||
*/
|
||||
this._selectedFilesMetaData = [];
|
||||
/**
|
||||
* @type {FileSelectResponse[]}
|
||||
*/
|
||||
// TODO: make readonly?
|
||||
this._fileSelectResponse = [];
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this.__initialFileSelectResponse = this._fileSelectResponse;
|
||||
// TODO: public default booleans are always false
|
||||
this.uploadOnSelect = false;
|
||||
this.multiple = false;
|
||||
this.enableDropZone = false;
|
||||
this.maxFileSize = MAX_FILE_SIZE;
|
||||
this.accept = '';
|
||||
/**
|
||||
* @type {InputFile[]}
|
||||
*/
|
||||
this.modelValue = [];
|
||||
/**
|
||||
* @protected
|
||||
* @type {EventListener}
|
||||
*/
|
||||
this._onRemoveFile = this._onRemoveFile.bind(this);
|
||||
|
||||
/** @private */
|
||||
this.__duplicateFileNamesValidator = new DuplicateFileNames({ show: false });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.__initialFileSelectResponse = this._fileSelectResponse;
|
||||
|
||||
this._inputNode.addEventListener('change', this._onChange);
|
||||
this._inputNode.addEventListener('click', this._onClick);
|
||||
this.addEventListener(
|
||||
'file-remove-requested',
|
||||
/** @type {EventListener} */ (this._onRemoveFile),
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._inputNode.removeEventListener('change', this._onChange);
|
||||
this._inputNode.removeEventListener('click', this._onClick);
|
||||
this.removeEventListener(
|
||||
'file-remove-requested',
|
||||
/** @type {EventListener} */ (this._onRemoveFile),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @configure LocalizeMixin
|
||||
*/
|
||||
onLocaleUpdated() {
|
||||
super.onLocaleUpdated();
|
||||
if (this.multiple) {
|
||||
// @ts-ignore
|
||||
this.buttonLabel = this.msgLit('lion-input-file:selectTextMultipleFile');
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.buttonLabel = this.msgLit('lion-input-file:selectTextSingleFile');
|
||||
}
|
||||
}
|
||||
|
||||
get _acceptCriteria() {
|
||||
/** @type {string[]} */
|
||||
let allowedFileTypes = [];
|
||||
/** @type {string[]} */
|
||||
let allowedFileExtensions = [];
|
||||
if (this.accept) {
|
||||
const acceptedFiles = this.accept.replace(/\s+/g, '').replace(/\.+/g, '').split(',');
|
||||
allowedFileTypes = acceptedFiles.filter(acceptedFile => acceptedFile.includes('/'));
|
||||
allowedFileExtensions = acceptedFiles.filter(acceptedFile => !acceptedFile.includes('/'));
|
||||
}
|
||||
return {
|
||||
allowedFileTypes,
|
||||
allowedFileExtensions,
|
||||
maxFileSize: this.maxFileSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @enhance LionField
|
||||
* Resets modelValue to initial value.
|
||||
* Interaction states are cleared
|
||||
*/
|
||||
reset() {
|
||||
super.reset();
|
||||
this._selectedFilesMetaData = [];
|
||||
this._fileSelectResponse = this.__initialFileSelectResponse;
|
||||
this.modelValue = [];
|
||||
// TODO: find out why it stays dirty
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears modelValue.
|
||||
* Interaction states are not cleared (use resetInteractionState for this)
|
||||
* @override LionField
|
||||
*/
|
||||
clear() {
|
||||
this._selectedFilesMetaData = [];
|
||||
this._fileSelectResponse = [];
|
||||
this.modelValue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @override ValidateMixin: override to hide the IsAcceptedFile feedback at component level as they are displayed at each file level in file list
|
||||
* @param {string} type could be 'error', 'warning', 'info', 'success' or any other custom
|
||||
* @param {object} meta meta info (interaction states etc)
|
||||
* @protected
|
||||
*/
|
||||
_showFeedbackConditionFor(type, meta) {
|
||||
return (
|
||||
super._showFeedbackConditionFor(type, meta) &&
|
||||
!(
|
||||
this.validationStates.error?.FileTypeAllowed || this.validationStates.error?.FileSizeAllowed
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @configure FormatMixin
|
||||
* @returns {InputFile[]} parsedValue
|
||||
*/
|
||||
parser() {
|
||||
// @ts-ignore
|
||||
return this._inputNode.files ? Array.from(this._inputNode.files) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @configure FormatMixin
|
||||
* @param {InputFile[]} v - modelValue: File[]
|
||||
* @returns {string} formattedValue
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
formatter(v) {
|
||||
return this._inputNode?.value || '';
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupDragDropEventListeners() {
|
||||
// TODO: this will break as soon as a Subclasser changes the template ... (changing class names is allowed, ids should be kept)
|
||||
const dropZone = this.shadowRoot?.querySelector('.input-file__drop-zone');
|
||||
['dragenter', 'dragover', 'dragleave'].forEach(eventName => {
|
||||
dropZone?.addEventListener(
|
||||
eventName,
|
||||
(/** @type {Event} */ ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (eventName !== 'dragleave') {
|
||||
this.setAttribute('is-dragging', '');
|
||||
} else {
|
||||
this.removeAttribute('is-dragging');
|
||||
}
|
||||
},
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener(
|
||||
'drop',
|
||||
ev => {
|
||||
if (ev.target === this._inputNode) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
this.removeAttribute('is-dragging');
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit').PropertyValues } changedProperties
|
||||
*/
|
||||
firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
|
||||
this.__setupFileValidators();
|
||||
// We need to update our light dom
|
||||
this._enhanceSelectedList();
|
||||
|
||||
// TODO: if there's no inputNode by now, we should either throw an error or add a slot change listener
|
||||
if (this._inputNode) {
|
||||
// TODO: should it be possible to change this on the fly (in updated)?
|
||||
this._inputNode.type = this.type;
|
||||
this._inputNode.setAttribute('tabindex', '-1');
|
||||
// TODO: should it be possible to change this on the fly (in updated)?
|
||||
this._inputNode.multiple = this.multiple;
|
||||
if (this.accept.length) {
|
||||
// TODO: should it be possible to change this on the fly (in updated)?
|
||||
this._inputNode.accept = this.accept;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.enableDropZone && this._isDragAndDropSupported) {
|
||||
this.__setupDragDropEventListeners();
|
||||
this.setAttribute('drop-zone', '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit').PropertyValues } changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
|
||||
// TODO: mention code originates from LionInput, but we could not extend from it bc/o x/y/z
|
||||
if (changedProperties.has('disabled')) {
|
||||
this._inputNode.disabled = this.disabled;
|
||||
this.validate();
|
||||
}
|
||||
|
||||
if (changedProperties.has('buttonLabel') && this._buttonNode) {
|
||||
this._buttonNode.textContent = this.buttonLabel;
|
||||
}
|
||||
|
||||
// TODO: mention code originates from LionInput, but we could not extend from it bc/o x/y/z
|
||||
if (changedProperties.has('name')) {
|
||||
this._inputNode.name = this.name;
|
||||
}
|
||||
|
||||
if (changedProperties.has('_ariaLabelledNodes')) {
|
||||
this.__syncAriaLabelledByAttributesToButton();
|
||||
}
|
||||
|
||||
if (changedProperties.has('_ariaDescribedNodes')) {
|
||||
this.__syncAriaDescribedByAttributesToButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update _selectedFilesMetaData only if:
|
||||
* 1. It is invoked from the file-removed event handler.
|
||||
* 2. There is a mismatch between the selected files and files on UI.
|
||||
*/
|
||||
if (changedProperties.has('_fileSelectResponse')) {
|
||||
if (this._selectedFilesMetaData.length === 0) {
|
||||
this._fileSelectResponse.forEach(preResponse => {
|
||||
const file = {
|
||||
systemFile: {
|
||||
name: preResponse.name,
|
||||
},
|
||||
response: preResponse,
|
||||
status: preResponse.status,
|
||||
validationFeedback: [
|
||||
{
|
||||
message: preResponse.errorMessage,
|
||||
},
|
||||
],
|
||||
};
|
||||
// @ts-ignore
|
||||
this._selectedFilesMetaData = [...this._selectedFilesMetaData, file];
|
||||
});
|
||||
}
|
||||
this._selectedFilesMetaData.forEach(file => {
|
||||
if (
|
||||
!this._fileSelectResponse.some(response => response.name === file.systemFile.name) &&
|
||||
this.uploadOnSelect
|
||||
) {
|
||||
this.__removeFileFromList(file);
|
||||
} else {
|
||||
this._fileSelectResponse.forEach(response => {
|
||||
if (response.name === file.systemFile.name) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
file.response = response;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
file.downloadUrl = response.downloadUrl ? response.downloadUrl : file.downloadUrl;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
file.status = response.status;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
file.validationFeedback = [
|
||||
{
|
||||
type: response.errorMessage?.length > 0 ? 'error' : 'success',
|
||||
message: response.errorMessage,
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
// this._selectedFilesMetaData = [...this._selectedFilesMetaData];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this method also triggers a validator...
|
||||
/**
|
||||
* @private
|
||||
* @param {InputFile[]} fileList
|
||||
*/
|
||||
__computeNewAddedFiles(fileList) {
|
||||
const computedFileList = fileList.filter(
|
||||
file =>
|
||||
this._selectedFilesMetaData.findIndex(
|
||||
existLionFile => existLionFile.systemFile.name === file.name,
|
||||
) === -1,
|
||||
);
|
||||
// TODO: put this logic in the Validator itself. Changing the param should trigger a re-validate
|
||||
this.__duplicateFileNamesValidator.param = {
|
||||
show: fileList.length !== computedFileList.length,
|
||||
};
|
||||
this.validate();
|
||||
|
||||
return computedFileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DragEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_processDroppedFiles(ev) {
|
||||
ev.preventDefault();
|
||||
this.removeAttribute('is-dragging');
|
||||
|
||||
const isDraggingMultipleWhileNotSupported =
|
||||
ev.dataTransfer && ev.dataTransfer.items.length > 1 && !this.multiple;
|
||||
if (isDraggingMultipleWhileNotSupported || !ev.dataTransfer?.files) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._inputNode.files = ev.dataTransfer.files;
|
||||
// @ts-ignore
|
||||
this.modelValue = Array.from(ev.dataTransfer.files);
|
||||
// if same file is selected again, e.dataTransfer.files lists that file.
|
||||
// So filter if the file already exists
|
||||
// @ts-ignore
|
||||
// const newFiles = this.__computeNewAddedFiles(Array.from(ev.dataTransfer.files));
|
||||
// if (newFiles.length > 0) {
|
||||
// this._processFiles(newFiles);
|
||||
// }
|
||||
this._processFiles(Array.from(ev.dataTransfer.files));
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {Event=} ev
|
||||
* @protected
|
||||
*/
|
||||
|
||||
_onChange(ev) {
|
||||
// Here, we take over the responsibility of InteractionStateMixin, as _leaveEvent is not the best trigger in this case.
|
||||
// Giving feedback right after the file dialog is closed results in best UX.
|
||||
this.touched = true;
|
||||
// Here, we connect ourselves to the FormatMixin flow...
|
||||
// TODO: should we call super._onChange(ev) here instead?
|
||||
this._onUserInputChanged();
|
||||
this._processFiles(/** @type {HTMLInputElement & {files:InputFile[]}} */ (ev?.target)?.files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear _inputNode.value to make sure onChange is called even for duplicate files
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_onClick(ev) {
|
||||
// @ts-ignore
|
||||
ev.target.value = ''; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_enhanceSelectedList() {
|
||||
/**
|
||||
* @type {LionSelectedFileList | null}
|
||||
*/
|
||||
const selectedFileList = this.querySelector('[slot="selected-file-list"]');
|
||||
if (selectedFileList) {
|
||||
selectedFileList.setAttribute('id', `selected-file-list-${this._inputId}`);
|
||||
this.addToAriaDescribedBy(selectedFileList, { idPrefix: 'selected-file-list' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
__syncAriaLabelledByAttributesToButton() {
|
||||
if (this._inputNode.hasAttribute('aria-labelledby')) {
|
||||
const ariaLabelledBy = this._inputNode.getAttribute('aria-labelledby');
|
||||
this._buttonNode?.setAttribute(
|
||||
'aria-labelledby',
|
||||
`select-button-${this._inputId} ${ariaLabelledBy}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
__syncAriaDescribedByAttributesToButton() {
|
||||
if (this._inputNode.hasAttribute('aria-describedby')) {
|
||||
const ariaDescribedby = this._inputNode.getAttribute('aria-describedby') || '';
|
||||
this._buttonNode?.setAttribute('aria-describedby', ariaDescribedby);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupFileValidators() {
|
||||
// TODO: update .param when _acceptCriteria changes
|
||||
this.defaultValidators = [
|
||||
new IsAcceptedFile(this._acceptCriteria),
|
||||
this.__duplicateFileNamesValidator,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs on drag or change event
|
||||
*
|
||||
* @param {InputFile[]} selectedFiles
|
||||
* @protected
|
||||
*/
|
||||
_processFiles(selectedFiles) {
|
||||
// file size and type validators are required only when file is selected and not in case of prefill
|
||||
// TODO: is this needed every time?
|
||||
const newFiles = this.__computeNewAddedFiles(Array.from(selectedFiles));
|
||||
if (!this.multiple && newFiles.length > 0) {
|
||||
this._selectedFilesMetaData = [];
|
||||
this._fileSelectResponse = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {InputFile}
|
||||
*/
|
||||
let fileObj;
|
||||
for (const selectedFile of newFiles.values()) {
|
||||
// @ts-ignore
|
||||
fileObj = new FileHandle(selectedFile, this._acceptCriteria);
|
||||
if (fileObj.failedProp?.length) {
|
||||
this._handleErroredFiles(fileObj);
|
||||
this._fileSelectResponse = [
|
||||
...this._fileSelectResponse,
|
||||
{
|
||||
name: fileObj.systemFile.name,
|
||||
status: 'FAIL',
|
||||
// @ts-expect-error
|
||||
errorMessage: fileObj.validationFeedback[0].message,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
this._fileSelectResponse = [
|
||||
...this._fileSelectResponse,
|
||||
{
|
||||
name: fileObj.systemFile.name,
|
||||
status: 'SUCCESS',
|
||||
},
|
||||
];
|
||||
}
|
||||
this._selectedFilesMetaData = [...this._selectedFilesMetaData, fileObj];
|
||||
this._handleErrors();
|
||||
}
|
||||
|
||||
// only send error-free files to file-list-changed event
|
||||
const _successFiles = this._selectedFilesMetaData
|
||||
.filter(
|
||||
({ systemFile, status }) =>
|
||||
newFiles.includes(/** @type {InputFile} */ (systemFile)) && status === 'SUCCESS',
|
||||
)
|
||||
.map(({ systemFile }) => /** @type {InputFile} */ (systemFile));
|
||||
|
||||
if (_successFiles.length > 0) {
|
||||
this._dispatchFileListChangeEvent(_successFiles);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {InputFile[]} newFiles
|
||||
* @protected
|
||||
*/
|
||||
_dispatchFileListChangeEvent(newFiles) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('file-list-changed', {
|
||||
// TODO: check if composed and bubbles are needed
|
||||
// composed: true,
|
||||
// bubbles: true,
|
||||
detail: {
|
||||
newFiles,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_handleErrors() {
|
||||
let hasErrors = false;
|
||||
this._selectedFilesMetaData.forEach(fileObj => {
|
||||
if (fileObj.failedProp && fileObj.failedProp.length > 0) {
|
||||
hasErrors = true;
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: handle via ValidateMixin (otherwise it breaks as soon as private ValidateMixin internals change)
|
||||
if (hasErrors) {
|
||||
this.hasFeedbackFor?.push('error');
|
||||
// @ts-ignore use private property
|
||||
this.shouldShowFeedbackFor.push('error');
|
||||
} else if (this._prevHasErrors && this.hasFeedbackFor.includes('error')) {
|
||||
const hasFeedbackForIndex = this.hasFeedbackFor.indexOf('error');
|
||||
this.hasFeedbackFor.slice(hasFeedbackForIndex, hasFeedbackForIndex + 1);
|
||||
// @ts-ignore use private property
|
||||
const shouldShowFeedbackForIndex = this.shouldShowFeedbackFor.indexOf('error');
|
||||
// @ts-ignore use private property
|
||||
this.shouldShowFeedbackFor.slice(shouldShowFeedbackForIndex, shouldShowFeedbackForIndex + 1);
|
||||
}
|
||||
this._prevHasErrors = hasErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {InputFile} fileObj
|
||||
* @protected
|
||||
*/
|
||||
/* eslint-disable no-param-reassign */
|
||||
_handleErroredFiles(fileObj) {
|
||||
fileObj.validationFeedback = [];
|
||||
const { allowedFileExtensions, allowedFileTypes } = this._acceptCriteria;
|
||||
/**
|
||||
* @type {string[]}
|
||||
*/
|
||||
let array = [];
|
||||
let arrayLength = 0;
|
||||
let lastItem;
|
||||
|
||||
if (allowedFileExtensions.length) {
|
||||
array = allowedFileExtensions;
|
||||
// eslint-disable-next-line no-return-assign
|
||||
array = array.map(item => (item = `.${item}`));
|
||||
lastItem = array.pop();
|
||||
arrayLength = array.length;
|
||||
} else if (allowedFileTypes.length) {
|
||||
allowedFileTypes.forEach(MIMETypes => {
|
||||
if (MIMETypes.endsWith('/*')) {
|
||||
array.push(MIMETypes.slice(0, -2));
|
||||
} else if (MIMETypes === 'text/plain') {
|
||||
array.push('text');
|
||||
} else {
|
||||
const index = MIMETypes.indexOf('/');
|
||||
const subTypes = MIMETypes.slice(index + 1);
|
||||
|
||||
if (!subTypes.includes('+')) {
|
||||
array.push(`.${subTypes}`);
|
||||
} else {
|
||||
const subType = subTypes.split('+');
|
||||
array.push(`.${subType[0]}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
lastItem = array.pop();
|
||||
arrayLength = array.length;
|
||||
}
|
||||
let message = '';
|
||||
if (!lastItem) {
|
||||
message = `${this.msgLit('lion-input-file:allowedFileSize', {
|
||||
maxSize: formatBytes(this.maxFileSize),
|
||||
})}`;
|
||||
} else if (!arrayLength) {
|
||||
message = `${this.msgLit('lion-input-file:allowedFileValidatorSimple', {
|
||||
allowedType: lastItem,
|
||||
maxSize: formatBytes(this.maxFileSize),
|
||||
})}`;
|
||||
} else {
|
||||
message = `${this.msgLit('lion-input-file:allowedFileValidatorComplex', {
|
||||
allowedTypesArray: array.join(', '),
|
||||
allowedTypesLastItem: lastItem,
|
||||
maxSize: formatBytes(this.maxFileSize),
|
||||
})}`;
|
||||
}
|
||||
|
||||
const errorObj = {
|
||||
message,
|
||||
type: 'error',
|
||||
};
|
||||
fileObj.validationFeedback?.push(errorObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {InputFile} removedFile
|
||||
*/
|
||||
__removeFileFromList(removedFile) {
|
||||
this._selectedFilesMetaData = this._selectedFilesMetaData.filter(
|
||||
currentFile => currentFile.systemFile.name !== removedFile.systemFile.name,
|
||||
);
|
||||
// checks if the file is not a pre-filled file
|
||||
if (this.modelValue) {
|
||||
this.modelValue = this.modelValue.filter(
|
||||
(/** @type {InputFile} */ currentFile) => currentFile.name !== removedFile.systemFile.name,
|
||||
);
|
||||
}
|
||||
this._inputNode.value = '';
|
||||
this._handleErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CustomEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_onRemoveFile(ev) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
const { removedFile } = ev.detail;
|
||||
if (!this.uploadOnSelect && removedFile) {
|
||||
this.__removeFileFromList(removedFile);
|
||||
}
|
||||
|
||||
this._removeFile(removedFile);
|
||||
}
|
||||
|
||||
// TODO: this doesn't remove the file from the list, but fires an event
|
||||
/**
|
||||
* @param {InputFile} removedFile
|
||||
* @protected
|
||||
*/
|
||||
_removeFile(removedFile) {
|
||||
this.dispatchEvent(
|
||||
// TODO: check if composed and bubbles are needed
|
||||
new CustomEvent('file-removed', {
|
||||
// bubbles: true,
|
||||
// composed: true,
|
||||
detail: {
|
||||
removedFile,
|
||||
status: removedFile.status,
|
||||
_fileSelectResponse: removedFile.response,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every time .formattedValue is attempted to sync to the view value (on change/blur and on
|
||||
* modelValue change), this condition is checked. In case of the input-file we don't want
|
||||
* this sync to happen, since the view value is already correct.
|
||||
* @override FormatMixin
|
||||
* @return {boolean}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_reflectBackOn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for the mutually exclusive Required Validator
|
||||
* @override ValidateMixin
|
||||
*/
|
||||
_isEmpty() {
|
||||
return this.modelValue?.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
_dropZoneTemplate() {
|
||||
return html`
|
||||
<div @drop="${this._processDroppedFiles}" class="input-file__drop-zone">
|
||||
<div class="input-file__drop-zone__text">
|
||||
${this.msgLit('lion-input-file:dragAndDropText')}
|
||||
</div>
|
||||
<slot name="file-select-button"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override FormControlMixin
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupAfterTemplate() {
|
||||
return html` <slot name="selected-file-list"></slot> `;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override FormControlMixin
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
_inputGroupInputTemplate() {
|
||||
return html`
|
||||
<slot name="input"> </slot>
|
||||
${this.enableDropZone && this._isDragAndDropSupported
|
||||
? this._dropZoneTemplate()
|
||||
: html`
|
||||
<div class="input-group__file-select-button">
|
||||
<slot name="file-select-button"></slot>
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
super.styles,
|
||||
css`
|
||||
.input-group__container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
:host([drop-zone]) .input-group__container {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.input-group__container ::slotted(input[type='file']) {
|
||||
/** Invisible, since means of interaction is button */
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
/** Full cover positioned, so it will be a drag and drop surface */
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.input-file__drop-zone {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: dashed 2px black;
|
||||
padding: 24px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
__openDialogOnBtnClick(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._inputNode.click();
|
||||
}
|
||||
}
|
||||
232
packages/ui/components/input-file/src/LionSelectedFileList.js
Normal file
232
packages/ui/components/input-file/src/LionSelectedFileList.js
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
import { uuid } from '@lion/ui/core.js';
|
||||
import { LionValidationFeedback } from '@lion/ui/form-core.js';
|
||||
import { LocalizeMixin } from '@lion/ui/localize.js';
|
||||
import { ScopedElementsMixin } from '@open-wc/scoped-elements';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { localizeNamespaceLoader } from './localizeNamespaceLoader.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('lit').TemplateResult} TemplateResult
|
||||
* @typedef {import('../types/input-file.js').InputFile} InputFile
|
||||
* @typedef {import('../types/input-file.js').SystemFile} SystemFile
|
||||
*/
|
||||
|
||||
export class LionSelectedFileList extends LocalizeMixin(ScopedElementsMixin(LitElement)) {
|
||||
static get scopedElements() {
|
||||
return {
|
||||
// @ts-expect-error [external] fix types scopedElements
|
||||
...super.scopedElements,
|
||||
'lion-validation-feedback': LionValidationFeedback,
|
||||
};
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
fileList: { type: Array },
|
||||
multiple: { type: Boolean },
|
||||
};
|
||||
}
|
||||
|
||||
static localizeNamespaces = [
|
||||
{ 'lion-input-file': localizeNamespaceLoader },
|
||||
...super.localizeNamespaces,
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* @type {InputFile[]}
|
||||
*/
|
||||
this.fileList = [];
|
||||
this.multiple = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('lit').PropertyValues} changedProperties
|
||||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('fileList')) {
|
||||
this._enhanceLightDomA11y();
|
||||
}
|
||||
}
|
||||
|
||||
_enhanceLightDomA11y() {
|
||||
const fileFeedbackElementList = this.shadowRoot?.querySelectorAll('[id^="file-feedback"]');
|
||||
// TODO: this will break as soon as a Subclasser changes the template ...
|
||||
const inputFileNode = this.parentNode?.parentNode;
|
||||
fileFeedbackElementList?.forEach(feedbackEl => {
|
||||
// Generic focus/blur handling that works for both Fields/FormGroups
|
||||
inputFileNode?.addEventListener('focusin', () => {
|
||||
feedbackEl.setAttribute('aria-live', 'polite');
|
||||
});
|
||||
inputFileNode?.addEventListener('focusout', () => {
|
||||
feedbackEl.setAttribute('aria-live', 'assertive');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {InputFile} removedFile
|
||||
*/
|
||||
_removeFile(removedFile) {
|
||||
this.dispatchEvent(
|
||||
// TODO: do we need bubble and composed for an internal event that only LionInputFile should know about?
|
||||
new CustomEvent('file-remove-requested', {
|
||||
// composed: true,
|
||||
// bubbles: true,
|
||||
detail: {
|
||||
removedFile,
|
||||
status: removedFile.status,
|
||||
_fileSelectResponse: removedFile.response,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {Array<import('../../form-core/types/validate/ValidateMixinTypes.js').FeedbackMessage>} validationFeedback
|
||||
* @param {string} fileUuid
|
||||
* @return {TemplateResult}
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_validationFeedbackTemplate(validationFeedback, fileUuid) {
|
||||
return html`
|
||||
<lion-validation-feedback
|
||||
id="file-feedback-${fileUuid}"
|
||||
.feedbackData="${validationFeedback}"
|
||||
aria-live="assertive"
|
||||
></lion-validation-feedback>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {InputFile} file
|
||||
* @return {TemplateResult|nothing}
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
||||
_listItemBeforeTemplate(file) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {InputFile} file
|
||||
* @param {string} fileUuid
|
||||
* @return {TemplateResult}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_listItemAfterTemplate(file, fileUuid) {
|
||||
return html`
|
||||
<button
|
||||
class="selected__list__item__remove-button"
|
||||
aria-label="${this.msgLit('lion-input-file:removeButtonLabel', {
|
||||
fileName: file.systemFile.name,
|
||||
})}"
|
||||
@click=${() => this._removeFile(file)}
|
||||
>
|
||||
${this._removeButtonContentTemplate()}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @return {TemplateResult}
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_removeButtonContentTemplate() {
|
||||
return html`✖️`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {InputFile} file
|
||||
* @return {TemplateResult}
|
||||
*/
|
||||
_selectedListItemTemplate(file) {
|
||||
const fileUuid = uuid();
|
||||
return html`
|
||||
<div class="selected__list__item" status="${file.status ? file.status.toLowerCase() : ''}">
|
||||
<div class="selected__list__item__label">
|
||||
${this._listItemBeforeTemplate(file)}
|
||||
<span id="selected-list-item-label-${fileUuid}" class="selected__list__item__label__text">
|
||||
<span class="sr-only">${this.msgLit('lion-input-file:fileNameDescriptionLabel')}</span>
|
||||
${file.downloadUrl && file.status !== 'LOADING'
|
||||
? html`
|
||||
<a
|
||||
class="selected__list__item__label__link"
|
||||
href="${file.downloadUrl}"
|
||||
target="${file.downloadUrl.startsWith('blob') ? '_blank' : ''}"
|
||||
rel="${ifDefined(
|
||||
file.downloadUrl.startsWith('blob') ? 'noopener noreferrer' : undefined,
|
||||
)}"
|
||||
>${file.systemFile?.name}</a
|
||||
>
|
||||
`
|
||||
: file.systemFile?.name}
|
||||
</span>
|
||||
${this._listItemAfterTemplate(file, fileUuid)}
|
||||
</div>
|
||||
${file.status === 'FAIL' && file.validationFeedback
|
||||
? html`
|
||||
${repeat(
|
||||
file.validationFeedback,
|
||||
validationFeedback => html`
|
||||
${this._validationFeedbackTemplate([validationFeedback], fileUuid)}
|
||||
`,
|
||||
)}
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.fileList?.length
|
||||
? html`
|
||||
${this.multiple
|
||||
? html`
|
||||
<ul class="selected__list">
|
||||
${this.fileList.map(
|
||||
file => html` <li>${this._selectedListItemTemplate(file)}</li> `,
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: html` ${this._selectedListItemTemplate(this.fileList[0])} `}
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
css`
|
||||
.selected__list {
|
||||
list-style-type: none;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(100%);
|
||||
clip: rect(1px, 1px, 1px, 1px);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
export const localizeNamespaceLoader = /** @param {string} locale */ locale => {
|
||||
switch (locale) {
|
||||
case 'bg-BG':
|
||||
return import('@lion/ui/input-file-translations/bg-BG.js');
|
||||
case 'bg':
|
||||
return import('@lion/ui/input-file-translations/bg.js');
|
||||
case 'cs-CZ':
|
||||
return import('@lion/ui/input-file-translations/cs-CZ.js');
|
||||
case 'cs':
|
||||
return import('@lion/ui/input-file-translations/cs.js');
|
||||
case 'de-DE':
|
||||
return import('@lion/ui/input-file-translations/de-DE.js');
|
||||
case 'de':
|
||||
return import('@lion/ui/input-file-translations/de.js');
|
||||
case 'en-AU':
|
||||
return import('@lion/ui/input-file-translations/en-AU.js');
|
||||
case 'en-GB':
|
||||
return import('@lion/ui/input-file-translations/en-GB.js');
|
||||
case 'en-US':
|
||||
return import('@lion/ui/input-file-translations/en-US.js');
|
||||
case 'en-PH':
|
||||
case 'en':
|
||||
return import('@lion/ui/input-file-translations/en.js');
|
||||
case 'es-ES':
|
||||
return import('@lion/ui/input-file-translations/es-ES.js');
|
||||
case 'es':
|
||||
return import('@lion/ui/input-file-translations/es.js');
|
||||
case 'fr-FR':
|
||||
return import('@lion/ui/input-file-translations/fr-FR.js');
|
||||
case 'fr-BE':
|
||||
return import('@lion/ui/input-file-translations/fr-BE.js');
|
||||
case 'fr':
|
||||
return import('@lion/ui/input-file-translations/fr.js');
|
||||
case 'hu-HU':
|
||||
return import('@lion/ui/input-file-translations/hu-HU.js');
|
||||
case 'hu':
|
||||
return import('@lion/ui/input-file-translations/hu.js');
|
||||
case 'it-IT':
|
||||
return import('@lion/ui/input-file-translations/it-IT.js');
|
||||
case 'it':
|
||||
return import('@lion/ui/input-file-translations/it.js');
|
||||
case 'nl-BE':
|
||||
return import('@lion/ui/input-file-translations/nl-BE.js');
|
||||
case 'nl-NL':
|
||||
return import('@lion/ui/input-file-translations/nl-NL.js');
|
||||
case 'nl':
|
||||
return import('@lion/ui/input-file-translations/nl.js');
|
||||
case 'pl-PL':
|
||||
return import('@lion/ui/input-file-translations/pl-PL.js');
|
||||
case 'pl':
|
||||
return import('@lion/ui/input-file-translations/pl.js');
|
||||
case 'ro-RO':
|
||||
return import('@lion/ui/input-file-translations/ro-RO.js');
|
||||
case 'ro':
|
||||
return import('@lion/ui/input-file-translations/ro.js');
|
||||
case 'ru-RU':
|
||||
return import('@lion/ui/input-file-translations/ru-RU.js');
|
||||
case 'ru':
|
||||
return import('@lion/ui/input-file-translations/ru.js');
|
||||
case 'sk-SK':
|
||||
return import('@lion/ui/input-file-translations/sk-SK.js');
|
||||
case 'sk':
|
||||
return import('@lion/ui/input-file-translations/sk.js');
|
||||
case 'uk-UA':
|
||||
return import('@lion/ui/input-file-translations/uk-UA.js');
|
||||
case 'uk':
|
||||
return import('@lion/ui/input-file-translations/uk.js');
|
||||
case 'zh-CN':
|
||||
case 'zh':
|
||||
return import('@lion/ui/input-file-translations/zh.js');
|
||||
default:
|
||||
return import('@lion/ui/input-file-translations/en.js');
|
||||
}
|
||||
};
|
||||
114
packages/ui/components/input-file/src/validators.js
Normal file
114
packages/ui/components/input-file/src/validators.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { Validator } from '@lion/ui/form-core.js';
|
||||
import { getLocalizeManager } from '@lion/ui/localize-no-side-effects.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../../form-core/types/validate/validate.js').ValidatorConfig} ValidatorConfig
|
||||
* @typedef {import('../../form-core/types/validate/validate.js').ValidatorParam} ValidatorParam
|
||||
* @typedef {import('../../form-core/types/validate/validate.js').ValidatorOutcome} ValidatorOutcome
|
||||
* @typedef {import('../types/input-file.js').InputFile} InputFile
|
||||
*/
|
||||
|
||||
/* eslint max-classes-per-file: ["error", 2] */
|
||||
export class IsAcceptedFile extends Validator {
|
||||
static validatorName = 'IsAcceptedFile';
|
||||
|
||||
/**
|
||||
* @param {number} fileFileSize
|
||||
* @param {number} maxFileSize
|
||||
*/
|
||||
static checkFileSize(fileFileSize, maxFileSize) {
|
||||
return fileFileSize <= maxFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension of a file name
|
||||
* @param {string} fileName like myFile.txt
|
||||
* @return {string} like .txt
|
||||
*/
|
||||
static getExtension(fileName) {
|
||||
return fileName?.slice(fileName.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} extension
|
||||
* @param {string[]} allowedFileExtensions
|
||||
*/
|
||||
static isExtensionAllowed(extension, allowedFileExtensions) {
|
||||
return allowedFileExtensions?.find(
|
||||
allowedExtension => allowedExtension.toUpperCase() === extension.toUpperCase(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @param {string[]} allowedFileTypes
|
||||
*/
|
||||
static isFileTypeAllowed(type, allowedFileTypes) {
|
||||
return allowedFileTypes?.find(allowedType => allowedType.toUpperCase() === type.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<File>} modelValue array of file objects
|
||||
* @param {{ allowedFileTypes: string[]; allowedFileExtensions: string[]; maxFileSize: number; }} params
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
execute(modelValue, params = this.param) {
|
||||
let isInvalidType;
|
||||
let isInvalidFileExt;
|
||||
|
||||
const ctor = /** @type {typeof IsAcceptedFile} */ (this.constructor);
|
||||
const { allowedFileTypes, allowedFileExtensions, maxFileSize } = params;
|
||||
if (allowedFileTypes?.length) {
|
||||
isInvalidType = modelValue.some(file => !ctor.isFileTypeAllowed(file.type, allowedFileTypes));
|
||||
return isInvalidType;
|
||||
}
|
||||
if (allowedFileExtensions?.length) {
|
||||
isInvalidFileExt = modelValue.some(
|
||||
file => !ctor.isExtensionAllowed(ctor.getExtension(file.name), allowedFileExtensions),
|
||||
);
|
||||
return isInvalidFileExt;
|
||||
}
|
||||
const invalidFileSize = modelValue.findIndex(
|
||||
file => !ctor.checkFileSize(file.size, maxFileSize),
|
||||
);
|
||||
return invalidFileSize > -1;
|
||||
}
|
||||
|
||||
// @todo: validation should be handled with ValidateMixin so overriding getMessage to return an empty string should
|
||||
// not be necessary anymore
|
||||
static async getMessage() {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateFileNames extends Validator {
|
||||
static validatorName = 'DuplicateFileNames';
|
||||
|
||||
/**
|
||||
* @param {ValidatorParam} [param]
|
||||
* @param {ValidatorConfig} [config]
|
||||
*/
|
||||
constructor(param, config) {
|
||||
super(param, config);
|
||||
this.type = 'info';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {InputFile[]} modelValue
|
||||
* @param {ValidatorParam} [params]
|
||||
* @returns {ValidatorOutcome|Promise<ValidatorOutcome>}
|
||||
*/
|
||||
execute(modelValue, params = this.param) {
|
||||
return params.show;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string|Element>}
|
||||
*/
|
||||
static async getMessage() {
|
||||
const localizeManager = getLocalizeManager();
|
||||
// TODO: we need to make sure namespace is loaded
|
||||
// TODO: keep Validators localize system agnostic
|
||||
return localizeManager.msg('lion-input-file:uploadTextDuplicateFileName');
|
||||
}
|
||||
}
|
||||
170
packages/ui/components/input-file/test/FileValidators.test.js
Normal file
170
packages/ui/components/input-file/test/FileValidators.test.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { expect } from '@open-wc/testing';
|
||||
import { IsAcceptedFile } from '../src/validators.js';
|
||||
|
||||
describe('lion-input-file: IsAcceptedFile', () => {
|
||||
describe('valid file type', () => {
|
||||
it('should return false for allowed file extension .csv', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ name: 'foo.csv' }], {
|
||||
allowedFileExtensions: ['.csv'],
|
||||
allowedFileTypes: undefined,
|
||||
});
|
||||
expect(returnVal).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false for allowed file extension .csv and .pdf', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ name: 'foo.pdf' }], {
|
||||
allowedFileExtensions: ['.csv', '.pdf'],
|
||||
allowedFileTypes: undefined,
|
||||
});
|
||||
expect(returnVal).to.be.false;
|
||||
});
|
||||
|
||||
it('should return true for not allowed file extension .csv and .pdf', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ name: 'foo.pdf' }], {
|
||||
allowedFileExtensions: ['.txt'],
|
||||
allowedFileTypes: undefined,
|
||||
});
|
||||
expect(returnVal).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false for valid file type for single file', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ type: 'foo' }], {
|
||||
allowedFileTypes: ['foo'],
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false for valid file type for multiple file', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ type: 'foo' }, { type: 'foo' }], {
|
||||
allowedFileTypes: ['foo'],
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false if no file types are defined, and has small file size', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
const returnVal = fileTypeObj.execute(
|
||||
[
|
||||
// @ts-ignore ignore file typing
|
||||
{ type: 'foo', size: 1 },
|
||||
// @ts-ignore ignore file typing
|
||||
{ type: 'foo', size: 1 },
|
||||
],
|
||||
{
|
||||
allowedFileTypes: [],
|
||||
maxFileSize: 1028,
|
||||
},
|
||||
);
|
||||
|
||||
expect(returnVal).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false for valid file type for multiple file and multiple allowed types', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ type: 'foo' }, { type: 'bar' }], {
|
||||
allowedFileTypes: ['foo', 'bar'],
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid file type', () => {
|
||||
it('should return true for invalid file type for single file', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ type: 'bar' }], {
|
||||
allowedFileTypes: ['foo'],
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false for invalid file type for multiple file', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ type: 'foo' }, { type: 'bar' }], {
|
||||
allowedFileTypes: ['foo'],
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false for invalid file type for multiple file and multiple allowed types', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ type: 'foo' }, { type: 'pdf' }], {
|
||||
allowedFileTypes: ['foo', 'bar'],
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true for invalid file type for empty type', async () => {
|
||||
const fileTypeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileTypeObj.execute([{ type: '' }], {
|
||||
allowedFileTypes: ['foo'],
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid file size', () => {
|
||||
it('should return false for valid file size for single file', async () => {
|
||||
const fileSizeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileSizeObj.execute([{ size: 5 }], {
|
||||
maxFileSize: 10,
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false for valid file size for multiple file', async () => {
|
||||
const fileSizeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileSizeObj.execute([{ size: 5 }, { size: 9 }], {
|
||||
maxFileSize: 10,
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid file size', () => {
|
||||
it('should return true for invalid file size for single file', async () => {
|
||||
const fileSizeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileSizeObj.execute([{ size: 5 }], {
|
||||
maxFileSize: 3,
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false for invalid file size for multiple file', async () => {
|
||||
const fileSizeObj = new IsAcceptedFile();
|
||||
// @ts-ignore ignore file typing
|
||||
const returnVal = fileSizeObj.execute([{ size: 5 }, { size: 9 }], {
|
||||
maxFileSize: 7,
|
||||
});
|
||||
|
||||
expect(returnVal).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
1171
packages/ui/components/input-file/test/lion-input-file.test.js
Normal file
1171
packages/ui/components/input-file/test/lion-input-file.test.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,207 @@
|
|||
import '@lion/ui/define/lion-selected-file-list.js';
|
||||
import { expect, fixture as _fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
/**
|
||||
* @typedef {import('../src/LionSelectedFileList.js').LionSelectedFileList} LionSelectedFileList
|
||||
* @typedef {import('../types/input-file.js').InputFile} InputFile
|
||||
* @typedef {import('lit').TemplateResult} TemplateResult
|
||||
*/
|
||||
const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionSelectedFileList>} */ (
|
||||
_fixture
|
||||
);
|
||||
|
||||
describe('lion-selected-file-list', () => {
|
||||
/**
|
||||
* @type {Partial<InputFile>}
|
||||
*/
|
||||
const fileSuccess = {
|
||||
name: 'foo.txt',
|
||||
status: 'SUCCESS',
|
||||
downloadUrl: '#foo',
|
||||
systemFile: {
|
||||
name: 'foo.txt',
|
||||
type: 'text/plain',
|
||||
size: 1000,
|
||||
status: 'SUCCESS',
|
||||
},
|
||||
response: {
|
||||
name: 'foo',
|
||||
type: 'text/plain',
|
||||
size: 1000,
|
||||
status: 'SUCCESS',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {Partial<InputFile>}
|
||||
*/
|
||||
const fileLoading = {
|
||||
status: 'LOADING',
|
||||
systemFile: {
|
||||
name: 'bar.txt',
|
||||
type: 'text/plain',
|
||||
size: 100,
|
||||
status: 'LOADING',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {Partial<InputFile>}
|
||||
*/
|
||||
const fileFail = {
|
||||
systemFile: {
|
||||
name: 'foobar.txt',
|
||||
type: 'text/plain',
|
||||
size: 1000,
|
||||
status: 'FAIL',
|
||||
},
|
||||
status: 'FAIL',
|
||||
validationFeedback: [{ message: 'foobar', type: 'error' }],
|
||||
};
|
||||
|
||||
it('when empty show nothing', async () => {
|
||||
const el = await fixture(html`<lion-selected-file-list></lion-selected-file-list>`);
|
||||
expect(el.children.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('can have 1 item', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-selected-file-list .fileList="${[fileSuccess]}"></lion-selected-file-list>
|
||||
`);
|
||||
const fileItems = el.shadowRoot?.querySelectorAll('.selected__list__item');
|
||||
expect(fileItems?.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('shows a list when multiple', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-selected-file-list
|
||||
.fileList="${[fileSuccess, fileLoading]}"
|
||||
.multiple="${true}"
|
||||
></lion-selected-file-list>
|
||||
`);
|
||||
const fileItems = el.shadowRoot?.querySelectorAll('.selected__list__item');
|
||||
expect(fileItems?.length).to.equal(2);
|
||||
const fileList = el.shadowRoot?.querySelector('.selected__list');
|
||||
expect(fileList?.tagName).to.equal('UL');
|
||||
});
|
||||
|
||||
it('displays an anchor when status="SUCCESS" and a downloadUrl is available', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-selected-file-list
|
||||
.fileList="${[fileSuccess, fileLoading, fileFail]}"
|
||||
.multiple="${true}"
|
||||
></lion-selected-file-list>
|
||||
`);
|
||||
const fileItems = el.shadowRoot?.querySelectorAll('.selected__list__item');
|
||||
// @ts-ignore
|
||||
expect(fileItems[0].querySelector('.selected__list__item__label__link')).to.exist;
|
||||
// @ts-ignore
|
||||
expect(fileItems[1].querySelector('.selected__list__item__label__link')).to.not.exist;
|
||||
// @ts-ignore
|
||||
expect(fileItems[2].querySelector('.selected__list__item__label__link')).to.not.exist;
|
||||
});
|
||||
|
||||
it('displays a feedback message when status="FAIL"', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-selected-file-list .fileList="${[fileFail]}"></lion-selected-file-list>
|
||||
`);
|
||||
const fileItems = el.shadowRoot?.querySelectorAll('.selected__list__item');
|
||||
const feedback = fileItems ? fileItems[0].querySelector('lion-validation-feedback') : undefined;
|
||||
// @ts-ignore
|
||||
expect(feedback).to.exist;
|
||||
expect(feedback).shadowDom.to.equal('foobar');
|
||||
});
|
||||
|
||||
it('should call removeFile method on click of remove button', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-selected-file-list .fileList="${[fileSuccess, fileLoading]}"></lion-selected-file-list>
|
||||
`);
|
||||
|
||||
/**
|
||||
* @type {HTMLButtonElement | null | undefined}
|
||||
*/
|
||||
const removeButton = el.shadowRoot?.querySelector('.selected__list__item__remove-button');
|
||||
const removeFileSpy = sinon.spy(el, /** @type {keyof LionSelectedFileList} */ ('_removeFile'));
|
||||
removeButton?.click();
|
||||
expect(removeFileSpy).have.been.calledOnce;
|
||||
removeFileSpy.restore();
|
||||
});
|
||||
|
||||
it('should fire file-remove-requested event with removed file data', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-selected-file-list .fileList="${[fileSuccess, fileLoading]}"></lion-selected-file-list>
|
||||
`);
|
||||
|
||||
/**
|
||||
* @type {Partial<InputFile>}
|
||||
*/
|
||||
const removedFile = {
|
||||
name: el.fileList[0].name,
|
||||
status: el.fileList[0].status,
|
||||
systemFile: {
|
||||
name: el.fileList[0].name,
|
||||
},
|
||||
response: {
|
||||
name: el.fileList[0].name,
|
||||
status: el.fileList[0].status,
|
||||
},
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
// @ts-ignore ignore file typing
|
||||
el._removeFile(removedFile);
|
||||
});
|
||||
|
||||
const removeFileEvent = await oneEvent(el, 'file-remove-requested');
|
||||
expect(removeFileEvent).to.exist;
|
||||
expect(removeFileEvent.detail.removedFile).to.deep.equal({
|
||||
name: 'foo.txt',
|
||||
status: 'SUCCESS',
|
||||
systemFile: {
|
||||
name: 'foo.txt',
|
||||
},
|
||||
response: {
|
||||
name: 'foo.txt',
|
||||
status: 'SUCCESS',
|
||||
},
|
||||
});
|
||||
expect(removeFileEvent.detail.status).to.deep.equal('SUCCESS');
|
||||
expect(removeFileEvent.detail._fileSelectResponse).to.deep.equal({
|
||||
name: 'foo.txt',
|
||||
status: 'SUCCESS',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', async () => {
|
||||
it('is accessible', async () => {
|
||||
const el = await fixture(
|
||||
html`<lion-selected-file-list
|
||||
.fileList="${[fileSuccess, fileLoading, fileFail]}"
|
||||
></lion-selected-file-list>`,
|
||||
);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it(`adds aria-live="polite" to the feedback slot on focus, aria-live="assertive" to the feedback slot on blur,
|
||||
so error messages appearing on blur will be read before those of the next input`, async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-selected-file-list .fileList="${[fileFail]}"></lion-selected-file-list>
|
||||
`);
|
||||
const fileFeedback = el?.shadowRoot?.querySelectorAll('[id^="file-feedback"]')[0];
|
||||
const removeButton = /** @type {HTMLButtonElement} */ (
|
||||
el.shadowRoot?.querySelector('.selected__list__item__remove-button')
|
||||
);
|
||||
// @ts-ignore
|
||||
expect(fileFeedback.getAttribute('aria-live')).to.equal('assertive');
|
||||
|
||||
removeButton?.focus();
|
||||
// @ts-ignore
|
||||
expect(fileFeedback.getAttribute('aria-live')).to.equal('polite');
|
||||
|
||||
removeButton?.blur();
|
||||
// @ts-ignore
|
||||
expect(fileFeedback.getAttribute('aria-live')).to.equal('assertive');
|
||||
});
|
||||
});
|
||||
});
|
||||
5
packages/ui/components/input-file/translations/bg-BG.js
Normal file
5
packages/ui/components/input-file/translations/bg-BG.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import bg from './bg.js';
|
||||
|
||||
export default {
|
||||
...bg,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/bg.js
Normal file
12
packages/ui/components/input-file/translations/bg.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Моля, качете файл с макс. размер {maxSize}.',
|
||||
allowedFileValidatorSimple: 'Моля, качете файл от тип {allowedType} с макс. размер {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Моля, качете файл от тип {allowedTypesArray} или {allowedTypesLastItem} с макс. размер {maxSize}.',
|
||||
dragAndDropText: 'Плъзнете и пуснете Вашите файлове тук или',
|
||||
fileNameDescriptionLabel: 'Име на файл: {fileName}',
|
||||
removeButtonLabel: 'Отстраняване на файла {fileName}',
|
||||
selectTextDuplicateFileName: 'Файл със същото име на файл вече е налице.',
|
||||
selectTextMultipleFile: 'Избор на файлове',
|
||||
selectTextSingleFile: 'Избор на файл',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/cs-CZ.js
Normal file
5
packages/ui/components/input-file/translations/cs-CZ.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import cs from './cs.js';
|
||||
|
||||
export default {
|
||||
...cs,
|
||||
};
|
||||
13
packages/ui/components/input-file/translations/cs.js
Normal file
13
packages/ui/components/input-file/translations/cs.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export default {
|
||||
allowedFileSize: 'Nahrajte soubor s max. velikostí {maxSize}.',
|
||||
allowedFileValidatorSimple:
|
||||
'Nahrajte soubor typu {allowedTypesLastItem} s max. velikostí {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Nahrajte soubor typu {allowedTypesArray} nebo {allowedTypesLastItem} s max. velikostí {maxSize}.',
|
||||
dragAndDropText: 'Přetáhněte soubory sem nebo',
|
||||
fileNameDescriptionLabel: 'Název souboru: {fileName}',
|
||||
removeButtonLabel: 'Odebrat soubor {fileName}',
|
||||
selectTextDuplicateFileName: 'Soubor se stejným názvem byl již přítomen.',
|
||||
selectTextMultipleFile: 'Vybrat soubory',
|
||||
selectTextSingleFile: 'Vybrat soubor',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/de-DE.js
Normal file
5
packages/ui/components/input-file/translations/de-DE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import de from './de.js';
|
||||
|
||||
export default {
|
||||
...de,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/de.js
Normal file
12
packages/ui/components/input-file/translations/de.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Laden Sie eine Datei mit max. {maxSize} hoch.',
|
||||
allowedFileValidatorSimple: 'Laden Sie eine {allowedType}-Datei mit max. {maxSize} hoch.',
|
||||
allowedFileValidatorComplex:
|
||||
'Laden Sie eine {allowedTypesArray} oder {allowedTypesLastItem}-Datei mit max. {maxSize} hoch.',
|
||||
dragAndDropText: 'Ziehen Sie Ihre Dateien per Drag & Drop hierher oder',
|
||||
fileNameDescriptionLabel: 'Dateiname: {fileName}',
|
||||
removeButtonLabel: 'Datei {fileName} entfernen',
|
||||
selectTextDuplicateFileName: 'Eine Datei mit demselben Dateinamen war bereits vorhanden.',
|
||||
selectTextMultipleFile: 'Dateien auswählen',
|
||||
selectTextSingleFile: 'Datei auswählen',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/en-AU.js
Normal file
5
packages/ui/components/input-file/translations/en-AU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import en from './en.js';
|
||||
|
||||
export default {
|
||||
...en,
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/en-GB.js
Normal file
5
packages/ui/components/input-file/translations/en-GB.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import en from './en.js';
|
||||
|
||||
export default {
|
||||
...en,
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/en-PH.js
Normal file
5
packages/ui/components/input-file/translations/en-PH.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import en from './en.js';
|
||||
|
||||
export default {
|
||||
...en,
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/en-US.js
Normal file
5
packages/ui/components/input-file/translations/en-US.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import en from './en.js';
|
||||
|
||||
export default {
|
||||
...en,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/en.js
Normal file
12
packages/ui/components/input-file/translations/en.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Please select a file with max {maxSize}.',
|
||||
allowedFileValidatorSimple: 'Please select a(n) {allowedType} file with max {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Please select a {allowedTypesArray} or {allowedTypesLastItem} file with max {maxSize}.',
|
||||
dragAndDropText: 'Drag & Drop your files here or', // TODO: or what? Why is Drag & Drop capitalized?
|
||||
fileNameDescriptionLabel: 'File name: {fileName}',
|
||||
removeButtonLabel: 'Remove {fileName} file',
|
||||
selectTextDuplicateFileName: 'A file with same filename was already present.',
|
||||
selectTextMultipleFile: 'Select files',
|
||||
selectTextSingleFile: 'Select file',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/es-ES.js
Normal file
5
packages/ui/components/input-file/translations/es-ES.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import es from './es.js';
|
||||
|
||||
export default {
|
||||
...es,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/es.js
Normal file
12
packages/ui/components/input-file/translations/es.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Cargue un archivo de {maxSize} como máximo.',
|
||||
allowedFileValidatorSimple: 'Cargue un archivo {allowedType} de {maxSize} como máximo.',
|
||||
allowedFileValidatorComplex:
|
||||
'Cargue un archivo {allowedTypesArray} o {allowedTypesLastItem} de {maxSize} como máximo.',
|
||||
dragAndDropText: 'Arrastre y suelte los archivos aquí o',
|
||||
fileNameDescriptionLabel: 'Nombre de archivo: {fileName}',
|
||||
removeButtonLabel: 'Elimine el archivo: {fileName}',
|
||||
selectTextDuplicateFileName: 'Ya había un archivo con el mismo nombre de archivo.',
|
||||
selectTextMultipleFile: 'Seleccionar archivos',
|
||||
selectTextSingleFile: 'Seleccionar archivo',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/fr-BE.js
Normal file
5
packages/ui/components/input-file/translations/fr-BE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import fr from './fr.js';
|
||||
|
||||
export default {
|
||||
...fr,
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/fr-FR.js
Normal file
5
packages/ui/components/input-file/translations/fr-FR.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import fr from './fr.js';
|
||||
|
||||
export default {
|
||||
...fr,
|
||||
};
|
||||
13
packages/ui/components/input-file/translations/fr.js
Normal file
13
packages/ui/components/input-file/translations/fr.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export default {
|
||||
allowedFileSize: 'Veuillez télécharger un fichier avec une taille maximale de {maxSize}.',
|
||||
allowedFileValidatorSimple:
|
||||
'Veuillez télécharger un fichier {allowedType} avec une taille maximale de {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Veuillez télécharger un fichier {allowedTypesArray} ou {allowedTypesLastItem} avec une taille maximale de {maxSize}.',
|
||||
dragAndDropText: 'Glissez et déposez vos fichiers ici ou',
|
||||
fileNameDescriptionLabel: 'Nom de fichier: {fileName}',
|
||||
removeButtonLabel: 'Supprimer le fichier {fileName}',
|
||||
selectTextDuplicateFileName: 'Un fichier portant le même nom de fichier était déjà présent.',
|
||||
selectTextMultipleFile: 'Sélectionnez des fichiers',
|
||||
selectTextSingleFile: 'Sélectionnez un fichier',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/hu-HU.js
Normal file
5
packages/ui/components/input-file/translations/hu-HU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import hu from './hu.js';
|
||||
|
||||
export default {
|
||||
...hu,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/hu.js
Normal file
12
packages/ui/components/input-file/translations/hu.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Töltsön fel egy legfeljebb {maxSize} méretű fájlt.',
|
||||
allowedFileValidatorSimple: 'Töltsön fel egy legfeljebb {maxSize} méretű {allowedType} fájlt.',
|
||||
allowedFileValidatorComplex:
|
||||
'Töltsön fel egy legfeljebb {maxSize} méretű {allowedTypesArray} vagy {allowedTypesLastItem} fájlt.',
|
||||
dragAndDropText: 'Húzza át a fájlokat ide vagy',
|
||||
fileNameDescriptionLabel: 'Fájlnév: {fileName}',
|
||||
removeButtonLabel: 'A(z) {fileName} fájl eltávolítása',
|
||||
selectTextDuplicateFileName: 'Már volt ilyen nevű fájl.',
|
||||
selectTextMultipleFile: 'Fájl(ok) kiválasztása',
|
||||
selectTextSingleFile: 'Fájl kiválasztása',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/it-IT.js
Normal file
5
packages/ui/components/input-file/translations/it-IT.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import it from './it.js';
|
||||
|
||||
export default {
|
||||
...it,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/it.js
Normal file
12
packages/ui/components/input-file/translations/it.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Caricare un file di {maxSize} max.',
|
||||
allowedFileValidatorSimple: 'Caricare un file {allowedType} di {maxSize} max.',
|
||||
allowedFileValidatorComplex:
|
||||
'Caricare un file {allowedTypesArray} o {allowedTypesLastItem} di {maxSize} max.',
|
||||
dragAndDropText: 'Trascinare i file qui o',
|
||||
fileNameDescriptionLabel: 'Nome file: {fileName}',
|
||||
removeButtonLabel: 'Rimuovere il file {fileName}',
|
||||
selectTextDuplicateFileName: 'Un file con lo stesso nome file era già presente.',
|
||||
selectTextMultipleFile: 'Seleziona file',
|
||||
selectTextSingleFile: 'Seleziona file',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/nl-BE.js
Normal file
5
packages/ui/components/input-file/translations/nl-BE.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import nl from './nl.js';
|
||||
|
||||
export default {
|
||||
...nl,
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/nl-NL.js
Normal file
5
packages/ui/components/input-file/translations/nl-NL.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import nl from './nl.js';
|
||||
|
||||
export default {
|
||||
...nl,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/nl.js
Normal file
12
packages/ui/components/input-file/translations/nl.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Upload een bestand van maximaal {maxSize}.',
|
||||
allowedFileValidatorSimple: 'Upload een {allowedType}-bestand van maximaal {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Upload een {allowedTypesArray} of {allowedTypesLastItem}-bestand van maximaal {maxSize}.',
|
||||
dragAndDropText: 'Sleep uw bestanden hierheen of',
|
||||
fileNameDescriptionLabel: 'Bestandsnaam: {fileName}',
|
||||
removeButtonLabel: 'Verwijder het bestand {fileName}',
|
||||
selectTextDuplicateFileName: 'Er bestaat al een bestand met dezelfde bestandsnaam.',
|
||||
selectTextMultipleFile: 'Selecteer bestanden',
|
||||
selectTextSingleFile: 'Selecteer bestand',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/pl-PL.js
Normal file
5
packages/ui/components/input-file/translations/pl-PL.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import pl from './pl.js';
|
||||
|
||||
export default {
|
||||
...pl,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/pl.js
Normal file
12
packages/ui/components/input-file/translations/pl.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Prześlij plik o maks. rozmiarze {maxSize}.',
|
||||
allowedFileValidatorSimple: 'Prześlij plik {allowedType} o maks. rozmiarze {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Prześlij plik {allowedTypesArray} lub {allowedTypesLastItem} o maks. rozmiarze {maxSize}.',
|
||||
dragAndDropText: 'Przeciągnij i upuść pliki tutaj lub',
|
||||
fileNameDescriptionLabel: 'Nazwa pliku: {fileName}',
|
||||
removeButtonLabel: 'Usuń plik {fileName}',
|
||||
selectTextDuplicateFileName: 'Plik o tej samej nazwie już istnieje.',
|
||||
selectTextMultipleFile: 'Wybierz pliki',
|
||||
selectTextSingleFile: 'Wybierz plik',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/ro-RO.js
Normal file
5
packages/ui/components/input-file/translations/ro-RO.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import ro from './ro.js';
|
||||
|
||||
export default {
|
||||
...ro,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/ro.js
Normal file
12
packages/ui/components/input-file/translations/ro.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Încărcaţi un fişier de max. {maxSize}.',
|
||||
allowedFileValidatorSimple: 'Încărcaţi un fişier {allowedType} de max. {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Încărcaţi un fişier {allowedTypesArray} sau {allowedTypesLastItem} de max. {maxSize}.',
|
||||
dragAndDropText: 'Glisaţi şi fixaţi fişierele aici sau',
|
||||
fileNameDescriptionLabel: 'Nume fişier: {fileName}',
|
||||
removeButtonLabel: 'Eliminaţi fişierul {filename}',
|
||||
selectTextDuplicateFileName: 'Există deja un fişier cu acelaşi nume de fişier.',
|
||||
selectTextMultipleFile: 'Selectare fișiere',
|
||||
selectTextSingleFile: 'Selectare fișier',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/ru-RU.js
Normal file
5
packages/ui/components/input-file/translations/ru-RU.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import ru from './ru.js';
|
||||
|
||||
export default {
|
||||
...ru,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/ru.js
Normal file
12
packages/ui/components/input-file/translations/ru.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Загрузите файл размером не более {maxSize}.',
|
||||
allowedFileValidatorSimple: 'Загрузите файл {allowedType} размером не более {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Загрузите файл {allowedTypesArray} или {allowedTypesLastItem} размером не более {maxSize}.',
|
||||
dragAndDropText: 'Перетащите файлы сюда или',
|
||||
fileNameDescriptionLabel: 'Название файла: {fileName}',
|
||||
removeButtonLabel: 'Удалить файл {fileName}',
|
||||
selectTextDuplicateFileName: 'Файл с таким названием уже существует.',
|
||||
selectTextMultipleFile: 'Выберите файлы',
|
||||
selectTextSingleFile: 'Выберите файл',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/sk-SK.js
Normal file
5
packages/ui/components/input-file/translations/sk-SK.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import sk from './sk.js';
|
||||
|
||||
export default {
|
||||
...sk,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/sk.js
Normal file
12
packages/ui/components/input-file/translations/sk.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Nahrajte súbor s maximálnou veľkosťou {maxSize}.',
|
||||
allowedFileValidatorSimple: 'Nahrajte súbor {allowedType} s maximálnou veľkosťou {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Nahrajte súbor {allowedTypesArray} alebo {allowedTypesLastItem} s maximálnou veľkosťou {maxSize}.',
|
||||
dragAndDropText: 'Súbory presuňte sem alebo',
|
||||
fileNameDescriptionLabel: 'Názov súboru: {fileName}',
|
||||
removeButtonLabel: 'Odstrániť súbor {fileName}',
|
||||
selectTextDuplicateFileName: 'Súbor s rovnakým názvom súboru už existoval.',
|
||||
selectTextMultipleFile: 'Vybrať súbory',
|
||||
selectTextSingleFile: 'Vybrať súbor',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/uk-UA.js
Normal file
5
packages/ui/components/input-file/translations/uk-UA.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import uk from './uk.js';
|
||||
|
||||
export default {
|
||||
...uk,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/uk.js
Normal file
12
packages/ui/components/input-file/translations/uk.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: 'Завантажте файл розміром до {maxSize}.',
|
||||
allowedFileValidatorSimple: 'Завантажте файл {allowedType} розміром до {maxSize}.',
|
||||
allowedFileValidatorComplex:
|
||||
'Завантажте файл {allowedTypesArray} або {allowedTypesLastItem} розміром до {maxSize}.',
|
||||
dragAndDropText: 'Перетягніть файли сюди або',
|
||||
fileNameDescriptionLabel: 'Ім’я файлу: {fileName}',
|
||||
removeButtonLabel: 'Видалення файлу {fileName}',
|
||||
selectTextDuplicateFileName: 'Файл із такою назвою вже існував.',
|
||||
selectTextMultipleFile: 'Виберіть файли',
|
||||
selectTextSingleFile: 'Виберіть файл',
|
||||
};
|
||||
5
packages/ui/components/input-file/translations/zh-CN.js
Normal file
5
packages/ui/components/input-file/translations/zh-CN.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import zh from './zh.js';
|
||||
|
||||
export default {
|
||||
...zh,
|
||||
};
|
||||
12
packages/ui/components/input-file/translations/zh.js
Normal file
12
packages/ui/components/input-file/translations/zh.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
allowedFileSize: '请上传最大 {maxSize} 的文件。',
|
||||
allowedFileValidatorSimple: '请上传最大 {maxSize} 的 {allowedType} 文件。',
|
||||
allowedFileValidatorComplex:
|
||||
'请上传最大 {maxSize} 的 {allowedTypesArray} 或 {allowedTypesLastItem} 文件。',
|
||||
dragAndDropText: '将您的文件拖放到此处,或',
|
||||
fileNameDescriptionLabel: '文件名: {fileName}',
|
||||
removeButtonLabel: '删除 {fileName} 文件',
|
||||
selectTextDuplicateFileName: '已存在具有相同文件名的文件。',
|
||||
selectTextMultipleFile: '选择多个文件',
|
||||
selectTextSingleFile: '选择文件',
|
||||
};
|
||||
32
packages/ui/components/input-file/types/input-file.d.ts
vendored
Normal file
32
packages/ui/components/input-file/types/input-file.d.ts
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { FeedbackMessage } from "../../form-core/types/validate/ValidateMixinTypes.js";
|
||||
|
||||
type FileBasics = {
|
||||
name: string;
|
||||
/** size in bytes */
|
||||
size: number;
|
||||
type: string;
|
||||
status?: 'SUCCESS' | 'FAIL' | 'LOADING';
|
||||
};
|
||||
|
||||
export type InputFile = {
|
||||
downloadUrl?: string;
|
||||
errorMessage?: FeedbackMessage.message;
|
||||
failedProp?: Array<string|number>;
|
||||
response?: FileSelectResponse;
|
||||
systemFile: Partial<SystemFile>;
|
||||
validationFeedback?: Array<FeedbackMessage>;
|
||||
} & FileBasics & Partial<File>;
|
||||
|
||||
export type SystemFile = {
|
||||
downloadUrl?: string;
|
||||
errorMessage?: FeedbackMessage.message;
|
||||
failedProp?: Array<string|number>;
|
||||
response?: FileSelectResponse;
|
||||
} & FileBasics & Partial<File>;
|
||||
|
||||
|
||||
export type FileSelectResponse = {
|
||||
downloadUrl?: string;
|
||||
errorMessage?: FeedbackMessage.message;
|
||||
id?: string;
|
||||
} & Partial<FileBasics>;
|
||||
3
packages/ui/exports/define/lion-input-file.js
Normal file
3
packages/ui/exports/define/lion-input-file.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionInputFile } from '../input-file.js';
|
||||
|
||||
customElements.define('lion-input-file', LionInputFile);
|
||||
3
packages/ui/exports/define/lion-selected-file-list.js
Normal file
3
packages/ui/exports/define/lion-selected-file-list.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionSelectedFileList } from '../input-file.js';
|
||||
|
||||
customElements.define('lion-selected-file-list', LionSelectedFileList);
|
||||
3
packages/ui/exports/input-file.js
Normal file
3
packages/ui/exports/input-file.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { LionInputFile } from '../components/input-file/src/LionInputFile.js';
|
||||
export { LionSelectedFileList } from '../components/input-file/src/LionSelectedFileList.js';
|
||||
export { IsAcceptedFile, DuplicateFileNames } from '../components/input-file/src/validators.js';
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
"./pagination-translations/*": "./components/pagination/translations/*",
|
||||
"./progress-indicator-translations/*": "./components/progress-indicator/translations/*",
|
||||
"./input-datepicker-translations/*": "./components/input-datepicker/translations/*",
|
||||
"./input-file-translations/*": "./components/input-file/translations/*",
|
||||
"./input-iban-translations/*": "./components/input-iban/translations/*",
|
||||
"./input-tel-translations/*": "./components/input-tel/translations/*",
|
||||
"./overlays-translations/*": "./components/overlays/translations/*",
|
||||
|
|
|
|||
Loading…
Reference in a new issue