UNPKG

vuetify

Version:

Vue Material Component Framework

287 lines (254 loc) 7.86 kB
// Styles import './VFileInput.sass' // Extensions import VTextField from '../VTextField' // Components import { VChip } from '../VChip' // Types import { PropValidator } from 'vue/types/options' // Utilities import { deepEqual, humanReadableFileSize, wrapInArray } from '../../util/helpers' import { consoleError } from '../../util/console' import { mergeStyles } from '../../util/mergeData' export default VTextField.extend({ name: 'v-file-input', model: { prop: 'value', event: 'change', }, props: { chips: Boolean, clearable: { type: Boolean, default: true, }, counterSizeString: { type: String, default: '$vuetify.fileInput.counterSize', }, counterString: { type: String, default: '$vuetify.fileInput.counter', }, hideInput: Boolean, placeholder: String, prependIcon: { type: String, default: '$file', }, readonly: { type: Boolean, default: false, }, showSize: { type: [Boolean, Number], default: false, validator: (v: boolean | number) => { return ( typeof v === 'boolean' || [1000, 1024].includes(v) ) }, } as PropValidator<boolean | 1000 | 1024>, smallChips: Boolean, truncateLength: { type: [Number, String], default: 22, }, type: { type: String, default: 'file', }, value: { default: undefined, validator: val => { return wrapInArray(val).every(v => v != null && typeof v === 'object') }, } as PropValidator<File | File[]>, }, computed: { classes (): object { return { ...VTextField.options.computed.classes.call(this), 'v-file-input': true, } }, computedCounterValue (): string { const fileCount = (this.isMultiple && this.lazyValue) ? this.lazyValue.length : (this.lazyValue instanceof File) ? 1 : 0 if (!this.showSize) return this.$vuetify.lang.t(this.counterString, fileCount) const bytes = this.internalArrayValue.reduce((bytes: number, { size = 0 }: File) => { return bytes + size }, 0) return this.$vuetify.lang.t( this.counterSizeString, fileCount, humanReadableFileSize(bytes, this.base === 1024) ) }, internalArrayValue (): File[] { return wrapInArray(this.internalValue) }, internalValue: { get (): File[] { return this.lazyValue }, set (val: File | File[]) { this.lazyValue = val this.$emit('change', this.lazyValue) }, }, isDirty (): boolean { return this.internalArrayValue.length > 0 }, isLabelActive (): boolean { return this.isDirty }, isMultiple (): boolean { return this.$attrs.hasOwnProperty('multiple') }, text (): string[] { if (!this.isDirty) return [this.placeholder] return this.internalArrayValue.map((file: File) => { const { name = '', size = 0, } = file const truncatedText = this.truncateText(name) return !this.showSize ? truncatedText : `${truncatedText} (${humanReadableFileSize(size, this.base === 1024)})` }) }, base (): 1000 | 1024 | undefined { return typeof this.showSize !== 'boolean' ? this.showSize : undefined }, hasChips (): boolean { return this.chips || this.smallChips }, }, watch: { readonly: { handler (v) { if (v === true) consoleError('readonly is not supported on <v-file-input>', this) }, immediate: true, }, value (v) { const value = this.isMultiple ? v : v ? [v] : [] if (!deepEqual(value, this.$refs.input.files)) { // When the input value is changed programatically, clear the // internal input's value so that the `onInput` handler // can be triggered again if the user re-selects the exact // same file(s). Ideally, `input.files` should be // manipulated directly but that property is readonly. this.$refs.input.value = '' } }, }, methods: { clearableCallback () { this.internalValue = this.isMultiple ? [] : undefined this.$refs.input.value = '' }, genChips () { if (!this.isDirty) return [] return this.text.map((text, index) => this.$createElement(VChip, { props: { small: this.smallChips }, on: { 'click:close': () => { const internalValue = this.internalValue internalValue.splice(index, 1) this.internalValue = internalValue // Trigger the watcher }, }, }, [text])) }, genControl () { const render = VTextField.options.methods.genControl.call(this) if (this.hideInput) { render.data!.style = mergeStyles( render.data!.style, { display: 'none' } ) } return render }, genInput () { const input = VTextField.options.methods.genInput.call(this) // We should not be setting value // programmatically on the input // when it is using type="file" delete input.data!.domProps!.value // This solves an issue in Safari where // nothing happens when adding a file // do to the input event not firing // https://github.com/vuetifyjs/vuetify/issues/7941 delete input.data!.on!.input input.data!.on!.change = this.onInput return [this.genSelections(), input] }, genPrependSlot () { if (!this.prependIcon) return null const icon = this.genIcon('prepend', () => { this.$refs.input.click() }) return this.genSlot('prepend', 'outer', [icon]) }, genSelectionText (): string[] { const length = this.text.length if (length < 2) return this.text if (this.showSize && !this.counter) return [this.computedCounterValue] return [this.$vuetify.lang.t(this.counterString, length)] }, genSelections () { const children = [] if (this.isDirty && this.$scopedSlots.selection) { this.internalArrayValue.forEach((file: File, index: number) => { if (!this.$scopedSlots.selection) return children.push( this.$scopedSlots.selection({ text: this.text[index], file, index, }) ) }) } else { children.push(this.hasChips && this.isDirty ? this.genChips() : this.genSelectionText()) } return this.$createElement('div', { staticClass: 'v-file-input__text', class: { 'v-file-input__text--placeholder': this.placeholder && !this.isDirty, 'v-file-input__text--chips': this.hasChips && !this.$scopedSlots.selection, }, }, children) }, genTextFieldSlot () { const node = VTextField.options.methods.genTextFieldSlot.call(this) node.data!.on = { ...(node.data!.on || {}), click: () => this.$refs.input.click(), } return node }, onInput (e: Event) { const files = [...(e.target as HTMLInputElement).files || []] this.internalValue = this.isMultiple ? files : files[0] // Set initialValue here otherwise isFocused // watcher in VTextField will emit a change // event whenever the component is blurred this.initialValue = this.internalValue }, onKeyDown (e: KeyboardEvent) { this.$emit('keydown', e) }, truncateText (str: string) { if (str.length < Number(this.truncateLength)) return str const charsKeepOneSide = Math.floor((Number(this.truncateLength) - 1) / 2) return `${str.slice(0, charsKeepOneSide)}…${str.slice(str.length - charsKeepOneSide)}` }, }, })