├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .storybook ├── main.js └── preview.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── COMMIT_MESSAGE_CONVENTION.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── index.d.ts ├── package-lock.json ├── package.json ├── src ├── index.js └── isEqual.js ├── stories ├── app.css ├── index.stories.js └── myTheme.js └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | parserOptions: { 4 | ecmaVersion: 7, 5 | sourceType: 'module', 6 | ecmaFeatures: { 7 | jsx: true 8 | } 9 | }, 10 | extends: ['tui/es6', 'plugin:react/recommended', 'plugin:prettier/recommended', 'plugin:storybook/recommended'], 11 | plugins: ['react', 'prettier'], 12 | rules: { 13 | 'react/prop-types': 0 14 | }, 15 | settings: { 16 | react: { 17 | version: 'detect' 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Compiled files for DEV 11 | dist 12 | 13 | # Dependency directory 14 | node_modules 15 | 16 | # Bower Components 17 | /lib 18 | 19 | #JSDOC 20 | doc 21 | 22 | # IDEA 23 | .idea 24 | *.iml 25 | 26 | # Window 27 | Thumbs.db 28 | Desktop.ini 29 | 30 | # MACgi 31 | .DS_Store 32 | 33 | # eclipse 34 | .project 35 | .metadata 36 | 37 | #report / screenshots 38 | report 39 | screenshots 40 | 41 | # Atom 42 | tags 43 | .ctags 44 | .tern-project 45 | 46 | # etc 47 | .agignore -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "bracketSpacing": false, 5 | "arrowParens": "always" 6 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "stories": [ 3 | "../stories/*.stories.@(js|jsx|ts|tsx)" 4 | ], 5 | "addons": [ 6 | "@storybook/addon-essentials", 7 | ], 8 | "framework": "@storybook/react" 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import 'tui-time-picker/dist/tui-time-picker.css'; 2 | import 'tui-date-picker/dist/tui-date-picker.css'; 3 | import 'tui-calendar/dist/tui-calendar.css'; 4 | import '../stories/app.css'; 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dl_javascript@nhn.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to TOAST UI 2 | 3 | First off, thanks for taking the time to contribute! 🎉 😘 ✨ 4 | 5 | The following is a set of guidelines for contributing to TOAST UI. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## Reporting Bugs 8 | Bugs are tracked as GitHub issues. Search the list and try reproduce on [demo][demo] before you create an issue. When you create an issue, please provide the following information by filling in the template. 9 | 10 | Explain the problem and include additional details to help maintainers reproduce the problem: 11 | 12 | * **Use a clear and descriptive title** for the issue to identify the problem. 13 | * **Describe the exact steps which reproduce the problem** in as many details as possible. Don't just say what you did, but explain how you did it. For example, if you moved the cursor to the end of a line, explain if you used a mouse or a keyboard. 14 | * **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets on the issue, use Markdown code blocks. 15 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 16 | * **Explain which behavior you expected to see instead and why.** 17 | * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. 18 | 19 | ## Suggesting Enhancements 20 | In case you want to suggest for TOAST UI Calendar, please follow this guideline to help maintainers and the community understand your suggestion. 21 | Before creating suggestions, please check [issue list](../../labels/feature%20request) if there's already a request. 22 | 23 | Create an issue and provide the following information: 24 | 25 | * **Use a clear and descriptive title** for the issue to identify the suggestion. 26 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 27 | * **Provide specific examples to demonstrate the steps.** Include copy/pasteable snippets which you use in those examples, as Markdown code blocks. 28 | * **Include screenshots and animated GIFs** which helps demonstrate the steps or point out the part of TOAST UI Calendar which the suggestion is related to. 29 | * **Explain why this enhancement would be useful** to most TOAST UI users. 30 | * **List some other calendars or applications where this enhancement exists.** 31 | 32 | ## First Code Contribution 33 | 34 | Unsure where to begin contributing to TOAST UI? You can start by looking through these `document`, `good first issue` and `help wanted` issues: 35 | 36 | * **document issues**: issues which should be reviewed or improved. 37 | * **good first issues**: issues which should only require a few lines of code, and a test or two. 38 | * **help wanted issues**: issues which should be a bit more involved than beginner issues. 39 | 40 | ## Pull Requests 41 | 42 | ### Development WorkFlow 43 | - Set up your development environment 44 | - Make change from a right branch 45 | - Be sure the code passes `npm run lint`, `npm run test` 46 | - Make a pull request 47 | 48 | ### Development environment 49 | - Prepare your machine node and it's packages installed. 50 | - Checkout our repository 51 | - Install dependencies by `npm install && bower install` 52 | - Start webpack-dev-server by `npm run serve` 53 | 54 | ### Make changes 55 | #### Checkout a branch 56 | - **develop**: PR base branch. merge features, updates for next minor or major release 57 | - **master**: bug fix or document update for next patch release. develop branch will rebase every time master branch update. so keep code change to a minimum. 58 | - **production**: lastest release branch with distribution files. never make a PR on this 59 | - **gh-pages**: API docs, examples and demo 60 | 61 | #### Check Code Style 62 | Run `npm run eslint` and make sure all the tests pass. 63 | 64 | #### Test 65 | Run `npm run test` and verify all the tests pass. 66 | If you are adding new commands or features, they must include tests. 67 | If you are changing functionality, update the tests if you need to. 68 | 69 | #### Commit 70 | Follow our [commit message conventions](./docs/COMMIT_MESSAGE_CONVENTION.md). 71 | 72 | ### Yes! Pull request 73 | Make your pull request, then describe your changes. 74 | #### Title 75 | Follow other PR title format on below. 76 | ``` 77 | : Short Description (fix #111) 78 | : Short Description (fix #123, #111, #122) 79 | : Short Description (ref #111) 80 | ``` 81 | * capitalize first letter of Type 82 | * use present tense: 'change' not 'changed' or 'changes' 83 | 84 | #### Description 85 | If it has related to issues, add links to the issues(like `#123`) in the description. 86 | Fill in the [Pull Request Template](./docs/PULL_REQUEST_TEMPLATE.md) by check your case. 87 | 88 | ## Code of Conduct 89 | This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to dl_javascript@nhn.com. 90 | 91 | > This Guide is base on [atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), [CocoaPods](http://guides.cocoapods.org/contributing/contribute-to-cocoapods.html) and [ESLint](http://eslint.org/docs/developer-guide/contributing/pull-requests) 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 NHN Cloud Corp. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Notice: This repository is deprecated️️️️️ 2 | 3 | TOAST UI Calendar for React has been managed separately from the TOAST UI Calendar repository. As a result of the distribution of these issues, we decided to deprecate each wrapper repository and manage repository as a [mono-repo](https://en.wikipedia.org/wiki/Monorepo) from the [TOAST UI Calendar repository](https://github.com/nhn/tui.calendar/tree/main/apps/react-calendar). 4 | 5 | From now on, please submit issues or contributions related to TOAST UI Calendar for React to [TOAST UI Calendar repository](https://github.com/nhn/tui.calendar/tree/main/apps/react-calendar). Thank you 🙂 6 | 7 | # TOAST UI Calendar for React 8 | 9 | > This is a React component wrapping [TOAST UI Calendar](https://github.com/nhn/tui.calendar). 10 | 11 | [![github version](https://img.shields.io/github/release/nhnent/toast-ui.react-calendar.svg)](https://github.com/nhn/toast-ui.react-calendar/releases/latest) 12 | [![npm version](https://img.shields.io/npm/v/@toast-ui/react-calendar.svg)](https://www.npmjs.com/package/@toast-ui/react-calendar) 13 | [![license](https://img.shields.io/github/license/nhnent/toast-ui.vue-calendar.svg)](https://github.com/nhn/toast-ui.react-calendar/blob/master/LICENSE) 14 | [![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/toast-ui.react-calendar/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) 15 | [![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_Cloud-ff1414.svg)](https://github.com/nhn) 16 | 17 | ## 🚩 Table of Contents 18 | * [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source) 19 | * [Install](#-install) 20 | * [Using npm](#using-npm) 21 | * [Usage](#-usage) 22 | * [Import](#Import) 23 | * [Props](#props) 24 | * [Instance Methods](#Instance-Methods) 25 | * [Getting the root element](#Getting-the-root-element) 26 | * [Event](#event) 27 | * [Pull Request Steps](#-pull-request-steps) 28 | * [Documents](#-documents) 29 | * [Contributing](#-contributing) 30 | * [License](#-license) 31 | 32 | ## Collect statistics on the use of open source 33 | 34 | React Wrapper of TOAST UI Calendar applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Calendar is used throughout the world. It also serves as important index to determine the future course of projects. location.hostname (e.g. > “ui.toast.com") is to be collected and the sole purpose is nothing but to measure statistics on the usage. To disable GA, use the `usageStatistics` props like the example below. 35 | 36 | ```js 37 | 40 | ``` 41 | 42 | Or, include `tui-code-snippet.js` (**v1.4.0** or **later**) and then immediately write the options as follows: 43 | 44 | ```js 45 | tui.usageStatistics = false; 46 | ``` 47 | 48 | ## 💾 Install 49 | 50 | ### Using npm 51 | 52 | ```sh 53 | npm install --save @toast-ui/react-calendar 54 | ``` 55 | 56 | ## 📊 Usage 57 | We provide a [simple example](https://codesandbox.io/s/toast-uireact-calendar-82fi9) and you can start right away. 58 | 59 | ### Import 60 | 61 | You can use Toast UI Calendar for React as a ECMAScript module or a CommonJS module. As this module does not contain CSS files, you should import `tui-calendar.css` from `tui-calendar/dist` manually. 62 | 63 | * Using ECMAScript module 64 | 65 | ```js 66 | import Calendar from '@toast-ui/react-calendar'; 67 | import 'tui-calendar/dist/tui-calendar.css'; 68 | 69 | // If you use the default popups, use this. 70 | import 'tui-date-picker/dist/tui-date-picker.css'; 71 | import 'tui-time-picker/dist/tui-time-picker.css'; 72 | ``` 73 | 74 | * Using CommonJS module 75 | 76 | ```js 77 | var Calendar = require('@toast-ui/react-calendar'); 78 | require('tui-calendar/dist/tui-calendar.css'); 79 | 80 | // If you use the default popups, use this. 81 | require('tui-date-picker/dist/tui-date-picker.css'); 82 | require('tui-time-picker/dist/tui-time-picker.css'); 83 | ``` 84 | 85 | ### Props 86 | We are supported in the form of props for [Options of TOAST UI Calendar](https://nhn.github.io/tui.calendar/latest/Options). Each name of props is same options of Toast UI Calendar except `view` is for `defaultView` of option. Additionally you can set schedules using `schedules` of prop. 87 | 88 | ```js 89 | const myTheme = { 90 | // Theme object to extends default dark theme. 91 | }; 92 | 93 | const MyComponent = () => ( 94 | ${ 161 | schedule.title 162 | }`; 163 | }, 164 | milestoneTitle() { 165 | return 'Milestone'; 166 | }, 167 | allday(schedule) { 168 | return `${schedule.title}`; 169 | }, 170 | alldayTitle() { 171 | return 'All Day'; 172 | } 173 | }} 174 | theme={myTheme} 175 | timezones={[ 176 | { 177 | timezoneOffset: 540, 178 | displayLabel: 'GMT+09:00', 179 | tooltip: 'Seoul' 180 | }, 181 | { 182 | timezoneOffset: -420, 183 | displayLabel: 'GMT-08:00', 184 | tooltip: 'Los Angeles' 185 | } 186 | ]} 187 | useDetailPopup 188 | useCreationPopup 189 | view={selectedView} // You can also set the `defaultView` option. 190 | week={{ 191 | showTimezoneCollapseButton: true, 192 | timezonesCollapsed: true 193 | }} 194 | /> 195 | ); 196 | ``` 197 | 198 | 199 | #### Theme 200 | 201 | You can write your own theme object. [Link - See "themeConfig"](https://nhn.github.io/tui.calendar/latest/themeConfig) 202 | 203 | #### ⚠️ Note for passing props 204 | 205 | The calendar component check deep equality of `props` when re-rendered. However, for performance and to avoid unnecessary re-rendering, it's recommended to extract props to the outside of the component or memoize them with `useMemo` when props don't have to be affected by component state changes. 206 | 207 | For more information, check [this issue](https://github.com/nhn/toast-ui.react-calendar/issues/26#issuecomment-906929298). 208 | 209 | ```jsx 210 | const calendars = [ 211 | { 212 | id: '0', 213 | name: 'Private', 214 | bgColor: '#9e5fff', 215 | borderColor: '#9e5fff' 216 | }, 217 | { 218 | id: '1', 219 | name: 'Company', 220 | bgColor: '#00a9ff', 221 | borderColor: '#00a9ff' 222 | } 223 | ]; 224 | 225 | // Especially avoid to declare the `template` prop inside the component. 226 | const template = { 227 | milestone(schedule) { 228 | return `${ 229 | schedule.title 230 | }`; 231 | }, 232 | milestoneTitle() { 233 | return 'Milestone'; 234 | }, 235 | allday(schedule) { 236 | return `${schedule.title}`; 237 | }, 238 | alldayTitle() { 239 | return 'All Day'; 240 | } 241 | }; 242 | 243 | function MyCalendar() { 244 | // ... 245 | 246 | return ( 247 | 252 | ) 253 | } 254 | ``` 255 | 256 | ### Instance Methods 257 | 258 | For using [instance methods of TOAST UI Calendar](https://nhn.github.io/tui.calendar/latest/Calendar), first thing to do is creating Refs of wrapper component using [`createRef()`](https://reactjs.org/docs/refs-and-the-dom.html#creating-refs). But the wrapper component does not provide a way to call instance methods of TOAST UI Calendar directly. Instead, you can call `getInstance()` method of the wrapper component to get the instance, and call the methods on it. 259 | 260 | ```js 261 | const calendarOptions = { 262 | // sort of option properties. 263 | }; 264 | 265 | class MyComponent extends React.Component { 266 | calendarRef = React.createRef(); 267 | 268 | handleClickNextButton = () => { 269 | const calendarInstance = this.calendarRef.current.getInstance(); 270 | 271 | calendarInstance.next(); 272 | }; 273 | 274 | render() { 275 | return ( 276 | <> 277 | 281 | 282 | 283 | ); 284 | } 285 | } 286 | ``` 287 | 288 | ### Getting the root element 289 | 290 | An instance of the wrapper component also provides a handy method for getting the root element. If you want to manipulate the root element directly, you can call `getRootElement` to get the element. 291 | 292 | ```js 293 | class MyComponent extends React.Component { 294 | calendarRef = React.createRef(); 295 | 296 | handleClickButton = () => { 297 | this.calendarRef.current.getRootElement().classList.add('calendar-root'); 298 | }; 299 | 300 | render() { 301 | return ( 302 | <> 303 | 307 | 308 | 309 | ); 310 | } 311 | } 312 | ``` 313 | 314 | ### Event 315 | 316 | [All the events of TOAST UI Calendar](https://nhn.github.io/tui.calendar/latest/Calendar#event-afterRenderSchedule) are supported in the form of `on[EventName]` props. The first letter of each event name should be capitalized. For example, for using `mousedown` event you can use `onMousedown` prop like the example below. 317 | 318 | ```js 319 | class MyComponent extends React.Component { 320 | handleClickDayname = (ev) => { 321 | // view : week, day 322 | console.group('onClickDayname'); 323 | console.log(ev.date); 324 | console.groupEnd(); 325 | }; 326 | 327 | render() { 328 | return ( 329 | 332 | ); 333 | } 334 | } 335 | ``` 336 | 337 | ## 🔧 Pull Request Steps 338 | 339 | TOAST UI products are open source, so you can create a pull request(PR) after you fix issues. 340 | Run npm scripts and develop yourself with the following process. 341 | 342 | ### Setup 343 | 344 | Fork `develop` branch into your personal repository. 345 | Clone it to local computer. Install node modules. 346 | Before starting development, you should check to haveany errors. 347 | 348 | ``` sh 349 | $ git clone https://github.com/{your-personal-repo}/[[repo name]].git 350 | $ cd [[repo name]] 351 | $ npm install 352 | ``` 353 | 354 | ### Develop 355 | 356 | Let's start development! 357 | 358 | ### Pull Request 359 | 360 | Before PR, check to test lastly and then check any errors. 361 | If it has no error, commit and then push it! 362 | 363 | For more information on PR's step, please see links of Contributing section. 364 | 365 | ## 💬 Contributing 366 | * [Code of Conduct](https://github.com/nhn/toast-ui.react-calendar/blob/master/CODE_OF_CONDUCT.md) 367 | * [Contributing guideline](https://github.com/nhn/toast-ui.react-calendar/blob/master/CONTRIBUTING.md) 368 | * [Commit convention](https://github.com/nhn/toast-ui.react-calendar/blob/master/docs/COMMIT_MESSAGE_CONVENTION.md) 369 | 370 | ## 📜 License 371 | This software is licensed under the [MIT](./LICENSE) © [NHN Cloud](https://github.com/nhn). 372 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | 4 | return { 5 | presets: [ 6 | [ 7 | '@babel/preset-env', 8 | { 9 | modules: false, 10 | useBuiltIns: 'entry' 11 | } 12 | ], 13 | '@babel/preset-react' 14 | ], 15 | plugins: [ 16 | '@babel/plugin-proposal-class-properties', 17 | '@babel/plugin-proposal-object-rest-spread' 18 | ] 19 | }; 20 | }; -------------------------------------------------------------------------------- /docs/COMMIT_MESSAGE_CONVENTION.md: -------------------------------------------------------------------------------- 1 | # Commit Message Convention 2 | 3 | ## Commit Message Format 4 | 5 | ``` 6 | : Short description (fix #1234) 7 | 8 | Logger description here if necessary 9 | 10 | BREAKING CHANGE: only contain breaking change 11 | ``` 12 | * Any line of the commit message cannot be longer 100 characters! 13 | 14 | ## Revert 15 | ``` 16 | revert: commit 17 | 18 | This reverts commit 19 | More description if needed 20 | ``` 21 | 22 | ## Type 23 | Must be one of the following: 24 | 25 | * **feat**: A new feature 26 | * **fix**: A bug fix 27 | * **docs**: Documentation only changes 28 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 29 | * **refactor**: A code change that neither fixes a bug nor adds a feature 30 | * **perf**: A code change that improves performance 31 | * **test**: Adding missing or correcting existing tests 32 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 33 | 34 | ## Subject 35 | * use the imperative, __present__ tense: "change" not "changed" nor "changes" 36 | * don't capitalize the first letter 37 | * no dot (.) at the end 38 | * reference GitHub issues at the end. If the commit doesn’t completely fix the issue, then use `(refs #1234)` instead of `(fixes #1234)`. 39 | 40 | ## Body 41 | 42 | * use the imperative, __present__ tense: "change" not "changed" nor "changes". 43 | * the motivation for the change and contrast this with previous behavior. 44 | 45 | ## BREAKING CHANGE 46 | * This commit contains breaking change(s). 47 | * start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this. 48 | 49 | This convention is based on [AngularJS](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) and [ESLint](https://eslint.org/docs/developer-guide/contributing/pull-requests#step2) 50 | -------------------------------------------------------------------------------- /docs/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | ## Version 11 | 12 | 13 | ## Test Environment 14 | 15 | 16 | ## Current Behavior 17 | 19 | 20 | ```js 21 | // Write example code 22 | ``` 23 | 24 | ## Expected Behavior 25 | 26 | -------------------------------------------------------------------------------- /docs/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 27 | 28 | ### Please check if the PR fulfills these requirements 29 | - [ ] It's the right issue type on the title 30 | - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number) 31 | - [ ] The commit message follows our guidelines 32 | - [ ] Tests for the changes have been added (for bug fixes/features) 33 | - [ ] Docs have been added/updated (for bug fixes/features) 34 | - [ ] It does not introduce a breaking change or has a description of the breaking change 35 | 36 | ### Description 37 | 38 | 39 | 40 | --- 41 | Thank you for your contribution to TOAST UI product. 🎉 😘 ✨ 42 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {HTMLAttributes, Component} from "react"; 2 | 3 | import TuiCalendar, { 4 | ISchedule, 5 | IEvents, 6 | IOptions 7 | } from 'tui-calendar'; 8 | 9 | 10 | type EventNameMapping = { 11 | onAfterRenderSchedule: "afterRenderSchedule"; 12 | onBeforeCreateSchedule: "beforeCreateSchedule"; 13 | onBeforeDeleteSchedule: "beforeDeleteSchedule"; 14 | onBeforeUpdateSchedule: "beforeUpdateSchedule"; 15 | onClickDayname: "clickDayname"; 16 | onClickMore: "clickMore"; 17 | onClickSchedule: "clickSchedule"; 18 | onClickTimezonesCollapseBtn: "clickTimezonesCollapseBtn"; 19 | }; 20 | 21 | type EventMaps = { 22 | [K in keyof EventNameMapping]?: IEvents[EventNameMapping[K]] 23 | }; 24 | 25 | type Props = IOptions & EventMaps & { 26 | height: string; 27 | view?: string; 28 | schedules?: ISchedule[]; 29 | } & HTMLAttributes; 30 | 31 | export default class Calendar extends Component { 32 | public getInstance(): TuiCalendar; 33 | public getRootElement(): HTMLElement; 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@toast-ui/react-calendar", 3 | "version": "1.0.6", 4 | "description": "React wrapper component for tui.calendar", 5 | "main": "dist/toastui-react-calendar.js", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "dist", 9 | "src", 10 | "index.d.ts" 11 | ], 12 | "scripts": { 13 | "build:dev": "webpack --mode development", 14 | "build": "webpack --mode production", 15 | "serve": "webpack-dev-server", 16 | "storybook": "start-storybook -p 6006", 17 | "build-storybook": "build-storybook", 18 | "release-note": "tuie" 19 | }, 20 | "homepage": "https://github.com/nhn/toast-ui.react-calendar", 21 | "bugs": "https://github.com/nhn/toast-ui.react-calendar/issues", 22 | "author": "NHN Cloud. FE Development Lab ", 23 | "repository": "https://github.com/nhn/toast-ui.react-calendar.git", 24 | "license": "MIT", 25 | "browserslist": ["last 2 versions", "ie 9"], 26 | "peerDependencies": { 27 | "react": "^16.3.0" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.2.2", 31 | "@babel/plugin-proposal-class-properties": "^7.2.3", 32 | "@babel/plugin-proposal-object-rest-spread": "^7.2.0", 33 | "@babel/preset-env": "^7.2.3", 34 | "@babel/preset-react": "^7.0.0", 35 | "@storybook/addon-actions": "^6.4.14", 36 | "@storybook/addon-essentials": "^6.4.14", 37 | "@storybook/react": "^6.4.14", 38 | "babel-eslint": "^10.0.1", 39 | "babel-loader": "^8.0.5", 40 | "css-loader": "^2.1.0", 41 | "eslint": "^5.12.1", 42 | "eslint-config-prettier": "^3.6.0", 43 | "eslint-config-tui": "^2.1.0", 44 | "eslint-plugin-prettier": "^3.0.1", 45 | "eslint-plugin-react": "^7.12.4", 46 | "eslint-plugin-storybook": "^0.5.6", 47 | "prettier": "^1.16.0", 48 | "react": "^16.7.0", 49 | "react-dom": "^16.7.0", 50 | "style-loader": "^0.23.1", 51 | "tui-release-notes": "git+https://github.com/nhn/toast-ui.release-notes.git", 52 | "webpack": "^4.46.0", 53 | "webpack-cli": "^3.3.12", 54 | "webpack-dev-server": "^3.11.3" 55 | }, 56 | "dependencies": { 57 | "tui-calendar": "^1.15.2" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview TOAST UI Calendar React wrapper component 3 | */ 4 | import React from 'react'; 5 | import TuiCalendar from 'tui-calendar'; 6 | import {isEqual} from './isEqual'; 7 | 8 | /** 9 | * Calendar's options prop 10 | * @type {string[]} 11 | */ 12 | const optionProps = [ 13 | 'disableDblClick', 14 | 'isReadOnly', 15 | 'month', 16 | 'scheduleView', 17 | 'taskView', 18 | 'theme', 19 | 'timezones', 20 | 'week', 21 | 'template' 22 | ]; 23 | 24 | export default class Calendar extends React.Component { 25 | rootEl = React.createRef(); 26 | 27 | static defaultProps = { 28 | height: '800px', 29 | view: 'week' 30 | }; 31 | 32 | calendarInst = null; 33 | 34 | componentDidMount() { 35 | const {schedules = [], view, ...restProps} = this.props; 36 | const rootElement = this.getRootElement(); 37 | 38 | this.calendarInst = new TuiCalendar(rootElement, { 39 | ...restProps, 40 | defaultView: view 41 | }); 42 | 43 | rootElement.style.height = this.props.height; 44 | 45 | this.setSchedules(schedules); 46 | 47 | this.bindEventHandlers(restProps); 48 | } 49 | 50 | shouldComponentUpdate(nextProps) { 51 | const {calendars, height, schedules, theme, view} = this.props; 52 | 53 | if (!isEqual(height, nextProps.height)) { 54 | this.getRootElement().style.height = nextProps.height; 55 | } 56 | 57 | if (!isEqual(calendars, nextProps.calendars)) { 58 | this.setCalendars(nextProps.calendars); 59 | } 60 | 61 | if (!isEqual(schedules, nextProps.schedules)) { 62 | this.calendarInst.clear(); 63 | this.setSchedules(nextProps.schedules); 64 | } 65 | 66 | if (!isEqual(theme, nextProps.theme)) { 67 | this.calendarInst.setTheme(this.cloneData(nextProps.theme)); 68 | } 69 | 70 | if (!isEqual(view, nextProps.view)) { 71 | this.calendarInst.changeView(nextProps.view); 72 | } 73 | 74 | optionProps.forEach((key) => { 75 | if (!isEqual(this.props[key], nextProps[key])) { 76 | this.setOptions(key, nextProps[key]); 77 | } 78 | }); 79 | 80 | this.bindEventHandlers(this.props); 81 | 82 | return false; 83 | } 84 | 85 | componentWillUnmount() { 86 | this.calendarInst.destroy(); 87 | } 88 | 89 | cloneData(data) { 90 | return JSON.parse(JSON.stringify(data)); 91 | } 92 | 93 | setCalendars(calendars) { 94 | if (calendars && calendars.length) { 95 | this.calendarInst.setCalendars(calendars); 96 | } 97 | } 98 | 99 | setSchedules(schedules) { 100 | if (schedules && schedules.length) { 101 | const clonedSchedules = this.cloneData(schedules); 102 | 103 | this.calendarInst.createSchedules(clonedSchedules); 104 | } 105 | } 106 | 107 | setOptions(propKey, prop) { 108 | this.calendarInst.setOptions({[propKey]: prop}, true); 109 | } 110 | 111 | getInstance() { 112 | return this.calendarInst; 113 | } 114 | 115 | getRootElement() { 116 | return this.rootEl.current; 117 | } 118 | 119 | bindEventHandlers = (props) => { 120 | const eventHandlerNames = Object.keys(props).filter((key) => /^on[A-Z][a-zA-Z]+/.test(key)); 121 | 122 | eventHandlerNames.forEach((key) => { 123 | const eventName = key[2].toLowerCase() + key.slice(3); 124 | this.calendarInst.off(eventName); 125 | this.calendarInst.on(eventName, props[key]); 126 | }); 127 | }; 128 | 129 | render() { 130 | return
; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/isEqual.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable complexity,prefer-destructuring */ 2 | 3 | const hasOwnProp = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); 4 | 5 | /** 6 | * Compare two objects deeply 7 | * 8 | * It doesn't care about some types like RegExp, Map, Set etc. 9 | * Because they are not proper types for props of the Calendar component. 10 | * 11 | * Comparing two functions with `toString` is not completely reliable 12 | * because their closure variables might not the same. 13 | * 14 | * @param {any} a the first object 15 | * @param {any} b the second object to compare 16 | * @returns {boolean} 17 | */ 18 | export function isEqual(a, b) { 19 | if (a === b) { 20 | return true; 21 | } 22 | 23 | if (a instanceof Function) { 24 | return a.toString() === b.toString(); 25 | } 26 | 27 | if (a && b && typeof a === 'object' && typeof b === 'object') { 28 | let length; 29 | 30 | if (a.constructor !== b.constructor) { 31 | return false; 32 | } 33 | if (Array.isArray(a)) { 34 | length = a.length; 35 | if (length !== b.length) { 36 | return false; 37 | } 38 | for (let i = 0; i < length; i += 1) { 39 | // eslint-disable-next-line max-depth 40 | if (!isEqual(a[i], b[i])) { 41 | return false; 42 | } 43 | } 44 | 45 | return true; 46 | } 47 | if (a.valueOf !== Object.prototype.valueOf) { 48 | return a.valueOf() === b.valueOf(); 49 | } 50 | if (a.toString !== Object.prototype.toString) { 51 | return a.toString() === b.toString(); 52 | } 53 | const keys = Object.keys(a); 54 | length = keys.length; 55 | if (length !== Object.keys(b).length) { 56 | return false; 57 | } 58 | for (let i = 0; i < length; i += 1) { 59 | const key = keys[i]; 60 | if (!hasOwnProp(b, key) || !isEqual(a[key], b[key])) { 61 | return false; 62 | } 63 | } 64 | 65 | return true; 66 | } 67 | 68 | // eslint-disable-next-line no-self-compare 69 | return a !== a && b !== b; 70 | } 71 | -------------------------------------------------------------------------------- /stories/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Noto Sans', sans-serif; 3 | color: '#333'; 4 | font-size: 12px; 5 | } 6 | 7 | /** custom bootstrap - start */ 8 | .btn { 9 | border-radius: 25px; 10 | border-color: #ddd; 11 | } 12 | 13 | .btn:hover { 14 | border: solid 1px #bbb; 15 | background-color: #fff; 16 | } 17 | 18 | .btn:active { 19 | background-color: #f9f9f9; 20 | border: solid 1px #bbb; 21 | outline: none; 22 | } 23 | 24 | .btn:disabled { 25 | background-color: #f9f9f9; 26 | border: solid 1px #ddd; 27 | color: #bbb; 28 | } 29 | 30 | .btn:focus:active, 31 | .btn:focus, 32 | .btn:active { 33 | outline: none; 34 | } 35 | 36 | .open > .dropdown-toggle.btn-default { 37 | background-color: #fff; 38 | } 39 | 40 | .dropdown-menu { 41 | top: 25px; 42 | padding: 3px 0; 43 | border-radius: 2px; 44 | border: 1px solid #bbb; 45 | } 46 | 47 | .dropdown-menu > li > a { 48 | padding: 9px 12px; 49 | cursor: pointer; 50 | } 51 | 52 | .dropdown-menu > li > a:hover { 53 | background-color: rgba(81, 92, 230, 0.05); 54 | color: #333; 55 | } 56 | 57 | .bi15 { 58 | width: 15px; 59 | height: 15px; 60 | } 61 | 62 | /** custom fontawesome - end */ 63 | 64 | .calendar-icon { 65 | width: 14px; 66 | height: 14px; 67 | } 68 | 69 | #top { 70 | height: 49px; 71 | border-bottom: 1px solid #bbb; 72 | padding: 16px; 73 | font-size: 10px; 74 | } 75 | 76 | #lnb { 77 | position: absolute; 78 | width: 200px; 79 | top: 49px; 80 | bottom: 0; 81 | border-right: 1px solid #d5d5d5; 82 | padding: 12px 10px; 83 | background: #fafafa; 84 | } 85 | 86 | #right { 87 | position: absolute; 88 | left: 200px; 89 | top: 49px; 90 | right: 0; 91 | bottom: 0; 92 | } 93 | 94 | #lnb label { 95 | margin-bottom: 0; 96 | cursor: pointer; 97 | } 98 | 99 | .lnb-new-schedule { 100 | padding-bottom: 12px; 101 | border-bottom: 1px solid #e5e5e5; 102 | } 103 | 104 | .lnb-new-schedule-btn { 105 | height: 100%; 106 | font-size: 14px; 107 | background-color: #ff6618; 108 | color: #ffffff; 109 | border: 0; 110 | border-radius: 25px; 111 | padding: 10px 20px; 112 | font-weight: bold; 113 | } 114 | 115 | .lnb-new-schedule-btn:hover { 116 | height: 100%; 117 | font-size: 14px; 118 | background-color: #e55b15; 119 | color: #ffffff; 120 | border: 0; 121 | border-radius: 25px; 122 | padding: 10px 20px; 123 | font-weight: bold; 124 | } 125 | 126 | .lnb-new-schedule-btn:active { 127 | height: 100%; 128 | font-size: 14px; 129 | background-color: #d95614; 130 | color: #ffffff; 131 | border: 0; 132 | border-radius: 25px; 133 | padding: 10px 20px; 134 | font-weight: bold; 135 | } 136 | 137 | .lnb-calendars > div { 138 | padding: 12px 16px; 139 | border-bottom: 1px solid #e5e5e5; 140 | font-weight: normal; 141 | } 142 | 143 | .lnb-calendars-d1 { 144 | padding-left: 8px; 145 | } 146 | 147 | .lnb-calendars-d1 label { 148 | font-weight: normal; 149 | } 150 | 151 | .lnb-calendars-item { 152 | min-height: 14px; 153 | line-height: 14px; 154 | padding: 8px 0; 155 | } 156 | 157 | .lnb-footer { 158 | color: #999; 159 | font-size: 11px; 160 | position: absolute; 161 | bottom: 12px; 162 | padding-left: 16px; 163 | } 164 | 165 | #menu { 166 | padding: 16px; 167 | } 168 | 169 | #dropdownMenu-calendarType { 170 | padding: 0 8px 0 11px; 171 | } 172 | 173 | #calendarTypeName { 174 | min-width: 62px; 175 | display: inline-block; 176 | text-align: left; 177 | line-height: 30px; 178 | } 179 | 180 | .move-today { 181 | padding: 0 16px; 182 | line-height: 30px; 183 | } 184 | 185 | .move-day { 186 | padding: 8px; 187 | } 188 | 189 | .render-range { 190 | padding-left: 12px; 191 | font-size: 19px; 192 | vertical-align: middle; 193 | } 194 | 195 | .dropdown-menu-title .calendar-icon { 196 | margin-right: 8px; 197 | } 198 | 199 | .calendar-bar { 200 | width: 16px; 201 | height: 16px; 202 | margin-right: 5px; 203 | display: inline-block; 204 | border: 1px solid #eee; 205 | vertical-align: middle; 206 | } 207 | 208 | .calendar-name { 209 | font-size: 14px; 210 | font-weight: bold; 211 | vertical-align: middle; 212 | } 213 | 214 | .schedule-time { 215 | color: #005aff; 216 | } 217 | 218 | #calendar { 219 | position: absolute; 220 | left: 0; 221 | right: 0; 222 | bottom: 5px; 223 | top: 64px; 224 | } 225 | 226 | /** custom fontawesome */ 227 | .fa { 228 | width: 12px; 229 | height: 12px; 230 | margin-right: 2px; 231 | } 232 | 233 | .tui-full-calendar-month-week-item 234 | .tui-full-calendar-today 235 | .tui-full-calendar-weekday-grid-date-decorator { 236 | background-color: #009688; 237 | } 238 | -------------------------------------------------------------------------------- /stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Calendar from '../src/index'; 4 | import myTheme from './myTheme'; 5 | 6 | const today = new Date(); 7 | const getDate = (type, start, value, operator) => { 8 | start = new Date(start); 9 | type = type.charAt(0).toUpperCase() + type.slice(1); 10 | 11 | if (operator === '+') { 12 | start[`set${type}`](start[`get${type}`]() + value); 13 | } else { 14 | start[`set${type}`](start[`get${type}`]() - value); 15 | } 16 | 17 | return start; 18 | }; 19 | 20 | const viewModeOptions = [ 21 | { 22 | title: 'Monthly', 23 | value: 'month' 24 | }, 25 | { 26 | title: 'Weekly', 27 | value: 'week' 28 | }, 29 | { 30 | title: 'Daily', 31 | value: 'day' 32 | } 33 | ]; 34 | 35 | class ClassComponent extends React.Component { 36 | ref = React.createRef(); 37 | 38 | calendarInst = null; 39 | 40 | state = { 41 | dateRange: '', 42 | view: 'week' 43 | }; 44 | 45 | componentDidMount() { 46 | this.calendarInst = this.ref.current.getInstance(); 47 | this.setState({view: this.props.view}); 48 | 49 | this.setRenderRangeText(); 50 | } 51 | 52 | onAfterRenderSchedule(res) { 53 | console.group('onAfterRenderSchedule'); 54 | console.log('Schedule Info : ', res.schedule); 55 | console.groupEnd(); 56 | } 57 | 58 | onBeforeDeleteSchedule(res) { 59 | console.group('onBeforeDeleteSchedule'); 60 | console.log('Schedule Info : ', res.schedule); 61 | console.groupEnd(); 62 | 63 | const {id, calendarId} = res.schedule; 64 | 65 | this.calendarInst.deleteSchedule(id, calendarId); 66 | } 67 | 68 | onChangeSelect(ev) { 69 | this.setState({view: ev.target.value}); 70 | 71 | this.setRenderRangeText(); 72 | } 73 | 74 | onClickDayname(res) { 75 | // view : week, day 76 | console.group('onClickDayname'); 77 | console.log(res.date); 78 | console.groupEnd(); 79 | } 80 | 81 | onClickNavi(event) { 82 | if (event.target.tagName === 'BUTTON') { 83 | const {target} = event; 84 | let action = target.dataset ? target.dataset.action : target.getAttribute('data-action'); 85 | action = action.replace('move-', ''); 86 | 87 | this.calendarInst[action](); 88 | this.setRenderRangeText(); 89 | } 90 | } 91 | 92 | onClickSchedule(res) { 93 | console.group('onClickSchedule'); 94 | console.log('MouseEvent : ', res.event); 95 | console.log('Calendar Info : ', res.calendar); 96 | console.log('Schedule Info : ', res.schedule); 97 | console.groupEnd(); 98 | } 99 | 100 | onClickTimezonesCollapseBtn(timezonesCollapsed) { 101 | // view : week, day 102 | console.group('onClickTimezonesCollapseBtn'); 103 | console.log('Is Collapsed Timezone? ', timezonesCollapsed); 104 | console.groupEnd(); 105 | 106 | const theme = { 107 | 'week.daygridLeft.width': '100px', 108 | 'week.timegridLeft.width': '100px' 109 | }; 110 | 111 | this.calendarInst.setTheme(theme); 112 | } 113 | 114 | setRenderRangeText() { 115 | const view = this.calendarInst.getViewName(); 116 | const calDate = this.calendarInst.getDate(); 117 | const rangeStart = this.calendarInst.getDateRangeStart(); 118 | const rangeEnd = this.calendarInst.getDateRangeEnd(); 119 | let year = calDate.getFullYear(); 120 | let month = calDate.getMonth() + 1; 121 | let date = calDate.getDate(); 122 | let dateRangeText = ''; 123 | let endMonth, endDate, start, end; 124 | 125 | switch (view) { 126 | case 'month': 127 | dateRangeText = `${year}-${month}`; 128 | break; 129 | case 'week': 130 | year = rangeStart.getFullYear(); 131 | month = rangeStart.getMonth() + 1; 132 | date = rangeStart.getDate(); 133 | endMonth = rangeEnd.getMonth() + 1; 134 | endDate = rangeEnd.getDate(); 135 | 136 | start = `${year}-${month < 10 ? '0' : ''}${month}-${date < 10 ? '0' : ''}${date}`; 137 | end = `${year}-${endMonth < 10 ? '0' : ''}${endMonth}-${endDate < 10 ? '0' : ''}${endDate}`; 138 | dateRangeText = `${start} ~ ${end}`; 139 | break; 140 | default: 141 | dateRangeText = `${year}-${month}-${date}`; 142 | } 143 | 144 | this.setState({dateRange: dateRangeText}); 145 | } 146 | 147 | onBeforeUpdateSchedule(updateData) { 148 | console.group('onBeforeUpdateSchedule'); 149 | console.log(updateData); 150 | console.groupEnd(); 151 | const targetSchedule = updateData.schedule; 152 | const changes = { 153 | start: updateData.start, 154 | end: updateData.end, 155 | ...(updateData.changes || {}) 156 | }; 157 | 158 | this.calendarInst.updateSchedule(targetSchedule.id, targetSchedule.calendarId, changes); 159 | } 160 | 161 | onBeforeCreateSchedule(scheduleData) { 162 | const schedule = { 163 | calendarId: scheduleData.calendarId || '', 164 | id: String(Math.random()), 165 | title: scheduleData.title, 166 | isAllDay: scheduleData.isAllDay, 167 | start: scheduleData.start, 168 | end: scheduleData.end, 169 | category: scheduleData.isAllDay ? 'allday' : 'time', 170 | dueDateClass: '', 171 | location: scheduleData.location, 172 | state: scheduleData.state, 173 | isPrivate: scheduleData.isPrivate 174 | }; 175 | 176 | this.calendarInst.createSchedules([schedule]); 177 | } 178 | 179 | render() { 180 | const {dateRange, view} = this.state; 181 | const selectedView = view || this.props.view; 182 | 183 | return ( 184 |
185 |

🍞📅 TOAST UI Calendar + React.js

186 |
187 | 194 | 195 | 203 | 211 | 219 | 220 | {dateRange} 221 |
222 | ${ 292 | schedule.title 293 | }`; 294 | }, 295 | milestoneTitle() { 296 | return 'Milestone'; 297 | }, 298 | allday(schedule) { 299 | return `${schedule.title}`; 300 | }, 301 | alldayTitle() { 302 | return 'All Day'; 303 | } 304 | }} 305 | theme={myTheme} 306 | timezones={[ 307 | { 308 | timezoneOffset: 540, 309 | displayLabel: 'GMT+09:00', 310 | tooltip: 'Seoul' 311 | }, 312 | { 313 | timezoneOffset: -420, 314 | displayLabel: 'GMT-08:00', 315 | tooltip: 'Los Angeles' 316 | } 317 | ]} 318 | useDetailPopup 319 | useCreationPopup 320 | view={selectedView} 321 | week={{ 322 | showTimezoneCollapseButton: true, 323 | timezonesCollapsed: false 324 | }} 325 | ref={this.ref} 326 | onAfterRenderSchedule={this.onAfterRenderSchedule.bind(this)} 327 | onBeforeDeleteSchedule={this.onBeforeDeleteSchedule.bind(this)} 328 | onClickDayname={this.onClickDayname.bind(this)} 329 | onClickSchedule={this.onClickSchedule.bind(this)} 330 | onClickTimezonesCollapseBtn={this.onClickTimezonesCollapseBtn.bind(this)} 331 | onBeforeUpdateSchedule={this.onBeforeUpdateSchedule.bind(this)} 332 | onBeforeCreateSchedule={this.onBeforeCreateSchedule.bind(this)} 333 | /> 334 |
335 | ); 336 | } 337 | } 338 | 339 | // eslint-disable-next-line require-jsdoc 340 | function FunctionComponent({view}) { 341 | const calendarRef = React.useRef(); 342 | const [selectedDateRangeText, setSelectedDateRangeText] = React.useState(''); 343 | const [selectedView, setSelectedView] = React.useState(view); 344 | const initialCalendars = [ 345 | { 346 | id: '0', 347 | name: 'Private', 348 | bgColor: '#9e5fff', 349 | borderColor: '#9e5fff', 350 | dragBgColor: '#9e5fff' 351 | }, 352 | { 353 | id: '1', 354 | name: 'Company', 355 | bgColor: '#00a9ff', 356 | borderColor: '#00a9ff', 357 | dragBgColor: '#00a9ff' 358 | } 359 | ]; 360 | const initialSchedules = [ 361 | { 362 | id: '1', 363 | calendarId: '0', 364 | title: 'TOAST UI Calendar Study', 365 | category: 'time', 366 | dueDateClass: '', 367 | start: today.toISOString(), 368 | end: getDate('hours', today, 3, '+').toISOString() 369 | }, 370 | { 371 | id: '2', 372 | calendarId: '0', 373 | title: 'Practice', 374 | category: 'milestone', 375 | dueDateClass: '', 376 | start: getDate('date', today, 1, '+').toISOString(), 377 | end: getDate('date', today, 1, '+').toISOString(), 378 | isReadOnly: true 379 | }, 380 | { 381 | id: '3', 382 | calendarId: '0', 383 | title: 'FE Workshop', 384 | category: 'allday', 385 | dueDateClass: '', 386 | start: getDate('date', today, 2, '-').toISOString(), 387 | end: getDate('date', today, 1, '-').toISOString(), 388 | isReadOnly: true 389 | }, 390 | { 391 | id: '4', 392 | calendarId: '0', 393 | title: 'Report', 394 | category: 'time', 395 | dueDateClass: '', 396 | start: today.toISOString(), 397 | end: getDate('hours', today, 1, '+').toISOString() 398 | } 399 | ]; 400 | 401 | const getCalInstance = React.useCallback(() => calendarRef.current.getInstance(), []); 402 | 403 | const updateRenderRangeText = React.useCallback(() => { 404 | const calInstance = getCalInstance(); 405 | if (!calInstance) { 406 | setSelectedDateRangeText(''); 407 | } 408 | 409 | const viewName = calInstance.getViewName(); 410 | const calDate = calInstance.getDate(); 411 | const rangeStart = calInstance.getDateRangeStart(); 412 | const rangeEnd = calInstance.getDateRangeEnd(); 413 | 414 | let year = calDate.getFullYear(); 415 | let month = calDate.getMonth() + 1; 416 | let date = calDate.getDate(); 417 | let dateRangeText = ''; 418 | 419 | switch (viewName) { 420 | case 'month': { 421 | dateRangeText = `${year}-${month}`; 422 | break; 423 | } 424 | case 'week': { 425 | year = rangeStart.getFullYear(); 426 | month = rangeStart.getMonth() + 1; 427 | date = rangeStart.getDate(); 428 | const endMonth = rangeEnd.getMonth() + 1; 429 | const endDate = rangeEnd.getDate(); 430 | 431 | const start = `${year}-${month < 10 ? '0' : ''}${month}-${date < 10 ? '0' : ''}${date}`; 432 | const end = `${year}-${endMonth < 10 ? '0' : ''}${endMonth}-${ 433 | endDate < 10 ? '0' : '' 434 | }${endDate}`; 435 | dateRangeText = `${start} ~ ${end}`; 436 | break; 437 | } 438 | default: 439 | dateRangeText = `${year}-${month}-${date}`; 440 | } 441 | 442 | setSelectedDateRangeText(dateRangeText); 443 | }, []); 444 | 445 | React.useEffect(() => { 446 | setSelectedView(view); 447 | }, [view]); 448 | 449 | React.useEffect(() => { 450 | updateRenderRangeText(); 451 | }, [selectedView, updateRenderRangeText]); 452 | 453 | const onAfterRenderSchedule = (res) => { 454 | console.group('onAfterRenderSchedule'); 455 | console.log('Schedule Info : ', res.schedule); 456 | console.groupEnd(); 457 | }; 458 | 459 | const onBeforeDeleteSchedule = (res) => { 460 | console.group('onBeforeDeleteSchedule'); 461 | console.log('Schedule Info : ', res.schedule); 462 | console.groupEnd(); 463 | 464 | const {id, calendarId} = res.schedule; 465 | 466 | getCalInstance().deleteSchedule(id, calendarId); 467 | }; 468 | 469 | const onChangeSelect = (ev) => { 470 | setSelectedView(ev.target.value); 471 | }; 472 | 473 | const onClickDayName = (res) => { 474 | console.group('onClickDayName'); 475 | console.log('Date : ', res.date); 476 | console.groupEnd(); 477 | }; 478 | 479 | const onClickNavi = (ev) => { 480 | if (ev.target.tagName === 'BUTTON') { 481 | const {target} = ev; 482 | const actionName = target.getAttribute('data-action').replace('move-', ''); 483 | getCalInstance()[actionName](); 484 | updateRenderRangeText(); 485 | } 486 | }; 487 | 488 | const onClickSchedule = (res) => { 489 | console.group('onClickSchedule'); 490 | console.log('MouseEvent : ', res.event); 491 | console.log('Calendar Info : ', res.calendar); 492 | console.log('Schedule Info : ', res.schedule); 493 | console.groupEnd(); 494 | }; 495 | 496 | const onClickTimezonesCollapseBtn = (timezoneCollapsed) => { 497 | console.group('onClickTimezonesCollapseBtn'); 498 | console.log('Is Timezone Collapsed?: ', timezoneCollapsed); 499 | console.groupEnd(); 500 | 501 | const newTheme = { 502 | 'week.daygridLeft.width': '100px', 503 | 'week.timegridLeft.width': '100px' 504 | }; 505 | 506 | getCalInstance().setTheme(newTheme); 507 | }; 508 | 509 | const onBeforeUpdateSchedule = (updateData) => { 510 | console.group('onBeforeUpdateSchedule'); 511 | console.log(updateData); 512 | console.groupEnd(); 513 | const targetSchedule = updateData.schedule; 514 | const changes = { 515 | start: updateData.start, 516 | end: updateData.end, 517 | ...(updateData.changes || {}) 518 | }; 519 | 520 | getCalInstance().updateSchedule(targetSchedule.id, targetSchedule.calendarId, changes); 521 | }; 522 | 523 | const onBeforeCreateSchedule = (scheduleData) => { 524 | const schedule = { 525 | calendarId: scheduleData.calendarId || '', 526 | id: String(Math.random()), 527 | title: scheduleData.title, 528 | isAllDay: scheduleData.isAllDay, 529 | start: scheduleData.start, 530 | end: scheduleData.end, 531 | category: scheduleData.isAllDay ? 'allday' : 'time', 532 | dueDateClass: '', 533 | location: scheduleData.location, 534 | state: scheduleData.state, 535 | isPrivate: scheduleData.isPrivate 536 | }; 537 | 538 | getCalInstance().createSchedules([schedule]); 539 | }; 540 | 541 | return ( 542 |
543 |

🍞📅 TOAST UI Calendar + React.js

544 |
545 | 552 | 553 | 561 | 569 | 577 | 578 | {selectedDateRangeText} 579 |
580 | ${ 596 | schedule.title 597 | }`; 598 | }, 599 | milestoneTitle() { 600 | return 'Milestone'; 601 | }, 602 | allday(schedule) { 603 | return `${schedule.title}`; 604 | }, 605 | alldayTitle() { 606 | return 'All Day'; 607 | } 608 | }} 609 | theme={myTheme} 610 | timezones={[ 611 | { 612 | timezoneOffset: 540, 613 | displayLabel: 'GMT+09:00', 614 | tooltip: 'Seoul' 615 | }, 616 | { 617 | timezoneOffset: -420, 618 | displayLabel: 'GMT-08:00', 619 | tooltip: 'Los Angeles' 620 | } 621 | ]} 622 | useDetailPopup 623 | useCreationPopup 624 | view={selectedView} 625 | week={{ 626 | showTimezoneCollapseButton: true, 627 | timezonesCollapsed: false 628 | }} 629 | ref={calendarRef} 630 | onAfterRenderSchedule={onAfterRenderSchedule} 631 | onBeforeDeleteSchedule={onBeforeDeleteSchedule} 632 | onClickDayname={onClickDayName} 633 | onClickSchedule={onClickSchedule} 634 | onClickTimezonesCollapseBtn={onClickTimezonesCollapseBtn} 635 | onBeforeUpdateSchedule={onBeforeUpdateSchedule} 636 | onBeforeCreateSchedule={onBeforeCreateSchedule} 637 | /> 638 |
639 | ); 640 | } 641 | 642 | export default { 643 | title: 'Wrapper Examples', 644 | component: Calendar 645 | }; 646 | 647 | export const WithClassComponent = (args) => ; 648 | WithClassComponent.args = { 649 | view: 'month' 650 | }; 651 | WithClassComponent.argTypes = { 652 | view: { 653 | control: { 654 | type: 'select', 655 | options: ['month', 'week', 'day'] 656 | } 657 | } 658 | }; 659 | 660 | export const WithFunctionComponent = (args) => ; 661 | WithFunctionComponent.args = { 662 | view: 'month' 663 | }; 664 | WithFunctionComponent.argTypes = { 665 | view: { 666 | control: { 667 | type: 'select', 668 | options: ['month', 'week', 'day'] 669 | } 670 | } 671 | }; 672 | -------------------------------------------------------------------------------- /stories/myTheme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'common.border': '1px solid #ddd', 3 | 'common.backgroundColor': 'white', 4 | 'common.holiday.color': '#f54f3d', 5 | 'common.saturday.color': '#135de6', 6 | 'common.dayname.color': '#333', 7 | 'common.today.color': '#009688', 8 | 9 | // creation guide style 10 | 'common.creationGuide.backgroundColor': 'rgba(19, 93, 230, 0.1)', 11 | 'common.creationGuide.border': '1px solid #135de6', 12 | 13 | // month header 'dayname' 14 | 'month.dayname.height': '42px', 15 | 'month.dayname.borderLeft': 'none', 16 | 'month.dayname.paddingLeft': '0', 17 | 'month.dayname.paddingRight': '0', 18 | 'month.dayname.fontSize': '13px', 19 | 'month.dayname.backgroundColor': 'inherit', 20 | 'month.dayname.fontWeight': 'normal', 21 | 'month.dayname.textAlign': 'center', 22 | 23 | // month day grid cell 'day' 24 | 'month.holidayExceptThisMonth.color': '#f3acac', 25 | 'month.dayExceptThisMonth.color': '#bbb', 26 | 'month.weekend.backgroundColor': '#fafafa', 27 | 'month.day.fontSize': '16px', 28 | 29 | // month schedule style 30 | 'month.schedule.height': '18px', 31 | 'month.schedule.marginTop': '2px', 32 | 'month.schedule.marginLeft': '10px', 33 | 'month.schedule.marginRight': '10px', 34 | 35 | // month more view 36 | 'month.moreView.boxShadow': 'none', 37 | 'month.moreView.paddingBottom': '0', 38 | 'month.moreViewTitle.height': '28px', 39 | 'month.moreViewTitle.marginBottom': '0', 40 | 'month.moreViewTitle.backgroundColor': '#f4f4f4', 41 | 'month.moreViewTitle.borderBottom': '1px solid #ddd', 42 | 'month.moreViewTitle.padding': '0 10px', 43 | 'month.moreViewList.padding': '10px', 44 | 45 | // week header 'dayname' 46 | 'week.dayname.height': '41px', 47 | 'week.dayname.borderTop': '1px solid #ddd', 48 | 'week.dayname.borderBottom': '1px solid #ddd', 49 | 'week.dayname.borderLeft': '1px solid #ddd', 50 | 'week.dayname.paddingLeft': '5px', 51 | 'week.dayname.backgroundColor': 'inherit', 52 | 'week.dayname.textAlign': 'left', 53 | 'week.today.color': '#009688', 54 | 'week.pastDay.color': '#999', 55 | 56 | // week vertical panel 'vpanel' 57 | 'week.vpanelSplitter.border': '1px solid #ddd', 58 | 'week.vpanelSplitter.height': '3px', 59 | 60 | // week daygrid 'daygrid' 61 | 'week.daygrid.borderRight': '1px solid #ddd', 62 | 63 | 'week.daygridLeft.width': '100px', 64 | 'week.daygridLeft.backgroundColor': '', 65 | 'week.daygridLeft.paddingRight': '5px', 66 | 'week.daygridLeft.borderRight': '1px solid #ddd', 67 | 68 | 'week.today.backgroundColor': 'inherit', 69 | 'week.weekend.backgroundColor': 'inherit', 70 | 71 | // week timegrid 'timegrid' 72 | 'week.timegridLeft.width': '100px', 73 | 'week.timegridLeft.backgroundColor': '#fafafa', 74 | 'week.timegridLeft.borderRight': '1px solid #ddd', 75 | 'week.timegridLeft.fontSize': '12px', 76 | 'week.timegridLeftTimezoneLabel.height': '51px', 77 | 'week.timegridLeftAdditionalTimezone.backgroundColor': '#fdfdfd', 78 | 79 | 'week.timegridOneHour.height': '48px', 80 | 'week.timegridHalfHour.height': '24px', 81 | 'week.timegridHalfHour.borderBottom': '1px dotted #f9f9f9', 82 | 'week.timegridHorizontalLine.borderBottom': '1px solid #eee', 83 | 84 | 'week.timegrid.paddingRight': '10px', 85 | 'week.timegrid.borderRight': '1px solid #ddd', 86 | 'week.timegridSchedule.borderRadius': '0', 87 | 'week.timegridSchedule.paddingLeft': '0', 88 | 89 | 'week.currentTime.color': '#135de6', 90 | 'week.currentTime.fontSize': '12px', 91 | 'week.currentTime.fontWeight': 'bold', 92 | 93 | 'week.pastTime.color': '#999', 94 | 'week.pastTime.fontWeight': 'normal', 95 | 96 | 'week.futureTime.color': '#333', 97 | 'week.futureTime.fontWeight': 'normal', 98 | 99 | 'week.currentTimeLinePast.border': '1px solid rgba(19, 93, 230, 0.3)', 100 | 'week.currentTimeLineBullet.backgroundColor': '#135de6', 101 | 'week.currentTimeLineToday.border': '1px solid #135de6', 102 | 'week.currentTimeLineFuture.border': '1px solid #135de6', 103 | 104 | // week creation guide style 105 | 'week.creationGuide.color': '#135de6', 106 | 'week.creationGuide.fontSize': '12px', 107 | 'week.creationGuide.fontWeight': 'bold', 108 | 109 | // week daygrid schedule style 110 | 'week.dayGridSchedule.borderRadius': '0', 111 | 'week.dayGridSchedule.height': '18px', 112 | 'week.dayGridSchedule.marginTop': '2px', 113 | 'week.dayGridSchedule.marginLeft': '10px', 114 | 'week.dayGridSchedule.marginRight': '10px' 115 | }; 116 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const config = { 4 | entry: './src/index.js', 5 | output: { 6 | filename: 'toastui-react-calendar.js', 7 | path: path.resolve(__dirname, 'dist'), 8 | libraryTarget: 'commonjs2' 9 | }, 10 | externals: { 11 | 'tui-calendar': { 12 | commonjs: 'tui-calendar', 13 | commonjs2: 'tui-calendar' 14 | }, 15 | react: { 16 | commonjs: 'react', 17 | commonjs2: 'react' 18 | } 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | include: [path.resolve(__dirname, 'src')], 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: ['@babel/preset-env'] 29 | } 30 | } 31 | } 32 | ] 33 | } 34 | }; 35 | 36 | module.exports = () => config; 37 | --------------------------------------------------------------------------------