VField.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import { mergeProps as _mergeProps, Fragment as _Fragment, withDirectives as _withDirectives, vShow as _vShow, resolveDirective as _resolveDirective, createVNode as _createVNode } from "vue";
  2. // Styles
  3. import "./VField.css";
  4. // Components
  5. import { VFieldLabel } from "./VFieldLabel.mjs";
  6. import { VExpandXTransition } from "../transitions/index.mjs";
  7. import { VDefaultsProvider } from "../VDefaultsProvider/index.mjs";
  8. import { useInputIcon } from "../VInput/InputIcon.mjs"; // Composables
  9. import { useBackgroundColor, useTextColor } from "../../composables/color.mjs";
  10. import { makeComponentProps } from "../../composables/component.mjs";
  11. import { makeFocusProps, useFocus } from "../../composables/focus.mjs";
  12. import { IconValue } from "../../composables/icons.mjs";
  13. import { LoaderSlot, makeLoaderProps, useLoader } from "../../composables/loader.mjs";
  14. import { useRtl } from "../../composables/locale.mjs";
  15. import { makeRoundedProps, useRounded } from "../../composables/rounded.mjs";
  16. import { makeThemeProps, provideTheme } from "../../composables/theme.mjs"; // Utilities
  17. import { computed, ref, toRef, watch } from 'vue';
  18. import { animate, convertToUnit, EventProp, genericComponent, getUid, isOn, nullifyTransforms, pick, propsFactory, standardEasing, useRender } from "../../util/index.mjs"; // Types
  19. const allowedVariants = ['underlined', 'outlined', 'filled', 'solo', 'solo-inverted', 'solo-filled', 'plain'];
  20. export const makeVFieldProps = propsFactory({
  21. appendInnerIcon: IconValue,
  22. bgColor: String,
  23. clearable: Boolean,
  24. clearIcon: {
  25. type: IconValue,
  26. default: '$clear'
  27. },
  28. active: Boolean,
  29. centerAffix: {
  30. type: Boolean,
  31. default: undefined
  32. },
  33. color: String,
  34. baseColor: String,
  35. dirty: Boolean,
  36. disabled: {
  37. type: Boolean,
  38. default: null
  39. },
  40. error: Boolean,
  41. flat: Boolean,
  42. label: String,
  43. persistentClear: Boolean,
  44. prependInnerIcon: IconValue,
  45. reverse: Boolean,
  46. singleLine: Boolean,
  47. variant: {
  48. type: String,
  49. default: 'filled',
  50. validator: v => allowedVariants.includes(v)
  51. },
  52. 'onClick:clear': EventProp(),
  53. 'onClick:appendInner': EventProp(),
  54. 'onClick:prependInner': EventProp(),
  55. ...makeComponentProps(),
  56. ...makeLoaderProps(),
  57. ...makeRoundedProps(),
  58. ...makeThemeProps()
  59. }, 'VField');
  60. export const VField = genericComponent()({
  61. name: 'VField',
  62. inheritAttrs: false,
  63. props: {
  64. id: String,
  65. ...makeFocusProps(),
  66. ...makeVFieldProps()
  67. },
  68. emits: {
  69. 'update:focused': focused => true,
  70. 'update:modelValue': value => true
  71. },
  72. setup(props, _ref) {
  73. let {
  74. attrs,
  75. emit,
  76. slots
  77. } = _ref;
  78. const {
  79. themeClasses
  80. } = provideTheme(props);
  81. const {
  82. loaderClasses
  83. } = useLoader(props);
  84. const {
  85. focusClasses,
  86. isFocused,
  87. focus,
  88. blur
  89. } = useFocus(props);
  90. const {
  91. InputIcon
  92. } = useInputIcon(props);
  93. const {
  94. roundedClasses
  95. } = useRounded(props);
  96. const {
  97. rtlClasses
  98. } = useRtl();
  99. const isActive = computed(() => props.dirty || props.active);
  100. const hasLabel = computed(() => !props.singleLine && !!(props.label || slots.label));
  101. const uid = getUid();
  102. const id = computed(() => props.id || `input-${uid}`);
  103. const messagesId = computed(() => `${id.value}-messages`);
  104. const labelRef = ref();
  105. const floatingLabelRef = ref();
  106. const controlRef = ref();
  107. const isPlainOrUnderlined = computed(() => ['plain', 'underlined'].includes(props.variant));
  108. const {
  109. backgroundColorClasses,
  110. backgroundColorStyles
  111. } = useBackgroundColor(toRef(props, 'bgColor'));
  112. const {
  113. textColorClasses,
  114. textColorStyles
  115. } = useTextColor(computed(() => {
  116. return props.error || props.disabled ? undefined : isActive.value && isFocused.value ? props.color : props.baseColor;
  117. }));
  118. watch(isActive, val => {
  119. if (hasLabel.value) {
  120. const el = labelRef.value.$el;
  121. const targetEl = floatingLabelRef.value.$el;
  122. requestAnimationFrame(() => {
  123. const rect = nullifyTransforms(el);
  124. const targetRect = targetEl.getBoundingClientRect();
  125. const x = targetRect.x - rect.x;
  126. const y = targetRect.y - rect.y - (rect.height / 2 - targetRect.height / 2);
  127. const targetWidth = targetRect.width / 0.75;
  128. const width = Math.abs(targetWidth - rect.width) > 1 ? {
  129. maxWidth: convertToUnit(targetWidth)
  130. } : undefined;
  131. const style = getComputedStyle(el);
  132. const targetStyle = getComputedStyle(targetEl);
  133. const duration = parseFloat(style.transitionDuration) * 1000 || 150;
  134. const scale = parseFloat(targetStyle.getPropertyValue('--v-field-label-scale'));
  135. const color = targetStyle.getPropertyValue('color');
  136. el.style.visibility = 'visible';
  137. targetEl.style.visibility = 'hidden';
  138. animate(el, {
  139. transform: `translate(${x}px, ${y}px) scale(${scale})`,
  140. color,
  141. ...width
  142. }, {
  143. duration,
  144. easing: standardEasing,
  145. direction: val ? 'normal' : 'reverse'
  146. }).finished.then(() => {
  147. el.style.removeProperty('visibility');
  148. targetEl.style.removeProperty('visibility');
  149. });
  150. });
  151. }
  152. }, {
  153. flush: 'post'
  154. });
  155. const slotProps = computed(() => ({
  156. isActive,
  157. isFocused,
  158. controlRef,
  159. blur,
  160. focus
  161. }));
  162. function onClick(e) {
  163. if (e.target !== document.activeElement) {
  164. e.preventDefault();
  165. }
  166. }
  167. function onKeydownClear(e) {
  168. if (e.key !== 'Enter' && e.key !== ' ') return;
  169. e.preventDefault();
  170. e.stopPropagation();
  171. props['onClick:clear']?.(new MouseEvent('click'));
  172. }
  173. useRender(() => {
  174. const isOutlined = props.variant === 'outlined';
  175. const hasPrepend = !!(slots['prepend-inner'] || props.prependInnerIcon);
  176. const hasClear = !!(props.clearable || slots.clear);
  177. const hasAppend = !!(slots['append-inner'] || props.appendInnerIcon || hasClear);
  178. const label = () => slots.label ? slots.label({
  179. ...slotProps.value,
  180. label: props.label,
  181. props: {
  182. for: id.value
  183. }
  184. }) : props.label;
  185. return _createVNode("div", _mergeProps({
  186. "class": ['v-field', {
  187. 'v-field--active': isActive.value,
  188. 'v-field--appended': hasAppend,
  189. 'v-field--center-affix': props.centerAffix ?? !isPlainOrUnderlined.value,
  190. 'v-field--disabled': props.disabled,
  191. 'v-field--dirty': props.dirty,
  192. 'v-field--error': props.error,
  193. 'v-field--flat': props.flat,
  194. 'v-field--has-background': !!props.bgColor,
  195. 'v-field--persistent-clear': props.persistentClear,
  196. 'v-field--prepended': hasPrepend,
  197. 'v-field--reverse': props.reverse,
  198. 'v-field--single-line': props.singleLine,
  199. 'v-field--no-label': !label(),
  200. [`v-field--variant-${props.variant}`]: true
  201. }, themeClasses.value, backgroundColorClasses.value, focusClasses.value, loaderClasses.value, roundedClasses.value, rtlClasses.value, props.class],
  202. "style": [backgroundColorStyles.value, props.style],
  203. "onClick": onClick
  204. }, attrs), [_createVNode("div", {
  205. "class": "v-field__overlay"
  206. }, null), _createVNode(LoaderSlot, {
  207. "name": "v-field",
  208. "active": !!props.loading,
  209. "color": props.error ? 'error' : typeof props.loading === 'string' ? props.loading : props.color
  210. }, {
  211. default: slots.loader
  212. }), hasPrepend && _createVNode("div", {
  213. "key": "prepend",
  214. "class": "v-field__prepend-inner"
  215. }, [props.prependInnerIcon && _createVNode(InputIcon, {
  216. "key": "prepend-icon",
  217. "name": "prependInner"
  218. }, null), slots['prepend-inner']?.(slotProps.value)]), _createVNode("div", {
  219. "class": "v-field__field",
  220. "data-no-activator": ""
  221. }, [['filled', 'solo', 'solo-inverted', 'solo-filled'].includes(props.variant) && hasLabel.value && _createVNode(VFieldLabel, {
  222. "key": "floating-label",
  223. "ref": floatingLabelRef,
  224. "class": [textColorClasses.value],
  225. "floating": true,
  226. "for": id.value,
  227. "style": textColorStyles.value
  228. }, {
  229. default: () => [label()]
  230. }), hasLabel.value && _createVNode(VFieldLabel, {
  231. "key": "label",
  232. "ref": labelRef,
  233. "for": id.value
  234. }, {
  235. default: () => [label()]
  236. }), slots.default?.({
  237. ...slotProps.value,
  238. props: {
  239. id: id.value,
  240. class: 'v-field__input',
  241. 'aria-describedby': messagesId.value
  242. },
  243. focus,
  244. blur
  245. })]), hasClear && _createVNode(VExpandXTransition, {
  246. "key": "clear"
  247. }, {
  248. default: () => [_withDirectives(_createVNode("div", {
  249. "class": "v-field__clearable",
  250. "onMousedown": e => {
  251. e.preventDefault();
  252. e.stopPropagation();
  253. }
  254. }, [_createVNode(VDefaultsProvider, {
  255. "defaults": {
  256. VIcon: {
  257. icon: props.clearIcon
  258. }
  259. }
  260. }, {
  261. default: () => [slots.clear ? slots.clear({
  262. ...slotProps.value,
  263. props: {
  264. onKeydown: onKeydownClear,
  265. onFocus: focus,
  266. onBlur: blur,
  267. onClick: props['onClick:clear']
  268. }
  269. }) : _createVNode(InputIcon, {
  270. "name": "clear",
  271. "onKeydown": onKeydownClear,
  272. "onFocus": focus,
  273. "onBlur": blur
  274. }, null)]
  275. })]), [[_vShow, props.dirty]])]
  276. }), hasAppend && _createVNode("div", {
  277. "key": "append",
  278. "class": "v-field__append-inner"
  279. }, [slots['append-inner']?.(slotProps.value), props.appendInnerIcon && _createVNode(InputIcon, {
  280. "key": "append-icon",
  281. "name": "appendInner"
  282. }, null)]), _createVNode("div", {
  283. "class": ['v-field__outline', textColorClasses.value],
  284. "style": textColorStyles.value
  285. }, [isOutlined && _createVNode(_Fragment, null, [_createVNode("div", {
  286. "class": "v-field__outline__start"
  287. }, null), hasLabel.value && _createVNode("div", {
  288. "class": "v-field__outline__notch"
  289. }, [_createVNode(VFieldLabel, {
  290. "ref": floatingLabelRef,
  291. "floating": true,
  292. "for": id.value
  293. }, {
  294. default: () => [label()]
  295. })]), _createVNode("div", {
  296. "class": "v-field__outline__end"
  297. }, null)]), isPlainOrUnderlined.value && hasLabel.value && _createVNode(VFieldLabel, {
  298. "ref": floatingLabelRef,
  299. "floating": true,
  300. "for": id.value
  301. }, {
  302. default: () => [label()]
  303. })])]);
  304. });
  305. return {
  306. controlRef
  307. };
  308. }
  309. });
  310. // TODO: this is kinda slow, might be better to implicitly inherit props instead
  311. export function filterFieldProps(attrs) {
  312. const keys = Object.keys(VField.props).filter(k => !isOn(k) && k !== 'class' && k !== 'style');
  313. return pick(attrs, keys);
  314. }
  315. //# sourceMappingURL=VField.mjs.map