UNPKG

@uploadcare/blocks

Version:

Building blocks for Uploadcare products integration

455 lines (397 loc) 13.1 kB
import { UploaderBlock } from '../../abstract/UploaderBlock.js'; import { ActivityBlock } from '../../abstract/ActivityBlock.js'; import { generateThumb } from '../utils/resizeImage.js'; import { uploadFile } from '@uploadcare/upload-client'; import { UiMessage } from '../MessageBox/MessageBox.js'; import { fileCssBg } from '../svg-backgrounds/svg-backgrounds.js'; import { createCdnUrl, createCdnUrlModifiers, createOriginalUrl } from '../../utils/cdn-utils.js'; import { EVENT_TYPES, EventData, EventManager } from '../../abstract/EventManager.js'; import { debounce } from '../utils/debounce.js'; import { IMAGE_ACCEPT_LIST, mergeFileTypes, matchFileType } from '../../utils/fileTypes.js'; const FileItemState = { FINISHED: Symbol(0), FAILED: Symbol(1), UPLOADING: Symbol(2), IDLE: Symbol(3), }; export class FileItem extends UploaderBlock { pauseRender = true; /** @private */ _entrySubs = new Set(); /** @private */ _entry = null; /** @private */ _isIntersecting = false; /** @private */ _debouncedGenerateThumb = debounce(this._generateThumbnail.bind(this), 100); /** @private */ _debouncedCalculateState = debounce(this._calculateState.bind(this), 0); /** @private */ _renderedOnce = false; init$ = { ...this.ctxInit, uid: '', itemName: '', thumbUrl: '', progressValue: 0, progressVisible: false, progressUnknown: false, notImage: true, badgeIcon: '', isFinished: false, isFailed: false, isUploading: false, isFocused: false, state: FileItemState.IDLE, '*uploadTrigger': null, onEdit: () => { this.set$({ '*focusedEntry': this._entry, '*currentActivity': ActivityBlock.activities.DETAILS, }); }, onRemove: () => { let entryUuid = this._entry.getValue('uuid'); if (entryUuid) { let data = this.getOutputData((dataItem) => { return dataItem.getValue('uuid') === entryUuid; }); EventManager.emit( new EventData({ type: EVENT_TYPES.REMOVE, ctx: this.ctxName, data, }) ); } this.uploadCollection.remove(this.$.uid); }, onUpload: () => { this.upload(); }, }; _reset() { for (let sub of this._entrySubs) { sub.remove(); } this._debouncedGenerateThumb.cancel(); this._entrySubs = new Set(); this._entry = null; this._isIntersecting = false; } /** @private */ _observerCallback(entries) { let [entry] = entries; this._isIntersecting = entry.isIntersecting; if (entry.isIntersecting && !this._renderedOnce) { this.render(); this._renderedOnce = true; } if (entry.intersectionRatio === 0) { this._debouncedGenerateThumb.cancel(); } else { this._debouncedGenerateThumb(); } } /** @private */ _calculateState() { let entry = this._entry; let state = FileItemState.IDLE; if (entry.getValue('uploadError') || entry.getValue('validationErrorMsg')) { state = FileItemState.FAILED; } else if (entry.getValue('isUploading')) { state = FileItemState.UPLOADING; } else if (entry.getValue('uuid')) { state = FileItemState.FINISHED; } if (this.$.state !== state) { this.$.state = state; } } /** @private */ async _generateThumbnail() { let entry = this._entry; if (entry.getValue('uuid') && entry.getValue('isImage')) { let size = this.getCssData('--cfg-thumb-size') || 76; let thumbUrl = this.proxyUrl( createCdnUrl( createOriginalUrl(this.getCssData('--cfg-cdn-cname'), this._entry.getValue('uuid')), createCdnUrlModifiers(entry.getValue('cdnUrlModifiers'), `scale_crop/${size}x${size}/center`) ) ); let blobSrc = entry.getValue('thumbUrl'); entry.setValue('thumbUrl', thumbUrl); URL.revokeObjectURL(blobSrc); return; } if (entry.getValue('thumbUrl')) { return; } if (entry.getValue('file')?.type.includes('image')) { try { let thumbUrl = await generateThumb(entry.getValue('file'), this.getCssData('--cfg-thumb-size') || 76); entry.setValue('thumbUrl', thumbUrl); } catch (err) { let color = window.getComputedStyle(this).getPropertyValue('--clr-generic-file-icon'); entry.setValue('thumbUrl', fileCssBg(color)); } } else { let color = window.getComputedStyle(this).getPropertyValue('--clr-generic-file-icon'); entry.setValue('thumbUrl', fileCssBg(color)); } } /** * @private * @param {'success' | 'error'} type * @param {String} caption * @param {String} text */ _showMessage(type, caption, text) { let msg = new UiMessage(); msg.caption = caption; msg.text = text; msg.isError = type === 'error'; this.set$({ badgeIcon: `badge-${type}`, '*message': msg, }); } _subEntry(prop, handler) { let sub = this._entry.subscribe(prop, handler); this._entrySubs.add(sub); } /** @param {String} id */ _handleEntryId(id) { this._reset(); /** @type {import('../../abstract/TypedData.js').TypedData} */ let entry = this.uploadCollection?.read(id); this._entry = entry; if (!entry) { return; } this._subEntry('validationErrorMsg', (validationErrorMsg) => { this._debouncedCalculateState(); if (!validationErrorMsg) { return; } let caption = this.l10n('validation-error') + ': ' + (entry.getValue('file')?.name || entry.getValue('externalUrl')); this._showMessage('error', caption, validationErrorMsg); }); this._subEntry('uploadError', (uploadError) => { this._debouncedCalculateState(); if (!uploadError) { return; } let caption = this.l10n('upload-error') + ': ' + (entry.getValue('file')?.name || entry.getValue('externalUrl')); this._showMessage('error', caption, uploadError.message); }); this._subEntry('isUploading', () => { this._debouncedCalculateState(); }); this._subEntry('uploadProgress', (uploadProgress) => { this.$.progressValue = uploadProgress; }); this._subEntry('fileName', (name) => { this.$.itemName = name || entry.getValue('externalUrl') || this.l10n('file-no-name'); }); this._subEntry('fileSize', (fileSize) => { let maxFileSize = this.getCssData('--cfg-max-local-file-size-bytes'); if (maxFileSize && fileSize && fileSize > maxFileSize) { entry.setValue('validationErrorMsg', this.l10n('files-max-size-limit-error', { maxFileSize })); } }); this._subEntry('mimeType', (mimeType) => { if (!mimeType) { return; } let imagesOnly = this.getCssData('--cfg-img-only'); let accept = this.getCssData('--cfg-accept'); let allowedFileTypes = mergeFileTypes([...(imagesOnly ? IMAGE_ACCEPT_LIST : []), accept]); if (allowedFileTypes.length > 0 && !matchFileType(mimeType, allowedFileTypes)) { entry.setValue('validationErrorMsg', this.l10n('file-type-not-allowed')); } }); this._subEntry('isImage', (isImage) => { let imagesOnly = this.getCssData('--cfg-img-only'); if (entry.getValue('externalUrl') && !entry.getValue('uuid') && imagesOnly && !isImage) { // don't validate not uploaded files with external url, cause we don't know if they're images or not return; } if (imagesOnly && !isImage) { entry.setValue('validationErrorMsg', this.l10n('images-only-accepted')); } }); this._subEntry('externalUrl', (externalUrl) => { this.$.itemName = entry.getValue('fileName') || externalUrl || this.l10n('file-no-name'); }); this._subEntry('uuid', (uuid) => { this._debouncedCalculateState(); if (uuid) { this._debouncedGenerateThumb(); } }); this._subEntry('cdnUrlModifiers', () => { this._debouncedGenerateThumb(); }); this._subEntry('thumbUrl', (thumbUrl) => { this.$.thumbUrl = thumbUrl ? `url(${thumbUrl})` : ''; }); if (!this.getCssData('--cfg-confirm-upload')) { this.upload(); } if (this._isIntersecting) { this._debouncedGenerateThumb(); } } initCallback() { super.initCallback(); this.sub('uid', (uid) => { this._handleEntryId(uid); }); this.sub('state', (state) => { this.set$({ isFailed: state === FileItemState.FAILED, isUploading: state === FileItemState.UPLOADING, isFinished: state === FileItemState.FINISHED, progressVisible: state === FileItemState.UPLOADING, }); if (state === FileItemState.FAILED) { this.$.badgeIcon = 'badge-error'; } else if (state === FileItemState.FINISHED) { this.$.badgeIcon = 'badge-success'; } if (state === FileItemState.UPLOADING) { this.$.isFocused = false; } else { this.$.progressValue = 0; } }); this.onclick = () => { FileItem.activeInstances.forEach((inst) => { if (inst === this) { inst.setAttribute('focused', ''); } else { inst.removeAttribute('focused'); } }); }; this.$['*uploadTrigger'] = null; this.sub('*uploadTrigger', (val) => { if (!val || !this.isConnected) { return; } this.upload(); }); FileItem.activeInstances.add(this); } destroyCallback() { super.destroyCallback(); FileItem.activeInstances.delete(this); this._reset(); } connectedCallback() { super.connectedCallback(); /** @private */ this._observer = new window.IntersectionObserver(this._observerCallback.bind(this), { root: this.parentElement, rootMargin: '50% 0px 50% 0px', threshold: [0, 1], }); this._observer.observe(this); } disconnectedCallback() { super.disconnectedCallback(); this._debouncedGenerateThumb.cancel(); this._observer?.disconnect(); } async upload() { let entry = this._entry; if (entry.getValue('uuid') || entry.getValue('isUploading') || entry.getValue('validationErrorMsg')) { return; } let data = this.getOutputData((dataItem) => { return !dataItem.getValue('uuid'); }); EventManager.emit( new EventData({ type: EVENT_TYPES.UPLOAD_START, ctx: this.ctxName, data, }) ); this._debouncedCalculateState(); entry.setValue('isUploading', true); entry.setValue('uploadError', null); entry.setValue('validationErrorMsg', null); if (!entry.getValue('file') && entry.getValue('externalUrl')) { this.$.progressUnknown = true; } try { let abortController = new AbortController(); entry.setValue('abortController', abortController); let fileInfo = await uploadFile(entry.getValue('file') || entry.getValue('externalUrl'), { ...this.getUploadClientOptions(), fileName: entry.getValue('fileName'), onProgress: (progress) => { if (progress.isComputable) { let percentage = progress.value * 100; entry.setValue('uploadProgress', percentage); } this.$.progressUnknown = !progress.isComputable; }, signal: abortController.signal, }); if (entry === this._entry) { this._debouncedCalculateState(); } entry.setMultipleValues({ fileInfo, isUploading: false, fileName: fileInfo.name, fileSize: fileInfo.size, isImage: fileInfo.isImage, mimeType: fileInfo.mimeType, uuid: fileInfo.uuid, cdnUrl: fileInfo.cdnUrl, }); } catch (error) { entry.setValue('abortController', null); entry.setValue('isUploading', false); entry.setValue('uploadProgress', 0); if (entry === this._entry) { this._debouncedCalculateState(); } if (!error?.isCancel) { entry.setValue('uploadError', error); } } } } FileItem.template = /* HTML */ ` <div class="inner" set="@finished: isFinished; @uploading: isUploading; @failed: isFailed; @focused: isFocused"> <div class="thumb" set="style.backgroundImage: thumbUrl"> <div class="badge"> <lr-icon set="@name: badgeIcon"></lr-icon> </div> </div> <div class="file-name-wrapper"> <span class="file-name" set="@title: itemName">{{itemName}}</span> </div> <button type="button" class="edit-btn" set="onclick: onEdit;"> <lr-icon name="edit-file"></lr-icon> </button> <button type="button" class="remove-btn" set="onclick: onRemove;"> <lr-icon name="remove-file"></lr-icon> </button> <button type="button" class="upload-btn" set="onclick: onUpload;"> <lr-icon name="upload"></lr-icon> </button> <lr-progress-bar class="progress-bar" set="value: progressValue; visible: progressVisible; unknown: progressUnknown" > </lr-progress-bar> </div> `; FileItem.activeInstances = new Set();