[增添]添加了datasource的setting数据库以及默认值

This commit is contained in:
makotocc0107
2024-08-27 09:57:44 +08:00
parent d111dfaea4
commit 72eb990970
10955 changed files with 978898 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import 'vanilla-colorful/hex-color-picker.js'
import 'vanilla-colorful/hsl-string-color-picker.js'
import 'vanilla-colorful/rgb-string-color-picker.js'
import 'vanilla-colorful/rgba-string-color-picker.js'
export default function colorPickerFormComponent({
isAutofocused,
isDisabled,
isLive,
isLiveDebounced,
isLiveOnBlur,
liveDebounce,
state,
}) {
return {
state,
init: function () {
if (!(this.state === null || this.state === '')) {
this.setState(this.state)
}
if (isAutofocused) {
this.togglePanelVisibility(this.$refs.input)
}
this.$refs.input.addEventListener('change', (event) => {
this.setState(event.target.value)
})
this.$refs.panel.addEventListener('color-changed', (event) => {
this.setState(event.detail.value)
if (isLiveOnBlur || !(isLive || isLiveDebounced)) {
return
}
setTimeout(
() => {
if (this.state !== event.detail.value) {
return
}
this.commitState()
},
isLiveDebounced ? liveDebounce : 250,
)
})
if (isLive || isLiveDebounced || isLiveOnBlur) {
new MutationObserver(() =>
this.isOpen() ? null : this.commitState(),
).observe(this.$refs.panel, {
attributes: true,
childList: true,
})
}
},
togglePanelVisibility: function () {
if (isDisabled) {
return
}
this.$refs.panel.toggle(this.$refs.input)
},
setState: function (value) {
this.state = value
this.$refs.input.value = value
this.$refs.panel.color = value
},
isOpen: function () {
return this.$refs.panel.style.display === 'block'
},
commitState: function () {
if (
JSON.stringify(this.$wire.__instance.canonical) ===
JSON.stringify(this.$wire.__instance.ephemeral)
) {
return
}
this.$wire.$commit()
},
}
}

View File

