// Styles import "../../../src/components/VFileInput/VFileInput.sass"; // Extensions import VTextField from '../VTextField'; // Components import { VChip } from '../VChip'; // 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 => { return typeof v === 'boolean' || [1000, 1024].includes(v); } }, 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'); } } }, computed: { classes() { return { ...VTextField.options.computed.classes.call(this), 'v-file-input': true }; }, computedCounterValue() { 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, { size = 0 }) => { return bytes + size; }, 0); return this.$vuetify.lang.t(this.counterSizeString, fileCount, humanReadableFileSize(bytes, this.base === 1024)); }, internalArrayValue() { return wrapInArray(this.internalValue); }, internalValue: { get() { return this.lazyValue; }, set(val) { this.lazyValue = val; this.$emit('change', this.lazyValue); } }, isDirty() { return this.internalArrayValue.length > 0; }, isLabelActive() { return this.isDirty; }, isMultiple() { return this.$attrs.hasOwnProperty('multiple'); }, text() { if (!this.isDirty) return [this.placeholder]; return this.internalArrayValue.map(file => { const { name = '', size = 0 } = file; const truncatedText = this.truncateText(name); return !this.showSize ? truncatedText : `${truncatedText} (${humanReadableFileSize(size, this.base === 1024)})`; }); }, base() { return typeof this.showSize !== 'boolean' ? this.showSize : undefined; }, hasChips() { return this.chips || this.smallChips; } }, watch: { readonly: { handler(v) { if (v === true) consoleError('readonly is not supported on ', 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() { 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, index) => { 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) { const files = [...(e.target.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) { this.$emit('keydown', e); }, truncateText(str) { 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)}`; } } }); //# sourceMappingURL=VFileInput.js.map