initial commit 2

This commit is contained in:
equippedcoding-master
2025-09-17 15:19:57 -05:00
parent e2c98790b2
commit 1c59875b8a
55391 changed files with 15 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>SurveyJS Test</title>
<!-- Boxiocns CDN Link -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href='https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css' rel='stylesheet'>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta2/css/all.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.6/underscore-min.js" integrity="sha512-2V49R8ndaagCOnwmj8QnbT1Gz/rie17UouD9Re5WxbzRVUGoftCu5IuqqtAM9+UC3fwfHCSJR1hkzNQh/2wdtg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js" integrity="sha512-TztyCWDNoN0YKl30gDCMKsiWs35juID+W7ZM2uvPeLLmiNvZg789SglgB/QeUbewqIF2Z4mVq3PyIEa+YXXADQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/appfactory3.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afssponsorship.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afssubscriber.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsnotifications.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afspayments.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsform.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsextras.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsevents.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsdocuments.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsaccount.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsemail.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsanalytics.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsdonations.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsspinner.js"></script>
<link href="/portal/admin/core/api/styles/tabler/tabler.min.css" rel="stylesheet"/>
<script src="/portal/admin/core/api/js/libs/tabler/tabler.js"></script>
</head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<script data-main="main" src="/portal/admin/core/api/js/libs/require.js"></script>
</body>
</html>

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,14 @@
<?php
date_default_timezone_set('America/Chicago');
require dirname( __DIR__, 4 ) . "/includes/init.php";
require dirname( __DIR__, 4 ) . "/includes/functions.php";
if(Input::get("init")){
init();
}
function init(){
echo json_encode(array("success" => true));
}
?>

View File

@@ -0,0 +1,4 @@
/* styles */

View File

@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>SurveyJS Test</title>
<!-- Boxiocns CDN Link -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href='https://unpkg.com/boxicons@2.0.7/css/boxicons.min.css' rel='stylesheet'>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta2/css/all.min.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.3.min.js" integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.6/underscore-min.js" integrity="sha512-2V49R8ndaagCOnwmj8QnbT1Gz/rie17UouD9Re5WxbzRVUGoftCu5IuqqtAM9+UC3fwfHCSJR1hkzNQh/2wdtg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.4.1/backbone-min.js" integrity="sha512-TztyCWDNoN0YKl30gDCMKsiWs35juID+W7ZM2uvPeLLmiNvZg789SglgB/QeUbewqIF2Z4mVq3PyIEa+YXXADQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/appfactory3.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afssponsorship.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afssubscriber.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsnotifications.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afspayments.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsform.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsextras.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsevents.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsdocuments.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsaccount.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsemail.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsanalytics.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsdonations.js"></script>
<script src="/portal/admin/core/api/js/libs/appfactory/afsspinner.js"></script>
<link href="/portal/admin/core/api/styles/tabler/tabler.min.css" rel="stylesheet"/>
<script src="/portal/admin/core/api/js/libs/tabler/tabler.js"></script>
</head>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<script data-main="main" src="/portal/admin/core/api/js/libs/require.js"></script>
</body>
</html>

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,14 @@
<?php
date_default_timezone_set('America/Chicago');
require dirname( __DIR__, 4 ) . "/includes/init.php";
require dirname( __DIR__, 4 ) . "/includes/functions.php";
if(Input::get("init")){
init();
}
function init(){
echo json_encode(array("success" => true));
}
?>

View File

@@ -0,0 +1,4 @@
/* styles */

View File

@@ -0,0 +1,4 @@
build/
dist/
node_modules/
perf/

View File

@@ -0,0 +1,119 @@
const defaultExtends = [
'tui',
'prettier',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:prettier/recommended',
];
module.exports = {
root: true,
env: {
browser: true,
es6: true,
node: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
parser: 'typescript-eslint-parser',
ecmaVersion: 2018,
sourceType: 'module',
},
plugins: [
'unused-imports',
'simple-import-sort',
'prettier',
'react',
'react-hooks',
'@typescript-eslint',
'jest',
],
extends: defaultExtends,
settings: {
react: {
pragma: 'h',
version: '16.13',
},
},
globals: {
fixture: true,
},
ignorePatterns: ['**/*.d.ts'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/ban-types': 0,
'@typescript-eslint/no-use-before-define': 0,
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-shadow': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'no-duplicate-imports': 'off',
'@typescript-eslint/no-duplicate-imports': 'error',
'no-shadow': 'off',
'no-use-before-define': 0,
// use unused-imports plugin
'@typescript-eslint/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{ args: 'after-used', argsIgnorePattern: '^_', ignoreRestSiblings: true },
],
'react/prop-types': 0,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
'jest/no-conditional-expect': 0,
'simple-import-sort/imports': [
'warn',
{
groups: [
// Side effect imports.
['^\\u0000'],
// Preact.
['^preact'],
// Any other packages.
['^(\\w|@)(?!src/|test/|stories/|t/)'],
// Source files.
['^@src/'],
// stories files
['^@stories/'],
// Types.
['^@t/'],
// Absolute imports and other imports such as Vue-style `@/foo`.
// Anything not matched in another group.
['^'],
// Relative imports.
// Anything that starts with a dot.
['^\\.'],
],
},
],
'simple-import-sort/exports': 'warn',
complexity: ['error', { max: 8 }],
'no-warning-comments': 0,
},
overrides: [
{
files: ['*.spec.ts', '*.spec.tsx'],
extends: ['plugin:jest/recommended'],
rules: {
'max-nested-callbacks': ['error', { max: 5 }],
'jest/expect-expect': [
'warn',
{
assertFunctionNames: ['expect', 'assert*'],
},
],
'jest/no-conditional-expect': 'warn',
},
},
{
files: ['apps/calendar/playwright/**/*.ts'],
extends: ['plugin:playwright/playwright-test'],
rules: {
'playwright/no-force-option': 'off',
'max-nested-callbacks': ['error', { max: 5 }],
'dot-notation': ['error', { allowKeywords: true }],
},
},
],
};

View File

@@ -0,0 +1 @@
_

View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View File

@@ -0,0 +1,2 @@
*.md
*.html

View File

@@ -0,0 +1,17 @@
{
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "es5",
"arrowParens": "always",
"endOfLine": "lf",
"bracketSpacing": true,
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve",
"vueIndentScriptAndStyle": false
}

View File

@@ -0,0 +1,73 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at dl_javascript@nhn.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org

View File

