Skip to content

Commit bc7c095

Browse files
committed
Initial commit
0 parents  commit bc7c095

File tree

5 files changed

+349
-0
lines changed

5 files changed

+349
-0
lines changed

LICENSE

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Copyright 2020 Martti Laine
2+
3+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4+
5+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

README.md

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# boring-datepicker
2+
3+
> Styleable datepicker utilizing the native `<input type="date">`
4+
5+
Features:
6+
7+
- Light-weight, no dependencies
8+
- Includes optional React-component
9+
- Supports datetime strings (only replaces date-portion upon change)
10+
- Simple styling, with BEM classes
11+
12+
## Browser support
13+
14+
Supported:
15+
16+
- Chrome
17+
- Firefox
18+
- Edge
19+
- Safari iOS
20+
21+
Not supported (datepicker is hidden):
22+
23+
- Safari MacOS
24+
- IE
25+
26+
## Usage
27+
28+
### Vanilla JS
29+
30+
```js
31+
const BoringDatepicker = require('boring-datepicker');
32+
const picker = new BoringDatepicker({
33+
onChange: (newValue) => {
34+
console.log(newValue);
35+
},
36+
});
37+
someElement.appendChild(picker.element);
38+
```
39+
40+
See [API](#api).
41+
42+
### React
43+
44+
```jsx
45+
const BoringDatepicker = require('boring-datepicker/react');
46+
const SomeComponent = () => {
47+
const [date, setDate] = useState('2020-11-01');
48+
return (
49+
<BoringDatepicker value={date} onChange={(newValue) => setDate(newValue)} />
50+
);
51+
};
52+
```
53+
54+
See [React API](#react-api).
55+
56+
## API
57+
58+
### `class BoringDatepicker`
59+
60+
#### `constructor(options)`
61+
62+
`options` is an object with the following properties:
63+
64+
##### `options.onChange`
65+
66+
type: `function` default: `(value) => {}`
67+
68+
Callback function which is called when the user selects a new date.
69+
70+
Receives the new value as string (e.g. `"2020-11-01"` or `"2020-11-01 13:15:00"`; just the date-portion of the original value is replaced).
71+
72+
##### `options.existingElement`
73+
74+
type: `DOMElement` default: `null`
75+
76+
If set, existing element will be used instead of creating a new `span` element.
77+
78+
##### `options.win`
79+
80+
type: `Window` default: `window`
81+
82+
For the rare use case (e.g. using inside a child iframe) when setting `window` is necessary.
83+
84+
#### `setValue(dateString)`
85+
86+
Set the value of the datepicker.
87+
88+
`dateString` should be a string containing a date in `YYYY-MM-DD` format. For example, all of these are valid:
89+
90+
- `"2020-11-01"`
91+
- `"2020-11-01 13:15:00"`
92+
- `"2020-11-01T13:15:00"`
93+
94+
Upon changes, BoringDatepicker will replace the date-portion of the string and return the result.
95+
96+
#### `element`
97+
98+
Contains a reference to the datepicker element.
99+
100+
## React API
101+
102+
### `BoringDatepicker` component
103+
104+
Props:
105+
106+
```jsx
107+
<BoringDatepicker
108+
value={date}
109+
onChange={(newValue) => {}}
110+
className="customClassName"
111+
>
112+
{optionalChildren}
113+
</BoringDatepicker>
114+
```
115+
116+
#### `props.value`
117+
118+
type: `string` default: `""`
119+
120+
Initial value. Examples:
121+
122+
- `value="2020-11-01"`
123+
- `value="2020-11-01 13:15:00"`
124+
- `value="2020-11-01T13:15:00"`
125+
126+
#### `props.onChange`
127+
128+
type: `function` default: `(value) => {}`
129+
130+
Callback function which is called when the user selects a new date.
131+
132+
Receives the new value as string (e.g. `"2020-11-01"` or `"2020-11-01 13:15:00"`; just the date-portion of the original value is replaced).
133+
134+
#### `props.className`
135+
136+
type: `string` default: `""`
137+
138+
Custom className for the created element.
139+
140+
Example with `className="CustomClass"`:
141+
142+
```html
143+
<span class="BoringDatepicker CustomClass">
144+
<input class="BoringDatepicker__input" type="date" />
145+
</span>
146+
```
147+
148+
#### `optionalChildren`
149+
150+
If `children` are given, they are inserted into the resulting DOM. This can be used for any styling needs.
151+
152+
Example:
153+
154+
```html
155+
<span class="BoringDatepicker">
156+
<!-- Children will be inserted here -->
157+
<input class="BoringDatepicker__input" type="date" />
158+
</span>
159+
```
160+
161+
## Styling / DOM structure
162+
163+
The following DOM is created for each datepicker:
164+
165+
```html
166+
<span class="BoringDatepicker">
167+
<input class="BoringDatepicker__input" type="date" />
168+
</span>
169+
```
170+
171+
The recommended way to style the datepicker is to apply styles (e.g. width/height and a background-image) to the topmost element. Example:
172+
173+
```css
174+
.BoringDatepicker {
175+
width: 16px;
176+
height: 16px;
177+
background: transparent url(...) no-repeat center center;
178+
}
179+
```
180+
181+
Note: under normal circumstances you should not add any styles to `.BoringDatepicker__input`!
182+
183+
## Development
184+
185+
Source files reside in `src/`. They should be remain valid ES5 code (they are not precompiled in any way).
186+
187+
## License
188+
189+
[ISC](./LICENSE)

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "boring-datepicker",
3+
"version": "1.0.0",
4+
"main": "src/index.js",
5+
"repository": "https://github.com/codeclown/boring-datepicker",
6+
"author": "Martti Laine <martti@marttilaine.com>",
7+
"license": "ISC"
8+
}

src/index.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
const classNames = {
2+
wrapper: 'BoringDatepicker',
3+
input: 'BoringDatepicker__input',
4+
};
5+
6+
const dateRegex = /\d{4}-\d{2}-\d{2}/;
7+
8+
class BoringDatepicker {
9+
constructor(options) {
10+
this.options = Object.assign(
11+
{
12+
win: typeof window !== 'undefined' ? window : undefined,
13+
existingElement: null,
14+
onChange: () => {},
15+
},
16+
options
17+
);
18+
19+
this.addStylesheet();
20+
this.buildDom();
21+
}
22+
23+
setValue(fullString) {
24+
const match = fullString.match(dateRegex);
25+
if (match) {
26+
this.fullValue = fullString;
27+
this.dateValue = match[0];
28+
this.dateInputElement.value = match[0];
29+
}
30+
}
31+
32+
buildDom() {
33+
// DOM structure:
34+
// <span class="datepicker-toggle">
35+
// <!-- optional DOM nodes from user -->
36+
// <input type="date" class="datepicker-input">
37+
// </span>
38+
39+
const element =
40+
this.options.existingElement ||
41+
this.options.win.document.createElement('span');
42+
element.classList.add(classNames.wrapper);
43+
this.element = element;
44+
45+
if (!this.isSupported()) {
46+
// Not via CSS class because we don't want to mess with
47+
// CSS-set display values, to not mess up user styles
48+
element.style.display = 'none';
49+
}
50+
51+
const dateInputElement = this.options.win.document.createElement('input');
52+
dateInputElement.type = 'date';
53+
dateInputElement.classList.add(classNames.input);
54+
element.appendChild(dateInputElement);
55+
this.dateInputElement = dateInputElement;
56+
57+
dateInputElement.addEventListener('change', () => {
58+
let newValue = this.fullValue.replace(dateRegex, dateInputElement.value);
59+
// Regex didn't match, fallback to setting the entire value
60+
if (!newValue.includes(dateInputElement.value)) {
61+
newValue = dateInputElement.value;
62+
}
63+
dateInputElement.value = this.dateValue;
64+
this.options.onChange(newValue);
65+
});
66+
}
67+
68+
addStylesheet() {
69+
if (!this.options.win.document.querySelector('style#boringDatepicker')) {
70+
const style = this.options.win.document.createElement('style');
71+
style.id = 'boringDatepicker';
72+
style.textContent = `
73+
.${classNames.wrapper} {
74+
display: inline-block;
75+
position: relative;
76+
}
77+
.${classNames.input} {
78+
position: absolute;
79+
left: 0;
80+
top: 0;
81+
width: 100%;
82+
height: 100%;
83+
opacity: 0;
84+
cursor: pointer;
85+
box-sizing: border-box;
86+
}
87+
.${classNames.input}::-webkit-calendar-picker-indicator {
88+
position: absolute;
89+
left: 0;
90+
top: 0;
91+
width: 100%;
92+
height: 100%;
93+
margin: 0;
94+
padding: 0;
95+
cursor: pointer;
96+
}
97+
`;
98+
this.options.win.document.head.appendChild(style);
99+
}
100+
}
101+
102+
isSupported() {
103+
const input = this.options.win.document.createElement('input');
104+
input.type = 'date';
105+
input.value = 'invalid date value';
106+
return input.value !== 'invalid date value';
107+
}
108+
}
109+
110+
module.exports = BoringDatepicker;

src/react.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const { createElement: h, useRef, useEffect, useState } = require('react');
2+
const BoringDatepickerClass = require('./BoringDatepicker');
3+
4+
const BoringDatepicker = ({
5+
value = '',
6+
onChange = () => {},
7+
className = '',
8+
children,
9+
}) => {
10+
const spanRef = useRef(null);
11+
const [datepicker, setDatepicker] = useState();
12+
useEffect(() => {
13+
if (spanRef.current) {
14+
const picker = new BoringDatepickerClass({
15+
existingElement: spanRef.current,
16+
onChange,
17+
});
18+
picker.setValue(value);
19+
setDatepicker(picker);
20+
}
21+
}, [spanRef.current]);
22+
useEffect(() => {
23+
if (datepicker) {
24+
datepicker.setValue(value);
25+
}
26+
}, [datepicker, value]);
27+
return h(
28+
'span',
29+
{
30+
ref: spanRef,
31+
className,
32+
},
33+
children
34+
);
35+
};
36+
37+
module.exports = BoringDatepicker;

0 commit comments

Comments
 (0)