Repository: ecmadao/react-times
Branch: master
Commit: f7046c0afc90
Files: 67
Total size: 160.1 KB
Directory structure:
gitextract_0j5lhn9t/
├── .babelrc
├── .coveralls.yml
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .storybook/
│ ├── addons.js
│ ├── config.js
│ ├── preview-head.html
│ └── webpack.config.js
├── .travis.yml
├── LICENSE
├── README.md
├── css/
│ ├── base.css
│ ├── classic/
│ │ └── default.css
│ └── material/
│ ├── base.css
│ ├── button.css
│ ├── default.css
│ └── timezone.css
├── doc/
│ ├── CHANGELOG.md
│ └── README_CN.md
├── examples/
│ ├── TimePickerWrapper.js
│ ├── TimePickerWrapper2.js
│ └── TimeZonesPickerWrapper.js
├── index.js
├── package.json
├── src/
│ ├── components/
│ │ ├── ClassicTheme/
│ │ │ └── index.jsx
│ │ ├── Common/
│ │ │ ├── AsyncComponent.jsx
│ │ │ └── Button.jsx
│ │ ├── MaterialTheme/
│ │ │ ├── TwelveHoursMode.jsx
│ │ │ ├── TwentyFourHoursMode.jsx
│ │ │ └── index.jsx
│ │ ├── OutsideClickHandler.jsx
│ │ ├── Picker/
│ │ │ ├── PickerDragHandler.jsx
│ │ │ ├── PickerPoint.jsx
│ │ │ └── PickerPointGenerator.jsx
│ │ ├── TimePicker.jsx
│ │ └── Timezone/
│ │ ├── TimezonePicker.jsx
│ │ └── index.jsx
│ └── utils/
│ ├── constant.js
│ ├── drag.js
│ ├── func.js
│ ├── icons.js
│ ├── language.js
│ └── time.js
├── stories/
│ ├── ClassicThemePicker.js
│ ├── CustomTrigger.js
│ ├── DarkColor.js
│ ├── DifferentLanguage.js
│ ├── TimePicker.js
│ ├── TimePicker2.js
│ ├── TwelveHoursMode.js
│ └── WithTimeZones.js
└── test/
├── _helpers/
│ ├── adapter.js
│ └── ignoreSVGStrings.jsx
├── components/
│ ├── ClassicTheme_spec.jsx
│ ├── MaterialTheme_spec.jsx
│ ├── PickerDargHandler_spec.jsx
│ ├── PickerPointGenerator_spec.jsx
│ ├── PickerPoint_spec.jsx
│ ├── TimePicker_func_spec.jsx
│ ├── TimePicker_init_spec.jsx
│ ├── Time_zone_spec.jsx
│ ├── Timezone_Picker_spec.jsx
│ ├── TwelveHoursTheme_spec.jsx
│ └── TwentyFourHoursMode_spec.jsx
└── utils_spec.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
"env",
"stage-0",
"react"
],
"plugins": [
"system-import-transformer"
]
}
================================================
FILE: .coveralls.yml
================================================
service_name: travis-ci
repo_token: MMi1goJ3ZCpZ5Iaz9i1tSus4G5psdpQTY
================================================
FILE: .eslintignore
================================================
node_modules/
lib/
.out.
.storybook/
webpack.config.js
index.js
================================================
FILE: .eslintrc.json
================================================
{
"parser": "babel-eslint",
"extends": "standard",
"plugins": [
"react",
"import",
"babel"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"browser": true,
"node": true,
"es6": true,
"jquery": true,
"commonjs": true,
"phantomjs": true,
"mocha": true
},
"rules": {
"strict": 0,
"no-console": 1,
"no-debugger": 1,
"no-extra-semi": 1,
"no-constant-condition": 2,
"no-extra-boolean-cast": 2,
"no-return-assign": 0,
"use-isnan": 2,
"no-undef-init": 2,
"camelcase": 2,
"no-mixed-spaces-and-tabs": 2,
"no-const-assign":2,
"no-func-assign": 2,
"no-else-return": 1,
"no-obj-calls": 2,
"valid-typeof": 2,
"no-unused-vars": 1,
"quotes": 0,
"block-spacing": 1,
"semi": 0,
"keyword-spacing": 1,
"comma-dangle": 0,
"arrow-body-style": 0,
"array-bracket-spacing": 1,
"space-before-function-paren": 0,
"no-extra-bind": 1,
"no-var": "error",
"arrow-spacing": ["error", { "before": true, "after": true }],
"no-empty-function": ["error", { "allow": ["arrowFunctions", "constructors"] }],
"react/no-did-mount-set-state": "error",
"react/no-did-update-set-state": "error",
"react/react-in-jsx-scope": "error",
"react/jsx-uses-vars": [2],
"react/jsx-uses-react": [2],
"import/no-unresolved": [2, {"commonjs": true, "amd": true}],
"import/namespace": 2,
"import/default": 2,
"import/export": 2,
"babel/new-cap": 1,
"babel/object-curly-spacing": 0,
"babel/no-invalid-this": 1,
"babel/semi": 1,
"operator-linebreak": 0
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".jsx"]
},
"webpack": {
"config": "webpack.config.js"
}
},
"import/ignore": ["node_modules"]
},
"globals": {
"require": true
}
}
================================================
FILE: .gitignore
================================================
*.lock
.DS_Store
/lib
/.out
/node_modules
/out*
================================================
FILE: .npmignore
================================================
/components/
/examples
/stories
/webpack
/.storybook
/intro_src
/test
/src
/.coveralls.yml
/.travis.yml
/webpack.config.js
/doc
================================================
FILE: .storybook/addons.js
================================================
import '@storybook/addons';
import '@storybook/addon-knobs/register'
================================================
FILE: .storybook/config.js
================================================
import { addDecorator, configure, setAddon } from '@storybook/react';
import infoAddon from '@storybook/addon-info';
import moment from 'moment';
addDecorator((story) => {
moment.locale('zh-cn');
return (story());
});
function loadStories() {
require('../stories/TimePicker');
require('../stories/TimePicker2');
require('../stories/DarkColor');
require('../stories/TwelveHoursMode');
require('../stories/ClassicThemePicker');
require('../stories/CustomTrigger');
require('../stories/DifferentLanguage');
require('../stories/WithTimeZones');
}
setAddon(infoAddon);
configure(loadStories, module);
================================================
FILE: .storybook/preview-head.html
================================================
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<link href="http://obpykithy.bkt.clouddn.com/hawkeye/styles/fonts.css" rel="stylesheet">
<style>
* {
font-family: 'Open Sans', 'Lato', sans-serif;
}
.time_picker_wrapper {
height: 500px;
width: 300px;
margin: 50px auto;
}
.time_picker_wrapper2 {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-top: 100px;
}
.time_picker_trigger {
width: auto;
display: inline-block;
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.gap {
float: left;
width: 20px;
}
.time_picker_container {
width: 300px;
}
</style>
================================================
FILE: .storybook/webpack.config.js
================================================
const path = require('path');
const webpack = require('webpack');
const SOURCE_PATH = path.join(__dirname, '../src');
module.exports = {
context: SOURCE_PATH,
module: {
rules: [
{
test: /\.jsx?$/,
enforce: "pre",
loader: "eslint-loader",
exclude: /node_modules/,
include: SOURCE_PATH,
},
{
test: /\.css/,
loaders: ['style-loader', 'css-loader'],
include: path.resolve(__dirname, '../css/')
},
{
test: /\.css/,
loaders: ['style-loader', 'css-loader'],
include: path.resolve(__dirname, '../src/')
},
{
test: /\.svg$/,
loader: 'babel!react-svg'
},
{
test: /\.(js|jsx)$/,
include: SOURCE_PATH,
use: ['babel-loader'],
exclude: /node_modules/
},
]
},
resolve: {
modules: ['node_modules'],
extensions: ['.js', '.jsx'],
},
plugins: [
new webpack.LoaderOptionsPlugin({
debug: true,
minimize: true,
options: {
context: SOURCE_PATH,
}
}),
],
devtool: '#source-map',
};
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "7"
- "6"
before_script:
- npm install -g mocha
- npm install -g eslint
- npm i
- npm i react
- npm i react-dom
script: npm test
env:
- REACT=16
bundler_args: --retry 2
matrix:
fast_finish: true
cache:
directories:
- node_modules
after_script:
- npm run coveralls
notifications:
webhooks: https://hook.bearychat.com/=bw9fs/travis/613010ff56ad38e540b93d5543cea6dd
slack: ecmadao:fKFA5rnMSWRUqZrA9bS3gaD2
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2016 ecmadao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================

[](https://badge.fury.io/js/react-times) [](https://travis-ci.org/ecmadao/react-times) [](http://standardjs.com) [](https://www.npmjs.com/package/react-times) [](https://raw.githubusercontent.com/ecmadao/react-times/master/LICENSE)
[](https://nodei.co/npm/react-times)
README:[中文版](./doc/README_CN.md)
> A time picker react-component, no jquery-rely, writing in es6 standard style.
**Check [here](./doc/CHANGELOG.md) to see changed props in new version.**

# Online demo
Check [here](https://ecmadao.github.io/react-times) to play online demo.
# Play in local
```bash
$ git clone https://github.com/ecmadao/react-times.git
$ npm install
$ npm run storybook
```
# Install
dependencies:
- [`moment`](https://github.com/moment/moment/)
- [`react`](https://github.com/facebook/react)
- [`react-dom`](https://github.com/facebook/react)
> No jQuery rely 😤😤😤
So generally speaking, you should already have `react` & `react-dom` dependencies in your project. If not:
```bash
$ npm install react react-dom moment moment-timezone --save-dev
# and
$ npm install react-times --save-dev
```
# Config
Cause I'm using `moment-timezone`, you need to be able to parse json file.
Use webpack (version < 2) config as example:
- [How should I use moment-timezone with webpack?](https://stackoverflow.com/questions/29548386/how-should-i-use-moment-timezone-with-webpack)
```bash
$ npm i json-loader --save
```
```javascript
// webpack.config.js
// ATTENTION:
// webpack >= v2.0.0 has native JSON support.
// check here: https://github.com/webpack-contrib/json-loader/issues/65 for more information
{
module: {
loaders: [
{include: /\.json$/, loaders: ["json-loader"]}
]
},
resolve: {
extensions: ['', '.json', '.jsx', '.js']
}
}
```
# Usage
This component has two themes now: Material Theme by default, or Classic Theme.
> Always remember import css file when you use react-times
```javascript
// basic usage
// in some react component
import React from 'react';
import TimePicker from 'react-times';
// use material theme
import 'react-times/css/material/default.css';
// or you can use classic theme
import 'react-times/css/classic/default.css';
export default class SomeComponent extends React.Component {
onTimeChange(options) {
// do something
}
onFocusChange(focusStatue) {
// do something
}
render() {
<TimePicker
onFocusChange={this.onFocusChange.bind(this)}
onTimeChange={this.onTimeChange.bind(this)}
/>
}
}
```
> See more examples here:
```javascript
// some config example
render() {
<TimePicker
showTimezone // show the timezone, default false
focused // whether to show timepicker modal after rendered. default false
withoutIcon // whether to has time icon on button, default false
colorPalette="dark" // main color, default "light"
time="13:05" // initial time, default current time
theme="material"
// or
// theme="classic"
timeMode="12" // use 24 or 12 hours mode, default 24
timezone="America/New_York" // what timezone to use, detects the user's local timezone by default
/>
}
```
> For more detail usage, you can visit [example](https://github.com/ecmadao/react-times/tree/master/examples) or see the source code.
# Show time
- 24 hours mode with Material Theme, light color by default
```javascript
<TimePicker />
```

- 12 hours mode with Material Theme, light color by default
```javascript
<TimePicker timeMode="12"/>
```

- 24 hours mode with Material Theme, dark color
```javascript
<TimePicker colorPalette="dark"/>
```

- 24 hours mode, showing user current timezone. (Besides, your can use `timezone` props to custom timezone)
```javascript
<TimePicker showTimezone={true}/>
```

- 24 hours mode with Classic Theme, light color by default
```javascript
<TimePicker theme="classic"/>
```

- 24 hours mode with Classic Theme, dark color
```javascript
<TimePicker colorPalette="dark" theme="classic"/>
```

# APIs
## Props
- `time`
> Initial time, must be a string, with `${hour}:${minute}` format, default now (by using `moment()`):
```javascript
// PropTypes.string
time='11:11'
time='11:01'
time='1:01'
time='1:1'
```
- `timeFormat`
> To show the time using custom style
```javascript
// PropTypes.string
// HH, MM means 24 hours mode
// hh, mm means 12 hours mode
timeFormat='HH:MM'
timeFormat='hh:mm'
timeFormat='H:M'
timeFormat='h:m'
// Warning:
// If you are using 12 hours mode but with hh or mm format,
// or using 24 hours mode with HH or MM format,
// you will receive a warning on console, and force to use the timeMode props
// So, if you wanna use hh:mm or h:m, you need to set timeMode props to 12
// (cause timeMode default is 24)
```
- `timeFormatter`
> To show the time using custom style
```javascript
// PropTypes.func
timeFormatter={({ hour, minute, meridiem }) => `${hour} - ${minute}`}
// Note:
// If you both set timeFormat and timeFormatter props (and they all validate), component will use timeFormatter function
```
- `focused`
> Whether the timepicker pannel is focused or not when it did mount. Default `false`
```javascript
// PropTypes.bool
focused={false}
focused={true}
```
- `withoutIcon`
> Whether the timepicker has a svg icon. Default `false`.
```javascript
// PropTypes.bool
withoutIcon={true}
```
- `colorPalette`
> The main color palette of picker pannel. Default `light`.
```javascript
// PropTypes.string
colorPalette="dark"
colorPalette="light"
```
- `timeMode`
> Support "12" or "24" hours mode.
```javascript
// PropTypes.string or PropTypes.number
timeMode="24"
timeMode=24
timeMode="12"
timeMode=12
```
- `meridiem`
> `PropTypes.string`, support "PM" or "AM" for 12 hours mode, default `AM`
- `showTimezone`
> `PropTypes.bool`, whether show user timezone or not, default `false`
- `timezone`
> `PropTypes.string`, change user timezone, default user current local timezone.
- `trigger`
> `React.component`, means a DOM which can control TimePicker Modal "open" or "close" status.
```javascript
<TimePicker
focused={focused}
trigger={(
<div
onClick={this.handleFocusedChange.bind(this)} >
click to open modal
</div>
)}
/>
```
- `closeOnOutsideClick`
> If you don't wanna close panel when outside click, you can use closeOnOutsideClick={false}. Default true
```
<TimePicker
closeOnOutsideClick={false}
/>
```
- `disabled`
> Disable component. Default false
```
<TimePicker
disabled={true}
/>
```
- `draggable`
If you don't want to drag the pointer, then you can set `draggable` props to `false`, then users can only use click to change time. Default `true`
```
<TimePicker
draggable={false}
/>
```
- `language`
> `React.string`, use different language. Default english.
```javascript
/*
* support
* en: english
* zh-cn: 中文简体
* zh-tw: 中文繁体
* fr: Français
* ja: 日本語
*/
<TimePicker
language="zh-cn" // 中文简体
/>
```
- `phrases`
> `React.object`, specify text values to use for specific messages. By default, phrases will be set from defaults based on language.
> Specify any of the available phrases you wish to override or all of them if your desired language is not yet supported.
> See [language.js](./src/utils/language.js) for default phrases.
```javascript
<TimePicker
phrases={{
confirm: 'Are you sure?',
cancel: 'Do you want to cancel?',
close: 'DONE',
am: 'Ante Meridiem',
pm: 'Post Meridiem'
}}
/>
```
- `minuteStep`
> `React.number`, default `5`. It means the minium minute can change. You can set it to 1, 2, 3...
```javascript
<TimePicker
minuteStep={1}
/>
```
- `timeConfig`
> `React.object`, to config from, to, step limit for classic theme panel.
```javascript
<TimePicker
theme="classic"
timeMode="12"
timeConfig={{
from: '08:00 PM',
to: '08:00 AM',
step: 1,
unit: 'hour'
}}
/>
<TimePickerWrapper
theme="classic"
timeMode="24"
timeConfig={{
from: 9,
to: 19,
step: 30,
unit: 'minutes'
}}
/>
```
- `limitDrag`
> `React.bool`, default `false`. If `true`, it will limite the drag rotation by `minuteStep`
```javascript
<TimePicker
limitDrag
/>
```
## Callback
- `onFocusChange`
`PropTypes.func`
> The callback func when component `focused` state is changed.
- `onTimeChange`
`PropTypes.func`
> The callback func when component `hour` or `minute` or `AM/PM` state is changed.
```javascript
onTimeChange(options) {
// you can get hour, minute and meridiem here
const {
hour,
minute,
meridiem
} = options;
}
```
- `onTimezoneChange`
`PropTypes.func`
> The callback func when timezone changed. Receives timezone object as argument with the following properties:
* city
* zoneAbbr
* zoneName
# Article
- [一言不合造轮子--撸一个ReactTimePicker](https://github.com/ecmadao/Coding-Guide/blob/master/Notes/React/ReactJS/Write%20a%20React%20Timepicker%20Component%20hand%20by%20hand.md)
# Todos
- Test
- [x] TimePicker Component
- [x] PickerDragHandler Component
- [x] PickerPointGenerator Component
- [x] MaterialTheme Component
- [x] TwelveHoursTheme Component
- [x] PickerPoint Component
- [x] OutsideClickHandler Component
- [x] utils test
- Color Palette (Now It has light and dark color)
- [x] light
- [x] dark
- [ ] colorful
- Themes
- [x] Material Theme
- [x] Classical Theme
- Mode
- [x] 12h mode
- [x] 24h mode
- Animations
# Thx
Thanks to the Airbnb's open source project: [react-dates](https://github.com/airbnb/react-dates), I have learn a lot from that. Thanks.
# Core Contributors 🎉
- **[carlodicelico](https://github.com/carlodicelico)**
- **[erin-doyle](https://github.com/erin-doyle)**
- **[MatthieuLemoine](https://github.com/MatthieuLemoine)**
- **[naseeihity](https://github.com/naseeihity)**
- **[shianqi](https://github.com/shianqi)**
- **[thg303](https://github.com/thg303)**
# License
[MIT License](./LICENSE)
================================================
FILE: css/base.css
================================================
.time_picker_container {
position: relative;
}
.time_picker_preview {
height: 50px;
}
.time_picker_preview:not(.disabled):active, .time_picker_preview:not(.disabled).active {
box-shadow: 0 8px 8px 0 rgba(0, 0, 0, 0.12), 0 0 8px 0 rgba(0, 0, 0, 0.08);
-moz-box-shadow: 0 8px 8px 0 rgba(0, 0, 0, 0.12), 0 0 8px 0 rgba(0, 0, 0, 0.08);
-webkit-box-shadow: 0 8px 8px 0 rgba(0, 0, 0, 0.12), 0 0 8px 0 rgba(0, 0, 0, 0.08);
}
.time_picker_preview.disabled {
cursor: not-allowed;
}
.preview_container {
position: absolute;
left: 50%;
height: 50px;
line-height: 50px;
padding-left: 30px;
transform: translateX(-50%);
-o-transform: translateX(-50%);
-ms-transform: translateX(-50%);
-webkit-transform: translateX(-50%);
-moz-transform: translateX(-50%);
}
.preview_container.without_icon {
padding-right: 30px;
}
.preview_container svg {
width: 25px;
height: 25px;
position: absolute;
top: 12px;
left: 0;
}
.react_times_button {
user-select: none;
position: relative;
cursor: pointer;
color: #343434;
border-radius: 2px;
background-color: #fff;
transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
-ms-transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
-moz-transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
-o-transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
-webkit-transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
box-shadow: 2px 2px 15px 0 rgba(0, 0, 0, .15);
-moz-box-shadow: 2px 2px 15px 0 rgba(0, 0, 0, .15);
-webkit-box-shadow: 2px 2px 15px 0 rgba(0, 0, 0, .15);
}
.react_times_button.pressDown {
box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.1);
-moz-box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.1);
-webkit-box-shadow: 1px 1px 4px 0 rgba(0, 0, 0, 0.1);
}
.react_times_button.pressDown .wrapper {
transform: translateY(1px);
}
.react_times_button .wrapper {
transform: translateY(0);
height: 100%;
}
.modal_container {
user-select: none;
cursor: default;
position: absolute;
width: 100%;
transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
-ms-transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
-moz-transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
-o-transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
-webkit-transition: all 200ms cubic-bezier(0.165, 0.84, 0.44, 1);
background-color: #fff;
border-radius: 2px;
top: 100%;
left: 0;
box-shadow: 4px 4px 30px 0 rgba(0, 0, 0, 0.2);
-moz-box-shadow: 4px 4px 30px 0 rgba(0, 0, 0, 0.2);
-webkit-box-shadow: 4px 4px 30px 0 rgba(0, 0, 0, 0.2);
opacity: 0;
z-index: -1;
visibility: hidden;
backface-visibility: hidden;
transform: scale(0.7) translateY(20px);
-ms-transform: scale(0.7) translateY(20px);
-moz-transform: scale(0.7) translateY(20px);
-o-transform: scale(0.7) translateY(20px);
-webkit-transform: scale(0.7) translateY(20px);
}
.outside_container.active .modal_container {
opacity: 1;
z-index: 2;
visibility: visible;
transform: scale(1) translateY(20px);
-ms-transform: scale(1) translateY(20px);
-moz-transform: scale(1) translateY(20px);
-o-transform: scale(1) translateY(20px);
-webkit-transform: scale(1) translateY(20px);
}
================================================
FILE: css/classic/default.css
================================================
@import "../base.css";
.classic_theme_container {
height: 250px;
overflow-y: scroll;
}
.classic_theme_container .classic_time {
cursor: pointer;
width: 100%;
height: 40px;
line-height: 40px;
text-align: center;
border-bottom: 1px solid #f3f3f3;
background-color: #fff;
transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
-ms-transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
-moz-transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
-o-transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
-webkit-transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
}
.classic_theme_container .classic_time .meridiem {
font-size: 0.8em;
opacity: 0.7;
padding-left: 5px;
}
.classic_theme_container .classic_time.dark.active,
.classic_theme_container .classic_time.dark:hover {
background-color: #4a4a4a;
color: #fff;
}
.classic_theme_container .classic_time.light.active,
.classic_theme_container .classic_time.light:hover {
background-color: #3498db;
color: #fff;
}
/* dark theme */
.dark .classic_theme_container {
background-color: #4a4a4a;
}
.dark .classic_theme_container .classic_time {
border-bottom: 1px solid #5d5d5d;
background-color: #4a4a4a;
color: #fff;
}
.dark .classic_theme_container .classic_time.active,
.dark .classic_theme_container .classic_time:hover {
background-color: #343434;
}
================================================
FILE: css/material/base.css
================================================
@import "../base.css";
.time_picker_modal_container {
}
.time_picker_modal_header,
.time_picker_modal_footer,
.timezone_picker_modal_header {
height: 75px;
line-height: 75px;
text-align: center;
margin-bottom: 30px;
background-color: #3498db;
color: #FFFFFF;
font-size: 2.5em;
border-radius: 2px 2px 0 0;
}
.timezone_picker_modal_header {
line-height: initial;
}
.time_picker_header_delivery {
opacity: 0.5;
}
.time_picker_modal_header .time_picker_header {
cursor: pointer;
opacity: 0.5;
transition: opacity 0.3s;
}
.time_picker_modal_header .time_picker_header.active {
cursor: default;
opacity: 1;
}
.time_picker_modal_header .time_picker_header:hover {
opacity: 1;
}
.time_picker_modal_header .time_picker_header.meridiem {
font-size: 0.8em;
}
.time_picker_modal_footer {
font-size: 1em;
margin-bottom: 0;
}
.time_picker_modal_footer.clickable {
cursor: pointer;
}
.picker_container {
width: 260px;
height: 260px;
margin: 0 20px 20px;
border-radius: 50%;
background-color: #f0f0f0;
position: relative;
}
.picker_pointer_container {
opacity: 1;
transition: all 300ms cubic-bezier(0.165, 0.84, 0.44, 1);
-ms-transition: all 300ms cubic-bezier(0.165, 0.84, 0.44, 1);
-moz-transition: all 300ms cubic-bezier(0.165, 0.84, 0.44, 1);
-o-transition: all 300ms cubic-bezier(0.165, 0.84, 0.44, 1);
-webkit-transition: all 300ms cubic-bezier(0.165, 0.84, 0.44, 1);
}
.picker_pointer_container.animation {
opacity: 0;
transform: scale3d(0.85, 0.85, 1);
-o-transform: scale3d(0.85, 0.85, 1);
-ms-transform: scale3d(0.85, 0.85, 1);
-moz-transform: scale3d(0.85, 0.85, 1);
-webkit-transform: scale3d(0.85, 0.85, 1);
}
.picker_center {
position: absolute;
top: 50%;
left: 50%;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #3498db;
transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
}
.picker_point {
left: 50%;
cursor: pointer;
position: absolute;
width: 30px;
height: 30px;
text-align: center;
line-height: 30px;
border-radius: 50%;
}
.picker_point.point_outter {
top: 5px;
color: #5d5d5d;
transform-origin: center 125px;
-o-transform-origin: center 125px;
-ms-transform-origin: center 125px;
-moz-transform-origin: center 125px;
-webkit-transform-origin: center 125px;
}
.picker_point.point_inner {
top: 40px;
color: #a7a7a7;
transform-origin: center 90px;
-o-transform-origin: center 90px;
-ms-transform-origin: center 90px;
-moz-transform-origin: center 90px;
-webkit-transform-origin: center 90px;
}
.picker_minute_point {
left: 50%;
cursor: pointer;
position: absolute;
top: 15px;
color: #5d5d5d;
transform-origin: center 115px;
-o-transform-origin: center 115px;
-ms-transform-origin: center 115px;
-moz-transform-origin: center 115px;
-webkit-transform-origin: center 115px;
width: 2px;
height: 2px;
border-radius: 50%;
background-color: #3498db;
}
.picker_pointer {
position: absolute;
width: 4px;
height: 110px;
left: 50%;
top: 20px;
background-color: #3498db;
transform-origin: center bottom;
}
.picker_pointer.animation {
transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
-ms-transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
-moz-transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
-o-transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
-webkit-transition: all 400ms cubic-bezier(0.165, 0.84, 0.44, 1);
}
.picker_pointer .pointer_drag {
position: absolute;
width: 35px;
height: 35px;
border-radius: 50%;
top: -17.5px;
left: -15.5px;
background-color: #3498db;
color: #fff;
text-align: center;
line-height: 35px;
}
.picker_pointer .pointer_drag.draggable {
cursor: move;
}
.buttons_wrapper {
float: right;
margin-top: 5px;
}
================================================
FILE: css/material/button.css
================================================
.time_picker_button {
padding: 5px 10px;
background-color: transparent;
display: inline-block;
color: #949494;
opacity: 0.6;
transition: opacity 0.2s;
box-shadow: none;
}
.time_picker_button:hover {
opacity: 1;
}
================================================
FILE: css/material/default.css
================================================
@import "./base.css";
@import "./button.css";
@import "./timezone.css";
.dark .time_picker_preview {
}
.dark .time_picker_preview .preview_container svg {
}
.dark .time_picker_preview.active {
}
.dark .time_picker_modal_container {
background-color: #4a4a4a;
}
.dark .time_picker_modal_header,
.dark .time_picker_modal_footer {
background-color: #343434;
}
.dark .time_picker_modal_header .time_picker_header.active,
.dark .time_picker_modal_header .time_picker_header:hover {
}
.dark .picker_container {
background-color: #4a4a4a;
}
.dark .picker_container .picker_center,
.dark .picker_container .picker_pointer,
.dark .picker_container .picker_pointer .pointer_drag{
background-color: #F4511E;
}
.dark .picker_minute_point,
.dark .picker_point.point_outter {
color: #fff;
}
.dark .picker_point.point_inner {
color: #D0D0D0;
}
================================================
FILE: css/material/timezone.css
================================================
.timezone_picker_modal_container {
user-select: none;
cursor: default;
position: absolute;
z-index: 3;
background-color: #fff;
border-radius: 2px;
top: 0;
width: 100%;
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.12), 0 0 4px 0 rgba(0, 0, 0, 0.08);
-moz-box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.12), 0 0 4px 0 rgba(0, 0, 0, 0.08);
-webkit-box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.12), 0 0 4px 0 rgba(0, 0, 0, 0.08);
}
.timezone_picker_modal_container-enter {
right: -100%;
opacity: 0.5;
}
.timezone_picker_modal_container-enter.timezone_picker_modal_container-enter-active {
right: 0;
opacity: 1;
transition: right 100ms ease-out, opacity 100ms ease-out;
-ms-transition: right 100ms ease-out, opacity 100ms ease-out;
-moz-transition: right 100ms ease-out, opacity 100ms ease-out;
-o-transition: right 100ms ease-out, opacity 100ms ease-out;
-webkit-transition: right 100ms ease-out, opacity 100ms ease-out;
}
.timezone_picker_modal_container-exit {
right: 0;
opacity: 1;
}
.timezone_picker_modal_container-exit.timezone_picker_modal_container-exit-active {
right: -100%;
opacity: 0.5;
transition: right 100ms ease-in, opacity 100ms ease-in;
-ms-transition: right 100ms ease-in, opacity 100ms ease-in;
-moz-transition: right 100ms ease-in, opacity 100ms ease-in;
-o-transition: right 100ms ease-in, opacity 100ms ease-in;
-webkit-transition: right 100ms ease-in, opacity 100ms ease-in;
}
.timezone_picker_modal_header {
font-size: 1em;
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.timezone_picker_header_title {
flex: 1;
text-align: left;
}
.timezone_picker_modal_header span.icon {
height: 25px;
width: 50px;
}
.timezone_picker_modal_header svg {
width: 25px;
height: 25px;
fill: #fff;
cursor: pointer;
}
.timezone_picker_container {
min-width: 260px;
min-height: 300px;
display: flex;
margin: 0 20px 20px;
position: relative;
}
.timezone_picker_search {
padding: 0 10px;
position: relative;
width: 100%;
}
.timezone_picker_search input {
box-sizing: border-box;
margin-bottom: 1%;
padding: 10px 10px;
width: 100%;
height: 100%;
font-size: 0.9rem;
line-height: 2;
border: none;
border-bottom: 1px solid #adb5bd;
outline: none;
border-radius: 2px;
transition: border .2s;
}
.timezone_picker_search input::-webkit-input-placeholder,
.timezone_picker_search input::-moz-input-placeholder,
.timezone_picker_search input:-ms-input-placeholder,
.timezone_picker_search input:-moz-input-placeholder {
color: #c6cace;
}
.timezone_picker_search .bootstrap-typeahead-input-main {
color: #757575;
}
.timezone_picker_search input:focus {
color: #4b4b4b;
border-bottom: 1px solid #3498db;
}
/**
* The react-bootstrap-typeahead library sort of assumes bootstrap is already in use for styling
* so it refers to some bootstrap classes. We don't need to use bootstrap just for a few classes so
* the relevant styles have been copied here
*/
.clearfix:before,
.clearfix:after {
display: table;
content: " ";
}
.clearfix:after {
clear: both;
}
.open > .dropdown-menu {
display: block;
}
.open > a {
outline: 0;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, .15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}
.dropdown-menu > li > a {
display: block;
padding: 3px 20px;
clear: both;
font-weight: normal;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
color: #262626;
text-decoration: none;
background-color: #f5f5f5;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
color: #fff;
text-decoration: none;
background-color: #337ab7;
outline: 0;
}
.dropdown-menu > .disabled > a,
.dropdown-menu > .disabled > a:hover,
.dropdown-menu > .disabled > a:focus {
color: #777;
}
.dropdown-menu > .disabled > a:hover,
.dropdown-menu > .disabled > a:focus {
text-decoration: none;
cursor: not-allowed;
background-color: transparent;
background-image: none;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
================================================
FILE: doc/CHANGELOG.md
================================================
# CHANGELOG
### v3.1.3
#### new props
- Add `timeConfig` props: to config from, to, step for classic theme panel.
### v3.1.0
#### remove props
- Remove `onHourChange`
- Remove `onMinuteChange`
- Remove `onMeridiemChange`
#### change props
- `onTimeChange` will get a dict now, including `hour`, `minute`, `meridiem`
#### new props
- Add `closeOnOutsideClick`
### v2.2.3
#### new props
- Add `timeFormat` props
- Add `timeFormatter` props
### v2.2.0
#### new props
- Add `minuteStep` props
- Add `limitDrag` props
### v2.1.3
- Bugfixed for drag position offset
- Add `onTimezoneChange` callback
### v2.1.0
#### new props
- `phrases`: `PropTypes.object`
- `timezone`: `PropTypes.string`
- `onTimezoneChange`: `PropTypes.func`
### v2.0.0
#### changed props
- `onTimeQuantumChange` --> `onMeridiemChange`
- `timeQuantum` --> `meridiem`
- `dragable` --> `draggable`
#### new props
- `showTimezone`: `PropTypes.bool`, default `false`
- `timezone`: `PropTypes.string`, default user current local timezone
================================================
FILE: doc/README_CN.md
================================================

[](https://badge.fury.io/js/react-times) [](https://travis-ci.org/ecmadao/react-times) [](https://coveralls.io/github/ecmadao/react-times?branch=master) [](http://standardjs.com) [](https://www.npmjs.com/package/react-times) [](https://raw.githubusercontent.com/ecmadao/react-times/master/LICENSE)
[](https://nodei.co/npm/react-times)
README:[English Version](./README.md)
> 一个 React 时间选择器组件,使用 ES6 标准语法编写,没有 jQuery 依赖
**戳 [这里](./doc/CHANGELOG.md) 查看新版中更改/新增的 props。**

# 线上 demo
戳[这里](https://ecmadao.github.io/react-times)玩线上 demo
# 本地玩起来
```bash
$ git clone https://github.com/ecmadao/react-times.git
$ npm install
$ npm run storybook
```
# 安装说明
单独使用插件时所需的依赖:
- [`moment`](https://github.com/moment/moment/)
- [`react`](https://github.com/facebook/react)
- [`react-dom`](https://github.com/facebook/react)
> No jQuery 😤😤😤
使用的时候,需要你的项目里已经安装了`react`和`react-dom`。如果没有的话:
```bash
$ npm install react react-dom --save-dev
# and
$ npm install react-times --save-dev
```
注意:因为组件使用了`moment-timezone`,所以你本地需要能够编辑 json 文件。webpack 2 以下的用户可以通过 json-loader 解决该问题。webpack >= 2 后自带 json 解析功能。
# 使用方式
目前组件总共有两种主题:Material 主题和经典主题
> 在使用组件的时候,记得要引入对应主题的 CSS 文件
```javascript
// 基本使用方式
// 假设要在某个组件里使用该插件 (SomeComponent)
import React from 'react';
import TimePicker from 'react-times';
// 使用 Material 主题的话引入:
import 'react-times/css/material/default.css';
// 否则经典主题的话则引入:
import 'react-times/css/classic/default.css';
export default class SomeComponent extends React.Component {
onTimeChange(options) {
// do something
}
onFocusChange(focusStatue) {
// do something
}
render() {
<TimePicker
onFocusChange={this.onFocusChange.bind(this)}
onTimeChange={this.onTimeChange.bind(this)}
// 确定主题,不填该 props 则默认为 material
theme="material"
// or
// theme="classic"
/>
}
}
```
关于配置的栗子:
```javascript
render() {
<TimePicker
colorPalette="dark" // main color, default "light"
focused={true} // whether to show timepicker modal after rendered. default false
withoutIcon={true} // whether to has time icon on button, default false
time="13:05" // initial time, default current time
theme="material"
// or
// theme="classic"
timeMode="12" // use 24 or 12 hours mode, default 24
/>
}
```
> 你可以戳 [这里](https://github.com/ecmadao/react-times/tree/master/examples) 查看更多栗子
# 秀一下
- 24 小时制,亮色调的 Material 主题(默认)
```javascript
<TimePicker />
```

- 12 小时制,亮色调的 Material 主题
```javascript
<TimePicker timeMode="12"/>
```

- 24 小时制,暗色调的 Material 主题
```javascript
<TimePicker colorPalette="dark"/>
```

- 24 小时制,展示用户当前时区。(除此以外,可以通过 `timezone` props 来手动改变时区)
```javascript
<TimePicker showTimezone={true}/>
```

- 24 小时制,亮色调的经典主题
```javascript
<TimePicker theme="classic"/>
```

- 24 小时制,暗色调的经典主题
```javascript
<TimePicker colorPalette="dark" theme="classic"/>
```

# APIs
## Props
- `time`
> 初始化时的时间,格式是 `${hour}:${minute}`,不传则默认使用当前时间(通过`moment()`)
```javascript
// PropTypes.string
time="11:11"
time="11:01"
time="1:01"
time="1:1"
```
- `timeFormat`
> 自定义时间的格式
```javascript
// PropTypes.string
// HH, MM 代表 24 小时制
// hh, mm 代表 12 小时制
timeFormat='HH:MM'
timeFormat='hh:mm'
// Warning:
// 如果设定 timeMode 为 12 小时制,且 timeFormat 中含有 hh 或者 mm;
// 或者设定 timeMode 为 24 小时制,且 timeFormat 中含有 HH 或者 MM,
// 则会在浏览器控制台中输出一条警告,且时间格式会被转换为 timeMode 所设定的格式
// 因此,如果想把 timeFormat 设定为 hh:mm 或者 h:m,则还需要把 timeMode 设置为 12
// (因为 timeMode 默认为 24)
```
- `timeFormatter`
> 自定义时间的格式
```javascript
// PropTypes.func
timeFormatter={({ hour, minute, meridiem }) => `${hour} - ${minute}`}
// 注:
// 当同时设定了 timeFormat 和 timeFormatter 时(都合法),会使用 timeFormatter
```
- `focused`
> 初始化时时间选择器的 modal 是否打开,默认为`false`
```javascript
// PropTypes.bool
focused={false}
focused={true}
```
- `withoutIcon`
> 时间选择器的按钮上是否不需要 svg icon,默认为`false`
```javascript
// PropTypes.bool
withoutIcon={true}
```
- `colorPalette`
> 配色方案,默认为`light`
```javascript
// PropTypes.string
colorPalette="dark"
colorPalette="light"
```
- `timeMode`
> 12 或 24 小时制,默认为 24
```javascript
// PropTypes.string or PropTypes.number
timeMode="24"
timeMode=24
timeMode="12"
timeMode=12
```
- `meridiem`
> 上下午,在 12 小时制里为 "AM" 或 "PM"。默认为 `AM`
- `showTimezone`
> `PropTypes.bool`,代表是否展示用户的时区。默认为 `false`
- `timezone`
> `PropTypes.string`,可以通过该 props 改变用户所处的时区。默认为用户当前本地时区。
- `trigger`
> 开启、关闭时间选择器 Modal 的触发器,是一个 React Component
```javascript
<TimePicker
focused={focused}
trigger={(
<div
onClick={this.handleFocusedChange.bind(this)} >
click to open modal
</div>
)}
/>
```
- `closeOnOutsideClick`
> 点击 Modal 外部后是否关闭。默认为 true
```
<TimePicker
closeOnOutsideClick={false}
/>
```
- `disabled`
> 禁用组件。默认为 false
```
<TimePicker
disabled={true}
/>
```
- `draggable`
如果想禁用拖拽,则可以设置 `draggable` 为 `false`,那样的话用户只能通过点击来改变时间。默认为 `true`
```
<TimePicker
draggable={true}
/>
```
- `language`
> 语言。默认为英语
```javascript
/*
* support
* en: english
* zh-cn: 中文简体
* zh-tw: 中文繁体
* fr: Français
* ja: 日本語
*/
<TimePicker
language="zh-cn" // 中文简体
/>
```
- `phrases`
> `React.object`,用于自定义一些短语。可以在 [language.js](./src/utils/language.js) 查看所有的默认短语
```javascript
<TimePicker
phrases={{
confirm: '确认更改?',
cancel: '确认取消?',
close: 'DONE',
am: '上午',
pm: '下午'
}}
/>
```
- `minuteStep`
> `React.number`, 默认为 `5`。该属性代表当分针改变时的最小步长(minute)。可以设置为 1,2,3....
```javascript
<TimePicker
minuteStep={1}
/>
```
- `timeConfig`
> `React.object`, 用于配置 classic theme 时可选的时间范围以及步长
```javascript
<TimePicker
theme="classic"
timeMode="12"
timeConfig={{
from: '08:00 PM',
to: '08:00 AM',
step: 1,
unit: 'hour'
}}
/>
<TimePickerWrapper
theme="classic"
timeMode="24"
timeConfig={{
from: 9,
to: 19,
step: 30,
unit: 'minutes'
}}
/>
```
- `limitDrag`
> `React.bool`, 默认为 `false`. 当设置为 `true` 时,将会限制用户的拖拽(从连续性的拖拽变为间断性拖拽,间隔由 `minuteStep` 确定)
```javascript
<TimePicker
limitDrag
/>
```
## 回调
- `onFocusChange`
`PropTypes.func`
> 当组件`focused`属性改变,也就是选择器 modal 被打开或关闭时调用
- `onTimeChange`
`PropTypes.func`
> 小时`hour`,分钟`minute`或者上下午`meridiem`被改变时的回调
```javascript
onTimeChange(options) {
const {
hour,
minute,
meridiem
} = options;
// ...
}
```
- `onTimezoneChange`
`PropTypes.func`
> 当时区改变时的回调
# 相关文章
- [一言不合造轮子--撸一个ReactTimePicker](https://github.com/ecmadao/Coding-Guide/blob/master/Notes/React/ReactJS/Write%20a%20React%20Timepicker%20Component%20hand%20by%20hand.md)
# Todos
- 测试
- [x] TimePicker Component
- [x] PickerDragHandler Component
- [x] PickerPointGenerator Component
- [x] MaterialTheme Component
- [x] TwelveHoursTheme Component
- [x] PickerPoint Component
- [x] OutsideClickHandler Component
- [x] utils test
- 配色
- [x] light
- [x] dark
- [ ] colorful
- 主题
- [x] Material Theme
- [x] Classical Theme
- 小时制
- [x] 12h mode
- [x] 24h mode
- 动画
# 致谢
感谢 Airbnb 的 [react-dates](https://github.com/airbnb/react-dates) 组件,没有它我也不会想着写一个小时选择组件
# 核心贡献者 🎉
- **[carlodicelico](https://github.com/carlodicelico)**
- **[erin-doyle](https://github.com/erin-doyle)**
- **[MatthieuLemoine](https://github.com/MatthieuLemoine)**
- **[naseeihity](https://github.com/naseeihity)**
- **[shianqi](https://github.com/shianqi)**
- **[thg303](https://github.com/thg303)**
# 版权
[MIT License](./LICENSE)
================================================
FILE: examples/TimePickerWrapper.js
================================================
import React from 'react';
import TimePicker from '../src/components/TimePicker';
import timeHelper from '../src/utils/time';
import ICONS from '../src/utils/icons';
class TimePickerWrapper extends React.Component {
constructor(props) {
super(props);
const { defaultTime, meridiem, focused, showTimezone, timezone } = props;
let hour = '';
let minute = '';
if (!defaultTime) {
[hour, minute] = timeHelper.current().split(/:/);
} else {
[hour, minute] = defaultTime.split(/:/);
}
this.state = {
hour,
minute,
meridiem,
focused,
timezone,
showTimezone,
};
this.onFocusChange = this.onFocusChange.bind(this);
this.onTimeChange = this.onTimeChange.bind(this);
this.handleFocusedChange = this.handleFocusedChange.bind(this);
}
onTimeChange(options) {
const {
hour,
minute,
meridiem
} = options;
this.setState({ hour, minute, meridiem });
}
onFocusChange(focused) {
console.log(`onFocusChange: ${focused}`);
this.setState({ focused });
}
handleFocusedChange() {
const { focused } = this.state;
this.setState({ focused: !focused });
}
get basicTrigger() {
const { hour, minute } = this.state;
return (
<div
onClick={this.handleFocusedChange}
className="time_picker_trigger"
>
<div>
Click to open panel<br />
{hour}:{minute}
</div>
</div>
);
}
get customTrigger() {
return (
<div
onClick={this.handleFocusedChange}
className="time_picker_trigger"
>
{ICONS.time}
</div>
);
}
get trigger() {
const { customTriggerId } = this.props;
const triggers = {
0: (<div />),
1: this.basicTrigger,
2: this.customTrigger
};
return triggers[customTriggerId] || null;
}
render() {
const {
hour,
minute,
focused,
meridiem,
timezone,
showTimezone,
} = this.state;
return (
<div className="time_picker_wrapper">
<TimePicker
trigger={this.trigger}
{...this.props}
focused={focused}
meridiem={meridiem}
timezone={timezone}
onFocusChange={this.onFocusChange}
onTimeChange={this.onTimeChange}
showTimezone={showTimezone}
time={hour && minute ? `${hour}:${minute}` : null}
/>
</div>
);
}
}
TimePickerWrapper.defaultProps = {
customTriggerId: null,
defaultTime: null,
focused: false,
meridiem: null,
showTimezone: false
};
export default TimePickerWrapper;
================================================
FILE: examples/TimePickerWrapper2.js
================================================
import React from 'react';
import TimePicker from '../src/components/TimePicker';
import timeHelper from '../src/utils/time';
import ICONS from '../src/utils/icons';
class TimePickerWrapper extends React.Component {
constructor(props) {
super(props);
const { defaultTime, meridiem, focused, showTimezone, timezone } = props;
let hour = '';
let minute = '';
if (!defaultTime) {
[hour, minute] = timeHelper.current().split(/:/);
} else {
[hour, minute] = defaultTime.split(/:/);
}
this.state = {
1: {
hour,
minute,
meridiem,
focused,
timezone,
showTimezone,
},
2: {
hour,
minute,
meridiem,
focused,
timezone,
showTimezone,
}
};
this.onFocusChange = this.onFocusChange.bind(this);
this.onTimeChange = this.onTimeChange.bind(this);
this.handleFocusedChange = this.handleFocusedChange.bind(this);
}
onTimeChange(section) {
return (options) => {
const {
hour,
minute,
meridiem
} = options;
this.setState({
[section]: Object.assign({}, this.state[section], {
hour, minute, meridiem
})
});
};
}
onFocusChange(section) {
return focused => this.setState({
[section]: Object.assign({}, this.state[section], {
focused
})
});
}
handleFocusedChange(section) {
return () => this.setState({
[section]: Object.assign({}, this.state[section], {
focused: !this.state[section].focused
})
});
}
getBasicTrigger() {
const { hour, minute } = this.state;
return (
<div
onClick={this.handleFocusedChange}
className="time_picker_trigger"
>
<div>
Click to open panel<br />
{hour}:{minute}
</div>
</div>
);
}
getCustomTrigger() {
return (
<div
onClick={this.handleFocusedChange}
className="time_picker_trigger"
>
{ICONS.time}
</div>
);
}
getTrigger(section) {
const { customTriggerId } = this.props;
const triggers = {
0: (<div />),
1: this.getBasicTrigger(section),
2: this.getCustomTrigger()
};
return triggers[customTriggerId] || null;
}
renderTrigger(section) {
const {
hour,
minute,
focused,
meridiem,
timezone,
showTimezone,
} = this.state[section];
return (
<TimePicker
id={section}
trigger={this.getTrigger(section)}
{...this.props}
focused={focused}
meridiem={meridiem}
timezone={timezone}
onFocusChange={this.onFocusChange(section)}
onTimeChange={this.onTimeChange(section)}
showTimezone={showTimezone}
time={hour && minute ? `${hour}:${minute}` : null}
/>
);
}
render() {
return (
<div className="time_picker_wrapper2">
{this.renderTrigger(1)}
<div className="gap" />
{this.renderTrigger(2)}
</div>
);
}
}
TimePickerWrapper.defaultProps = {
customTriggerId: null,
defaultTime: null,
focused: false,
meridiem: null,
showTimezone: false
};
export default TimePickerWrapper;
================================================
FILE: examples/TimeZonesPickerWrapper.js
================================================
import React from 'react';
import Timezone from '../src/components/Timezone';
import timeHelper from '../src/utils/time';
import languageHelper from '../src/utils/language';
const TIME = timeHelper.time();
TIME.current = timeHelper.current();
TIME.tz = timeHelper.guessUserTz();
const style = {
width: '300px',
position: 'absolute',
left: '50%',
top: '100px',
transform: 'translateX(-50%)'
};
class TimeZonesPickerWrapper extends React.Component {
constructor(props) {
super(props);
const {timezone} = this.props;
this.state = {
timezone
};
}
languageData() {
const {language = 'en', phrases = {}} = this.props;
return Object.assign({}, languageHelper.get(language), phrases);
}
render() {
const {timezone} = this.state;
const {onTimezoneChange} = this.props;
return (
<div style={style}>
<div className="outside_container active">
<div className='time_picker_modal_container'>
<Timezone
phrases={this.languageData()}
timezone={timezone}
timezoneIsEditable={true}
onTimezoneChange={onTimezoneChange}
/>
</div>
</div>
</div>
)
}
}
TimeZonesPickerWrapper.defaultProps = {
timezone: TIME.tz
};
export default TimeZonesPickerWrapper;
================================================
FILE: index.js
================================================
require('./lib/utils/time').default;
var TimePicker = require('./lib/components/TimePicker').default;
module.exports = TimePicker;
================================================
FILE: package.json
================================================
{
"name": "react-times",
"description": "A react time-picker component, no jquery-rely",
"version": "3.1.12",
"author": "ecmadao",
"bugs": {
"url": "https://github.com/ecmadao/react-times/issues"
},
"dependencies": {
"classnames": "^2.2.6",
"prop-types": "^15.6.0",
"react-bootstrap-typeahead": "^2.4.0",
"react-transition-group": "^2.2.1"
},
"peerDependencies": {
"moment": "^2.19.1",
"moment-timezone": "^0.5.14",
"react": "^16.2.0",
"react-dom": "^16.2.0"
},
"devDependencies": {
"@storybook/addon-info": "^3.3.14",
"@storybook/addon-knobs": "^3.3.14",
"@storybook/addons": "^3.3.14",
"@storybook/react": "^3.3.12",
"@storybook/storybook-deployer": "^2.2.0",
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-eslint": "^6.1.2",
"babel-loader": "^7.1.2",
"babel-plugin-system-import-transformer": "^3.1.0",
"babel-polyfill": "^6.16.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-0": "^6.16.0",
"babel-register": "^6.14.0",
"chai": "^3.5.0",
"coveralls": "^2.13.1",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"eslint": "^3.17.1",
"eslint-config-standard": "^7.0.1",
"eslint-loader": "^1.6.3",
"eslint-plugin-babel": "^4.1.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-react": "^6.10.0",
"eslint-plugin-standard": "^2.1.1",
"husky": "^0.14.3",
"in-publish": "^2.0.0",
"jsdom": "^11.6.2",
"mocha": "^3.3.0",
"mocha-lcov-reporter": "^1.3.0",
"moment": "^2.22.0",
"moment-timezone": "^0.5.14",
"react": "^16.3.1",
"react-dom": "^16.3.1",
"react-svg-loader": "^2.1.0",
"rimraf": "^2.6.1",
"safe-publish-latest": "^1.1.1",
"sinon": "^2.2.0",
"sinon-sandbox": "^1.0.2",
"style-loader": "^0.20.1",
"webpack": "3.10.0"
},
"homepage": "https://github.com/ecmadao/react-times#readme",
"keywords": [
"react",
"reactjs",
"time picker",
"time-picker",
"timepicker"
],
"license": "MIT",
"main": "index.js",
"maintainers": [
{
"email": "wlec@outlook.com",
"name": "ecmadao"
},
{
"email": "carlodicelico@gmail.com",
"name": "carlodicelico"
}
],
"repository": {
"type": "git",
"url": "git+https://github.com/ecmadao/react-times.git"
},
"scripts": {
"babel": "babel ./src --out-dir ./lib",
"build": "npm run clean && npm run babel",
"build:js": "babel src/ -d lib/ --ignore src/components",
"clean": "rimraf lib",
"coveralls": "cat ./coverage/lcov/lcov.info | ./node_modules/.bin/coveralls",
"deploy-storybook": "storybook-to-ghpages",
"eslint": "./node_modules/eslint/bin/eslint.js src",
"mocha": "./node_modules/mocha/bin/mocha --recursive ./test/_helpers --compilers js:babel-register,jsx:babel-register",
"postversion": "git commit package.json -m \"Version $npm_package_version\" && npm run tag && git push && git push --tags && npm publish --registry=https://registry.npmjs.org/",
"prepublish": "in-publish && safe-publish-latest && npm run build || not-in-publish",
"pretest": "npm run --silent eslint",
"scratch": "test/components/TwelveHoursTheme_spec.jsx",
"storybook": "start-storybook -p 9001 -c .storybook",
"storybook-deploy": "npm i && npm run storybook-pro && npm run deploy-storybook",
"storybook-pro": "build-storybook -c .storybook -o .out",
"tag": "git tag v$npm_package_version",
"test": "npm run mocha --silent test",
"test:watch": "npm run mocha test -- --watch",
"lint": "./node_modules/.bin/eslint ./src && ./node_modules/.bin/eslint ./test && ./node_modules/.bin/eslint ./stories",
"prepush": "npm run lint && npm test"
}
}
================================================
FILE: src/components/ClassicTheme/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import timeHelper from '../../utils/time';
const propTypes = {
hour: PropTypes.string,
minute: PropTypes.string,
timeMode: PropTypes.number,
meridiem: PropTypes.string,
clearFocus: PropTypes.func,
colorPalette: PropTypes.string,
handleTimeChange: PropTypes.func,
handleMeridiemChange: PropTypes.func,
focusDropdownOnTime: PropTypes.bool,
};
const defaultProps = {
hour: '00',
minute: '00',
timeMode: 24,
meridiem: 'AM',
colorPalette: 'light',
clearFocus: Function.prototype,
handleTimeChange: Function.prototype,
handleMeridiemChange: Function.prototype,
focusDropdownOnTime: false,
};
class ClassicTheme extends React.PureComponent {
constructor(props) {
super(props);
this.handleTimeChange = this.handleTimeChange.bind(this);
this.handleFocusDropdownOnTime = this.handleFocusDropdownOnTime.bind(this);
this.dropDown = React.createRef();
this.dropDownActiveTime = React.createRef();
}
componentDidMount() {
this.handleFocusDropdownOnTime();
}
componentDidUpdate() {
this.handleFocusDropdownOnTime();
}
handleFocusDropdownOnTime() {
if (this.props.focusDropdownOnTime) {
this.dropDown.current.scrollTop = this.dropDownActiveTime && this.dropDownActiveTime.current && this.dropDownActiveTime.current.offsetTop || 0;
}
}
handleTimeChange(timeData) {
const [time, meridiem] = timeData.split(' ');
const [hour, minute] = time.split(':');
const { handleTimeChange, clearFocus } = this.props;
handleTimeChange && handleTimeChange({
hour,
minute,
meridiem: meridiem || null
});
clearFocus && clearFocus();
}
checkTimeIsActive(time) {
const { hour, minute, meridiem } = this.props;
const [times, rawMeridiem] = time.split(' ');
const [rawHour, rawMinute] = times.split(':');
const currentHour = timeHelper.validate(rawHour);
const currentMinute = timeHelper.validate(rawMinute);
if (parseInt(hour, 10) !== parseInt(currentHour, 10)) {
return false;
}
if (meridiem && meridiem !== rawMeridiem) {
return false;
}
if (Math.abs(parseInt(minute, 10) - parseInt(currentMinute, 10)) < 15) {
return true;
}
return false;
}
renderTimes(timeDatas) {
const { colorPalette, focusDropdownOnTime } = this.props;
return timeDatas.map((timeData, index) => {
const timeClass = this.checkTimeIsActive(timeData)
? 'classic_time active'
: 'classic_time';
const [time, meridiem] = timeData.split(' ');
return (
<div
key={index}
onClick={() => {
this.handleTimeChange(timeData);
}}
className={`${timeClass} ${colorPalette}`}
ref={this.checkTimeIsActive(timeData) ? this.dropDownActiveTime : null}
>
{time}
{meridiem ? <span className="meridiem">{meridiem}</span> : null}
</div>
);
});
}
render() {
const { timeMode, timeConfig = {} } = this.props;
const timeDatas = timeMode === 12
? timeHelper.get12ModeTimes(timeConfig)
: timeHelper.get24ModeTimes(timeConfig);
return (
<div
className="modal_container classic_theme_container"
ref={this.dropDown}
>
{this.renderTimes(timeDatas)}
</div>
);
}
}
ClassicTheme.propTypes = propTypes;
ClassicTheme.defaultProps = defaultProps;
export default ClassicTheme;
================================================
FILE: src/components/Common/AsyncComponent.jsx
================================================
import React from 'react';
const asyncComponent = getComponent =>
class AsyncComponent extends React.Component {
static Component = null;
state = { Component: AsyncComponent.Component };
componentWillMount() {
if (!this.state.Component) {
getComponent().then(Component => {
AsyncComponent.Component = Component;
this.setState({ Component });
});
}
}
render() {
const { Component } = this.state;
if (Component) {
return <Component {...this.props} />;
}
return null;
}
}
export default asyncComponent;
================================================
FILE: src/components/Common/Button.jsx
================================================
import React from 'react';
import cx from 'classnames';
import PropTypes from 'prop-types';
class Button extends React.Component {
constructor(props) {
super(props);
this.state = {
pressed: false
};
this.onMouseUp = this.onMouseUp.bind(this);
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseEnter = this.onMouseEnter.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
}
onMouseDown() {
this.setState({ pressed: true });
}
onMouseUp() {
this.setState({ pressed: false });
}
onMouseEnter() {
const { onMouseEnter } = this.props;
onMouseEnter && onMouseEnter();
}
onMouseLeave() {
this.onMouseUp();
const { onMouseLeave } = this.props;
onMouseLeave && onMouseLeave();
}
render() {
const {
onClick,
children,
className,
} = this.props;
const { pressed } = this.state;
const buttonClass = cx(
'react_times_button',
pressed && 'pressDown',
className
);
return (
<div
onClick={onClick}
className={buttonClass}
onMouseUp={this.onMouseUp}
onMouseOut={this.onMouseUp}
onMouseDown={this.onMouseDown}
onMouseLeave={this.onMouseLeave}
onMouseEnter={this.onMouseEnter}
>
<div className="wrapper">
{children}
</div>
</div>
);
}
}
Button.propTypes = {
text: PropTypes.string,
onClick: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.element,
PropTypes.node,
PropTypes.array,
PropTypes.string
]),
className: PropTypes.string,
};
Button.defaultProps = {
text: 'button',
onClick: Function.prototype,
children: null,
className: '',
};
export default Button;
================================================
FILE: src/components/MaterialTheme/TwelveHoursMode.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import {
MINUTES,
TWELVE_HOURS,
PICKER_RADIUS,
POINTER_RADIUS,
MAX_ABSOLUTE_POSITION,
MIN_ABSOLUTE_POSITION,
} from '../../utils/constant.js';
import timeHelper from '../../utils/time';
import Button from '../Common/Button';
import PickerDragHandler from '../Picker/PickerDragHandler';
import pickerPointGenerator from '../Picker/PickerPointGenerator';
const TIME = timeHelper.time();
const propTypes = {
hour: PropTypes.string,
language: PropTypes.string,
minute: PropTypes.string,
draggable: PropTypes.bool,
meridiem: PropTypes.string,
phrases: PropTypes.object,
handleHourChange: PropTypes.func,
handleMinuteChange: PropTypes.func,
};
const defaultProps = {
hour: TIME.hour12,
language: 'en',
minute: TIME.minute,
draggable: false,
meridiem: TIME.meridiem,
handleHourChange: Function.prototype,
handleMinuteChange: Function.prototype,
};
class TwelveHoursMode extends React.PureComponent {
constructor(props) {
super(props);
const hourPointerRotate = this.resetHourDegree();
const minutePointerRotate = this.resetMinuteDegree();
this.state = {
hourPointerRotate,
minutePointerRotate
};
this.handleHourChange = this.handleHourChange.bind(this);
this.handleMinuteChange = this.handleMinuteChange.bind(this);
this.handleDegreeChange = this.handleDegreeChange.bind(this);
this.handleMeridiemChange = this.handleMeridiemChange.bind(this);
this.handleHourPointerClick = this.handleHourPointerClick.bind(this);
this.handleMinutePointerClick = this.handleMinutePointerClick.bind(this);
}
resetHourDegree() {
const hour = parseInt(this.props.hour, 10);
let pointerRotate = 0;
TWELVE_HOURS.forEach((h, index) => {
if (hour === index + 1) {
pointerRotate = (360 * (index + 1)) / 12;
}
});
return pointerRotate;
}
resetMinuteDegree() {
const minute = parseInt(this.props.minute, 10);
let pointerRotate = 0;
MINUTES.forEach((m, index) => {
if (minute === index) {
pointerRotate = (360 * index) / 60;
}
});
return pointerRotate;
}
getHourTopAndHeight() {
const height = MIN_ABSOLUTE_POSITION - POINTER_RADIUS;
const top = (PICKER_RADIUS - MIN_ABSOLUTE_POSITION) + POINTER_RADIUS;
return [top, height];
}
getMinuteTopAndHeight() {
const height = MAX_ABSOLUTE_POSITION - POINTER_RADIUS;
const top = (PICKER_RADIUS - MAX_ABSOLUTE_POSITION) + POINTER_RADIUS;
return [top, height];
}
handleMeridiemChange() {
const { meridiem, phrases } = this.props;
const newMeridiem = (meridiem === 'AM' || meridiem === phrases.am)
? phrases.pm
: phrases.am;
if (newMeridiem !== meridiem) {
const { handleMeridiemChange } = this.props;
handleMeridiemChange && handleMeridiemChange(newMeridiem);
}
}
handleHourPointerClick(options) {
const {
time,
pointerRotate = null,
} = options;
this.handleHourChange(time);
pointerRotate !== null && this.handleDegreeChange({ hourPointerRotate: pointerRotate });
}
handleMinutePointerClick(options) {
const {
time,
pointerRotate = null,
} = options;
this.handleMinuteChange(time);
pointerRotate !== null && this.handleDegreeChange({ minutePointerRotate: pointerRotate });
}
handleDegreeChange(pointerRotate) {
this.setState(pointerRotate);
}
handleHourChange(time) {
const hour = parseInt(time, 10);
const { handleHourChange } = this.props;
handleHourChange && handleHourChange(hour);
}
handleMinuteChange(time) {
const minute = parseInt(time, 10);
const { handleMinuteChange } = this.props;
handleMinuteChange && handleMinuteChange(minute);
}
render() {
const {
hour,
minute,
phrases,
timeMode,
meridiem,
draggable,
clearFocus,
limitDrag,
minuteStep,
showTimezone,
} = this.props;
const { hourPointerRotate, minutePointerRotate } = this.state;
const [top, height] = this.getHourTopAndHeight();
const hourRotateState = {
top,
height,
pointerRotate: hourPointerRotate
};
const [minuteTop, minuteHeight] = this.getMinuteTopAndHeight();
const minuteRotateState = {
top: minuteTop,
height: minuteHeight,
pointerRotate: minutePointerRotate
};
const HourPickerPointGenerator = pickerPointGenerator('hour', 12);
const MinutePickerPointGenerator = pickerPointGenerator('minute', 12);
return (
<React.Fragment>
<div className="time_picker_modal_header">
<span className="time_picker_header active">
{hour}:{minute}
</span>
<span
onClick={this.handleMeridiemChange}
className="time_picker_header meridiem"
>
{meridiem}
</span>
</div>
<div className="picker_container">
<HourPickerPointGenerator
handleTimePointerClick={this.handleHourPointerClick}
pointerRotate={hourPointerRotate}
/>
<MinutePickerPointGenerator
handleTimePointerClick={this.handleMinutePointerClick}
pointerRotate={minutePointerRotate}
/>
<PickerDragHandler
step={1}
timeMode={timeMode}
limitDrag={limitDrag}
minuteStep={minuteStep}
rotateState={minuteRotateState}
time={parseInt(minute, 10)}
minLength={MAX_ABSOLUTE_POSITION}
draggable={draggable}
handleTimePointerClick={this.handleMinutePointerClick}
/>
<PickerDragHandler
step={0}
timeMode={timeMode}
limitDrag={limitDrag}
minuteStep={minuteStep}
rotateState={hourRotateState}
time={parseInt(hour, 10)}
maxLength={MIN_ABSOLUTE_POSITION}
draggable={draggable}
handleTimePointerClick={this.handleHourPointerClick}
/>
</div>
{!showTimezone ? (
<div className="buttons_wrapper">
<Button
onClick={clearFocus}
className="time_picker_button"
>
{phrases.close}
</Button>
</div>
) : null}
</React.Fragment>
);
}
}
TwelveHoursMode.propTypes = propTypes;
TwelveHoursMode.defaultProps = defaultProps;
export default TwelveHoursMode;
================================================
FILE: src/components/MaterialTheme/TwentyFourHoursMode.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import {
HOURS,
MINUTES,
PICKER_RADIUS,
POINTER_RADIUS,
MAX_ABSOLUTE_POSITION,
MIN_ABSOLUTE_POSITION,
} from '../../utils/constant.js';
import PickerDragHandler from '../Picker/PickerDragHandler';
import pickerPointGenerator from '../Picker/PickerPointGenerator';
const propTypes = {
step: PropTypes.number,
hour: PropTypes.string,
autoMode: PropTypes.bool,
minute: PropTypes.string,
handleHourChange: PropTypes.func,
handleMinuteChange: PropTypes.func,
clearFocus: PropTypes.func
};
const defaultProps = {
step: 0,
hour: '00',
minute: '00',
autoMode: true,
clearFocus: Function.prototype,
handleHourChange: Function.prototype,
handleMinuteChange: Function.prototype,
};
class TwentyFourHoursMode extends React.PureComponent {
constructor(props) {
super(props);
const pointerRotate = this.resetHourDegree();
const { step } = props;
this.state = {
step,
pointerRotate
};
this.handleStepChange = this.handleStepChange.bind(this);
this.handleTimeChange = this.handleTimeChange.bind(this);
this.handleTimePointerClick = this.handleTimePointerClick.bind(this);
}
handleStepChange(step) {
const stateStep = this.state.step;
if (stateStep !== step) {
this.pickerPointerContainer && this.pickerPointerContainer.addAnimation();
setTimeout(() => {
this.pickerPointerContainer && this.pickerPointerContainer.removeAnimation();
this.setStep(step);
}, 300);
}
}
setStep(step) {
const pointerRotate = step === 0
? this.resetHourDegree()
: this.resetMinuteDegree();
this.setState({
step,
pointerRotate
});
}
clearFocus() {
const { autoClose, clearFocus } = this.props;
autoClose && clearFocus && clearFocus();
}
handleTimePointerClick(options) {
const {
time,
autoMode = null,
pointerRotate = null,
} = options;
const isInteger = function(num) {
return (num ^ 0) === +num;
}
if (Number.isInteger) {
Number.isInteger(pointerRotate) && this.setState({ pointerRotate: pointerRotate });
} else {
isInteger(pointerRotate) && this.setState({ pointerRotate: pointerRotate });
}
this.handleTimeChange(time, autoMode);
}
handleTimeChange(time, autoMode = null) {
const validateTime = parseInt(time, 10);
const { step } = this.state;
const auto = autoMode === null ? this.props.autoMode : autoMode;
const {
handleHourChange,
handleMinuteChange,
} = this.props;
if (step === 0) {
handleHourChange && handleHourChange(validateTime);
} else {
handleMinuteChange && handleMinuteChange(validateTime);
}
if (!auto) return;
if (step === 0) {
this.handleStepChange(1);
} else {
this.clearFocus();
this.setStep(0);
}
}
resetHourDegree() {
const hour = parseInt(this.props.hour, 10);
let pointerRotate = 0;
HOURS.forEach((h, index) => {
if (hour === index + 1) {
pointerRotate = index < 12
? (360 * (index + 1)) / 12
: (360 * ((index + 1) - 12)) / 12;
}
});
return pointerRotate;
}
resetMinuteDegree() {
const minute = parseInt(this.props.minute, 10);
let pointerRotate = 0;
MINUTES.forEach((m, index) => {
if (minute === index) {
pointerRotate = (360 * index) / 60;
}
});
return pointerRotate;
}
getTopAndHeight() {
const { step } = this.state;
const { hour, minute } = this.props;
const time = step === 0 ? hour : minute;
const splitNum = step === 0 ? 12 : 60;
const minLength = step === 0
? MIN_ABSOLUTE_POSITION
: MAX_ABSOLUTE_POSITION;
const height = time < splitNum
? minLength - POINTER_RADIUS
: MAX_ABSOLUTE_POSITION - POINTER_RADIUS;
const top = time < splitNum
? (PICKER_RADIUS - minLength) + POINTER_RADIUS
: (PICKER_RADIUS - MAX_ABSOLUTE_POSITION) + POINTER_RADIUS;
return [top, height];
}
render() {
const {
hour,
minute,
timeMode,
draggable,
limitDrag,
minuteStep,
} = this.props;
const { step, pointerRotate } = this.state;
const activeHourClass = step === 0
? 'time_picker_header active'
: 'time_picker_header';
const activeMinuteClass = step === 1
? 'time_picker_header active'
: 'time_picker_header';
const [top, height] = this.getTopAndHeight();
const rotateState = {
top,
height,
pointerRotate
};
const type = step === 0 ? 'hour' : 'minute';
const PickerPointGenerator = pickerPointGenerator(type);
return (
<React.Fragment>
<div className="time_picker_modal_header">
<span
className={activeHourClass}
onClick={() => this.handleStepChange(0)}
>
{hour}
</span>
<span className="time_picker_header_delivery">:</span>
<span
className={activeMinuteClass}
onClick={() => this.handleStepChange(1)}
>
{minute}
</span>
</div>
<div className="picker_container">
<PickerPointGenerator
ref={ref => (this.pickerPointerContainer = ref)}
handleTimePointerClick={this.handleTimePointerClick}
pointerRotate={pointerRotate}
/>
<PickerDragHandler
step={step}
timeMode={timeMode}
limitDrag={limitDrag}
minuteStep={minuteStep}
draggable={draggable}
rotateState={rotateState}
time={step === 0 ? parseInt(hour, 10) : parseInt(minute, 10)}
minLength={step === 0
? MIN_ABSOLUTE_POSITION
: MAX_ABSOLUTE_POSITION}
handleTimePointerClick={this.handleTimePointerClick}
/>
</div>
</React.Fragment>
);
}
}
TwentyFourHoursMode.propTypes = propTypes;
TwentyFourHoursMode.defaultProps = defaultProps;
export default TwentyFourHoursMode;
================================================
FILE: src/components/MaterialTheme/index.jsx
================================================
import React from 'react';
import asyncComponent from '../Common/AsyncComponent';
import Timezone from '../Timezone';
const DialPlates = {
12: asyncComponent(
() => System.import('./TwelveHoursMode')
.then(component => component.default)
),
24: asyncComponent(
() => System.import('./TwentyFourHoursMode')
.then(component => component.default)
),
};
const MaterialTheme = (props) => {
const {
phrases,
timeMode,
timezone,
showTimezone,
onTimezoneChange,
timezoneIsEditable,
} = props;
const DialPlate = DialPlates[timeMode];
return (
<div className="modal_container time_picker_modal_container" id="MaterialTheme">
<DialPlate
{...props}
/>
{showTimezone
? <Timezone
phrases={phrases}
timezone={timezone}
timezoneIsEditable={timezoneIsEditable}
onTimezoneChange={onTimezoneChange}
/>
: null
}
</div>
);
};
export default MaterialTheme;
================================================
FILE: src/components/OutsideClickHandler.jsx
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
const propTypes = {
children: PropTypes.node,
onOutsideClick: PropTypes.func,
};
const defaultProps = {
children: <span />,
onOutsideClick: Function.prototype,
};
class OutsideClickHandler extends React.PureComponent {
constructor(props) {
super(props);
this.hasAction = false;
this.onOutsideClick = this.onOutsideClick.bind(this);
}
componentDidMount() {
this.bindActions();
}
componentDidUpdate() {
this.bindActions();
}
componentWillUnmount() {
this.unbindActions();
}
bindActions() {
const { closeOnOutsideClick } = this.props;
if (closeOnOutsideClick) {
if (this.hasAction) return;
if (document.addEventListener) {
document.addEventListener('mousedown', this.onOutsideClick, true);
} else {
document.attachEvent('onmousedown', this.onOutsideClick);
}
this.hasAction = true;
}
}
unbindActions() {
if (!this.hasAction) return;
const { closeOnOutsideClick } = this.props;
if (closeOnOutsideClick) {
if (document.removeEventListener) {
document.removeEventListener('mousedown', this.onOutsideClick, true);
} else {
document.detachEvent('onmousedown', this.onOutsideClick);
}
this.hasAction = false;
}
}
onOutsideClick(e) {
const event = e || window.event;
const mouseTarget = (typeof event.which !== 'undefined') ? event.which : event.button;
const isDescendantOfRoot = ReactDOM.findDOMNode(this.childNode).contains(event.target);
if (!isDescendantOfRoot && mouseTarget === 1) {
const { onOutsideClick } = this.props;
onOutsideClick && onOutsideClick(event);
}
}
render() {
const { focused } = this.props;
const outsideClass = focused
? 'outside_container active'
: 'outside_container';
return (
<div ref={c => (this.childNode = c)} className={outsideClass}>
{this.props.children}
</div>
);
}
}
OutsideClickHandler.propTypes = propTypes;
OutsideClickHandler.defaultProps = defaultProps;
export default OutsideClickHandler;
================================================
FILE: src/components/Picker/PickerDragHandler.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import {
PICKER_RADIUS,
POINTER_RADIUS,
MAX_ABSOLUTE_POSITION,
MIN_ABSOLUTE_POSITION,
} from '../../utils/constant.js';
import darg from '../../utils/drag';
const propTypes = {
time: PropTypes.number,
step: PropTypes.number,
draggable: PropTypes.bool,
pointerRotate: PropTypes.number,
minLength: PropTypes.number,
maxLength: PropTypes.number,
minuteStep: PropTypes.number,
limitDrag: PropTypes.bool,
rotateState: PropTypes.shape({
top: PropTypes.number,
height: PropTypes.number,
pointerRotate: PropTypes.number
}),
handleTimePointerClick: PropTypes.func
};
const defaultProps = {
time: 0,
step: 0,
pointerRotate: 0,
rotateState: {
top: 0,
height: 0,
pointerRotate: 0
},
minLength: MIN_ABSOLUTE_POSITION,
maxLength: MAX_ABSOLUTE_POSITION,
minuteStep: 5,
limitDrag: false,
handleTimePointerClick: Function.prototype
};
class PickerDragHandler extends React.PureComponent {
constructor(props) {
super(props);
this.startX = 0;
this.startY = 0;
this.originX = null;
this.originY = null;
this.dragCenterX = null;
this.dragCenterY = null;
this.offsetDragCenterX = 0;
this.offsetDragCenterY = 0;
this.state = this.initialRotationAndLength();
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.resetOrigin = this.resetOrigin.bind(this);
}
componentDidMount() {
this.resetOrigin();
if (window.addEventListener) {
window.addEventListener('resize', this.resetOrigin, true);
} else {
window.addEventListener('onresize', this.resetOrigin);
}
if (document.addEventListener) {
document.addEventListener('scroll', this.resetOrigin, true);
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('mouseup', this.handleMouseUp, true);
document.addEventListener('touchmove', this.handleMouseMove, true);
document.addEventListener('touchend', this.handleMouseUp, true);
} else {
document.addEventListener('onscroll', this.resetOrigin);
document.attachEvent('onmousemove', this.handleMouseMove);
document.attachEvent('onmouseup', this.handleMouseUp);
document.attachEvent('ontouchmove', this.handleMouseMove);
document.attachEvent('ontouchend', this.handleMouseUp);
}
}
componentWillUnmount() {
if (window.addEventListener) {
window.removeEventListener('resize', this.resetOrigin, true);
} else {
window.detachEvent('onresize', this.resetOrigin);
}
if (document.removeEventListener) {
document.removeEventListener('scroll', this.resetOrigin, true);
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('mouseup', this.handleMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseMove, true);
document.removeEventListener('touchend', this.handleMouseUp, true);
} else {
document.detachEvent('onscroll', this.resetOrigin);
document.detachEvent('onmousemove', this.handleMouseMove);
document.detachEvent('onmouseup', this.handleMouseUp);
document.detachEvent('ontouchmove', this.handleMouseMove);
document.detachEvent('ontouchend', this.handleMouseUp);
}
}
componentDidUpdate(prevProps) {
const { step, time, rotateState } = this.props;
const { draging } = this.state;
const prevStep = prevProps.step;
const prevTime = prevProps.time;
const PrevRotateState = prevProps.rotateState;
if ((step !== prevStep
|| time !== prevTime
|| rotateState.pointerRotate !== PrevRotateState.pointerRotate)
&& !draging) {
this.resetState();
}
}
initialRotationAndLength() {
const { rotateState } = this.props;
const {
top,
height,
pointerRotate
} = rotateState;
this.initialHeight = height;
return {
top,
height,
pointerRotate,
draging: false
};
}
resetState() {
this.setState(this.initialRotationAndLength());
}
resetOrigin() {
const centerPoint = this.pickerCenter;
const centerPointPos = centerPoint.getBoundingClientRect();
this.originX =
centerPointPos.left +
(centerPoint.clientWidth / 2) +
Math.max(document.documentElement.scrollLeft, document.body.scrollLeft) + POINTER_RADIUS;
this.originY =
centerPointPos.top +
(centerPoint.clientHeight / 2) +
Math.max(document.documentElement.scrollTop, document.body.scrollTop) + POINTER_RADIUS;
this.resetDragCenter();
}
resetDragCenter() {
this.offsetDragCenterX = 0;
this.offsetDragCenterY = 0;
const dragCenterPoint = this.dragCenter;
const dragCenterPointPos = dragCenterPoint.getBoundingClientRect();
this.dragCenterX =
dragCenterPointPos.left +
(dragCenterPoint.clientWidth / 2) +
Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
this.dragCenterY =
dragCenterPointPos.top +
(dragCenterPoint.clientHeight / 2) +
Math.max(document.documentElement.scrollTop, document.body.scrollTop);
}
getRadian(x, y) {
let sRad = Math.atan2(y - this.originY, x - this.originX);
sRad -= Math.atan2(
this.startY - this.originY,
this.startX - this.originX
);
if (sRad > Math.PI) {
sRad -= Math.PI * 2;
} else if (sRad < -Math.PI) {
sRad += Math.PI * 2;
}
sRad += darg.degree2Radian(this.props.rotateState.pointerRotate);
return sRad;
}
getAbsolutePosition(x, y) {
return Math.sqrt(
Math.pow((x - this.originX), 2) + Math.pow((y - this.originY), 2)
);
}
getPointerRotate(options = {}) {
const {
dragX,
dragY,
} = options;
const {
step,
limitDrag,
minuteStep,
} = this.props;
const sRad = this.getRadian(dragX, dragY);
let pointerRotate = sRad * (360 / (2 * Math.PI));
if (limitDrag) {
const degree = sRad * (360 / (2 * Math.PI));
const isHour = step === 0;
const sectionCount = isHour ? 12 : (60 / minuteStep);
const roundSeg = Math.round(degree / (360 / sectionCount));
pointerRotate = roundSeg * (360 / sectionCount);
}
return pointerRotate;
}
handleTimePointerChange(options = {}) {
const {
dragX,
dragY,
autoMode = null,
pointerRotate = null,
} = options;
const {
step,
timeMode,
minLength,
maxLength,
minuteStep,
handleTimePointerClick,
} = this.props;
const sRad = this.getRadian(dragX, dragY);
const degree = sRad * (360 / (2 * Math.PI));
const isHour = step === 0;
const sectionCount = isHour ? 12 : (60 / minuteStep);
let roundSeg = Math.round(degree / (360 / sectionCount));
let absolutePosition = this.getAbsolutePosition(dragX, dragY);
absolutePosition = darg.validatePosition(
absolutePosition,
minLength,
maxLength
);
if (minLength < absolutePosition && absolutePosition < maxLength) {
if ((absolutePosition - minLength) > (maxLength - minLength) / 2) {
absolutePosition = maxLength;
} else {
absolutePosition = minLength;
}
}
while (roundSeg > sectionCount) {
roundSeg -= sectionCount;
}
let time = absolutePosition === minLength
? roundSeg
: roundSeg + sectionCount;
if (isHour) {
if (absolutePosition === minLength && time < 0) {
time += 12;
} else if (absolutePosition !== minLength && time < 12) {
time = 24 + (time - 12);
}
time = time === 24 ? 12 : time;
if (time === 12 && Number(timeMode) === 12) time = 0;
} else {
time = (time * minuteStep === 60 ? 0 : time * minuteStep);
time = time < 0 ? 60 + time : time;
}
handleTimePointerClick && handleTimePointerClick({
time,
autoMode,
pointerRotate
});
}
handleMouseDown(e) {
if (!this.state.draging) {
const event = e || window.event;
event.preventDefault();
event.stopPropagation();
const pos = darg.mousePosition(event);
this.startX = pos.x;
this.startY = pos.y;
this.resetDragCenter();
this.offsetDragCenterX = this.dragCenterX - this.startX;
this.offsetDragCenterY = this.dragCenterY - this.startY;
this.setState({
draging: true
});
}
}
handleMouseMove(e) {
if (this.state.draging) {
const {
minLength,
maxLength,
} = this.props;
const pos = darg.mousePosition(e);
const dragX = pos.x + this.offsetDragCenterX;
const dragY = pos.y + this.offsetDragCenterY;
if (this.originX !== dragX && this.originY !== dragY) {
const pointerRotate = this.getPointerRotate({ dragX, dragY });
const absolutePosition = this.getAbsolutePosition(dragX, dragY);
const height = darg.validatePosition(
absolutePosition,
minLength - POINTER_RADIUS,
maxLength - POINTER_RADIUS
);
const top = PICKER_RADIUS - height;
this.setState({
top,
height,
pointerRotate
});
this.handleTimePointerChange({
dragX,
dragY,
autoMode: false
});
}
}
}
handleMouseUp(e) {
if (this.state.draging) {
this.setState({
draging: false
});
const pos = darg.mousePosition(e);
const endX = pos.x + this.offsetDragCenterX;
const endY = pos.y + this.offsetDragCenterY;
let pointerRotate = this.getPointerRotate({
dragX: endX,
dragY: endY
});
const remainder = pointerRotate % 30;
const base = Math.floor(pointerRotate / 30);
pointerRotate = (base + (remainder >= 15 ? 1 : 0)) * 30;
this.setState({ pointerRotate });
this.handleTimePointerChange({
dragX: endX,
dragY: endY,
pointerRotate,
});
}
}
render() {
const { time, draggable } = this.props;
const { draging, height, top, pointerRotate } = this.state;
const pickerPointerClass = draging
? 'picker_pointer'
: 'picker_pointer animation';
return (
<div className="picker_handler">
<div
className={pickerPointerClass}
style={darg.initialPointerStyle(height, top, pointerRotate)}
>
<div
ref={r => (this.dragCenter = r)}
className={`pointer_drag ${draggable ? 'draggable' : ''}`}
style={darg.rotateStyle(-pointerRotate)}
onMouseDown={draggable ? this.handleMouseDown : Function.prototype}
onTouchStart={draggable ? this.handleMouseDown : Function.prototype}
>
{time}
</div>
</div>
<div
className="picker_center"
ref={p => (this.pickerCenter = p)}
/>
</div>
);
}
}
PickerDragHandler.propTypes = propTypes;
PickerDragHandler.defaultProps = defaultProps;
export default PickerDragHandler;
================================================
FILE: src/components/Picker/PickerPoint.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import darg from '../../utils/drag';
const propTypes = {
index: PropTypes.number,
angle: PropTypes.number,
onClick: PropTypes.func,
pointClass: PropTypes.string,
};
const defaultProps = {
index: 0,
angle: 0,
onClick: Function.prototype,
pointClass: 'picker_point point_outter',
};
const PickerPoint = (props) => {
const {
index,
angle,
onClick,
pointClass,
pointerRotate,
} = props;
const inlineStyle = darg.inlineRotateStyle(angle);
const wrapperStyle = darg.rotateStyle(-angle);
return (
<div
style={inlineStyle}
className={pointClass}
onClick={() => {
let relativeRotate = angle - (pointerRotate % 360);
if (relativeRotate >= 180) {
relativeRotate -= 360;
} else if (relativeRotate < -180) {
relativeRotate += 360;
}
onClick && onClick({
time: index,
pointerRotate: relativeRotate + pointerRotate
});
}}
onMouseDown={darg.disableMouseDown}
>
<div className="point_wrapper" style={wrapperStyle}>
{index}
</div>
</div>
);
};
PickerPoint.propTypes = propTypes;
PickerPoint.defaultProps = defaultProps;
export default PickerPoint;
================================================
FILE: src/components/Picker/PickerPointGenerator.jsx
================================================
import React from 'react';
import {
HOURS,
MINUTES,
TWELVE_HOURS
} from '../../utils/constant.js';
import PickerPoint from './PickerPoint';
const pickerPointGenerator = (type = 'hour', mode = 24) =>
class PickerPointGenerator extends React.PureComponent {
addAnimation() {
this.pickerPointerContainer.className = 'animation';
}
removeAnimation() {
this.pickerPointerContainer.className = '';
}
renderMinutePointes() {
return MINUTES.map((_, index) => {
const angle = (360 * index) / 60;
if (index % 5 === 0) {
return (
<PickerPoint
key={index}
angle={angle}
index={index}
pointerRotate={this.props.pointerRotate}
onClick={this.props.handleTimePointerClick}
/>
);
}
return null;
});
}
renderHourPointes() {
const hours = parseInt(mode, 10) === 24 ? HOURS : TWELVE_HOURS;
return hours.map((_, index) => {
const pointClass = index < 12
? 'picker_point point_inner'
: 'picker_point point_outter';
const angle = index < 12
? (360 * index) / 12
: (360 * (index - 12)) / 12;
return (
<PickerPoint
key={index}
angle={angle}
index={index}
pointClass={pointClass}
pointerRotate={this.props.pointerRotate}
onClick={this.props.handleTimePointerClick}
/>
);
});
}
render() {
return (
<div
ref={ref => (this.pickerPointerContainer = ref)}
className="picker_pointer_container"
>
{type === 'hour'
? this.renderHourPointes()
: this.renderMinutePointes()}
</div>
);
}
};
export default pickerPointGenerator;
================================================
FILE: src/components/TimePicker.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import OutsideClickHandler from './OutsideClickHandler';
import Button from './Common/Button';
import timeHelper from '../utils/time.js';
import languageHelper from '../utils/language';
import ICONS from '../utils/icons';
import { is } from '../utils/func';
import asyncComponent from './Common/AsyncComponent';
const DialPlates = {
material: asyncComponent(
() => System.import('./MaterialTheme')
.then(component => component.default)
),
classic: asyncComponent(
() => System.import('./ClassicTheme')
.then(component => component.default)
),
};
// aliases for defaultProps readability
const TIME = timeHelper.time({ useTz: false });
TIME.current = timeHelper.current();
const propTypes = {
autoMode: PropTypes.bool,
autoClose: PropTypes.bool,
colorPalette: PropTypes.string,
draggable: PropTypes.bool,
focused: PropTypes.bool,
language: PropTypes.string,
meridiem: PropTypes.string,
onFocusChange: PropTypes.func,
onTimeChange: PropTypes.func,
onTimezoneChange: PropTypes.func,
phrases: PropTypes.object,
placeholder: PropTypes.string,
showTimezone: PropTypes.bool,
theme: PropTypes.string,
time: PropTypes.string,
timeMode: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
timezone: PropTypes.string,
timezoneIsEditable: PropTypes.bool,
trigger: PropTypes.oneOfType([
PropTypes.func,
PropTypes.object,
PropTypes.element,
PropTypes.array,
PropTypes.node,
PropTypes.instanceOf(React.Component),
PropTypes.instanceOf(React.PureComponent)
]),
withoutIcon: PropTypes.bool,
minuteStep: PropTypes.number,
limitDrag: PropTypes.bool,
timeFormat: PropTypes.string,
timeFormatter: PropTypes.func,
useTz: PropTypes.bool,
closeOnOutsideClick: PropTypes.bool,
timeConfig: PropTypes.object,
disabled: PropTypes.bool,
focusDropdownOnTime: PropTypes.bool,
};
const defaultProps = {
autoMode: true,
autoClose: true,
colorPalette: 'light',
draggable: true,
focused: false,
language: 'en',
meridiem: TIME.meridiem,
onFocusChange: Function.prototype,
onTimeChange: Function.prototype,
onTimezoneChange: Function.prototype,
placeholder: '',
showTimezone: false,
theme: 'material',
time: '',
timeMode: TIME.mode,
trigger: null,
withoutIcon: false,
minuteStep: 5,
limitDrag: false,
timeFormat: '',
timeFormatter: null,
useTz: true,
closeOnOutsideClick: true,
timeConfig: {
step: 30,
unit: 'minutes'
},
disabled: false,
focusDropdownOnTime: true,
};
class TimePicker extends React.PureComponent {
constructor(props) {
super(props);
const { focused, timezone, onTimezoneChange } = props;
const timeData = this.timeData(false);
const timezoneData = timeHelper.tzForName(timeData.timezone);
this.state = {
focused,
timezoneData,
timeChanged: false
};
this.onBlur = this.onBlur.bind(this);
this.onFocus = this.onFocus.bind(this);
this.timeData = this.timeData.bind(this);
this.handleTimeChange = this.handleTimeChange.bind(this);
this.handleHourChange = this.handleHourChange.bind(this);
this.handleMinuteChange = this.handleMinuteChange.bind(this);
this.handleMeridiemChange = this.handleMeridiemChange.bind(this);
this.handleHourAndMinuteChange = this.handleHourAndMinuteChange.bind(this);
// if a timezone value was not passed in,
// call the callback with the default value used for timezone
if (!timezone) {
onTimezoneChange(timezoneData);
}
}
componentWillReceiveProps(nextProps) {
const { focused } = nextProps;
if (focused !== this.props.focused) {
this.setState({ focused });
}
}
onFocus() {
const { focused } = this.state;
if (!focused) {
this.onFocusChange(!focused);
}
}
onBlur() {
const { focused } = this.state;
if (focused) {
this.onFocusChange(!focused);
}
}
onFocusChange(focused) {
const { disabled } = this.props;
if (disabled) return;
this.setState({ focused });
const { onFocusChange } = this.props;
onFocusChange && onFocusChange(focused);
}
timeData(timeChanged) {
const {
time,
useTz,
timeMode,
timezone,
meridiem,
} = this.props;
const timeData = timeHelper.time({
time,
meridiem,
timeMode,
tz: timezone,
useTz: !time && !timeChanged && useTz
});
return timeData;
}
get languageData() {
const { language, phrases = {} } = this.props;
return Object.assign({}, languageHelper.get(language), phrases);
}
get hourAndMinute() {
const { timeMode } = this.props;
const timeData = this.timeData(this.state.timeChanged);
// Since someone might pass a time in 24h format, etc., we need to get it from
// timeData to 'translate' it into the local format, including its accurate meridiem
const hour = (parseInt(timeMode, 10) === 12)
? (parseInt(timeData.hour12, 10) === 12 ? '00' : timeData.hour12)
: (parseInt(timeData.hour24, 10) === 24 ? '00' : timeData.hour24);
const minute = timeData.minute;
return [hour, minute];
}
get formattedTime() {
const {
timeMode,
timeFormat,
timeFormatter,
} = this.props;
const [hour, minute] = this.hourAndMinute;
const validTimeMode = timeHelper.validateTimeMode(timeMode);
let times = '';
if (timeFormatter && is.func(timeFormatter)) {
times = timeFormatter({
hour,
minute,
meridiem: this.meridiem
});
} else if (timeFormat && is.string(timeFormat)) {
times = timeFormat;
if (/HH?/.test(times) || /MM?/.test(times)) {
if (validTimeMode === 12) {
console.warn('It seems you are using 12 hours mode with 24 hours time format. Please check your timeMode and timeFormat props');
}
} else if (/hh?/.test(times) || /mm?/.test(times)) {
if (validTimeMode === 24) {
console.warn('It seems you are using 24 hours mode with 12 hours time format. Please check your timeMode and timeFormat props');
}
}
times = times.replace(/(HH|hh)/g, hour);
times = times.replace(/(MM|mm)/g, minute);
times = times.replace(/(H|h)/g, Number(hour));
times = times.replace(/(M|m)/g, Number(minute));
} else {
times = (validTimeMode === 12)
? `${hour} : ${minute} ${this.meridiem}`
: `${hour} : ${minute}`;
}
return times;
}
get meridiem() {
const { meridiem } = this.props;
const timeData = this.timeData(this.state.timeChanged);
const localMessages = this.languageData;
// eslint-disable-next-line no-unneeded-ternary
const m = (meridiem) ? meridiem : timeData.meridiem;
// eslint-disable-next-line no-extra-boolean-cast
return m && !!(m.match(/^am|pm/i)) ? localMessages[m.toLowerCase()] : m;
}
onTimeChanged(timeChanged) {
this.setState({ timeChanged });
}
handleHourChange(hour) {
const validateHour = timeHelper.validate(hour);
const minute = this.hourAndMinute[1];
this.handleTimeChange({
hour: validateHour,
minute,
meridiem: this.meridiem
});
}
handleMinuteChange(minute) {
const validateMinute = timeHelper.validate(minute);
const hour = this.hourAndMinute[0];
this.handleTimeChange({
hour,
minute: validateMinute,
meridiem: this.meridiem
});
}
handleMeridiemChange(meridiem) {
const [hour, minute] = this.hourAndMinute;
this.handleTimeChange({
hour,
minute,
meridiem
});
}
handleTimeChange(options) {
const { onTimeChange } = this.props;
onTimeChange && onTimeChange(options);
this.onTimeChanged(true);
}
handleHourAndMinuteChange(time) {
this.onTimeChanged(true);
const { onTimeChange, autoClose } = this.props;
if (autoClose) this.onBlur();
return onTimeChange && onTimeChange(time);
}
renderDialPlate() {
const {
theme,
disabled,
timeMode,
autoMode,
autoClose,
draggable,
language,
limitDrag,
minuteStep,
timeConfig,
colorPalette,
showTimezone,
onTimezoneChange,
timezoneIsEditable,
focusDropdownOnTime,
} = this.props;
if (disabled) return null;
const dialTheme = theme === 'material' ? theme : 'classic';
const DialPlate = DialPlates[dialTheme];
const { timezoneData } = this.state;
const [hour, minute] = this.hourAndMinute;
return (
<DialPlate
hour={hour}
minute={minute}
autoMode={autoMode}
autoClose={autoClose}
language={language}
draggable={draggable}
limitDrag={limitDrag}
timezone={timezoneData}
meridiem={this.meridiem}
timeConfig={timeConfig}
showTimezone={showTimezone}
phrases={this.languageData}
colorPalette={colorPalette}
clearFocus={this.onBlur}
timeMode={parseInt(timeMode, 10)}
onTimezoneChange={onTimezoneChange}
minuteStep={parseInt(minuteStep, 10)}
timezoneIsEditable={timezoneIsEditable}
handleHourChange={this.handleHourChange}
handleTimeChange={this.handleTimeChange}
handleMinuteChange={this.handleMinuteChange}
handleMeridiemChange={this.handleMeridiemChange}
focusDropdownOnTime={focusDropdownOnTime}
/>
);
}
render() {
const {
trigger,
disabled,
placeholder,
withoutIcon,
colorPalette,
closeOnOutsideClick
} = this.props;
const { focused } = this.state;
const times = this.formattedTime;
const pickerPreviewClass = cx(
'time_picker_preview',
focused && 'active',
disabled && 'disabled'
);
const containerClass = cx(
'time_picker_container',
colorPalette === 'dark' && 'dark'
);
const previewContainerClass = cx(
'preview_container',
withoutIcon && 'without_icon'
);
return (
<div className={containerClass}>
{trigger || (
<Button
onClick={this.onFocus}
className={pickerPreviewClass}
>
<div className={previewContainerClass}>
{withoutIcon ? '' : (ICONS.time)}
{placeholder || times}
</div>
</Button>
)}
<OutsideClickHandler
focused={focused}
onOutsideClick={this.onBlur}
closeOnOutsideClick={disabled ? false : closeOnOutsideClick}
>
{this.renderDialPlate()}
</OutsideClickHandler>
</div>
);
}
}
TimePicker.propTypes = propTypes;
TimePicker.defaultProps = defaultProps;
export default TimePicker;
================================================
FILE: src/components/Timezone/TimezonePicker.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { Typeahead } from 'react-bootstrap-typeahead';
import timeHelper from '../../utils/time';
import ICONS from '../../utils/icons';
import Button from '../Common/Button';
class TimezonePicker extends React.PureComponent {
constructor(props) {
super(props);
this.handleTimezoneChange = this.handleTimezoneChange.bind(this);
}
handleTimezoneChange(selection) {
const { handleTimezoneChange, onClearFocus } = this.props;
const zoneObject = selection[0];
if (zoneObject) {
handleTimezoneChange && handleTimezoneChange(zoneObject);
onClearFocus();
}
}
render() {
const { phrases, onClearFocus } = this.props;
return (
<div className="timezone_picker_modal_container">
<div className="timezone_picker_modal_header">
<span onClick={onClearFocus} className="icon">
{ICONS.chevronLeft}
</span>
<span className="timezone_picker_header_title">
{phrases.timezonePickerTitle}
</span>
</div>
<div className="timezone_picker_container">
<div className="timezone_picker_search">
<Typeahead
onChange={this.handleTimezoneChange}
labelKey={option => `${option.city} - ${option.zoneAbbr}`}
options={timeHelper.tzMaps}
maxResults={5}
minLength={3}
placeholder={phrases.timezonePickerLabel}
/>
</div>
</div>
<div className="buttons_wrapper">
<Button
onClick={onClearFocus}
className="time_picker_button"
>
{phrases.close}
</Button>
</div>
</div>
);
}
}
TimezonePicker.propTypes = {
phrases: PropTypes.object,
onClearFocus: PropTypes.func,
handleTimezoneChange: PropTypes.func
};
TimezonePicker.defaultProps = {
onClearFocus: Function.prototype,
handleTimezoneChange: Function.prototype
};
export default TimezonePicker;
================================================
FILE: src/components/Timezone/index.jsx
================================================
import React from 'react';
import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import timeHelper from '../../utils/time';
import TimezonePicker from './TimezonePicker';
const TIME = timeHelper.time();
TIME.tz = timeHelper.guessUserTz();
class Timezone extends React.PureComponent {
constructor(props) {
super(props);
const { timezone } = this.props;
this.state = {
focused: false,
timezone,
};
this.onClearFocus = this.onClearFocus.bind(this);
this.handleFocusedChange = this.handleFocusedChange.bind(this);
this.handleTimezoneChange = this.handleTimezoneChange.bind(this);
}
onClearFocus() {
this.setState({ focused: false });
}
handleFocusedChange() {
if (!this.props.timezoneIsEditable) return;
const { focused } = this.state;
this.setState({ focused: !focused });
}
handleTimezoneChange(timezone) {
this.setState({ timezone });
const { onTimezoneChange } = this.props;
onTimezoneChange && onTimezoneChange(timezone);
}
render() {
const { focused, timezone } = this.state;
const { phrases, timezoneIsEditable } = this.props;
const footerClass = timezoneIsEditable
? 'time_picker_modal_footer clickable'
: 'time_picker_modal_footer';
const timeZonePicker = () => {
if (!timezoneIsEditable || !focused) return '';
return (
<CSSTransition
classNames="timezone_picker_modal_container"
timeout={{ enter: 200, exit: 200 }}>
<TimezonePicker
key="timezonePicker"
phrases={phrases}
onClearFocus={this.onClearFocus}
handleTimezoneChange={this.handleTimezoneChange}
/>
</CSSTransition>
);
};
return (
<div>
<div className={footerClass} onClick={this.handleFocusedChange}>
<span className="time_picker_modal_footer_timezone">
{timezone.zoneName} {timezone.zoneAbbr}
</span>
</div>
<TransitionGroup>{timeZonePicker()}</TransitionGroup>
</div>
);
}
}
Timezone.propTypes = {
phrases: PropTypes.object,
timezone: PropTypes.shape({
city: PropTypes.string,
zoneAbbr: PropTypes.string,
zoneName: PropTypes.string,
}),
timezoneIsEditable: PropTypes.bool,
onTimezoneChange: PropTypes.func,
};
Timezone.defaultProps = {
timezone: TIME.tz,
timezoneIsEditable: false,
onTimezoneChange: Function.prototype,
};
export default Timezone;
================================================
FILE: src/utils/constant.js
================================================
const getArray = length => new Array(length).join('0').split('');
export const HOURS = getArray(24 + 1);
export const TWELVE_HOURS = getArray(12 + 1);
export const MINUTES = getArray(60 + 1);
const PICKER_WIDTH = 260;
const POINTER_WIDTH = 35;
export const PICKER_RADIUS = PICKER_WIDTH / 2;
export const MAX_ABSOLUTE_POSITION = 125;
export const MIN_ABSOLUTE_POSITION = 90;
export const POINTER_RADIUS = POINTER_WIDTH / 2;
export const BROWSER_COMPATIBLE = [
'',
'O',
'Moz',
'Ms',
'ms',
'Webkit'
];
export const MERIDIEMS = ['AM', 'PM'];
================================================
FILE: src/utils/drag.js
================================================
import { BROWSER_COMPATIBLE } from './constant';
const getScrollPosition = () => {
const position = {
x: document.documentElement.scrollLeft
|| document.body.scrollLeft
|| 0,
y: document.documentElement.scrollTop
|| document.body.scrollTop
|| 0,
};
return position;
};
const mousePosition = (e) => {
const event = e || window.event;
let xPos;
const scrollPosition = getScrollPosition();
if (event.pageX) {
xPos = event.pageX;
} else if ((event.clientX + scrollPosition.x) - document.body.clientLeft) {
xPos = (event.clientX + scrollPosition.x) - document.body.clientLeft;
} else if (event.touches[0]) {
xPos = event.touches[0].clientX;
} else {
xPos = event.changedTouches[0].clientX;
}
let yPos;
if (event.pageY) {
yPos = event.pageY;
} else if ((event.clientY + scrollPosition.y) - document.body.clientTop) {
yPos = (event.clientY + scrollPosition.y) - document.body.clientTop;
} else if (event.touches[0]) {
yPos = event.touches[0].clientY;
} else {
yPos = event.changedTouches[0].clientY;
}
return {
x: xPos,
y: yPos,
};
};
const disableMouseDown = (e) => {
const event = e || window.event;
event.preventDefault();
event.stopPropagation();
};
const browserStyles = (type, style) => BROWSER_COMPATIBLE.reduce((dict, browser) => {
const key = browser
? `${browser}${type[0].toUpperCase()}${type.slice(1)}`
: type;
dict[key] = style;
return dict;
}, {});
const getRotateStyle = degree =>
browserStyles('transform', `rotate(${degree}deg)`);
const getInlineRotateStyle = degree =>
browserStyles('transform', `translateX(-50%) rotate(${degree}deg)`);
const getInitialPointerStyle = (height, top, degree) =>
Object.assign({
height: `${height}px`,
top: `${top}px`,
}, browserStyles('transform', `translateX(-50%) rotate(${degree}deg)`));
const getStandardAbsolutePosition = (position, minPosition, maxPosition) => {
let p = position;
if (p < minPosition) {
p = minPosition;
}
if (p > maxPosition) {
p = maxPosition;
}
return p;
};
const degree2Radian = degree => (degree * (2 * Math.PI)) / 360;
export default {
degree2Radian,
mousePosition,
disableMouseDown,
rotateStyle: getRotateStyle,
inlineRotateStyle: getInlineRotateStyle,
initialPointerStyle: getInitialPointerStyle,
validatePosition: getStandardAbsolutePosition
};
================================================
FILE: src/utils/func.js
================================================
// simple utils for working with sequences like Array or string
const checkType = (val, result) =>
Object.prototype.toString.call(val) === result;
export const is = {
object: val => checkType(val, '[object Object]'),
array: val => Array.isArray(val),
func: val => checkType(val, '[object Function]'),
string: val => checkType(val, '[object String]'),
undefined: val => typeof val === 'undefined',
};
export const isSeq = seq => (is.string(seq) || is.array(seq));
export const head = seq => isSeq(seq) ? seq[0] : null;
export const first = head;
export const tail = seq => isSeq(seq) ? seq.slice(1) : null;
export const rest = tail;
export const last = seq => isSeq(seq) ? seq[seq.length - 1] : null;
================================================
FILE: src/utils/icons.js
================================================
import React from 'react';
const time = (
<svg width="48" height="48" viewBox="0 0 48 48">
<path d="M23.99 4C12.94 4 4 12.95 4 24s8.94 20 19.99 20C35.04 44 44 35.05 44 24S35.04 4 23.99 4zM24 40c-8.84 0-16-7.16-16-16S15.16 8 24 8s16 7.16 16 16-7.16 16-16 16zM25 14h-3v12l10.49 6.3L34 29.84l-9-5.34z" />
</svg>
);
const chevronLeft = (
<svg width="1792" height="1792" viewBox="0 0 1792 1792">
<path d="M1427 301l-531 531 531 531q19 19 19 45t-19 45l-166 166q-19 19-45 19t-45-19l-742-742q-19-19-19-45t19-45l742-742q19-19 45-19t45 19l166 166q19 19 19 45t-19 45z" />
</svg>
);
export default {
time,
chevronLeft
};
================================================
FILE: src/utils/language.js
================================================
const LANGUAGES = {
en: {
confirm: 'confirm',
cancel: 'cancel',
close: 'close',
timezonePickerTitle: 'Pick a timezone',
timezonePickerLabel: 'Closest city or timezone',
am: 'AM',
pm: 'PM'
},
'zh-cn': {
confirm: '确认',
cancel: '取消',
close: '关闭',
timezonePickerTitle: '选择时区',
timezonePickerLabel: '最近的城市或时区',
am: '上午',
pm: '下午'
},
'zh-tw': {
confirm: '確認',
cancel: '取消',
close: '關閉',
timezonePickerTitle: '選擇時區',
timezonePickerLabel: '最近的城市或時區',
am: '上午',
pm: '下午'
},
fr: {
confirm: 'Confirmer',
cancel: 'Annulé',
close: 'Arrêter',
timezonePickerTitle: 'Choisissez un timezone',
timezonePickerLabel: 'Ville la plus proche ou timezone',
am: 'AM',
pm: 'PM'
},
ja: {
confirm: '確認します',
cancel: 'キャンセル',
close: 'クローズ',
timezonePickerTitle: 'タイムゾーンを選択する',
timezonePickerLabel: '最も近い都市またはTimezone',
am: 'AM',
pm: 'PM'
}
};
const language = (type = 'en') => LANGUAGES[type];
export default {
get: language
};
================================================
FILE: src/utils/time.js
================================================
import moment from 'moment-timezone';
import { head, last, is } from './func';
// loads moment-timezone's timezone data, which comes from the
// IANA Time Zone Database at https://www.iana.org/time-zones
moment.tz.load({
zones: [],
links: [],
version: 'latest',
});
const guessUserTz = () => {
// User-Agent sniffing is not always reliable, but is the recommended technique
// for determining whether or not we're on a mobile device according to MDN
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#Mobile_Tablet_or_Desktop
const isMobile = global.navigator !== undefined
? global.navigator.userAgent.match(/Mobi/)
: false;
const supportsIntl = global.Intl !== undefined;
let userTz;
if (isMobile && supportsIntl) {
// moment-timezone gives preference to the Intl API regardless of device type,
// so unset global.Intl to trick moment-timezone into using its fallback
// see https://github.com/moment/moment-timezone/issues/441
// TODO: Clean this up when that issue is resolved
const globalIntl = global.Intl;
global.Intl = undefined;
userTz = moment.tz.guess();
global.Intl = globalIntl;
} else {
userTz = moment.tz.guess();
}
// return GMT if we're unable to guess or the system is using UTC
if (!userTz || userTz === 'UTC') return getTzForName('Etc/Greenwich');
try {
return getTzForName(userTz);
} catch (e) {
console.error(e);
return getTzForName('Etc/Greenwich');
}
};
/**
* Create a time data object using moment.
* If a time is provided, just format it; if not, use the current time.
*
* @function getValidTimeData
* @param {string} time a time; defaults to now
* @param {string} meridiem AM or PM; defaults to AM via moment
* @param {Number} timeMode 12 or 24-hour mode
* @param {string} tz a timezone name; defaults to guessing a user's tz or GMT
* @return {Object} a key-value representation of time data
*/
const getValidTimeData = (options = {}) => {
const {
tz,
time,
timeMode,
useTz = true,
meridiem = null,
} = options;
const validMeridiem = getValidMeridiem(meridiem);
// when we only have a valid meridiem, that implies a 12h mode
const mode = (validMeridiem && !timeMode) ? 12 : timeMode || 24;
const timezone = tz || guessUserTz().zoneName;
const validMode = getValidateTimeMode(mode);
const validTime = getValidTimeString(time, validMeridiem);
const format12 = 'hh:mmA';
const format24 = 'HH:mmA';
// What format is the hour we provide to moment below in?
const hourFormat = (validMode === 12) ? format12 : format24;
let time24;
let time12;
const formatTime = moment(`1970-01-01 ${validTime}`, `YYYY-MM-DD ${hourFormat}`, 'en');
if (time || !useTz) {
time24 = ((validTime)
? formatTime.format(format24)
: moment().format(format24)).split(/:/);
time12 = ((validTime)
? formatTime.format(format12)
: moment().format(format12)).split(/:/);
} else {
time24 = ((validTime)
? formatTime.tz(timezone).format(format24)
: moment().tz(timezone).format(format24)).split(/:/);
time12 = ((validTime)
? formatTime.tz(timezone).format(format12)
: moment().tz(timezone).format(format12)).split(/:/);
}
const timeData = {
timezone,
mode: validMode,
hour24: head(time24),
minute: last(time24).slice(0, 2),
hour12: head(time12).replace(/^0/, ''),
meridiem: validMode === 12 ? last(time12).slice(2) : null,
};
return timeData;
};
/**
* Format the current time as a string
* @function getCurrentTime
* @return {string}
*/
const getCurrentTime = () => {
const time = getValidTimeData();
return `${time.hour24}:${time.minute}`;
};
/**
* Get an integer representation of a time.
* @function getValidateIntTime
* @param {string} time
* @return {Number}
*/
const getValidateIntTime = (time) => {
if (isNaN(parseInt(time, 10))) { return 0; }
return parseInt(time, 10);
};
/**
* Validate, set a default for, and stringify time data.
* @function getValidateTime
* @param {string}
* @return {string}
*/
const getValidateTime = (time) => {
let result = time;
if (is.undefined(result)) { result = '00'; }
if (isNaN(parseInt(result, 10))) { result = '00'; }
if (parseInt(result, 10) < 10) { result = `0${parseInt(result, 10)}`; }
return `${result}`;
};
/**
* Given a time and meridiem, produce a time string to pass to moment
* @function getValidTimeString
* @param {string} time
* @param {string} meridiem
* @return {string}
*/
const getValidTimeString = (time, meridiem) => {
if (is.string(time)) {
let validTime = (time && time.indexOf(':').length >= 0)
? time.split(/:/).map(t => getValidateTime(t)).join(':')
: time;
const hourAsInt = parseInt(head(validTime.split(/:/)), 10);
const is12hTime = (hourAsInt > 0 && hourAsInt <= 12);
validTime = (validTime && meridiem && is12hTime)
? `${validTime} ${meridiem}`
: validTime;
return validTime;
}
return time;
};
/**
* Given a meridiem, try to ensure that it's formatted for use with moment
* @function getValidMeridiem
* @param {string} meridiem
* @return {string}
*/
const getValidMeridiem = (meridiem) => {
if (is.string(meridiem)) {
return (meridiem && meridiem.match(/am|pm/i)) ? meridiem.toLowerCase() : null;
}
return meridiem;
};
/**
* Ensure that a meridiem passed as a prop has a valid value
* @function getValidateMeridiem
* @param {string} time
* @param {string|Number} timeMode
* @return {string|null}
*/
const getValidateMeridiem = (time, timeMode) => {
const validateTime = time || getCurrentTime();
const mode = parseInt(timeMode, 10);
// eslint-disable-next-line no-unused-vars
let hour = validateTime.split(/:/)[0];
hour = getValidateIntTime(hour);
if (mode === 12) return (hour > 12) ? 'PM' : 'AM';
return null;
};
/**
* Validate and set a sensible default for time modes.
*
* @function getValidateTimeMode
* @param {string|Number} timeMode
* @return {Number}
*/
const getValidateTimeMode = (timeMode) => {
const mode = parseInt(timeMode, 10);
if (isNaN(mode)) { return 24; }
if (mode !== 24 && mode !== 12) { return 24; }
return mode;
};
const tzNames = (() => {
// We want to subset the existing timezone data as much as possible, both for efficiency
// and to avoid confusing the user. Here, we focus on removing reduntant timezone names
// and timezone names for timezones we don't necessarily care about, like Antarctica, and
// special timezone names that exist for convenience.
const scrubbedPrefixes = ['Antarctica', 'Arctic', 'Chile'];
const scrubbedSuffixes = ['ACT', 'East', 'Knox_IN', 'LHI', 'North', 'NSW', 'South', 'West'];
const tznames = moment.tz.names()
.filter(name => name.indexOf('/') >= 0)
.filter(name => !scrubbedPrefixes.indexOf(name.split('/')[0]) >= 0)
.filter(name => !scrubbedSuffixes.indexOf(name.split('/').slice(-1)[0]) >= 0);
return tznames;
})();
// We need a human-friendly city name for each timezone identifier
// counting Canada/*, Mexico/*, and US/* allows users to search for
// things like 'Eastern' or 'Mountain' and get matches back
const tzCities = tzNames
.map(name => (['Canada', 'Mexico', 'US'].indexOf(name.split('/')[0]) >= 0)
? name : name.split('/').slice(-1)[0])
.map(name => name.replace(/_/g, ' '));
// Provide a mapping between a human-friendly city name and its corresponding
// timezone identifier and timezone abbreviation as a named export.
// We can fuzzy match on any of these.
const tzMaps = tzCities.map((city) => {
const tzMap = {};
const tzName = tzNames[tzCities.indexOf(city)];
tzMap.city = city;
tzMap.zoneName = tzName;
tzMap.zoneAbbr = moment().tz(tzName).zoneAbbr();
return tzMap;
});
const getTzForCity = (city) => {
const val = city.toLowerCase();
const maps = tzMaps.filter(tzMap => tzMap.city.toLowerCase() === val);
return head(maps);
};
const getTzCountryAndCity = (name) => {
const sections = name.split('/');
return {
country: sections[0].toLowerCase(),
city: sections.slice(-1)[0].toLowerCase()
};
};
const _matchTzByName = (target, name) => {
const v1 = getTzCountryAndCity(target);
const v2 = getTzCountryAndCity(name);
return v1.country === v2.country && v1.city === v2.city;
};
const getTzForName = (name) => {
let maps = tzMaps.filter(tzMap => tzMap.zoneName === name);
if (!maps.length && /\//.test(name)) {
maps = tzMaps.filter(tzMap => tzMap.zoneAbbr === name);
}
if (!maps.length) {
maps = tzMaps.filter(tzMap => _matchTzByName(tzMap.zoneName, name));
}
if (!maps.length) {
throw new Error(`Can not find target timezone for ${name}`);
}
return head(maps);
};
const hourFormatter = (hour, defaultTime = '00:00') => {
if (!hour) return defaultTime;
let [h, m, meridiem] = `${hour}`.split(/[:|\s]/);
if (meridiem && meridiem.toLowerCase() === 'pm') meridiem = 'PM';
if (meridiem && meridiem.toLowerCase() === 'am') meridiem = 'AM';
if (meridiem && meridiem !== 'AM' && meridiem !== 'PM') meridiem = 'AM';
if (!h || isNaN(h)) h = '0';
if (!meridiem && Number(h > 24)) h = Number(h) - 24;
if (meridiem && Number(h > 12)) h = Number(h) - 12;
if (!m || isNaN(m) || Number(m) >= 60) m = '0';
if (Number(h) < 10) h = `0${Number(h)}`;
if (Number(m) < 10) m = `0${Number(m)}`;
return meridiem ? `${h}:${m} ${meridiem}` : `${h}:${m}`;
};
const withoutMeridiem = hour => hour.replace(/\s[P|A]M$/, '');
const getStartAndEnd = (from, to) => {
const current = moment();
const date = current.format('YYYY-MM-DD');
const nextDate = current.add(1, 'day').format('YYYY-MM-DD');
const f = hourFormatter(from, '00:00');
const t = hourFormatter(to, '23:30');
let start = `${date} ${withoutMeridiem(f)}`;
const endTmp = withoutMeridiem(t);
let end = moment(`${date} ${endTmp}`) <= moment(start)
? `${nextDate} ${endTmp}`
: `${date} ${endTmp}`;
if (/PM$/.test(f)) start = moment(start).add(12, 'hours').format('YYYY-MM-DD HH:mm');
if (/PM$/.test(t)) end = moment(end).add(12, 'hours').format('YYYY-MM-DD HH:mm');
return {
start,
end
};
};
const get12ModeTimes = ({ from, to, step = 30, unit = 'minutes' }) => {
const {
start,
end
} = getStartAndEnd(from, to);
const times = [];
let time = moment(start);
while (time <= moment(end)) {
const hour = Number(time.format('HH'));
times.push(`${time.format('hh:mm')} ${hour >= 12 ? 'PM' : 'AM'}`);
time = time.add(step, unit);
}
return times;
};
const get24ModeTimes = ({ from, to, step = 30, unit = 'minutes' }) => {
const {
start,
end
} = getStartAndEnd(from, to);
const times = [];
let time = moment(start);
while (time <= moment(end)) {
times.push(time.format('HH:mm'));
time = time.add(step, unit);
}
return times;
};
export default {
tzMaps,
guessUserTz,
hourFormatter,
getStartAndEnd,
get12ModeTimes,
get24ModeTimes,
withoutMeridiem,
time: getValidTimeData,
current: getCurrentTime,
tzForCity: getTzForCity,
tzForName: getTzForName,
validate: getValidateTime,
validateInt: getValidateIntTime,
validateMeridiem: getValidateMeridiem,
validateTimeMode: getValidateTimeMode,
};
================================================
FILE: stories/ClassicThemePicker.js
================================================
import '../css/classic/default.css';
import React from 'react';
import TimePickerWrapper from '../examples/TimePickerWrapper';
import { storiesOf } from '@storybook/react';
storiesOf('Classic Theme', module)
.addWithInfo('basic', () => (
<TimePickerWrapper theme="classic" />
))
.addWithInfo('with default time', () => (
<TimePickerWrapper theme="classic" defaultTime="17:00" />
))
.addWithInfo('dropdown focus on time/default time', () => (
<React.Fragment>
<TimePickerWrapper
theme="classic"
defaultTime="17:00"
focusDropdownOnTime
/>
</React.Fragment>
))
.addWithInfo('dark color', () => (
<TimePickerWrapper theme="classic" colorPalette="dark" />
))
.addWithInfo('12 hours mode', () => (
<TimePickerWrapper
theme="classic"
timeMode="12"
defaultTime="10:30"
/>
))
.addWithInfo('limit start, end, step for 12 hours mode', () => (
<TimePickerWrapper
theme="classic"
timeMode="12"
timeConfig={{
from: '08:00 PM',
to: '08:00 AM',
step: 1,
unit: 'hour'
}}
/>
))
.addWithInfo('limit start, end, step for 24 hours mode', () => (
<TimePickerWrapper
theme="classic"
timeMode="24"
timeConfig={{
from: 9,
to: 19,
step: 30,
unit: 'minutes'
}}
/>
))
.addWithInfo('focused at setup', () => (
<TimePickerWrapper
focused
theme="classic"
/>
))
.addWithInfo('Set default time', () => (
<TimePickerWrapper
theme="classic"
defaultTime="12:00"
/>
))
.addWithInfo('Focus dropdown on time', () => (
<TimePickerWrapper
focusDropdownOnTime
theme="classic"
defaultTime="12:00"
/>
));
================================================
FILE: stories/CustomTrigger.js
================================================
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
import TimePickerWrapper from '../examples/TimePickerWrapper';
import '../css/material/default.css';
storiesOf('Custom TimePicker Trigger', module)
.addDecorator(withKnobs)
.addWithInfo('basic example', () => (
<TimePickerWrapper
customTriggerId={1}
/>
))
.addWithInfo('any custom DOM', () => (
<TimePickerWrapper
customTriggerId={2}
/>
))
.addWithInfo('only render picker modal', () => (
<TimePickerWrapper
focused
autoClose={false}
trigger={<div />}
closeOnOutsideClick={false}
/>
));
================================================
FILE: stories/DarkColor.js
================================================
import '../css/material/default.css';
import React from 'react';
import TimePickerWrapper from '../examples/TimePickerWrapper';
import { storiesOf } from '@storybook/react';
storiesOf('DarkColor', module)
.addWithInfo('basic', () => (
<TimePickerWrapper colorPalette="dark" />
))
.addWithInfo('with default time', () => (
<TimePickerWrapper
colorPalette="dark"
defaultTime="11:50"
/>
))
.addWithInfo('focused at setup', () => (
<TimePickerWrapper
colorPalette="dark"
focused
/>
))
.addWithInfo('without icon', () => (
<TimePickerWrapper
focused
withoutIcon
colorPalette="dark"
/>
));
================================================
FILE: stories/DifferentLanguage.js
================================================
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs, text } from '@storybook/addon-knobs';
import TimePickerWrapper from '../examples/TimePickerWrapper';
import '../css/material/default.css';
storiesOf('Different Languages', module)
.addDecorator(withKnobs)
.addWithInfo('English (basic)', () => (
<TimePickerWrapper timeMode="12" />
))
.addWithInfo('汉语 - 简体', () => (
<TimePickerWrapper
timeMode="12"
language="zh-cn"
/>
))
.addWithInfo('汉语 - 繁体', () => (
<TimePickerWrapper
timeMode="12"
language="zh-tw"
/>
))
.addWithInfo('Français', () => (
<TimePickerWrapper
timeMode="12"
language="fr"
/>
))
.addWithInfo('日本語', () => (
<TimePickerWrapper
timeMode="12"
language="ja"
/>
))
.addWithInfo('custom phrases', () => {
const confirm = text('confirm', 'okey dokey');
const cancel = text('cancel', 'hold it there!');
const close = text('close', 'DONE');
const am = text('am', 'Ante');
const pm = text('pm', 'Post');
return (
<TimePickerWrapper
timeMode="12"
language="en"
phrases={{
confirm,
cancel,
close,
am,
pm
}}
/>
);
});
================================================
FILE: stories/TimePicker.js
================================================
import React from 'react';
import { storiesOf } from '@storybook/react';
import '../css/material/default.css';
import { text, withKnobs } from '@storybook/addon-knobs';
import TimePickerWrapper from '../examples/TimePickerWrapper';
storiesOf('Default TimePicker', module)
.addDecorator(withKnobs)
.addWithInfo('basic', () => (
<TimePickerWrapper />
))
.addWithInfo('disabled', () => (
<TimePickerWrapper disabled />
))
.addWithInfo('with default time', () => {
const aDefaultTime = text('set default time', '13:20');
return (
<TimePickerWrapper
defaultTime={aDefaultTime}
/>
);
})
.addWithInfo('focused at setup', () => (
<TimePickerWrapper
focused
/>
))
.addWithInfo('not auto change time panel', () => (
<TimePickerWrapper
autoMode={false}
/>
))
.addWithInfo('undraggable', () => (
<TimePickerWrapper
draggable={false}
/>
))
.addWithInfo('disable outside click close', () => (
<TimePickerWrapper
closeOnOutsideClick={false}
/>
))
.addWithInfo('custom minute step', () => (
<TimePickerWrapper
autoMode={false}
minuteStep={5}
defaultTime={'22:10'}
/>
))
.addWithInfo('limit drag', () => (
<TimePickerWrapper
limitDrag
autoMode={false}
minuteStep={1}
/>
))
.addWithInfo('custom HH-MM format', () => (
<TimePickerWrapper
timeFormat={'HH-MM'}
/>
))
.addWithInfo('custom H-M format', () => (
<TimePickerWrapper
timeFormat={'H-M'}
/>
))
.addWithInfo('custom time formatter', () => (
<TimePickerWrapper
timeFormatter={({ hour, minute }) => `${hour} & ${minute}`}
/>
));
================================================
FILE: stories/TimePicker2.js
================================================
import React from 'react';
import { storiesOf } from '@storybook/react';
import '../css/material/default.css';
import { text, withKnobs } from '@storybook/addon-knobs';
import TimePickerWrapper2 from '../examples/TimePickerWrapper2';
storiesOf('Multi TimePicker', module)
.addDecorator(withKnobs)
.addWithInfo('basic', () => (
<TimePickerWrapper2 />
));
================================================
FILE: stories/TwelveHoursMode.js
================================================
import React from 'react';
import TimePickerWrapper from '../examples/TimePickerWrapper';
import { storiesOf } from '@storybook/react';
import '../css/material/default.css';
storiesOf('TwelveHoursMode', module)
.addWithInfo('basic', () => (
<TimePickerWrapper
timeMode="12"
/>
))
.addWithInfo('with default time', () => (
<TimePickerWrapper
timeMode="12"
defaultTime="13:15"
/>
))
.addWithInfo('focused at setup, no icon', () => (
<TimePickerWrapper
withoutIcon
timeMode="12"
focused
/>
))
.addWithInfo('custom minute step', () => (
<TimePickerWrapper
autoMode={false}
minuteStep={1}
timeMode="12"
/>
))
.addWithInfo('limit drag', () => (
<TimePickerWrapper
limitDrag
autoMode={false}
minuteStep={1}
timeMode="12"
/>
))
.addWithInfo('disable outside click close', () => (
<TimePickerWrapper
timeMode="12"
closeOnOutsideClick={false}
/>
));
================================================
FILE: stories/WithTimeZones.js
================================================
import '../css/material/default.css';
import { withKnobs } from '@storybook/addon-knobs';
import React from 'react';
import TimePickerWrapper from '../examples/TimePickerWrapper';
import TimeZonesPickerWrapper from '../examples/TimeZonesPickerWrapper';
import { storiesOf } from '@storybook/react';
import timeHelper from '../src/utils/time.js';
const tzForCity = timeHelper.tzForCity('Kuala Lumpur');
storiesOf('TimeZones', module)
.addDecorator(withKnobs)
.addWithInfo('with default (detected) timezone', () => (
<TimePickerWrapper showTimezone />
))
.addWithInfo('with default (custom) timezone', () => (
<TimePickerWrapper timezone={tzForCity.zoneName} showTimezone />
))
.addWithInfo('with timezone search', () => (
<TimePickerWrapper showTimezone timezoneIsEditable />
))
.addWithInfo('with 12 hour (custom) time', () => (
<TimePickerWrapper
timeMode="12"
defaultTime="13:15"
showTimezone
timezoneIsEditable
/>
))
.addWithInfo('with dark theme', () => (
<TimePickerWrapper colorPalette="dark" showTimezone timezoneIsEditable />
))
.addWithInfo('timezone picker', () => <TimeZonesPickerWrapper />);
================================================
FILE: test/_helpers/adapter.js
================================================
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
================================================
FILE: test/_helpers/ignoreSVGStrings.jsx
================================================
require.extensions['.svg'] = (obj) => {
obj.exports = () => (
<svg>SVG_TEST_STUB</svg>
);
};
================================================
FILE: test/components/ClassicTheme_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import '../_helpers/adapter';
import ClassicTheme from '../../src/components/ClassicTheme';
describe('ClassicTheme', () => {
describe('ClassicTheme render', () => {
it('should render correctly', () => {
const wrapper = shallow(<ClassicTheme />);
expect(wrapper.is('.classic_theme_container')).to.equal(true);
});
});
});
================================================
FILE: test/components/MaterialTheme_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import MaterialTheme from '../../src/components/MaterialTheme';
import languageHelper from '../../src/utils/language';
import '../_helpers/adapter';
const phrases = languageHelper.get('en');
describe('MaterialTheme', () => {
describe('MaterialTheme Timezone render', () => {
it('should render the Timezone footer', () => {
const mockTimezone = {
zoneName: 'Some Zone',
zoneAbbr: 'SZ'
};
const wrapper = shallow(
<MaterialTheme
showTimezone
phrases={phrases}
timezone={mockTimezone}
/>
);
expect(wrapper.find('Timezone')).to.have.lengthOf(1);
});
it('should not render the Timezone footer', () => {
const wrapper = shallow(<MaterialTheme />);
expect(wrapper.find('Timezone')).to.have.lengthOf(0);
});
});
describe('MaterialTheme render correctly', () => {
it('should render with className', () => {
const wrapper = shallow(<MaterialTheme />);
expect(wrapper.is('.modal_container')).to.equal(true);
expect(wrapper.is('.time_picker_modal_container')).to.equal(true);
});
});
});
================================================
FILE: test/components/PickerDargHandler_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { mount } from 'enzyme';
import PickerDragHandler from '../../src/components/Picker/PickerDragHandler';
import { JSDOM } from 'jsdom';
import '../_helpers/adapter';
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;
function copyProps(src, target) {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === 'undefined')
.reduce((result, prop) => ({
...result,
[prop]: Object.getOwnPropertyDescriptor(src, prop),
}), {});
Object.defineProperties(target, props);
}
global.window = window;
global.document = window.document;
global.navigator = {
userAgent: 'node.js',
};
copyProps(window, global);
describe('PickerDragHandler', () => {
describe('PickerDragHandler Init', () => {
const wrapper = mount(<PickerDragHandler />);
it('should render component correctly', () => {
expect(wrapper.find('.picker_handler').length).to.equal(1);
});
it('should render correct draging state', () => {
expect(wrapper.state().draging).to.equal(false);
});
});
});
================================================
FILE: test/components/PickerPointGenerator_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import PickerPoint from '../../src/components/Picker/PickerPoint';
import pickerPointGenerator from '../../src/components/Picker/PickerPointGenerator';
import '../_helpers/adapter';
describe('PickerPointGenerator', () => {
describe('Render 24 hours', () => {
const PickerPointGenerator = pickerPointGenerator('hour');
const wrapper = shallow(<PickerPointGenerator handleTimePointerClick={Function.prototype}/>);
it('should render with currect wrapper', () => {
expect(wrapper.is('.picker_pointer_container')).to.equal(true);
});
it('should render with 24 PickerPoint', () => {
expect(wrapper.find(PickerPoint)).to.have.lengthOf(24);
});
});
describe('Render 12 hours', () => {
const PickerPointGenerator = pickerPointGenerator('hour', 12);
const wrapper = shallow(<PickerPointGenerator handleTimePointerClick={Function.prototype}/>);
it('should render with currect wrapper', () => {
expect(wrapper.is('.picker_pointer_container')).to.equal(true);
});
it('should render with 12 PickerPoint', () => {
expect(wrapper.find(PickerPoint)).to.have.lengthOf(12);
});
});
describe('Render minutes', () => {
const PickerPointGenerator = pickerPointGenerator('minute');
const wrapper = shallow(<PickerPointGenerator handleTimePointerClick={Function.prototype}/>);
it('should render with 12 PickerPoint', () => {
expect(wrapper.find(PickerPoint)).to.have.lengthOf(12);
});
});
});
================================================
FILE: test/components/PickerPoint_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import PickerPoint from '../../src/components/Picker/PickerPoint';
import '../_helpers/adapter';
describe('PickerPoint', () => {
const wrapper = shallow(
<PickerPoint
index={12}
angle={360}
/>
);
it('should render with currect wrapper', () => {
expect(wrapper.is('.picker_point.point_outter')).to.equal(true);
});
});
================================================
FILE: test/components/TimePicker_func_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon-sandbox';
import languageHelper from '../../src/utils/language';
import TimePicker from '../../src/components/TimePicker';
import '../_helpers/adapter';
describe('TimePicker func', () => {
describe('handle focus change func', () => {
it('should focus', () => {
const wrapper = shallow(<TimePicker />);
wrapper.instance().onFocus();
expect(wrapper.state().focused).to.equal(true);
});
it('should clear focus', () => {
const wrapper = shallow(<TimePicker />);
wrapper.instance().onBlur();
expect(wrapper.state().focused).to.equal(false);
});
it('should callback when focus', () => {
const onFocusChangeStub = sinon.stub();
const wrapper = shallow(<TimePicker onFocusChange={onFocusChangeStub} />);
wrapper.instance().onFocus();
expect(onFocusChangeStub.callCount).to.equal(1);
});
});
describe('handle hour change func', () => {
// it('should change hour', () => {
// const wrapper = shallow(<TimePicker />);
// wrapper.instance().handleHourChange(11);
// expect(wrapper.props().time.split(':')[0]).to.equal('11');
// });
//
// it('should change to validate hour', () => {
// const wrapper = shallow(<TimePicker />);
// wrapper.instance().handleHourChange(1);
// expect(wrapper.props().time.split(':')[1]).to.equal('01');
// });
it('should change callback when hour change', () => {
const onTimeChangeStub = sinon.stub();
const wrapper = shallow(<TimePicker onTimeChange={onTimeChangeStub} />);
wrapper.instance().handleHourChange(1);
expect(onTimeChangeStub.callCount).to.equal(1);
});
});
describe('handle minute change func', () => {
// it('should change minute', () => {
// const wrapper = shallow(<TimePicker />);
// wrapper.instance().handleMinuteChange(59);
// expect(wrapper.state().minute).to.equal('59');
// });
//
// it('should change to validate minute', () => {
// const wrapper = shallow(<TimePicker />);
// wrapper.instance().handleMinuteChange(9);
// expect(wrapper.state().minute).to.equal('09');
// });
it('should change callback when minute change', () => {
const onTimeChangeStub = sinon.stub();
const wrapper = shallow(<TimePicker onTimeChange={onTimeChangeStub} />);
wrapper.instance().handleMinuteChange(1);
expect(onTimeChangeStub.callCount).to.equal(1);
});
});
describe('languageData func', () => {
it('should return the default language messages when no phrases provided', () => {
const wrapper = shallow(<TimePicker />);
const messages = wrapper.instance().languageData;
expect(messages).to.deep.equal(languageHelper.get('en'));
});
it('should return the phrases when all phrases provided', () => {
const phrases = {
confirm: 'foo',
cancel: 'bar',
close: 'baz',
timezonePickerLabel: 'This is a Label',
timezonePickerTitle: 'This is a Title',
am: 'fizz',
pm: 'buzz'
};
const wrapper = shallow(<TimePicker phrases={phrases} />);
const messages = wrapper.instance().languageData;
expect(messages).to.deep.equal(phrases);
});
it('should return the default language messages for any phrases not provided', () => {
const phrases = {
cancel: 'bar',
close: 'baz'
};
const expectedMessages = Object.assign({}, languageHelper.get('en'), phrases);
const wrapper = shallow(<TimePicker phrases={phrases} />);
const messages = wrapper.instance().languageData;
expect(messages).to.deep.equal(expectedMessages);
});
});
});
================================================
FILE: test/components/TimePicker_init_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
// import ClassicTheme from '../../src/components/ClassicTheme';
// import MaterialTheme from '../../src/components/MaterialTheme';
import OutsideClickHandler from '../../src/components/OutsideClickHandler';
import PickerDragHandler from '../../src/components/Picker/PickerDragHandler';
import TimePicker from '../../src/components/TimePicker';
import timeHelper from '../../src/utils/time';
import '../_helpers/adapter';
describe('TimePicker initial', () => {
describe('render basic picker', () => {
it('should be wrappered by div.time_picker_container', () => {
const wrapper = shallow(<TimePicker />);
expect(wrapper.is('.time_picker_container')).to.equal(true);
});
it('renders an OutsideClickHandler', () => {
const wrapper = shallow(<TimePicker />);
expect(wrapper.find(OutsideClickHandler)).to.have.lengthOf(1);
});
// it('renders an MaterialTheme', () => {
// const wrapper = shallow(<TimePicker />);
// expect(wrapper.contains(MaterialTheme)).to.have.lengthOf(1);
// });
// it('renders an ClassicTheme', () => {
// const wrapper = shallow(<TimePicker theme="classic" />);
// expect(wrapper.contains(ClassicTheme)).to.have.lengthOf(1);
// });
it('renders an PickerDragHandler', () => {
const wrapper = shallow(<TimePicker />);
expect(wrapper.find(PickerDragHandler)).to.have.lengthOf(0);
});
});
describe('render with props', () => {
it('should be wrapped by div.time_picker_container.dark', () => {
const wrapper = shallow(<TimePicker colorPalette="dark" />);
expect(wrapper.is('.time_picker_container.dark')).to.equal(true);
});
it('should render with focused', () => {
const wrapper = shallow(<TimePicker focused />);
expect(wrapper.find('.time_picker_preview.active')).to.have.lengthOf(1);
expect(wrapper.find(OutsideClickHandler).props().closeOnOutsideClick).to.equal(true);
});
it('should render disabled component', () => {
const wrapper = shallow(<TimePicker disabled />);
expect(wrapper.find('.time_picker_preview.disabled')).to.have.lengthOf(1);
expect(wrapper.find(OutsideClickHandler).props().closeOnOutsideClick).to.equal(false);
});
it('should render with focused on child', () => {
const wrapper = shallow(<TimePicker focused />);
expect(wrapper.find(OutsideClickHandler).props().focused).to.equal(true);
});
it('should render with no onOutsideClick handler', () => {
const wrapper = shallow(<TimePicker focused closeOnOutsideClick={false} />);
expect(wrapper.find(OutsideClickHandler).props().focused).to.equal(true);
wrapper.find(OutsideClickHandler).simulate('click');
expect(wrapper.find(OutsideClickHandler).props().focused).to.equal(true);
});
it('should render without icon', () => {
const wrapper = shallow(<TimePicker withoutIcon />);
expect(wrapper.find('.preview_container.without_icon')).to.have.lengthOf(1);
});
// it('should render with default time in child props', () => {
// const wrapper = mount(<TimePicker time="22:23" />);
// const time = timeHelper.time({ time: '22:23' });
// console.log(wrapper.find(MaterialTheme));
// expect(wrapper.find(MaterialTheme).props().hour).to.equal(time.hour24);
// expect(wrapper.find(MaterialTheme).props().minute).to.equal(time.minute);
// });
it('should render with default time in DOM', () => {
const wrapper = shallow(<TimePicker time="22:23" withoutIcon />);
const time = timeHelper.time({ time: '22:23' });
expect(wrapper.find('.preview_container').text()).to.equal(`${time.hour24} : ${time.minute}`);
});
// it('should render with current time in child props', () => {
// const wrapper = shallow(<TimePicker />);
// const time = timeHelper.time({
// time: timeHelper.current()
// });
// expect(wrapper.find('#MaterialTheme').props().hour).to.equal(time.hour24);
// expect(wrapper.find('#MaterialTheme').props().minute).to.equal(time.minute);
// });
it('should render with current time in DOM', () => {
const wrapper = shallow(<TimePicker withoutIcon />);
const time = timeHelper.time({
time: timeHelper.current()
});
expect(wrapper.find('.preview_container').text()).to.equal(`${time.hour24} : ${time.minute}`);
});
it('should render with current time format HH&MM', () => {
const wrapper = shallow(<TimePicker time="22:23" timeFormat="HH&MM" />);
const time = timeHelper.time({ time: '22:23' });
expect(wrapper.find('.preview_container').text()).to.equal(`${time.hour24}&23`);
});
it('should render with current time format hh&mm', () => {
const wrapper = shallow(<TimePicker time="12:23" timeFormat="hh&mm" timeMode={12} />);
const time = timeHelper.time({ time: '12:23' });
expect(wrapper.find('.preview_container').text()).to.equal('00&23');
});
});
});
================================================
FILE: test/components/Time_zone_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import Timezone from '../../src/components/Timezone';
import languageHelper from '../../src/utils/language';
import '../_helpers/adapter';
const phrases = languageHelper.get('en');
const mockTimezone = {
zoneName: 'Some Zone',
zoneAbbr: 'SZ'
};
describe('Timezone', () => {
describe('Timezone render', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezone={mockTimezone}
/>
);
it('should render the Timezone footer', () => {
expect(wrapper.find('.time_picker_modal_footer_timezone')).to.have.lengthOf(1);
});
it('should render the Timezone Name and Abbreviation', () => {
expect(wrapper.find('.time_picker_modal_footer_timezone').text())
.to.equal(`${mockTimezone.zoneName} ${mockTimezone.zoneAbbr}`);
});
});
describe('props', () => {
describe('when timezoneIsEditable is true', () => {
it('should render the Time Picker modal footer clickable', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezoneIsEditable
/>
);
expect(wrapper.find('.time_picker_modal_footer').hasClass('clickable')).to.equal(true);
});
describe('when focused is true', () => {
it('should render the TimezonePicker', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezoneIsEditable
/>
);
wrapper.setState({ focused: true });
expect(wrapper.find('TimezonePicker')).to.have.lengthOf(1);
});
});
describe('when focused is false', () => {
it('should not render the TimezonePicker', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezoneIsEditable
/>
);
wrapper.setState({ focused: false });
expect(wrapper.find('TimezonePicker')).to.have.lengthOf(0);
});
});
});
describe('when timezoneIsEditable is false', () => {
it('should not render the Time Picker modal footer clickable', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezoneIsEditable={false}
/>
);
expect(wrapper.find('.time_picker_modal_footer').hasClass('clickable')).to.equal(false);
});
describe('when focused is true', () => {
it('should not render the TimezonePicker', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezoneIsEditable={false}
/>
);
wrapper.setState({ focused: true });
expect(wrapper.find('TimezonePicker')).to.have.lengthOf(0);
});
});
describe('when focused is false', () => {
it('should not render the TimezonePicker', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezoneIsEditable={false}
/>
);
wrapper.setState({ focused: false });
expect(wrapper.find('TimezonePicker')).to.have.lengthOf(0);
});
});
});
});
describe('onClearFocus Func', () => {
it('should clear focused', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
/>
);
wrapper.setState({ focused: true });
wrapper.instance().onClearFocus();
expect(wrapper.state().focused).to.equal(false);
});
});
describe('handleFocusedChange Func', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezoneIsEditable
/>
);
it('should toggle focused', () => {
wrapper.setState({ focused: true });
wrapper.instance().handleFocusedChange();
expect(wrapper.state().focused).to.equal(false);
wrapper.instance().handleFocusedChange();
expect(wrapper.state().focused).to.equal(true);
});
it('should toggle focused onClick of modal footer', () => {
wrapper.setState({ focused: false });
wrapper.find('.time_picker_modal_footer').simulate('click');
expect(wrapper.state().focused).to.equal(true);
});
});
describe('handleTimezoneChange Func', () => {
it('should set the timezone', () => {
const wrapper = shallow(
<Timezone
phrases={phrases}
timezone={{}}
/>
);
wrapper.instance().handleTimezoneChange(mockTimezone);
expect(wrapper.state().timezone).to.equal(mockTimezone);
});
});
});
================================================
FILE: test/components/Timezone_Picker_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon-sandbox';
import TimezonePicker from '../../src/components/Timezone/TimezonePicker';
import languageHelper from '../../src/utils/language';
import '../_helpers/adapter';
const phrases = languageHelper.get('en');
const mockTimezone = {
zoneName: 'Some Zone',
zoneAbbr: 'SZ'
};
describe('TimezonePicker', () => {
describe('TimezonePicker render', () => {
const wrapper = shallow(
<TimezonePicker
phrases={phrases}
/>
);
it('should render a header with a title', () => {
expect(wrapper.find('.timezone_picker_header_title').text()).to.equal(phrases.timezonePickerTitle);
});
it('should render a Typeahead', () => {
expect(wrapper.find('OnClickOutside(Typeahead)')).to.have.lengthOf(1);
});
it('should render a close button', () => {
expect(wrapper.find('Button').prop('children')).to.equal(phrases.close);
});
});
describe('onClearFocus func', () => {
it('should callback when onClick header "back" icon', () => {
const onFocusChangeStub = sinon.stub();
const wrapper = shallow(
<TimezonePicker
phrases={phrases}
onClearFocus={onFocusChangeStub}
/>
);
wrapper.find('.timezone_picker_modal_header').find('svg').parent().simulate('click');
expect(onFocusChangeStub.callCount).to.equal(1);
});
it('should callback when onClick Button', () => {
const onFocusChangeStub = sinon.stub();
const wrapper = shallow(
<TimezonePicker
phrases={phrases}
onClearFocus={onFocusChangeStub}
/>
);
wrapper.find('Button').simulate('click');
expect(onFocusChangeStub.callCount).to.equal(1);
});
it('should callback when timezone change', () => {
const onFocusChangeStub = sinon.stub();
const wrapper = shallow(
<TimezonePicker
phrases={phrases}
onClearFocus={onFocusChangeStub}
/>
);
wrapper.instance().handleTimezoneChange([mockTimezone]);
expect(onFocusChangeStub.callCount).to.equal(1);
});
});
describe('handle timezone change func', () => {
it('should callback when timezone change', () => {
const onTimezoneChangeStub = sinon.stub();
const wrapper = shallow(
<TimezonePicker
phrases={phrases}
handleTimezoneChange={onTimezoneChangeStub}
/>
);
wrapper.instance().handleTimezoneChange([mockTimezone]);
expect(onTimezoneChangeStub.callCount).to.equal(1);
expect(onTimezoneChangeStub.calledWith(mockTimezone));
});
});
});
================================================
FILE: test/components/TwelveHoursTheme_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon-sandbox';
import TwelveHoursMode from '../../src/components/MaterialTheme/TwelveHoursMode';
import PickerDragHandler from '../../src/components/Picker/PickerDragHandler';
import languageHelper from '../../src/utils/language';
import '../_helpers/adapter';
const phrases = languageHelper.get('en');
describe('TwelveHoursMode', () => {
describe('TwelveHoursMode init with defaultTime', () => {
const wrapper = shallow(
<TwelveHoursMode
focused
hour={'01'}
minute={'45'}
phrases={phrases}
/>
);
it('should render component correctly', () => {
expect(wrapper.find('.meridiem')).to.have.lengthOf(1);
});
it('should render PickerDragHandler component', () => {
expect(wrapper.find(PickerDragHandler)).to.have.lengthOf(2);
});
it('should init correct state', () => {
expect(wrapper.state()).to.deep.equal({
hourPointerRotate: 30,
minutePointerRotate: 270
});
});
});
describe('TwelveHoursMode Func', () => {
const handleHourChange = sinon.stub();
const handleMinuteChange = sinon.stub();
const handleMeridiemChange = sinon.stub();
const wrapper = shallow(
<TwelveHoursMode
focused
hour={'01'}
minute={'45'}
meridiem={'AM'}
phrases={phrases}
handleHourChange={handleHourChange}
handleMinuteChange={handleMinuteChange}
handleMeridiemChange={handleMeridiemChange}
/>
);
it('should handleHourPointerClick', () => {
wrapper.instance().handleHourPointerClick({
time: 3,
pointerRotate: 90
});
expect(wrapper.state().hourPointerRotate).to.equal(90);
expect(handleHourChange.callCount).to.equal(1);
});
it('should handleHourPointerClick', () => {
wrapper.instance().handleMinutePointerClick({
time: 30,
pointerRotate: 180
});
expect(wrapper.state().minutePointerRotate).to.equal(180);
expect(handleMinuteChange.callCount).to.equal(1);
});
it('should handleMeridiemChange', () => {
wrapper.instance().handleMeridiemChange();
expect(handleMeridiemChange.callCount).to.equal(1);
wrapper.instance().handleMeridiemChange();
expect(handleMeridiemChange.callCount).to.equal(2);
});
});
});
================================================
FILE: test/components/TwentyFourHoursMode_spec.jsx
================================================
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import sinon from 'sinon-sandbox';
import TwentyFourHoursMode from '../../src/components/MaterialTheme/TwentyFourHoursMode';
import PickerDragHandler from '../../src/components/Picker/PickerDragHandler';
import languageHelper from '../../src/utils/language';
import '../_helpers/adapter';
const phrases = languageHelper.get('en');
describe('TwentyFourHoursMode', () => {
describe('TwentyFourHoursMode Init', () => {
const wrapper = shallow(
<TwentyFourHoursMode hour={'03'} />
);
it('should render PickerDragHandler component', () => {
expect(wrapper.find(PickerDragHandler)).to.have.lengthOf(1);
});
it('should init currect state', () => {
expect(wrapper.state()).to.deep.equal({
step: 0,
pointerRotate: 90
});
});
});
describe('TwentyFourHoursMode Func', () => {
const handleHourChange = sinon.stub();
const handleMinuteChange = sinon.stub();
const wrapper = shallow(
<TwentyFourHoursMode
hour={'01'}
minute={'45'}
handleHourChange={handleHourChange}
handleMinuteChange={handleMinuteChange}
/>
);
it('should handleHourChange', () => {
wrapper.instance().handleTimePointerClick({
time: 6,
pointerRotate: 180
});
expect(wrapper.state().pointerRotate).to.equal(180);
expect(handleHourChange.callCount).to.equal(1);
});
it('should handleStepChange', () => {
wrapper.instance().handleStepChange(1);
expect(wrapper.state()).to.deep.equal({
step: 0,
pointerRotate: 180
});
setTimeout(() => {
expect(wrapper.state()).to.deep.equal({
step: 1,
pointerRotate: 270
});
}, 300);
});
it('should handleMinuteChange', () => {
const newWrapper = shallow(
<TwentyFourHoursMode
hour={'01'}
minute={'45'}
step={1}
handleHourChange={handleHourChange}
handleMinuteChange={handleMinuteChange}
/>
);
newWrapper.instance().handleTimePointerClick({
time: 30,
pointerRotate: 180
});
// after click minute, we close the panel & reset step state.
expect(newWrapper.state().pointerRotate).to.equal(30);
expect(newWrapper.state().step).to.equal(0);
expect(handleMinuteChange.callCount).to.equal(1);
});
});
});
================================================
FILE: test/utils_spec.js
================================================
import moment from 'moment-timezone';
import { expect } from 'chai';
import timeHelper from '../src/utils/time';
import drag from '../src/utils/drag';
import {
MAX_ABSOLUTE_POSITION,
MIN_ABSOLUTE_POSITION
} from '../src/utils/constant';
import { isSeq, head, tail, last } from '../src/utils/func';
describe('Functional utils', () => {
describe('isSeq', () => {
it('should correctly detect a sequence', () => {
const isSequence = [isSeq('foo'), isSeq('foo'.split())].every(e => e === true);
const isNotSequence = [isSeq({ message: 'foo' }), isSeq(8), isSeq(true)].every(e => e === false);
expect(isSequence).to.equal(true);
expect(isNotSequence).to.equal(true);
});
});
describe('head', () => {
it('should return the first element of a sequence', () => {
expect(head('foo')).to.equal('f');
expect(head('foo'.split(''))).to.equal('f');
});
});
describe('tail', () => {
it('should return the last elements of a sequence', () => {
expect(tail('foo')).to.equal('oo');
expect(tail('foo'.split(''))).to.deep.equal(['o', 'o']);
});
});
describe('last', () => {
it('should return the last element of a sequence', () => {
expect(last('foo')).to.equal('o');
expect(last('foo'.split(''))).to.equal('o');
});
});
});
// because mocha doesn't play nice with arrow functions 😞
const tz = timeHelper.guessUserTz();
const time24 = moment().tz(tz.zoneName).format('HH:mmA').split(/:/);
const time12 = moment().tz(tz.zoneName).format('hh:mmA').split(/:/);
const modes = [24, 12];
const meridies = ['AM', 'PM']; // yes, this is the correct plural 😜
describe('Time utils', () => {
describe('getCurrentTime()', () => {
it('should return the current time as a string in 24h format', () => {
const timeString = timeHelper.current();
expect(timeString).to.equal(time24.join(':').slice(0, 5));
});
});
describe('given a call to getValidTimeData()', () => {
describe('when passed no arguments', () => {
it('then it should default to the current local time in 24h mode', () => {
const testTimeData = timeHelper.time();
const timeData = {
hour12: head(time12).replace(/^0/, ''),
hour24: head(time24),
minute: last(time24).slice(0, 2),
meridiem: null,
mode: 24,
timezone: tz.zoneName
};
expect(testTimeData).to.deep.equal(timeData);
});
});
describe('when passed only a mode', () => {
it('then it should default to the current local time, with user-specified mode', () => {
modes.forEach((mode) => {
const testTimeData = timeHelper.time({
timeMode: mode
});
const timeData = {
mode,
hour12: head(time12).replace(/^0/, ''),
hour24: head(time24),
minute: last(time24).slice(0, 2),
meridiem: mode === 12 ? last(time12).slice(2) : null,
timezone: tz.zoneName
};
expect(testTimeData).to.deep.equal(timeData);
});
});
});
describe('when we passed only a meridiem', () => {
it('then it should default to the current local time, in 12h mode, ignoring meridiem', () => {
meridies.forEach((meridiem) => {
const testTimeData = timeHelper.time({ meridiem });
const timeData = {
hour12: head(time12).replace(/^0/, ''),
hour24: head(time24),
minute: last(time24).slice(0, 2),
meridiem: last(time12).slice(2),
mode: 12,
timezone: tz.zoneName
};
expect(testTimeData).to.deep.equal(timeData);
});
});
});
});
describe('Test getValidateTime func', () => {
it('should return 00 when get undefined', () => {
expect(timeHelper.validate()).to.equal('00');
});
it('should return 00 when get NaN', () => {
expect(timeHelper.validate('abc')).to.equal('00');
});
it('should return itself when validate', () => {
expect(timeHelper.validate('12')).to.equal('12');
});
it('should return a string with 0', () => {
expect(timeHelper.validate('2')).to.equal('02');
});
});
describe('Test getValidateIntTime func', () => {
it('should return 0', () => {
expect(timeHelper.validateInt('a')).to.equal(0);
});
it('should return int', () => {
expect(timeHelper.validateInt('11')).to.equal(11);
});
it('should return 0', () => {
expect(timeHelper.validateInt(null)).to.equal(0);
});
});
describe('Test getStandardAbsolutePosition func', () => {
it('should return the MinPosition', () => {
expect(
drag.validatePosition(
MIN_ABSOLUTE_POSITION - 1, MIN_ABSOLUTE_POSITION, MAX_ABSOLUTE_POSITION
)
).to.equal(MIN_ABSOLUTE_POSITION);
});
it('should return the MaxPosition', () => {
expect(
drag.validatePosition(
MAX_ABSOLUTE_POSITION + 1, MAX_ABSOLUTE_POSITION, MAX_ABSOLUTE_POSITION
)
).to.equal(MAX_ABSOLUTE_POSITION);
});
});
describe('Test timezone utils function', () => {
it('should get timezone by name', () => {
expect(
timeHelper.tzForName('America/Indianapolis').zoneName
).to.equal('America/Indiana/Indianapolis');
});
it('should get timezone by city', () => {
expect(
timeHelper.tzForCity('shanghai').zoneName
).to.equal('Asia/Shanghai');
});
});
describe('Test time format function', () => {
it('should format hour', () => {
expect(timeHelper.hourFormatter('8')).to.equal('08:00');
expect(timeHelper.hourFormatter('13:1')).to.equal('13:01');
expect(timeHelper.hourFormatter('2:60')).to.equal('02:00');
});
it('should format hour with default time', () => {
expect(timeHelper.hourFormatter('', '11:11')).to.equal('11:11');
expect(timeHelper.hourFormatter()).to.equal('00:00');
});
it('should format hour with meridiem', () => {
expect(timeHelper.hourFormatter('2:6 pm')).to.equal('02:06 PM');
expect(timeHelper.hourFormatter('2:6 12')).to.equal('02:06 AM');
expect(timeHelper.hourFormatter('13:00 pm')).to.equal('01:00 PM');
});
it('should remove meridiem in time', () => {
expect(timeHelper.withoutMeridiem('08:00 PM')).to.equal('08:00');
expect(timeHelper.withoutMeridiem('08:00 AM')).to.equal('08:00');
expect(timeHelper.withoutMeridiem('08:00')).to.equal('08:00');
})
});
describe('Test times render function', () => {
it('should render full 24 hour times with 30 minutes step', () => {
const times = timeHelper.get24ModeTimes({});
expect(times.length).to.equal(48);
expect(times[0]).to.equal('00:00');
expect(times[47]).to.equal('23:30');
});
it('should render full 24 hour times with 1 hour step', () => {
const times = timeHelper.get24ModeTimes({ step: 1, unit: 'hour' });
expect(times.length).to.equal(24);
expect(times[0]).to.equal('00:00');
expect(times[23]).to.equal('23:00');
});
it('should render 24 hour times cross one day with 1 hour step', () => {
const times = timeHelper.get24ModeTimes({
from: '20',
to: '8',
step: 1,
unit: 'hour'
});
expect(times.length).to.equal(13);
expect(times[0]).to.equal('20:00');
expect(times[12]).to.equal('08:00');
});
it('should render full 12 hour times with 30 minutes step', () => {
const times = timeHelper.get12ModeTimes({});
expect(times.length).to.equal(48);
expect(times[0]).to.equal('12:00 AM');
expect(times[47]).to.equal('11:30 PM');
});
it('should render full 12 hour times with 1 hour step', () => {
const times = timeHelper.get12ModeTimes({ step: 1, unit: 'hour' });
expect(times.length).to.equal(24);
expect(times[0]).to.equal('12:00 AM');
expect(times[23]).to.equal('11:00 PM');
});
it('should render 12 hour times cross one day with 1 hour step', () => {
const times = timeHelper.get12ModeTimes({
from: '08:00 PM',
to: '08:00 AM',
step: 1,
unit: 'hour'
});
expect(times.length).to.equal(13);
expect(times[0]).to.equal('08:00 PM');
expect(times[12]).to.equal('08:00 AM');
});
});
});
gitextract_0j5lhn9t/
├── .babelrc
├── .coveralls.yml
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .storybook/
│ ├── addons.js
│ ├── config.js
│ ├── preview-head.html
│ └── webpack.config.js
├── .travis.yml
├── LICENSE
├── README.md
├── css/
│ ├── base.css
│ ├── classic/
│ │ └── default.css
│ └── material/
│ ├── base.css
│ ├── button.css
│ ├── default.css
│ └── timezone.css
├── doc/
│ ├── CHANGELOG.md
│ └── README_CN.md
├── examples/
│ ├── TimePickerWrapper.js
│ ├── TimePickerWrapper2.js
│ └── TimeZonesPickerWrapper.js
├── index.js
├── package.json
├── src/
│ ├── components/
│ │ ├── ClassicTheme/
│ │ │ └── index.jsx
│ │ ├── Common/
│ │ │ ├── AsyncComponent.jsx
│ │ │ └── Button.jsx
│ │ ├── MaterialTheme/
│ │ │ ├── TwelveHoursMode.jsx
│ │ │ ├── TwentyFourHoursMode.jsx
│ │ │ └── index.jsx
│ │ ├── OutsideClickHandler.jsx
│ │ ├── Picker/
│ │ │ ├── PickerDragHandler.jsx
│ │ │ ├── PickerPoint.jsx
│ │ │ └── PickerPointGenerator.jsx
│ │ ├── TimePicker.jsx
│ │ └── Timezone/
│ │ ├── TimezonePicker.jsx
│ │ └── index.jsx
│ └── utils/
│ ├── constant.js
│ ├── drag.js
│ ├── func.js
│ ├── icons.js
│ ├── language.js
│ └── time.js
├── stories/
│ ├── ClassicThemePicker.js
│ ├── CustomTrigger.js
│ ├── DarkColor.js
│ ├── DifferentLanguage.js
│ ├── TimePicker.js
│ ├── TimePicker2.js
│ ├── TwelveHoursMode.js
│ └── WithTimeZones.js
└── test/
├── _helpers/
│ ├── adapter.js
│ └── ignoreSVGStrings.jsx
├── components/
│ ├── ClassicTheme_spec.jsx
│ ├── MaterialTheme_spec.jsx
│ ├── PickerDargHandler_spec.jsx
│ ├── PickerPointGenerator_spec.jsx
│ ├── PickerPoint_spec.jsx
│ ├── TimePicker_func_spec.jsx
│ ├── TimePicker_init_spec.jsx
│ ├── Time_zone_spec.jsx
│ ├── Timezone_Picker_spec.jsx
│ ├── TwelveHoursTheme_spec.jsx
│ └── TwentyFourHoursMode_spec.jsx
└── utils_spec.js
SYMBOL INDEX (144 symbols across 19 files)
FILE: .storybook/config.js
function loadStories (line 11) | function loadStories() {
FILE: .storybook/webpack.config.js
constant SOURCE_PATH (line 4) | const SOURCE_PATH = path.join(__dirname, '../src');
FILE: examples/TimePickerWrapper.js
class TimePickerWrapper (line 7) | class TimePickerWrapper extends React.Component {
method constructor (line 8) | constructor(props) {
method onTimeChange (line 33) | onTimeChange(options) {
method onFocusChange (line 43) | onFocusChange(focused) {
method handleFocusedChange (line 48) | handleFocusedChange() {
method basicTrigger (line 53) | get basicTrigger() {
method customTrigger (line 68) | get customTrigger() {
method trigger (line 79) | get trigger() {
method render (line 89) | render() {
FILE: examples/TimePickerWrapper2.js
class TimePickerWrapper (line 7) | class TimePickerWrapper extends React.Component {
method constructor (line 8) | constructor(props) {
method onTimeChange (line 43) | onTimeChange(section) {
method onFocusChange (line 60) | onFocusChange(section) {
method handleFocusedChange (line 68) | handleFocusedChange(section) {
method getBasicTrigger (line 76) | getBasicTrigger() {
method getCustomTrigger (line 91) | getCustomTrigger() {
method getTrigger (line 102) | getTrigger(section) {
method renderTrigger (line 112) | renderTrigger(section) {
method render (line 138) | render() {
FILE: examples/TimeZonesPickerWrapper.js
constant TIME (line 7) | const TIME = timeHelper.time();
class TimeZonesPickerWrapper (line 19) | class TimeZonesPickerWrapper extends React.Component {
method constructor (line 20) | constructor(props) {
method languageData (line 29) | languageData() {
method render (line 34) | render() {
FILE: src/components/ClassicTheme/index.jsx
class ClassicTheme (line 30) | class ClassicTheme extends React.PureComponent {
method constructor (line 31) | constructor(props) {
method componentDidMount (line 39) | componentDidMount() {
method componentDidUpdate (line 42) | componentDidUpdate() {
method handleFocusDropdownOnTime (line 46) | handleFocusDropdownOnTime() {
method handleTimeChange (line 52) | handleTimeChange(timeData) {
method checkTimeIsActive (line 64) | checkTimeIsActive(time) {
method renderTimes (line 83) | renderTimes(timeDatas) {
method render (line 107) | render() {
FILE: src/components/Common/AsyncComponent.jsx
method componentWillMount (line 9) | componentWillMount() {
method render (line 17) | render() {
FILE: src/components/Common/Button.jsx
class Button (line 6) | class Button extends React.Component {
method constructor (line 7) | constructor(props) {
method onMouseDown (line 19) | onMouseDown() {
method onMouseUp (line 23) | onMouseUp() {
method onMouseEnter (line 27) | onMouseEnter() {
method onMouseLeave (line 32) | onMouseLeave() {
method render (line 38) | render() {
FILE: src/components/MaterialTheme/TwelveHoursMode.jsx
constant TIME (line 16) | const TIME = timeHelper.time();
class TwelveHoursMode (line 39) | class TwelveHoursMode extends React.PureComponent {
method constructor (line 40) | constructor(props) {
method resetHourDegree (line 57) | resetHourDegree() {
method resetMinuteDegree (line 68) | resetMinuteDegree() {
method getHourTopAndHeight (line 79) | getHourTopAndHeight() {
method getMinuteTopAndHeight (line 85) | getMinuteTopAndHeight() {
method handleMeridiemChange (line 91) | handleMeridiemChange() {
method handleHourPointerClick (line 102) | handleHourPointerClick(options) {
method handleMinutePointerClick (line 111) | handleMinutePointerClick(options) {
method handleDegreeChange (line 120) | handleDegreeChange(pointerRotate) {
method handleHourChange (line 124) | handleHourChange(time) {
method handleMinuteChange (line 130) | handleMinuteChange(time) {
method render (line 136) | render() {
FILE: src/components/MaterialTheme/TwentyFourHoursMode.jsx
class TwentyFourHoursMode (line 34) | class TwentyFourHoursMode extends React.PureComponent {
method constructor (line 35) | constructor(props) {
method handleStepChange (line 48) | handleStepChange(step) {
method setStep (line 59) | setStep(step) {
method clearFocus (line 69) | clearFocus() {
method handleTimePointerClick (line 74) | handleTimePointerClick(options) {
method handleTimeChange (line 92) | handleTimeChange(time, autoMode = null) {
method resetHourDegree (line 117) | resetHourDegree() {
method resetMinuteDegree (line 130) | resetMinuteDegree() {
method getTopAndHeight (line 141) | getTopAndHeight() {
method render (line 158) | render() {
FILE: src/components/OutsideClickHandler.jsx
class OutsideClickHandler (line 15) | class OutsideClickHandler extends React.PureComponent {
method constructor (line 16) | constructor(props) {
method componentDidMount (line 22) | componentDidMount() {
method componentDidUpdate (line 26) | componentDidUpdate() {
method componentWillUnmount (line 30) | componentWillUnmount() {
method bindActions (line 34) | bindActions() {
method unbindActions (line 47) | unbindActions() {
method onOutsideClick (line 60) | onOutsideClick(e) {
method render (line 71) | render() {
FILE: src/components/Picker/PickerDragHandler.jsx
class PickerDragHandler (line 44) | class PickerDragHandler extends React.PureComponent {
method constructor (line 45) | constructor(props) {
method componentDidMount (line 63) | componentDidMount() {
method componentWillUnmount (line 85) | componentWillUnmount() {
method componentDidUpdate (line 106) | componentDidUpdate(prevProps) {
method initialRotationAndLength (line 120) | initialRotationAndLength() {
method resetState (line 136) | resetState() {
method resetOrigin (line 140) | resetOrigin() {
method resetDragCenter (line 155) | resetDragCenter() {
method getRadian (line 171) | getRadian(x, y) {
method getAbsolutePosition (line 186) | getAbsolutePosition(x, y) {
method getPointerRotate (line 192) | getPointerRotate(options = {}) {
method handleTimePointerChange (line 215) | handleTimePointerChange(options = {}) {
method handleMouseDown (line 278) | handleMouseDown(e) {
method handleMouseMove (line 297) | handleMouseMove(e) {
method handleMouseUp (line 331) | handleMouseUp(e) {
method render (line 358) | render() {
FILE: src/components/Picker/PickerPointGenerator.jsx
method addAnimation (line 11) | addAnimation() {
method removeAnimation (line 15) | removeAnimation() {
method renderMinutePointes (line 19) | renderMinutePointes() {
method renderHourPointes (line 37) | renderHourPointes() {
method render (line 59) | render() {
FILE: src/components/TimePicker.jsx
constant TIME (line 24) | const TIME = timeHelper.time({ useTz: false });
class TimePicker (line 102) | class TimePicker extends React.PureComponent {
method constructor (line 103) | constructor(props) {
method componentWillReceiveProps (line 131) | componentWillReceiveProps(nextProps) {
method onFocus (line 138) | onFocus() {
method onBlur (line 145) | onBlur() {
method onFocusChange (line 152) | onFocusChange(focused) {
method timeData (line 161) | timeData(timeChanged) {
method languageData (line 179) | get languageData() {
method hourAndMinute (line 184) | get hourAndMinute() {
method formattedTime (line 196) | get formattedTime() {
method meridiem (line 236) | get meridiem() {
method onTimeChanged (line 246) | onTimeChanged(timeChanged) {
method handleHourChange (line 250) | handleHourChange(hour) {
method handleMinuteChange (line 260) | handleMinuteChange(minute) {
method handleMeridiemChange (line 271) | handleMeridiemChange(meridiem) {
method handleTimeChange (line 280) | handleTimeChange(options) {
method handleHourAndMinuteChange (line 286) | handleHourAndMinuteChange(time) {
method renderDialPlate (line 293) | renderDialPlate() {
method render (line 349) | render() {
FILE: src/components/Timezone/TimezonePicker.jsx
class TimezonePicker (line 9) | class TimezonePicker extends React.PureComponent {
method constructor (line 10) | constructor(props) {
method handleTimezoneChange (line 16) | handleTimezoneChange(selection) {
method render (line 25) | render() {
FILE: src/components/Timezone/index.jsx
constant TIME (line 8) | const TIME = timeHelper.time();
class Timezone (line 11) | class Timezone extends React.PureComponent {
method constructor (line 12) | constructor(props) {
method onClearFocus (line 26) | onClearFocus() {
method handleFocusedChange (line 30) | handleFocusedChange() {
method handleTimezoneChange (line 37) | handleTimezoneChange(timezone) {
method render (line 43) | render() {
FILE: src/utils/constant.js
constant HOURS (line 4) | const HOURS = getArray(24 + 1);
constant TWELVE_HOURS (line 5) | const TWELVE_HOURS = getArray(12 + 1);
constant MINUTES (line 6) | const MINUTES = getArray(60 + 1);
constant PICKER_WIDTH (line 8) | const PICKER_WIDTH = 260;
constant POINTER_WIDTH (line 9) | const POINTER_WIDTH = 35;
constant PICKER_RADIUS (line 11) | const PICKER_RADIUS = PICKER_WIDTH / 2;
constant MAX_ABSOLUTE_POSITION (line 12) | const MAX_ABSOLUTE_POSITION = 125;
constant MIN_ABSOLUTE_POSITION (line 13) | const MIN_ABSOLUTE_POSITION = 90;
constant POINTER_RADIUS (line 14) | const POINTER_RADIUS = POINTER_WIDTH / 2;
constant BROWSER_COMPATIBLE (line 16) | const BROWSER_COMPATIBLE = [
constant MERIDIEMS (line 25) | const MERIDIEMS = ['AM', 'PM'];
FILE: src/utils/language.js
constant LANGUAGES (line 1) | const LANGUAGES = {
FILE: test/components/PickerDargHandler_spec.jsx
function copyProps (line 11) | function copyProps(src, target) {
Condensed preview — 67 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (177K chars).
[
{
"path": ".babelrc",
"chars": 113,
"preview": "{\n \"presets\": [\n \"env\",\n \"stage-0\",\n \"react\"\n ],\n \"plugins\": [\n \"system-import-transformer\"\n ]\n}\n"
},
{
"path": ".coveralls.yml",
"chars": 70,
"preview": "service_name: travis-ci\nrepo_token: MMi1goJ3ZCpZ5Iaz9i1tSus4G5psdpQTY\n"
},
{
"path": ".eslintignore",
"chars": 64,
"preview": "node_modules/\nlib/\n.out.\n.storybook/\nwebpack.config.js\nindex.js\n"
},
{
"path": ".eslintrc.json",
"chars": 1919,
"preview": "{\n \"parser\": \"babel-eslint\",\n \"extends\": \"standard\",\n \"plugins\": [\n \"react\",\n \"import\",\n \"babel\"\n ],\n \"par"
},
{
"path": ".gitignore",
"chars": 47,
"preview": "*.lock\n.DS_Store\n/lib\n/.out\n/node_modules\n/out*"
},
{
"path": ".npmignore",
"chars": 128,
"preview": "/components/\n/examples\n/stories\n/webpack\n/.storybook\n/intro_src\n/test\n/src\n/.coveralls.yml\n/.travis.yml\n/webpack.config."
},
{
"path": ".storybook/addons.js",
"chars": 69,
"preview": "import '@storybook/addons';\nimport '@storybook/addon-knobs/register'\n"
},
{
"path": ".storybook/config.js",
"chars": 621,
"preview": "import { addDecorator, configure, setAddon } from '@storybook/react';\n\nimport infoAddon from '@storybook/addon-info';\nim"
},
{
"path": ".storybook/preview-head.html",
"chars": 778,
"preview": "<link href=\"https://fonts.googleapis.com/css?family=Open+Sans\" rel=\"stylesheet\">\n<link href=\"http://obpykithy.bkt.cloudd"
},
{
"path": ".storybook/webpack.config.js",
"chars": 1132,
"preview": "const path = require('path');\nconst webpack = require('webpack');\n\nconst SOURCE_PATH = path.join(__dirname, '../src');\n\n"
},
{
"path": ".travis.yml",
"chars": 464,
"preview": "language: node_js\nnode_js:\n - \"7\"\n - \"6\"\nbefore_script:\n - npm install -g mocha\n - npm install -g eslint\n - npm i\n "
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2016 ecmadao\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "README.md",
"chars": 10780,
"preview": "\n\n[](https://badg"
},
{
"path": "css/base.css",
"chars": 3205,
"preview": ".time_picker_container {\n position: relative;\n}\n\n.time_picker_preview {\n height: 50px;\n}\n\n.time_picker_preview:not(.di"
},
{
"path": "css/classic/default.css",
"chars": 1376,
"preview": "@import \"../base.css\";\n\n.classic_theme_container {\n height: 250px;\n overflow-y: scroll;\n}\n\n.classic_theme_container .c"
},
{
"path": "css/material/base.css",
"chars": 3952,
"preview": "@import \"../base.css\";\n\n.time_picker_modal_container {\n}\n\n.time_picker_modal_header,\n.time_picker_modal_footer,\n.timezon"
},
{
"path": "css/material/button.css",
"chars": 230,
"preview": ".time_picker_button {\n padding: 5px 10px;\n background-color: transparent;\n display: inline-block;\n color: #949494;\n "
},
{
"path": "css/material/default.css",
"chars": 851,
"preview": "@import \"./base.css\";\n@import \"./button.css\";\n@import \"./timezone.css\";\n\n.dark .time_picker_preview {\n}\n\n.dark .time_pic"
},
{
"path": "css/material/timezone.css",
"chars": 4605,
"preview": ".timezone_picker_modal_container {\n user-select: none;\n cursor: default;\n position: absolute;\n z-index: 3;\n backgro"
},
{
"path": "doc/CHANGELOG.md",
"chars": 1025,
"preview": "# CHANGELOG\n\n### v3.1.3\n\n#### new props\n\n- Add `timeConfig` props: to config from, to, step for classic theme panel.\n\n##"
},
{
"path": "doc/README_CN.md",
"chars": 8305,
"preview": "\n\n[](https://badg"
},
{
"path": "examples/TimePickerWrapper.js",
"chars": 2659,
"preview": "\nimport React from 'react';\nimport TimePicker from '../src/components/TimePicker';\nimport timeHelper from '../src/utils/"
},
{
"path": "examples/TimePickerWrapper2.js",
"chars": 3290,
"preview": "\nimport React from 'react';\nimport TimePicker from '../src/components/TimePicker';\nimport timeHelper from '../src/utils/"
},
{
"path": "examples/TimeZonesPickerWrapper.js",
"chars": 1338,
"preview": "\nimport React from 'react';\nimport Timezone from '../src/components/Timezone';\nimport timeHelper from '../src/utils/time"
},
{
"path": "index.js",
"chars": 132,
"preview": "require('./lib/utils/time').default;\nvar TimePicker = require('./lib/components/TimePicker').default;\n\nmodule.exports = "
},
{
"path": "package.json",
"chars": 3836,
"preview": "{\n \"name\": \"react-times\",\n \"description\": \"A react time-picker component, no jquery-rely\",\n \"version\": \"3.1.12\",\n \"a"
},
{
"path": "src/components/ClassicTheme/index.jsx",
"chars": 3496,
"preview": "\nimport React from 'react';\nimport PropTypes from 'prop-types';\nimport timeHelper from '../../utils/time';\n\nconst propTy"
},
{
"path": "src/components/Common/AsyncComponent.jsx",
"chars": 612,
"preview": "\nimport React from 'react';\n\nconst asyncComponent = getComponent =>\n class AsyncComponent extends React.Component {\n "
},
{
"path": "src/components/Common/Button.jsx",
"chars": 1768,
"preview": "\nimport React from 'react';\nimport cx from 'classnames';\nimport PropTypes from 'prop-types';\n\nclass Button extends React"
},
{
"path": "src/components/MaterialTheme/TwelveHoursMode.jsx",
"chars": 6553,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport {\n MINUTES,\n TWELVE_HOURS,\n PICKER_RADIUS,\n PO"
},
{
"path": "src/components/MaterialTheme/TwentyFourHoursMode.jsx",
"chars": 6137,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport {\n HOURS,\n MINUTES,\n PICKER_RADIUS,\n POINTER_R"
},
{
"path": "src/components/MaterialTheme/index.jsx",
"chars": 1006,
"preview": "\nimport React from 'react';\nimport asyncComponent from '../Common/AsyncComponent';\nimport Timezone from '../Timezone';\n\n"
},
{
"path": "src/components/OutsideClickHandler.jsx",
"chars": 2192,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport PropTypes from 'prop-types';\n\nconst propTypes = {\n "
},
{
"path": "src/components/Picker/PickerDragHandler.jsx",
"chars": 11250,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport {\n PICKER_RADIUS,\n POINTER_RADIUS,\n MAX_ABSOLUT"
},
{
"path": "src/components/Picker/PickerPoint.jsx",
"chars": 1297,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport darg from '../../utils/drag';\n\nconst propTypes = {"
},
{
"path": "src/components/Picker/PickerPointGenerator.jsx",
"chars": 1888,
"preview": "import React from 'react';\nimport {\n HOURS,\n MINUTES,\n TWELVE_HOURS\n} from '../../utils/constant.js';\nimport PickerPo"
},
{
"path": "src/components/TimePicker.jsx",
"chars": 10854,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport cx from 'classnames';\nimport OutsideClickHandler f"
},
{
"path": "src/components/Timezone/TimezonePicker.jsx",
"chars": 2061,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { Typeahead } from 'react-bootstrap-typeahead';\n\ni"
},
{
"path": "src/components/Timezone/index.jsx",
"chars": 2531,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { CSSTransition, TransitionGroup } from 'react-tra"
},
{
"path": "src/utils/constant.js",
"chars": 556,
"preview": "\nconst getArray = length => new Array(length).join('0').split('');\n\nexport const HOURS = getArray(24 + 1);\nexport const "
},
{
"path": "src/utils/drag.js",
"chars": 2404,
"preview": "\nimport { BROWSER_COMPATIBLE } from './constant';\n\nconst getScrollPosition = () => {\n const position = {\n x: documen"
},
{
"path": "src/utils/func.js",
"chars": 715,
"preview": "// simple utils for working with sequences like Array or string\n\nconst checkType = (val, result) =>\n Object.prototype.t"
},
{
"path": "src/utils/icons.js",
"chars": 631,
"preview": "import React from 'react';\n\nconst time = (\n <svg width=\"48\" height=\"48\" viewBox=\"0 0 48 48\">\n <path d=\"M23.99 4C12.9"
},
{
"path": "src/utils/language.js",
"chars": 1062,
"preview": "const LANGUAGES = {\n en: {\n confirm: 'confirm',\n cancel: 'cancel',\n close: 'close',\n timezonePickerTitle: '"
},
{
"path": "src/utils/time.js",
"chars": 11371,
"preview": "\nimport moment from 'moment-timezone';\nimport { head, last, is } from './func';\n\n// loads moment-timezone's timezone dat"
},
{
"path": "stories/ClassicThemePicker.js",
"chars": 1779,
"preview": "import '../css/classic/default.css';\n\nimport React from 'react';\nimport TimePickerWrapper from '../examples/TimePickerWr"
},
{
"path": "stories/CustomTrigger.js",
"chars": 687,
"preview": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { withKnobs } from '@storybook/addon-kno"
},
{
"path": "stories/DarkColor.js",
"chars": 672,
"preview": "import '../css/material/default.css';\n\nimport React from 'react';\nimport TimePickerWrapper from '../examples/TimePickerW"
},
{
"path": "stories/DifferentLanguage.js",
"chars": 1299,
"preview": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport { withKnobs, text } from '@storybook/add"
},
{
"path": "stories/TimePicker.js",
"chars": 1705,
"preview": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport '../css/material/default.css';\nimport { "
},
{
"path": "stories/TimePicker2.js",
"chars": 365,
"preview": "import React from 'react';\nimport { storiesOf } from '@storybook/react';\nimport '../css/material/default.css';\nimport { "
},
{
"path": "stories/TwelveHoursMode.js",
"chars": 1004,
"preview": "\nimport React from 'react';\nimport TimePickerWrapper from '../examples/TimePickerWrapper';\nimport { storiesOf } from '@s"
},
{
"path": "stories/WithTimeZones.js",
"chars": 1179,
"preview": "import '../css/material/default.css';\n\nimport { withKnobs } from '@storybook/addon-knobs';\nimport React from 'react';\nim"
},
{
"path": "test/_helpers/adapter.js",
"chars": 123,
"preview": "import Enzyme from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\n\nEnzyme.configure({ adapter: new Adapter() }"
},
{
"path": "test/_helpers/ignoreSVGStrings.jsx",
"chars": 101,
"preview": "require.extensions['.svg'] = (obj) => {\n obj.exports = () => (\n <svg>SVG_TEST_STUB</svg>\n );\n};\n"
},
{
"path": "test/components/ClassicTheme_spec.jsx",
"chars": 440,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport '../_helpers/adapter'"
},
{
"path": "test/components/MaterialTheme_spec.jsx",
"chars": 1226,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport MaterialTheme from '."
},
{
"path": "test/components/PickerDargHandler_spec.jsx",
"chars": 1155,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { mount } from 'enzyme';\nimport PickerDragHandler from "
},
{
"path": "test/components/PickerPointGenerator_spec.jsx",
"chars": 1572,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport PickerPoint from '../"
},
{
"path": "test/components/PickerPoint_spec.jsx",
"chars": 444,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport PickerPoint from '../"
},
{
"path": "test/components/TimePicker_func_spec.jsx",
"chars": 3817,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport sinon from 'sinon-san"
},
{
"path": "test/components/TimePicker_init_spec.jsx",
"chars": 5106,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\n// import ClassicTheme from "
},
{
"path": "test/components/Time_zone_spec.jsx",
"chars": 4711,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport Timezone from '../../"
},
{
"path": "test/components/Timezone_Picker_spec.jsx",
"chars": 2715,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport sinon from 'sinon-san"
},
{
"path": "test/components/TwelveHoursTheme_spec.jsx",
"chars": 2439,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport sinon from 'sinon-san"
},
{
"path": "test/components/TwentyFourHoursMode_spec.jsx",
"chars": 2490,
"preview": "import React from 'react';\nimport { expect } from 'chai';\nimport { shallow } from 'enzyme';\nimport sinon from 'sinon-san"
},
{
"path": "test/utils_spec.js",
"chars": 8428,
"preview": "import moment from 'moment-timezone';\nimport { expect } from 'chai';\nimport timeHelper from '../src/utils/time';\nimport "
}
]
About this extraction
This page contains the full source code of the ecmadao/react-times GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 67 files (160.1 KB), approximately 45.4k tokens, and a symbol index with 144 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.