90afdafbd9
You can now generate links to join chatrooms with your current user. This can be used to create Docks in OBS for example. This could also be used to generate authentication token to join the chat from 3rd party tools.
741 lines
24 KiB
TypeScript
741 lines
24 KiB
TypeScript
// SPDX-FileCopyrightText: 2024 Mehdi Benadel <https://mehdibenadel.com>
|
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import type { TagsInputElement } from './tags-input'
|
|
import type { DirectiveResult } from 'lit/directive'
|
|
import { ValidationErrorType } from '../models/validation'
|
|
import { maxSize, inputFileAccept } from 'shared/lib/emojis'
|
|
import { html, nothing, TemplateResult } from 'lit'
|
|
import { repeat } from 'lit/directives/repeat.js'
|
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
import { ifDefined } from 'lit/directives/if-defined.js'
|
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
|
|
import { classMap } from 'lit/directives/class-map.js'
|
|
import { LivechatElement } from './livechat'
|
|
import { ptTr } from '../directives/translation'
|
|
import { AddSVG, RemoveSVG } from '../buttons'
|
|
|
|
type DynamicTableAcceptedTypes = number | string | boolean | Date | Array<number | string>
|
|
|
|
type DynamicTableAcceptedInputTypes = 'textarea'
|
|
| 'select'
|
|
| 'checkbox'
|
|
| 'range'
|
|
| 'color'
|
|
| 'date'
|
|
| 'datetime'
|
|
| 'datetime-local'
|
|
| 'email'
|
|
| 'file'
|
|
| 'image'
|
|
| 'month'
|
|
| 'number'
|
|
| 'password'
|
|
| 'tel'
|
|
| 'text'
|
|
| 'time'
|
|
| 'url'
|
|
| 'week'
|
|
| 'tags'
|
|
| 'image-file'
|
|
|
|
interface CellDataSchema {
|
|
min?: number
|
|
max?: number
|
|
minlength?: number
|
|
maxlength?: number
|
|
size?: number
|
|
label?: TemplateResult | string
|
|
options?: { [key: string]: string }
|
|
datalist?: DynamicTableAcceptedTypes[]
|
|
separator?: string
|
|
inputType?: DynamicTableAcceptedInputTypes
|
|
default?: DynamicTableAcceptedTypes
|
|
colClassList?: string[] // CSS classes to add to the <td> element.
|
|
}
|
|
|
|
interface DynamicTableRowData {
|
|
_id: number
|
|
_originalIndex: number
|
|
row: { [key: string]: DynamicTableAcceptedTypes }
|
|
}
|
|
|
|
interface DynamicFormHeaderCellData {
|
|
colName: TemplateResult | DirectiveResult
|
|
description: TemplateResult | DirectiveResult
|
|
headerClassList?: string[]
|
|
}
|
|
|
|
export interface DynamicFormHeader {
|
|
[key: string]: DynamicFormHeaderCellData
|
|
}
|
|
export interface DynamicFormSchema { [key: string]: CellDataSchema }
|
|
|
|
@customElement('livechat-dynamic-table-form')
|
|
export class DynamicTableFormElement extends LivechatElement {
|
|
@property({ attribute: false })
|
|
public header: DynamicFormHeader = {}
|
|
|
|
@property({ attribute: false })
|
|
public schema: DynamicFormSchema = {}
|
|
|
|
@property({ attribute: false })
|
|
public maxLines?: number = undefined
|
|
|
|
@property()
|
|
public validation?: {[key: string]: ValidationErrorType[] }
|
|
|
|
@property({ attribute: false })
|
|
public validationPrefix: string = ''
|
|
|
|
@property({ attribute: false })
|
|
public rows: Array<{ [key: string]: DynamicTableAcceptedTypes }> = []
|
|
|
|
@state()
|
|
public _rowsById: DynamicTableRowData[] = []
|
|
|
|
@property({ attribute: false })
|
|
public formName: string = ''
|
|
|
|
@state()
|
|
private _lastRowId = 1
|
|
|
|
@property({ attribute: false })
|
|
private columnOrder: string[] = []
|
|
|
|
// fixes situations when list has been reinitialized or changed outside of CustomElement
|
|
private readonly _updateLastRowId = (): void => {
|
|
for (const rowById of this._rowsById) {
|
|
this._lastRowId = Math.max(this._lastRowId, rowById._id + 1)
|
|
}
|
|
}
|
|
|
|
private readonly _getDefaultRow = (): { [key: string]: DynamicTableAcceptedTypes } => {
|
|
this._updateLastRowId()
|
|
return Object.fromEntries([...Object.entries(this.schema).map((entry) => [entry[0], entry[1].default ?? ''])])
|
|
}
|
|
|
|
private async _addRow (): Promise<void> {
|
|
const newRow = this._getDefaultRow()
|
|
// Create row and assign id and original index
|
|
this._rowsById.push({ _id: this._lastRowId++, _originalIndex: this.rows.length, row: newRow })
|
|
this.rows.push(newRow)
|
|
this.requestUpdate('rows')
|
|
this.requestUpdate('_rowsById')
|
|
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
|
|
|
// Once the update is completed, we give focus to the first input field of the new row.
|
|
await this.updateComplete
|
|
// Note: we make multiple querySelector, to be sure to not get a nested table.
|
|
// We want the top level table associated tr.
|
|
const input = this.querySelector('table')?.querySelector(
|
|
'&>tbody>tr:last-child>td input:not([type=hidden]),' +
|
|
'&>tbody>tr:last-child>td livechat-tags-input,' +
|
|
'&>tbody>tr:last-child>td textarea'
|
|
)
|
|
if (input) {
|
|
(input as HTMLElement).focus()
|
|
}
|
|
}
|
|
|
|
private async _removeRow (rowId: number): Promise<void> {
|
|
const confirmMsg = await this.ptTranslate(LOC_ACTION_REMOVE_ENTRY_CONFIRM)
|
|
await new Promise<void>((resolve, reject) => {
|
|
this.ptOptions.peertubeHelpers.showModal({
|
|
title: confirmMsg,
|
|
content: '',
|
|
close: true,
|
|
cancel: {
|
|
value: 'cancel',
|
|
action: reject
|
|
},
|
|
confirm: {
|
|
value: 'confirm',
|
|
action: resolve
|
|
}
|
|
})
|
|
})
|
|
const rowToRemove = this._rowsById.filter(rowById => rowById._id === rowId).map(rowById => rowById.row)[0]
|
|
this._rowsById = this._rowsById.filter(rowById => rowById._id !== rowId)
|
|
this.rows = this.rows.filter((row) => row !== rowToRemove)
|
|
this.requestUpdate('rows')
|
|
this.requestUpdate('_rowsById')
|
|
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
|
}
|
|
|
|
protected override render = (): unknown => {
|
|
const inputId = `peertube-livechat-${this.formName.replace(/_/g, '-')}-table`
|
|
|
|
this._updateLastRowId()
|
|
|
|
// Filter removed rows
|
|
// FIXME: is this really necessary?
|
|
this._rowsById = this._rowsById.filter(rowById => this.rows.includes(rowById.row))
|
|
|
|
for (let i = 0; i < this.rows.length; i++) {
|
|
if (!this._rowsById.find(rowById => rowById.row === this.rows[i])) {
|
|
// Add row and assign id
|
|
this._rowsById.push({ _id: this._lastRowId++, _originalIndex: i, row: this.rows[i] })
|
|
} else {
|
|
// Update index in case it changed
|
|
this._rowsById.filter(rowById => rowById.row === this.rows[i])
|
|
.forEach((value) => { value._originalIndex = i })
|
|
}
|
|
}
|
|
|
|
if (this.columnOrder.length !== Object.keys(this.header).length) {
|
|
this.columnOrder = this.columnOrder.filter(key => Object.keys(this.header).includes(key))
|
|
this.columnOrder.push(...Object.keys(this.header).filter(key => !this.columnOrder.includes(key)))
|
|
}
|
|
|
|
return html`
|
|
<div class="table-responsive">
|
|
<table class="table" id=${inputId}>
|
|
${this._renderHeader()}
|
|
<tbody>
|
|
${repeat(this._rowsById, (rowById) => rowById._id, this._renderDataRow)}
|
|
</tbody>
|
|
${this._renderFooter()}
|
|
</table>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
private readonly _renderHeader = (): TemplateResult => {
|
|
const columns = Object.entries(this.header)
|
|
.sort(([k1, _1], [k2, _2]) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
|
|
return html`<thead>
|
|
<tr>
|
|
${columns.map(([_, v]) => this._renderHeaderCell(v))}
|
|
<th scope="col"></th>
|
|
</tr>
|
|
<tr>
|
|
${columns.map(([_, v]) => this._renderHeaderDescriptionCell(v))}
|
|
<th scope="col"></th>
|
|
</tr>
|
|
</thead>`
|
|
}
|
|
|
|
private readonly _renderHeaderCell = (headerCellData: DynamicFormHeaderCellData): TemplateResult => {
|
|
return html`<th scope="col" class=${headerCellData.headerClassList?.join(' ') ?? ''}>
|
|
<div
|
|
data-toggle="tooltip"
|
|
data-placement="bottom"
|
|
data-html="true"
|
|
>
|
|
${headerCellData.colName}
|
|
</div>
|
|
</th>`
|
|
}
|
|
|
|
private _renderHeaderDescriptionCell (headerCellData: DynamicFormHeaderCellData): TemplateResult {
|
|
const classList = ['livechat-dynamic-table-form-description-header']
|
|
if (headerCellData.headerClassList) {
|
|
classList.push(...headerCellData.headerClassList)
|
|
}
|
|
return html`<th scope="col" class=${classList.join(' ')}>
|
|
${headerCellData.description}
|
|
</th>`
|
|
}
|
|
|
|
private readonly _renderDataRow = (rowData: DynamicTableRowData): TemplateResult => {
|
|
const inputId = `peertube-livechat-${this.formName.replace(/_/g, '-')}-row-${rowData._id}`
|
|
|
|
return html`<tr id=${inputId}>
|
|
${Object.keys(this.header)
|
|
.sort((k1, k2) => this.columnOrder.indexOf(k1) - this.columnOrder.indexOf(k2))
|
|
.map(key => this.renderDataCell(key,
|
|
rowData.row[key] ?? this.schema[key].default,
|
|
rowData._id,
|
|
rowData._originalIndex))}
|
|
<td class="form-group">
|
|
<button type="button"
|
|
class="dynamic-table-remove-row"
|
|
.title=${ptTr(LOC_ACTION_REMOVE_ENTRY) as any}
|
|
@click=${async () => this._removeRow(rowData._id)}
|
|
>
|
|
${unsafeHTML(RemoveSVG)}
|
|
</button>
|
|
</td>
|
|
</tr>`
|
|
}
|
|
|
|
private readonly _renderFooter = (): TemplateResult => {
|
|
if (this.maxLines && this._rowsById.length >= this.maxLines) {
|
|
return html``
|
|
}
|
|
// Note: the addRow button is in first column, so it won't be hidden if screen not wide enough.
|
|
return html`<tfoot>
|
|
<tr>
|
|
<td class="dynamic-table-add-row-cell">
|
|
<button type="button"
|
|
class="dynamic-table-add-row"
|
|
.title=${ptTr(LOC_ACTION_ADD_ENTRY) as any}
|
|
@click=${this._addRow}
|
|
>
|
|
${unsafeHTML(AddSVG)}
|
|
</button>
|
|
</td>
|
|
${Object.values(this.header).map(() => html`<td></td>`)}
|
|
</tr>
|
|
</tfoot>`
|
|
}
|
|
|
|
renderDataCell = (propertyName: string,
|
|
propertyValue: DynamicTableAcceptedTypes,
|
|
rowId: number,
|
|
originalIndex: number): TemplateResult => {
|
|
const propertySchema = this.schema[propertyName] ?? {}
|
|
|
|
let formElement
|
|
|
|
const inputName = `${this.formName.replace(/-/g, '_')}_${propertyName.toString().replace(/-/g, '_')}_${rowId}`
|
|
const inputId =
|
|
`peertube-livechat-${this.formName.replace(/_/g, '-')}-${propertyName.toString().replace(/_/g, '-')}-${rowId}`
|
|
|
|
const feedback = this._renderFeedback(inputId, propertyName, originalIndex)
|
|
|
|
switch (propertySchema.default?.constructor) {
|
|
case String:
|
|
propertySchema.inputType ??= 'text'
|
|
switch (propertySchema.inputType) {
|
|
case 'text':
|
|
case 'color':
|
|
case 'date':
|
|
case 'datetime':
|
|
case 'datetime-local':
|
|
case 'email':
|
|
case 'file':
|
|
case 'image':
|
|
case 'month':
|
|
case 'number':
|
|
case 'password':
|
|
case 'range':
|
|
case 'tel':
|
|
case 'time':
|
|
case 'url':
|
|
case 'week':
|
|
formElement = html`${this._renderInput(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
propertyValue as string,
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
|
|
case 'textarea':
|
|
formElement = html`${this._renderTextarea(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
propertyValue as string,
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
|
|
case 'select':
|
|
formElement = html`${this._renderSelect(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
propertyValue as string,
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
|
|
case 'image-file':
|
|
formElement = html`${this._renderImageFileInput(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
propertyValue?.toString(),
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
}
|
|
break
|
|
|
|
case Date:
|
|
propertySchema.inputType ??= 'datetime'
|
|
switch (propertySchema.inputType) {
|
|
case 'date':
|
|
case 'datetime':
|
|
case 'datetime-local':
|
|
case 'time':
|
|
formElement = html`${this._renderInput(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
(propertyValue as Date).toISOString(),
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
}
|
|
break
|
|
|
|
case Number:
|
|
propertySchema.inputType ??= 'number'
|
|
switch (propertySchema.inputType) {
|
|
case 'number':
|
|
case 'range':
|
|
formElement = html`${this._renderInput(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
propertyValue as string,
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
}
|
|
break
|
|
|
|
case Boolean:
|
|
propertySchema.inputType ??= 'checkbox'
|
|
switch (propertySchema.inputType) {
|
|
case 'checkbox':
|
|
formElement = html`${this._renderCheckbox(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
propertyValue as boolean,
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
}
|
|
break
|
|
|
|
case Array:
|
|
propertySchema.inputType ??= 'text'
|
|
switch (propertySchema.inputType) {
|
|
case 'text':
|
|
case 'color':
|
|
case 'date':
|
|
case 'datetime':
|
|
case 'datetime-local':
|
|
case 'email':
|
|
case 'file':
|
|
case 'image':
|
|
case 'month':
|
|
case 'number':
|
|
case 'password':
|
|
case 'range':
|
|
case 'tel':
|
|
case 'time':
|
|
case 'url':
|
|
case 'week':
|
|
if (propertyValue.constructor !== Array) {
|
|
propertyValue = (propertyValue) ? [propertyValue as (number | string)] : []
|
|
}
|
|
formElement = html`${this._renderInput(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
|
propertyValue ?? propertySchema.default ?? '',
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
case 'textarea':
|
|
if (propertyValue.constructor !== Array) {
|
|
propertyValue = (propertyValue) ? [propertyValue as (number | string)] : []
|
|
}
|
|
formElement = html`${this._renderTextarea(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
(propertyValue)?.join(propertySchema.separator ?? ',') ??
|
|
propertyValue ?? propertySchema.default ?? '',
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
case 'tags':
|
|
if (propertyValue.constructor !== Array) {
|
|
propertyValue = (propertyValue) ? [propertyValue as (number | string)] : []
|
|
}
|
|
formElement = html`${this._renderTagsInput(rowId,
|
|
inputId,
|
|
inputName,
|
|
propertyName,
|
|
propertySchema,
|
|
propertyValue,
|
|
originalIndex)}
|
|
${feedback}
|
|
`
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!formElement) {
|
|
this.logger.warn(`value type '${(propertyValue.constructor.toString())}' is incompatible` +
|
|
`with field type '${propertySchema.inputType as string}' for form entry '${propertyName.toString()}'.`)
|
|
}
|
|
|
|
const classList = ['form-group']
|
|
if (propertySchema.colClassList) {
|
|
classList.push(...propertySchema.colClassList)
|
|
}
|
|
return html`<td class=${classList.join(' ')}>${formElement}</td>`
|
|
}
|
|
|
|
_renderInput = (rowId: number,
|
|
inputId: string,
|
|
inputName: string,
|
|
propertyName: string,
|
|
propertySchema: CellDataSchema,
|
|
propertyValue: string,
|
|
originalIndex: number): TemplateResult => {
|
|
return html`<input
|
|
type=${propertySchema.inputType as any}
|
|
name=${inputName}
|
|
class=${classMap(
|
|
Object.assign(
|
|
{ 'form-control': true },
|
|
this._getInputValidationClass(propertyName, originalIndex)
|
|
)
|
|
)}
|
|
id=${inputId}
|
|
aria-describedby="${inputId}-feedback"
|
|
list=${ifDefined(propertySchema.datalist ? inputId + '-datalist' : undefined)}
|
|
min=${ifDefined(propertySchema.min)}
|
|
max=${ifDefined(propertySchema.max)}
|
|
minlength=${ifDefined(propertySchema.minlength)}
|
|
maxlength=${ifDefined(propertySchema.maxlength)}
|
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
|
.value=${propertyValue}
|
|
/>
|
|
${(propertySchema.datalist)
|
|
? html`<datalist id=${inputId + '-datalist'}>
|
|
${(propertySchema.datalist ?? []).map((value) => html`<option value=${value.toString()}>`)}
|
|
</datalist>`
|
|
: nothing}`
|
|
}
|
|
|
|
_renderTagsInput = (rowId: number,
|
|
inputId: string,
|
|
inputName: string,
|
|
propertyName: string,
|
|
propertySchema: CellDataSchema,
|
|
propertyValue: Array<string | number>,
|
|
originalIndex: number): TemplateResult => {
|
|
return html`<livechat-tags-input
|
|
.name=${inputName}
|
|
class=${classMap(
|
|
Object.assign(
|
|
{ 'form-control': true },
|
|
this._getInputValidationClass(propertyName, originalIndex)
|
|
)
|
|
)}
|
|
id=${inputId}
|
|
.inputPlaceholder=${propertySchema.label as any}
|
|
aria-describedby="${inputId}-feedback"
|
|
.min=${propertySchema.min}
|
|
.max=${propertySchema.max}
|
|
.minlength=${propertySchema.minlength}
|
|
.maxlength=${propertySchema.maxlength}
|
|
.datalist=${propertySchema.datalist as any}
|
|
.separator=${propertySchema.separator ?? '\n'}
|
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
|
.value=${propertyValue as any}
|
|
></livechat-tags-input>`
|
|
}
|
|
|
|
_renderTextarea = (rowId: number,
|
|
inputId: string,
|
|
inputName: string,
|
|
propertyName: string,
|
|
propertySchema: CellDataSchema,
|
|
propertyValue: string,
|
|
originalIndex: number): TemplateResult => {
|
|
return html`<textarea
|
|
name=${inputName}
|
|
class=${classMap(
|
|
Object.assign(
|
|
{ 'form-control': true },
|
|
this._getInputValidationClass(propertyName, originalIndex)
|
|
)
|
|
)}
|
|
id=${inputId}
|
|
aria-describedby="${inputId}-feedback"
|
|
min=${ifDefined(propertySchema.min)}
|
|
max=${ifDefined(propertySchema.max)}
|
|
minlength=${ifDefined(propertySchema.minlength)}
|
|
maxlength=${ifDefined(propertySchema.maxlength)}
|
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
|
.value=${propertyValue}></textarea>`
|
|
}
|
|
|
|
_renderCheckbox = (rowId: number,
|
|
inputId: string,
|
|
inputName: string,
|
|
propertyName: string,
|
|
propertySchema: CellDataSchema,
|
|
propertyValue: boolean,
|
|
originalIndex: number): TemplateResult => {
|
|
return html`<input
|
|
type="checkbox"
|
|
name=${inputName}
|
|
class=${classMap(
|
|
Object.assign(
|
|
{ 'form-check-input': true },
|
|
this._getInputValidationClass(propertyName, originalIndex)
|
|
)
|
|
)}
|
|
id=${inputId}
|
|
aria-describedby="${inputId}-feedback"
|
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
|
value="1"
|
|
?checked=${propertyValue} />`
|
|
}
|
|
|
|
_renderSelect = (rowId: number,
|
|
inputId: string,
|
|
inputName: string,
|
|
propertyName: string,
|
|
propertySchema: CellDataSchema,
|
|
propertyValue: string,
|
|
originalIndex: number): TemplateResult => {
|
|
return html`<select
|
|
class=${classMap(
|
|
Object.assign(
|
|
{ 'form-select': true },
|
|
this._getInputValidationClass(propertyName, originalIndex)
|
|
)
|
|
)}
|
|
id=${inputId}
|
|
aria-describedby="${inputId}-feedback"
|
|
aria-label=${inputName}
|
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
|
>
|
|
<option ?selected=${!propertyValue}>${propertySchema.label ?? 'Choose your option'}</option>
|
|
${Object.entries(propertySchema.options ?? {})
|
|
?.map(([value, name]) =>
|
|
html`<option ?selected=${propertyValue === value} value=${value}>${name}</option>`
|
|
)}
|
|
</select>`
|
|
}
|
|
|
|
_renderImageFileInput = (rowId: number,
|
|
inputId: string,
|
|
inputName: string,
|
|
propertyName: string,
|
|
propertySchema: CellDataSchema,
|
|
propertyValue: string,
|
|
originalIndex: number
|
|
): TemplateResult => {
|
|
return html`<livechat-image-file-input
|
|
.name=${inputName}
|
|
class=${classMap(this._getInputValidationClass(propertyName, originalIndex))}
|
|
id=${inputId}
|
|
aria-describedby="${inputId}-feedback"
|
|
@change=${(event: Event) => this._updatePropertyFromValue(event, propertyName, propertySchema, rowId)}
|
|
.value=${propertyValue}
|
|
.maxSize=${maxSize}
|
|
.accept=${inputFileAccept}
|
|
></livechat-image-file-input>`
|
|
}
|
|
|
|
_getInputValidationClass = (propertyName: string,
|
|
originalIndex: number): { [key: string]: boolean } => {
|
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
|
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
|
|
|
return validationErrorTypes !== undefined
|
|
? (validationErrorTypes.length ? { 'is-invalid': true } : { 'is-valid': true })
|
|
: {}
|
|
}
|
|
|
|
_renderFeedback = (inputId: string,
|
|
propertyName: string,
|
|
originalIndex: number): TemplateResult | typeof nothing => {
|
|
const errorMessages: TemplateResult[] = []
|
|
const validationErrorTypes: ValidationErrorType[] | undefined =
|
|
this.validation?.[`${this.validationPrefix}.${originalIndex}.${propertyName}`]
|
|
|
|
if (validationErrorTypes !== undefined && validationErrorTypes.length !== 0) {
|
|
if (validationErrorTypes.includes(ValidationErrorType.Missing)) {
|
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_MISSING)}`)
|
|
}
|
|
if (validationErrorTypes.includes(ValidationErrorType.WrongType)) {
|
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_WRONG_TYPE)}`)
|
|
}
|
|
if (validationErrorTypes.includes(ValidationErrorType.WrongFormat)) {
|
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_WRONG_FORMAT)}`)
|
|
}
|
|
if (validationErrorTypes.includes(ValidationErrorType.NotInRange)) {
|
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_NOT_IN_RANGE)}`)
|
|
}
|
|
if (validationErrorTypes.includes(ValidationErrorType.Duplicate)) {
|
|
errorMessages.push(html`${ptTr(LOC_INVALID_VALUE_DUPLICATE)}`)
|
|
}
|
|
|
|
return html`<div id="${inputId}-feedback" class="invalid-feedback">${errorMessages}</div>`
|
|
} else {
|
|
return nothing
|
|
}
|
|
}
|
|
|
|
_updatePropertyFromValue = (event: Event,
|
|
propertyName: string,
|
|
propertySchema: CellDataSchema,
|
|
rowId: number): void => {
|
|
const target = event.target as (TagsInputElement | HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)
|
|
const value = (target)
|
|
? (target instanceof HTMLInputElement && target.type === 'checkbox')
|
|
? !!(target.checked)
|
|
: target.value
|
|
: undefined
|
|
|
|
if (value === undefined) {
|
|
this.logger.warn('Could not update property : Target or value was undefined')
|
|
return
|
|
}
|
|
|
|
const rowById = this._rowsById.find(rowById => rowById._id === rowId)
|
|
if (!rowById) {
|
|
this.logger.warn(`Could not update property : Did not find a property named '${propertyName}' in row '${rowId}'`)
|
|
return
|
|
}
|
|
|
|
switch (propertySchema.default?.constructor) {
|
|
case Array:
|
|
if (value.constructor === Array || !propertySchema.separator) {
|
|
rowById.row[propertyName] = value
|
|
} else {
|
|
rowById.row[propertyName] = (value as string)
|
|
.split(propertySchema.separator)
|
|
}
|
|
break
|
|
default:
|
|
rowById.row[propertyName] = value
|
|
break
|
|
}
|
|
|
|
this.rows = this._rowsById.map(rowById => rowById.row)
|
|
|
|
this.requestUpdate('rows')
|
|
this.requestUpdate('_rowsById')
|
|
this.dispatchEvent(new CustomEvent('update', { detail: this.rows }))
|
|
}
|
|
}
|