VFileInput.mjs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import { resolveDirective as _resolveDirective, createVNode as _createVNode, mergeProps as _mergeProps, Fragment as _Fragment } from "vue";
  2. // Styles
  3. import "./VFileInput.css";
  4. // Components
  5. import { VChip } from "../VChip/index.mjs";
  6. import { VCounter } from "../VCounter/index.mjs";
  7. import { VField } from "../VField/index.mjs";
  8. import { filterFieldProps, makeVFieldProps } from "../VField/VField.mjs";
  9. import { makeVInputProps, VInput } from "../VInput/VInput.mjs"; // Composables
  10. import { useFocus } from "../../composables/focus.mjs";
  11. import { forwardRefs } from "../../composables/forwardRefs.mjs";
  12. import { useLocale } from "../../composables/locale.mjs";
  13. import { useProxiedModel } from "../../composables/proxiedModel.mjs"; // Utilities
  14. import { computed, nextTick, ref, watch } from 'vue';
  15. import { callEvent, filterInputAttrs, genericComponent, humanReadableFileSize, propsFactory, useRender, wrapInArray } from "../../util/index.mjs"; // Types
  16. export const makeVFileInputProps = propsFactory({
  17. chips: Boolean,
  18. counter: Boolean,
  19. counterSizeString: {
  20. type: String,
  21. default: '$vuetify.fileInput.counterSize'
  22. },
  23. counterString: {
  24. type: String,
  25. default: '$vuetify.fileInput.counter'
  26. },
  27. hideInput: Boolean,
  28. multiple: Boolean,
  29. showSize: {
  30. type: [Boolean, Number, String],
  31. default: false,
  32. validator: v => {
  33. return typeof v === 'boolean' || [1000, 1024].includes(Number(v));
  34. }
  35. },
  36. ...makeVInputProps({
  37. prependIcon: '$file'
  38. }),
  39. modelValue: {
  40. type: [Array, Object],
  41. default: props => props.multiple ? [] : null,
  42. validator: val => {
  43. return wrapInArray(val).every(v => v != null && typeof v === 'object');
  44. }
  45. },
  46. ...makeVFieldProps({
  47. clearable: true
  48. })
  49. }, 'VFileInput');
  50. export const VFileInput = genericComponent()({
  51. name: 'VFileInput',
  52. inheritAttrs: false,
  53. props: makeVFileInputProps(),
  54. emits: {
  55. 'click:control': e => true,
  56. 'mousedown:control': e => true,
  57. 'update:focused': focused => true,
  58. 'update:modelValue': files => true
  59. },
  60. setup(props, _ref) {
  61. let {
  62. attrs,
  63. emit,
  64. slots
  65. } = _ref;
  66. const {
  67. t
  68. } = useLocale();
  69. const model = useProxiedModel(props, 'modelValue', props.modelValue, val => wrapInArray(val), val => !props.multiple && Array.isArray(val) ? val[0] : val);
  70. const {
  71. isFocused,
  72. focus,
  73. blur
  74. } = useFocus(props);
  75. const base = computed(() => typeof props.showSize !== 'boolean' ? props.showSize : undefined);
  76. const totalBytes = computed(() => (model.value ?? []).reduce((bytes, _ref2) => {
  77. let {
  78. size = 0
  79. } = _ref2;
  80. return bytes + size;
  81. }, 0));
  82. const totalBytesReadable = computed(() => humanReadableFileSize(totalBytes.value, base.value));
  83. const fileNames = computed(() => (model.value ?? []).map(file => {
  84. const {
  85. name = '',
  86. size = 0
  87. } = file;
  88. return !props.showSize ? name : `${name} (${humanReadableFileSize(size, base.value)})`;
  89. }));
  90. const counterValue = computed(() => {
  91. const fileCount = model.value?.length ?? 0;
  92. if (props.showSize) return t(props.counterSizeString, fileCount, totalBytesReadable.value);else return t(props.counterString, fileCount);
  93. });
  94. const vInputRef = ref();
  95. const vFieldRef = ref();
  96. const inputRef = ref();
  97. const isActive = computed(() => isFocused.value || props.active);
  98. const isPlainOrUnderlined = computed(() => ['plain', 'underlined'].includes(props.variant));
  99. function onFocus() {
  100. if (inputRef.value !== document.activeElement) {
  101. inputRef.value?.focus();
  102. }
  103. if (!isFocused.value) focus();
  104. }
  105. function onClickPrepend(e) {
  106. inputRef.value?.click();
  107. }
  108. function onControlMousedown(e) {
  109. emit('mousedown:control', e);
  110. }
  111. function onControlClick(e) {
  112. inputRef.value?.click();
  113. emit('click:control', e);
  114. }
  115. function onClear(e) {
  116. e.stopPropagation();
  117. onFocus();
  118. nextTick(() => {
  119. model.value = [];
  120. callEvent(props['onClick:clear'], e);
  121. });
  122. }
  123. watch(model, newValue => {
  124. const hasModelReset = !Array.isArray(newValue) || !newValue.length;
  125. if (hasModelReset && inputRef.value) {
  126. inputRef.value.value = '';
  127. }
  128. });
  129. useRender(() => {
  130. const hasCounter = !!(slots.counter || props.counter);
  131. const hasDetails = !!(hasCounter || slots.details);
  132. const [rootAttrs, inputAttrs] = filterInputAttrs(attrs);
  133. const {
  134. modelValue: _,
  135. ...inputProps
  136. } = VInput.filterProps(props);
  137. const fieldProps = filterFieldProps(props);
  138. return _createVNode(VInput, _mergeProps({
  139. "ref": vInputRef,
  140. "modelValue": model.value,
  141. "onUpdate:modelValue": $event => model.value = $event,
  142. "class": ['v-file-input', {
  143. 'v-file-input--chips': !!props.chips,
  144. 'v-file-input--hide': props.hideInput,
  145. 'v-input--plain-underlined': isPlainOrUnderlined.value
  146. }, props.class],
  147. "style": props.style,
  148. "onClick:prepend": onClickPrepend
  149. }, rootAttrs, inputProps, {
  150. "centerAffix": !isPlainOrUnderlined.value,
  151. "focused": isFocused.value
  152. }), {
  153. ...slots,
  154. default: _ref3 => {
  155. let {
  156. id,
  157. isDisabled,
  158. isDirty,
  159. isReadonly,
  160. isValid
  161. } = _ref3;
  162. return _createVNode(VField, _mergeProps({
  163. "ref": vFieldRef,
  164. "prepend-icon": props.prependIcon,
  165. "onMousedown": onControlMousedown,
  166. "onClick": onControlClick,
  167. "onClick:clear": onClear,
  168. "onClick:prependInner": props['onClick:prependInner'],
  169. "onClick:appendInner": props['onClick:appendInner']
  170. }, fieldProps, {
  171. "id": id.value,
  172. "active": isActive.value || isDirty.value,
  173. "dirty": isDirty.value || props.dirty,
  174. "disabled": isDisabled.value,
  175. "focused": isFocused.value,
  176. "error": isValid.value === false
  177. }), {
  178. ...slots,
  179. default: _ref4 => {
  180. let {
  181. props: {
  182. class: fieldClass,
  183. ...slotProps
  184. }
  185. } = _ref4;
  186. return _createVNode(_Fragment, null, [_createVNode("input", _mergeProps({
  187. "ref": inputRef,
  188. "type": "file",
  189. "readonly": isReadonly.value,
  190. "disabled": isDisabled.value,
  191. "multiple": props.multiple,
  192. "name": props.name,
  193. "onClick": e => {
  194. e.stopPropagation();
  195. if (isReadonly.value) e.preventDefault();
  196. onFocus();
  197. },
  198. "onChange": e => {
  199. if (!e.target) return;
  200. const target = e.target;
  201. model.value = [...(target.files ?? [])];
  202. },
  203. "onFocus": onFocus,
  204. "onBlur": blur
  205. }, slotProps, inputAttrs), null), _createVNode("div", {
  206. "class": fieldClass
  207. }, [!!model.value?.length && !props.hideInput && (slots.selection ? slots.selection({
  208. fileNames: fileNames.value,
  209. totalBytes: totalBytes.value,
  210. totalBytesReadable: totalBytesReadable.value
  211. }) : props.chips ? fileNames.value.map(text => _createVNode(VChip, {
  212. "key": text,
  213. "size": "small",
  214. "text": text
  215. }, null)) : fileNames.value.join(', '))])]);
  216. }
  217. });
  218. },
  219. details: hasDetails ? slotProps => _createVNode(_Fragment, null, [slots.details?.(slotProps), hasCounter && _createVNode(_Fragment, null, [_createVNode("span", null, null), _createVNode(VCounter, {
  220. "active": !!model.value?.length,
  221. "value": counterValue.value,
  222. "disabled": props.disabled
  223. }, slots.counter)])]) : undefined
  224. });
  225. });
  226. return forwardRefs({}, vInputRef, vFieldRef, inputRef);
  227. }
  228. });
  229. //# sourceMappingURL=VFileInput.mjs.map