@@ -0,0 +1,516 @@
import dayjs from 'dayjs/esm'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import localeData from 'dayjs/plugin/localeData'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
dayjs.extend(customParseFormat)
dayjs.extend(localeData)
dayjs.extend(timezone)
dayjs.extend(utc)
window.dayjs = dayjs
export default function dateTimePickerFormComponent({
displayFormat,
firstDayOfWeek,
isAutofocused,
locale,
shouldCloseOnDateSelection,
state,
}) {
const timezone = dayjs.tz.guess()
return {
daysInFocusedMonth: [],
displayText: '',
emptyDaysInFocusedMonth: [],
focusedDate: null,
focusedMonth: null,
focusedYear: null,
hour: null,
isClearingState: false,
minute: null,
second: null,
state,
dayLabels: [],
months: [],
init: function () {
dayjs.locale(locales[locale] ?? locales['en'])
this.focusedDate = dayjs().tz(timezone)
let date =
this.getSelectedDate() ??
dayjs().tz(timezone).hour(0).minute(0).second(0)
if (this.getMaxDate() !== null && date.isAfter(this.getMaxDate())) {
date = null
} else if (
this.getMinDate() !== null &&
date.isBefore(this.getMinDate())
) {
date = null
}
this.hour = date?.hour() ?? 0
this.minute = date?.minute() ?? 0
this.second = date?.second() ?? 0
this.setDisplayText()
this.setMonths()
this.setDayLabels()
if (isAutofocused) {
this.$nextTick(() =>
this.togglePanelVisibility(this.$refs.button),
)
}
this.$watch('focusedMonth', () => {
this.focusedMonth = +this.focusedMonth
if (this.focusedDate.month() === this.focusedMonth) {
return
}
this.focusedDate = this.focusedDate.month(this.focusedMonth)
})
this.$watch('focusedYear', () => {
if (this.focusedYear?.length > 4) {
this.focusedYear = this.focusedYear.substring(0, 4)
}
if (!this.focusedYear || this.focusedYear?.length !== 4) {
return
}
let year = +this.focusedYear
if (!Number.isInteger(year)) {
year = dayjs().tz(timezone).year()
this.focusedYear = year
}
if (this.focusedDate.year() === year) {
return
}
this.focusedDate = this.focusedDate.year(year)
})
this.$watch('focusedDate', () => {
let month = this.focusedDate.month()
let year = this.focusedDate.year()
if (this.focusedMonth !== month) {
this.focusedMonth = month
}
if (this.focusedYear !== year) {
this.focusedYear = year
}
this.setupDaysGrid()
})
this.$watch('hour', () => {
let hour = +this.hour
if (!Number.isInteger(hour)) {
this.hour = 0
} else if (hour > 23) {
this.hour = 0
} else if (hour < 0) {
this.hour = 23
} else {
this.hour = hour
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.hour(this.hour ?? 0))
})
this.$watch('minute', () => {
let minute = +this.minute
if (!Number.isInteger(minute)) {
this.minute = 0
} else if (minute > 59) {
this.minute = 0
} else if (minute < 0) {
this.minute = 59
} else {
this.minute = minute
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.minute(this.minute ?? 0))
})
this.$watch('second', () => {
let second = +this.second
if (!Number.isInteger(second)) {
this.second = 0
} else if (second > 59) {
this.second = 0
} else if (second < 0) {
this.second = 59
} else {
this.second = second
}
if (this.isClearingState) {
return
}
let date = this.getSelectedDate() ?? this.focusedDate
this.setState(date.second(this.second ?? 0))
})
this.$watch('state', () => {
if (this.state === undefined) {
return
}
let date = this.getSelectedDate()
if (date === null) {
this.clearState()
return
}
if (
this.getMaxDate() !== null &&
date?.isAfter(this.getMaxDate())
) {
date = null
}
if (
this.getMinDate() !== null &&
date?.isBefore(this.getMinDate())
) {
date = null
}
const newHour = date?.hour() ?? 0
if (this.hour !== newHour) {
this.hour = newHour
}
const newMinute = date?.minute() ?? 0
if (this.minute !== newMinute) {
this.minute = newMinute
}
const newSecond = date?.second() ?? 0
if (this.second !== newSecond) {
this.second = newSecond
}
this.setDisplayText()
})
},
clearState: function () {
this.isClearingState = true
this.setState(null)
this.hour = 0
this.minute = 0
this.second = 0
this.$nextTick(() => (this.isClearingState = false))
},
dateIsDisabled: function (date) {
if (
this.$refs?.disabledDates &&
JSON.parse(this.$refs.disabledDates.value ?? []).some(
(disabledDate) => {
disabledDate = dayjs(disabledDate)
if (!disabledDate.isValid()) {
return false
}
return disabledDate.isSame(date, 'day')
},
)
) {
return true
}
if (this.getMaxDate() && date.isAfter(this.getMaxDate(), 'day')) {
return true
}
if (this.getMinDate() && date.isBefore(this.getMinDate(), 'day')) {
return true
}
return false
},
dayIsDisabled: function (day) {
this.focusedDate ??= dayjs().tz(timezone)
return this.dateIsDisabled(this.focusedDate.date(day))
},
dayIsSelected: function (day) {
let selectedDate = this.getSelectedDate()
if (selectedDate === null) {
return false
}
this.focusedDate ??= dayjs().tz(timezone)
return (
selectedDate.date() === day &&
selectedDate.month() === this.focusedDate.month() &&
selectedDate.year() === this.focusedDate.year()
)
},
dayIsToday: function (day) {
let date = dayjs().tz(timezone)
this.focusedDate ??= date
return (
date.date() === day &&
date.month() === this.focusedDate.month() &&
date.year() === this.focusedDate.year()
)
},
focusPreviousDay: function () {
this.focusedDate ??= dayjs().tz(timezone)
this.focusedDate = this.focusedDate.subtract(1, 'day')
},
focusPreviousWeek: function () {
this.focusedDate ??= dayjs().tz(timezone)
this.focusedDate = this.focusedDate.subtract(1, 'week')
},
focusNextDay: function () {
this.focusedDate ??= dayjs().tz(timezone)
this.focusedDate = this.focusedDate.add(1, 'day')
},
focusNextWeek: function () {
this.focusedDate ??= dayjs().tz(timezone)
this.focusedDate = this.focusedDate.add(1, 'week')
},
getDayLabels: function () {
const labels = dayjs.weekdaysShort()
if (firstDayOfWeek === 0) {
return labels
}
return [
...labels.slice(firstDayOfWeek),
...labels.slice(0, firstDayOfWeek),
]
},
getMaxDate: function () {
let date = dayjs(this.$refs.maxDate?.value)
return date.isValid() ? date : null
},
getMinDate: function () {
let date = dayjs(this.$refs.minDate?.value)
return date.isValid() ? date : null
},
getSelectedDate: function () {
if (this.state === undefined) {
return null
}
if (this.state === null) {
return null
}
let date = dayjs(this.state)
if (!date.isValid()) {
return null
}
return date
},
togglePanelVisibility: function () {
if (!this.isOpen()) {
this.focusedDate =
this.getSelectedDate() ??
this.getMinDate() ??
dayjs().tz(timezone)
this.setupDaysGrid()
}
this.$refs.panel.toggle(this.$refs.button)
},
selectDate: function (day = null) {
if (day) {
this.setFocusedDay(day)
}
this.focusedDate ??= dayjs().tz(timezone)
this.setState(this.focusedDate)
if (shouldCloseOnDateSelection) {
this.togglePanelVisibility()
}
},
setDisplayText: function () {
this.displayText = this.getSelectedDate()
? this.getSelectedDate().format(displayFormat)
: ''
},
setMonths: function () {
this.months = dayjs.months()
},
setDayLabels: function () {
this.dayLabels = this.getDayLabels()
},
setupDaysGrid: function () {
this.focusedDate ??= dayjs().tz(timezone)
this.emptyDaysInFocusedMonth = Array.from(
{
length: this.focusedDate.date(8 - firstDayOfWeek).day(),
},
(_, i) => i + 1,
)
this.daysInFocusedMonth = Array.from(
{
length: this.focusedDate.daysInMonth(),
},
(_, i) => i + 1,
)
},
setFocusedDay: function (day) {
this.focusedDate = (this.focusedDate ?? dayjs().tz(timezone)).date(
day,
)
},
setState: function (date) {
if (date === null) {
this.state = null
this.setDisplayText()
return
}
if (this.dateIsDisabled(date)) {
return
}
this.state = date
.hour(this.hour ?? 0)
.minute(this.minute ?? 0)
.second(this.second ?? 0)
.format('YYYY-MM-DD HH:mm:ss')
this.setDisplayText()
},
isOpen: function () {
return this.$refs.panel?.style.display === 'block'
},
}
}
const locales = {
ar: require('dayjs/locale/ar'),
bs: require('dayjs/locale/bs'),
ca: require('dayjs/locale/ca'),
ckb: require('dayjs/locale/ku'),
cs: require('dayjs/locale/cs'),
cy: require('dayjs/locale/cy'),
da: require('dayjs/locale/da'),
de: require('dayjs/locale/de'),
en: require('dayjs/locale/en'),
es: require('dayjs/locale/es'),
et: require('dayjs/locale/et'),
fa: require('dayjs/locale/fa'),
fi: require('dayjs/locale/fi'),
fr: require('dayjs/locale/fr'),
hi: require('dayjs/locale/hi'),
hu: require('dayjs/locale/hu'),
hy: require('dayjs/locale/hy-am'),
id: require('dayjs/locale/id'),
it: require('dayjs/locale/it'),
ja: require('dayjs/locale/ja'),
ka: require('dayjs/locale/ka'),
km: require('dayjs/locale/km'),
ku: require('dayjs/locale/ku'),
lt: require('dayjs/locale/lt'),
lv: require('dayjs/locale/lv'),
ms: require('dayjs/locale/ms'),
my: require('dayjs/locale/my'),
nl: require('dayjs/locale/nl'),
no: require('dayjs/locale/nb'),
pl: require('dayjs/locale/pl'),
pt_BR: require('dayjs/locale/pt-br'),
pt_PT: require('dayjs/locale/pt'),
ro: require('dayjs/locale/ro'),
ru: require('dayjs/locale/ru'),
sv: require('dayjs/locale/sv'),
tr: require('dayjs/locale/tr'),
uk: require('dayjs/locale/uk'),
vi: require('dayjs/locale/vi'),
zh_CN: require('dayjs/locale/zh-cn'),
zh_TW: require('dayjs/locale/zh-tw'),
}

