289 lines
10 KiB
JavaScript
289 lines
10 KiB
JavaScript
|
import { mergeProps as _mergeProps, Fragment as _Fragment, withDirectives as _withDirectives, vShow as _vShow, resolveDirective as _resolveDirective, createVNode as _createVNode } from "vue";
|
||
|
// Styles
|
||
|
import "./VField.css";
|
||
|
|
||
|
// Components
|
||
|
import { VFieldLabel } from "./VFieldLabel.mjs";
|
||
|
import { VExpandXTransition } from "../transitions/index.mjs";
|
||
|
import { useInputIcon } from "../VInput/InputIcon.mjs"; // Composables
|
||
|
import { useBackgroundColor, useTextColor } from "../../composables/color.mjs";
|
||
|
import { makeComponentProps } from "../../composables/component.mjs";
|
||
|
import { makeFocusProps, useFocus } from "../../composables/focus.mjs";
|
||
|
import { IconValue } from "../../composables/icons.mjs";
|
||
|
import { LoaderSlot, makeLoaderProps, useLoader } from "../../composables/loader.mjs";
|
||
|
import { useRtl } from "../../composables/locale.mjs";
|
||
|
import { makeRoundedProps, useRounded } from "../../composables/rounded.mjs";
|
||
|
import { makeThemeProps, provideTheme } from "../../composables/theme.mjs"; // Utilities
|
||
|
import { computed, ref, toRef, watch } from 'vue';
|
||
|
import { animate, convertToUnit, EventProp, genericComponent, getUid, isOn, nullifyTransforms, pick, propsFactory, standardEasing, useRender } from "../../util/index.mjs"; // Types
|
||
|
const allowedVariants = ['underlined', 'outlined', 'filled', 'solo', 'solo-inverted', 'solo-filled', 'plain'];
|
||
|
export const makeVFieldProps = propsFactory({
|
||
|
appendInnerIcon: IconValue,
|
||
|
bgColor: String,
|
||
|
clearable: Boolean,
|
||
|
clearIcon: {
|
||
|
type: IconValue,
|
||
|
default: '$clear'
|
||
|
},
|
||
|
active: Boolean,
|
||
|
centerAffix: {
|
||
|
type: Boolean,
|
||
|
default: undefined
|
||
|
},
|
||
|
color: String,
|
||
|
baseColor: String,
|
||
|
dirty: Boolean,
|
||
|
disabled: {
|
||
|
type: Boolean,
|
||
|
default: null
|
||
|
},
|
||
|
error: Boolean,
|
||
|
flat: Boolean,
|
||
|
label: String,
|
||
|
persistentClear: Boolean,
|
||
|
prependInnerIcon: IconValue,
|
||
|
reverse: Boolean,
|
||
|
singleLine: Boolean,
|
||
|
variant: {
|
||
|
type: String,
|
||
|
default: 'filled',
|
||
|
validator: v => allowedVariants.includes(v)
|
||
|
},
|
||
|
'onClick:clear': EventProp(),
|
||
|
'onClick:appendInner': EventProp(),
|
||
|
'onClick:prependInner': EventProp(),
|
||
|
...makeComponentProps(),
|
||
|
...makeLoaderProps(),
|
||
|
...makeRoundedProps(),
|
||
|
...makeThemeProps()
|
||
|
}, 'VField');
|
||
|
export const VField = genericComponent()({
|
||
|
name: 'VField',
|
||
|
inheritAttrs: false,
|
||
|
props: {
|
||
|
id: String,
|
||
|
...makeFocusProps(),
|
||
|
...makeVFieldProps()
|
||
|
},
|
||
|
emits: {
|
||
|
'update:focused': focused => true,
|
||
|
'update:modelValue': value => true
|
||
|
},
|
||
|
setup(props, _ref) {
|
||
|
let {
|
||
|
attrs,
|
||
|
emit,
|
||
|
slots
|
||
|
} = _ref;
|
||
|
const {
|
||
|
themeClasses
|
||
|
} = provideTheme(props);
|
||
|
const {
|
||
|
loaderClasses
|
||
|
} = useLoader(props);
|
||
|
const {
|
||
|
focusClasses,
|
||
|
isFocused,
|
||
|
focus,
|
||
|
blur
|
||
|
} = useFocus(props);
|
||
|
const {
|
||
|
InputIcon
|
||
|
} = useInputIcon(props);
|
||
|
const {
|
||
|
roundedClasses
|
||
|
} = useRounded(props);
|
||
|
const {
|
||
|
rtlClasses
|
||
|
} = useRtl();
|
||
|
const isActive = computed(() => props.dirty || props.active);
|
||
|
const hasLabel = computed(() => !props.singleLine && !!(props.label || slots.label));
|
||
|
const uid = getUid();
|
||
|
const id = computed(() => props.id || `input-${uid}`);
|
||
|
const messagesId = computed(() => `${id.value}-messages`);
|
||
|
const labelRef = ref();
|
||
|
const floatingLabelRef = ref();
|
||
|
const controlRef = ref();
|
||
|
const isPlainOrUnderlined = computed(() => ['plain', 'underlined'].includes(props.variant));
|
||
|
const {
|
||
|
backgroundColorClasses,
|
||
|
backgroundColorStyles
|
||
|
} = useBackgroundColor(toRef(props, 'bgColor'));
|
||
|
const {
|
||
|
textColorClasses,
|
||
|
textColorStyles
|
||
|
} = useTextColor(computed(() => {
|
||
|
return props.error || props.disabled ? undefined : isActive.value && isFocused.value ? props.color : props.baseColor;
|
||
|
}));
|
||
|
watch(isActive, val => {
|
||
|
if (hasLabel.value) {
|
||
|
const el = labelRef.value.$el;
|
||
|
const targetEl = floatingLabelRef.value.$el;
|
||
|
requestAnimationFrame(() => {
|
||
|
const rect = nullifyTransforms(el);
|
||
|
const targetRect = targetEl.getBoundingClientRect();
|
||
|
const x = targetRect.x - rect.x;
|
||
|
const y = targetRect.y - rect.y - (rect.height / 2 - targetRect.height / 2);
|
||
|
const targetWidth = targetRect.width / 0.75;
|
||
|
const width = Math.abs(targetWidth - rect.width) > 1 ? {
|
||
|
maxWidth: convertToUnit(targetWidth)
|
||
|
} : undefined;
|
||
|
const style = getComputedStyle(el);
|
||
|
const targetStyle = getComputedStyle(targetEl);
|
||
|
const duration = parseFloat(style.transitionDuration) * 1000 || 150;
|
||
|
const scale = parseFloat(targetStyle.getPropertyValue('--v-field-label-scale'));
|
||
|
const color = targetStyle.getPropertyValue('color');
|
||
|
el.style.visibility = 'visible';
|
||
|
targetEl.style.visibility = 'hidden';
|
||
|
animate(el, {
|
||
|
transform: `translate(${x}px, ${y}px) scale(${scale})`,
|
||
|
color,
|
||
|
...width
|
||
|
}, {
|
||
|
duration,
|
||
|
easing: standardEasing,
|
||
|
direction: val ? 'normal' : 'reverse'
|
||
|
}).finished.then(() => {
|
||
|
el.style.removeProperty('visibility');
|
||
|
targetEl.style.removeProperty('visibility');
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}, {
|
||
|
flush: 'post'
|
||
|
});
|
||
|
const slotProps = computed(() => ({
|
||
|
isActive,
|
||
|
isFocused,
|
||
|
controlRef,
|
||
|
blur,
|
||
|
focus
|
||
|
}));
|
||
|
function onClick(e) {
|
||
|
if (e.target !== document.activeElement) {
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
}
|
||
|
useRender(() => {
|
||
|
const isOutlined = props.variant === 'outlined';
|
||
|
const hasPrepend = slots['prepend-inner'] || props.prependInnerIcon;
|
||
|
const hasClear = !!(props.clearable || slots.clear);
|
||
|
const hasAppend = !!(slots['append-inner'] || props.appendInnerIcon || hasClear);
|
||
|
const label = () => slots.label ? slots.label({
|
||
|
...slotProps.value,
|
||
|
label: props.label,
|
||
|
props: {
|
||
|
for: id.value
|
||
|
}
|
||
|
}) : props.label;
|
||
|
return _createVNode("div", _mergeProps({
|
||
|
"class": ['v-field', {
|
||
|
'v-field--active': isActive.value,
|
||
|
'v-field--appended': hasAppend,
|
||
|
'v-field--center-affix': props.centerAffix ?? !isPlainOrUnderlined.value,
|
||
|
'v-field--disabled': props.disabled,
|
||
|
'v-field--dirty': props.dirty,
|
||
|
'v-field--error': props.error,
|
||
|
'v-field--flat': props.flat,
|
||
|
'v-field--has-background': !!props.bgColor,
|
||
|
'v-field--persistent-clear': props.persistentClear,
|
||
|
'v-field--prepended': hasPrepend,
|
||
|
'v-field--reverse': props.reverse,
|
||
|
'v-field--single-line': props.singleLine,
|
||
|
'v-field--no-label': !label(),
|
||
|
[`v-field--variant-${props.variant}`]: true
|
||
|
}, themeClasses.value, backgroundColorClasses.value, focusClasses.value, loaderClasses.value, roundedClasses.value, rtlClasses.value, props.class],
|
||
|
"style": [backgroundColorStyles.value, props.style],
|
||
|
"onClick": onClick
|
||
|
}, attrs), [_createVNode("div", {
|
||
|
"class": "v-field__overlay"
|
||
|
}, null), _createVNode(LoaderSlot, {
|
||
|
"name": "v-field",
|
||
|
"active": !!props.loading,
|
||
|
"color": props.error ? 'error' : typeof props.loading === 'string' ? props.loading : props.color
|
||
|
}, {
|
||
|
default: slots.loader
|
||
|
}), hasPrepend && _createVNode("div", {
|
||
|
"key": "prepend",
|
||
|
"class": "v-field__prepend-inner"
|
||
|
}, [props.prependInnerIcon && _createVNode(InputIcon, {
|
||
|
"key": "prepend-icon",
|
||
|
"name": "prependInner"
|
||
|
}, null), slots['prepend-inner']?.(slotProps.value)]), _createVNode("div", {
|
||
|
"class": "v-field__field",
|
||
|
"data-no-activator": ""
|
||
|
}, [['filled', 'solo', 'solo-inverted', 'solo-filled'].includes(props.variant) && hasLabel.value && _createVNode(VFieldLabel, {
|
||
|
"key": "floating-label",
|
||
|
"ref": floatingLabelRef,
|
||
|
"class": [textColorClasses.value],
|
||
|
"floating": true,
|
||
|
"for": id.value,
|
||
|
"style": textColorStyles.value
|
||
|
}, {
|
||
|
default: () => [label()]
|
||
|
}), _createVNode(VFieldLabel, {
|
||
|
"ref": labelRef,
|
||
|
"for": id.value
|
||
|
}, {
|
||
|
default: () => [label()]
|
||
|
}), slots.default?.({
|
||
|
...slotProps.value,
|
||
|
props: {
|
||
|
id: id.value,
|
||
|
class: 'v-field__input',
|
||
|
'aria-describedby': messagesId.value
|
||
|
},
|
||
|
focus,
|
||
|
blur
|
||
|
})]), hasClear && _createVNode(VExpandXTransition, {
|
||
|
"key": "clear"
|
||
|
}, {
|
||
|
default: () => [_withDirectives(_createVNode("div", {
|
||
|
"class": "v-field__clearable",
|
||
|
"onMousedown": e => {
|
||
|
e.preventDefault();
|
||
|
e.stopPropagation();
|
||
|
}
|
||
|
}, [slots.clear ? slots.clear() : _createVNode(InputIcon, {
|
||
|
"name": "clear"
|
||
|
}, null)]), [[_vShow, props.dirty]])]
|
||
|
}), hasAppend && _createVNode("div", {
|
||
|
"key": "append",
|
||
|
"class": "v-field__append-inner"
|
||
|
}, [slots['append-inner']?.(slotProps.value), props.appendInnerIcon && _createVNode(InputIcon, {
|
||
|
"key": "append-icon",
|
||
|
"name": "appendInner"
|
||
|
}, null)]), _createVNode("div", {
|
||
|
"class": ['v-field__outline', textColorClasses.value],
|
||
|
"style": textColorStyles.value
|
||
|
}, [isOutlined && _createVNode(_Fragment, null, [_createVNode("div", {
|
||
|
"class": "v-field__outline__start"
|
||
|
}, null), hasLabel.value && _createVNode("div", {
|
||
|
"class": "v-field__outline__notch"
|
||
|
}, [_createVNode(VFieldLabel, {
|
||
|
"ref": floatingLabelRef,
|
||
|
"floating": true,
|
||
|
"for": id.value
|
||
|
}, {
|
||
|
default: () => [label()]
|
||
|
})]), _createVNode("div", {
|
||
|
"class": "v-field__outline__end"
|
||
|
}, null)]), isPlainOrUnderlined.value && hasLabel.value && _createVNode(VFieldLabel, {
|
||
|
"ref": floatingLabelRef,
|
||
|
"floating": true,
|
||
|
"for": id.value
|
||
|
}, {
|
||
|
default: () => [label()]
|
||
|
})])]);
|
||
|
});
|
||
|
return {
|
||
|
controlRef
|
||
|
};
|
||
|
}
|
||
|
});
|
||
|
// TODO: this is kinda slow, might be better to implicitly inherit props instead
|
||
|
export function filterFieldProps(attrs) {
|
||
|
const keys = Object.keys(VField.props).filter(k => !isOn(k) && k !== 'class' && k !== 'style');
|
||
|
return pick(attrs, keys);
|
||
|
}
|
||
|
//# sourceMappingURL=VField.mjs.map
|