@@ -0,0 +1,109 @@
# Contributing to TOAST UI
First off, thanks for taking the time to contribute! 🎉 😘 ✨
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.
## Reporting Bugs
Bugs are tracked as [GitHub issues](https://github.com/nhn/tui.calendar/issues). Search the issue list and try to reproduce on [demo](https://nhn.github.io/tui.calendar/latest/tutorial-00-calendar-app) before you create an issue. When you create an issue, please provide the following information by filling in the template.
Explain the problem and include additional details to help maintainers reproduce the problem:
* **Use a clear and descriptive title** for the issue to identify the problem.
* **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.
* **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/paste-able snippets, which you use in those examples. If you're providing snippets on the issue, use Markdown code blocks.
* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
* **Explain which behavior you expected to see instead and why.**
* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem.
## Suggesting Enhancements
In case you want to suggest for TOAST UI Calendar, please follow this guideline to help maintainers and the community understand your suggestion.
Before creating suggestions, please check [issue list](https://github.com/nhn/tui.calendar/labels/Type:%20Enhancement) if there's already a request.
Create an issue and provide the following information:
* **Use a clear and descriptive title** for the issue to identify the suggestion.
* **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
* **Provide specific examples to demonstrate the steps.** Include copy/paste-able snippets which you use in those examples, as Markdown code blocks.
* **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.
* **Explain why this enhancement would be useful** to most TOAST UI users.
* **List some other applications where this enhancement exists.**
## First Code Contribution
Unsure where to begin contributing to TOAST UI? You can start by looking through these `document`, `good first issue` and `help wanted` issues:
* **document issues**: issues which should be reviewed or improved.
* **good first issues**: issues which should only require a few lines of code, and a test or two.
* **help wanted issues**: issues which should be a bit more involved than beginner issues.
## Pull Requests
### Development WorkFlow
- Set up your development environment
- Make change from a right branch
- Be sure the code passes `npm run lint`, `npm run test`, `npm run test:playwright`
- Make a pull request
### Development environment
- Prepare your machine Node.js 16+ and it's packages installed.
- Checkout to the right branch
- Install dependencies by `npm install`
- Start development by `npm run develop`
- For wrappers, `npm run develop --workspace @toast-ui/react-calendar` or `npm run develop --workspace @toast-ui/vue-calendar`
### Make changes
#### Checkout a branch
- **main**: PR base branch. merge features, updates for next minor or major release.
- **v1**: Legacy version of the project.
- **gh-pages**: API docs, examples and demo
#### Check Code Style
Run `npm run lint` and make sure all the tests pass.
#### Test
Run `npm run test` and verify all the tests pass.
If you are adding new commands or features, they must include tests.
If you are changing functionality, update the tests if you need to.
#### Commit
Follow our [commit message conventions](./docs/COMMIT_MESSAGE_CONVENTION.md).
### Yes! Pull request
Make your pull request, then describe your changes.
#### Title
Follow other PR title format on below.
```
<Type>: Short Description (fix #111)
<Type>: Short Description (fix #123, #111, #122)
<Type>: Short Description (ref #111)
```
* capitalize first letter of Type
* use present tense: 'change' not 'changed' or 'changes'
#### Description
If it has related to issues, add links to the issues(like `#123`) in the description.
Fill in the [Pull Request Template](./docs/PULL_REQUEST_TEMPLATE.md) by check your case.
## Code of Conduct
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@github.com.
> 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)
[demo]:https://nhn.github.io/tui.calendar/latest

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021 NHN Corp.
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.

View File

@@ -0,0 +1,124 @@
# ![TOAST UI Calendar](https://user-images.githubusercontent.com/26706716/39230183-7f8ff186-48a0-11e8-8d9c-9699d2d0e471.png)
> 🍞📅 A JavaScript calendar that is full featured. Now your service just got the customizable calendar.
[![npm](https://img.shields.io/npm/v/@toast-ui/calendar.svg)](https://www.npmjs.com/package/@toast-ui/calendar)
[![GitHub license](https://img.shields.io/github/license/nhn/tui.calendar.svg)](https://github.com/nhn/tui.calendar/blob/main/LICENSE)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.calendar/labels/help%20wanted)
[![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)
## 🚩 Table of Contents
- [📦 Packages](#-packages)
- [📙 Documents](#-documents)
- [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source)
- [📅 Features](#-features)
- [✨ Monthly, Weekly, Daily and Various View Types](#-monthly-weekly-daily-and-various-view-types)
- [Easy to Use: Dragging and Resizing a Schedule](#easy-to-use-dragging-and-resizing-a-schedule)
- [Ready to Use: Default Popups](#ready-to-use-default-popups)
- [🎨 Other Features](#-other-features)
- [💬 Contributing](#-contributing)
- [🌏 Browser Support](#-browser-support)
- [🔩 Dependencies](#-dependencies)
- [🍞 TOAST UI Family](#-toast-ui-family)
- [🚀 Used By](#-used-by)
- [📜 License](#-license)
## 📦 Packages
The functionality of TOAST UI Calendar is available when using the Plain JavaScript, React, Vue Component.
- [@toast-ui/calendar](/apps/calendar) - Plain JavaScript component implemented by [NHN Cloud](https://github.com/nhn).
- [@toast-ui/react-calendar](/apps/react-calendar) - React wrapper component implemented by [NHN Cloud](https://github.com/nhn).
- [@toast-ui/vue-calendar](/apps/vue-calendar) - Vue wrapper component implemented by [NHN Cloud](https://github.com/nhn).
## 📙 Documents
- [English](./docs/README.md)
- [Korean](./docs/ko/README.md)
## Collect statistics on the use of open source
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, refer to the docs below.
- [TOAST UI Calendar](/docs/en/guide/getting-started.md#disable-to-collect-hostname-for-google-analyticsga)
- [TOAST UI Calendar for React](/apps/react-calendar/docs/en/guide/getting-started.md#disable-to-collect-hostname-for-google-analyticsga)
- [TOAST UI Calendar for Vue](/apps/vue-calendar/docs/en/guide/getting-started.md#disable-to-collect-hostname-for-google-analyticsga)
## 📅 Features
### ✨ Monthly, Weekly, Daily and Various View Types
| Monthly | Weekly |
| --- | --- |
| ![image](https://user-images.githubusercontent.com/26706716/39230396-4d79a592-48a1-11e8-9849-08e80f1bedf6.png) | ![image](https://user-images.githubusercontent.com/26706716/39230459-83beac38-48a1-11e8-8cd4-11b97817f1f8.png) |
| Daily | 2 Weeks |
| --- | --- |
| ![image](https://user-images.githubusercontent.com/26706716/39230685-60a2a1d6-48a2-11e8-9d46-ce5693277a64.png) | ![image](https://user-images.githubusercontent.com/26706716/39230638-281d5266-48a2-11e8-84d8-ab289f372051.png) |
### Easy to Use: Dragging and Resizing a Schedule
| Dragging | Resizing |
| --- | --- |
| ![image](https://user-images.githubusercontent.com/26706716/39230930-591031f8-48a3-11e8-8f62-e12e6c19920c.gif) | ![image](https://user-images.githubusercontent.com/26706716/39231671-c926d0da-48a5-11e8-959d-35fd32f2c522.gif) |
### Ready to Use: Default Popups
| Creation Popup | Detail Popup |
| --- | --- |
| ![image](https://user-images.githubusercontent.com/26706716/39230798-d151a9ae-48a2-11e8-842d-b19b40432f48.png) | ![image](https://user-images.githubusercontent.com/26706716/39230820-e73fa11c-48a2-11e8-9348-8e3d81979a78.png) |
## 🎨 Other Features
- Supports various view types: daily, weekly, monthly(6 weeks, 2 weeks, 3 weeks)
- Supports efficient management of milestone and task schedules
- Supports the narrow width of weekend
- Supports changing start day of week
- Supports customizing the date and schedule information UI(including a header and a footer of grid cell)
- Supports adjusting a schedule by mouse dragging
- Supports customizing UI by theme
## 💬 Contributing
- [Code of Conduct](/CODE_OF_CONDUCT.md)
- [Contributing Guidelines](/CONTRIBUTING.md)
- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)
- [Issue Guidelines](/docs/ISSUE_TEMPLATE.md)
## 🌏 Browser Support
| <img src="https://user-images.githubusercontent.com/1215767/34348387-a2e64588-ea4d-11e7-8267-a43365103afe.png" alt="Chrome" width="16px" height="16px" /> Chrome | <img src="https://user-images.githubusercontent.com/1215767/34348590-250b3ca2-ea4f-11e7-9efb-da953359321f.png" alt="IE" width="16px" height="16px" /> Internet Explorer | <img src="https://user-images.githubusercontent.com/1215767/34348380-93e77ae8-ea4d-11e7-8696-9a989ddbbbf5.png" alt="Edge" width="16px" height="16px" /> Edge | <img src="https://user-images.githubusercontent.com/1215767/34348394-a981f892-ea4d-11e7-9156-d128d58386b9.png" alt="Safari" width="16px" height="16px" /> Safari | <img src="https://user-images.githubusercontent.com/1215767/34348383-9e7ed492-ea4d-11e7-910c-03b39d52f496.png" alt="Firefox" width="16px" height="16px" /> Firefox |
| :---------: | :---------: | :---------: | :---------: | :---------: |
| Latest | 11+ | Latest | Latest | Latest |
## 🔩 Dependencies
- [Preact](https://github.com/preactjs/preact)
- [Immer](https://github.com/immerjs/immer)
- [DOMPurify](https://github.com/cure53/DOMPurify)
- (Optional) [tui-date-picker](https://github.com/nhn/tui.date-picker)
- (Optional) [tui-time-picker](https://github.com/nhn/tui.time-picker)
## 🍞 TOAST UI Family
- [TOAST UI Grid](https://github.com/nhn/tui.grid)
- [TOAST UI Chart](https://github.com/nhn/tui.chart)
- [TOAST UI Editor](https://github.com/nhn/tui.editor)
- [TOAST UI Image-Editor](https://github.com/nhn/tui.image-editor)
- [TOAST UI Components](https://github.com/nhn?q=tui)
## 🚀 Used By
- [NHN Dooray! - Collaboration Service (Project, Messenger, Mail, Calendar, Drive, Wiki, Contacts)](https://dooray.com)
- [NCP - Commerce Platform](https://www.e-ncp.com/)
- [shopby](https://www.godo.co.kr/shopby/main.gd)
- [payco-shopping](https://shopping.payco.com/)
- [iamTeacher](https://teacher.iamservice.net)
- [linder](https://www.linder.kr)
## 📜 License
This software is licensed under the [MIT](/LICENSE) © [NHN Cloud](https://github.com/nhn).

View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not ie <= 10

View File

@@ -0,0 +1,5 @@
module.exports = {
'**/*.js': 'eslint --fix',
'**/*.{ts,tsx}': [() => 'npm run check-types', 'eslint --fix', 'jest --bail --findRelatedTests'],
'**/*.css': 'stylelint',
};

View File

@@ -0,0 +1,65 @@
const path = require('path');
module.exports = {
core: {
builder: 'webpack5',
},
stories: ['../**/*.stories.@(ts|tsx)'],
babel: async (config) => {
// Replace storybook babel preset & plugins with custom ones
config.presets.splice(config.presets.length - 1, 1, [
require.resolve('@babel/preset-typescript'),
{ jsxPragma: 'h', jsxPragmaFrag: 'Fragment' },
]);
config.plugins.splice(config.plugins.length - 1, 1, [
require.resolve('@babel/plugin-transform-react-jsx'),
{ pragma: 'h', pragmaFrag: 'Fragment' },
'preset',
]);
return config;
},
webpackFinal: async (config) => {
config.module.rules = config.module.rules
.filter((rule) => !(rule?.test?.test('.css') ?? false))
.concat([
{
test: /\.css$/,
include: /node_modules/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
},
],
},
{
test: /\.css$/,
exclude: /node_modules/,
sideEffects: true,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
},
require.resolve('postcss-loader'),
],
},
]);
Object.assign(config.resolve.alias, {
'core-js/modules': path.resolve(__dirname, '../../../node_modules/core-js/modules'),
'@src': path.resolve(__dirname, '../src/'),
'@t': path.resolve(__dirname, '../src/types/'),
'@stories': path.resolve(__dirname, '../stories/'),
});
return config;
},
};

View File

@@ -0,0 +1,6 @@
import { addons } from '@storybook/addons';
import calendarTheme from './theme';
addons.setConfig({
theme: calendarTheme,
});

View File

@@ -0,0 +1,8 @@
import 'preact/debug';
import '@src/css/index.css';
import 'tui-date-picker/dist/tui-date-picker.css';
import 'tui-time-picker/dist/tui-time-picker.css';
export const parameters = {
layout: 'fullscreen',
};

View File

@@ -0,0 +1,8 @@
import { create } from '@storybook/theming';
export default create({
base: 'light',
brandTitle: 'TOAST UI Calendar',
brandUrl: 'https://ui.toast.com/tui-calendar',
brandImage: 'https://user-images.githubusercontent.com/26706716/39230183-7f8ff186-48a0-11e8-8d9c-9699d2d0e471.png',
});

View File

@@ -0,0 +1,183 @@
# ![TOAST UI Calendar](https://user-images.githubusercontent.com/26706716/39230183-7f8ff186-48a0-11e8-8d9c-9699d2d0e471.png)
> A JavaScript calendar that is full featured. Now your service just got the customizable calendar.
[![npm](https://img.shields.io/npm/v/@toast-ui/calendar.svg)](https://www.npmjs.com/package/@toast-ui/calendar)
[![license](https://img.shields.io/github/license/nhn/tui.calendar.svg)](https://github.com/nhn/tui.calendar/blob/master/LICENSE)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.calendar/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
[![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)
## 🚩 Table of Contents
- [📙 Documents](#-documents)
- [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source)
- [💾 Install](#-install)
- [Using npm](#using-npm)
- [Via Contents Delivery Network (CDN)](#via-contents-delivery-network-cdn)
- [Download Source Files](#download-source-files)
- [📅 Usage](#-usage)
- [Load](#load)
- [Implement](#implement)
- [🔧 Pull Request Steps](#-pull-request-steps)
- [Setup](#setup)
- [Develop](#develop)
- [Pull Request](#pull-request)
- [💬 Contributing](#-contributing)
- [📜 License](#-license)
## 📙 Documents
- [English](/docs/README.md)
- [Korean](/docs/ko/README.md)
## Collect statistics on the use of open source
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, set the [`usageStatistics` option](/docs/en/apis/options.md#usagestatistics) to `false`:
```js
const calendar = new Calendar('#calendar', {
usageStatistics: false
});
```
## 💾 Install
### Using npm
```sh
npm install --save @toast-ui/calendar
```
### Via Contents Delivery Network (CDN)
TOAST UI products are available over the CDN powered by [NHN Cloud](https://www.toast.com).
```html
<link rel="stylesheet" href="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.css" />
<script src="https://uicdn.toast.com/calendar/latest/toastui-calendar.min.js"></script>
<!-- To get bundle file for legacy browser -->
<!-- <script src="https://uicdn.toast.com/calendar /latest/toastui-calendar.ie11.min.js"></script> -->
<!-- Import as es module -->
<!-- <script type="module" src="https:// uicdn.toast.com/calendar/latest/toastui-calendar.mjs"></script> -->
```
If you want to use a specific version, use the tag name instead of `latest` in the url's path.
The CDN directory has the following structure.
```
- uicdn.toast.com/
├─ calendar/
│ ├─ latest
│ │ ├─ toastui-calendar.css
│ │ ├─ toastui-calendar.js
│ │ ├─ toastui-calendar.min.css
│ │ ├─ toastui-calendar.min.js
│ │ ├─ toastui-calendar.ie11.js
│ │ ├─ toastui-calendar.ie11.min.js
│ │ │ toastui-calendar.mjs
│ ├─ v2.0.0/
```
### Download Source Files
- [Download all sources for each version](https://github.com/nhn/tui.calendar/releases)
## 📅 Usage
### Load
TOAST UI Calendar can be instantiated through the constructor function. There are three ways to access the constructor function depending on the environment.
```js
/* ES6 module in Node.js environment */
import Calendar from '@toast-ui/calendar';
import '@toast-ui/calendar/dist/toastui-calendar.min.css';
```
```js
/* CommonJS in Node.js environment */
const Calendar = require('@toast-ui/calendar');
require('@toast-ui/calendar/dist/toastui-calendar.min.css');
```
```js
/* in the browser environment namespace */
const Calendar = tui.Calendar;
```
### Implement
```html
<div id="calendar" style="height: 800px"></div>
```
```javascript
const calendar = new Calendar('#calendar', {
defaultView: 'week',
template: {
time(event) {
const { start, end, title } = event;
return `<span style="color: white;">${formatTime(start)}~${formatTime(end)} ${title}</span>`;
},
allday(event) {
return `<span style="color: gray;">${event.title}</span>`;
},
},
calendars: [
{
id: 'cal1',
name: 'Personal',
backgroundColor: '#03bd9e',
},
{
id: 'cal2',
name: 'Work',
backgroundColor: '#00a9ff',
},
],
});
```
## 🔧 Pull Request Steps
TOAST UI products are open source, so you can create a pull request(PR) after you fix issues.
Run npm scripts and develop yourself with the following process.
### Setup
Fork `main` branch into your personal repository.
Clone it to local computer. Install node modules.
Before starting development, you should check to have any errors.
``` sh
git clone https://github.com/{your-personal-repo}/[[repo name]].git
cd [[repo name]]
npm install
```
### Develop
Let's start development!
### Pull Request
Before PR, check to test lastly and then check any errors.
If it has no error, commit and then push it!
For more information on PR's step, please see links of Contributing section.
## 💬 Contributing
- [Code of Conduct](/CODE_OF_CONDUCT.md)
- [Contributing Guidelines](/CONTRIBUTING.md)
- [Commit Message Convention](/docs/COMMIT_MESSAGE_CONVENTION.md)
## 📜 License
This software is licensed under the [MIT](/LICENSE) © [NHN Cloud](https://github.com/nhn).

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar App Demo</title>
<link
rel="stylesheet"
href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.min.css"
/>
<link
rel="stylesheet"
href="https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.min.css"
/>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
</head>
<body>
<div class="app-container code-html">
<header class="header">
<a href="https://github.com/nhn/tui.calendar">
<img
alt="TOAST UI Calendar Brand Image"
src="./images/img-bi.png"
srcset="./images/img-bi@2x.png 2x, ./images/img-bi@3x.png 3x"
/>
</a>
</header>
<article class="content">
<aside class="sidebar">
<div class="sidebar-item">
<input class="checkbox-all" type="checkbox" id="all" value="all" checked />
<label class="checkbox checkbox-all" for="all">View all</label>
</div>
<hr />
<div class="sidebar-item">
<input type="checkbox" id="1" value="1" checked />
<label class="checkbox checkbox-calendar checkbox-1" for="1">My Calendar</label>
</div>
<div class="sidebar-item">
<input type="checkbox" id="2" value="2" checked />
<label class="checkbox checkbox-calendar checkbox-2" for="2">Work</label>
</div>
<div class="sidebar-item">
<input type="checkbox" id="3" value="3" checked />
<label class="checkbox checkbox-calendar checkbox-3" for="3">Family</label>
</div>
<div class="sidebar-item">
<input type="checkbox" id="4" value="4" checked />
<label class="checkbox checkbox-calendar checkbox-4" for="4">Friends</label>
</div>
<div class="sidebar-item">
<input type="checkbox" id="5" value="5" checked />
<label class="checkbox checkbox-calendar checkbox-5" for="5">Travel</label>
</div>
<hr />
<div class="app-footer">© NHN Cloud Corp.</div>
</aside>
<section class="app-column">
<nav class="navbar">
<div class="dropdown">
<div class="dropdown-trigger">
<button
class="button is-rounded"
aria-haspopup="true"
aria-controls="dropdown-menu"
>
<span class="button-text"></span>
<span
class="dropdown-icon toastui-calendar-icon toastui-calendar-ic-dropdown-arrow"
></span>
</button>
</div>
<div class="dropdown-menu">
<div class="dropdown-content">
<a href="#" class="dropdown-item" data-view-name="month">Monthly</a>
<a href="#" class="dropdown-item" data-view-name="week">Weekly</a>
<a href="#" class="dropdown-item" data-view-name="day">Daily</a>
</div>
</div>
</div>
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="
./images/ic-arrow-line-left@2x.png 2x,
./images/ic-arrow-line-left@3x.png 3x
"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
<div class="nav-checkbox">
<input class="checkbox-collapse" type="checkbox" id="collapse" value="collapse" />
<label for="collapse">Collapse duplicate events and disable the detail popup</label>
</div>
</nav>
<main id="app"></main>
</section>
</article>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.min.js"></script>
<script src="https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script src="./scripts/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Monthly View Basic</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
range.textContent = getNavbarRange(cal.getDateRangeStart(), cal.getDateRangeEnd(), 'month');
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Monthly View Basic</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
month: {
visibleWeeksCount: 2,
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
range.textContent = getNavbarRange(cal.getDateRangeStart(), cal.getDateRangeEnd(), 'week');
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Monthly View Basic</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
month: {
visibleWeeksCount: 3,
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
range.textContent = getNavbarRange(cal.getDateRangeStart(), cal.getDateRangeEnd(), 'week');
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Weekly View</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'week',
calendars: MOCK_CALENDARS,
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Weekly View (No Event View)</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'week',
calendars: MOCK_CALENDARS,
week: {
taskView: true,
eventView: false,
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Daily View</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'day',
calendars: MOCK_CALENDARS,
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Narrow Weekends</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded switch-view">Switch View (Monthly / Weekly)</button>
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
month: {
narrowWeekend: true,
},
week: {
narrowWeekend: true,
},
});
</script>
<script type="text/javascript">
var switchButton = $('.switch-view');
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
switchButton.addEventListener('click', function () {
if (cal.getViewName() === 'month') {
cal.changeView('week');
} else {
cal.changeView('month');
}
displayRenderRange();
});
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Hidden Weekends</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded switch-view">Switch View (Monthly / Weekly)</button>
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
month: {
workweek: true,
},
week: {
workweek: true,
},
});
</script>
<script type="text/javascript">
var switchButton = $('.switch-view');
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
switchButton.addEventListener('click', function () {
if (cal.getViewName() === 'month') {
cal.changeView('week');
} else {
cal.changeView('month');
}
displayRenderRange();
});
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Timezone (Weekly View)</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'week',
calendars: MOCK_CALENDARS,
timezone: {
zones: [
{
timezoneName: 'Asia/Seoul',
displayLabel: 'UTC+09',
tooltip: 'Seoul',
},
{
timezoneName: 'Europe/London',
displayLabel: 'UTC+00',
tooltip: 'London',
},
],
},
week: {
showTimezoneCollapseButton: true,
},
theme: {
week: {
dayGridLeft: {
width: '8rem',
},
timeGridLeft: {
width: '8rem',
},
},
},
});
cal.on('clickTimezonesCollapseBtn', function (prevCollapsedState) {
cal.setOptions({
week: {
timezonesCollapsed: !prevCollapsedState,
},
});
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Common Theme</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
theme: {
// NOTE: Not every theme is not included. For more info, check the theme docs.
common: {
backgroundColor: '#8de0cd',
border: '1px solid #818545',
gridSelection: {
backgroundColor: 'rgba(81, 230, 92, 0.05)',
border: '1px dotted #515ce6',
},
saturday: {
color: 'rgba(64, 64, 255, 0.5)',
},
today: {
color: '#ff4194',
},
},
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,129 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Theme for Weekly View</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
// NOTE: Not every theme is not included. For more info, check the theme docs.
theme: {
month: {
dayName: {
borderLeft: 'none',
backgroundColor: 'rgba(51, 51, 51, 0.4)',
},
moreView: {
border: '1px solid grey',
boxShadow: '0 2px 6px 0 grey',
backgroundColor: 'white',
width: 320,
height: 200,
},
weekend: {
backgroundColor: 'rgba(255, 64, 64, 0.4)',
},
gridCell: {
footerHeight: 31,
},
},
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Theme for Weekly View</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'week',
calendars: MOCK_CALENDARS,
timezone: {
zones: [
{
timezoneName: 'Asia/Seoul',
displayLabel: 'UTC+09',
tooltip: 'Seoul',
},
{
timezoneName: 'Europe/London',
displayLabel: 'UTC+00',
tooltip: 'London',
},
],
},
// NOTE: Not every theme is not included. For more info, check the theme docs.
theme: {
week: {
dayName: {
borderLeft: 'none',
borderTop: '1px dotted red',
borderBottom: '1px dotted red',
backgroundColor: 'rgba(81, 92, 230, 0.05)',
},
dayGrid: {
backgroundColor: 'rgba(81, 92, 230, 0.05)',
},
dayGridLeft: {
borderRight: 'none',
backgroundColor: 'rgba(81, 92, 230, 0.05)',
width: '144px',
},
timeGridLeft: {
borderRight: 'none',
backgroundColor: 'rgba(81, 92, 230, 0.05)',
width: '144px',
},
timeGridLeftAdditionalTimezone: {
backgroundColor: '#e5e5e5',
},
timeGridHalfHourLine: {
borderBottom: '1px dotted #e5e5e5',
},
nowIndicatorPast: {
border: '1px dashed red',
},
futureTime: {
color: 'red',
},
},
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Templates for Monthly View</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
template: {
monthGridHeader: function (data) {
var date = parseInt(data.date.split('-')[2], 10);
return (
'<span class="calendar-month-header" style="margin-left: 4px;">' +
(data.month + 1) +
'/' +
date +
'</span>'
);
},
monthGridHeaderExceed: function (hiddenEvents) {
return (
'<span class="calendar-month-header-exceed" style="font-size: 0.8rem">' +
'+' +
hiddenEvents +
'</span>'
);
},
monthDayName: function (data) {
var label = data.label;
if (data.day === 5) {
label = '🎉 TGIF';
}
return '<span class="calendar-month-day-name">' + label + '</span>';
},
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Template for Weekly View</title>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body k>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'week',
calendars: MOCK_CALENDARS,
timezone: {
zones: [
{
timezoneName: 'Asia/Seoul',
displayLabel: 'UTC+09',
tooltip: 'Seoul',
},
{
timezoneName: 'Europe/London',
tooltip: 'London',
},
],
},
theme: {
week: {
dayGridLeft: {
width: '8rem',
},
timeGridLeft: {
width: '8rem',
},
},
},
template: {
milestone: function (event) {
return '<span class="calendar-event-milestone">' + '⛳ ' + event.title + '</span>';
},
milestoneTitle: function () {
return '<strong>Milestones</strong>';
},
task: function (event) {
return '<span class="calendar-event-task">' + '✅ ' + event.title + '</span>';
},
taskTitle: function () {
return '<strong>Tasks</strong>';
},
allday: function (event) {
return '<span class="calendar-event-allday">' + event.title + '</span>';
},
alldayTitle: function () {
return '<strong>All day events</strong>';
},
time: function (event) {
return (
'<span class="calendar-event-time" style="color: #222;">' + event.title + '</span>'
);
},
goingDuration: function (event) {
return (
'<span class="calendar-event-going-duration" style="color: #222;">' +
event.goingDuration +
'</span>'
);
},
comingDuration: function (event) {
return (
'<span class="calendar-event-coming-duration" style="color: #222;">' +
event.comingDuration +
'</span>'
);
},
weekDayName: function (data) {
var date = data.dateInstance.toDate();
return (
'<span class="calendar-event-weekday-name">' +
moment(date).format('MM-DD') +
'</span>'
);
},
weekGridFooterExceed: function (hiddenEvents) {
return (
'<span class="calendar-event-weekgrid-footer-exceed">+' + hiddenEvents + '</span>'
);
},
collapseBtnTitle: function () {
return '⬆️';
},
timezoneDisplayLabel: function (data) {
if (data.displayLabel) {
return data.displayLabel;
}
return String(
data.timezoneOffset > 0 ? '+' + data.timezoneOffset : data.timezoneOffset
);
},
timegridNowIndicatorLabel: function (data) {
return 'Now: ' + moment(data.time.toDate()).format('hh:mm');
},
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TOAST UI Calendar Example - Theme for Popup</title>
<link
rel="stylesheet"
href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.min.css"
/>
<link
rel="stylesheet"
href="https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.min.css"
/>
<link rel="stylesheet" href="../dist/toastui-calendar.css" />
<link rel="stylesheet" href="./styles/reset.css" />
<link rel="stylesheet" href="./styles/app.css" />
<link rel="stylesheet" href="./styles/icons.css" />
<style>
.navbar {
padding: 0;
}
</style>
</head>
<body>
<div class="app-container code-html">
<header class="header">
<nav class="navbar">
<button class="button is-rounded today">Today</button>
<button class="button is-rounded prev">
<img
alt="prev"
src="./images/ic-arrow-line-left.png"
srcset="./images/ic-arrow-line-left@2x.png 2x, ./images/ic-arrow-line-left@3x.png 3x"
/>
</button>
<button class="button is-rounded next">
<img
alt="prev"
src="./images/ic-arrow-line-right.png"
srcset="
./images/ic-arrow-line-right@2x.png 2x,
./images/ic-arrow-line-right@3x.png 3x
"
/>
</button>
<span class="navbar--range"></span>
</nav>
</header>
<main id="app"></main>
</div>
<script src="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.min.js"></script>
<script src="https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.3/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chance/1.1.8/chance.min.js"></script>
<script src="../dist/toastui-calendar.ie11.min.js"></script>
<script src="./scripts/mock-data.js"></script>
<script src="./scripts/utils.js"></script>
<script type="text/javascript" class="code-js">
var Calendar = window.tui.Calendar;
var cal = new Calendar('#app', {
defaultView: 'month',
calendars: MOCK_CALENDARS,
useFormPopup: true,
useDetailPopup: true,
template: {
popupIsAllday: function () {
return 'All day?';
},
popupStateFree: function () {
return '🏝️ Free';
},
popupStateBusy: function () {
return '🔥 Busy';
},
titlePlaceholder: function () {
return 'Enter title';
},
locationPlaceholder: function () {
return 'Enter location';
},
startDatePlaceholder: function () {
return 'Start date';
},
endDatePlaceholder: function () {
return 'End date';
},
popupSave: function () {
return 'Add Event';
},
popupUpdate: function () {
return 'Update Event';
},
popupEdit: function () {
return 'Modify';
},
popupDelete: function () {
return 'Remove';
},
popupDetailTitle: function (data) {
return 'Detail of ' + data.title;
},
},
});
</script>
<script type="text/javascript">
var todayButton = $('.today');
var prevButton = $('.prev');
var nextButton = $('.next');
var range = $('.navbar--range');
function displayEvents() {
var events = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.clear();
cal.createEvents(events);
}
function displayRenderRange() {
var viewName = cal.getViewName();
range.textContent = getNavbarRange(
cal.getDateRangeStart(),
cal.getDateRangeEnd(),
viewName
);
}
todayButton.addEventListener('click', function () {
cal.today();
displayEvents();
displayRenderRange();
});
prevButton.addEventListener('click', function () {
cal.prev();
displayEvents();
displayRenderRange();
});
nextButton.addEventListener('click', function () {
cal.next();
displayEvents();
displayRenderRange();
});
displayEvents();
displayRenderRange();
</script>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="icomoon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="ic_location_b" horiz-adv-x="745" d="M372.364-64c-248.242 297.324-372.364 514.536-372.364 651.636 0 205.651 166.713 372.364 372.364 372.364s372.364-166.713 372.364-372.364c0-137.101-124.121-354.313-372.364-651.636zM372.364 401.455c102.825 0 186.182 83.356 186.182 186.182s-83.356 186.182-186.182 186.182c-102.825 0-186.182-83.356-186.182-186.182s83.356-186.182 186.182-186.182z" />
<glyph unicode="&#xe901;" glyph-name="ic_lock_b" horiz-adv-x="819" d="M716.8 550.4c56.554 0 102.4-45.846 102.4-102.4v-409.6c0-56.554-45.846-102.4-102.4-102.4h-614.4c-56.554 0-102.4 45.846-102.4 102.4v409.6c0 56.554 45.846 102.4 102.4 102.4v102.4c0 169.662 137.538 307.2 307.2 307.2s307.2-137.538 307.2-307.2v-102.4zM563.2 550.4v102.4c0 84.831-68.769 153.6-153.6 153.6s-153.6-68.769-153.6-153.6v-102.4h307.2z" />
<glyph unicode="&#xe902;" glyph-name="ic_milestone" horiz-adv-x="922" d="M512 345.6h-307.2v-409.6h-204.8v1024h512c56.554 0 102.4-45.846 102.4-102.4h204.8c56.554 0 102.4-45.846 102.4-102.4v-409.6c0-56.554-45.846-102.4-102.4-102.4h-204.8c-56.554 0-102.4 45.846-102.4 102.4z" />
<glyph unicode="&#xe903;" glyph-name="ic_readonly_b" d="M316.713 147.428c56.18-36.576 123.252-57.828 195.287-57.828 197.939 0 358.4 160.461 358.4 358.4 0 72.035-21.252 139.107-57.828 195.287l-495.859-495.859zM208.828 256.766l494.406 494.406c-55.341 34.981-120.923 55.228-191.234 55.228-197.939 0-358.4-160.461-358.4-358.4 0-70.311 20.247-135.893 55.228-191.234zM512-64c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512z" />
<glyph unicode="&#xe904;" glyph-name="ic_repeat_b" d="M149.961 85.961l-149.961-149.961v409.6h409.6l-151.027-151.027c64.858-64.858 154.458-104.973 253.427-104.973 162.356 0 299.498 107.956 343.558 256h158.2c-47.439-233.701-254.057-409.6-501.758-409.6-141.385 0-269.385 57.308-362.039 149.961zM874.039 810.039l149.961 149.961v-409.6h-409.6l151.027 151.027c-64.858 64.858-154.458 104.973-253.427 104.973-162.356 0-299.498-107.956-343.558-256h-158.2c47.439 233.701 254.057 409.6 501.758 409.6 141.385 0 269.385-57.308 362.039-149.961z" />
<glyph unicode="&#xe905;" glyph-name="ic_state_b" d="M409.6 960h204.8c113.108 0 204.8-91.692 204.8-204.8v-204.8c0-113.108-91.692-204.8-204.8-204.8h-204.8c-113.108 0-204.8 91.692-204.8 204.8v204.8c0 113.108 91.692 204.8 204.8 204.8zM409.6 806.4v-307.2h204.8v307.2h-204.8zM102.4 652.8h819.2c56.554 0 102.4-45.846 102.4-102.4v-512c0-56.554-45.846-102.4-102.4-102.4h-819.2c-56.554 0-102.4 45.846-102.4 102.4v512c0 56.554 45.846 102.4 102.4 102.4z" />
<glyph unicode="&#xe906;" glyph-name="ic_user_b" horiz-adv-x="1138" d="M19.766 49.778c-12.815 5.936-19.766 12.441-19.766 19.521 0 177.732 254.7 321.812 568.889 321.812s568.889-144.080 568.889-321.812c0-7.058-7.462-13.567-21.177-19.521h21.177v-113.778h-1137.778v113.778h19.766zM568.889 960c125.675 0 227.556-101.88 227.556-227.556s-101.88-227.556-227.556-227.556c-125.675 0-227.556 101.88-227.556 227.556s101.88 227.556 227.556 227.556z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,278 @@
/* eslint-disable no-var,prefer-destructuring,prefer-template,no-undef,object-shorthand,no-console */
// for testing IE11 compatibility, this file doesn't use ES6 syntax.
(function (Calendar) {
var cal;
// Constants
var CALENDAR_CSS_PREFIX = 'toastui-calendar-';
var cls = function (className) {
return CALENDAR_CSS_PREFIX + className;
};
// Elements
var navbarRange = $('.navbar--range');
var prevButton = $('.prev');
var nextButton = $('.next');
var todayButton = $('.today');
var dropdown = $('.dropdown');
var dropdownTrigger = $('.dropdown-trigger');
var dropdownTriggerIcon = $('.dropdown-icon');
var dropdownContent = $('.dropdown-content');
var checkboxCollapse = $('.checkbox-collapse');
var sidebar = $('.sidebar');
// App State
var appState = {
activeCalendarIds: MOCK_CALENDARS.map(function (calendar) {
return calendar.id;
}),
isDropdownActive: false,
};
// functions to handle calendar behaviors
function reloadEvents() {
var randomEvents;
cal.clear();
randomEvents = generateRandomEvents(
cal.getViewName(),
cal.getDateRangeStart(),
cal.getDateRangeEnd()
);
cal.createEvents(randomEvents);
}
function getReadableViewName(viewType) {
switch (viewType) {
case 'month':
return 'Monthly';
case 'week':
return 'Weekly';
case 'day':
return 'Daily';
default:
throw new Error('no view type');
}
}
function displayRenderRange() {
var rangeStart = cal.getDateRangeStart();
var rangeEnd = cal.getDateRangeEnd();
navbarRange.textContent = getNavbarRange(rangeStart, rangeEnd, cal.getViewName());
}
function setDropdownTriggerText() {
var viewName = cal.getViewName();
var buttonText = $('.dropdown .button-text');
buttonText.textContent = getReadableViewName(viewName);
}
function toggleDropdownState() {
appState.isDropdownActive = !appState.isDropdownActive;
dropdown.classList.toggle('is-active', appState.isDropdownActive);
dropdownTriggerIcon.classList.toggle(cls('open'), appState.isDropdownActive);
}
function setAllCheckboxes(checked) {
var checkboxes = $$('.sidebar-item > input[type="checkbox"]');
checkboxes.forEach(function (checkbox) {
checkbox.checked = checked;
setCheckboxBackgroundColor(checkbox);
});
}
function setCheckboxBackgroundColor(checkbox) {
var calendarId = checkbox.value;
var label = checkbox.nextElementSibling;
var calendarInfo = MOCK_CALENDARS.find(function (calendar) {
return calendar.id === calendarId;
});
if (!calendarInfo) {
calendarInfo = {
backgroundColor: '#2a4fa7',
};
}
label.style.setProperty(
'--checkbox-' + calendarId,
checkbox.checked ? calendarInfo.backgroundColor : '#fff'
);
}
function update() {
setDropdownTriggerText();
displayRenderRange();
reloadEvents();
}
function bindAppEvents() {
dropdownTrigger.addEventListener('click', toggleDropdownState);
prevButton.addEventListener('click', function () {
cal.prev();
update();
});
nextButton.addEventListener('click', function () {
cal.next();
update();
});
todayButton.addEventListener('click', function () {
cal.today();
update();
});
dropdownContent.addEventListener('click', function (e) {
var targetViewName;
if ('viewName' in e.target.dataset) {
targetViewName = e.target.dataset.viewName;
cal.changeView(targetViewName);
checkboxCollapse.disabled = targetViewName === 'month';
toggleDropdownState();
update();
}
});
checkboxCollapse.addEventListener('change', function (e) {
if ('checked' in e.target) {
cal.setOptions({
week: {
collapseDuplicateEvents: !!e.target.checked,
},
useDetailPopup: !e.target.checked,
});
}
});
sidebar.addEventListener('click', function (e) {
if ('value' in e.target) {
if (e.target.value === 'all') {
if (appState.activeCalendarIds.length > 0) {
cal.setCalendarVisibility(appState.activeCalendarIds, false);
appState.activeCalendarIds = [];
setAllCheckboxes(false);
} else {
appState.activeCalendarIds = MOCK_CALENDARS.map(function (calendar) {
return calendar.id;
});
cal.setCalendarVisibility(appState.activeCalendarIds, true);
setAllCheckboxes(true);
}
} else if (appState.activeCalendarIds.indexOf(e.target.value) > -1) {
appState.activeCalendarIds.splice(appState.activeCalendarIds.indexOf(e.target.value), 1);
cal.setCalendarVisibility(e.target.value, false);
setCheckboxBackgroundColor(e.target);
} else {
appState.activeCalendarIds.push(e.target.value);
cal.setCalendarVisibility(e.target.value, true);
setCheckboxBackgroundColor(e.target);
}
}
});
}
function bindInstanceEvents() {
cal.on({
clickMoreEventsBtn: function (btnInfo) {
console.log('clickMoreEventsBtn', btnInfo);
},
clickEvent: function (eventInfo) {
console.log('clickEvent', eventInfo);
},
clickDayName: function (dayNameInfo) {
console.log('clickDayName', dayNameInfo);
},
selectDateTime: function (dateTimeInfo) {
console.log('selectDateTime', dateTimeInfo);
},
beforeCreateEvent: function (event) {
console.log('beforeCreateEvent', event);
event.id = chance.guid();
cal.createEvents([event]);
cal.clearGridSelections();
},
beforeUpdateEvent: function (eventInfo) {
var event, changes;
console.log('beforeUpdateEvent', eventInfo);
event = eventInfo.event;
changes = eventInfo.changes;
cal.updateEvent(event.id, event.calendarId, changes);
},
beforeDeleteEvent: function (eventInfo) {
console.log('beforeDeleteEvent', eventInfo);
cal.deleteEvent(eventInfo.id, eventInfo.calendarId);
},
});
}
function initCheckbox() {
var checkboxes = $$('input[type="checkbox"]');
checkboxes.forEach(function (checkbox) {
setCheckboxBackgroundColor(checkbox);
});
}
function getEventTemplate(event, isAllday) {
var html = [];
var start = moment(event.start.toDate().toUTCString());
if (!isAllday) {
html.push('<strong>' + start.format('HH:mm') + '</strong> ');
}
if (event.isPrivate) {
html.push('<span class="calendar-font-icon ic-lock-b"></span>');
html.push(' Private');
} else {
if (event.recurrenceRule) {
html.push('<span class="calendar-font-icon ic-repeat-b"></span>');
} else if (event.attendees.length > 0) {
html.push('<span class="calendar-font-icon ic-user-b"></span>');
} else if (event.location) {
html.push('<span class="calendar-font-icon ic-location-b"></span>');
}
html.push(' ' + event.title);
}
return html.join('');
}
// Calendar instance with options
// eslint-disable-next-line no-undef
cal = new Calendar('#app', {
calendars: MOCK_CALENDARS,
useFormPopup: true,
useDetailPopup: true,
eventFilter: function (event) {
var currentView = cal.getViewName();
if (currentView === 'month') {
return ['allday', 'time'].includes(event.category) && event.isVisible;
}
return event.isVisible;
},
template: {
allday: function (event) {
return getEventTemplate(event, true);
},
time: function (event) {
return getEventTemplate(event, false);
},
},
});
// Init
bindInstanceEvents();
bindAppEvents();
initCheckbox();
update();
})(tui.Calendar);

View File

@@ -0,0 +1,173 @@
/* eslint-disable */
var MOCK_CALENDARS = [
{
id: '1',
name: 'My Calendar',
color: '#ffffff',
borderColor: '#9e5fff',
backgroundColor: '#9e5fff',
dragBackgroundColor: '#9e5fff',
},
{
id: '2',
name: 'Work',
color: '#ffffff',
borderColor: '#00a9ff',
backgroundColor: '#00a9ff',
dragBackgroundColor: '#00a9ff',
},
{
id: '3',
name: 'Family',
color: '#ffffff',
borderColor: '#DB473F',
backgroundColor: '#DB473F',
dragBackgroundColor: '#DB473F',
},
{
id: '4',
name: 'Friends',
color: '#ffffff',
borderColor: '#03bd9e',
backgroundColor: '#03bd9e',
dragBackgroundColor: '#03bd9e',
},
{
id: '5',
name: 'Travel',
color: '#ffffff',
borderColor: '#bbdc00',
backgroundColor: '#bbdc00',
dragBackgroundColor: '#bbdc00',
},
];
var EVENT_CATEGORIES = ['milestone', 'task'];
function generateRandomEvent(calendar, renderStart, renderEnd) {
function generateTime(event, renderStart, renderEnd) {
var startDate = moment(renderStart.getTime());
var endDate = moment(renderEnd.getTime());
var diffDate = endDate.diff(startDate, 'days');
event.isAllday = chance.bool({ likelihood: 30 });
if (event.isAllday) {
event.category = 'allday';
} else if (chance.bool({ likelihood: 30 })) {
event.category = EVENT_CATEGORIES[chance.integer({ min: 0, max: 1 })];
if (event.category === EVENT_CATEGORIES[1]) {
event.dueDateClass = 'morning';
}
} else {
event.category = 'time';
}
startDate.add(chance.integer({ min: 0, max: diffDate }), 'days');
startDate.hours(chance.integer({ min: 0, max: 23 }));
startDate.minutes(chance.bool() ? 0 : 30);
event.start = startDate.toDate();
endDate = moment(startDate);
if (event.isAllday) {
endDate.add(chance.integer({ min: 0, max: 3 }), 'days');
}
event.end = endDate.add(chance.integer({ min: 1, max: 4 }), 'hour').toDate();
if (!event.isAllday && chance.bool({ likelihood: 20 })) {
event.goingDuration = chance.integer({ min: 30, max: 120 });
event.comingDuration = chance.integer({ min: 30, max: 120 });
if (chance.bool({ likelihood: 50 })) {
event.end = event.start;
}
}
}
function generateNames() {
var names = [];
var i = 0;
var length = chance.integer({ min: 1, max: 10 });
for (; i < length; i += 1) {
names.push(chance.name());
}
return names;
}
var id = chance.guid();
var calendarId = calendar.id;
var title = chance.sentence({ words: 3 });
var body = chance.bool({ likelihood: 20 }) ? chance.sentence({ words: 10 }) : '';
var isReadOnly = chance.bool({ likelihood: 20 });
var isPrivate = chance.bool({ likelihood: 20 });
var location = chance.address();
var attendees = chance.bool({ likelihood: 70 }) ? generateNames() : [];
var recurrenceRule = '';
var state = chance.bool({ likelihood: 50 }) ? 'Busy' : 'Free';
var goingDuration = chance.bool({likelihood: 20}) ? chance.integer({ min: 30, max: 120 }) : 0;
var comingDuration = chance.bool({likelihood: 20}) ? chance.integer({ min: 30, max: 120 }) : 0;
var raw = {
memo: chance.sentence(),
creator: {
name: chance.name(),
avatar: chance.avatar(),
email: chance.email(),
phone: chance.phone(),
},
};
var event = {
id: id,
calendarId: calendarId,
title: title,
body: body,
isReadOnly: isReadOnly,
isPrivate: isPrivate,
location: location,
attendees: attendees,
recurrenceRule: recurrenceRule,
state: state,
goingDuration: goingDuration,
comingDuration: comingDuration,
raw: raw,
}
generateTime(event, renderStart, renderEnd);
if (event.category === 'milestone') {
event.color = '#000'
event.backgroundColor = 'transparent';
event.borderColor = 'transparent';
event.dragBackgroundColor = 'transparent';
}
return event;
}
function generateRandomEvents(viewName, renderStart, renderEnd) {
var i, j;
var event, duplicateEvent;
var events = [];
MOCK_CALENDARS.forEach(function(calendar) {
for (i = 0; i < chance.integer({ min: 20, max: 50 }); i += 1) {
event = generateRandomEvent(calendar, renderStart, renderEnd);
events.push(event);
if (i % 5 === 0) {
for (j = 0; j < chance.integer({min: 0, max: 2}); j+= 1) {
duplicateEvent = JSON.parse(JSON.stringify(event));
duplicateEvent.id += `-${j}`;
duplicateEvent.calendarId = chance.integer({min: 1, max: 5}).toString();
duplicateEvent.goingDuration = 30 * chance.integer({min: 0, max: 4});
duplicateEvent.comingDuration = 30 * chance.integer({min: 0, max: 4});
events.push(duplicateEvent);
}
}
}
});
return events;
}

View File

@@ -0,0 +1,26 @@
/* eslint-disable no-var,prefer-template,no-undef */
var $ = function (selector) {
return document.querySelector(selector);
};
var $$ = function (selector) {
return Array.prototype.slice.call(document.querySelectorAll(selector));
};
function getNavbarRange(tzStart, tzEnd, viewType) {
var start = tzStart.toDate();
var end = tzEnd.toDate();
var middle;
if (viewType === 'month') {
middle = new Date(start.getTime() + (end.getTime() - start.getTime()) / 2);
return moment(middle).format('YYYY-MM');
}
if (viewType === 'day') {
return moment(start).format('YYYY-MM-DD');
}
if (viewType === 'week') {
return moment(start).format('YYYY-MM-DD') + ' ~ ' + moment(end).format('YYYY-MM-DD');
}
throw new Error('no view type');
}

View File

@@ -0,0 +1,133 @@
@import "https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css";
.header {
padding: 1rem;
border-bottom: 1px solid #bbb;
}
.app-container {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
}
.content {
display: flex;
height: 100%;
}
.sidebar {
display: flex;
flex: 0 1 12rem;
flex-direction: column;
padding: 1.25rem;
background-color: #fafafa;
border-right: 1px solid #d5d5d5;
}
.sidebar hr {
margin: 1rem 0;
}
.sidebar-item + .sidebar-item {
margin-top: 0.75rem;
}
.sidebar .app-footer {
margin-top: auto;
font-size: 0.75rem;
}
.app-column {
display: flex;
flex-direction: column;
flex: 1 0 auto;
}
.app-column nav {
flex: 0 1 4rem;
border-bottom: 1px solid #e5e5e5;
}
#app {
flex: 1 0 auto;
}
.navbar {
display: flex;
align-items: center;
padding: 1rem;
}
.navbar .dropdown {
margin-right: 1rem;
}
.navbar .dropdown .toastui-calendar-icon {
margin-left: 0.5rem;
}
.button.prev, .button.next {
padding: 0.8rem;
}
.navbar .button + .button {
margin-left: 0.25rem;
}
.navbar .navbar--range {
margin-left: 1rem;
font-size: 1.25rem;
}
.navbar .nav-checkbox {
margin-left: auto;
}
input:disabled + label {
color: #ccc;
cursor: not-allowed;
}
.toastui-calendar-template-time strong {
color: inherit;
}
.sidebar-item input[type="checkbox"]:not(.checkbox-all) {
visibility: hidden;
}
.checkbox {
position: relative;
}
.checkbox-calendar::before {
content: "";
position: absolute;
left: -1.5rem;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
border: 1px solid #ddd;
}
.checkbox.checkbox-1::before {
background-color: var(--checkbox-1);
}
.checkbox.checkbox-2::before {
background-color: var(--checkbox-2);
}
.checkbox.checkbox-3::before {
background-color: var(--checkbox-3);
}
.checkbox.checkbox-4::before {
background-color: var(--checkbox-4);
}
.checkbox.checkbox-5::before {
background-color: var(--checkbox-5);
}

View File

@@ -0,0 +1,80 @@
/* font icons */
@font-face {
font-family: 'tui-calendar-font-icon';
src: url('../fonts/icon.eot') format('embedded-opentype'),
url('../fonts/icon.ttf') format('truetype'),
url('../fonts/icon.woff') format('woff'),
url('../fonts/icon.svg') format('svg');
}
.calendar-icon {
width: 14px;
height: 14px;
display: inline-block;
vertical-align: middle;
}
.calendar-font-icon {
font-family: 'tui-calendar-font-icon', sans-serif;
font-size: 10px;
font-weight: normal;
}
.img-bi {
background: url('../images/img-bi.png') no-repeat;
width: 215px;
height: 16px;
}
.ic_view_month {
background: url('../images/ic-view-month.png') no-repeat;
}
.ic_view_week {
background: url('../images/ic-view-week.png') no-repeat;
}
.ic_view_day {
background: url('../images/ic-view-day.png') no-repeat;
}
.ic-arrow-line-left {
background: url('../images/ic-arrow-line-left.png') no-repeat;
}
.ic-arrow-line-right {
background: url('../images/ic-arrow-line-right.png') no-repeat;
}
.ic-travel-time {
background: url('../images/ic-traveltime-w.png') no-repeat;
}
/* font icons */
.ic-location-b:before {
content: '\e900';
}
.ic-lock-b:before {
content: '\e901';
}
.ic-milestone-b:before {
content: '\e902';
}
.ic-readonly-b:before {
content: '\e903';
}
.ic-repeat-b:before {
content: '\e904';
}
.ic-state-b:before {
content: '\e905';
}
.ic-user-b:before {
content: '\e906';
}

View File

@@ -0,0 +1,32 @@
@import url(https://fonts.googleapis.com/css?family=Noto+Sans);
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
html, body {
height: 100%;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
font-family: 'Noto Sans', sans-serif;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}

View File

@@ -0,0 +1,15 @@
module.exports = {
testEnvironment: 'jsdom',
clearMocks: true,
preset: 'ts-jest',
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node', 'd.ts'],
moduleNameMapper: {
'\\.(css)$': '<rootDir>/src/test/cssFileMock.ts',
'^@src/(.*)$': '<rootDir>/src/$1',
'^@stories/(.*)$': '<rootDir>/stories/$1',
},
globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.test.json' } },
watchPathIgnorePatterns: ['<rootDir>/.storybook', '<rootDir>/.stories', '/node_modules/'],
testPathIgnorePatterns: ['/node_modules/', '/dist/', '/playwright/'],
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts', '<rootDir>/src/test/matchers.ts'],
};

View File

@@ -0,0 +1,33 @@
{
"source": {
"include": [
"src/js/factory/calendar.js",
"src/js/theme/themeConfig.js",
"src/js/common/timezone.js",
"README.md"
],
"exclude": [],
"includePattern": ".+\\.js(doc)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"plugins": [
"plugins/markdown"
],
"templates": {
"name": "Calendar",
"logo": {
"url": "https://cloud.githubusercontent.com/assets/12269563/20029815/01133928-a39a-11e6-80f3-12500a91c755.png",
"width": "150px",
"height": "13px",
"link": "https://github.com/nhn/tui.jsdoc-template"
}
},
"opts": {
"private": false,
"recurse": true,
"destination": "doc",
"tutorials": "examples",
"template": "../../node_modules/tui-jsdoc-template",
"package": "package.json"
}
}

View File

@@ -0,0 +1,108 @@
{
"name": "@toast-ui/calendar",
"author": "NHN Cloud FE Development Lab <dl_javascript@nhn.com>",
"version": "2.1.3",
"main": "./dist/toastui-calendar.js",
"types": "./types/index.d.ts",
"sideEffects": [
"*.css"
],
"module": "./dist/toastui-calendar.mjs",
"exports": {
".": {
"import": "./dist/toastui-calendar.mjs",
"require": "./dist/toastui-calendar.js"
},
"./ie11": "./dist/toastui-calendar.ie11.js",
"./esm": "./dist/toastui-calendar.mjs",
"./toastui-calendar.css": "./dist/toastui-calendar.css",
"./toastui-calendar.min.css": "./dist/toastui-calendar.min.css",
"./dist/*": "./dist/*"
},
"typesVersions": {
"*": {
"*": [
"./types/index.d.ts"
]
}
},
"license": "MIT",
"description": "TOAST UI Calendar",
"repository": {
"type": "git",
"url": "https://github.com/nhn/tui.calendar.git"
},
"keywords": [
"nhn",
"toast",
"toastui",
"toast-ui",
"calendar",
"fullcalendar",
"daily",
"weekly",
"monthly",
"business week",
"milestone",
"task",
"allday"
],
"files": [
"dist",
"types/index.d.ts",
"types/factory",
"types/time/date.d.ts",
"types/types/@(events|options|template|theme|eventBus).d.ts"
],
"dependencies": {
"immer": "^9.0.15",
"isomorphic-dompurify": "^0.20.0",
"preact": "^10.10.0",
"preact-render-to-string": "^5.2.1",
"tui-date-picker": "^4.0.1",
"tui-time-picker": "^2.0.1"
},
"devDependencies": {
"@storybook/addons": "^6.5.9",
"@storybook/builder-webpack5": "^6.5.9",
"@storybook/core": "^6.5.9",
"@storybook/manager-webpack5": "^6.5.9",
"@storybook/preact": "^6.5.9",
"@storybook/theming": "^6.5.9",
"@types/chance": "^1.1.3",
"chance": "^1.1.8",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^3.4.1",
"eslint-webpack-plugin": "^3.2.0",
"postcss": "^8.4.14",
"postcss-loader": "^6.2.1",
"postcss-prefixer": "^2.1.3",
"storybook": "^6.5.9",
"style-loader": "^3.3.1",
"stylelint": "^14.9.1",
"stylelint-config-recommended": "^8.0.0",
"stylelint-webpack-plugin": "^3.3.0",
"terser-webpack-plugin": "^5.3.3",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-inject-plugin": "^1.5.5"
},
"scripts": {
"check-types": "tsc -p ./tsconfig.json --noEmit",
"lint": "npm run check-types && eslint .",
"release-note": "tuie",
"build": "rimraf dist/ && concurrently 'npm:build:*'",
"build:modern": "webpack --config webpack.config.js && webpack --config webpack.config.js --env minify",
"build:ie11": "webpack --config webpack.config.js --env ie11 && webpack --config webpack.config.js --env minify ie11",
"build:esm": "vite build",
"build:types": "rimraf types/ && tsc -p ./tsconfig.declaration.json",
"analyze": "webpack --config webpack.config.js --env --profile --json > stats.json && webpack-bundle-analyzer stats.json ./dist",
"develop": "npm run storybook",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook",
"docs:prebuild": "npm run build && tsc --outDir tmpdoc --sourceMap false",
"docs:dev": "rimraf tmpdoc/ && npm run docs:prebuild && source ~/.nvm/nvm.sh && nvm use 10 && tuidoc --serv",
"docs:build": "rimraf tmpdoc/ && npm run docs:prebuild && source ~/.nvm/nvm.sh && nvm use 10 && tuidoc",
"publish:cdn": "node scripts/publishToCDN.js",
"update:wrapper": "node scripts/updateWrapper.js"
}
}

View File

@@ -0,0 +1,152 @@
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
import type { FormattedTimeString } from '@t/time/datetime';
import type { BoundingBox } from './types';
import { getBoundingBox } from './utils';
export async function assertDayGridSelectionMatching(
page: Page,
startIdx: number,
endIdx: number,
cellClassName: string,
selectionClassName: string
) {
const startCellLocator = page.locator(cellClassName).nth(startIdx);
const endCellLocator = page.locator(cellClassName).nth(endIdx);
const selectionLocator = page.locator(selectionClassName);
const selectionStartLocator = selectionLocator.first();
const selectionEndLocator = selectionLocator.last();
const [
startCellBoundingBox,
endCellBoundingBox,
selectionStartBoundingBox,
selectionEndBoundingBox,
] = await Promise.all([
getBoundingBox(startCellLocator),
getBoundingBox(endCellLocator),
getBoundingBox(selectionStartLocator),
getBoundingBox(selectionEndLocator),
]);
expect(selectionStartBoundingBox.x).toBeCloseTo(startCellBoundingBox.x, -1);
expect(selectionStartBoundingBox.y).toBeCloseTo(startCellBoundingBox.y, -1);
expect(selectionEndBoundingBox.x + selectionEndBoundingBox.width).toBeCloseTo(
endCellBoundingBox.x + endCellBoundingBox.width,
-1
);
expect(selectionEndBoundingBox.y).toBeCloseTo(endCellBoundingBox.y, -1);
const totalCellCount = endIdx - startIdx + 1;
const totalSelectionWidth = await selectionLocator.evaluateAll((selections) =>
(selections as HTMLElement[]).reduce(
(total, selectionRow) => selectionRow.getBoundingClientRect().width + total,
0
)
);
expect(Math.floor(totalSelectionWidth / totalCellCount)).toBeCloseTo(
startCellBoundingBox.width,
-1
);
}
export async function assertAccumulatedDayGridSelectionMatching(
page: Page,
startIdx: number,
endIdx: number,
nthSelection: number,
isAcrossWeeks: boolean
) {
const cellClassName = '.toastui-calendar-daygrid-cell';
const selectionClassName =
'.toastui-calendar-accumulated-grid-selection .toastui-calendar-grid-selection';
const startCellLocator = page.locator(cellClassName).nth(startIdx);
const endCellLocator = page.locator(cellClassName).nth(endIdx);
const selectionLocator = page.locator(selectionClassName);
const selectionStartLocator = selectionLocator.nth(nthSelection);
const selectionEndLocator = selectionLocator.nth(isAcrossWeeks ? nthSelection + 1 : nthSelection);
const [
startCellBoundingBox,
endCellBoundingBox,
selectionStartBoundingBox,
selectionEndBoundingBox,
] = await Promise.all([
getBoundingBox(startCellLocator),
getBoundingBox(endCellLocator),
getBoundingBox(selectionStartLocator),
getBoundingBox(selectionEndLocator),
]);
expect(selectionStartBoundingBox.x).toBeCloseTo(startCellBoundingBox.x, -1);
expect(selectionStartBoundingBox.y).toBeCloseTo(startCellBoundingBox.y, -1);
expect(selectionEndBoundingBox.x + selectionEndBoundingBox.width).toBeCloseTo(
endCellBoundingBox.x + endCellBoundingBox.width,
-1
);
expect(selectionEndBoundingBox.y).toBeCloseTo(endCellBoundingBox.y, -1);
const totalCellCount = endIdx - startIdx + 1;
const startSelectionWidth = await selectionStartLocator.evaluateAll((selections) =>
(selections as HTMLElement[]).reduce(
(total, selectionRow) => selectionRow.getBoundingClientRect().width + total,
0
)
);
const endSelectionWidth = await selectionEndLocator.evaluateAll((selections) =>
(selections as HTMLElement[]).reduce(
(total, selectionRow) => selectionRow.getBoundingClientRect().width + total,
0
)
);
const totalSelectionWidth = isAcrossWeeks
? startSelectionWidth + endSelectionWidth
: (startSelectionWidth + endSelectionWidth) / 2;
expect(Math.floor(totalSelectionWidth / totalCellCount)).toBeCloseTo(
startCellBoundingBox.width,
-1
);
}
export function assertBoundingBoxIncluded(targetBox: BoundingBox, wrappingBox: BoundingBox) {
expect(targetBox.x).toBeGreaterThanOrEqual(wrappingBox.x);
expect(targetBox.y).toBeGreaterThanOrEqual(wrappingBox.y);
expect(targetBox.x + targetBox.width).toBeLessThanOrEqual(wrappingBox.x + wrappingBox.width);
expect(targetBox.y + targetBox.height).toBeLessThanOrEqual(wrappingBox.y + wrappingBox.height);
}
export async function assertTimeGridSelection(
selectionLocator: Locator,
expected: {
startTop: number;
endBottom: number;
totalElements?: number; // not used in day view tests
formattedTimes: FormattedTimeString[];
}
) {
const timeGridSelectionElements = (await selectionLocator.evaluateAll(
(selection) => selection
)) as HTMLElement[];
const expectedFormattedTime = expected.formattedTimes.join(' - ');
if (expected.totalElements) {
expect(timeGridSelectionElements).toHaveLength(expected.totalElements);
}
await expect(selectionLocator.first()).toHaveText(expectedFormattedTime);
const firstElementBoundingBox = await getBoundingBox(selectionLocator.first());
expect(firstElementBoundingBox.y).toBeCloseTo(expected.startTop, 0);
const lastElementBoundingBox = await getBoundingBox(selectionLocator.last());
expect(lastElementBoundingBox.y + lastElementBoundingBox.height).toBeCloseTo(
expected.endBottom,
0
);
}

View File

@@ -0,0 +1,24 @@
const PORT = process.env.CI ? 8080 : 6006;
const generatePageUrl = (viewId: string) =>
`http://localhost:${PORT}/iframe.html?id=${viewId}&args=&viewMode=story`;
export const DAY_VIEW_PAGE_URL = generatePageUrl('e2e-day-view--fixed-events');
export const WEEK_VIEW_PAGE_URL = generatePageUrl('e2e-week-view--fixed-events');
export const WEEK_VIEW_TIMEZONE_PAGE_URL = generatePageUrl(
'e2e-week-view--different-primary-timezone'
);
export const WEEK_VIEW_DUPLICATE_EVENTS_PAGE_URL = generatePageUrl(
'e2e-week-view--duplicate-events'
);
export const WEEK_VIEW_HOUR_START_OPTION_PAGE_URL = generatePageUrl(
'e2e-week-view--hour-start-option'
);
export const MONTH_VIEW_EMPTY_PAGE_URL = generatePageUrl('e2e-month-view--empty');
export const MONTH_VIEW_PAGE_URL = generatePageUrl('e2e-month-view--fixed-events');

View File

@@ -0,0 +1,5 @@
export enum ClickDelay {
Immediate = 1,
Short = 100,
Long = 300,
}

View File

@@ -0,0 +1,239 @@
import { expect, test } from '@playwright/test';
import type { Matchers } from '@playwright/test/types/expect-types';
import type TZDate from '../../src/time/date';
import { addHours, isSameDate, setTimeStrToDate } from '../../src/time/datetime';
import type { FormattedTimeString } from '../../src/types/time/datetime';
import { mockDayViewEvents } from '../../stories/mocks/mockDayViewEvents';
import { DAY_VIEW_PAGE_URL } from '../configs';
import {
dragAndDrop,
getBoundingBox,
getGuideTimeEventSelector,
getTimeEventSelector,
getTimeGridLineSelector,
getTimeStrFromDate,
waitForSingleElement,
} from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(DAY_VIEW_PAGE_URL);
});
const MOVE_EVENT_SELECTOR = '[class*="dragging--move-event"]';
// Every time grid events in mockDayViewEvents should include DRAG_START_TIME.
const DRAG_START_TIME = '04:00';
const cases: {
title: string;
step: number;
matcherToCompare: Extract<keyof Matchers<number>, 'toBeGreaterThan' | 'toBeLessThan'>;
}[] = [
{
title: 'to the top',
step: -3, // move to 3 hours back
matcherToCompare: 'toBeLessThan',
},
{
title: 'to the bottom',
step: 5, // move to 5 hours later
matcherToCompare: 'toBeGreaterThan',
},
];
const timeEvents = mockDayViewEvents.filter(({ isAllday }) => !isAllday);
const [, SHORT_TIME_EVENT] = timeEvents;
timeEvents.forEach(({ title: eventTitle, start, end }) => {
test.describe(`Move the ${eventTitle} event in the time grid`, () => {
cases.forEach(({ title, step }) => {
test(`${title}`, async ({ page }) => {
// Given
const targetEventSelector = `[data-testid*="time-event-${eventTitle}"]`;
const eventLocator = page.locator(targetEventSelector);
const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);
const dragStartRowLocator = page.locator(getTimeGridLineSelector(DRAG_START_TIME));
const dragStartRowBoundingBox = await getBoundingBox(dragStartRowLocator);
const targetTime = getTimeStrFromDate(
addHours(setTimeStrToDate(end, DRAG_START_TIME), step)
) as FormattedTimeString;
const targetRowLocator = page.locator(getTimeGridLineSelector(targetTime));
const expectedStartTimeAfterMove = getTimeStrFromDate(addHours(start, step));
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: targetRowLocator,
options: {
sourcePosition: {
x: 1,
y: dragStartRowBoundingBox.y - eventBoundingBoxBeforeMove.y + 1,
},
targetPosition: {
y: 1,
x: 1,
},
},
});
await waitForSingleElement(eventLocator);
// Then
await expect
.poll(() => eventLocator.textContent())
.toMatch(new RegExp(expectedStartTimeAfterMove));
});
});
});
});
test('When pressing down the ESC key, the moving event resets to the initial position.', async ({
page,
}) => {
// Given
const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));
const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);
const targetStartTime = getTimeStrFromDate(
addHours(SHORT_TIME_EVENT.end as TZDate, 1)
) as FormattedTimeString;
const targetRowLocator = page.locator(getTimeGridLineSelector(targetStartTime));
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: targetRowLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const eventBoundingBoxAfterMove = await getBoundingBox(eventLocator);
expect(eventBoundingBoxAfterMove).toEqual(eventBoundingBoxBeforeMove);
});
test.describe('CSS class for a move event', () => {
test('should be applied depending on a dragging state.', async ({ page }) => {
// Given
const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));
const eventBoundingBox = await getBoundingBox(eventLocator);
const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);
// When (a drag has not started yet)
await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 10);
await page.mouse.down();
// Then
expect(await moveEventClassLocator.count()).toBe(0);
// When (a drag is working)
await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 50);
// Then
expect(await moveEventClassLocator.count()).toBe(1);
// When (a drag is finished)
await page.mouse.up();
// Then
expect(await moveEventClassLocator.count()).toBe(0);
});
test('should not be applied when a drag is canceled.', async ({ page }) => {
// Given
const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));
const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: eventLocator,
options: {
targetPosition: { x: 10, y: 30 },
},
hold: true,
});
await page.keyboard.down('Escape');
// Then
expect(await moveEventClassLocator.count()).toBe(0);
});
});
const [LONG_TIME_EVENT] = mockDayViewEvents.filter(({ title }) => title === 'long time');
test.describe(`Calibrate event's height while dragging`, () => {
cases.forEach(({ title, step, matcherToCompare }) => {
test(`${title}`, async ({ page }) => {
// Given
const targetEventSelector = `[data-testid*="time-event-${LONG_TIME_EVENT.title}"]`;
const eventLocator = page.locator(targetEventSelector);
const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);
const targetTime = getTimeStrFromDate(
addHours(setTimeStrToDate(LONG_TIME_EVENT.end, DRAG_START_TIME), step)
) as FormattedTimeString;
const targetRowLocator = page.locator(getTimeGridLineSelector(targetTime));
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: targetRowLocator,
hold: true,
});
// Then
await expect
.poll(async () => {
const guideBoundingBox = await getBoundingBox(eventLocator.first());
return guideBoundingBox.height;
})
[matcherToCompare](eventBoundingBoxBeforeMove.height);
});
});
});
const ONE_DAY_TIME_EVENTS = mockDayViewEvents.filter(
({ isAllday, start, end }) => !isAllday && isSameDate(start, end)
);
ONE_DAY_TIME_EVENTS.forEach(({ title }) => {
test(`The height of guide element should be same as the event element. - ${title}`, async ({
page,
}) => {
// Given
const eventLocator = page.locator(getTimeEventSelector(title));
const eventBoundingBox = await getBoundingBox(eventLocator);
const targetRowLocator = page.locator(getTimeGridLineSelector('02:00'));
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: targetRowLocator,
options: {
sourcePosition: {
x: 5,
y: 5,
},
targetPosition: {
y: 5,
x: 5,
},
},
hold: true,
});
// Then
const guideLocator = page.locator(getGuideTimeEventSelector());
const guideBoundingBox = await getBoundingBox(guideLocator);
expect(guideBoundingBox.height).toBeCloseTo(eventBoundingBox.height, 0);
});
});

View File

@@ -0,0 +1,220 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import type { Matchers } from '@playwright/test/types/expect-types';
import type TZDate from '../../src/time/date';
import { addHours, addMinutes } from '../../src/time/datetime';
import type { FormattedTimeString } from '../../src/types/time/datetime';
import { mockDayViewEvents } from '../../stories/mocks/mockDayViewEvents';
import { DAY_VIEW_PAGE_URL } from '../configs';
import {
dragAndDrop,
getBoundingBox,
getTimeEventSelector,
getTimeGridLineSelector,
getTimeStrFromDate,
waitForSingleElement,
} from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(DAY_VIEW_PAGE_URL);
});
const RESIZE_HANDLER_SELECTOR = '[class*="resize-handler"]';
const RESIZE_EVENT_SELECTOR = '[class*="dragging--resize-vertical-event"]';
const cases: {
title: string;
step: number;
matcherToCompare: Extract<keyof Matchers<number>, 'toBeGreaterThan' | 'toBeLessThan'>;
}[] = [
{
title: 'to the top',
step: -1, // move the end time to 1 hour back
matcherToCompare: 'toBeLessThan',
},
{
title: 'to the bottom',
step: 2, // move the end time to 2 hours later
matcherToCompare: 'toBeGreaterThan',
},
];
async function setup({
page,
targetEventTitle,
targetEndTime,
}: {
page: Page;
targetEventTitle: string;
targetEndTime: FormattedTimeString;
}) {
// Given
const targetEventSelector = `[data-testid*="time-event-${targetEventTitle}"]`;
const eventLocator = page.locator(targetEventSelector);
const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator);
const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);
const targetRowLocator = page.locator(getTimeGridLineSelector(targetEndTime));
const targetRowBoundingBox = await getBoundingBox(targetRowLocator);
// When
await dragAndDrop({
page,
sourceLocator: resizeHandlerLocator,
targetLocator: targetRowLocator,
options: {
sourcePosition: {
x: 1,
y: 1,
},
targetPosition: {
x: 1,
y: targetRowBoundingBox.height / 2,
},
},
});
await waitForSingleElement(eventLocator);
const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);
return {
eventLocator,
eventBoundingBoxBeforeResize,
eventBoundingBoxAfterResize,
targetRowBoundingBox,
};
}
const timeEvents = mockDayViewEvents.filter(
({ isAllday, goingDuration, comingDuration }) => !isAllday && !goingDuration && !comingDuration
);
const [, SHORT_TIME_EVENT] = timeEvents;
timeEvents.forEach(({ title: eventTitle, start, end }) => {
test.describe(`Resize the ${eventTitle} event in the time grid`, () => {
cases.forEach(({ title, step, matcherToCompare: compareAssertion }) => {
test(`${title}`, async ({ page }) => {
const targetEndTime = getTimeStrFromDate(
addMinutes(end, (step * 2 - 1) * 30)
) as FormattedTimeString;
const {
eventLocator,
eventBoundingBoxBeforeResize,
eventBoundingBoxAfterResize,
targetRowBoundingBox,
} = await setup({
page,
targetEventTitle: eventTitle,
targetEndTime,
});
// Then
expect(eventBoundingBoxAfterResize.height)[compareAssertion](
eventBoundingBoxBeforeResize.height
);
await expect.poll(() => eventLocator.textContent()).toContain(getTimeStrFromDate(start));
expect(
eventBoundingBoxAfterResize.height - eventBoundingBoxBeforeResize.height
).toBeCloseTo(targetRowBoundingBox.height * step * 2, -1);
});
});
test(`then it should have a minimum height(=1 row) even if the event is resized to before the start time`, async ({
page,
}) => {
const { eventBoundingBoxAfterResize, targetRowBoundingBox } = await setup({
page,
targetEventTitle: eventTitle,
targetEndTime: '00:00',
});
// Then
expect(eventBoundingBoxAfterResize.height).toBeCloseTo(targetRowBoundingBox.height, -1);
});
});
});
test('When pressing down the ESC key, the resizing event resets to the initial size.', async ({
page,
}) => {
// Given
const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));
const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator);
const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);
const targetStartTime = getTimeStrFromDate(
addHours(SHORT_TIME_EVENT.end as TZDate, 1)
) as FormattedTimeString;
const targetRowLocator = page.locator(getTimeGridLineSelector(targetStartTime));
// When
await dragAndDrop({
page,
sourceLocator: resizeHandlerLocator,
targetLocator: targetRowLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);
expect(eventBoundingBoxAfterResize).toEqual(eventBoundingBoxBeforeResize);
});
test.describe('CSS class for a resize event', () => {
test('should be applied depending on a dragging state.', async ({ page }) => {
// Given
const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));
const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);
const resizeHandlerBoundingBox = await getBoundingBox(resizeHandlerLocator);
const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);
// When (a drag has not started yet)
await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 3);
await page.mouse.down();
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
// When (a drag is working)
await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 50);
// Then
expect(await resizeEventClassLocator.count()).toBe(1);
// When (a drag is finished)
await page.mouse.up();
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
});
test('should not be applied when a drag is canceled.', async ({ page }) => {
// Given
const eventLocator = page.locator(getTimeEventSelector(SHORT_TIME_EVENT.title));
const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);
const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);
// When
await dragAndDrop({
page,
sourceLocator: resizeHandlerLocator,
targetLocator: resizeHandlerLocator,
options: {
targetPosition: { x: 10, y: 30 },
},
hold: true,
});
await page.keyboard.down('Escape');
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
});
});

View File

@@ -0,0 +1,227 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { mockDayViewEvents } from '../../stories/mocks/mockDayViewEvents';
import { DAY_VIEW_PAGE_URL } from '../configs';
import { getBoundingBox, getPrefixedClassName } from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(DAY_VIEW_PAGE_URL);
});
// NOTE: Syncing scroll only happens when the mousemove event is fired
// and cannot use `dragAndDrop` because it's better to be manually controlled.
function getScrollTop(el: HTMLElement) {
return el.scrollTop;
}
test.describe('Scroll syncing in time grid when selecting grid', () => {
/**
* Top right of the column should be empty
*/
async function setup(page: Page) {
const timeGridContainerLocator = page.locator(
`${getPrefixedClassName('panel')}${getPrefixedClassName('time')}`
);
const targetColumnLocator = page.locator('[data-testid*=timegrid-column]');
const containerBoundingBox = await getBoundingBox(timeGridContainerLocator);
const columnBoundingBox = await getBoundingBox(targetColumnLocator);
return {
targetColumnLocator,
timeGridContainerLocator,
containerBoundingBox,
columnBoundingBox,
};
}
test('it should sync scroll while dragging down to the bottom', async ({ page }) => {
// Given
const {
targetColumnLocator,
columnBoundingBox,
timeGridContainerLocator,
containerBoundingBox,
} = await setup(page);
const scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);
// When
await targetColumnLocator.hover({
position: {
x: columnBoundingBox.width - 10,
y: 10,
},
force: true,
});
await page.mouse.down();
await page.mouse.move(
columnBoundingBox.x + columnBoundingBox.width / 2,
containerBoundingBox.y + containerBoundingBox.height - 10
);
// Then
await expect
.poll(async () => {
const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);
return scrollTopAfterSync;
})
.toBeGreaterThan(scrollTopBeforeSync);
});
test('it should sync scroll while dragging up to the top', async ({ page }) => {
// Given
const {
targetColumnLocator,
columnBoundingBox,
timeGridContainerLocator,
containerBoundingBox,
} = await setup(page);
// Middle of the column
const xPosition = columnBoundingBox.x + columnBoundingBox.width / 2;
// Scroll down to the bottom of the column
await targetColumnLocator.hover();
await page.mouse.wheel(0, containerBoundingBox.height);
let scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);
await expect
.poll(async () => {
scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);
return scrollTopBeforeSync;
})
.toBeCloseTo(containerBoundingBox.height, -2);
// When
// drag up to the top of the column
await page.mouse.move(xPosition, containerBoundingBox.y + containerBoundingBox.height - 10);
await page.mouse.down();
await expect
.poll(async () => {
await page.mouse.move(xPosition, containerBoundingBox.y);
// Then
const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);
return scrollTopAfterSync;
})
.toBeLessThan(scrollTopBeforeSync);
});
});
mockDayViewEvents
.filter(({ isAllday }) => !isAllday)
.forEach(({ title: eventTitle }) => {
test.describe(`Scroll syncing in time grid when moving the ${eventTitle} event`, () => {
async function setup(page: Page) {
const timeGridContainerLocator = page.locator(
`${getPrefixedClassName('panel')}${getPrefixedClassName('time')}`
);
const targetEventLocator = page.locator(`[data-testid*="time-event-${eventTitle}"]`);
const containerBoundingBox = await getBoundingBox(timeGridContainerLocator);
const eventBoundingBox = await getBoundingBox(targetEventLocator);
return {
timeGridContainerLocator,
targetEventLocator,
containerBoundingBox,
eventBoundingBox,
};
}
test('it should sync scroll while moving event to the edge of the bottom', async ({
page,
}) => {
// Given
const {
timeGridContainerLocator,
targetEventLocator,
containerBoundingBox,
eventBoundingBox,
} = await setup(page);
const scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);
// When
await targetEventLocator.hover({
position: {
x: eventBoundingBox.width / 2,
y: 3,
},
force: true,
});
await page.mouse.down();
await page.mouse.move(
eventBoundingBox.x + eventBoundingBox.width / 2,
containerBoundingBox.y + containerBoundingBox.height - 10
);
await page.mouse.up();
// Then
await expect
.poll(async () => {
const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);
return scrollTopAfterSync;
})
.toBeGreaterThan(scrollTopBeforeSync);
});
test('it should sync scroll while moving event to the edge of the top', async ({ page }) => {
// Given
const {
timeGridContainerLocator,
targetEventLocator,
containerBoundingBox,
eventBoundingBox,
} = await setup(page);
// Let's move the event to the bottom first.
const middleXOfEvent = eventBoundingBox.x + eventBoundingBox.width / 2;
await targetEventLocator.hover({
position: {
x: eventBoundingBox.width / 2,
y: 3,
},
force: true,
});
await page.mouse.down();
await page.mouse.move(
middleXOfEvent,
containerBoundingBox.y + containerBoundingBox.height - 10
);
await page.mouse.up();
// Then scroll down a little.
await page.mouse.wheel(0, containerBoundingBox.height / 2);
let scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);
await expect
.poll(async () => {
scrollTopBeforeSync = await timeGridContainerLocator.evaluate(getScrollTop);
return scrollTopBeforeSync;
})
.toBeGreaterThan(containerBoundingBox.height / 2);
// When
await targetEventLocator.hover({
position: {
x: eventBoundingBox.width / 2,
y: 3,
},
force: true,
});
await page.mouse.down();
await expect
.poll(async () => {
await page.mouse.move(middleXOfEvent, containerBoundingBox.y);
// Then
const scrollTopAfterSync = await timeGridContainerLocator.evaluate(getScrollTop);
return scrollTopAfterSync;
})
.toBeLessThan(scrollTopBeforeSync);
});
});
});

View File

@@ -0,0 +1,126 @@
import { expect, test } from '@playwright/test';
import { assertTimeGridSelection } from '../assertions';
import { DAY_VIEW_PAGE_URL } from '../configs';
import { ClickDelay } from '../constants';
import {
dragAndDrop,
getBoundingBox,
getTimeGridLineSelector,
waitForSingleElement,
} from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(DAY_VIEW_PAGE_URL);
});
const GRID_SELECTION_SELECTOR = '[data-testid*="time-grid-selection"]';
// NOTE: Only firefox automatically scrolls into view at some random tests, so narrowing the range of movement.
// Maybe `scrollIntoViewIfNeeded` is not supported in the firefox?
// reference: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded
test.describe('TimeGrid Selection', () => {
const SELECT_START_TIME = '03:00';
test('should be able to select a time slot with clicking', async ({ page }) => {
// Given
const startGridLineLocator = page.locator(getTimeGridLineSelector(SELECT_START_TIME));
const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);
const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);
// When
await startGridLineLocator.click({ force: true, delay: ClickDelay.Long });
await waitForSingleElement(timeGridSelectionLocator); // Test for debounced click handler.
// Then
await assertTimeGridSelection(timeGridSelectionLocator, {
startTop: startGridLineBoundingBox.y,
endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,
formattedTimes: ['03:00', '03:30'],
});
});
test.fixme('should be able to select a time slot with double clicking', async ({ page }) => {
// Given
const startGridLineLocator = page.locator(getTimeGridLineSelector(SELECT_START_TIME));
const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);
const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);
// When
await startGridLineLocator.dblclick({ force: true, delay: ClickDelay.Immediate });
// Then
await assertTimeGridSelection(timeGridSelectionLocator, {
startTop: startGridLineBoundingBox.y,
endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,
formattedTimes: ['03:00', '03:30'],
});
});
test('should be able to select a range of time from top to bottom', async ({ page }) => {
// Given
const startGridLineLocator = page.locator(getTimeGridLineSelector(SELECT_START_TIME));
const targetGridLineLocator = page.locator(getTimeGridLineSelector('05:00'));
const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);
const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);
const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);
// When
await dragAndDrop({
page,
sourceLocator: startGridLineLocator,
targetLocator: targetGridLineLocator,
});
// Then
await assertTimeGridSelection(timeGridSelectionLocator, {
formattedTimes: ['03:00', '05:30'],
startTop: startGridLineBoundingBox.y,
endBottom: targetGridLineBoundingBox.y + targetGridLineBoundingBox.height,
});
});
test('should be able to select a range of time from bottom to top', async ({ page }) => {
// Given
const startGridLineLocator = page.locator(getTimeGridLineSelector(SELECT_START_TIME));
const targetGridLineLocator = page.locator(getTimeGridLineSelector('01:00'));
const timeGridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);
const startGridLineBoundingBox = await getBoundingBox(startGridLineLocator);
const targetGridLineBoundingBox = await getBoundingBox(targetGridLineLocator);
// When
await dragAndDrop({
page,
sourceLocator: startGridLineLocator,
targetLocator: targetGridLineLocator,
});
// Then
await assertTimeGridSelection(timeGridSelectionLocator, {
formattedTimes: ['01:00', '03:30'],
startTop: targetGridLineBoundingBox.y,
endBottom: startGridLineBoundingBox.y + startGridLineBoundingBox.height,
});
});
});
test('When pressing down the ESC key, the grid selection is canceled.', async ({ page }) => {
// Given
const startGridLineLocator = page.locator(getTimeGridLineSelector('03:00'));
const targetGridLineLocator = page.locator(getTimeGridLineSelector('05:00'));
// When
await dragAndDrop({
page,
sourceLocator: startGridLineLocator,
targetLocator: targetGridLineLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const gridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);
expect(await gridSelectionLocator.count()).toBe(0);
});

View File

@@ -0,0 +1,30 @@
import { test } from '@playwright/test';
import { assertAccumulatedDayGridSelectionMatching } from '../assertions';
import { MONTH_VIEW_EMPTY_PAGE_URL } from '../configs';
import { selectMonthGridCells } from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(MONTH_VIEW_EMPTY_PAGE_URL);
});
test('select 2 cells in each week', async ({ page }) => {
await selectMonthGridCells(page, 21, 23);
await selectMonthGridCells(page, 28, 30);
await assertAccumulatedDayGridSelectionMatching(page, 21, 23, 0, false);
});
test('select 2 cells across 2 weeks', async ({ page }) => {
await selectMonthGridCells(page, 13, 14);
await selectMonthGridCells(page, 20, 21);
await assertAccumulatedDayGridSelectionMatching(page, 13, 14, 0, true);
});
test('select cell across 2 weeks and select cell in 1 week', async ({ page }) => {
await selectMonthGridCells(page, 13, 14);
await selectMonthGridCells(page, 24, 25);
await assertAccumulatedDayGridSelectionMatching(page, 13, 14, 0, true);
});

View File

@@ -0,0 +1,287 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import type { EventObject } from '../../src/types/events';
import { mockMonthViewEventsFixed } from '../../stories/mocks/mockMonthViewEvents';
import { MONTH_VIEW_PAGE_URL } from '../configs';
import { Direction } from '../types';
import {
dragAndDrop,
getBoundingBox,
getCellSelector,
getHorizontalEventSelector,
waitForSingleElement,
} from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(MONTH_VIEW_PAGE_URL);
});
const MOVE_EVENT_SELECTOR = '[class*="dragging--move-event"]';
const [TARGET_EVENT1, TARGET_EVENT2, TARGET_EVENT3] = mockMonthViewEventsFixed;
const testCases: {
event: EventObject;
startCellIndex: number;
endCellIndex: number;
directions: Direction[];
}[] = [
{
event: TARGET_EVENT1,
startCellIndex: 7,
endCellIndex: 16,
directions: [Direction.Up, Direction.Right, Direction.Down],
},
{
event: TARGET_EVENT2,
startCellIndex: 16,
endCellIndex: 18,
directions: [Direction.Up, Direction.Right, Direction.Down, Direction.Left],
},
{
event: TARGET_EVENT3,
startCellIndex: 25,
endCellIndex: 27,
directions: [Direction.Up, Direction.Right, Direction.Down, Direction.Left],
},
];
const rightDirectionTestCases = testCases.filter((testCase) =>
testCase.directions.includes(Direction.Right)
);
const leftDirectionTestCases = testCases.filter((testCase) =>
testCase.directions.includes(Direction.Left)
);
const lowerDirectionTestCases = testCases.filter((testCase) =>
testCase.directions.includes(Direction.Down)
);
const upperDirectionTestCases = testCases.filter((testCase) =>
testCase.directions.includes(Direction.Up)
);
async function setup(page: Page, event: EventObject, targetCellIndex: number) {
const targetCellLocator = page.locator(getCellSelector(targetCellIndex));
const targetCellBoundingBox = await getBoundingBox(targetCellLocator);
const eventLocator = page.locator(getHorizontalEventSelector(event)).first();
const boundingBoxBeforeMoving = await getBoundingBox(eventLocator);
await dragAndDrop({ page, sourceLocator: eventLocator, targetLocator: targetCellLocator });
await waitForSingleElement(eventLocator);
let boundingBoxAfterMoving = await getBoundingBox(eventLocator);
await expect
.poll(async () => {
boundingBoxAfterMoving = await getBoundingBox(eventLocator);
return boundingBoxAfterMoving;
})
.not.toEqual(boundingBoxBeforeMoving);
return {
targetCellBoundingBox,
boundingBoxBeforeMoving,
boundingBoxAfterMoving,
};
}
test.describe('event moving', () => {
/**
* Suppose we have the following cells in the month view.
* Each number represents the index of the cell.
*
* [
* [ 0, 1, 2, 3, 4, 5, 6],
* [ 7, 8, 9, 10, 11, 12, 13],
* [14, 15, 16, 17, 18, 19 ,20],
* [21, 22, 23, 24, 25, 26, 27],
* [28, 29, 30, 31, 32, 33, 34],
* ]
*/
rightDirectionTestCases.forEach(({ event, startCellIndex }) => {
const getRightCellIndex = (cellIndex: number) => cellIndex + 1;
test(`moving month event ${event.title} for direction right`, async ({ page }) => {
// Given
const rightCellIndex = getRightCellIndex(startCellIndex);
// When
const { targetCellBoundingBox, boundingBoxBeforeMoving, boundingBoxAfterMoving } =
await setup(page, event, rightCellIndex);
// Then
expect(boundingBoxAfterMoving.x).toBeGreaterThan(boundingBoxBeforeMoving.x);
expect(boundingBoxAfterMoving.x).toBeCloseTo(targetCellBoundingBox.x, 1);
expect(boundingBoxAfterMoving.x).toBeLessThan(
targetCellBoundingBox.x + targetCellBoundingBox.width
);
});
});
leftDirectionTestCases.forEach(({ event, startCellIndex }) => {
const getLeftCellIndex = (cellIndex: number) => cellIndex - 1;
test(`moving month event ${event.title} for direction left`, async ({ page }) => {
// Given
const leftCellIndex = getLeftCellIndex(startCellIndex);
// When
const { targetCellBoundingBox, boundingBoxBeforeMoving, boundingBoxAfterMoving } =
await setup(page, event, leftCellIndex);
// Then
expect(boundingBoxAfterMoving.x).toBeLessThan(boundingBoxBeforeMoving.x);
expect(boundingBoxAfterMoving.x).toBeCloseTo(targetCellBoundingBox.x, 1);
expect(boundingBoxAfterMoving.x).toBeLessThan(
targetCellBoundingBox.x + targetCellBoundingBox.width
);
});
});
lowerDirectionTestCases.forEach(({ event, startCellIndex }) => {
const getDownCellIndex = (cellIndex: number) => cellIndex + 7;
test(`moving month event ${event.title} for direction down`, async ({ page }) => {
// Given
const downCellIndex = getDownCellIndex(startCellIndex);
// When
const { targetCellBoundingBox, boundingBoxBeforeMoving, boundingBoxAfterMoving } =
await setup(page, event, downCellIndex);
// Then
expect(boundingBoxAfterMoving.y).toBeGreaterThan(boundingBoxBeforeMoving.y);
expect(boundingBoxAfterMoving.width).toBeCloseTo(boundingBoxBeforeMoving.width);
expect(boundingBoxAfterMoving.y).toBeGreaterThan(targetCellBoundingBox.y);
expect(boundingBoxAfterMoving.y).toBeLessThan(
targetCellBoundingBox.y + targetCellBoundingBox.height
);
});
});
upperDirectionTestCases.forEach(({ event, startCellIndex }) => {
const getUpCellIndex = (cellIndex: number) => cellIndex - 7;
test(`moving month event ${event.title} for direction up`, async ({ page }) => {
// Given
const upCellIndex = getUpCellIndex(startCellIndex);
// When
const { targetCellBoundingBox, boundingBoxBeforeMoving, boundingBoxAfterMoving } =
await setup(page, event, upCellIndex);
// Then
expect(boundingBoxAfterMoving.y).toBeLessThan(boundingBoxBeforeMoving.y);
expect(boundingBoxAfterMoving.width).toBeCloseTo(boundingBoxBeforeMoving.width);
expect(boundingBoxAfterMoving.y).toBeGreaterThan(targetCellBoundingBox.y);
expect(boundingBoxAfterMoving.y).toBeLessThan(
targetCellBoundingBox.y + targetCellBoundingBox.height
);
});
});
test('moving month grid event to end of week', async ({ page }) => {
// Given
const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2));
const endOfWeekCellLocator = page.locator(getCellSelector(20));
const endOfWeekCellBoundingBox = await getBoundingBox(endOfWeekCellLocator);
const secondOfWeekCellLocator = page.locator(getCellSelector(22));
const secondOfWeekCellBoundingBox = await getBoundingBox(secondOfWeekCellLocator);
// When
await dragAndDrop({ page, sourceLocator: eventLocator, targetLocator: endOfWeekCellLocator });
// Then
await expect.poll(() => eventLocator.evaluateAll((events) => events.length)).toBe(2);
const targetEventLength = await eventLocator.evaluateAll((events) =>
(events as HTMLElement[]).reduce(
(total, eventRow) => eventRow.getBoundingClientRect().width + total,
0
)
);
const firstEventLocator = eventLocator.first();
const lastEventLocator = eventLocator.last();
const firstEventBoundingBox = await getBoundingBox(firstEventLocator);
const lastEventBoundingBox = await getBoundingBox(lastEventLocator);
expect(firstEventBoundingBox.x).toBeCloseTo(endOfWeekCellBoundingBox.x, 3);
expect(lastEventBoundingBox.x).toBeLessThan(
secondOfWeekCellBoundingBox.x + secondOfWeekCellBoundingBox.width
);
expect(firstEventBoundingBox.y).toBeLessThan(secondOfWeekCellBoundingBox.y);
expect(lastEventBoundingBox.y).toBeGreaterThan(secondOfWeekCellBoundingBox.y);
expect(targetEventLength).toBeCloseTo(endOfWeekCellBoundingBox.width * 3, 1);
});
});
test('When pressing down the ESC key, the moving event resets to the initial position.', async ({
page,
}) => {
// Given
const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2)).first();
const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);
const targetCellLocator = page.locator(getCellSelector(20));
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: targetCellLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const eventBoundingBoxAfterMove = await getBoundingBox(eventLocator);
expect(eventBoundingBoxAfterMove).toEqual(eventBoundingBoxBeforeMove);
});
test.describe('CSS class for a move event', () => {
test('should be applied depending on a dragging state.', async ({ page }) => {
// Given
const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2)).first();
const eventBoundingBox = await getBoundingBox(eventLocator);
const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);
// When (a drag has not started yet)
await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 10);
await page.mouse.down();
// Then
expect(await moveEventClassLocator.count()).toBe(0);
// When (a drag is working)
await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 50);
// Then
expect(await moveEventClassLocator.count()).toBe(1);
// When (a drag is finished)
await page.mouse.up();
// Then
expect(await moveEventClassLocator.count()).toBe(0);
});
test('should not be applied when a drag is canceled.', async ({ page }) => {
// Given
const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2)).first();
const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: eventLocator,
options: {
targetPosition: { x: 10, y: 30 },
},
hold: true,
});
await page.keyboard.down('Escape');
// Then
expect(await moveEventClassLocator.count()).toBe(0);
});
});

