Created
May 13, 2020 19:35
-
-
Save iErik/8067bf13e5832b1d279f3aefe3f659b5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<input-container v-bind="$attrs" :is-active="!!value || hasFocus"> | |
<template v-for="slotName in fieldSlots" :slot="slotName"> | |
<slot :name="slotName" /> | |
</template> | |
<component | |
:is="componentIs" | |
:type="type" | |
:class="classes" | |
:disabled="disabled" | |
:readonly="readonly" | |
:name="name" | |
v-html="['textarea'].includes(type) && value" | |
v-bind="[$attrs, $props]" | |
@input="runMask" | |
@focus="emitFocus" | |
@blur="emitBlur" | |
@keyup="$emit('keyup', $event)" | |
class="basic-input" | |
ref="input" | |
/> | |
</input-container> | |
</template> | |
<script> | |
import InputContainer from './InputContainer' | |
import MaskMixin from './MaskMixin' | |
export default { | |
name: 'basic-input', | |
components: { InputContainer }, | |
mixins: [MaskMixin], | |
data() { | |
return { | |
hasFocus: false, | |
innerValue: this.__getInitialMaskedValue() | |
} | |
}, | |
props: { | |
value: [String, Number], | |
disabled: Boolean, | |
readonly: Boolean, | |
name: { | |
default: '', | |
type: String, | |
required: true | |
}, | |
type: { | |
default: 'text', | |
type: String, | |
validator: val => | |
[ | |
'text', | |
'password', | |
'tel', | |
'url', | |
'email', | |
'textarea', | |
'number' | |
].includes(val) | |
}, | |
color: { | |
type: String, | |
default: 'primary' | |
} | |
}, | |
watch: { | |
value: { | |
handler(newValue, oldValue) { | |
if ( | |
newValue !== oldValue && | |
(newValue === '' || (newValue === null && this.$refs.input)) | |
) { | |
this.$refs.input.value = newValue | |
} | |
} | |
} | |
}, | |
computed: { | |
componentIs() { | |
return this.isTextarea ? 'textarea' : 'input' | |
}, | |
isTextarea() { | |
return this.type === 'textarea' | |
}, | |
classes() { | |
return { | |
'basic-input': true, | |
'basic-input__textarea': this.type === 'textarea', | |
'basic-input--readonly': this.readonly, | |
'basic-input--disabled': this.disabled | |
} | |
}, | |
fieldSlots() { | |
return ['before', 'after', 'label', 'append', 'error', 'hint'] | |
} | |
}, | |
methods: { | |
emitFocus(ev) { | |
this.hasFocus = true | |
this.$emit('focus', ev) | |
}, | |
emitBlur(ev) { | |
this.hasFocus = false | |
this.$emit('focus', ev) | |
}, | |
runMask(e) { | |
let value = e.target.value | |
if (this.mask && !this.isTextarea) return this.__updateMaskValue(value) | |
this.__emitValue(value) | |
}, | |
__emitValue(value) { | |
this.$emit('input', value) | |
} | |
} | |
} | |
</script> | |
<style lang="scss" scoped> | |
.basic-input { | |
border-width: 1px; | |
border-radius: 0.25rem; | |
color: var(--color-font-base); | |
line-height: 1.25; | |
display: inline-block; | |
width: 100%; | |
height: 100%; | |
padding-left: 0.75rem; | |
padding-right: 0.75rem; | |
//padding-top: 0.5rem; | |
//padding-bottom: 0.5rem; | |
&:focus { | |
outline: 0; | |
border-color: var(--color-primary); | |
} | |
&:hover { | |
border-color: var(--color-primary); | |
} | |
&__textarea { | |
width: 100%; | |
} | |
&--hasError { | |
border-width: 1px; | |
border-color: #f56565; | |
} | |
&--readonly { | |
color: var(--color-gray-300); | |
} | |
&--disabled { | |
color: var(--color-gray-300); | |
} | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div class="input-container"> | |
<div v-if="$slots.before" class="input-container__before"> | |
<slot name="before" /> | |
</div> | |
<div :class="innerClasses"> | |
<div :class="innerFieldClasses"> | |
<label v-if="hasLabel" :class="innerLabelClasses"> | |
<slot name="label">{{ label || ' ' }}</slot> | |
</label> | |
<div class="input-container__inner__input"> | |
<slot /> | |
<div v-if="$slots.append" class="input-container__inner__append"> | |
<slot name="append" /> | |
</div> | |
</div> | |
</div> | |
<div | |
v-if="($slots.hint || hint) && !hasError" | |
class="input-container__inner__hint" | |
> | |
<slot name="hint">{{ hint }}</slot> | |
</div> | |
<div v-if="hasError" class="input-container__inner__error"> | |
<slot name="error"> | |
{{ errorMessage || 'Há um erro neste campo' }} | |
</slot> | |
</div> | |
</div> | |
<div v-if="$slots.after" class="input-container__after"> | |
<slot name="after" /> | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
name: 'input-container', | |
props: { | |
/** | |
* The field's label | |
*/ | |
label: { | |
type: String, | |
default: '' | |
}, | |
/** | |
* Wether or not the input has a value | |
*/ | |
isActive: { | |
type: Boolean, | |
default: false | |
}, | |
/** | |
* The hint text to display below the field | |
*/ | |
hint: { | |
type: String, | |
default: '' | |
}, | |
/** | |
* The error message to be displayed for validation. | |
*/ | |
errorMessage: { | |
type: String, | |
default: '' | |
} | |
}, | |
computed: { | |
hasError() { | |
if (!this.$slots.error) return false | |
let slotText = this.$slots.error | |
.map(item => item.text) | |
.join('') | |
.trim() | |
return !!slotText || !!this.errorMessage | |
}, | |
hasLabel() { | |
return !!this.$slots.label || !!this.label | |
}, | |
innerClasses() { | |
return ['input-container__inner', { 'input-container__inner--hasLabel': this.hasLabel }] | |
}, | |
innerLabelClasses() { | |
return [ | |
'input-container__inner__label', | |
{ | |
'input-container__inner__label--top': this.isActive | |
} | |
] | |
}, | |
innerFieldClasses() { | |
return [ | |
'input-container__inner__field', | |
{ | |
'input-container__inner--hasAppend': this.$slots.append, | |
'input-container__inner--hasError': this.hasError | |
} | |
] | |
} | |
} | |
} | |
</script> | |
<style lang="scss" scoped> | |
$fieldHeight: 48px; | |
.input-container { | |
display: flex; | |
flex-wrap: wrap; | |
&__before { | |
display: flex; | |
flex-wrap: nowrap; | |
align-items: center; | |
padding-right: 1rem; | |
height: $fieldHeight; | |
} | |
&__after { | |
flex-wrap: wrap; | |
display: flex; | |
align-items: center; | |
height: $fieldHeight; | |
button { | |
margin-top: 0; | |
margin-bottom: 0; | |
} | |
} | |
&__inner { | |
width: auto; | |
position: relative; | |
max-width: 100%; | |
flex-grow: 10000; | |
flex-shrink: 1; | |
flex-basis: 0%; | |
&__field { | |
position: relative; | |
height: 48px; | |
&:hover .input-container__inner__label { | |
color: var(--color-primary); | |
} | |
} | |
&__label { | |
position: absolute; | |
z-index: 20; | |
top: 50%; | |
left: 15px; | |
transform: translateY(-50%); | |
user-select: none; | |
color: var(--color-gray); | |
font-size: var(--text-base); | |
transition: top 200ms ease, font-size 200ms ease, left 200ms ease, | |
padding 200ms ease; | |
&--active, | |
&--top { | |
color: var(--color-primary); | |
} | |
&--top { | |
top: -7px; | |
left: 8px; | |
font-size: var(--text-xs); | |
padding: 0 5px; | |
transform: translateY(0px); | |
background-color: #fff; | |
} | |
} | |
&__hint { | |
display: block; | |
letter-spacing: 0.025em; | |
font-size: var(--text-sm); | |
margin-bottom: 0.5rem; | |
margin-top: 0.5rem; | |
color: var(--color-font-base); | |
} | |
&__error { | |
display: block; | |
letter-spacing: 0.025em; | |
color: var(--color-red); | |
font-size: 0.875rem; | |
margin-bottom: 0.5rem; | |
margin-top: 0.5rem; | |
} | |
&__input { | |
position: relative; | |
z-index: 10; | |
height: 100%; | |
} | |
&__append { | |
right: 4.85px; | |
display: flex; | |
align-items: center; | |
height: 100%; | |
position: absolute; | |
bottom: 0; | |
z-index: 10; | |
button { | |
margin-left: 0; | |
margin-right: 0; | |
padding-left: 0.5rem; | |
padding-right: 0.5rem; | |
} | |
} | |
&--hasLabel { | |
.input-container__inner__input ::placeholder { | |
display: none; | |
visibility: hidden; | |
} | |
} | |
&--hasError { | |
input { | |
border: 1px solid var(--color-red); | |
} | |
} | |
&--hasAppend { | |
input { | |
padding-right: 3rem; | |
} | |
} | |
} | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// leave NAMED_MASKS at top of file (code referenced from docs) | |
const NAMED_MASKS = { | |
date: '####/##/##', | |
datetime: '####/##/## ##:##', | |
time: '##:##', | |
fulltime: '##:##:##', | |
phone: '(###) ### - ####', | |
card: '#### #### #### ####' | |
} | |
const TOKENS = { | |
'#': { pattern: '[\\d]', negate: '[^\\d]' }, | |
S: { pattern: '[a-zA-Z]', negate: '[^a-zA-Z]' }, | |
N: { pattern: '[0-9a-zA-Z]', negate: '[^0-9a-zA-Z]' }, | |
A: { | |
pattern: '[a-zA-Z]', | |
negate: '[^a-zA-Z]', | |
transform: v => v.toLocaleUpperCase() | |
}, | |
a: { | |
pattern: '[a-zA-Z]', | |
negate: '[^a-zA-Z]', | |
transform: v => v.toLocaleLowerCase() | |
}, | |
X: { | |
pattern: '[0-9a-zA-Z]', | |
negate: '[^0-9a-zA-Z]', | |
transform: v => v.toLocaleUpperCase() | |
}, | |
x: { | |
pattern: '[0-9a-zA-Z]', | |
negate: '[^0-9a-zA-Z]', | |
transform: v => v.toLocaleLowerCase() | |
} | |
} | |
const KEYS = Object.keys(TOKENS) | |
KEYS.forEach(key => { | |
TOKENS[key].regex = new RegExp(TOKENS[key].pattern) | |
}) | |
const tokenRegexMask = new RegExp( | |
'\\\\([^.*+?^${}()|([\\]])|([.*+?^${}()|[\\]])|([' + | |
KEYS.join('') + | |
'])|(.)', | |
'g' | |
), | |
escRegex = /[.*+?^${}()|[\]\\]/g | |
const MARKER = String.fromCharCode(1) | |
export default { | |
props: { | |
mask: String, | |
reverseFillMask: Boolean, | |
fillMask: [Boolean, String], | |
unmaskedValue: Boolean | |
}, | |
watch: { | |
type() { | |
this.__updateMaskInternals() | |
}, | |
mask(v) { | |
if (v !== void 0) { | |
this.__updateMaskValue(this.innerValue, true) | |
} else { | |
const val = this.__unmask(this.innerValue) | |
this.__updateMaskInternals() | |
this.value !== val && this.$emit('input', val) | |
} | |
}, | |
fillMask() { | |
this.hasMask === true && this.__updateMaskValue(this.innerValue, true) | |
}, | |
reverseFillMask() { | |
this.hasMask === true && this.__updateMaskValue(this.innerValue, true) | |
}, | |
unmaskedValue() { | |
this.hasMask === true && this.__updateMaskValue(this.innerValue) | |
} | |
}, | |
methods: { | |
__getInitialMaskedValue() { | |
this.__updateMaskInternals() | |
if (this.hasMask === true) { | |
const masked = this.__mask(this.__unmask(this.value)) | |
return this.fillMask !== false ? this.__fillWithMask(masked) : masked | |
} | |
return this.value | |
}, | |
__getPaddedMaskMarked(size) { | |
if (size < this.maskMarked.length) { | |
return this.maskMarked.slice(-size) | |
} | |
let maskMarked = this.maskMarked, | |
padPos = maskMarked.indexOf(MARKER), | |
pad = '' | |
if (padPos > -1) { | |
for (let i = size - maskMarked.length; i > 0; i--) { | |
pad += MARKER | |
} | |
maskMarked = | |
maskMarked.slice(0, padPos) + pad + maskMarked.slice(padPos) | |
} | |
return maskMarked | |
}, | |
__updateMaskInternals() { | |
this.hasMask = | |
this.mask !== void 0 && | |
this.mask.length > 0 && | |
['text', 'search', 'url', 'tel', 'password'].includes(this.type) | |
if (this.hasMask === false) { | |
this.computedUnmask = void 0 | |
this.maskMarked = '' | |
this.maskReplaced = '' | |
return | |
} | |
const computedMask = | |
NAMED_MASKS[this.mask] === void 0 | |
? this.mask | |
: NAMED_MASKS[this.mask], | |
fillChar = | |
typeof this.fillMask === 'string' && this.fillMask.length > 0 | |
? this.fillMask.slice(0, 1) | |
: '_', | |
fillCharEscaped = fillChar.replace(escRegex, '\\$&'), | |
unmask = [], | |
extract = [], | |
mask = [] | |
let firstMatch = this.reverseFillMask === true, | |
unmaskChar = '', | |
negateChar = '' | |
computedMask.replace(tokenRegexMask, (_, char1, esc, token, char2) => { | |
if (token !== void 0) { | |
const c = TOKENS[token] | |
mask.push(c) | |
negateChar = c.negate | |
if (firstMatch === true) { | |
extract.push( | |
'(?:' + | |
negateChar + | |
'+?)?(' + | |
c.pattern + | |
'+)?(?:' + | |
negateChar + | |
'+?)?(' + | |
c.pattern + | |
'+)?' | |
) | |
firstMatch = false | |
} | |
extract.push('(?:' + negateChar + '+?)?(' + c.pattern + ')?') | |
} else if (esc !== void 0) { | |
unmaskChar = '\\' + esc | |
mask.push(esc) | |
unmask.push('([^' + unmaskChar + ']+)?' + unmaskChar + '?') | |
} else { | |
const c = char1 !== void 0 ? char1 : char2 | |
unmaskChar = c.replace(escRegex, '\\\\$&') | |
mask.push(c) | |
unmask.push('([^' + unmaskChar + ']+)?' + unmaskChar + '?') | |
} | |
}) | |
const unmaskMatcher = new RegExp( | |
'^' + | |
unmask.join('') + | |
'(' + | |
(unmaskChar === '' ? '.' : '[^' + unmaskChar + ']') + | |
'+)?' + | |
'$' | |
), | |
extractMatcher = new RegExp( | |
'^' + | |
(this.reverseFillMask === true ? fillCharEscaped + '*' : '') + | |
extract.join('') + | |
'(' + | |
(negateChar === '' ? '.' : negateChar) + | |
'+)?' + | |
(this.reverseFillMask === true ? '' : fillCharEscaped + '*') + | |
'$' | |
) | |
this.computedMask = mask | |
this.computedUnmask = val => { | |
const unmaskMatch = unmaskMatcher.exec(val) | |
if (unmaskMatch !== null) { | |
val = unmaskMatch.slice(1).join('') | |
} | |
const extractMatch = extractMatcher.exec(val) | |
if (extractMatch !== null) { | |
return extractMatch.slice(1).join('') | |
} | |
return val | |
} | |
this.maskMarked = mask | |
.map(v => (typeof v === 'string' ? v : MARKER)) | |
.join('') | |
this.maskReplaced = this.maskMarked.split(MARKER).join(fillChar) | |
}, | |
__updateMaskValue(rawVal, updateMaskInternals) { | |
const inp = this.$refs.input, | |
oldCursor = | |
this.reverseFillMask === true | |
? inp.value.length - inp.selectionEnd | |
: inp.selectionEnd, | |
unmasked = this.__unmask(rawVal) | |
// Update here so unmask uses the original fillChar | |
updateMaskInternals === true && this.__updateMaskInternals() | |
const masked = | |
this.fillMask !== false | |
? this.__fillWithMask(this.__mask(unmasked)) | |
: this.__mask(unmasked), | |
changed = this.innerValue !== masked | |
// We want to avoid "flickering" so we set value immediately | |
inp.value !== masked && (inp.value = masked) | |
changed === true && (this.innerValue = masked) | |
this.$nextTick(() => { | |
if (this.reverseFillMask === true) { | |
if (changed === true) { | |
const cursor = Math.max( | |
0, | |
masked.length - (masked === this.maskReplaced ? 0 : oldCursor + 1) | |
) | |
this.__moveCursorRightReverse(inp, cursor, cursor) | |
} else { | |
const cursor = masked.length - oldCursor | |
inp.setSelectionRange(cursor, cursor) | |
} | |
} else if (changed === true) { | |
if (masked === this.maskReplaced) { | |
this.__moveCursorLeft(inp, 0, 0) | |
} else { | |
const cursor = Math.max( | |
0, | |
this.maskMarked.indexOf(MARKER), | |
oldCursor - 1 | |
) | |
this.__moveCursorRight(inp, cursor, cursor) | |
} | |
} else { | |
this.__moveCursorLeft(inp, oldCursor, oldCursor) | |
} | |
}) | |
const val = this.unmaskedValue === true ? this.__unmask(masked) : masked | |
this.value !== val && this.__emitValue(val, true) | |
}, | |
__moveCursorLeft(inp, start, end, selection) { | |
const noMarkBefore = | |
this.maskMarked.slice(start - 1).indexOf(MARKER) === -1 | |
let i = Math.max(0, start - 1) | |
for (; i >= 0; i--) { | |
if (this.maskMarked[i] === MARKER) { | |
start = i | |
noMarkBefore === true && start++ | |
break | |
} | |
} | |
if ( | |
i < 0 && | |
this.maskMarked[start] !== void 0 && | |
this.maskMarked[start] !== MARKER | |
) { | |
return this.__moveCursorRight(inp, 0, 0) | |
} | |
start >= 0 && | |
inp.setSelectionRange( | |
start, | |
selection === true ? end : start, | |
'backward' | |
) | |
}, | |
__moveCursorRight(inp, start, end, selection) { | |
const limit = inp.value.length | |
let i = Math.min(limit, end + 1) | |
for (; i <= limit; i++) { | |
if (this.maskMarked[i] === MARKER) { | |
end = i | |
break | |
} else if (this.maskMarked[i - 1] === MARKER) { | |
end = i | |
} | |
} | |
if ( | |
i > limit && | |
this.maskMarked[end - 1] !== void 0 && | |
this.maskMarked[end - 1] !== MARKER | |
) { | |
return this.__moveCursorLeft(inp, limit, limit) | |
} | |
inp.setSelectionRange(selection ? start : end, end, 'forward') | |
}, | |
__moveCursorLeftReverse(inp, start, end, selection) { | |
const maskMarked = this.__getPaddedMaskMarked(inp.value.length) | |
let i = Math.max(0, start - 1) | |
for (; i >= 0; i--) { | |
if (maskMarked[i - 1] === MARKER) { | |
start = i | |
break | |
} else if (maskMarked[i] === MARKER) { | |
start = i | |
if (i === 0) { | |
break | |
} | |
} | |
} | |
if ( | |
i < 0 && | |
maskMarked[start] !== void 0 && | |
maskMarked[start] !== MARKER | |
) { | |
return this.__moveCursorRightReverse(inp, 0, 0) | |
} | |
start >= 0 && | |
inp.setSelectionRange( | |
start, | |
selection === true ? end : start, | |
'backward' | |
) | |
}, | |
__moveCursorRightReverse(inp, start, end, selection) { | |
const limit = inp.value.length, | |
maskMarked = this.__getPaddedMaskMarked(limit), | |
noMarkBefore = maskMarked.slice(0, end + 1).indexOf(MARKER) === -1 | |
let i = Math.min(limit, end + 1) | |
for (; i <= limit; i++) { | |
if (maskMarked[i - 1] === MARKER) { | |
end = i | |
end > 0 && noMarkBefore === true && end-- | |
break | |
} | |
} | |
if ( | |
i > limit && | |
maskMarked[end - 1] !== void 0 && | |
maskMarked[end - 1] !== MARKER | |
) { | |
return this.__moveCursorLeftReverse(inp, limit, limit) | |
} | |
inp.setSelectionRange(selection === true ? start : end, end, 'forward') | |
}, | |
__onMaskedKeydown(e) { | |
const inp = this.$refs.input, | |
start = inp.selectionStart, | |
end = inp.selectionEnd | |
if (e.keyCode === 37 || e.keyCode === 39) { | |
// Left / Right | |
const fn = this[ | |
'__moveCursor' + | |
(e.keyCode === 39 ? 'Right' : 'Left') + | |
(this.reverseFillMask === true ? 'Reverse' : '') | |
] | |
e.preventDefault() | |
fn(inp, start, end, e.shiftKey) | |
} else if ( | |
e.keyCode === 8 && // Backspace | |
this.reverseFillMask !== true && | |
start === end | |
) { | |
this.__moveCursorLeft(inp, start, end, true) | |
} else if ( | |
e.keyCode === 46 && // Delete | |
this.reverseFillMask === true && | |
start === end | |
) { | |
this.__moveCursorRightReverse(inp, start, end, true) | |
} | |
this.$emit('keydown', e) | |
}, | |
__mask(val) { | |
if (val === void 0 || val === null || val === '') { | |
return '' | |
} | |
if (this.reverseFillMask === true) { | |
return this.__maskReverse(val) | |
} | |
const mask = this.computedMask | |
let valIndex = 0, | |
output = '' | |
for (let maskIndex = 0; maskIndex < mask.length; maskIndex++) { | |
const valChar = val[valIndex], | |
maskDef = mask[maskIndex] | |
if (typeof maskDef === 'string') { | |
output += maskDef | |
valChar === maskDef && valIndex++ | |
} else if (valChar !== void 0 && maskDef.regex.test(valChar)) { | |
output += | |
maskDef.transform !== void 0 ? maskDef.transform(valChar) : valChar | |
valIndex++ | |
} else { | |
return output | |
} | |
} | |
return output | |
}, | |
__maskReverse(val) { | |
const mask = this.computedMask, | |
firstTokenIndex = this.maskMarked.indexOf(MARKER) | |
let valIndex = val.length - 1, | |
output = '' | |
for (let maskIndex = mask.length - 1; maskIndex >= 0; maskIndex--) { | |
const maskDef = mask[maskIndex] | |
let valChar = val[valIndex] | |
if (typeof maskDef === 'string') { | |
output = maskDef + output | |
valChar === maskDef && valIndex-- | |
} else if (valChar !== void 0 && maskDef.regex.test(valChar)) { | |
do { | |
output = | |
(maskDef.transform !== void 0 | |
? maskDef.transform(valChar) | |
: valChar) + output | |
valIndex-- | |
valChar = val[valIndex] | |
// eslint-disable-next-line no-unmodified-loop-condition | |
} while ( | |
firstTokenIndex === maskIndex && | |
valChar !== void 0 && | |
maskDef.regex.test(valChar) | |
) | |
} else { | |
return output | |
} | |
} | |
return output | |
}, | |
__unmask(val) { | |
return typeof val !== 'string' || this.computedUnmask === void 0 | |
? val | |
: this.computedUnmask(val) | |
}, | |
__fillWithMask(val) { | |
if (this.maskReplaced.length - val.length <= 0) { | |
return val | |
} | |
return this.reverseFillMask === true && val.length > 0 | |
? this.maskReplaced.slice(0, -val.length) + val | |
: val + this.maskReplaced.slice(val.length) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment