/* eslint-disable ts/no-this-alias */ /* eslint-disable no-var */ /* eslint-disable vars-on-top */ import Sortable from 'sortablejs'; import { Prop, toNative } from 'vue-facing-decorator'; import { globalState } from '../../app/global-state'; import PowerduckState from '../../app/powerduck-state'; import TsxComponent, { Component } from '../../app/vuetsx'; import { remove, sortBy } from '../../common/extensions/array-extensions'; import StringUtils from '../../common/utils/string-utils'; import Button from '../button/button'; import { ButtonLayout } from '../button/button-layout'; // Side-effect import: Dropzone's UMD attaches the class to window.Dropzone AND registers // $.fn.dropzone. Vite 8 / Rolldown's CJS-ESM interop strips static methods (createElement) // and the jQuery plugin from the default import, so we read the class off powerduck's // globalState (window in browser, SSR-safe shim in Node) instead. import 'dropzone'; import 'dropzone/dist/min/dropzone.min.css'; import './css/gallery-dropzone.css'; const Dropzone: any = (globalState as any).Dropzone; interface DropzoneFormItem { name: string; value: string; } interface DropzoneGalleryArgs { items: DropzoneGalleryItem[]; showUploadButton?: boolean; changed?: (items: DropzoneGalleryItem[]) => void; removedFile?: (file: File) => void; uploadUrl: string; headers?: any; resizeBeforeUpload?: DropzoneResizeBeforeUploadArgs; parseNewItemFromResponse?: (resp: any) => DropzoneGalleryItem; formArr?: DropzoneFormItem[]; allowedExtensions?: string[]; errInvalidFileMessage: string; defaultMessage: string; errorHandler: (err: string) => void; } interface DropzoneResizeBeforeUploadArgs { enabled: boolean; maxSize: number; quality?: number; // 0.0 - 1.0 } export interface DropzoneGalleryItem { Id: number; ImageUrl: string; SortOrder: number; } @Component class DropzoneGalleryComponent extends TsxComponent implements DropzoneGalleryArgs { instance: any; @Prop() items: DropzoneGalleryItem[]; @Prop() errorHandler: (err: string) => void; @Prop() uploadUrl: string; @Prop() headers: any; @Prop({ default: true }) showUploadButton?: boolean; @Prop() resizeBeforeUpload?: DropzoneResizeBeforeUploadArgs; @Prop() formArr?: DropzoneFormItem[]; @Prop() allowedExtensions?: string[]; @Prop() parseNewItemFromResponse?: (resp: any) => DropzoneGalleryItem; @Prop() defaultMessage: string; @Prop() errInvalidFileMessage: string; @Prop() changed?: (items: DropzoneGalleryItem[]) => void; @Prop() removedFile?: (file: File) => void; mounted() { this.bindScript(); } beforeUnmount() { try { (this.$refs.galleryDropzone as any).dropzone.destroy(); } catch (error) { } } updateSortOrder(itemArr?: DropzoneGalleryItem[]) { let applySort = false; if (itemArr == null) { applySort = true; itemArr = (itemArr || [...this.items])[sortBy](p => p.SortOrder); } itemArr.forEach((item, i) => { if (item != null) { item.SortOrder = i; } }); if (applySort) { itemArr = itemArr[sortBy](p => p.SortOrder); } if (this.changed != null) { this.changed(itemArr); } } bindScript() { const mySelf = this; // Use Dropzone's native constructor instead of the jQuery plugin form so // this component no longer needs jQuery at runtime. Both code paths hit the // same underlying Dropzone class. this.instance = new Dropzone(this.$refs.galleryDropzone as HTMLElement, { // autoProcessQueue: true, url: mySelf.uploadUrl, headers: mySelf.headers, previewTemplate: '
\n
\n
\n
\n
\n
\n
\n
\n
\n \n Check\n \n \n \n \n \n
\n
\n \n Error\n \n \n \n \n \n \n \n
\n
', maxFilesize: 5, // MB accept(file, done) { const _this = this; const extension = file.name.split('.').pop()?.toLowerCase(); const allowedExtensions = mySelf.allowedExtensions || [ 'jpg', 'jpeg', 'png', 'webp', ]; if (extension != null && allowedExtensions.includes(extension)) { done(); } else { done(`${file.name} - ${mySelf.errInvalidFileMessage}`); _this.removeFile(file); } }, thumbnail: function thumbnail( file, dataUrl, sortOrder, ) { if (file.previewElement) { file.previewElement.classList.remove('dz-file-preview'); // eslint-disable-next-line ts/no-redeclare for (var _iterator6 = file.previewElement.querySelectorAll('[data-dz-thumbnail]'), _isArray6 = true, _i6 = 0, _iterator6 = _isArray6 ? _iterator6 : _iterator6[Symbol.iterator](); ;) { var _ref5; if (_isArray6) { if (_i6 >= _iterator6.length) { break; } _ref5 = _iterator6[_i6++]; } else { _i6 = _iterator6.next(); if ((_i6 as any).done) { break; } _ref5 = (_i6 as any).value; } const thumbnailElement = _ref5; sortOrder = mySelf.items.length + 1; thumbnailElement.setAttribute('sortOrder', sortOrder); thumbnailElement.style.width = '100%'; thumbnailElement.style.height = '100%'; thumbnailElement.style.backgroundImage = `url('${dataUrl}')`; thumbnailElement.style.backgroundSize = 'cover'; thumbnailElement.style.backgroundRepeat = 'no-repeat'; thumbnailElement.style.backgroundPosition = 'center center'; } return setTimeout(() => { return file.previewElement.classList.add('dz-image-preview'); }, 1); } }, init() { const addFile = this.addFile; this.addFile = async (file: File) => { const extension = file.name.split('.').pop()?.toLowerCase(); const isSvg = file.type === 'image/svg+xml' || extension === 'svg'; if (mySelf.resizeBeforeUpload?.enabled == true && !isSvg) { const result = await scaleImageBeforeUpload( file, mySelf.resizeBeforeUpload.maxSize, mySelf.resizeBeforeUpload.quality, ); if (result.resized) { addFile.call(this, result.fileBlob); } else { addFile.call(this, file); } } else { addFile.call(this, file); } }; this.on('addedfile', function (this: any, file) { // hide display initial dz-message (document.querySelector('.dz-message') as HTMLElement).style.display = 'none'; // Create the remove button const removeButton = Dropzone.createElement('X'); // var removeButton = Dropzone.createElement(''); const _this = this; // remove image from dropzone and adjust new sortorder attribute removeButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); let removeItem; const matchingArr = mySelf.items.filter(p => p.ImageUrl == file.url); if (matchingArr.length == 1) { removeItem = matchingArr[0]; } else { removeItem = mySelf.items.find((item) => { if ((item as any)._uuid != null) { return (item as any)._uuid == file.uuid; } else if (item.Id > 0) { return item.Id == file.uuid; } else { return false; } }); } mySelf.items.splice(mySelf.items.indexOf(removeItem), 1); // Remove the file preview. _this.removeFile(file); mySelf.removedFile(removeItem); mySelf.updateSortOrder(); // show init dropzone message only if there is no image if (document.getElementsByClassName('dz-preview').length == 0) { (document.querySelector('.dz-message') as HTMLElement).style.display = 'block'; } else { (document.querySelector('.dz-message') as HTMLElement).style.display = 'none'; } }); // Add the button to the file preview element. file.previewElement.getElementsByClassName('dz-details')[0].appendChild(removeButton); }); this.on('success', (file, response) => { let newItem: DropzoneGalleryItem; if (mySelf.parseNewItemFromResponse != null) { newItem = mySelf.parseNewItemFromResponse(response); } if (newItem == null) { newItem = { Id: 0, ImageUrl: response, SortOrder: mySelf.items.length + 1, }; } (newItem as any)._uuid = (Math.floor(Math.random() * 2000000000)) * (-1); file.uuid = (newItem as any)._uuid; mySelf.items.push(newItem); if (mySelf.changed != null) { mySelf.changed(mySelf.items); } }); this.on('error', ( _file, error, _xhr, ) => { mySelf.errorHandler(error); }); const self = this; // initialization of existing images into dropzone area if (mySelf.items != null && mySelf.items.length > 0) { const eventGallery = [...mySelf.items][sortBy](p => p.SortOrder); for (let i = 0; i < eventGallery.length; i++) { let id = eventGallery[i].Id; if (id < 1 && (eventGallery[i] as any)._uuid == null) { (eventGallery[i] as any)._uuid = (Math.floor(Math.random() * 2000000000)) * (-1); id = (eventGallery[i] as any)._uuid; } const mock = { name: eventGallery[i].Id, size: 12345, type: 'image/jpeg', url: eventGallery[i].ImageUrl, sortOrder: eventGallery[i].SortOrder, uuid: id, }; self.emit('addedfile', mock); self.emit( 'thumbnail', mock, mock.url, mock.sortOrder, ); self.emit('complete', mock); } (document.querySelector('.dz-message') as HTMLElement).style.display = 'none'; } else { (document.querySelector('.dz-message') as HTMLElement).style.display = 'block'; } }, }); // eslint-disable-next-line no-new new Sortable(this.$refs.galleryDropzone, { animation: 150, onEnd: (evt) => { const oldIndex = evt.oldIndex - 2; const newIndex = evt.newIndex - 2; const itemArr = [...this.items][sortBy](p => p.SortOrder); const item = itemArr[oldIndex]; itemArr[remove](item); itemArr.splice( newIndex, 0, item, ); this.updateSortOrder(itemArr); }, }); } render(h) { return ( ); } } // eslint-disable-next-line func-style export async function scaleImageBeforeUpload( file: File, maxSize: number, quality?: number, ): Promise<{ resized: boolean; fileBlob: File }> { // ensure the file is an image if (!file.type.match(/image.*/)) { return { resized: false, fileBlob: null, }; } const image = new Image(); image.src = URL.createObjectURL(file); await new Promise(res => (image.onload = res)); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d', { alpha: true }); let width = image.width; let height = image.height; if (width > height) { if (width > maxSize) { height *= maxSize / width; width = maxSize; } else { return { resized: false, fileBlob: null, }; } } else { if (height > maxSize) { width *= maxSize / height; height = maxSize; } else { return { resized: false, fileBlob: null, }; } } canvas.width = width; canvas.height = height; context.drawImage( image, 0, 0, width, height, ); let blob: Blob; if (quality != null) { blob = await new Promise(res => canvas.toBlob( res, file.type, quality, )); } else { blob = await new Promise(res => canvas.toBlob(res)); } (blob as any).name = StringUtils.normalizeFileName(file.name); // eslint-disable-next-line no-restricted-syntax (blob as any).lastModifiedDate = new Date(); (blob as any).lastModified = (blob as any).lastModifiedDate.getTime(); return { resized: true, fileBlob: blob as any, }; } const DropzoneGallery = toNative(DropzoneGalleryComponent); export default DropzoneGallery;