Skip to content

Commit

Permalink
feat: add custom colon property (closes #1)
Browse files Browse the repository at this point in the history
  • Loading branch information
antonfisher committed Sep 10, 2017
1 parent 6297483 commit 554c144
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 36 deletions.
12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import TimeField from 'react-simple-timefield';
value={time} // {String} required, format '00:00' or '00:00:00'
onChange={(value) => {...}} // {Function} required
input={<MyCustomInputElement>} // {Element} default: <input>
colon=":" // {String} default: ":"
showSeconds // {Boolean} default: false
/>
```
Expand Down Expand Up @@ -48,20 +49,14 @@ class App extends React.Component {
const {time} = this.state;

return (
<section>
<TimeField value={time} onChange={this.onTimeChange} />
<TimeField
value={time}
onChange={this.onTimeChange}
style={{color: '#333'}}
/>
</section>
<TimeField value={time} onChange={this.onTimeChange} />
);
}
}
```

## Changelog
* 1.3.0 Added custom colon property
* 1.2.0 Added custom input field property
* 1.1.0 Added `showSeconds` property
* 1.0.0 Initial release
Expand All @@ -87,6 +82,7 @@ npm run lint
- [x] Support full time format with seconds
- [x] Tests
- [x] Custom input field (like Material UI TextField)
- [x] Custom colon
- [ ] Support for Date object as value

## License
Expand Down
30 changes: 25 additions & 5 deletions demo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,24 @@ class App extends React.Component {

this.state = {
time: '12:34',
timeSeconds: '12:34:56'
timeSeconds: '12:34:56',
timeSecondsCustomColon: '12-34-56'
};

this.onTimeChange = this.onTimeChange.bind(this);
}

onTimeChange(newTime) {
onTimeChange(value) {
const newTime = value.replace(/-/g, ':');
const time = newTime.substr(0, 5);
const timeSeconds = newTime.padEnd(8, this.state.timeSeconds.substr(5, 3));
const timeSecondsCustomColon = timeSeconds.replace(/:/g, '-');

this.setState({time, timeSeconds});
this.setState({time, timeSeconds, timeSecondsCustomColon});
}

render() {
const {time, timeSeconds} = this.state;
const {time, timeSeconds, timeSecondsCustomColon} = this.state;

const muiTheme = getMuiTheme({
fontFamily: 'Arial',
Expand Down Expand Up @@ -75,10 +78,27 @@ class App extends React.Component {
}}
/>
</section>
<h2>Custom colon:</h2>
<section>
<TimeField
showSeconds
colon="-"
value={timeSecondsCustomColon}
onChange={this.onTimeChange}
style={{
border: '2px solid #666',
fontSize: 42,
width: 170,
padding: '5px 8px',
color: '#333',
borderRadius: 3
}}
/>
</section>
<h2>React Material-UI:</h2>
<section>
<MuiThemeProvider muiTheme={muiTheme}>
<div>
<div style={{marginRight: 20}}>
<IconClock style={{width: 25, marginRight: 6, marginBottom: -6}} color="#bbb" />
<TimeField
showSeconds
Expand Down
2 changes: 1 addition & 1 deletion demo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-simple-timefield-demo",
"version": "1.1.0",
"version": "1.3.0",
"scripts": {
"dev": "webpack-dev-server --open --progress --config=webpack.config.dev.js",
"build": "webpack --p --display-error-details --config=webpack.config.prod.js"
Expand Down
4 changes: 2 additions & 2 deletions docs/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="react,timefield,timeinput,input"><meta name="mobile-web-app-capable" content="yes"><meta name="author" content="Anton Fisher"><title>react-simple-timefield - Demos</title><link href="/favicon.ico" rel="icon"><style>body,html{margin:0;width:100%;font-size:.95rem;font-family:Comfortaa,monospace}body{display:flex;flex-direction:column;align-items:center}#app,footer,header{width:92%;text-align:center}header{margin-top:33px;display:flex;flex-direction:column;align-items:center;margin-bottom:60px}.container>section{margin-bottom:40px}h1{font-size:1.7rem;display:inline-block;padding:20px 40px;border:13px solid rgba(255,255,0,.33);margin-top:20px}h2{line-height:1px;font-size:1.1rem;display:inline-block;border-bottom:13px solid rgba(255,255,0,.33)}a,a:visited{color:#00f}a:hover{color:#00bfff}footer{font-size:.75rem;margin:30px 0 15px}@media all and (min-width:900px){body>section,header{width:75%}}@media all and (min-width:1600px){body>section,header{width:50%}}</style></head><body><noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>, please <strong>enable JS</strong> to make this app work.</noscript><header><h1>react-simple-timefield demos</h1><a href="https://github.com/antonfisher/react-simple-timefield/blob/master/demo/index.js">Demo source code</a></header><section id="app"></section><footer>MIT License | 2017 | <a href="https://antonfisher.com">Anton Fisher</a></footer><link href="https://fonts.googleapis.com/css?family=Comfortaa" rel="stylesheet"><script type="text/javascript" src="bundle.js?4c64c353effd05143e74"></script></body></html>
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="react,timefield,timeinput,input"><meta name="mobile-web-app-capable" content="yes"><meta name="author" content="Anton Fisher"><title>react-simple-timefield - Demos</title><link href="/favicon.ico" rel="icon"><style>body,html{margin:0;width:100%;font-size:.95rem;font-family:Comfortaa,monospace}body{display:flex;flex-direction:column;align-items:center}#app,footer,header{width:92%;text-align:center}header{margin-top:33px;display:flex;flex-direction:column;align-items:center;margin-bottom:60px}.container>section{margin-bottom:40px}h1{font-size:1.7rem;display:inline-block;padding:20px 40px;border:13px solid rgba(255,255,0,.33);margin-top:20px}h2{line-height:1px;font-size:1.1rem;display:inline-block;border-bottom:13px solid rgba(255,255,0,.33)}a,a:visited{color:#00f}a:hover{color:#00bfff}footer{font-size:.75rem;margin:30px 0 15px}@media all and (min-width:900px){body>section,header{width:75%}}@media all and (min-width:1600px){body>section,header{width:50%}}</style></head><body><noscript>If you're seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>, please <strong>enable JS</strong> to make this app work.</noscript><header><h1>react-simple-timefield demos</h1><a href="https://github.com/antonfisher/react-simple-timefield/blob/master/demo/index.js">Demo source code</a></header><section id="app"></section><footer>MIT License | 2017 | <a href="https://antonfisher.com">Anton Fisher</a></footer><link href="https://fonts.googleapis.com/css?family=Comfortaa" rel="stylesheet"><script type="text/javascript" src="bundle.js?cfbcae1ddca0c9acb8cf"></script></body></html>
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "react-simple-timefield",
"version": "1.2.0",
"version": "1.3.0",
"description": "Simple React time input field",
"author": "Anton Fisher <[email protected]> (http://antonfisher.com)",
"license": "MIT",
"main": "index.js",
"scripts": {
"build": "babel -d dist src",
"build": "babel -d dist src && cd ./demo && npm run build",
"test": "jest",
"lint": "eslint --ignore-path .gitignore --ignore-pattern node_modules --ignore-pattern docs -- .",
"cover": "jest --coverage",
Expand Down
45 changes: 30 additions & 15 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';

const DEFAULT_VALUE_SHORT = '00:00';
const DEFAULT_VALUE_FULL = '00:00:00';
const DEFAULT_COLON = ':';
const DEFAULT_VALUE_SHORT = `00${DEFAULT_COLON}00`;
const DEFAULT_VALUE_FULL = `00${DEFAULT_COLON}00${DEFAULT_COLON}00`;

export function isNumber(value) {
const number = Number(value);
Expand All @@ -13,11 +14,11 @@ export function formatTimeItem(value) {
return (`${value || ''}00`).substr(0, 2);
}

export function validateTimeAndCursor(showSeconds, value, defaultValue, cursorPosition) {
const [oldH, oldM, oldS] = defaultValue.split(':');
export function validateTimeAndCursor(showSeconds, value, defaultValue, colon = DEFAULT_COLON, cursorPosition = 0) {
const [oldH, oldM, oldS] = defaultValue.split(colon);

let newCursorPosition = Number(cursorPosition);
let [newH, newM, newS] = value.split(':');
let [newH, newM, newS] = value.split(colon);

newH = formatTimeItem(newH);
if (Number(newH[0]) > 2) {
Expand Down Expand Up @@ -46,7 +47,7 @@ export function validateTimeAndCursor(showSeconds, value, defaultValue, cursorPo
}
}

const validatedValue = (showSeconds ? `${newH}:${newM}:${newS}` : `${newH}:${newM}`);
const validatedValue = (showSeconds ? `${newH}${colon}${newM}${colon}${newS}` : `${newH}${colon}${newM}`);

return [validatedValue, newCursorPosition];
}
Expand All @@ -57,21 +58,28 @@ export default class TimeField extends React.Component {
onChange: PropTypes.func.isRequired,
showSeconds: PropTypes.bool,
input: PropTypes.element,
colon: PropTypes.string,
style: PropTypes.object
};

static defaultProps = {
showSeconds: false,
input: null,
style: {}
style: {},
colon: DEFAULT_COLON
};

constructor(props, ...args) {
super(props, ...args);

this.configure(props);

const [validatedTime] = validateTimeAndCursor(this._showSeconds, this.props.value, this._defaultValue);
const [validatedTime] = validateTimeAndCursor(
this._showSeconds,
this.props.value,
this._defaultValue,
this._colon
);

this.state = {
value: validatedTime
Expand All @@ -86,14 +94,20 @@ export default class TimeField extends React.Component {
this.configure(nextProps);

if (value !== nextProps.value) {
const [validatedTime] = validateTimeAndCursor(this._showSeconds, nextProps.value, this._defaultValue);
const [validatedTime] = validateTimeAndCursor(
this._showSeconds,
nextProps.value,
this._defaultValue,
this._colon
);
this.setState({
value: validatedTime
});
}
}

configure(props) {
this._colon = (props.colon && props.colon.length === 1 ? props.colon : DEFAULT_COLON);
this._showSeconds = Boolean(props.showSeconds);
this._defaultValue = (this._showSeconds ? DEFAULT_VALUE_FULL : DEFAULT_VALUE_SHORT);
this._maxLength = this._defaultValue.length;
Expand All @@ -108,17 +122,18 @@ export default class TimeField extends React.Component {
const isType = (inputValue.length > oldValue.length);
const addedCharacter = (isType ? inputValue[position - 1] : null);
const removedCharacter = (isType ? null : oldValue[position]);
const colon = this._colon;

let newValue = oldValue;
let newPosition = position;

if (addedCharacter !== null) {
if (position > this._maxLength) {
newPosition = this._maxLength;
} else if ((position === 3 || position === 6) && addedCharacter === ':') {
newValue = `${inputValue.substr(0, position - 1)}:${inputValue.substr(position + 1)}`;
} else if ((position === 3 || position === 6) && addedCharacter === colon) {
newValue = `${inputValue.substr(0, position - 1)}${colon}${inputValue.substr(position + 1)}`;
} else if ((position === 3 || position === 6) && isNumber(addedCharacter)) {
newValue = `${inputValue.substr(0, position - 1)}:${addedCharacter}${inputValue.substr(position + 2)}`;
newValue = `${inputValue.substr(0, position - 1)}${colon}${addedCharacter}${inputValue.substr(position + 2)}`;
newPosition = (position + 1);
} else if (isNumber(addedCharacter)) { // user typed a number
newValue = (inputValue.substr(0, position - 1) + addedCharacter + inputValue.substr(position + 1));
Expand All @@ -129,8 +144,8 @@ export default class TimeField extends React.Component {
newPosition = (position - 1);
}
} else if (removedCharacter !== null) {
if ((position === 2 || position === 5) && removedCharacter === ':') {
newValue = `${inputValue.substr(0, position - 1)}0:${inputValue.substr(position)}`;
if ((position === 2 || position === 5) && removedCharacter === colon) {
newValue = `${inputValue.substr(0, position - 1)}0${colon}${inputValue.substr(position)}`;
newPosition = position - 1;
} else { // user removed a number
newValue = `${inputValue.substr(0, position)}0${inputValue.substr(position)}`;
Expand All @@ -140,7 +155,7 @@ export default class TimeField extends React.Component {
const [
validatedTime,
validatedCursorPosition
] = validateTimeAndCursor(this._showSeconds, newValue, oldValue, newPosition);
] = validateTimeAndCursor(this._showSeconds, newValue, oldValue, this._colon, newPosition);

this.setState({value: validatedTime}, () => {
inputEl.selectionStart = validatedCursorPosition;
Expand Down
11 changes: 9 additions & 2 deletions tests/unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,26 @@ describe('#validateTimeAndCursor()', () => {
const DF = '00:00:00';

test('should return an array', () => {
const res = validateTimeAndCursor(true, '', DF, 0);
const res = validateTimeAndCursor(true, '', DF, ':', 0);
expect(res).toBeInstanceOf(Array);
expect(res).toHaveLength(2);
expect(res).toEqual([DF, 0]);
});

test('should handle showSeconds option', () => {
test('should handle "showSeconds" option', () => {
expect(validateTimeAndCursor(true, '12:34:56', DF)[0]).toEqual('12:34:56');
expect(validateTimeAndCursor(true, '12:34', DF)[0]).toEqual('12:34:00');
expect(validateTimeAndCursor(false, '12:34:56', DF)[0]).toEqual('12:34');
expect(validateTimeAndCursor(false, '12:34', DF)[0]).toEqual('12:34');
});

test('should handle "colon" option', () => {
expect(validateTimeAndCursor(true, '12-34-56', DF, '-')[0]).toEqual('12-34-56');
expect(validateTimeAndCursor(true, '12-34', DF, '-')[0]).toEqual('12-34-00');
expect(validateTimeAndCursor(false, '12-34-56', DF, '-')[0]).toEqual('12-34');
expect(validateTimeAndCursor(false, '12-34', DF, '-')[0]).toEqual('12-34');
});

test('should return default value if bad format of hours', () => {
expect(validateTimeAndCursor(false, '30:00', DF)[0]).toEqual('00:00');
expect(validateTimeAndCursor(false, ':', DF)[0]).toEqual('00:00');
Expand Down

0 comments on commit 554c144

Please sign in to comment.