File: /home/posscale/www/printmanager/vendor/filament/forms/resources/js/components/file-upload.js
import * as FilePond from 'filepond'
import Cropper from 'cropperjs'
import mime from 'mime'
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,
isPasteable,
isPreviewable,
isReorderable,
itemPanelAspectRatio,
loadingIndicatorPosition,
locale,
maxFiles,
maxSize,
minSize,
maxParallelUploads,
mimeTypeMap,
panelAspectRatio,
panelLayout,
placeholder,
removeUploadedFileButtonPosition,
removeUploadedFileUsing,
reorderUploadedFilesUsing,
shouldAppendFiles,
shouldOrientImageFromExif,
shouldTransformImage,
state,
uploadButtonPosition,
uploadingMessage,
uploadProgressIndicatorPosition,
uploadUsing,
}) {
return {
fileKeyIndex: {},
pond: null,
shouldUpdateState: true,
state,
lastState: null,
error: 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: isPasteable,
allowRemove: isDeletable,
allowReorder: isReorderable,
allowImagePreview: isPreviewable,
allowVideoPreview: isPreviewable,
allowAudioPreview: isPreviewable,
allowImageTransform: shouldTransformImage,
credits: false,
files: await this.getFiles(),
imageCropAspectRatio,
imagePreviewHeight,
imageResizeTargetHeight,
imageResizeTargetWidth,
imageResizeMode,
imageResizeUpscale,
imageTransformOutputStripImageHead: false,
itemInsertLocation: shouldAppendFiles ? 'after' : 'before',
...(placeholder && { labelIdle: placeholder }),
maxFiles,
maxFileSize: maxSize,
minFileSize: minSize,
...(maxParallelUploads && { maxParallelUploads }),
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(),
},
fileValidateTypeDetectType: (source, detectedType) => {
return new Promise((resolve, reject) => {
const extension = source.name
.split('.')
.pop()
.toLowerCase()
const mimeType =
mimeTypeMap[extension] ||
detectedType ||
mime.getType(extension)
mimeType ? resolve(mimeType) : reject()
})
},
})
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)
if (panelLayout === 'compact circle') {
// The compact circle layout does not have enough space to render an error message inside the input.
// As such, we need to display the error message outside of the input, using the `error` Alpine.js
// property that is output as a message in the field's view.
this.pond.on('error', (error) => {
// FilePond has a weird English translation for the error message when a file of an unexpected
// type is uploaded, for example: `File of invalid type: Expects or image/*`. This is a
// hacky workaround to fix the message to be `File of invalid type: Expects image/*`.
this.error = `${error.main}: ${error.sub}`.replace(
'Expects or',
'Expects',
)
})
this.pond.on('removefile', () => (this.error = null))
}
},
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 am from 'filepond/locale/am-et'
import ar from 'filepond/locale/ar-ar'
import az from 'filepond/locale/az-az'
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 el from 'filepond/locale/el-el'
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 he from 'filepond/locale/he-he'
import hr from 'filepond/locale/hr-hr'
import hu from 'filepond/locale/hu-hu'
import id from 'filepond/locale/id-id'
import it from 'filepond/locale/it-it'
import ja from 'filepond/locale/ja-ja'
import km from 'filepond/locale/km-km'
import ko from 'filepond/locale/ko-kr'
import lt from 'filepond/locale/lt-lt'
import lv from 'filepond/locale/lv-lv'
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 sk from 'filepond/locale/sk-sk'
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 = {
am,
ar,
az,
ca,
ckb,
cs,
da,
de,
el,
en,
es,
fa,
fi,
fr,
he,
hr,
hu,
id,
it,
ja,
km,
ko,
lt,
lv,
nl,
no,
pl,
pt_BR,
pt_PT,
ro,
ru,
sk,
sv,
tr,
uk,
vi,
zh_CN,
zh_TW,
}