Skip to content

Commit

Permalink
Email mask (text-mask#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
msafi authored Aug 7, 2016
1 parent c6269b8 commit fc5017b
Show file tree
Hide file tree
Showing 26 changed files with 474 additions and 76 deletions.
21 changes: 18 additions & 3 deletions addons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ These addons are ready-to-use pipes and masks that can be used with Text Mask.
npm i text-mask-addons --save
```

## Mask functions
## Masks

These functions here can be passed as a
These can be passed as a
[`mask`](https://github.com/msafi/text-mask/blob/master/componentDocumentation.md#mask)
to Text Mask.

Expand All @@ -29,7 +29,7 @@ to Text Mask.
1. `requireDecimal` (boolean): whether or not to always include a decimal point and placeholder for decimal digits
after the integer. Defaults to `false`.

### Usage
#### Usage

```js
import createNumberMask from 'text-mask-addons/dist/createNumberMask.js'
Expand All @@ -42,6 +42,21 @@ const numberMask = createNumberMask({
// ...then pass `numberMask` to the Text Mask component
```

### `emailMask`

`emailMask` formats user input as an email address.

#### Usage

```js
import emailMask from 'text-mask-addons/dist/emailMask.js'

// ...then pass `emailMask` to the Text Mask component
```

*Technical side note*: even though `emailMask` is passed as a `mask`, it is actually made of both a `mask` and a `pipe` bundled
together for convenience. The Text Mask component knows how to unwrap and separate the `pipe` and `mask` functions to use them.

## Pipes

These functions here can be passed as a
Expand Down
1 change: 1 addition & 0 deletions addons/dist/emailMask.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions addons/src/createNumberMask.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const dollarSign = '$'
const emptyString = ''
const comma = ','
const period = '.'
const noneDigitsRegExp = /\D+/g
const nonDigitsRegExp = /\D+/g
const number = 'number'
const digitRegExp = /\d/

Expand Down Expand Up @@ -37,12 +37,12 @@ export default function createNumberMask({
integer = rawValue.slice(0, indexOfLastDecimal)

fraction = rawValue.slice(indexOfLastDecimal + 1, rawValueLength)
fraction = convertToMask(fraction.replace(noneDigitsRegExp, emptyString))
fraction = convertToMask(fraction.replace(nonDigitsRegExp, emptyString))
} else {
integer = rawValue
}

integer = integer.replace(noneDigitsRegExp, emptyString)
integer = integer.replace(nonDigitsRegExp, emptyString)

integer = (includeThousandsSeparator) ? addThousandsSeparator(integer, thousandsSeparatorSymbol) : integer

Expand Down
112 changes: 112 additions & 0 deletions addons/src/emailMask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import emailPipe from './emailPipe.js'

const asterisk = '*'
const dot = '.'
const emptyString = ''
const atSymbol = '@'
const caretTrap = '[]'
const space = ' '
const g = 'g'
const anyNonWhitespaceRegExp = /[^\s]/
const anyNonDotOrWhitespaceRegExp = /[^.\s]/
const allWhitespaceRegExp = /\s/g

function emailMask(rawValue, config) {
rawValue = rawValue.replace(allWhitespaceRegExp, emptyString)

const {placeholderChar, currentCaretPosition} = config
const indexOfFirstAtSymbol = rawValue.indexOf(atSymbol)
const indexOfLastDot = rawValue.lastIndexOf(dot)
const indexOfTopLevelDomainDot = (indexOfLastDot < indexOfFirstAtSymbol) ? -1 : indexOfLastDot

let localPartToDomainConnector = getConnector(rawValue, indexOfFirstAtSymbol + 1, atSymbol)
let domainNameToTopLevelDomainConnector = getConnector(rawValue, indexOfTopLevelDomainDot - 1, dot)

let localPart = getLocalPart(rawValue, indexOfFirstAtSymbol, placeholderChar)
let domainName = getDomainName(rawValue, indexOfFirstAtSymbol, indexOfTopLevelDomainDot, placeholderChar)
let topLevelDomain = getTopLevelDomain(rawValue, indexOfTopLevelDomainDot, placeholderChar, currentCaretPosition)

localPart = convertToMask(localPart)
domainName = convertToMask(domainName)
topLevelDomain = convertToMask(topLevelDomain, true)

const mask = localPart
.concat(localPartToDomainConnector)
.concat(domainName)
.concat(domainNameToTopLevelDomainConnector)
.concat(topLevelDomain)

return mask
}

function getConnector(rawValue, indexOfConnection, connectionSymbol) {
const connector = []

if (rawValue[indexOfConnection] === connectionSymbol) {
connector.push(connectionSymbol)
} else {
connector.push(caretTrap, connectionSymbol)
}

connector.push(caretTrap)

return connector
}

function getLocalPart(rawValue, indexOfFirstAtSymbol) {
if (indexOfFirstAtSymbol === -1) {
return rawValue
} else {
return rawValue.slice(0, indexOfFirstAtSymbol)
}
}

function getDomainName(rawValue, indexOfFirstAtSymbol, indexOfTopLevelDomainDot, placeholderChar) {
let domainName = emptyString

if (indexOfFirstAtSymbol !== -1) {
if (indexOfTopLevelDomainDot === -1) {
domainName = rawValue.slice(indexOfFirstAtSymbol + 1, rawValue.length)
} else {
domainName = rawValue.slice(indexOfFirstAtSymbol + 1, indexOfTopLevelDomainDot)
}
}

domainName = domainName.replace(new RegExp(`[\\s${placeholderChar}]`, g), emptyString)

if (domainName === atSymbol) {
return asterisk
} else if (domainName.length < 1) {
return space
} else if (domainName[domainName.length - 1] === dot) {
return domainName.slice(0, domainName.length - 1)
} else {
return domainName
}
}

function getTopLevelDomain(rawValue, indexOfTopLevelDomainDot, placeholderChar, currentCaretPosition) {
let topLevelDomain = emptyString

if (indexOfTopLevelDomainDot !== -1) {
topLevelDomain = rawValue.slice(indexOfTopLevelDomainDot + 1, rawValue.length)
}

topLevelDomain = topLevelDomain.replace(new RegExp(`[\\s${placeholderChar}.]`, g), emptyString)

if (topLevelDomain.length === 0) {
return (rawValue[indexOfTopLevelDomainDot - 1] === dot && currentCaretPosition !== rawValue.length) ?
asterisk :
emptyString
} else {
return topLevelDomain
}
}

function convertToMask(str, noDots) {
return str
.split(emptyString)
.map((char) => char === space ? char : (noDots) ? anyNonDotOrWhitespaceRegExp : anyNonWhitespaceRegExp)
}

export default {mask: emailMask, pipe: emailPipe}
55 changes: 55 additions & 0 deletions addons/src/emailPipe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const atSymbol = '@'
const allAtSymbolsRegExp = /@/g
const emptyString = ''
const atDot = '@.'
const dot = '.'
const dotDot = '..'
const emptyArray = []
const allDotsRegExp = /\./g

export default function emailPipe(conformedValue, config) {
const {currentCaretPosition, rawValue, previousConformedValue, placeholderChar} = config

let value = conformedValue

value = removeAllAtSymbolsButFirst(value)

const indexOfAtDot = value.indexOf(atDot)

const emptyEmail = rawValue.match(new RegExp(`[^@\\s.${placeholderChar}]`)) === null

if (emptyEmail) {
return emptyString
}

if (
value.indexOf(dotDot) !== -1 ||
indexOfAtDot !== -1 && currentCaretPosition !== (indexOfAtDot + 1) ||
rawValue.indexOf(atSymbol) === -1 && previousConformedValue !== emptyString && rawValue.indexOf(dot) !== -1
) {
return false
}

const indexOfAtSymbol = value.indexOf(atSymbol)
const domainPart = value.slice(indexOfAtSymbol + 1, value.length)

if (
(domainPart.match(allDotsRegExp) || emptyArray).length > 1 &&
value.substr(-1) === dot &&
currentCaretPosition !== rawValue.length
) {
value = value.slice(0, value.length - 1)
}

return value
}

function removeAllAtSymbolsButFirst(str) {
let atSymbolCount = 0

return str.replace(allAtSymbolsRegExp, () => {
atSymbolCount++

return (atSymbolCount === 1) ? atSymbol : emptyString
})
}
158 changes: 158 additions & 0 deletions addons/test/emailMask.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import createTextMaskInputElement from '../../core/src/createTextMaskInputElement.js'

const emailMask = (isVerify()) ?
require('../dist/emailMask.js').default :
require('../src/emailMask.js').default

describe('emailMask', () => {
let inputElement
let textMaskInputElement

beforeEach(() => {
inputElement = document.createElement('input')
textMaskInputElement = createTextMaskInputElement({inputElement, mask: emailMask})
})

it('masks initial input as follows `a@ .`', () => {
input('a', 1)
expectResults('a@ .', 1)
})

it('allows a dot at the end of the local part', () => {
input('a', 1)
expectResults('a@ .', 1)

input('a.@ .', 2)
expectResults('a.@ .', 2)
})

it('moves the caret to after the @ symbol when user enters an @ symbol where the current @ symbol is', () => {
input('a', 1)
expectResults('a@ .', 1)

input('a@@ .', 2)
expectResults('a@_.', 2)
})

it('moves the caret to after the TLD dot when user enters a dot where the current TLD dot is', () => {
input('[email protected]', 7)
expectResults('[email protected]', 7)

input('[email protected]', 4)
expectResults('[email protected]', 4)
})

it('limits the number of @ symbols in input to 1', () => {
input('[email protected]', 7)
expectResults('[email protected]', 7)
input('@[email protected]', 1)
expectResults('@aa.com', 1)

input('[email protected]', 7)
expectResults('[email protected]', 7)
input('a@[email protected]', 4)
expectResults('[email protected]', 3)

input('[email protected]', 7)
expectResults('[email protected]', 7)
input('[email protected]@m', 7)
expectResults('[email protected]', 6)
})

it('does not add a placeholder in the end when user types a dot after the TLD dot when there is no TLD', () => {
input('a@a.', 4)
expectResults('a@a.', 4)

input('a@a..', 5)
expectResults('a@a.', 4)
})

it('removes the dot in the end if the domain part already contains a dot', () => {
input('a@acom.', 7)
expectResults('a@acom.', 7)

input('[email protected].', 4)
expectResults('[email protected]', 4)
})

it('prevents two consecutive dots', () => {
input('[email protected]', 9)
expectResults('[email protected]', 9)

input('[email protected]', 5)
expectResults('[email protected]', 4)
})

it('just moves the caret over when user enters a dot before the TLD dot', () => {
input('[email protected]', 7)
expectResults('[email protected]', 7)

input('[email protected]', 4)
expectResults('[email protected]', 4)
})

it('works as expected', () => {
input('a', 1)
expectResults('a@ .', 1)

input('@ .', 0)
expectResults('', 0)

input('a', 1)
expectResults('a@ .', 1)

input('a@@ .', 2)
expectResults('a@_.', 2)

input('a@f_.', 3)
expectResults('a@f.', 3)

input('af.', 1)
expectResults('a@f.', 1)

input('a.@f.', 2)
expectResults('a.@f.', 2)

input('m', 1)
expectResults('m@ .', 1)

input('m@k .', 3)
expectResults('m@k.', 3)

input('[email protected].', 3)
expectResults('m@k.', 2)

input('m@k', 3)
expectResults('m@k.', 3)

input('m@k..', 5)
expectResults('m@k.', 4)

input('[email protected]', 5)
expectResults('[email protected]', 5)

input('m@ks', 3)
expectResults('m@ks.', 3)

input('[email protected]', 6)
expectResults('[email protected]', 6)

input('[email protected]', 2)
expectResults('[email protected]', 2)

input('[email protected]', 3)
expectResults('[email protected]', 3)
})

function input(rawValue, currentCaretPosition) {
inputElement.focus()
inputElement.value = rawValue
inputElement.selectionStart = currentCaretPosition
textMaskInputElement.update()
}

function expectResults(conformedValue, adjustedCaretPosition) {
expect(inputElement.value).to.equal(conformedValue)
expect(inputElement.selectionStart).to.equal(adjustedCaretPosition)
}
})
Loading

0 comments on commit fc5017b

Please sign in to comment.