View File

@@ -0,0 +1,257 @@
import type { Locator } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { mockMonthViewEventsFixed } from '../../stories/mocks/mockMonthViewEvents';
import { assertBoundingBoxIncluded } from '../assertions';
import { MONTH_VIEW_PAGE_URL } from '../configs';
import {
dragAndDrop,
getBoundingBox,
getCellSelector,
getHorizontalEventSelector,
waitForSingleElement,
} from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(MONTH_VIEW_PAGE_URL);
});
const RESIZE_EVENT_SELECTOR = '[class*="dragging--resize-horizontal-event"]';
const [TARGET_EVENT1, TARGET_EVENT2] = mockMonthViewEventsFixed;
function getResizeIconLocatorOfEvent(eventLocator: Locator) {
return eventLocator.last().locator('data-testid=horizontal-event-resize-icon');
}
test.describe('event resizing', () => {
/**
* Suppose we have the following cells in the month view.
* Each number represents the index of the cell.
*
* [
* [ 0, 1, 2, 3, 4, 5, 6],
* [ 7, 8, 9, 10, 11, 12, 13],
* [14, 15, 16, 17, 18, 19 ,20],
* [21, 22, 23, 24, 25, 26, 27],
* [28, 29, 30, 31, 32, 33, 34],
* ]
*/
// target event is rendered from #7 to #16
const RESIZE_TARGET_SELECTOR = getHorizontalEventSelector(TARGET_EVENT1);
test('resize event to the right in the same row', async ({ page }) => {
// Given
const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);
const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);
const targetCellLocator = page.locator(getCellSelector(19));
const eventBoundingBoxBeforeResizing = await getBoundingBox(eventsLocator.last());
// When
await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });
// Then
await expect.poll(() => eventsLocator.count()).toBe(2);
await expect
.poll(async () => {
const eventBoundingBoxAfterResizing = await getBoundingBox(eventsLocator.last());
return eventBoundingBoxAfterResizing.width;
})
.toBeGreaterThan(eventBoundingBoxBeforeResizing.width);
const targetCellBoundingBox = await getBoundingBox(targetCellLocator);
const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);
assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);
});
test('resize event to the left in the same row', async ({ page }) => {
// Given
const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);
const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);
const targetCellLocator = page.locator(getCellSelector(14));
const eventBoundingBoxBeforeResizing = await getBoundingBox(eventsLocator.last());
// When
await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });
// Then
await expect.poll(() => eventsLocator.count()).toBe(2);
await expect
.poll(async () => {
const eventBoundingBoxAfterResizing = await getBoundingBox(eventsLocator.last());
return eventBoundingBoxAfterResizing.width;
})
.toBeLessThan(eventBoundingBoxBeforeResizing.width);
const targetCellBoundingBox = await getBoundingBox(targetCellLocator);
const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);
assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);
});
test('resize event to the right in the next two rows', async ({ page }) => {
// Given
const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);
const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);
const targetCellLocator = page.locator(getCellSelector(31));
// When
await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });
// Then
await expect.poll(() => eventsLocator.count()).toBe(4);
const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);
const targetCellBoundingBox = await getBoundingBox(targetCellLocator);
assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);
});
test('resize event to the left in the next two rows', async ({ page }) => {
// Given
const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);
const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);
const targetCellLocator = page.locator(getCellSelector(28));
// When
await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });
// Then
await expect.poll(() => eventsLocator.count()).toBe(4);
const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);
const targetCellBoundingBox = await getBoundingBox(targetCellLocator);
assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);
});
test('shrink event - to the end of the first row of rendered events', async ({ page }) => {
// Given
const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);
const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);
const targetCellLocator = page.locator(getCellSelector(13));
// When
await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });
await waitForSingleElement(eventsLocator);
// Then
const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);
const targetCellBoundingBox = await getBoundingBox(targetCellLocator);
assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);
});
test('shrink event - to take place of just one cell', async ({ page }) => {
// Given
const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);
const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);
const targetCellLocator = page.locator(getCellSelector(7));
// When
await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });
await waitForSingleElement(eventsLocator);
// Then
const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);
const targetCellBoundingBox = await getBoundingBox(targetCellLocator);
assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, targetCellBoundingBox);
});
test('prevent resizing when dragging to above the first row of the event or left of the first cell of the event', async ({
page,
}) => {
// Given
const eventsLocator = page.locator(RESIZE_TARGET_SELECTOR);
const resizeIconLocator = getResizeIconLocatorOfEvent(eventsLocator);
const targetCellLocator = page.locator(getCellSelector(0));
const expectedCellLocator = page.locator(getCellSelector(16));
// When
await dragAndDrop({ page, sourceLocator: resizeIconLocator, targetLocator: targetCellLocator });
// Then
await expect.poll(() => eventsLocator.count()).toBe(2);
const resizeIconBoundingBoxAfterResizing = await getBoundingBox(resizeIconLocator);
const expectedCellBoundingBox = await getBoundingBox(expectedCellLocator);
assertBoundingBoxIncluded(resizeIconBoundingBoxAfterResizing, expectedCellBoundingBox);
});
});
test('When pressing down the ESC key, the resizing event resets to the initial size.', async ({
page,
}) => {
// Given
const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2));
const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator.last());
const resizeHandlerLocator = getResizeIconLocatorOfEvent(eventLocator);
const targetCellLocator = page.locator(getCellSelector(20));
// When
await dragAndDrop({
page,
sourceLocator: resizeHandlerLocator,
targetLocator: targetCellLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);
expect(eventBoundingBoxAfterResize).toEqual(eventBoundingBoxBeforeResize);
});
test.describe('CSS class for a resize event', () => {
test('should be applied depending on a dragging state.', async ({ page }) => {
// Given
const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2));
const resizeHandlerLocator = getResizeIconLocatorOfEvent(eventLocator);
const resizeHandlerBoundingBox = await getBoundingBox(resizeHandlerLocator);
const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);
// When (a drag has not started yet)
await page.mouse.move(resizeHandlerBoundingBox.x + 1, resizeHandlerBoundingBox.y + 3);
await page.mouse.down();
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
// When (a drag is working)
await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 50);
// Then
expect(await resizeEventClassLocator.count()).toBe(1);
// When (a drag is finished)
await page.mouse.up();
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
});
test('should not be applied when a drag is canceled.', async ({ page }) => {
// Given
const eventLocator = page.locator(getHorizontalEventSelector(TARGET_EVENT2));
const resizeHandlerLocator = getResizeIconLocatorOfEvent(eventLocator);
const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);
// When
await dragAndDrop({
page,
sourceLocator: resizeHandlerLocator,
targetLocator: resizeHandlerLocator,
options: {
targetPosition: { x: 10, y: 30 },
},
hold: true,
});
await page.keyboard.down('Escape');
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
});
});

