UNPKG

quasar

Version:

Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time

594 lines (506 loc) 14.7 kB
import { h, ref, isRef, computed, watch, provide, onBeforeUnmount, getCurrentInstance } from 'vue' import QBtn from '../btn/QBtn.js' import QIcon from '../icon/QIcon.js' import QSpinner from '../spinner/QSpinner.js' import QCircularProgress from '../circular-progress/QCircularProgress.js' import useDark, { useDarkProps } from '../../composables/private.use-dark/use-dark.js' import useFile, { useFileProps, useFileEmits } from '../../composables/private.use-file/use-file.js' import { stop } from '../../utils/event/event.js' import { humanStorageSize } from '../../utils/format/format.js' import { uploaderKey } from '../../utils/private.symbols/symbols.js' import { injectProp, injectMultipleProps } from '../../utils/private.inject-obj-prop/inject-obj-prop.js' import { vmIsDestroyed } from '../../utils/private.vm/vm.js' function getProgressLabel(p) { return (p * 100).toFixed(2) + '%' } export const coreProps = { ...useDarkProps, ...useFileProps, label: String, color: String, textColor: String, square: Boolean, flat: Boolean, bordered: Boolean, noThumbnails: Boolean, thumbnailFit: { type: String, default: 'cover' }, autoUpload: Boolean, hideUploadBtn: Boolean, disable: Boolean, readonly: Boolean } export const coreEmits = [ ...useFileEmits, 'start', 'finish', 'added', 'removed' ] export function getRenderer(getPlugin, expose) { const vm = getCurrentInstance() const { props, slots, emit, proxy } = vm const { $q } = proxy const isDark = useDark(props, $q) function updateFileStatus(file, status, uploadedSize) { file.__status = status if (status === 'idle') { file.__uploaded = 0 file.__progress = 0 file.__sizeLabel = humanStorageSize(file.size) file.__progressLabel = '0.00%' return } if (status === 'failed') { proxy.$forceUpdate() return } file.__uploaded = status === 'uploaded' ? file.size : uploadedSize file.__progress = status === 'uploaded' ? 1 : Math.min(0.9999, file.__uploaded / file.size) file.__progressLabel = getProgressLabel(file.__progress) proxy.$forceUpdate() } const editable = computed( () => props.disable !== true && props.readonly !== true ) const dnd = ref(false) const rootRef = ref(null) const inputRef = ref(null) const state = { files: ref([]), queuedFiles: ref([]), uploadedFiles: ref([]), uploadedSize: ref(0), updateFileStatus, isAlive: () => vmIsDestroyed(vm) === false } const { pickFiles, addFiles, onDragover, onDragleave, processFiles, getDndNode, maxFilesNumber, maxTotalSizeNumber } = useFile({ editable, dnd, getFileInput, addFilesToQueue }) Object.assign( state, getPlugin({ props, slots, emit, helpers: state, exposeApi: obj => { Object.assign(state, obj) } }) ) if (state.isBusy === void 0) { state.isBusy = ref(false) } const uploadSize = ref(0) const uploadProgress = computed(() => uploadSize.value === 0 ? 0 : state.uploadedSize.value / uploadSize.value ) const uploadProgressLabel = computed(() => getProgressLabel(uploadProgress.value) ) const uploadSizeLabel = computed(() => humanStorageSize(uploadSize.value)) const canAddFiles = computed( () => editable.value === true && state.isUploading.value !== true && // if single selection and no files are queued: (props.multiple === true || state.queuedFiles.value.length === 0) && // if max-files is set and current number of files does not exceeds it: (props.maxFiles === void 0 || state.files.value.length < maxFilesNumber.value) && // if max-total-size is set and current upload size does not exceeds it: (props.maxTotalSize === void 0 || uploadSize.value < maxTotalSizeNumber.value) ) const canUpload = computed( () => editable.value === true && state.isBusy.value !== true && state.isUploading.value !== true && state.queuedFiles.value.length !== 0 ) provide(uploaderKey, renderInput) const classes = computed( () => 'q-uploader column no-wrap' + (isDark.value === true ? ' q-uploader--dark q-dark' : '') + (props.bordered === true ? ' q-uploader--bordered' : '') + (props.square === true ? ' q-uploader--square no-border-radius' : '') + (props.flat === true ? ' q-uploader--flat no-shadow' : '') + (props.disable === true ? ' disabled q-uploader--disable' : '') + (dnd.value === true ? ' q-uploader--dnd' : '') ) const colorClass = computed( () => 'q-uploader__header' + (props.color !== void 0 ? ` bg-${props.color}` : '') + (props.textColor !== void 0 ? ` text-${props.textColor}` : '') ) watch(state.isUploading, (newVal, oldVal) => { if (oldVal === false && newVal === true) { emit('start') } else if (oldVal === true && newVal === false) { emit('finish') } }) function reset() { if (props.disable === false) { state.abort() state.uploadedSize.value = 0 uploadSize.value = 0 revokeImgURLs() state.files.value = [] state.queuedFiles.value = [] state.uploadedFiles.value = [] } } function removeUploadedFiles() { if (props.disable === false) { batchRemoveFiles(['uploaded'], () => { state.uploadedFiles.value = [] }) } } function removeQueuedFiles() { batchRemoveFiles(['idle', 'failed'], ({ size }) => { uploadSize.value -= size state.queuedFiles.value = [] }) } function batchRemoveFiles(statusList, cb) { if (props.disable === true) return const removed = { files: [], size: 0 } const localFiles = state.files.value.filter(f => { if (statusList.indexOf(f.__status) === -1) { return true } removed.size += f.size removed.files.push(f) if (f.__img !== void 0) window.URL.revokeObjectURL(f.__img.src) return false }) if (removed.files.length !== 0) { state.files.value = localFiles cb(removed) emit('removed', removed.files) } } function removeFile(file) { if (props.disable) return if (file.__status === 'uploaded') { state.uploadedFiles.value = state.uploadedFiles.value.filter( f => f.__key !== file.__key ) } else if (file.__status === 'uploading') { file.__abort() } else { uploadSize.value -= file.size } state.files.value = state.files.value.filter(f => { if (f.__key !== file.__key) { return true } if (f.__img !== void 0) window.URL.revokeObjectURL(f.__img.src) return false }) state.queuedFiles.value = state.queuedFiles.value.filter( f => f.__key !== file.__key ) emit('removed', [file]) } function revokeImgURLs() { state.files.value.forEach(f => { if (f.__img !== void 0) window.URL.revokeObjectURL(f.__img.src) }) } function getFileInput() { return ( inputRef.value || rootRef.value.getElementsByClassName('q-uploader__input')[0] ) } function addFilesToQueue(e, fileList) { const localFiles = processFiles(e, fileList, state.files.value, true) const fileInput = getFileInput() if (fileInput !== void 0 && fileInput !== null) { fileInput.value = '' } if (localFiles === void 0) return localFiles.forEach(file => { state.updateFileStatus(file, 'idle') uploadSize.value += file.size if ( props.noThumbnails !== true && file.type.toUpperCase().startsWith('IMAGE') ) { const img = new Image() img.src = window.URL.createObjectURL(file) file.__img = img } }) state.files.value = state.files.value.concat(localFiles) state.queuedFiles.value = state.queuedFiles.value.concat(localFiles) emit('added', localFiles) if (props.autoUpload === true) state.upload() } function upload() { if (canUpload.value === true) state.upload() } function getBtn(show, icon, fn) { if (show === true) { const data = { type: 'a', key: icon, icon: $q.iconSet.uploader[icon], flat: true, dense: true } let child = void 0 if (icon === 'add') { data.onClick = pickFiles child = renderInput } else { data.onClick = fn } return h(QBtn, data, child) } } function renderInput() { return h('input', { ref: inputRef, class: 'q-uploader__input overflow-hidden absolute-full', tabindex: -1, type: 'file', title: '', // try to remove default tooltip accept: props.accept, multiple: props.multiple === true ? 'multiple' : void 0, capture: props.capture, onMousedown: stop, // need to stop refocus from QBtn onClick: pickFiles, onChange: addFilesToQueue }) } function getHeader() { if (slots.header !== void 0) { return slots.header(publicApi) } return [ h( 'div', { class: 'q-uploader__header-content column' }, [ h( 'div', { class: 'flex flex-center no-wrap q-gutter-xs' }, [ getBtn( state.queuedFiles.value.length !== 0, 'removeQueue', removeQueuedFiles ), getBtn( state.uploadedFiles.value.length !== 0, 'removeUploaded', removeUploadedFiles ), state.isUploading.value === true ? h(QSpinner, { class: 'q-uploader__spinner' }) : null, h('div', { class: 'col column justify-center' }, [ props.label !== void 0 ? h('div', { class: 'q-uploader__title' }, [props.label]) : null, h('div', { class: 'q-uploader__subtitle' }, [ uploadSizeLabel.value + ' / ' + uploadProgressLabel.value ]) ]), getBtn(canAddFiles.value, 'add'), getBtn( props.hideUploadBtn === false && canUpload.value === true, 'upload', state.upload ), getBtn(state.isUploading.value, 'clear', state.abort) ] ) ] ) ] } function getList() { if (slots.list !== void 0) { return slots.list(publicApi) } return state.files.value.map(file => h( 'div', { key: file.__key, class: 'q-uploader__file relative-position' + (props.noThumbnails !== true && file.__img !== void 0 ? ' q-uploader__file--img' : '') + (file.__status === 'failed' ? ' q-uploader__file--failed' : file.__status === 'uploaded' ? ' q-uploader__file--uploaded' : ''), style: props.noThumbnails !== true && file.__img !== void 0 ? { backgroundImage: 'url("' + file.__img.src + '")', backgroundSize: props.thumbnailFit } : null }, [ h( 'div', { class: 'q-uploader__file-header row flex-center no-wrap' }, [ file.__status === 'failed' ? h(QIcon, { class: 'q-uploader__file-status', name: $q.iconSet.type.negative, color: 'negative' }) : null, h('div', { class: 'q-uploader__file-header-content col' }, [ h('div', { class: 'q-uploader__title' }, [file.name]), h( 'div', { class: 'q-uploader__subtitle row items-center no-wrap' }, [file.__sizeLabel + ' / ' + file.__progressLabel] ) ]), file.__status === 'uploading' ? h(QCircularProgress, { value: file.__progress, min: 0, max: 1, indeterminate: file.__progress === 0 }) : h(QBtn, { round: true, dense: true, flat: true, icon: $q.iconSet.uploader[ file.__status === 'uploaded' ? 'done' : 'clear' ], onClick: () => { removeFile(file) } }) ] ) ] ) ) } onBeforeUnmount(() => { if (state.isUploading.value === true) state.abort() if (state.files.value.length !== 0) revokeImgURLs() }) const publicApi = {} for (const key in state) { if (isRef(state[key]) === true) { injectProp(publicApi, key, () => state[key].value) } else { // method or non-computed prop publicApi[key] = state[key] } } Object.assign(publicApi, { upload, reset, removeUploadedFiles, removeQueuedFiles, removeFile, pickFiles, addFiles }) injectMultipleProps(publicApi, { canAddFiles: () => canAddFiles.value, canUpload: () => canUpload.value, uploadSizeLabel: () => uploadSizeLabel.value, uploadProgressLabel: () => uploadProgressLabel.value }) // expose public api (methods & computed props) expose({ ...state, upload, reset, removeUploadedFiles, removeQueuedFiles, removeFile, pickFiles, addFiles, canAddFiles, canUpload, uploadSizeLabel, uploadProgressLabel }) return () => { const children = [ h('div', { class: colorClass.value }, getHeader()), h('div', { class: 'q-uploader__list scroll' }, getList()), getDndNode('uploader') ] if (state.isBusy.value === true) { children.push( h( 'div', { class: 'q-uploader__overlay absolute-full flex flex-center' }, [h(QSpinner)] ) ) } const data = { ref: rootRef, class: classes.value } if (canAddFiles.value === true) { Object.assign(data, { onDragover, onDragleave }) } return h('div', data, children) } }