UNPKG

vue-material-adapter

Version:

Vue 3 wrapper arround Material Components for the Web

349 lines (308 loc) 9.74 kB
import { applyPassive } from '@material/dom/events.js'; import { MDCTextFieldFoundation } from '@material/textfield/foundation.js'; import { computed, onBeforeUnmount, onMounted, provide, reactive, ref, toRef, toRefs, watch, } from 'vue'; import { mcwLineRipple } from '../line-ripple/index.js'; import { mcwNotchedOutline } from '../notched-outline/index.js'; import { useRipplePlugin } from '../ripple/ripple-plugin.js'; let uid_ = 0; const getAttributesList = mutationsList => mutationsList.map(mutation => mutation.attributeName); export default { name: 'mcw-textfield', inheritAttrs: false, props: { modelValue: [String, Number], type: { type: String, default: 'text', validator: function (value) { return [ 'text', 'email', 'search', 'password', 'tel', 'url', 'number', ].includes(value); }, }, label: String, outline: Boolean, disabled: Boolean, required: Boolean, valid: { type: Boolean, default: undefined }, multiline: Boolean, size: { type: [Number, String], default: 20 }, minlength: { type: [Number, String], default: undefined }, maxlength: { type: [Number, String], default: undefined }, rows: { type: [Number, String], default: 8 }, cols: { type: [Number, String], default: 40 }, id: { type: String }, helptext: String, helptextPersistent: Boolean, helptextValidation: Boolean, resizer: { type: Boolean, default: () => true }, prefix: String, suffix: String, characterCounter: Boolean, characterCounterInternal: Boolean, useNativeValidation: { type: Boolean, default: () => true }, }, setup(props, { emit, slots, attrs }) { const uiState = reactive({ text: props.modelValue, classes: { 'mdc-textfield': true, 'mdc-text-field': true, 'mdc-text-field--upgraded': true, 'mdc-text-field--disabled': props.disabled, 'mdc-text-field--textarea': props.multiline, 'mdc-text-field--outlined': !props.fullwidth && props.outline, 'mdc-text-field--with-leading-icon': Boolean( slots.leadingIcon || slots.leading, ), 'mdc-text-field--with-trailing-icon': Boolean( slots.trailingIcon || slots.trailing, ), 'mdc-text-field--filled': Boolean(!props.outline), 'mdc-text-field--no-label': !props.label, }, inputClasses: { 'mdc-text-field__input': true, }, inputAttrs: {}, labelClasses: { 'mdc-floating-label': true, }, lineRippleClasses: { 'mdc-line-ripple': true, }, lineRippleStyles: {}, outlineClasses: {}, notchStyles: {}, helpTextId: `mcw-help-${uid_++}`, labelId: `mcw-label-${uid_}`, root: undefined, wrapper: undefined, helpertext: undefined, input: undefined, labelEl: undefined, lineRippleEl: undefined, characterCounterEl: undefined, helpertextEl: undefined, }); let foundation; let rippleClasses; let rippleStyles; const icons = ref({}); const addIconFoundation = ({ foundation, trailing }) => { icons.value[trailing ? 'trailing' : 'leading'] = foundation; }; provide('addIconFoundation', addIconFoundation); if (!props.multiline && !props.outline) { const { classes, styles } = useRipplePlugin(toRef(uiState, 'root')); rippleClasses = classes; rippleStyles = styles; } const inputAriaControls = computed(() => { return props.helptext ? uiState.helpTextId : undefined; }); const hasLabel = computed(() => { return !props.outline && props.label; }); const hasOutlineLabel = computed(() => { return props.outline && props.label; }); const hasLineRipple = computed(() => { return !(props.outline || props.multiline); }); const hasHelptext = computed(() => { return slots.helpText || props.helptext; }); const internalCharacterCounter = computed( () => props.characterCounter && props.characterCounterInternal, ); const helperCharacterCounter = computed( () => props.characterCounter && !(props.multiline && props.characterCounterInternal), ); const hasHelpline = computed(() => { return props.helptext || helperCharacterCounter.value; }); const rootClasses = computed(() => ({ ...rippleClasses, ...uiState.classes, })); const inputListeners = { // ...listeners, input: ({ target: { value } }) => emit('update:modelValue', value), }; const focus = () => uiState.input?.focus(); const isValid = () => foundation.isValid(); const inputAttributes = computed(() => { // eslint-disable-next-line no-unused-vars const { class: _, ...rest } = attrs; return { ...rest, ...uiState.inputAttrs, }; }); const adapter = { addClass: className => (uiState.classes = { ...uiState.classes, [className]: true }), removeClass: className => { // eslint-disable-next-line no-unused-vars const { [className]: removed, ...rest } = uiState.classes; uiState.classes = rest; }, hasClass: className => Boolean(uiState.classes[className]), registerTextFieldInteractionHandler: (eventType, handler) => { uiState.root.addEventListener(eventType, handler); }, deregisterTextFieldInteractionHandler: (eventType, handler) => { uiState.root.removeEventListener(eventType, handler); }, isFocused: () => { return document.activeElement === uiState.input; }, registerValidationAttributeChangeHandler: handler => { const observer = new MutationObserver(mutationsList => handler(getAttributesList(mutationsList)), ); const targetNode = uiState.input; const config = { attributes: true }; observer.observe(targetNode, config); return observer; }, deregisterValidationAttributeChangeHandler: observer => { observer.disconnect(); }, // input adapter methods registerInputInteractionHandler: (eventType, handler) => { uiState.input.addEventListener(eventType, handler, applyPassive()); }, deregisterInputInteractionHandler: (eventType, handler) => { uiState.input.removeEventListener(eventType, handler, applyPassive()); }, getNativeInput: () => { return uiState.input; }, setInputAttr: (attribute, value) => { uiState.inputAttrs = { ...uiState.inputAttrs, [attribute]: value }; }, removeInputAttr: attribute => { // eslint-disable-next-line no-unused-vars const { [attribute]: removed, ...rest } = uiState.inputAttrs; uiState.inputAttrs = rest; }, // label adapter methods shakeLabel: shouldShake => { uiState.labelEl?.shake(shouldShake); }, floatLabel: shouldFloat => { uiState.labelEl?.float(shouldFloat); }, hasLabel: () => { return !!uiState.labelEl || !!uiState.notchedEl; }, getLabelWidth: () => { return uiState.labelEl.getWidth(); }, // line ripple adapter methods deactivateLineRipple: () => uiState.lineRippleEl?.deactivate(), activateLineRipple: () => uiState.lineRippleEl?.activate(), setLineRippleTransformOrigin: normalizedX => uiState.lineRippleEl?.setRippleCenter(normalizedX), // outline adapter methods hasOutline: () => !!props.outline, notchOutline: (notchWidth, isRtl) => uiState.labelEl.notch(notchWidth, isRtl), closeOutline: () => uiState.labelEl.closeNotch(), }; watch( () => props.disabled, nv => foundation?.setDisabled(nv), ); watch( () => props.required, nv => { uiState.input && (uiState.input.required = nv); }, ); watch( () => props.valid, nv => { if (typeof nv !== 'undefined') { foundation?.setValid(nv); } }, ); watch( () => props.useNativeValidation, nv => { if (typeof nv !== 'undefined') { foundation?.setUseNativeValidation(nv); } }, ); watch( () => props.modelValue, nv => { if (foundation && nv !== foundation.getValue()) { foundation.setValue(nv); } }, ); onMounted(() => { foundation = new MDCTextFieldFoundation( { ...adapter }, { characterCounter: uiState.characterCounterEl?.foundation, helperText: uiState.helpertext?.foundation, leadingIcon: icons.leading?.foundation, trailingIcon: icons.trailing?.foundation, }, ); foundation.init(); foundation.setValue(props.modelValue); props.disabled && foundation.setDisabled(props.disabled); uiState.input && (uiState.input.required = props.required); if (typeof props.valid !== 'undefined') { foundation.setValid(props.valid); } }); onBeforeUnmount(() => { foundation?.destroy(); }); return { ...toRefs(uiState), inputAriaControls, hasLabel, hasOutlineLabel, inputListeners, hasLineRipple, hasHelptext, hasHelpline, focus, helperCharacterCounter, internalCharacterCounter, rootClasses, rippleStyles, isValid, inputAttrs: inputAttributes, }; }, components: { mcwLineRipple, mcwNotchedOutline }, };