View File

@@ -0,0 +1,126 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { assertDayGridSelectionMatching } from '../assertions';
import { MONTH_VIEW_PAGE_URL } from '../configs';
import { ClickDelay } from '../constants';
import { dragAndDrop, getPrefixedClassName, selectMonthGridCells } from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(MONTH_VIEW_PAGE_URL);
});
const MONTH_GRID_CELL_SELECTOR = getPrefixedClassName('daygrid-cell');
const GRID_SELECTION_SELECTOR = `${getPrefixedClassName('weekday')} > ${getPrefixedClassName(
'grid-selection'
)}`;
/**
* Suppose we have the following cells in the month view.
* Each number represents the index of the cell.
*
* [
* [ 0, 1, 2, 3, 4, 5, 6],
* [ 7, 8, 9, 10, 11, 12, 13],
* [14, 15, 16, 17, 18, 19 ,20],
* [21, 22, 23, 24, 25, 26, 27],
* [28, 29, 30, 31, 32, 33, 34],
* ]
*/
function assertMonthGridSelectionMatching(page: Page, startIndex: number, endIndex: number) {
return assertDayGridSelectionMatching(
page,
startIndex,
endIndex,
MONTH_GRID_CELL_SELECTOR,
GRID_SELECTION_SELECTOR
);
}
test('select a cell by clicking.', async ({ page }) => {
// Given
const monthGridCellLocator = page.locator(MONTH_GRID_CELL_SELECTOR).nth(31);
// When
await monthGridCellLocator.click({ delay: ClickDelay.Short });
// Then
await assertMonthGridSelectionMatching(page, 31, 31);
});
// It looks like triple click happens.
// Affected by auto clearing grid selection when form popup closed.
test.fixme('select a cell by double clicking.', async ({ page }) => {
// Given
const monthGridCellLocator = page.locator(MONTH_GRID_CELL_SELECTOR).nth(31);
// When
await monthGridCellLocator.dblclick({ delay: ClickDelay.Immediate });
// Then
await assertMonthGridSelectionMatching(page, 31, 31);
});
test('select a cell by drag and drop.', async ({ page }) => {
await selectMonthGridCells(page, 31, 31);
await assertMonthGridSelectionMatching(page, 31, 31);
});
test('select 2 cells from left to right', async ({ page }) => {
await selectMonthGridCells(page, 31, 32);
await assertMonthGridSelectionMatching(page, 31, 32);
});
test('select 2 cells from right to left(reverse)', async ({ page }) => {
await selectMonthGridCells(page, 32, 31);
await assertMonthGridSelectionMatching(page, 31, 32);
});
test('select 2 rows from top to bottom', async ({ page }) => {
await selectMonthGridCells(page, 25, 32);
await assertMonthGridSelectionMatching(page, 25, 32);
});
test('select 2 rows from bottom to top(reverse)', async ({ page }) => {
await selectMonthGridCells(page, 32, 25);
await assertMonthGridSelectionMatching(page, 25, 32);
});
test('select entire row', async ({ page }) => {
await selectMonthGridCells(page, 28, 34);
await assertMonthGridSelectionMatching(page, 28, 34);
});
test('event form popup with grid selection', async ({ page }) => {
await selectMonthGridCells(page, 28, 34);
const floatingLayer = page.locator('css=[role=dialog]');
expect(floatingLayer).not.toBeNull();
});
test('When pressing down the ESC key, the grid selection is canceled.', async ({ page }) => {
// Given
const startCellLocator = page.locator(MONTH_GRID_CELL_SELECTOR).nth(31);
const targetCellLocator = page.locator(MONTH_GRID_CELL_SELECTOR).nth(32);
// When
await dragAndDrop({
page,
sourceLocator: startCellLocator,
targetLocator: targetCellLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const gridSelectionLocator = page.locator(GRID_SELECTION_SELECTOR);
expect(await gridSelectionLocator.count()).toBe(0);
});

View File

@@ -0,0 +1,21 @@
import { expect, test } from '@playwright/test';
import { MONTH_VIEW_PAGE_URL } from '../configs';
test.beforeEach(async ({ page }) => {
await page.goto(MONTH_VIEW_PAGE_URL);
});
test.describe('more events popup', () => {
test('when clicking on "more events" button, popup should be visible', async ({ page }) => {
await page.click('text=/\\d+ more/i >> nth=0');
const popupLocator = page.locator('css=[role=dialog]');
expect(await popupLocator.isVisible()).toBe(true);
const listLocator = popupLocator.locator('css=[class*=list]');
const listItemCount = await listLocator.evaluate((list) => list.children.length);
expect(listItemCount).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,59 @@
import { expect, test } from '@playwright/test';
import { MONTH_VIEW_PAGE_URL } from '../configs';
import { getCellSelector, getPrefixedClassName } from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(MONTH_VIEW_PAGE_URL);
});
const eventsRowClassName = getPrefixedClassName('weekday-events');
test.describe('visibleEventCount option', () => {
// In the default viewport, 3 event blocks are visible.
// 16th cell has 12 events.
test('when visibleEventCount is set to 0, no events should be visible', async ({ page }) => {
// Given
const targetCell = page.locator(getCellSelector(16));
const events = page.locator(eventsRowClassName).nth(2);
// When
await page.evaluate(() => {
window.$cal.setOptions({
month: {
visibleEventCount: 0,
},
});
});
// Then
expect(await events.evaluate((_events) => _events.children.length)).toBe(0);
const moreButton = targetCell.locator('button');
await expect(moreButton).toHaveText('12 more');
});
test('when visibleEventCount is bigger than the content area, it only shows events within the content area', async ({
page,
}) => {
// Given
const targetCell = page.locator(getCellSelector(16));
const events = page.locator(eventsRowClassName).nth(2);
// When
await page.evaluate(() => {
window.$cal.setOptions({
month: {
visibleEventCount: 10,
},
});
});
// Then
expect(await events.evaluate((_events) => _events.children.length)).toBe(3);
const moreButton = targetCell.locator('button');
await expect(moreButton).toHaveText('9 more');
});
});

View File

@@ -0,0 +1,7 @@
import type Calendar from '../src/factory/calendar';
declare global {
interface Window {
$cal: Calendar;
}
}

View File

@@ -0,0 +1,17 @@
export type BoundingBox = {
x: number;
y: number;
width: number;
height: number;
};
export enum Direction {
Up = 0,
UpperRight = 1,
Right = 2,
LowerRight = 3,
Down = 4,
LowerLeft = 5,
Left = 6,
UpperLeft = 7,
}

View File

@@ -0,0 +1,108 @@
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
import type TZDate from '../src/time/date';
import type { EventObject } from '../src/types/events';
import type { FormattedTimeString } from '../src/types/time/datetime';
import type { BoundingBox } from './types';
export function getPrefixedClassName(className: string) {
return `.toastui-calendar-${className}`;
}
export async function dragAndDrop({
page,
sourceLocator,
targetLocator,
options = {},
hold = false,
}: {
page: Page;
sourceLocator: Locator;
targetLocator: Locator;
options?: Parameters<Locator['dragTo']>[1];
hold?: boolean;
}) {
const sourceBoundingBox = await getBoundingBox(sourceLocator);
const targetBoundingBox = await getBoundingBox(targetLocator);
const sourceX = sourceBoundingBox.x + (options?.sourcePosition?.x ?? sourceBoundingBox.width / 2);
const sourceY =
sourceBoundingBox.y + (options?.sourcePosition?.y ?? sourceBoundingBox.height / 2);
const targetX = targetBoundingBox.x + (options?.targetPosition?.x ?? targetBoundingBox.width / 2);
const targetY =
targetBoundingBox.y + (options?.targetPosition?.y ?? targetBoundingBox.height / 2);
await page.mouse.move(sourceX, sourceY);
await page.mouse.down();
await page.mouse.move(targetX, targetY, { steps: 4 });
if (!hold) {
await page.mouse.up();
}
}
export async function selectGridCells(
page: Page,
startCellIdx: number,
endCellIdx: number,
className: string
) {
const startCellLocator = page.locator(className).nth(startCellIdx);
const endCellLocator = page.locator(className).nth(endCellIdx);
await dragAndDrop({ page, sourceLocator: startCellLocator, targetLocator: endCellLocator });
}
export function selectMonthGridCells(page: Page, startCellIndex: number, endCellIndex: number) {
return selectGridCells(page, startCellIndex, endCellIndex, '.toastui-calendar-daygrid-cell');
}
export async function getBoundingBox(locator: Locator): Promise<BoundingBox> {
const boundingBox = await locator.boundingBox();
if (!boundingBox) {
throw new Error(`BoundingBox of ${locator} is not found`);
}
return boundingBox;
}
export function getTimeEventSelector(title: string): string {
return `[data-testid^="time-event-${title}-"]`;
}
export function getGuideTimeEventSelector(): string {
return `[data-testid^="guide-time-event"]`;
}
export function getHorizontalEventSelector(event: EventObject): string {
return `data-testid=${event.calendarId}-${event.id}-${event.title}`;
}
export function getTimeGridLineSelector(start: FormattedTimeString): string {
return `[data-testid*="gridline-${start}"]`;
}
export function getCellSelector(cellIndex: number): string {
return `.toastui-calendar-daygrid-cell >> nth=${cellIndex}`;
}
export function getTimeStrFromDate(d: TZDate) {
const fixToTwoDigits = (num: number) => num.toString().padStart(2, '0');
const hour = d.getHours();
const minute = d.getMinutes();
return `${fixToTwoDigits(hour)}:${fixToTwoDigits(minute)}`;
}
export function waitForSingleElement(locator: Locator) {
return expect.poll(() => locator.count()).toBe(1);
}
/**
* Get locator matches testId.
*/
export function queryLocatorByTestId(page: Page, testId: string) {
return page.locator(`[data-testid*="${testId}"]`);
}

View File

@@ -0,0 +1,184 @@
import type { Locator } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';
import { WEEK_VIEW_PAGE_URL } from '../configs';
import {
dragAndDrop,
getBoundingBox,
getHorizontalEventSelector,
getPrefixedClassName,
} from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(WEEK_VIEW_PAGE_URL);
});
const ALL_DAY_GRID_CELL_SELECTOR = `${getPrefixedClassName(
'panel'
)}:has-text("All Day") ${getPrefixedClassName('panel-grid')}`;
const MOVE_EVENT_SELECTOR = '[class*="dragging--move-event"]';
const [TARGET_EVENT] = mockWeekViewEvents.filter(({ isAllday }) => isAllday);
const TARGET_EVENT_SELECTOR = getHorizontalEventSelector(TARGET_EVENT);
async function getX(locator: Locator) {
const boundingBox = await getBoundingBox(locator);
return boundingBox.x;
}
async function getWidth(locator: Locator) {
const boundingBox = await getBoundingBox(locator);
return boundingBox.width;
}
/**
* Suppose we have the following cells in the week view.
* Each number represents the index of the cell.
*
* [ 0, 1, 2, 3, 4, 5, 6]
*/
test('moving allday grid row event from left to right', async ({ page }) => {
// Given
const targetEventLocator = page.locator(TARGET_EVENT_SELECTOR);
const boundingBoxBeforeMoving = await getBoundingBox(targetEventLocator);
const fifthOfWeekCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(4);
// When
await dragAndDrop({
page,
sourceLocator: targetEventLocator,
targetLocator: fifthOfWeekCellLocator,
});
// Then
await expect.poll(() => getX(targetEventLocator)).toBeGreaterThan(boundingBoxBeforeMoving.x);
await expect
.poll(() => getWidth(targetEventLocator))
.toBeCloseTo(boundingBoxBeforeMoving.width, 3);
});
test.describe('moving allday grid row event when moving by holding the middle or end', () => {
test('holding middle of event', async ({ page }) => {
// Given
const targetEventLocator = page.locator(TARGET_EVENT_SELECTOR);
const boundingBoxBeforeMoving = await getBoundingBox(targetEventLocator);
const fourthOfWeekCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(3);
// When
await dragAndDrop({
page,
sourceLocator: targetEventLocator,
targetLocator: fourthOfWeekCellLocator,
});
// Then
await expect
.poll(() => getX(targetEventLocator))
.toBeCloseTo(boundingBoxBeforeMoving.x + (boundingBoxBeforeMoving.width * 2) / 3, 1);
await expect
.poll(() => getX(targetEventLocator))
.toBeLessThan(boundingBoxBeforeMoving.x + boundingBoxBeforeMoving.width);
});
test('holding end of event', async ({ page }) => {
// Given
const targetEventLocator = page.locator(TARGET_EVENT_SELECTOR);
const boundingBoxBeforeMoving = await getBoundingBox(targetEventLocator);
const fourthOfWeekCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(3);
// When
await dragAndDrop({
page,
sourceLocator: targetEventLocator,
targetLocator: fourthOfWeekCellLocator,
options: {
sourcePosition: {
x: (boundingBoxBeforeMoving.width * 5) / 6,
y: boundingBoxBeforeMoving.height / 2,
},
},
});
// Then
await expect
.poll(() => getX(targetEventLocator))
.toBeCloseTo(boundingBoxBeforeMoving.x + boundingBoxBeforeMoving.width / 3, 1);
await expect
.poll(() => getX(targetEventLocator))
.toBeLessThan(boundingBoxBeforeMoving.x + boundingBoxBeforeMoving.width);
});
});
test('When pressing down the ESC key, the moving event resets to the initial position.', async ({
page,
}) => {
// Given
const eventLocator = page.locator(TARGET_EVENT_SELECTOR);
const eventBoundingBoxBeforeMove = await getBoundingBox(eventLocator);
const targetCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(4);
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: targetCellLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const eventBoundingBoxAfterMove = await getBoundingBox(eventLocator);
expect(eventBoundingBoxAfterMove).toEqual(eventBoundingBoxBeforeMove);
});
test.describe('CSS class for a move event', () => {
test('should be applied depending on a dragging state.', async ({ page }) => {
// Given
const eventLocator = page.locator(TARGET_EVENT_SELECTOR);
const eventBoundingBox = await getBoundingBox(eventLocator);
const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);
// When (a drag has not started yet)
await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 10);
await page.mouse.down();
// Then
expect(await moveEventClassLocator.count()).toBe(0);
// When (a drag is working)
await page.mouse.move(eventBoundingBox.x + 10, eventBoundingBox.y + 50);
// Then
expect(await moveEventClassLocator.count()).toBe(1);
// When (a drag is finished)
await page.mouse.up();
// Then
expect(await moveEventClassLocator.count()).toBe(0);
});
test('should not be applied when a drag is canceled.', async ({ page }) => {
// Given
const eventLocator = page.locator(TARGET_EVENT_SELECTOR);
const moveEventClassLocator = page.locator(MOVE_EVENT_SELECTOR);
// When
await dragAndDrop({
page,
sourceLocator: eventLocator,
targetLocator: eventLocator,
options: {
targetPosition: { x: 10, y: 30 },
},
});
await page.keyboard.down('Escape');
// Then
expect(await moveEventClassLocator.count()).toBe(0);
});
});