View File

@@ -0,0 +1,776 @@
import * as FilePond from 'filepond'
import Cropper from 'cropperjs'
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size'
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type'
import FilePondPluginImageCrop from 'filepond-plugin-image-crop'
import FilePondPluginImageEdit from 'filepond-plugin-image-edit'
import FilePondPluginImageExifOrientation from 'filepond-plugin-image-exif-orientation'
import FilePondPluginImagePreview from 'filepond-plugin-image-preview'
import FilePondPluginImageResize from 'filepond-plugin-image-resize'
import FilePondPluginImageTransform from 'filepond-plugin-image-transform'
import FilePondPluginMediaPreview from 'filepond-plugin-media-preview'
FilePond.registerPlugin(FilePondPluginFileValidateSize)
FilePond.registerPlugin(FilePondPluginFileValidateType)
FilePond.registerPlugin(FilePondPluginImageCrop)
FilePond.registerPlugin(FilePondPluginImageEdit)
FilePond.registerPlugin(FilePondPluginImageExifOrientation)
FilePond.registerPlugin(FilePondPluginImagePreview)
FilePond.registerPlugin(FilePondPluginImageResize)
FilePond.registerPlugin(FilePondPluginImageTransform)
FilePond.registerPlugin(FilePondPluginMediaPreview)
window.FilePond = FilePond
export default function fileUploadFormComponent({
acceptedFileTypes,
imageEditorEmptyFillColor,
imageEditorMode,
imageEditorViewportHeight,
imageEditorViewportWidth,
deleteUploadedFileUsing,
isDeletable,
isDisabled,
getUploadedFilesUsing,
imageCropAspectRatio,
imagePreviewHeight,
imageResizeMode,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeUpscale,
isAvatar,
hasImageEditor,
hasCircleCropper,
canEditSvgs,
isSvgEditingConfirmed,
confirmSvgEditingMessage,
disabledSvgEditingMessage,
isDownloadable,
isMultiple,
isOpenable,
isPreviewable,
isReorderable,
itemPanelAspectRatio,
loadingIndicatorPosition,
locale,
maxFiles,
maxSize,
minSize,
panelAspectRatio,
panelLayout,
placeholder,
removeUploadedFileButtonPosition,
removeUploadedFileUsing,
reorderUploadedFilesUsing,
shouldAppendFiles,
shouldOrientImageFromExif,
shouldTransformImage,
state,
uploadButtonPosition,
uploadingMessage,
uploadProgressIndicatorPosition,
uploadUsing,
}) {
return {
fileKeyIndex: {},
pond: null,
shouldUpdateState: true,
state,
lastState: null,
uploadedFileIndex: {},
isEditorOpen: false,
editingFile: {},
currentRatio: '',
editor: {},
init: async function () {
FilePond.setOptions(locales[locale] ?? locales['en'])
this.pond = FilePond.create(this.$refs.input, {
acceptedFileTypes,
allowImageExifOrientation: shouldOrientImageFromExif,
allowPaste: false,
allowRemove: isDeletable,
allowReorder: isReorderable,
allowImagePreview: isPreviewable,
allowVideoPreview: isPreviewable,
allowAudioPreview: isPreviewable,
allowImageTransform: shouldTransformImage,
credits: false,
files: await this.getFiles(),
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeMode,
imageResizeUpscale,
itemInsertLocation: shouldAppendFiles ? 'after' : 'before',
...(placeholder && { labelIdle: placeholder }),
maxFiles,
maxFileSize: maxSize,
minFileSize: minSize,
styleButtonProcessItemPosition: uploadButtonPosition,
styleButtonRemoveItemPosition: removeUploadedFileButtonPosition,
styleItemPanelAspectRatio: itemPanelAspectRatio,
styleLoadIndicatorPosition: loadingIndicatorPosition,
stylePanelAspectRatio: panelAspectRatio,
stylePanelLayout: panelLayout,
styleProgressIndicatorPosition: uploadProgressIndicatorPosition,
server: {
load: async (source, load) => {
let response = await fetch(source, {
cache: 'no-store',
})
let blob = await response.blob()
load(blob)
},
process: (
fieldName,
file,
metadata,
load,
error,
progress,
) => {
this.shouldUpdateState = false
let fileKey = (
[1e7] +
-1e3 +
-4e3 +
-8e3 +
-1e11
).replace(/[018]/g, (c) =>
(
c ^
(crypto.getRandomValues(new Uint8Array(1))[0] &
(15 >> (c / 4)))
).toString(16),
)
uploadUsing(
fileKey,
file,
(fileKey) => {
this.shouldUpdateState = true
load(fileKey)
},
error,
progress,
)
},
remove: async (source, load) => {
let fileKey = this.uploadedFileIndex[source] ?? null
if (!fileKey) {
return
}
await deleteUploadedFileUsing(fileKey)
load()
},
revert: async (uniqueFileId, load) => {
await removeUploadedFileUsing(uniqueFileId)
load()
},
},
allowImageEdit: hasImageEditor,
imageEditEditor: {
open: (file) => this.loadEditor(file),
onconfirm: () => {},
oncancel: () => this.closeEditor(),
onclose: () => this.closeEditor(),
},
})
this.$watch('state', async () => {
if (!this.pond) {
return
}
if (!this.shouldUpdateState) {
return
}
if (this.state === undefined) {
return
}
// We don't want to overwrite the files that are already in the input, if they haven't been saved yet.
if (
this.state !== null &&
Object.values(this.state).filter((file) =>
file.startsWith('livewire-file:'),
).length
) {
this.lastState = null
return
}
// Don't do anything if the state hasn't changed
if (JSON.stringify(this.state) === this.lastState) {
return
}
this.lastState = JSON.stringify(this.state)
this.pond.files = await this.getFiles()
})
this.pond.on('reorderfiles', async (files) => {
const orderedFileKeys = files
.map((file) =>
file.source instanceof File
? file.serverId
: this.uploadedFileIndex[file.source] ?? null,
) // file.serverId is null for a file that is not yet uploaded
.filter((fileKey) => fileKey)
await reorderUploadedFilesUsing(
shouldAppendFiles
? orderedFileKeys
: orderedFileKeys.reverse(),
)
})
this.pond.on('initfile', async (fileItem) => {
if (!isDownloadable) {
return
}
if (isAvatar) {
return
}
this.insertDownloadLink(fileItem)
})
this.pond.on('initfile', async (fileItem) => {
if (!isOpenable) {
return
}
if (isAvatar) {
return
}
this.insertOpenLink(fileItem)
})
this.pond.on('addfilestart', async (file) => {
if (file.status !== FilePond.FileStatus.PROCESSING_QUEUED) {
return
}
this.dispatchFormEvent('form-processing-started', {
message: uploadingMessage,
})
})
const handleFileProcessing = async () => {
if (
this.pond
.getFiles()
.filter(
(file) =>
file.status ===
FilePond.FileStatus.PROCESSING ||
file.status ===
FilePond.FileStatus.PROCESSING_QUEUED,
).length
) {
return
}
this.dispatchFormEvent('form-processing-finished')
}
this.pond.on('processfile', handleFileProcessing)
this.pond.on('processfileabort', handleFileProcessing)
this.pond.on('processfilerevert', handleFileProcessing)
},
destroy: function () {
this.destroyEditor()
FilePond.destroy(this.$refs.input)
this.pond = null
},
dispatchFormEvent: function (name, detail = {}) {
this.$el.closest('form')?.dispatchEvent(
new CustomEvent(name, {
composed: true,
cancelable: true,
detail,
}),
)
},
getUploadedFiles: async function () {
const uploadedFiles = await getUploadedFilesUsing()
this.fileKeyIndex = uploadedFiles ?? {}
this.uploadedFileIndex = Object.entries(this.fileKeyIndex)
.filter(([key, value]) => value?.url)
.reduce((obj, [key, value]) => {
obj[value.url] = key
return obj
}, {})
},
getFiles: async function () {
await this.getUploadedFiles()
let files = []
for (const uploadedFile of Object.values(this.fileKeyIndex)) {
if (!uploadedFile) {
continue
}
files.push({
source: uploadedFile.url,
options: {
type: 'local',
...(!uploadedFile.type ||
(isPreviewable &&
(/^audio/.test(uploadedFile.type) ||
/^image/.test(uploadedFile.type) ||
/^video/.test(uploadedFile.type)))
? {}
: {
file: {
name: uploadedFile.name,
size: uploadedFile.size,
type: uploadedFile.type,
},
}),
},
})
}
return shouldAppendFiles ? files : files.reverse()
},
insertDownloadLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
const anchor = this.getDownloadLink(file)
if (!anchor) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
insertOpenLink: function (file) {
if (file.origin !== FilePond.FileOrigin.LOCAL) {
return
}
const anchor = this.getOpenLink(file)
if (!anchor) {
return
}
document
.getElementById(`filepond--item-${file.id}`)
.querySelector('.filepond--file-info-main')
.prepend(anchor)
},
getDownloadLink: function (file) {
let fileSource = file.source
if (!fileSource) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--download-icon'
anchor.href = fileSource
anchor.download = file.file.name
return anchor
},
getOpenLink: function (file) {
let fileSource = file.source
if (!fileSource) {
return
}
const anchor = document.createElement('a')
anchor.className = 'filepond--open-icon'
anchor.href = fileSource
anchor.target = '_blank'
return anchor
},
initEditor: function () {
if (isDisabled) {
return
}
if (!hasImageEditor) {
return
}
this.editor = new Cropper(this.$refs.editor, {
aspectRatio:
imageEditorViewportWidth / imageEditorViewportHeight,
autoCropArea: 1,
center: true,
crop: (event) => {
this.$refs.xPositionInput.value = Math.round(event.detail.x)
this.$refs.yPositionInput.value = Math.round(event.detail.y)
this.$refs.heightInput.value = Math.round(
event.detail.height,
)
this.$refs.widthInput.value = Math.round(event.detail.width)
this.$refs.rotationInput.value = event.detail.rotate
},
cropBoxResizable: true,
guides: true,
highlight: true,
responsive: true,
toggleDragModeOnDblclick: true,
viewMode: imageEditorMode,
wheelZoomRatio: 0.02,
})
},
closeEditor: function () {
this.editingFile = {}
this.isEditorOpen = false
this.destroyEditor()
},
fixImageDimensions: function (file, callback) {
if (file.type !== 'image/svg+xml') {
return callback(file)
}
const svgReader = new FileReader()
svgReader.onload = (event) => {
const svgElement = new DOMParser()
.parseFromString(event.target.result, 'image/svg+xml')
?.querySelector('svg')
if (!svgElement) {
return callback(file)
}
const viewBoxAttribute = ['viewBox', 'ViewBox', 'viewbox'].find(
(attribute) => svgElement.hasAttribute(attribute),
)
if (!viewBoxAttribute) {
return callback(file)
}
const viewBox = svgElement
.getAttribute(viewBoxAttribute)
.split(' ')
if (!viewBox || viewBox.length !== 4) {
return callback(file)
}
svgElement.setAttribute('width', parseFloat(viewBox[2]) + 'pt')
svgElement.setAttribute('height', parseFloat(viewBox[3]) + 'pt')
return callback(
new File(
[
new Blob(
[
new XMLSerializer().serializeToString(
svgElement,
),
],
{ type: 'image/svg+xml' },
),
],
file.name,
{
type: 'image/svg+xml',
_relativePath: '',
},
),
)
}
svgReader.readAsText(file)
},
loadEditor: function (file) {
if (isDisabled) {
return
}
if (!hasImageEditor) {
return
}
if (!file) {
return
}
const isFileSvg = file.type === 'image/svg+xml'
if (!canEditSvgs && isFileSvg) {
alert(disabledSvgEditingMessage)
return
}
if (
isSvgEditingConfirmed &&
isFileSvg &&
!confirm(confirmSvgEditingMessage)
) {
return
}
this.fixImageDimensions(file, (editingFile) => {
this.editingFile = editingFile
this.initEditor()
const reader = new FileReader()
reader.onload = (event) => {
this.isEditorOpen = true
setTimeout(
() => this.editor.replace(event.target.result),
200,
)
}
reader.readAsDataURL(file)
})
},
getRoundedCanvas: function (sourceCanvas) {
let width = sourceCanvas.width
let height = sourceCanvas.height
let canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
let context = canvas.getContext('2d')
context.imageSmoothingEnabled = true
context.drawImage(sourceCanvas, 0, 0, width, height)
context.globalCompositeOperation = 'destination-in'
context.beginPath()
context.ellipse(
width / 2,
height / 2,
width / 2,
height / 2,
0,
0,
2 * Math.PI,
)
context.fill()
return canvas
},
saveEditor: function () {
if (isDisabled) {
return
}
if (!hasImageEditor) {
return
}
let croppedCanvas = this.editor.getCroppedCanvas({
fillColor: imageEditorEmptyFillColor ?? 'transparent',
height: imageResizeTargetHeight,
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
width: imageResizeTargetWidth,
})
if (hasCircleCropper) {
croppedCanvas = this.getRoundedCanvas(croppedCanvas)
}
croppedCanvas.toBlob(
(croppedImage) => {
if (isMultiple) {
this.pond.removeFile(
this.pond
.getFiles()
.find(
(uploadedFile) =>
uploadedFile.filename ===
this.editingFile.name,
)?.id,
{ revert: true },
)
}
this.$nextTick(() => {
this.shouldUpdateState = false
let editingFileName = this.editingFile.name.slice(
0,
this.editingFile.name.lastIndexOf('.'),
)
let editingFileExtension = this.editingFile.name
.split('.')
.pop()
if (editingFileExtension === 'svg') {
editingFileExtension = 'png'
}
const fileNameVersionRegex = /-v(\d+)/
if (fileNameVersionRegex.test(editingFileName)) {
editingFileName = editingFileName.replace(
fileNameVersionRegex,
(match, number) => {
const newNumber = Number(number) + 1
return `-v${newNumber}`
},
)
} else {
editingFileName += '-v1'
}
this.pond
.addFile(
new File(
[croppedImage],
`${editingFileName}.${editingFileExtension}`,
{
type:
this.editingFile.type ===
'image/svg+xml' ||
hasCircleCropper
? 'image/png'
: this.editingFile.type,
lastModified: new Date().getTime(),
},
),
)
.then(() => {
this.closeEditor()
})
.catch(() => {
this.closeEditor()
})
})
},
hasCircleCropper ? 'image/png' : this.editingFile.type,
)
},
destroyEditor: function () {
if (this.editor && typeof this.editor.destroy === 'function') {
this.editor.destroy()
}
this.editor = null
},
}
}
import ar from 'filepond/locale/ar-ar'
import ca from 'filepond/locale/ca-ca'
import ckb from 'filepond/locale/ku-ckb'
import cs from 'filepond/locale/cs-cz'
import da from 'filepond/locale/da-dk'
import de from 'filepond/locale/de-de'
import en from 'filepond/locale/en-en'
import es from 'filepond/locale/es-es'
import fa from 'filepond/locale/fa_ir'
import fi from 'filepond/locale/fi-fi'
import fr from 'filepond/locale/fr-fr'
import hu from 'filepond/locale/hu-hu'
import id from 'filepond/locale/id-id'
import it from 'filepond/locale/it-it'
import km from 'filepond/locale/km-km'
import nl from 'filepond/locale/nl-nl'
import no from 'filepond/locale/no_nb'
import pl from 'filepond/locale/pl-pl'
import pt_BR from 'filepond/locale/pt-br'
import pt_PT from 'filepond/locale/pt-br'
import ro from 'filepond/locale/ro-ro'
import ru from 'filepond/locale/ru-ru'
import sv from 'filepond/locale/sv_se'
import tr from 'filepond/locale/tr-tr'
import uk from 'filepond/locale/uk-ua'
import vi from 'filepond/locale/vi-vi'
import zh_CN from 'filepond/locale/zh-cn'
import zh_TW from 'filepond/locale/zh-tw'
const locales = {
ar,
ca,
ckb,
cs,
da,
de,
en,
es,
fa,
fi,
fr,
hu,
id,
it,
km,
nl,
no,
pl,
pt_BR,
pt_PT,
ro,
ru,
sv,
tr,
uk,
vi,
zh_CN,
zh_TW,
}

