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 ================================================ ================================================ 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 ================================================ ![react-times](./doc/intro_src/react_times.png) [![npm version](https://badge.fury.io/js/react-times.svg)](https://badge.fury.io/js/react-times) [![Build Status](https://travis-ci.org/ecmadao/react-times.svg?branch=master)](https://travis-ci.org/ecmadao/react-times) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) [![react-times](http://img.shields.io/npm/dm/react-times.svg)](https://www.npmjs.com/package/react-times) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/ecmadao/react-times/master/LICENSE) [![NPM](https://nodei.co/npm/react-times.png?downloads=true&downloadRank=true&stars=true)](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.** ![react-times](./doc/intro_src/react-times.gif) # 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() { } } ``` > See more examples here: ```javascript // some config example render() { } ``` > 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 ``` ![24HoursMode](./doc/intro_src/24HoursMode.png) - 12 hours mode with Material Theme, light color by default ```javascript ``` ![12HoursMode](./doc/intro_src/12HoursMode.png) - 24 hours mode with Material Theme, dark color ```javascript ``` ![DarkColor](./doc/intro_src/DarkColor.png) - 24 hours mode, showing user current timezone. (Besides, your can use `timezone` props to custom timezone) ```javascript ``` ![showTimezone](./doc/intro_src/24HoursMode-showTimezone.png) - 24 hours mode with Classic Theme, light color by default ```javascript ``` ![24HoursMode-ClassicTheme](./doc/intro_src/24HoursMode-ClassicTheme.png) - 24 hours mode with Classic Theme, dark color ```javascript ``` ![24HoursMode-ClassicTheme-dark](./doc/intro_src/24HoursMode-ClassicTheme-dark.png) # 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 click to open modal )} /> ``` - `closeOnOutsideClick` > If you don't wanna close panel when outside click, you can use closeOnOutsideClick={false}. Default true ``` ``` - `disabled` > Disable component. Default false ``` ``` - `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` ``` ``` - `language` > `React.string`, use different language. Default english. ```javascript /* * support * en: english * zh-cn: 中文简体 * zh-tw: 中文繁体 * fr: Français * ja: 日本語 */ ``` - `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 ``` - `minuteStep` > `React.number`, default `5`. It means the minium minute can change. You can set it to 1, 2, 3... ```javascript ``` - `timeConfig` > `React.object`, to config from, to, step limit for classic theme panel. ```javascript ``` - `limitDrag` > `React.bool`, default `false`. If `true`, it will limite the drag rotation by `minuteStep` ```javascript ``` ## 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 ================================================ ![react-times](./doc/intro_src/react_times.png) [![npm version](https://badge.fury.io/js/react-times.svg)](https://badge.fury.io/js/react-times) [![Build Status](https://travis-ci.org/ecmadao/react-times.svg?branch=master)](https://travis-ci.org/ecmadao/react-times) [![Coverage Status](https://coveralls.io/repos/github/ecmadao/react-times/badge.svg?branch=master)](https://coveralls.io/github/ecmadao/react-times?branch=master) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com) [![react-times](http://img.shields.io/npm/dm/react-times.svg)](https://www.npmjs.com/package/react-times) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/ecmadao/react-times/master/LICENSE) [![NPM](https://nodei.co/npm/react-times.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/react-times) README:[English Version](./README.md) > 一个 React 时间选择器组件,使用 ES6 标准语法编写,没有 jQuery 依赖 **戳 [这里](./doc/CHANGELOG.md) 查看新版中更改/新增的 props。** ![react-times](./doc/intro_src/react-times.gif) # 线上 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() { } } ``` 关于配置的栗子: ```javascript render() { } ``` > 你可以戳 [这里](https://github.com/ecmadao/react-times/tree/master/examples) 查看更多栗子 # 秀一下 - 24 小时制,亮色调的 Material 主题(默认) ```javascript ``` ![24HoursMode](./doc/intro_src/24HoursMode.png) - 12 小时制,亮色调的 Material 主题 ```javascript ``` ![12HoursMode](./doc/intro_src/12HoursMode.png) - 24 小时制,暗色调的 Material 主题 ```javascript ``` ![DarkColor](./doc/intro_src/DarkColor.png) - 24 小时制,展示用户当前时区。(除此以外,可以通过 `timezone` props 来手动改变时区) ```javascript ``` ![showTimezone](./doc/intro_src/24HoursMode-showTimezone.png) - 24 小时制,亮色调的经典主题 ```javascript ``` ![24HoursMode-ClassicTheme](./doc/intro_src/24HoursMode-ClassicTheme.png) - 24 小时制,暗色调的经典主题 ```javascript ``` ![24HoursMode-ClassicTheme-dark](./doc/intro_src/24HoursMode-ClassicTheme-dark.png) # 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 click to open modal )} /> ``` - `closeOnOutsideClick` > 点击 Modal 外部后是否关闭。默认为 true ``` ``` - `disabled` > 禁用组件。默认为 false ``` ``` - `draggable` 如果想禁用拖拽,则可以设置 `draggable` 为 `false`,那样的话用户只能通过点击来改变时间。默认为 `true` ``` ``` - `language` > 语言。默认为英语 ```javascript /* * support * en: english * zh-cn: 中文简体 * zh-tw: 中文繁体 * fr: Français * ja: 日本語 */ ``` - `phrases` > `React.object`,用于自定义一些短语。可以在 [language.js](./src/utils/language.js) 查看所有的默认短语 ```javascript ``` - `minuteStep` > `React.number`, 默认为 `5`。该属性代表当分针改变时的最小步长(minute)。可以设置为 1,2,3.... ```javascript ``` - `timeConfig` > `React.object`, 用于配置 classic theme 时可选的时间范围以及步长 ```javascript ``` - `limitDrag` > `React.bool`, 默认为 `false`. 当设置为 `true` 时,将会限制用户的拖拽(从连续性的拖拽变为间断性拖拽,间隔由 `minuteStep` 确定) ```javascript ``` ## 回调 - `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 (
Click to open panel
{hour}:{minute}
); } get customTrigger() { return (
{ICONS.time}
); } get trigger() { const { customTriggerId } = this.props; const triggers = { 0: (
), 1: this.basicTrigger, 2: this.customTrigger }; return triggers[customTriggerId] || null; } render() { const { hour, minute, focused, meridiem, timezone, showTimezone, } = this.state; return (
); } } 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 (
Click to open panel
{hour}:{minute}
); } getCustomTrigger() { return (
{ICONS.time}
); } getTrigger(section) { const { customTriggerId } = this.props; const triggers = { 0: (
), 1: this.getBasicTrigger(section), 2: this.getCustomTrigger() }; return triggers[customTriggerId] || null; } renderTrigger(section) { const { hour, minute, focused, meridiem, timezone, showTimezone, } = this.state[section]; return ( ); } render() { return (
{this.renderTrigger(1)}
{this.renderTrigger(2)}
); } } 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 (
) } } 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 (
{ this.handleTimeChange(timeData); }} className={`${timeClass} ${colorPalette}`} ref={this.checkTimeIsActive(timeData) ? this.dropDownActiveTime : null} > {time} {meridiem ? {meridiem} : null}
); }); } render() { const { timeMode, timeConfig = {} } = this.props; const timeDatas = timeMode === 12 ? timeHelper.get12ModeTimes(timeConfig) : timeHelper.get24ModeTimes(timeConfig); return (
{this.renderTimes(timeDatas)}
); } } 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 ; } 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 (
{children}
); } } 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 (
{hour}:{minute}   {meridiem}
{!showTimezone ? (
) : null}
); } } 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 (
this.handleStepChange(0)} > {hour} : this.handleStepChange(1)} > {minute}
(this.pickerPointerContainer = ref)} handleTimePointerClick={this.handleTimePointerClick} pointerRotate={pointerRotate} />
); } } 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 (
{showTimezone ? : null }
); }; 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: , 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 (
(this.childNode = c)} className={outsideClass}> {this.props.children}
); } } 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 (
(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}
(this.pickerCenter = p)} />
); } } 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 (
{ 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} >
{index}
); }; 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 ( ); } 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 ( ); }); } render() { return (
(this.pickerPointerContainer = ref)} className="picker_pointer_container" > {type === 'hour' ? this.renderHourPointes() : this.renderMinutePointes()}
); } }; 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 ( ); } 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 (
{trigger || ( )} {this.renderDialPlate()}
); } } 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 (
{ICONS.chevronLeft} {phrases.timezonePickerTitle}
`${option.city} - ${option.zoneAbbr}`} options={timeHelper.tzMaps} maxResults={5} minLength={3} placeholder={phrases.timezonePickerLabel} />
); } } 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 ( ); }; return (
{timezone.zoneName} {timezone.zoneAbbr}
{timeZonePicker()}
); } } 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 = ( ); const chevronLeft = ( ); 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', () => ( )) .addWithInfo('with default time', () => ( )) .addWithInfo('dropdown focus on time/default time', () => ( )) .addWithInfo('dark color', () => ( )) .addWithInfo('12 hours mode', () => ( )) .addWithInfo('limit start, end, step for 12 hours mode', () => ( )) .addWithInfo('limit start, end, step for 24 hours mode', () => ( )) .addWithInfo('focused at setup', () => ( )) .addWithInfo('Set default time', () => ( )) .addWithInfo('Focus dropdown on time', () => ( )); ================================================ 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', () => ( )) .addWithInfo('any custom DOM', () => ( )) .addWithInfo('only render picker modal', () => ( } 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', () => ( )) .addWithInfo('with default time', () => ( )) .addWithInfo('focused at setup', () => ( )) .addWithInfo('without icon', () => ( )); ================================================ 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)', () => ( )) .addWithInfo('汉语 - 简体', () => ( )) .addWithInfo('汉语 - 繁体', () => ( )) .addWithInfo('Français', () => ( )) .addWithInfo('日本語', () => ( )) .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 ( ); }); ================================================ 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', () => ( )) .addWithInfo('disabled', () => ( )) .addWithInfo('with default time', () => { const aDefaultTime = text('set default time', '13:20'); return ( ); }) .addWithInfo('focused at setup', () => ( )) .addWithInfo('not auto change time panel', () => ( )) .addWithInfo('undraggable', () => ( )) .addWithInfo('disable outside click close', () => ( )) .addWithInfo('custom minute step', () => ( )) .addWithInfo('limit drag', () => ( )) .addWithInfo('custom HH-MM format', () => ( )) .addWithInfo('custom H-M format', () => ( )) .addWithInfo('custom time formatter', () => ( `${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', () => ( )); ================================================ 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', () => ( )) .addWithInfo('with default time', () => ( )) .addWithInfo('focused at setup, no icon', () => ( )) .addWithInfo('custom minute step', () => ( )) .addWithInfo('limit drag', () => ( )) .addWithInfo('disable outside click close', () => ( )); ================================================ 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', () => ( )) .addWithInfo('with default (custom) timezone', () => ( )) .addWithInfo('with timezone search', () => ( )) .addWithInfo('with 12 hour (custom) time', () => ( )) .addWithInfo('with dark theme', () => ( )) .addWithInfo('timezone picker', () => ); ================================================ 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_TEST_STUB ); }; ================================================ 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(); 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( ); expect(wrapper.find('Timezone')).to.have.lengthOf(1); }); it('should not render the Timezone footer', () => { const wrapper = shallow(); expect(wrapper.find('Timezone')).to.have.lengthOf(0); }); }); describe('MaterialTheme render correctly', () => { it('should render with className', () => { const wrapper = shallow(); 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(''); 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(); 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(); 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(); 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(); 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( ); 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(); wrapper.instance().onFocus(); expect(wrapper.state().focused).to.equal(true); }); it('should clear focus', () => { const wrapper = shallow(); wrapper.instance().onBlur(); expect(wrapper.state().focused).to.equal(false); }); it('should callback when focus', () => { const onFocusChangeStub = sinon.stub(); const wrapper = shallow(); wrapper.instance().onFocus(); expect(onFocusChangeStub.callCount).to.equal(1); }); }); describe('handle hour change func', () => { // it('should change hour', () => { // const wrapper = shallow(); // wrapper.instance().handleHourChange(11); // expect(wrapper.props().time.split(':')[0]).to.equal('11'); // }); // // it('should change to validate hour', () => { // const wrapper = shallow(); // 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(); wrapper.instance().handleHourChange(1); expect(onTimeChangeStub.callCount).to.equal(1); }); }); describe('handle minute change func', () => { // it('should change minute', () => { // const wrapper = shallow(); // wrapper.instance().handleMinuteChange(59); // expect(wrapper.state().minute).to.equal('59'); // }); // // it('should change to validate minute', () => { // const wrapper = shallow(); // 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(); 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(); 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(); 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(); 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(); expect(wrapper.is('.time_picker_container')).to.equal(true); }); it('renders an OutsideClickHandler', () => { const wrapper = shallow(); expect(wrapper.find(OutsideClickHandler)).to.have.lengthOf(1); }); // it('renders an MaterialTheme', () => { // const wrapper = shallow(); // expect(wrapper.contains(MaterialTheme)).to.have.lengthOf(1); // }); // it('renders an ClassicTheme', () => { // const wrapper = shallow(); // expect(wrapper.contains(ClassicTheme)).to.have.lengthOf(1); // }); it('renders an PickerDragHandler', () => { const wrapper = shallow(); 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(); expect(wrapper.is('.time_picker_container.dark')).to.equal(true); }); it('should render with focused', () => { const wrapper = shallow(); 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(); 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(); expect(wrapper.find(OutsideClickHandler).props().focused).to.equal(true); }); it('should render with no onOutsideClick handler', () => { const wrapper = shallow(); 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(); expect(wrapper.find('.preview_container.without_icon')).to.have.lengthOf(1); }); // it('should render with default time in child props', () => { // const wrapper = mount(); // 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(); 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(); // 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(); 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(); 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(); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); wrapper.setState({ focused: false }); expect(wrapper.find('TimezonePicker')).to.have.lengthOf(0); }); }); }); }); describe('onClearFocus Func', () => { it('should clear focused', () => { const wrapper = shallow( ); wrapper.setState({ focused: true }); wrapper.instance().onClearFocus(); expect(wrapper.state().focused).to.equal(false); }); }); describe('handleFocusedChange Func', () => { const wrapper = shallow( ); 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( ); 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( ); 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( ); 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( ); wrapper.find('Button').simulate('click'); expect(onFocusChangeStub.callCount).to.equal(1); }); it('should callback when timezone change', () => { const onFocusChangeStub = sinon.stub(); const wrapper = shallow( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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( ); 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'); }); }); });