View File

@@ -0,0 +1,130 @@
import type { Locator } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { mockWeekViewEvents } from '../../stories/mocks/mockWeekViewEvents';
import { WEEK_VIEW_PAGE_URL } from '../configs';
import {
dragAndDrop,
getBoundingBox,
getHorizontalEventSelector,
getPrefixedClassName,
} from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(WEEK_VIEW_PAGE_URL);
});
const ALL_DAY_GRID_CELL_SELECTOR = `${getPrefixedClassName(
'panel'
)}:has-text("All Day") ${getPrefixedClassName('panel-grid')}`;
const RESIZE_HANDLER_SELECTOR = getPrefixedClassName('handle-y');
const RESIZE_EVENT_SELECTOR = '[class*="dragging--resize-horizontal-event"]';
const [TARGET_EVENT] = mockWeekViewEvents.filter(({ isAllday }) => isAllday);
const TARGET_EVENT_SELECTOR = getHorizontalEventSelector(TARGET_EVENT);
async function getWidth(locator: Locator) {
const boundingBox = await getBoundingBox(locator);
return boundingBox.width;
}
/**
* Suppose we have the following cells in the week view.
* Each number represents the index of the cell.
*
* [ 0, 1, 2, 3, 4, 5, 6]
*/
test('resizing allday grid row event from left to right', async ({ page }) => {
// Given
const targetEventLocator = page.locator(TARGET_EVENT_SELECTOR);
const boundingBoxBeforeResizing = await getBoundingBox(targetEventLocator);
const resizerLocator = targetEventLocator.locator(RESIZE_HANDLER_SELECTOR);
const endOfWeekCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).last();
// When
await dragAndDrop({ page, sourceLocator: resizerLocator, targetLocator: endOfWeekCellLocator });
// Then
await expect
.poll(() => getWidth(targetEventLocator))
.toBeGreaterThan(boundingBoxBeforeResizing.width);
});
test.describe('When pressing down the ESC key', () => {
test('the resizing event resets to the initial size.', async ({ page }) => {
// Given
const eventLocator = page.locator(TARGET_EVENT_SELECTOR);
const eventBoundingBoxBeforeResize = await getBoundingBox(eventLocator);
const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);
const targetCellLocator = page.locator(ALL_DAY_GRID_CELL_SELECTOR).nth(4);
// When
await dragAndDrop({
page,
sourceLocator: resizeHandlerLocator,
targetLocator: targetCellLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const eventBoundingBoxAfterResize = await getBoundingBox(eventLocator);
expect(eventBoundingBoxAfterResize).toEqual(eventBoundingBoxBeforeResize);
});
});
test.describe('CSS class for a resize event', () => {
test('should be applied depending on a dragging state.', async ({ page }) => {
// Given
const eventLocator = page.locator(TARGET_EVENT_SELECTOR);
const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);
const resizeHandlerBoundingBox = await getBoundingBox(resizeHandlerLocator);
const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);
// When (a drag has not started yet)
await page.mouse.move(resizeHandlerBoundingBox.x + 1, resizeHandlerBoundingBox.y + 3);
await page.mouse.down();
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
// When (a drag is working)
await page.mouse.move(resizeHandlerBoundingBox.x + 10, resizeHandlerBoundingBox.y + 50);
// Then
expect(await resizeEventClassLocator.count()).toBe(1);
// When (a drag is finished)
await page.mouse.up();
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
});
test('should not be applied when a drag is canceled.', async ({ page }) => {
// Given
const eventLocator = page.locator(TARGET_EVENT_SELECTOR);
const resizeHandlerLocator = eventLocator.locator(RESIZE_HANDLER_SELECTOR);
const resizeEventClassLocator = page.locator(RESIZE_EVENT_SELECTOR);
// When
await dragAndDrop({
page,
sourceLocator: resizeHandlerLocator,
targetLocator: resizeHandlerLocator,
options: {
targetPosition: { x: 10, y: 30 },
},
hold: true,
});
await page.keyboard.down('Escape');
// Then
expect(await resizeEventClassLocator.count()).toBe(0);
});
});

