quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
316 lines (260 loc) • 8.04 kB
JavaScript
import { h, ref, computed, getCurrentInstance } from 'vue'
import QChip from '../chip/QChip.js'
import useField, {
useFieldState,
useNonInputFieldProps,
useFieldEmits,
fieldValueIsFilled
} from '../../composables/private.use-field/use-field.js'
import {
useFormProps,
useFormInputNameAttr
} from '../../composables/use-form/private.use-form.js'
import useFile, {
useFileProps,
useFileEmits
} from '../../composables/private.use-file/use-file.js'
import useFileFormDomProps from '../../composables/private.use-file/use-file-dom-props.js'
import { createComponent } from '../../utils/private.create/create.js'
import { humanStorageSize } from '../../utils/format/format.js'
import { prevent } from '../../utils/event/event.js'
import { injectProp } from '../../utils/private.inject-obj-prop/inject-obj-prop.js'
export default createComponent({
name: 'QFile',
inheritAttrs: false,
props: {
...useNonInputFieldProps,
...useFormProps,
...useFileProps,
/* SSR does not know about File & FileList */
modelValue: __QUASAR_SSR_SERVER__ ? {} : [File, FileList, Array],
append: Boolean,
useChips: Boolean,
displayValue: [String, Number],
tabindex: {
type: [String, Number],
default: 0
},
counterLabel: Function,
inputClass: [Array, String, Object],
inputStyle: [Array, String, Object]
},
emits: [...useFieldEmits, ...useFileEmits],
setup(props, { slots, emit, attrs }) {
const { proxy } = getCurrentInstance()
const state = useFieldState()
const inputRef = ref(null)
const dnd = ref(false)
const nameProp = useFormInputNameAttr(props)
const { pickFiles, onDragover, onDragleave, processFiles, getDndNode } =
useFile({
editable: state.editable,
dnd,
getFileInput,
addFilesToQueue
})
const formDomProps = useFileFormDomProps(props)
const innerValue = computed(() =>
Object(props.modelValue) === props.modelValue
? 'length' in props.modelValue
? Array.from(props.modelValue)
: [props.modelValue]
: []
)
const hasValue = computed(() => fieldValueIsFilled(innerValue.value))
const selectedString = computed(() =>
innerValue.value.map(file => file.name).join(', ')
)
const totalSize = computed(() =>
humanStorageSize(
innerValue.value.reduce((acc, file) => acc + file.size, 0)
)
)
const counterProps = computed(() => ({
totalSize: totalSize.value,
filesNumber: innerValue.value.length,
maxFiles: props.maxFiles
}))
const inputAttrs = computed(() => ({
tabindex: -1,
type: 'file',
title: '', // try to remove default tooltip,
accept: props.accept,
capture: props.capture,
name: nameProp.value,
...attrs,
id: state.targetUid.value,
disabled: state.editable.value !== true
}))
const fieldClass = computed(
() =>
'q-file q-field--auto-height' +
(dnd.value === true ? ' q-file--dnd' : '')
)
const isAppending = computed(
() => props.multiple === true && props.append === true
)
function removeAtIndex(index) {
const files = innerValue.value.slice()
files.splice(index, 1)
emitValue(files)
}
function removeFile(file) {
const index = innerValue.value.indexOf(file)
if (index !== -1) {
removeAtIndex(index)
}
}
function emitValue(files) {
emit('update:modelValue', props.multiple === true ? files : files[0])
}
function onKeydown(e) {
// prevent form submit if ENTER is pressed
if (e.keyCode === 13) prevent(e)
}
function onKeyup(e) {
// only on ENTER and SPACE to match native input field
if (e.keyCode === 13 || e.keyCode === 32) {
pickFiles(e)
}
}
function getFileInput() {
return inputRef.value
}
function addFilesToQueue(e, fileList) {
const files = processFiles(
e,
fileList,
innerValue.value,
isAppending.value
)
const fileInput = getFileInput()
if (fileInput !== void 0 && fileInput !== null) {
fileInput.value = ''
}
// if nothing to do...
if (files === void 0) return
// protect against input @change being called in a loop
// like it happens on Safari, so don't emit same thing:
if (
props.multiple === true
? props.modelValue && files.every(f => innerValue.value.includes(f))
: props.modelValue === files[0]
) {
return
}
emitValue(
isAppending.value === true ? innerValue.value.concat(files) : files
)
}
function getFiller() {
return [
h('input', {
class: [props.inputClass, 'q-file__filler'],
style: props.inputStyle
})
]
}
function getSelection() {
if (slots.file !== void 0) {
return innerValue.value.length === 0
? getFiller()
: innerValue.value.map((file, index) =>
slots.file({ index, file, ref: this })
)
}
if (slots.selected !== void 0) {
return innerValue.value.length === 0
? getFiller()
: slots.selected({ files: innerValue.value, ref: this })
}
if (props.useChips === true) {
return innerValue.value.length === 0
? getFiller()
: innerValue.value.map((file, i) =>
h(
QChip,
{
key: 'file-' + i,
removable: state.editable.value,
dense: true,
textColor: props.color,
tabindex: props.tabindex,
onRemove: () => {
removeAtIndex(i)
}
},
() =>
h('span', {
class: 'ellipsis',
textContent: file.name
})
)
)
}
const textContent =
props.displayValue !== void 0
? props.displayValue
: selectedString.value
return textContent.length !== 0
? [
h('div', {
class: props.inputClass,
style: props.inputStyle,
textContent
})
]
: getFiller()
}
function getInput() {
const data = {
ref: inputRef,
...inputAttrs.value,
...formDomProps.value,
class: 'q-field__input fit absolute-full cursor-pointer',
onChange: addFilesToQueue
}
if (props.multiple === true) {
data.multiple = true
}
return h('input', data)
}
Object.assign(state, {
fieldClass,
emitValue,
hasValue,
inputRef,
innerValue,
floatingLabel: computed(
() => hasValue.value === true || fieldValueIsFilled(props.displayValue)
),
computedCounter: computed(() => {
if (props.counterLabel !== void 0) {
return props.counterLabel(counterProps.value)
}
const max = props.maxFiles
return `${innerValue.value.length}${max !== void 0 ? ' / ' + max : ''} (${totalSize.value})`
}),
getControlChild: () => getDndNode('file'),
getControl: () => {
const data = {
ref: state.targetRef,
class: 'q-field__native row items-center cursor-pointer',
tabindex: props.tabindex
}
if (state.editable.value === true) {
Object.assign(data, { onDragover, onDragleave, onKeydown, onKeyup })
}
return h('div', data, [getInput()].concat(getSelection()))
}
})
// expose public methods
Object.assign(proxy, {
removeAtIndex,
removeFile,
getNativeElement: () => inputRef.value // deprecated
})
injectProp(proxy, 'nativeEl', () => inputRef.value)
return useField(state)
}
})