View File

@@ -0,0 +1,114 @@
export default function keyValueFormComponent({ state }) {
return {
state,
rows: [],
shouldUpdateRows: true,
init: function () {
this.updateRows()
if (this.rows.length <= 0) {
this.rows.push({ key: '', value: '' })
} else {
this.updateState()
}
this.$watch('state', (state, oldState) => {
const getLength = (value) => {
if (value === null) {
return 0
}
if (Array.isArray(value)) {
return value.length
}
if (typeof value !== 'object') {
return 0
}
return Object.keys(value).length
}
if (getLength(state) === 0 && getLength(oldState) === 0) {
return
}
this.updateRows()
})
},
addRow: function () {
this.rows.push({ key: '', value: '' })
this.updateState()
},
deleteRow: function (index) {
this.rows.splice(index, 1)
if (this.rows.length <= 0) {
this.addRow()
}
this.updateState()
},
reorderRows: function (event) {
const rows = Alpine.raw(this.rows)
this.rows = []
const reorderedRow = rows.splice(event.oldIndex, 1)[0]
rows.splice(event.newIndex, 0, reorderedRow)
this.$nextTick(() => {
this.rows = rows
this.updateState()
})
},
updateRows: function () {
if (!this.shouldUpdateRows) {
this.shouldUpdateRows = true
return
}
let rows = []
for (let [key, value] of Object.entries(this.state ?? {})) {
rows.push({
key,
value,
})
}
this.rows = rows
},
updateState: function () {
let state = {}
this.rows.forEach((row) => {
if (row.key === '' || row.key === null) {
return
}
state[row.key] = row.value
})
// This is a hack to prevent the component from updating rows again
// after a state update, which would otherwise be done by the `state`
// watcher. If rows are updated again, duplicate keys are removed.
//
// https://github.com/filamentphp/filament/issues/1107
this.shouldUpdateRows = false
this.state = state
},
}
}