View File

@@ -0,0 +1,89 @@
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { assertDayGridSelectionMatching } from '../assertions';
import { WEEK_VIEW_PAGE_URL } from '../configs';
import { ClickDelay } from '../constants';
import { dragAndDrop, getPrefixedClassName, selectGridCells } from '../utils';
test.beforeEach(async ({ page }) => {
await page.goto(WEEK_VIEW_PAGE_URL);
});
const WEEK_GRID_CELL_SELECTOR = getPrefixedClassName('panel-grid');
const DAY_GRID_SELECTION_SELECTOR = getPrefixedClassName('grid-selection');
function selectWeekGridCells(page: Page, startCellIndex: number, endCellIndex: number) {
return selectGridCells(page, startCellIndex, endCellIndex, WEEK_GRID_CELL_SELECTOR);
}
function assertWeekGridSelectionMatching(page: Page, startIndex: number, endIndex: number) {
return assertDayGridSelectionMatching(
page,
startIndex,
endIndex,
WEEK_GRID_CELL_SELECTOR,
DAY_GRID_SELECTION_SELECTOR
);
}
test('select a cell by clicking.', async ({ page }) => {
// Given
const weekGridCellLocator = page.locator(WEEK_GRID_CELL_SELECTOR).nth(14);
// When
await weekGridCellLocator.click({ delay: ClickDelay.Short });
// Then
await assertWeekGridSelectionMatching(page, 14, 14);
});
test('select a cell by double clicking.', async ({ page }) => {
// Given
const weekGridCellLocator = page.locator(WEEK_GRID_CELL_SELECTOR).nth(14);
// When
await weekGridCellLocator.dblclick({ delay: ClickDelay.Immediate });
// Then
await assertWeekGridSelectionMatching(page, 14, 14);
});
test('select 2 cells from left to right', async ({ page }) => {
await selectWeekGridCells(page, 14, 15);
await assertWeekGridSelectionMatching(page, 14, 15);
});
test('select 2 cells from right to left(reverse)', async ({ page }) => {
await selectWeekGridCells(page, 15, 14);
await assertWeekGridSelectionMatching(page, 14, 15);
});
test('event form popup with grid selection', async ({ page }) => {
await selectWeekGridCells(page, 14, 15);
const floatingLayer = page.locator('css=[role=dialog]');
expect(floatingLayer).not.toBeNull();
});
test('When pressing down the ESC key, the grid selection is canceled.', async ({ page }) => {
// Given
const startCellLocator = page.locator(WEEK_GRID_CELL_SELECTOR).nth(14);
const targetCellLocator = page.locator(WEEK_GRID_CELL_SELECTOR).nth(15);
// When
await dragAndDrop({
page,
sourceLocator: startCellLocator,
targetLocator: targetCellLocator,
hold: true,
});
await page.keyboard.down('Escape');
// Then
const gridSelectionLocator = page.locator(DAY_GRID_SELECTION_SELECTOR);
expect(await gridSelectionLocator.count()).toBe(0);
});

Some files were not shown because too many files have changed in this diff Show More