View File

@@ -0,0 +1,375 @@
window.CodeMirror = require('codemirror/lib/codemirror')
require('codemirror')
require('codemirror/addon/mode/overlay')
require('codemirror/addon/edit/continuelist')
require('codemirror/addon/display/placeholder')
require('codemirror/addon/selection/mark-selection')
require('codemirror/addon/search/searchcursor')
require('codemirror/mode/clike/clike')
require('codemirror/mode/cmake/cmake')
require('codemirror/mode/css/css')
require('codemirror/mode/diff/diff')
require('codemirror/mode/django/django')
require('codemirror/mode/dockerfile/dockerfile')
require('codemirror/mode/gfm/gfm')
require('codemirror/mode/go/go')
require('codemirror/mode/htmlmixed/htmlmixed')
require('codemirror/mode/http/http')
require('codemirror/mode/javascript/javascript')
require('codemirror/mode/jinja2/jinja2')
require('codemirror/mode/jsx/jsx')
require('codemirror/mode/markdown/markdown')
require('codemirror/mode/nginx/nginx')
require('codemirror/mode/pascal/pascal')
require('codemirror/mode/perl/perl')
require('codemirror/mode/php/php')
require('codemirror/mode/protobuf/protobuf')
require('codemirror/mode/python/python')
require('codemirror/mode/ruby/ruby')
require('codemirror/mode/rust/rust')
require('codemirror/mode/sass/sass')
require('codemirror/mode/shell/shell')
require('codemirror/mode/sql/sql')
require('codemirror/mode/stylus/stylus')
require('codemirror/mode/swift/swift')
require('codemirror/mode/vue/vue')
require('codemirror/mode/xml/xml')
require('codemirror/mode/yaml/yaml')
require('./markdown-editor/EasyMDE')
CodeMirror.commands.tabAndIndentMarkdownList = function (codemirror) {
var ranges = codemirror.listSelections()
var pos = ranges[0].head
var eolState = codemirror.getStateAfter(pos.line)
var inList = eolState.list !== false
if (inList) {
codemirror.execCommand('indentMore')
return
}
if (codemirror.options.indentWithTabs) {
codemirror.execCommand('insertTab')
return
}
var spaces = Array(codemirror.options.tabSize + 1).join(' ')
codemirror.replaceSelection(spaces)
}
CodeMirror.commands.shiftTabAndUnindentMarkdownList = function (codemirror) {
var ranges = codemirror.listSelections()
var pos = ranges[0].head
var eolState = codemirror.getStateAfter(pos.line)
var inList = eolState.list !== false
if (inList) {
codemirror.execCommand('indentLess')
return
}
if (codemirror.options.indentWithTabs) {
codemirror.execCommand('insertTab')
return
}
var spaces = Array(codemirror.options.tabSize + 1).join(' ')
codemirror.replaceSelection(spaces)
}
export default function markdownEditorFormComponent({
canAttachFiles,
isLiveDebounced,
isLiveOnBlur,
liveDebounce,
maxHeight,
minHeight,
placeholder,
setUpUsing,
state,
translations,
toolbarButtons,
uploadFileAttachmentUsing,
}) {
return {
editor: null,
state,
init: async function () {
if (this.$root._editor) {
this.$root._editor.toTextArea()
this.$root._editor = null
}
this.$root._editor = this.editor = new EasyMDE({
autoDownloadFontAwesome: false,
autoRefresh: true,
autoSave: false,
element: this.$refs.editor,
imageAccept: 'image/png, image/jpeg, image/gif, image/avif',
imageUploadFunction: uploadFileAttachmentUsing,
initialValue: this.state ?? '',
maxHeight,
minHeight,
placeholder,
previewImagesInEditor: true,
spellChecker: false,
status: [
{
className: 'upload-image',
defaultValue: '',
},
],
toolbar: this.getToolbar(),
uploadImage: canAttachFiles,
})
this.editor.codemirror.setOption(
'direction',
document.documentElement?.dir ?? 'ltr',
)
// When creating a link, highlight the URL instead of the label:
this.editor.codemirror.on('changes', (instance, changes) => {
try {
const lastChange = changes[changes.length - 1]
if (lastChange.origin === '+input') {
const urlPlaceholder = '(https://)'
const urlLineText =
lastChange.text[lastChange.text.length - 1]
if (
urlLineText.endsWith(urlPlaceholder) &&
urlLineText !== '[]' + urlPlaceholder
) {
const from = lastChange.from
const to = lastChange.to
const isSelectionMultiline =
lastChange.text.length > 1
const baseIndex = isSelectionMultiline ? 0 : from.ch
setTimeout(() => {
instance.setSelection(
{
line: to.line,
ch:
baseIndex +
urlLineText.lastIndexOf('(') +
1,
},
{
line: to.line,
ch:
baseIndex +
urlLineText.lastIndexOf(')'),
},
)
}, 25)
}
}
} catch (error) {
// Revert to original behavior.
}
})
this.editor.codemirror.on(
'change',
Alpine.debounce(() => {
if (!this.editor) {
return
}
this.state = this.editor.value()
if (isLiveDebounced) {
this.$wire.call('$refresh')
}
}, liveDebounce ?? 300),
)
if (isLiveOnBlur) {
this.editor.codemirror.on('blur', () =>
this.$wire.call('$refresh'),
)
}
this.$watch('state', () => {
if (!this.editor) {
return
}
if (this.editor.codemirror.hasFocus()) {
return
}
Alpine.raw(this.editor).value(this.state ?? '')
})
if (setUpUsing) {
setUpUsing(this)
}
},
destroy: function () {
this.editor.cleanup()
this.editor = null
},
getToolbar: function () {
let toolbar = []
if (toolbarButtons.includes('bold')) {
toolbar.push({
name: 'bold',
action: EasyMDE.toggleBold,
title: translations.toolbar_buttons?.bold,
})
}
if (toolbarButtons.includes('italic')) {
toolbar.push({
name: 'italic',
action: EasyMDE.toggleItalic,
title: translations.toolbar_buttons?.italic,
})
}
if (toolbarButtons.includes('strike')) {
toolbar.push({
name: 'strikethrough',
action: EasyMDE.toggleStrikethrough,
title: translations.toolbar_buttons?.strike,
})
}
if (toolbarButtons.includes('link')) {
toolbar.push({
name: 'link',
action: EasyMDE.drawLink,
title: translations.toolbar_buttons?.link,
})
}
if (
['bold', 'italic', 'strike', 'link'].some((button) =>
toolbarButtons.includes(button),
) &&
['heading'].some((button) => toolbarButtons.includes(button))
) {
toolbar.push('|')
}
if (toolbarButtons.includes('heading')) {
toolbar.push({
name: 'heading',
action: EasyMDE.toggleHeadingSmaller,
title: translations.toolbar_buttons?.heading,
})
}
if (
['heading'].some((button) => toolbarButtons.includes(button)) &&
['blockquote', 'codeBlock', 'bulletList', 'orderedList'].some(
(button) => toolbarButtons.includes(button),
)
) {
toolbar.push('|')
}
if (toolbarButtons.includes('blockquote')) {
toolbar.push({
name: 'quote',
action: EasyMDE.toggleBlockquote,
title: translations.toolbar_buttons?.blockquote,
})
}
if (toolbarButtons.includes('codeBlock')) {
toolbar.push({
name: 'code',
action: EasyMDE.toggleCodeBlock,
title: translations.toolbar_buttons?.code_block,
})
}
if (toolbarButtons.includes('bulletList')) {
toolbar.push({
name: 'unordered-list',
action: EasyMDE.toggleUnorderedList,
title: translations.toolbar_buttons?.bullet_list,
})
}
if (toolbarButtons.includes('orderedList')) {
toolbar.push({
name: 'ordered-list',
action: EasyMDE.toggleOrderedList,
title: translations.toolbar_buttons?.ordered_list,
})
}
if (
['blockquote', 'codeBlock', 'bulletList', 'orderedList'].some(
(button) => toolbarButtons.includes(button),
) &&
['table', 'attachFiles'].some((button) =>
toolbarButtons.includes(button),
)
) {
toolbar.push('|')
}
if (toolbarButtons.includes('table')) {
toolbar.push({
name: 'table',
action: EasyMDE.drawTable,
title: translations.toolbar_buttons?.table,
})
}
if (toolbarButtons.includes('attachFiles')) {
toolbar.push({
name: 'upload-image',
action: EasyMDE.drawUploadedImage,
title: translations.toolbar_buttons?.attach_files,
})
}
if (
['table', 'attachFiles'].some((button) =>
toolbarButtons.includes(button),
) &&
['undo', 'redo'].some((button) =>
toolbarButtons.includes(button),
)
) {
toolbar.push('|')
}
if (toolbarButtons.includes('undo')) {
toolbar.push({
name: 'undo',
action: EasyMDE.undo,
title: translations.toolbar_buttons?.undo,
})
}
if (toolbarButtons.includes('redo')) {
toolbar.push({
name: 'redo',
action: EasyMDE.redo,
title: translations.toolbar_buttons?.redo,
})
}
return toolbar
},
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
import Trix from 'trix'
Trix.config.blockAttributes.default.tagName = 'p'
Trix.config.blockAttributes.default.breakOnReturn = true
Trix.config.blockAttributes.heading = {
tagName: 'h2',
terminal: true,
breakOnReturn: true,
group: false,
}
Trix.config.blockAttributes.subHeading = {
tagName: 'h3',
terminal: true,
breakOnReturn: true,
group: false,
}
Trix.config.textAttributes.underline = {
style: { textDecoration: 'underline' },
inheritable: true,
parser: (element) => {
const style = window.getComputedStyle(element)
return style.textDecoration.includes('underline')
},
}
Trix.Block.prototype.breaksOnReturn = function () {
const lastAttribute = this.getLastAttribute()
const blockConfig =
Trix.config.blockAttributes[lastAttribute ? lastAttribute : 'default']
return blockConfig?.breakOnReturn ?? false
}
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
if (
this.block.hasAttributes() &&
this.block.isListItem() &&
!this.block.isEmpty()
) {
return this.startLocation.offset > 0
} else {
return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false
}
}
export default function richEditorFormComponent({ state }) {
return {
state,
init: function () {
this.$refs.trixValue.value = this.state
this.$refs.trix.editor?.loadHTML(this.state)
this.$watch('state', () => {
if (document.activeElement === this.$refs.trix) {
return
}
this.$refs.trixValue.value = this.state
this.$refs.trix.editor?.loadHTML(this.state)
})
},
}
}

View File

@@ -0,0 +1,314 @@
import Choices from 'choices.js'
export default function selectFormComponent({
canSelectPlaceholder,
isHtmlAllowed,
getOptionLabelUsing,
getOptionLabelsUsing,
getOptionsUsing,
getSearchResultsUsing,
isAutofocused,
isMultiple,
isSearchable,
hasDynamicOptions,
hasDynamicSearchResults,
livewireId,
loadingMessage,
maxItems,
maxItemsMessage,
noSearchResultsMessage,
options,
optionsLimit,
placeholder,
position,
searchDebounce,
searchingMessage,
searchPrompt,
searchableOptionFields,
state,
statePath,
}) {
return {
isSearching: false,
select: null,
selectedOptions: [],
isStateBeingUpdated: false,
state,
init: async function () {
this.select = new Choices(this.$refs.input, {
allowHTML: isHtmlAllowed,
duplicateItemsAllowed: false,
itemSelectText: '',
loadingText: loadingMessage,
maxItemCount: maxItems ?? -1,
maxItemText: (maxItemCount) =>
window.pluralize(maxItemsMessage, maxItemCount, {
count: maxItemCount,
}),
noChoicesText: searchPrompt,
noResultsText: noSearchResultsMessage,
placeholderValue: placeholder,
position: position ?? 'auto',
removeItemButton: canSelectPlaceholder,
renderChoiceLimit: optionsLimit,
searchEnabled: isSearchable,
searchFields: searchableOptionFields ?? ['label'],
searchPlaceholderValue: searchPrompt,
searchResultLimit: optionsLimit,
shouldSort: false,
searchFloor: hasDynamicSearchResults ? 0 : 1,
})
await this.refreshChoices({ withInitialOptions: true })
if (![null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(this.formatState(this.state))
}
this.refreshPlaceholder()
if (isAutofocused) {
this.select.showDropdown()
}
this.$refs.input.addEventListener('change', () => {
this.refreshPlaceholder()
if (this.isStateBeingUpdated) {
return
}
this.isStateBeingUpdated = true
this.state = this.select.getValue(true) ?? null
this.$nextTick(() => (this.isStateBeingUpdated = false))
})
if (hasDynamicOptions) {
this.$refs.input.addEventListener('showDropdown', async () => {
this.select.clearChoices()
await this.select.setChoices([
{
label: loadingMessage,
value: '',
disabled: true,
},
])
await this.refreshChoices()
})
}
if (hasDynamicSearchResults) {
this.$refs.input.addEventListener('search', async (event) => {
let search = event.detail.value?.trim()
this.isSearching = true
this.select.clearChoices()
await this.select.setChoices([
{
label: [null, undefined, ''].includes(search)
? loadingMessage
: searchingMessage,
value: '',
disabled: true,
},
])
})
this.$refs.input.addEventListener(
'search',
Alpine.debounce(async (event) => {
await this.refreshChoices({
search: event.detail.value?.trim(),
})
this.isSearching = false
}, searchDebounce),
)
}
if (!isMultiple) {
window.addEventListener(
'filament-forms::select.refreshSelectedOptionLabel',
async (event) => {
if (event.detail.livewireId !== livewireId) {
return
}
if (event.detail.statePath !== statePath) {
return
}
await this.refreshChoices({
withInitialOptions: false,
})
},
)
}
this.$watch('state', async () => {
if (!this.select) {
return
}
this.refreshPlaceholder()
if (this.isStateBeingUpdated) {
return
}
await this.refreshChoices({
withInitialOptions: !hasDynamicOptions,
})
})
},
destroy: function () {
this.select.destroy()
this.select = null
},
refreshChoices: async function (config = {}) {
const choices = await this.getChoices(config)
if (!this.select) {
return
}
this.select.clearStore()
this.refreshPlaceholder()
this.setChoices(choices)
if (![null, undefined, ''].includes(this.state)) {
this.select.setChoiceByValue(this.formatState(this.state))
}
},
setChoices: function (choices) {
this.select.setChoices(choices, 'value', 'label', true)
},
getChoices: async function (config = {}) {
const existingOptions = await this.getExistingOptions(config)
return existingOptions.concat(
await this.getMissingOptions(existingOptions),
)
},
getExistingOptions: async function ({ search, withInitialOptions }) {
if (withInitialOptions) {
return options
}
let results = []
if (search !== '' && search !== null && search !== undefined) {
results = await getSearchResultsUsing(search)
} else {
results = await getOptionsUsing()
}
return results.map((result) => {
if (result.choices) {
result.choices = result.choices.map((groupedOption) => {
groupedOption.selected = Array.isArray(this.state)
? this.state.includes(groupedOption.value)
: this.state === groupedOption.value
return groupedOption
})
return result
}
result.selected = Array.isArray(this.state)
? this.state.includes(result.value)
: this.state === result.value
return result
})
},
refreshPlaceholder: function () {
if (isMultiple) {
return
}
this.select._renderItems()
if (![null, undefined, ''].includes(this.state)) {
return
}
this.$el.querySelector('.choices__list--single').innerHTML =
`<div class="choices__placeholder choices__item">${
placeholder ?? ''
}</div>`
},
formatState: function (state) {
if (isMultiple) {
return (state ?? []).map((item) => item?.toString())
}
return state?.toString()
},
getMissingOptions: async function (existingOptions) {
let state = this.formatState(this.state)
if ([null, undefined, '', [], {}].includes(state)) {
return {}
}
const existingOptionValues = new Set()
existingOptions.forEach((existingOption) => {
if (existingOption.choices) {
existingOption.choices.forEach((groupedExistingOption) =>
existingOptionValues.add(groupedExistingOption.value),
)
return
}
existingOptionValues.add(existingOption.value)
})
if (isMultiple) {
if (state.every((value) => existingOptionValues.has(value))) {
return {}
}
return (await getOptionLabelsUsing())
.filter((option) => !existingOptionValues.has(option.value))
.map((option) => {
option.selected = true
return option
})
}
if (existingOptionValues.has(state)) {
return existingOptionValues
}
return [
{
label: await getOptionLabelUsing(),
value: state,
selected: true,
},
]
},
}
}

View File

@@ -0,0 +1,72 @@
export default function tagsInputFormComponent({ state, splitKeys }) {
return {
newTag: '',
state,
createTag: function () {
this.newTag = this.newTag.trim()
if (this.newTag === '') {
return
}
if (this.state.includes(this.newTag)) {
this.newTag = ''
return
}
this.state.push(this.newTag)
this.newTag = ''
},
deleteTag: function (tagToDelete) {
this.state = this.state.filter((tag) => tag !== tagToDelete)
},
reorderTags: function (event) {
const reordered = this.state.splice(event.oldIndex, 1)[0]
this.state.splice(event.newIndex, 0, reordered)
this.state = [...this.state]
},
input: {
['x-on:blur']: 'createTag()',
['x-model']: 'newTag',
['x-on:keydown'](event) {
if (['Enter', ...splitKeys].includes(event.key)) {
event.preventDefault()
event.stopPropagation()
this.createTag()
}
},
['x-on:paste']() {
this.$nextTick(() => {
if (splitKeys.length === 0) {
this.createTag()
return
}
const pattern = splitKeys
.map((key) =>
key.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'),
)
.join('|')
this.newTag
.split(new RegExp(pattern, 'g'))
.forEach((tag) => {
this.newTag = tag
this.createTag()
})
})
},
},
}
}

View File

@@ -0,0 +1,14 @@
export default function textareaFormComponent({ initialHeight }) {
return {
init: function () {
this.render()
},
render: function () {
if (this.$el.scrollHeight > 0) {
this.$el.style.height = initialHeight + 'rem'
this.$el.style.height = this.$el.scrollHeight + 'px'
}
},
}
}