initial commit

This commit is contained in:
equippedcoding-master
2025-09-17 09:37:06 -05:00
parent 86108ca47e
commit e2c98790b2
55389 changed files with 6206730 additions and 0 deletions
@@ -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>
+1
View File
@@ -0,0 +1 @@
@@ -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));
}
?>
@@ -0,0 +1,4 @@
/* styles */
@@ -0,0 +1,17 @@
module.exports = {
extends: ['tui', 'plugin:prettier/recommended'],
parserOptions: {
ecmaVersion: 3,
sourceType: 'module'
},
env: {
commonjs: true,
jest: true,
amd: true,
node: true
},
globals: {
loadFixtures: true,
xhrMock: true
}
};
@@ -0,0 +1,37 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: Bug
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
## Expected behavior
A clear and concise description of what you expected to happen.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Desktop (please complete the following information):
- OS: [e.g. iOS]
- Browser: [e.g. chrome, safari]
- Version: [e.g. 22]
## Smartphone (please complete the following information):
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser: [e.g. stock browser, safari]
- Version: [e.g. 22]
## Additional context
Add any other context about the problem here.
@@ -0,0 +1,30 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: Enhancement, Need Discussion
assignees: ''
---
<!--
Thank you for your contribution.
When it comes to write an issue, please, use the template below.
To use the template is mandatory for submit new issue and we won't reply the issue that without the template.
-->
## Version
Write the version that you are currently using.
## Development Environment
Write the browser type, OS and so on.
## Current Behavior
Write a description of the current operation. You can add sample code, 'CodePen' or 'jsfiddle' links.
```js
// Write example code
```
## Expected Behavior
Write a description of the future action.
@@ -0,0 +1,33 @@
---
name: Question
about: Create a question about the Tree
title: ''
labels: Question
assignees: ''
---
<!--
To make it easier for us to help you, please include as much useful information as possible.
Useful Links:
- tutorial: https://github.com/nhn/tui.tree/tree/master/docs
- API/Example: https://nhn.github.io/tui.tree/latest/
Before opening a new issue, please search existing issues https://github.com/nhn/tui.tree/issues
-->
## Summary
A clear and concise description of what the question is.
## Screenshots
If applicable, add screenshots to help explain your question.
## Version
Write the version of the Editor you are currently using.
## Additional context
Add any other context about the problem here.
+26
View File
@@ -0,0 +1,26 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 30
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- Type: Feature
- Type: Enhancement
- Type: Bug
- NHN
# Label to use when marking as stale
staleLabel: inactive
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as inactive because there hasnt been much going on it lately.
It is going to be closed after 7 days. Thanks!
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
@@ -0,0 +1,11 @@
name: Jest Test
on: pull_request
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install modules
run: npm install
- name: Run tests
run: npm run test
@@ -0,0 +1,42 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Compiled files
# Dependency directory
node_modules
# Bower Components
bower_components
#JSDOC
doc
# IDEA
.idea
*.iml
# Window
Thumbs.db
Desktop.ini
# MACgi
.DS_Store
# eclipse
.project
.metadata
# Atom
tags
.ctags
.tern-project
# etc
.agignore
@@ -0,0 +1,4 @@
{
"printWidth" : 100,
"singleQuote" : true
}
@@ -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
@@ -0,0 +1,91 @@
# 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. Search the list and try reproduce on [demo][demo] before you create an issue. When you create an issue, please provide the following information by filling in the template.
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/pasteable 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 a TOAST UI product, please follow this guideline to help maintainers and the community understand your suggestion.
Before creating suggestions, please check [issue list](../../../labels/feature%20request) 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/pasteable 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 a TOAST UI product which the suggestion is related to.
* **Explain why this enhancement would be useful** to most TOAST UI users.
* **List some other products 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 test`
- Make a pull request
### Development environment
- Prepare your machine node and it's packages installed.
- Checkout our repository
- Install dependencies by `npm install`
- Start webpack-dev-server by `npm run serve`
### Make changes
#### Checkout a branch
- **develop**: PR base branch. merge features, updates for next minor or major release
- **master**: bug fix or document update for next patch release. develop branch will rebase every time master branch update. so keep code change to a minimum.
- **production**: lastest release branch with distribution files. never make a PR on this
- **gh-pages**: API docs, examples and demo
#### Check Code Style
Run `npm run eslint` 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@nhn.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)
@@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2019 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.
@@ -0,0 +1,262 @@
# TOAST UI Component : Tree
> Component that displays data hierarchically.
[![GitHub release](https://img.shields.io/github/release/nhn/tui.tree.svg)](https://github.com/nhn/tui.tree/releases/latest)
[![npm](https://img.shields.io/npm/v/tui-tree.svg)](https://www.npmjs.com/package/tui-tree)
[![GitHub license](https://img.shields.io/github/license/nhn/tui.tree.svg)](https://github.com/nhn/tui.tree/blob/master/LICENSE)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.tree/labels/Help%20Wanted%20🤝)
[![code with hearth by NHN](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN-ff1414.svg)](https://github.com/nhn)
<p><a href="https://nhn.github.io/tui.tree/latest/"><img src="https://user-images.githubusercontent.com/8615506/64508620-d784ef00-d318-11e9-82b1-9cb93a2955c5.gif" /></a></p>
## 🚩 Table of Contents
- [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source)
- [📙 Documents](#-documents)
- [🎨 Features](#-features)
- [🐾 Examples](#-examples)
- [💾 Install](#-install)
- [Via Package Manager](#via-package-manager)
- [npm](#npm)
- [Via Contents Delivery Network (CDN)](#via-contents-delivery-network-cdn)
- [Download Source Files](#download-source-files)
- [🔨 Usage](#-usage)
- [HTML](#html)
- [JavaScript](#javascript)
- [Using namespace in browser environment](#using-namespace-in-browser-environment)
- [Using module format in node environment](#using-module-format-in-node-environment)
- [🔩 Dependency](#-dependency)
- [🌏 Browser Support](#-browser-support)
- [🔧 Pull Request Steps](#-pull-request-steps)
- [Setup](#setup)
- [Develop](#develop)
- [Running dev server](#running-dev-server)
- [Running test](#running-test)
- [Pull Request](#pull-request)
- [💬 Contributing](#-contributing)
- [🍞 TOAST UI Family](#-toast-ui-family)
- [📜 License](#-license)
## Collect statistics on the use of open source
TOAST UI Tree applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI Tree is used throughout the world.
It also serves as important index to determine the future course of projects.
`location.hostname` (e.g. > “ui.toast.com") is to be collected and the sole purpose is nothing but to measure statistics on the usage.
To disable GA, use the following `usageStatistics` option when creating the instance.
```js
const options = {
...
usageStatistics: false
}
const instance = new Tree(container, options);
```
Or, include [`tui-code-snippet`](https://github.com/nhn/tui.code-snippet)(**v2.3.0** or **later**) and then immediately write the options as follows:
```js
tui.usageStatistics = false;
```
## 📙 Documents
- [Getting Started](https://github.com/nhn/tui.tree/blob/master/docs/getting-started.md)
- [How to use Ajax feature](https://github.com/nhn/tui.tree/blob/master/docs/ajax-feature.md)
- [APIs](https://nhn.github.io/tui.tree/latest)
- [v4.0.0 Migration Guide](https://github.com/nhn/tui.tree/blob/master/docs/v4.0.0-migration-guide.md)
You can also see the older versions of API page on the [releases page](https://github.com/nhn/tui.tree/releases).
## 🎨 Features
- Creates each node hierarchically by data.
- Folds or unfolds the children of each node.
- Supports optional features.
- `Selectable` : Each node can be selected.
- `Draggable` : Each node can be moved.
- `Editable` : Each node can be edited.
- `ContextMenu` : A context menu can be created for each node. (_Not supporting IE8_)
- `Checkbox` : A checkbox can be added to each node and a 3-state checkbox is used.
- `Ajax` : Requests server and handles the `CRUD` for each node.
- Supports templates.
- Supports custom events.
- Provides the file of default css style.
## 🐾 Examples
- [Basic](https://nhn.github.io/tui.tree/latest/tutorial-example01-basic) : Example of using default options.
- [Using checkbox](https://nhn.github.io/tui.tree/latest/tutorial-example07-checkbox) : Example of adding checkbox on each node and handling.
- [Using Ajax](https://nhn.github.io/tui.tree/latest/tutorial-example08-ajax) : Example of using server request, `Selectable`, `Draggable`, `Editable`, `ContextMenu` features.
More examples can be found on the left sidebar of each example page, and have fun with it.
## 💾 Install
TOAST UI products can be used by using the package manager or downloading the source directly.
However, we highly recommend using the package manager.
### Via Package Manager
TOAST UI products are registered in the package manager, [npm](https://www.npmjs.com/).
You can conveniently install it using the commands provided by each package manager.
When using npm, be sure to use it in the environment [Node.js](https://nodejs.org/ko/) is installed.
#### npm
```sh
$ npm install --save tui-tree # Latest version
$ npm install --save tui-tree@<version> # Specific version
```
### Via Contents Delivery Network (CDN)
TOAST UI products are available over the CDN powered by [TOAST Cloud](https://www.toast.com).
You can use the CDN as below.
```html
<script src="https://uicdn.toast.com/tui-tree/latest/tui-tree.js"></script>
<link
rel="stylesheet"
type="text/css"
href="https://uicdn.toast.com/tui-tree/latest/tui-tree.css"
/>
```
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.
```
tui-tree/
├─ latest/
│ ├─ tui-tree.css
│ ├─ tui-tree.js
│ ├─ tui-tree.min.css
│ └─ tui-tree.min.js
├─ v3.3.0/
│ ├─ ...
```
### Download Source Files
- [Download bundle files and all sources for each version](https://github.com/nhn/tui.tree/releases)
## 🔨 Usage
### HTML
Add the container element to create the component. A wrapper element should have `tui-tree-wrap` as a class name to apply tui-tree's style.
```html
<div id="tree" class="tui-tree-wrap"></div>
```
### JavaScript
This can be used by creating an instance with the constructor function.
To get the constructor function, you should import the module using one of the following ways depending on your environment.
#### Using namespace in browser environment
```javascript
const Tree = tui.Tree;
```
#### Using module format in node environment
```javascript
const Tree = require('tui-tree'); /* CommonJS */
```
```javascript
import Tree from 'tui-tree'; /* ES6 */
```
You can create an instance with [options](https://nhn.github.io/tui.tree/latest/Tree) and call various APIs after creating an instance.
```javascript
const container = document.getElementById('tree');
const instance = new Tree(container, { ... });
instance.add( ... );
```
For more information about the API, please see [here](https://nhn.github.io/tui.tree/latest/Tree).
## 🔩 Dependency
- [tui-context-menu](https://github.com/nhn/tui.context-menu) >=2.1.6 (Optional, needs forusing `ContextMenu` feature, _not supporting IE8_)
## 🌏 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 |
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| Yes | 8+ | Yes | Yes | Yes |
- ContextMenu feature does not support IE8.
## 🔧 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 `develop` branch into your personal repository.
Clone it to local computer. Install node modules.
Before starting development, you should check to haveany errors.
```sh
$ git clone https://github.com/{your-personal-repo}/tui.tree.git
$ cd tui.tree
$ npm install
$ npm run test
```
### Develop
Let's start development!
You can see your code is reflected as soon as you saving the codes by running a server.
Don't miss adding test cases and then make green rights.
#### Running dev server
```sh
$ npm run serve
$ npm run serve:ie8 # Run on Internet Explorer 8
```
#### Running test
```sh
$ npm run test
```
### 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](https://github.com/nhn/tui.tree/blob/master/CODE_OF_CONDUCT.md)
- [Contributing guideline](https://github.com/nhn/tui.tree/blob/master/CONTRIBUTING.md)
- [Issue guideline](https://github.com/nhn/tui.tree/blob/master/docs/ISSUE_TEMPLATE.md)
- [Commit convention](https://github.com/nhn/tui.tree/blob/master/docs/COMMIT_MESSAGE_CONVENTION.md)
## 🍞 TOAST UI Family
- [TOAST UI Editor](https://github.com/nhn/tui.editor)
- [TOAST UI Calendar](https://github.com/nhn/tui.calendar)
- [TOAST UI Chart](https://github.com/nhn/tui.chart)
- [TOAST UI Image-Editor](https://github.com/nhn/tui.image-editor)
- [TOAST UI Grid](https://github.com/nhn/tui.grid)
- [TOAST UI Components](https://github.com/nhn)
## 📜 License
This software is licensed under the [MIT](https://github.com/nhn/tui.tree/blob/master/LICENSE) © [NHN](https://github.com/nhn).
+371
View File
@@ -0,0 +1,371 @@
/*!
* TOAST UI Tree
* @version 4.0.9
* @author NHN FE Development Lab <dl_javascript@nhn.com>
* @license MIT
*/
@charset "utf-8";
.tui-ico-check,
.tui-ico-checked,
.tui-ico-file,
.tui-ico-folder,
.tui-ico-root,
.tui-ico-tree {
overflow: hidden;
display: inline-block;
width: 1px;
height: 1px;
line-height: 300px;
vertical-align: top;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADoAAAA8CAYAAAA34qk1AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0RDQxMjczNTJDREVFNjExQjg1QkJGNjEzMzRERjdGQyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoyMjg2Q0FCMUUyQzAxMUU2OTFBMURFRDYyQkYyRTFCOCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoyMjg2Q0FCMEUyQzAxMUU2OTFBMURFRDYyQkYyRTFCOCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjBEREQ0MTY3MTRFMkU2MTE5QkI1RjY0Qjc2MTdERDQwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjRENDEyNzM1MkNERUU2MTFCODVCQkY2MTMzNERGN0ZDIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+1pzJHQAABLxJREFUeNrsWnlIFGEUf5O6lmZhZacVrdKhkZRiUXQXlQXRHwsVQVDg0UEUgRXRfWh0X6LdUFmaSYdlRSdd5Nqp/hEpZWoRHWSptVrbm+mbGseZcWac2Z1d98FvZ5nvezPz9l2/75ulrFarHVRIVFQURR8n769QpZ8zryujb7FYhPSHI+4K6SUlJbFfhfRGI24J6XnTH5GRkYoeMj8/HzQWivP9LGItYqxCvUzEaklDDSbxiEIRj/GN48p8xAsxPYcYSknEmoB8QASJjNkb0eskpucQQ+cObQMUWnvgXiWoMEBKZOu10NKgdv5e4OtdP7JmRQfA1Ah/qPha59R8aKFleK6cGAirJgX+M9YysDVMj2wNh+9XQk5BtXsYSsfQITSob2cTrJgQCNPQi7OHBMCJvG+Q/azK6RVO09AtfGeDdZc+Q/+uJiYvs55WQbr1uyFKuebF6Fm5DTbmfoFB3X3hyINKw/QsXaru47c/GciRzMxMSsm1OcyIclroGlmczoxEuLIo1wWoUc91deCuTRW5XJcvaxCjlPBGZ3tUVHZPqFHIKJtbjjZ1PZmYmKgox5KTk12fMPByrFlU3TEq1pMu6VHDicdQt2ZGFxO6SE6ekvJO8wdgq7fsPlpcrCrXvd3dk2azuaGhenhMJTMS5awSXJfZ5EBsMHKOUhycgb/7s2pkPWK5LqGLTEfrvii5PysjNzchfiG2GD1HJfdn5f7+xNhtuhsaGxt7Eg8HETd4Q/QyKj4tLW26CgPkpIDj+qjdzjz3Kfj7PmQwZygakYVI1+O2AvgfunL6GFZGWee41bsbwHmSbzkc7nsBsQhxzt22Umiv+iOuIWrJDsBxpzAjgfUk7eFEktCMFIjnUoO5AnII0ZHophhtmZZEqtZWGdeQnJuamkofNsfFxf07h4XI8Y1axKPsuSWIHQU9F0l5tN5cDodtcLPi/zxVq+LTqH2CFFBAtiN+I3bJuGi9uaS9pJMCxBe6v51GPBIY20fa0hPe+QgEHRbztFymcSlZY0aKzT2AOIaYIaBjReSC8NZkHhkL45zrjbiKyBe5rxgkc9SuoBFLzb2JiCEe9SNFiRXa0/R7xGzETMRlzthRRADiCiH3NsR1Os/Za7DhaKT28hAxjjx0EClcrNC91ILIQCxEnOCM7eEY+5uE8k4j91Fa6sjDCkkR4iNiGM9Qlpy3JxHS5F1FvQ2NQlwiRGE/b6wXCckMsaUVylJX2DMaQUJvsYCR4Yh7dEtFLJNoFXatCL+eHk1AzBHhtdvJTgD/B4CQkBA5CwbjGIrsp0FbQXLCfo0hbMpxzEjOL6RmL0loR7GkpET2ZparrV4cYoQsjyL/tKt8aIYY7L39VZX+gpFtNdFX5NHg4GBFNykrKwNXE8+7F4+hKoT+c1V0T1/3MdTkRYEXr0QM6GaCoeaWmu1nGqK9xPT3g9pfdsgtrAY8QFhnE4wIbcX8iyzvTeP/JMNKKjmOFdoYHn1c+hN6BPrA+H5+0KeTD4zu3Qqel9vgfskPp+eoph4t/VIHl4uqYFK4P4QG+UDRexvceVUjW78pHnM4M3r9qQ6uFlWDuYMP3HxZA0YRXShg8cdaBp4+6jFU59B1Re6qVP4IMAASImaW2i3dswAAAABJRU5ErkJggg==) no-repeat;
}
.tui-ico-check {
width: 14px;
height: 14px;
background-position: 0px 0px;
}
.tui-ico-checked {
width: 14px;
height: 14px;
background-position: -16px 0px;
}
.tui-ico-file {
width: 9px;
height: 12px;
background-position: 0px -32px;
}
.tui-ico-folder {
width: 12px;
height: 10px;
background-position: -16px -16px;
}
.tui-ico-root {
width: 14px;
height: 14px;
background-position: 0px -16px;
}
.tui-ico-tree {
width: 5px;
height: 8px;
background-position: -32px -31px;
}
.tui-tree-wrap {
width: 300px;
height: auto;
padding: 30px 0;
margin: 0;
font-size: 11px;
background-color: #f9f9f9;
}
.tui-tree-root {
overflow-y: auto;
}
.tui-tree-wrap .tui-tree,
.tui-tree-wrap .tui-tree-subtree {
margin: 0;
padding-left: 0;
}
.tui-tree-wrap .tui-tree-node {
list-style: none;
padding: 0;
margin: 0;
min-height: 24px;
cursor: pointer;
}
.tui-tree-wrap .tui-tree-node {
position: relative;
}
.tui-tree-wrap .tui-tree-toggle-btn {
float: left;
margin: 6px 4px 0 0;
width: 13px;
height: 12px;
font-size: 0;
padding: 0;
color: #000000;
vertical-align: top;
border: 0;
border-radius: 0;
background-color: transparent;
outline: none;
cursor: pointer;
}
.tui-tree-root > .tui-tree-node > .tui-tree-toggle-btn {
margin: 11px 1px 0 0;
}
.tui-tree-content-wrapper .tui-tree-ico {
margin: 0 8px 0 0;
}
.tui-tree-content-wrapper .tui-ico-folder {
margin: 2px 8px 0 0;
}
.tui-tree-content-wrapper .tui-ico-file {
margin: 0 9px 0 2px;
}
.tui-tree-text {
overflow: hidden;
position: relative;
display: block;
padding: 5px 0;
font-size: 12px;
font-weight: 400;
text-overflow: ellipsis;
color: #777;
*display: inline;
zoom: 1;
}
.tui-tree-root .tui-tree-rootBtn .tui-tree-text {
font-size: 14px;
}
.tui-tree-root .tui-tree-rootBtn .tui-tree-toggle-btn {
margin-top: 7px;
}
.tui-tree-opened > .tui-tree-content-wrapper .tui-tree-text {
font-weight: 700;
color: #333;
}
.tui-tree-root > .tui-tree-opened > .tui-tree-content-wrapper .tui-tree-text {
color: #333;
}
.tui-tree-opened .tui-ico-folder {
background-position: -32px 0px;
}
.tui-tree-opened .tui-ico-tree {
width: 8px;
height: 5px;
background-position: -22px -28px;
}
.tui-tree-closed .tui-ico-tree {
width: 5px;
height: 8px;
background-position: -22px -35px;
}
.tui-tree-closed .tui-ico-folder {
background-position: -16px -16px;
}
.tui-tree-leaf > .tui-tree-content-wrapper .tui-tree-text {
padding-left: 17px;
}
.tui-tree-input {
position: absolute;
top: -2px;
width: 128px;
margin: 0 0 0 34px;
padding: 7px 9px;
height: 12px;
border: 1px solid #aaa;
outline: none;
}
.tui-tree-line {
margin: 0;
padding: 0;
width: 300px;
height: 1px;
background-color: #4b96e6;
font-size: 0;
}
.tui-tree-drop {
padding: 5px 22px 5px 10px;
height: 12px;
font-weight: normal;
background-color: #fff;
border: 1px solid #4b96e6;
color: #4b96e6;
}
.tui-tree-drop .tui-ico-folder,
.tui-tree-drop .tui-ico-file {
margin-right: 6px;
}
.tui-tree-selector {
position: absolute;
width: 100%;
height: 24px;
top: 0;
left: 0;
right: 0;
background-color: #fff;
}
.tui-tree-loader {
position: absolute;
margin-left: -16px;
margin-top: -16px;
left: 50%;
top: 50%;
padding: 0;
width: 32px;
height: 32px;
background: url(data:image/gif;base64,R0lGODlhIAAgAPMAAP///wAAAMbGxoSEhLa2tpqamjY2NlZWVtjY2OTk5Ly8vB4eHgQEBAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAIAAgAAAE5xDISWlhperN52JLhSSdRgwVo1ICQZRUsiwHpTJT4iowNS8vyW2icCF6k8HMMBkCEDskxTBDAZwuAkkqIfxIQyhBQBFvAQSDITM5VDW6XNE4KagNh6Bgwe60smQUB3d4Rz1ZBApnFASDd0hihh12BkE9kjAJVlycXIg7CQIFA6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YJvpJivxNaGmLHT0VnOgSYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ/V/nmOM82XiHRLYKhKP1oZmADdEAAAh+QQJCgAAACwAAAAAIAAgAAAE6hDISWlZpOrNp1lGNRSdRpDUolIGw5RUYhhHukqFu8DsrEyqnWThGvAmhVlteBvojpTDDBUEIFwMFBRAmBkSgOrBFZogCASwBDEY/CZSg7GSE0gSCjQBMVG023xWBhklAnoEdhQEfyNqMIcKjhRsjEdnezB+A4k8gTwJhFuiW4dokXiloUepBAp5qaKpp6+Ho7aWW54wl7obvEe0kRuoplCGepwSx2jJvqHEmGt6whJpGpfJCHmOoNHKaHx61WiSR92E4lbFoq+B6QDtuetcaBPnW6+O7wDHpIiK9SaVK5GgV543tzjgGcghAgAh+QQJCgAAACwAAAAAIAAgAAAE7hDISSkxpOrN5zFHNWRdhSiVoVLHspRUMoyUakyEe8PTPCATW9A14E0UvuAKMNAZKYUZCiBMuBakSQKG8G2FzUWox2AUtAQFcBKlVQoLgQReZhQlCIJesQXI5B0CBnUMOxMCenoCfTCEWBsJColTMANldx15BGs8B5wlCZ9Po6OJkwmRpnqkqnuSrayqfKmqpLajoiW5HJq7FL1Gr2mMMcKUMIiJgIemy7xZtJsTmsM4xHiKv5KMCXqfyUCJEonXPN2rAOIAmsfB3uPoAK++G+w48edZPK+M6hLJpQg484enXIdQFSS1u6UhksENEQAAIfkECQoAAAAsAAAAACAAIAAABOcQyEmpGKLqzWcZRVUQnZYg1aBSh2GUVEIQ2aQOE+G+cD4ntpWkZQj1JIiZIogDFFyHI0UxQwFugMSOFIPJftfVAEoZLBbcLEFhlQiqGp1Vd140AUklUN3eCA51C1EWMzMCezCBBmkxVIVHBWd3HHl9JQOIJSdSnJ0TDKChCwUJjoWMPaGqDKannasMo6WnM562R5YluZRwur0wpgqZE7NKUm+FNRPIhjBJxKZteWuIBMN4zRMIVIhffcgojwCF117i4nlLnY5ztRLsnOk+aV+oJY7V7m76PdkS4trKcdg0Zc0tTcKkRAAAIfkECQoAAAAsAAAAACAAIAAABO4QyEkpKqjqzScpRaVkXZWQEximw1BSCUEIlDohrft6cpKCk5xid5MNJTaAIkekKGQkWyKHkvhKsR7ARmitkAYDYRIbUQRQjWBwJRzChi9CRlBcY1UN4g0/VNB0AlcvcAYHRyZPdEQFYV8ccwR5HWxEJ02YmRMLnJ1xCYp0Y5idpQuhopmmC2KgojKasUQDk5BNAwwMOh2RtRq5uQuPZKGIJQIGwAwGf6I0JXMpC8C7kXWDBINFMxS4DKMAWVWAGYsAdNqW5uaRxkSKJOZKaU3tPOBZ4DuK2LATgJhkPJMgTwKCdFjyPHEnKxFCDhEAACH5BAkKAAAALAAAAAAgACAAAATzEMhJaVKp6s2nIkolIJ2WkBShpkVRWqqQrhLSEu9MZJKK9y1ZrqYK9WiClmvoUaF8gIQSNeF1Er4MNFn4SRSDARWroAIETg1iVwuHjYB1kYc1mwruwXKC9gmsJXliGxc+XiUCby9ydh1sOSdMkpMTBpaXBzsfhoc5l58Gm5yToAaZhaOUqjkDgCWNHAULCwOLaTmzswadEqggQwgHuQsHIoZCHQMMQgQGubVEcxOPFAcMDAYUA85eWARmfSRQCdcMe0zeP1AAygwLlJtPNAAL19DARdPzBOWSm1brJBi45soRAWQAAkrQIykShQ9wVhHCwCQCACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiRMDjI0Fd30/iI2UA5GSS5UDj2l6NoqgOgN4gksEBgYFf0FDqKgHnyZ9OX8HrgYHdHpcHQULXAS2qKpENRg7eAMLC7kTBaixUYFkKAzWAAnLC7FLVxLWDBLKCwaKTULgEwbLA4hJtOkSBNqITT3xEgfLpBtzE/jiuL04RGEBgwWhShRgQExHBAAh+QQJCgAAACwAAAAAIAAgAAAE7xDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfZiCqGk5dTESJeaOAlClzsJsqwiJwiqnFrb2nS9kmIcgEsjQydLiIlHehhpejaIjzh9eomSjZR+ipslWIRLAgMDOR2DOqKogTB9pCUJBagDBXR6XB0EBkIIsaRsGGMMAxoDBgYHTKJiUYEGDAzHC9EACcUGkIgFzgwZ0QsSBcXHiQvOwgDdEwfFs0sDzt4S6BK4xYjkDOzn0unFeBzOBijIm1Dgmg5YFQwsCMjp1oJ8LyIAACH5BAkKAAAALAAAAAAgACAAAATwEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GGl6NoiPOH16iZKNlH6KmyWFOggHhEEvAwwMA0N9GBsEC6amhnVcEwavDAazGwIDaH1ipaYLBUTCGgQDA8NdHz0FpqgTBwsLqAbWAAnIA4FWKdMLGdYGEgraigbT0OITBcg5QwPT4xLrROZL6AuQAPUS7bxLpoWidY0JtxLHKhwwMJBTHgPKdEQAACH5BAkKAAAALAAAAAAgACAAAATrEMhJaVKp6s2nIkqFZF2VIBWhUsJaTokqUCoBq+E71SRQeyqUToLA7VxF0JDyIQh/MVVPMt1ECZlfcjZJ9mIKoaTl1MRIl5o4CUKXOwmyrCInCKqcWtvadL2SYhyASyNDJ0uIiUd6GAULDJCRiXo1CpGXDJOUjY+Yip9DhToJA4RBLwMLCwVDfRgbBAaqqoZ1XBMHswsHtxtFaH1iqaoGNgAIxRpbFAgfPQSqpbgGBqUD1wBXeCYp1AYZ19JJOYgH1KwA4UBvQwXUBxPqVD9L3sbp2BNk2xvvFPJd+MFCN6HAAIKgNggY0KtEBAAh+QQJCgAAACwAAAAAIAAgAAAE6BDISWlSqerNpyJKhWRdlSAVoVLCWk6JKlAqAavhO9UkUHsqlE6CwO1cRdCQ8iEIfzFVTzLdRAmZX3I2SfYIDMaAFdTESJeaEDAIMxYFqrOUaNW4E4ObYcCXaiBVEgULe0NJaxxtYksjh2NLkZISgDgJhHthkpU4mW6blRiYmZOlh4JWkDqILwUGBnE6TYEbCgevr0N1gH4At7gHiRpFaLNrrq8HNgAJA70AWxQIH1+vsYMDAzZQPC9VCNkDWUhGkuE5PxJNwiUK4UfLzOlD4WvzAHaoG9nxPi5d+jYUqfAhhykOFwJWiAAAIfkECQoAAAAsAAAAACAAIAAABPAQyElpUqnqzaciSoVkXVUMFaFSwlpOCcMYlErAavhOMnNLNo8KsZsMZItJEIDIFSkLGQoQTNhIsFehRww2CQLKF0tYGKYSg+ygsZIuNqJksKgbfgIGepNo2cIUB3V1B3IvNiBYNQaDSTtfhhx0CwVPI0UJe0+bm4g5VgcGoqOcnjmjqDSdnhgEoamcsZuXO1aWQy8KAwOAuTYYGwi7w5h+Kr0SJ8MFihpNbx+4Erq7BYBuzsdiH1jCAzoSfl0rVirNbRXlBBlLX+BP0XJLAPGzTkAuAOqb0WT5AH7OcdCm5B8TgRwSRKIHQtaLCwg1RAAAOwAAAAAAAAAAAA==) no-repeat left;
background-position: 0 0;
font-size: 0;
}
.tui-tree-selected > .tui-tree-content-wrapper {
background: #e7eff7;
background-color: rgba(75, 150, 230, 0.1);
}
.tui-tree-selected > .tui-tree-content-wrapper .tui-tree-text {
font-weight: bold;
color: #4b96e6;
}
.tui-tree-selected > .tui-tree-content-wrapper .tui-ico-folder,
.tui-tree-drop .tui-ico-folder {
background-position: -32px -12px;
}
.tui-tree-selected.tui-tree-opened > .tui-tree-content-wrapper .tui-ico-folder {
background-position: -32px -24px;
}
.tui-tree-selected > .tui-tree-content-wrapper .tui-ico-file,
.tui-tree-drop .tui-ico-file {
background-position: -11px -32px;
}
.tui-tree-content-wrapper {
width: 300px;
overflow: hidden;
padding-left: 26px;
padding-right: 25px;
white-space: nowrap;
text-overflow: ellipsis;
}
.tui-tree-content-wrapper:after {
display: block;
clear: both;
content: '';
}
.tui-tree-content-wrapper:hover,
.tui-tree-hover > .tui-tree-content-wrapper {
background: #e7eff7;
background-color: rgba(75, 150, 230, 0.1);
}
.tui-tree-subtree .tui-tree-content-wrapper {
padding-left: 46px;
}
.tui-tree-subtree .tui-tree-subtree .tui-tree-content-wrapper {
padding-left: 69px;
}
.tui-tree-subtree .tui-tree-subtree .tui-tree-subtree .tui-tree-content-wrapper {
padding-left: 93px;
}
.tui-tree-wrap-checkbox .tui-tree-toggle-btn {
margin-right: 4px;
}
.tui-ico-check {
width: 14px;
height: 14px;
background-position: 0px 0px;
}
.tui-ico-check input {
position: absolute;
left: -9999px;
}
.tui-checkbox {
position: relative;
overflow: hidden;
display: inline-block;
margin-right: 8px;
height: 14px;
font-size: 0;
cursor: pointer;
vertical-align: top;
}
.tui-checkbox.tui-is-checked .tui-ico-check {
background-position: -16px 0;
}
.tui-tree-rootBtn .tui-checkbox-root {
margin: 1px 10px 0 0;
}
.tui-checkbox-root.tui-is-checked .tui-ico-check {
background-position: 0 -16px;
}
.tui-tree-node.tui-tree-drag > .tui-tree-content-wrapper .tui-tree-text {
color: #ddd;
}
.tui-tree-node.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-file {
background-position: -46px -24px;
}
.tui-tree-closed.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-folder {
background-position: -46px 0;
}
.tui-tree-opened.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-folder {
background-position: -46px -12px;
}
.tui-tree-closed.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-tree {
background-position: -29px -35px;
}
.tui-tree-opened.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-tree {
background-position: -36px -35px;
}
.tui-tree-node.tui-tree-drag > .tui-tree-content-wrapper .tui-checkbox .tui-ico-check {
background-position: 0 -46px;
}
.tui-tree-node.tui-tree-drag
> .tui-tree-content-wrapper
.tui-checkbox.tui-is-checked
.tui-ico-check {
background-position: -16px -46px;
}
.tui-tree-node.tui-tree-drag
> .tui-tree-content-wrapper
.tui-checkbox-root.tui-is-checked
.tui-ico-check {
background-position: -32px -46px;
}
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
@@ -0,0 +1,49 @@
# Commit Message Convention
## Commit Message Format
```
<Type>: Short description (fix #1234)
Longer description here if necessary
BREAKING CHANGE: only contain breaking change
```
* Any line of the commit message cannot be longer 100 characters!
## Revert
```
revert: commit <short-hash>
This reverts commit <full-hash>
More description if needed
```
## Type
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **perf**: A code change that improves performance
* **test**: Adding missing or correcting existing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation
## Subject
* use the imperative, __present__ tense: "change" not "changed" nor "changes"
* don't capitalize the first letter
* no dot (.) at the end
* reference GitHub issues at the end. If the commit doesnt completely fix the issue, then use `(refs #1234)` instead of `(fixes #1234)`.
## Body
* use the imperative, __present__ tense: "change" not "changed" nor "changes".
* the motivation for the change and contrast this with previous behavior.
## BREAKING CHANGE
* This commit contains breaking change(s).
* start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this.
This convention is based on [AngularJS](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) and [ESLint](https://eslint.org/docs/developer-guide/contributing/pull-requests#step2)
@@ -0,0 +1,42 @@
<!-- EDIT TITLE PLEASE -->
<!-- It should be one of them
<ISSUE TYPE>: Short Description (<CLOSING TYPE> #<ISSUE NUMBERS>)
ex)
feat: add new feature (close #111)
fix: wrong behavior (fix #111)
chore: change build tool (ref #111)
-->
<!-- SPECIFY A ISSUE TYPE AT HEAD
feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Changes that do not affect the meaning of the code (white-space, formatting etc)
refactor: A code change that neither fixes a bug or adds a feature
perf: A code change that improves performance
test: Adding missing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->
<!-- ADD CLOSING TYPE AND ISSUE NUMBER AT TAIL
(<CLOSING TYPE> #<ISSUE NUMBERS>)
close: resolve not a bug(feature, docs, etc) completely
fix: resolve a bug completely
ref: not fully resolved or related to
-->
### Please check if the PR fulfills these requirements
- [ ] It's submitted to right branch according to our branching model
- [ ] It's right issue type on title
- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [ ] The commit message follows our guidelines
- [ ] Tests for the changes have been added (for bug fixes/features)
- [ ] Docs have been added/updated (for bug fixes/features)
- [ ] It does not introduce a breaking change or has description for the breaking change
### Description
---
Thank you for your contribution to TOAST UI product. 🎉 😘 ✨
@@ -0,0 +1,12 @@
## Tutorials
- [Getting Started](getting-started.md)
- [How to use Ajax feature](ajax-feature.md)
## Documents
- [Code of Conduct](../CODE_OF_CONDUCTING.md)
- [Contributing Guide](../CONTRIBUTING.md)
- [Commit Message Convention](COMMIT_MESSAGE_CONVENTION.md)
- [API & Examples](https://nhn.github.io/tui.tree/latest)
- [v4.0.0 Migration Guide](v4.0.0-migration-guide.md)
@@ -0,0 +1,127 @@
## Description
- When getting data or occuring user action on tree component, this feature is request and response to server using Ajax.
## Feature operation
- When occurring read action on tree component, server is requested by options. And server respond with success data then tree view is updated.
- If server respond with failed data or is on error, tree view is not updated and each custom event can handle current state.
- View on [the example page](https://nhn.github.io/tui.tree/latest/tutorial-example08-ajax)
## How to use
### Step 1. Enable feature with options
```js
const options = {
command: {
read: {
url: 'api/read',
method: 'get',
contentType: 'application/json',
params: {
paramA: 'a',
paramB: 'b'
}
},
create: {
url: 'api/create',
method: 'post',
params: function(data) {
return {
pid: data.parentId
};
}
},
remove: {
url: function(data) {
return 'api/remove/' + data.nodeId;
}
}
},
parseData: function(command, responseData) {
if (command === 'read') {
return responseData.tree;
} else {
return true;
}
}
};
tree.enableFeature('Ajax', options);
```
Information about each option is as follows:
|Name|Type|Description|
|---|---|---|
|`command`|`{object}`|Server request data options of each action|
|`command.read`|[`{ajaxOptions}`](#ajaxOptions-each-options-of-optionscommandcommand_name)|Server request date of read tree nodes action|
|`command.create`|[`{ajaxOptions}`](#ajaxOptions-each-options-of-optionscommandcommand_name)|Server request data of create node action|
|`command.update`|[`{ajaxOptions}`](#ajaxOptions-each-options-of-optionscommandcommand_name)|Server request data of update node action|
|`command.remove`|[`{ajaxOptions}`](#ajaxOptions-each-options-of-optionscommandcommand_name)|Server request data of remove node action|
|`command.removeAllChildren`|[`{ajaxOptions}`](#ajaxOptions-each-options-of-optionscommandcommand_name)|Server request data of remove all children nodes action|
|`command.move`|[`{ajaxOptions}`](#ajaxOptions-each-options-of-optionscommandcommand_name)|Server request data of move node action|
|`parseData`|[`{parseDataCallback}`](#parseDataCallback-parameters-of-optionsparseData)|Callback method that remake server response data|
|`loaderClassName`|`{string}`|Class name of loader image (default `tui-tree-loader`)|
|`isLoadRoot`|`{boolean}`| Whether rerendering nodes from root after Ajax feature is enabled or not|
#### `ajaxOptions`: Each options of `options.command[COMMAND_NAME]`
`command[COMMAND_NAME]` is tree action to reqeust server.
|Name|Type|Description|
|---|---|---|
|`url`| `{string\|Function}` |When setting Restful API, using callback that returns URL|
|`method`|`{string}`|Server request method type (default: `GET`)|
|`contentType`|`{string}`|Server request data type (default: `application/json`)|
|`params`|`{Object\|Function}`|Server request parameters value. When you try to remake parameters, using callback that returns replaced parameters|
Also, you can use other options provided by the Ajax module of tui-code-snippet. Please refer to [tui-code-snippet/ajax API page](https://nhn.github.io/tui.code-snippet/2.3.0/ajax).
When you pass a function as `url` or `params`, the parameters of the function are different depending on which Tree API you used. Please refer to [this](#list-of-tree-api-that-using-ajax-after-feature-is-enabled) for details.
#### `parseDataCallback`: Parameters of `options.parseData`
`parseData` is callback method.
|Name|Description|
|---|---|
|`command`|Each command value to `options.command` option|
|`responseData`|Server response data. If `contentType` is `application/json`, the parsed object will be passed.|
### Step 3. Register custom events
There is no need to register all custom events. When firing custom events, each event callback can use parameters.
```js
tree.on({
beforeAjaxRequest: function(evt) {
// Control server request
...
},
successAjaxResponse: function(evt) {
// Control server success response
...
},
failAjaxResponse: function(evt) {
// Control server fail response (Server respond data, but response data is invalid)
...
},
errorAjaxResponse: function(evt) {
// Control server error response
...
}
});
```
### List of Tree API that using Ajax after feature is enabled
|API|Command Name|Parameters of `url` & `params` callback method|
|---|---|---|
|`open`|`read`|`treeData.nodeId`|
|`toggle`|`read`|`treeData.nodeId`|
|`resetData`|`read`|`treeData.nodeId`|
|`add`|`create`|`treeData.parentId`<br>`treeData.data`|
|`setNodeData`|`update`|`treeData.nodeId`<br>`treeData.data`<br>`treeData.type`|
|`removeNodeData`|`update`|`treeData.nodeId`<br>`treeData.data`<br>`treeData.type`|
|`remove`|`remove`|`treeData.nodeId`|
|`removeAllChildren`|`removeAllChildren`|`treeData.parentId`|
|`move`|`move`|`treeData.nodeId`<br>`treeData.newParentId`<br>`treeData.index`|
### Reference
- API : https://nhn.github.io/tui.tree/latest
@@ -0,0 +1,172 @@
## Install
``` sh
$ npm install --save tui-tree # Latest version
$ npm install --save tui-tree@<version> # Specific version
```
It can also be downloaded by CDN. Please refer to the [💾 Install](https://github.com/nhn/tui.tree#-install).
## Usage
### Write a wrapper element
A wrapper element should have `tui-tree-wrapper` as a class name to apply tui-tree's style.
```html
<div id="tree" class="tui-tree-wrap"></div>
```
### Import a component
```javascript
import Tree from 'tui-tree';
import 'tui-tree/dist/tui-tree.css';
```
It can also be used by namespace or CommonJS module. Please refer to the [🔨 Usage](https://github.com/nhn/tui.pagination#-usage).
### Create an instance
* Create with the selector of the container element
```js
const tree = new Tree('#tree', options);
```
* Create with a container element
```js
const container = document.getElementById('tree');
const tree = new Tree(container, options);
```
* Create with options
```js
const options = {
data: [],
nodeIdPrefix: 'tui-tree-node-',
nodeDefaultState: 'closed',
stateLabels: {
opened: '-',
closed: '+'
},
template: {
internalNode:
'<div class="tui-tree-btn">' +
'<button type="button" class="tui-tree-toggle-btn tui-js-tree-toggle-btn">' +
'<span class="tui-ico-tree"></span>' +
'{{stateLabel}}' +
'</button>' +
'<span class="tui-tree-text tui-js-tree-text">' +
'<span class="tui-tree-ico tui-ico-folder"></span>' +
'{{text}}' +
'</span>' +
'</div>' +
'<ul class="tui-tree-subtree tui-js-tree-subtree">{{children}}</ul>',
leafNode:
'<div class="tui-tree-btn">' +
'<span class="tui-tree-text tui-js-tree-text">' +
'<span class="tui-tree-ico tui-ico-file"></span>' +
'{{text}}' +
'</span>' +
'</div>'
},
renderTemplate: (tmpl, props) => Mustache.render(tmpl, props) // Mustache template engine
};
const tree = new Tree('#tree', options);
```
## Options
Information about each option is as follows:
|Name|Type|Description|
|---|---|---|
|`data`|`{array.<object>}`|Tree data|
|`[nodeIdPrefix]`|`{string}`|Each tree node's id prefix value|
|`[nodeDefaultState]`|`{string}`|Default node's state (`opened` or `closed`)|
|`[stateLabels]`|`{object}`|Toggle button's state label|
|`[stateLabels.opened]`|`{string}`|When state is opened, setting text|
|`[stateLabels.closed]`|`{string}`|When state is closed, setting text|
|`[template]`|`{string}`|Template for rendering each tree node|
|`[template.internalNode]`|`{string}`|Template of node having children|
|`[template.leafNode]`|`{string}`|Template of leaf node|
|`[renderTemplate]`|`{function}`|Render template function|
### Tree data format: `data`
* Default : Set `text`, `children` properties.
```js
const data = [
{
text: 'rootA',
children: [
{ text: 'root-1A' },
{ text: 'root-1B' },
{ text: 'root-1C' },
{
text: 'root-2A',
children: [{ text: 'sub_sub_1A' }]
},
{ text: 'sub_2A' }
]
},
{
text: 'rootB',
children: [{ text: 'B_sub1' }, { text: 'B_sub2' }, { text: 'b' }]
},
...
];
```
* Customizing : It is possible to set other properties.
```js
const data = [
{
pid: '001',
text: 'rootA',
children: [
{
pid: '003',
text: 'root-1A',
state: 'closed'
}
]
},
{
pid: '002',
text: 'rootB',
state: 'opened'
},
...
];
```
### How to use template: `template` and `renderTemplate`
You can customize each tree node's contents using `template` and `renderTemplate` options.
`template` option is override default template string.
`renderTemplate` option can process template using a template engine like [mustache.js](https://github.com/janl/mustache.js/).
tui.tree uses [tui-code-snippet's template](https://nhn.github.io/tui.code-snippet/2.2.0/domUtil#template) as default template engine.
This example show how to replace node's contents having children by a template engine.
```js
{
...,
template: {
// template for Mustache engine
internalNode: '<button type="button">{{stateLabel}}</button>{{text}}<ul>{{{children}}}</ul>'
},
renderTemplate: (tmpl, props) => {
// Mustache template engine
return Mustache.render(tmpl, props);
}
};
```
For more information about the API, please see [here](https://nhn.github.io/tui.tree/latest/Tree).
@@ -0,0 +1,64 @@
## v4.0.0 Migration Guide
### Drop bower support
Since v4.0.0, tui.tree does not support [bower](https://bower.io/) anymore. You can use [npm](https://www.npmjs.com/) and CDN instead of Bower. To know how to install tui.tree by npm or CDN, please refer to the [💾 Install section](https://github.com/nhn/tui.tree#-install).
### Remove jQuery
In v4.0.0, jQuery is removed. It might cause some errors if you pass a jQuery object as a `container`.
Previously, you can use the jQuery object (`$`) to create an instance.
```javascript
// v3
const tree = new Tree($('#tree'), options);
```
Now, you have to use `selector` or `HTMLElement` as a `container`.
```javascript
// v4
const tree = new Tree('#tree', options);
// or
const container = document.getElementById('tree');
const tree = new Tree(container, options);
```
### Use the selector, not id selector
When a string is passed into a `container`, it was considered as the **id of the container** in v3. Now, it is considered as the **selector**.
```javascript
// v3
const tree = new Tree('tree', options);
// v4
const tree = new Tree('#tree', options);
```
### Change Ajax feature options
By replacing the Ajax module (`$.ajax()` -> [tui-code-snippet/ajax](https://nhn.github.io/tui.code-snippet/2.3.0/ajax)), some options have been renamed or removed.
* `command[type].type` -> `command[type].method`: The method of the Ajax request
* `command[type].dataType` -> `command[type].contentType`: The media type of the resource ([MDN reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type))
* `command[type].jsonpCallback` -> removed
* `command[type].data` -> `command[type].params`: The parameters to be sent by the Ajax request
For more details, please refer to the [ajax-feature](./ajax-feature.md).
### Rename event properties
Some event properties are renamed in v4.0:
| Event | v3 | v4 |
| --- | --- | --- |
| [beforeAjaxRequest](https://nhn.github.io/tui.tree/4.0.0/Tree#event-beforeAjaxRequest) | type | command |
| | params | data |
| [successAjaxResponse](https://nhn.github.io/tui.tree/4.0.0/Tree#event-successAjaxResponse) | type | command |
| [failAjaxResponse](https://nhn.github.io/tui.tree/4.0.0/Tree#event-failAjaxResponse) | type | command |
| [errorAjaxResponse](https://nhn.github.io/tui.tree/4.0.0/Tree#event-errorAjaxResponse) | type | command |
| [selectContextMenu](https://nhn.github.io/tui.tree/4.0.0/Tree#event-selectContextMenu) | cmd | command |
@@ -0,0 +1,39 @@
/* It is not component's styling and only use in example page */
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
.explain {
position: absolute;
top: 20px;
left: 380px;
font-size: 12px;
}
.explain textarea {
float: left;
width: 300px;
height: 500px;
resize: none;
line-height: 2em;
border: none;
outline: 0;
}
.explain button {
overflow: hidden;
display: inline-block;
padding: 0 14px 0 15px;
height: 28px;
font-size: 1em;
color: #fff;
border: 0;
vertical-align: top;
line-height: 22px;
background: #777;
cursor: pointer;
border-radius: 5px;
outline: 0;
}
@@ -0,0 +1,20 @@
/* It use in the "template" example page and extended styles for service */
.tui-tree-root-btn .tui-checkbox-root {
margin: 1px 10px 0 0;
}
.tui-tree-root>.tui-tree-node>.tui-tree-toggle-btn {
margin: 11px 1px 0 0;
}
.tui-tree-root .tui-tree-root-btn .tui-tree-text {
font-size: 14px;
}
.tui-tree-root .tui-tree-root-btn .tui-tree-toggle-btn {
margin-top: 7px;
}
.tui-tree-root>.tui-tree-node>.tui-input-wrap {
padding-left: 63px;
}
@@ -0,0 +1,21 @@
body {
margin: 0;
padding: 0;
}
.code-description {
padding: 22px 52px;
background-color: rgba(81, 92, 230, 0.1);
line-height: 1.4em;
}
.code-description,
.code-description a {
font-family: Arial;
font-size: 14px;
color: #515ce6;
}
.code-html {
padding: 20px 52px;
}
@@ -0,0 +1,3 @@
{
"status": "200"
}
@@ -0,0 +1,30 @@
{
"tree": [
{
"pid": "001",
"text": "rootA",
"children": [
{
"pid": "003",
"text": "sub-A1",
"state": "closed",
"hasChild": true
},
{
"pid": "004",
"text": "sub-A2"
},
{
"pid": "005",
"text": "sub-A3"
}
]
},
{
"pid": "002",
"text": "rootB",
"state": "closed",
"hasChild": true
}
]
}
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tree</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="./css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<button id="addChildBtn">add child</button>
<button id="removeChildBtn">remove child</button>
<button id="sortBtn">sort</button>
</div>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
var util = {
addEventListener: function(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
} else {
element.attachEvent('on' + eventName, handler);
}
}
};
var data = [
{text: 'rootA', children: [
{text: 'sub-A1'},
{text: 'sub-A2'},
{text: 'sub-A3'},
{text: 'sub-A4'},
{text: 'sub-A5', state: 'closed', children: [
{text:'sub-A5A', children:[
{text:'sub-A5A1'}
]},
{text:'sub_A5B'}
]},
{text: 'sub-A6'},
{text: 'sub-A7'},
{text: 'sub-A8'},
{text: 'sub-A9', state: 'closed', children: [
{text:'sub-A9A'},
{text:'sub-A9B'}
]},
{text: 'sub-A10'},
{text: 'sub-A11'},
{text: 'sub-A12'}
]},
{text: 'rootB', state:'closed', children: [
{text:'sub-B1'},
{text:'sub-B2'},
{text:'sub-B3'}
]}
];
var tree = new tui.Tree('#tree', {
data: data,
nodeDefaultState: 'opened'
});
var addChildBtn = document.getElementById('addChildBtn');
var removeChildBtn = document.getElementById('removeChildBtn');
var sortBtn = document.getElementById('sortBtn');
var rootNodeId = tree.getRootNodeId();
var firstChildId = tree.getChildIds(rootNodeId)[0];
util.addEventListener(addChildBtn, 'click', function() {
tree.add({text:'hello world'}, firstChildId);
});
util.addEventListener(removeChildBtn, 'click', function() {
var lastGrandChildId = tree.getChildIds(firstChildId).slice(-1)[0];
tree.remove(lastGrandChildId);
});
util.addEventListener(sortBtn, 'click', function() {
tree.sort(function(nodeA, nodeB) {
var aValue = nodeA.getData('text'),
bValue = nodeB.getData('text');
if (!aValue.localeCompare) {
return 0;
}
return aValue.localeCompare(bValue);
});
});
</script>
</body>
</html>
@@ -0,0 +1,152 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tree with template function</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="./css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
<link rel="stylesheet" type="text/css" href="css/template.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<button id="addChildBtn">add</button>
<button id="removeChildBtn">remove</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
var util = {
addEventListener: function(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
} else {
element.attachEvent('on' + eventName, handler);
}
}
};
var data = [
{text: 'rootA', children: [
{text: 'sub-A1'},
{text: 'sub-A2'},
{text: 'sub-A3'},
{text: 'sub-A4'},
{text: 'sub-A5', state: 'opened', children: [
{text:'sub-A5A', children:[
{text:'sub-A5AA', children: [
{text: 'sub-A5AAA', children: [
{text: 'sub-A5AAAA'}
]}
]}
]},
{text:'sub_A5B'}
]},
{text: 'sub-A6'},
{text: 'sub-A7'},
{text: 'sub-A8'},
{text: 'sub-A9', state: 'closed', children: [
{text:'sub-A9A'},
{text:'sub-A9B'}
]},
{text: 'sub-A10'},
{text: 'sub-A11'},
{text: 'sub-A12'}
]},
{text: 'rootB', state:'closed', children: [
{text:'sub-B1'},
{text:'sub-B2'},
{text:'sub-B3'}
]}
];
// Custom template string
var customInternalNode =
'<div class="tui-tree-content-wrapper tui-tree-root-btn" style="padding-left: {{indent}}px">' +
'<button type="button" class="tui-tree-toggle-btn tui-js-tree-toggle-btn">' +
'<span class="tui-ico-tree"></span>' +
'{{stateLabel}}' +
'</button>' +
'<span class="tui-tree-text tui-js-tree-text">' +
'{{text}}' +
'</span>' +
'</div>' +
'<ul class="tui-tree-subtree tui-js-tree-subtree">' +
'{{{children}}}' + // Mustache's format
'</ul>';
var customLeafNode =
'<div class="tui-tree-content-wrapper tui-tree-root-btn" style="padding-left: {{indent}}px">' +
'<span class="tui-tree-text tui-js-tree-text">' +
'{{text}}' +
'</span>' +
'</div>';
var tree = new tui.Tree('#tree', {
data: data,
nodeDefaultState: 'opened',
template: {
internalNode: // Change to Mustache's format
'<div class="tui-tree-content-wrapper" style="padding-left: {{indent}}px">' + // Example for using indent value
'<button type="button" class="tui-tree-toggle-btn tui-js-tree-toggle-btn">' +
'<span class="tui-ico-tree"></span>' +
'{{stateLabel}}' +
'</button>' +
'<span class="tui-tree-text tui-js-tree-text">' +
'<span class="tui-tree-ico tui-ico-folder"></span>' +
'{{text}}' +
'</span>' +
'</div>' +
'<ul class="tui-tree-subtree tui-js-tree-subtree">' +
'{{{children}}}' + // Mustache's format
'</ul>',
leafNode:
'<div class="tui-tree-content-wrapper" style="padding-left: {{indent}}px">' + // Example for using indent value
'<span class="tui-tree-text {{textClass}}">' +
'<span class="tui-tree-ico tui-ico-file"></span>' +
'{{text}}' +
'</span>' +
'</div>'
},
renderTemplate: function(tmpl, props) {
var id = props.id;
var depth = this.getDepth(id);
var isFirstDepth = (this.getDepth(id) === 1);
var isLeaf = this.isLeaf(id);
if (isFirstDepth) { // customizing node template
if (isLeaf) {
tmpl = customLeafNode;
} else {
tmpl = customInternalNode;
}
}
props.indent = 20 * depth; // customizing indent
return Mustache.render(tmpl, props);
}
});
var addBtn = document.getElementById('addChildBtn');
var removeBtn = document.getElementById('removeChildBtn');
var rootNodeId = tree.getRootNodeId();
util.addEventListener(addBtn, 'click', function() {
tree.add({text: 'root'}, rootNodeId);
});
util.addEventListener(removeBtn, 'click', function() {
var lastGrandChildId = tree.getChildIds(rootNodeId).slice(-1)[0];
tree.remove(lastGrandChildId);
});
</script>
</body>
</html>
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tree with "Selectable" feature</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="./css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<button id="selectedBtn">select</button>
<button id="deselectedBtn">deselect</button>
<br>
<textarea id="selectedValue" readonly></textarea>
</div>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
var util = {
addEventListener: function(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
} else {
element.attachEvent('on' + eventName, handler);
}
}
};
var data = [
{text: 'rootA', children: [
{text: 'sub-A1'},
{text: 'sub-A2'},
{text: 'sub-A3'},
{text: 'sub-A4'},
{text: 'sub-A5', state: 'closed', children: [
{text:'sub-A5A', children:[
{text:'sub-A5A1'}
]},
{text:'sub_A5B'}
]},
{text: 'sub-A6'},
{text: 'sub-A7'},
{text: 'sub-A8'},
{text: 'sub-A9', state: 'closed', children: [
{text:'sub-A9A'},
{text:'sub-A9B'}
]},
{text: 'sub-A10'},
{text: 'sub-A11'},
{text: 'sub-A12'}
]},
{text: 'rootB', state:'closed', children: [
{text:'sub-B1'},
{text:'sub-B2'},
{text:'sub-B3'}
]}
];
var tree = new tui.Tree('#tree', {
data: data,
nodeDefaultState: 'opened'
}).enableFeature('Selectable', {
selectedClassName: 'tui-tree-selected',
});
var selectedBtn = document.getElementById('selectedBtn');
var deselectedBtn = document.getElementById('deselectedBtn');
var rootNodeId = tree.getRootNodeId();
var firstChildId = tree.getChildIds(rootNodeId)[0];
var selectedValue = document.getElementById('selectedValue');
tree.on('select', function(eventData) {
var nodeData = tree.getNodeData(eventData.nodeId);
selectedValue.value = 'selected : ' + nodeData.text;
});
tree.on('deselect', function(eventData) {
var nodeData = tree.getNodeData(eventData.nodeId);
selectedValue.value = 'deselected : ' + nodeData.text;
});
util.addEventListener(selectedBtn, 'click', function() {
tree.select(firstChildId);
});
util.addEventListener(deselectedBtn, 'click', function() {
tree.deselect();
});
</script>
</body>
</html>
@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>Tree with "Draggable" feature</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="./css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<textarea id="movedValue" readonly></textarea>
</div>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
var util = {
addEventListener: function(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
} else {
element.attachEvent('on' + eventName, handler);
}
}
};
var data = [
{
text: 'rootA',
children: [
{ text: 'sub-A1' },
{ text: 'sub-A2' },
{ text: 'sub-A3' },
{ text: 'sub-A4' },
{
text: 'sub-A5',
state: 'closed',
children: [{ text: 'sub-A5A', children: [{ text: 'sub-A5A1' }] }, { text: 'sub_A5B' }]
},
{ text: 'sub-A6' },
{ text: 'sub-A7' },
{ text: 'sub-A8' },
{
text: 'sub-A9',
state: 'closed',
children: [{ text: 'sub-A9A' }, { text: 'sub-A9B' }]
},
{ text: 'sub-A10' },
{ text: 'sub-A11' },
{ text: 'sub-A12' }
]
},
{
text: 'rootB',
state: 'closed',
children: [{ text: 'sub-B1' }, { text: 'sub-B2' }, { text: 'sub-B3' }]
}
];
var movedValue = document.getElementById('movedValue');
var tree = new tui.Tree('#tree', {
data: data,
nodeDefaultState: 'opened'
}).enableFeature('Draggable', {
helperClassName: 'tui-tree-drop',
lineClassName: 'tui-tree-line',
isSortable: true
});
tree.on('move', function(eventData) {
var msg;
msg = 'nodeId: ' + eventData.nodeId + '\n';
msg += 'originalParentId: ' + eventData.originalParentId + '\n';
msg += 'newParentId: ' + eventData.newParentId + '\n';
msg += 'index: ' + eventData.index + '\n';
movedValue.value = msg;
});
</script>
</body>
</html>
@@ -0,0 +1,60 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tree with "Editable" feature</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="./css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<h3>Double-click on each node.</h3>
</div>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
var data = [
{text: 'rootA', children: [
{text: 'sub-A1'},
{text: 'sub-A2'},
{text: 'sub-A3'},
{text: 'sub-A4'},
{text: 'sub-A5', state: 'closed', children: [
{text:'sub-A5A', children:[
{text:'sub-A5A1'}
]},
{text:'sub_A5B'}
]},
{text: 'sub-A6'},
{text: 'sub-A7'},
{text: 'sub-A8'},
{text: 'sub-A9', state: 'closed', children: [
{text:'sub-A9A'},
{text:'sub-A9B'}
]},
{text: 'sub-A10'},
{text: 'sub-A11'},
{text: 'sub-A12'}
]},
{text: 'rootB', state:'closed', children: [
{text:'sub-B1'},
{text:'sub-B2'},
{text:'sub-B3'}
]}
];
var tree = new tui.Tree('#tree', {
data: data,
nodeDefaultState: 'opened'
}).enableFeature('Editable', {
dataKey: 'text'
});
</script>
</body>
</html>
@@ -0,0 +1,98 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tree with "ContextMenu" feature</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
<link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui.context-menu/v2.1.9/tui-context-menu.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<h3>Right-click on each node</h3>
<p>ContextMenu feature does not support IE8.</p>
<textarea id="selectedMenuValue" readonly></textarea>
</div>
<script src="https://uicdn.toast.com/tui.context-menu/v2.1.9/tui-context-menu.min.js"></script>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
var data = [
{text: 'rootA', children: [
{text: 'sub-A1'},
{text: 'sub-A2'},
{text: 'sub-A3'},
{text: 'sub-A4'},
{text: 'sub-A5', state: 'closed', children: [
{text:'sub-A5A', children:[
{text:'sub-A5A1'}
]},
{text:'sub_A5B'}
]},
{text: 'sub-A6'},
{text: 'sub-A7'},
{text: 'sub-A8'},
{text: 'sub-A9', state: 'closed', children: [
{text:'sub-A9A'},
{text:'sub-A9B'}
]},
{text: 'sub-A10'},
{text: 'sub-A11'},
{text: 'sub-A12'}
]},
{text: 'rootB', state:'closed', children: [
{text:'sub-B1'},
{text:'sub-B2'},
{text:'sub-B3'}
]}
];
var tree = new tui.Tree('#tree', {
data: data,
nodeDefaultState: 'opened'
}).enableFeature('ContextMenu', {
menuData: [
{title: 'menu1'},
{title: 'menu2', disable: true}
]
});
tree.on('beforeOpenContextMenu', function(nodeId) {
var menuData = [];
if (nodeId === 'tui-tree-node-2') { // sub-A1 node
menuData = [
{title: 'menu'}
];
} else {
menuData = [
{title: 'menu1'},
{title: 'menu2', disable: true},
{title: 'menu3', menu: [
{title: 'submenu1', disable: true},
{title: 'submenu2'}
]},
{separator: true},
{title: 'menu4'},
{title: 'menu5'}
];
}
tree.changeContextMenu(menuData); // change menu api
});
tree.on('selectContextMenu', function(eventData) {
var treeNodeData = tree.getNodeData(eventData.nodeId);
var message = '"' + eventData.command + '" is selected on "' + treeNodeData.text + '" node';
selectedMenuValue.value = message;
});
</script>
</body>
</html>
@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tree with "Checkbox" feature</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="./css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<button id="checkBtn">check</button>
<button id="uncheckBtn">uncheck</button>
<br>
<textarea id="checkedValue" readonly></textarea>
</div>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
var util = {
addEventListener: function(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
} else {
element.attachEvent('on' + eventName, handler);
}
}
};
var data = [
{text: 'rootA', children: [
{text: 'sub-A1'},
{text: 'sub-A2'},
{text: 'sub-A3'},
{text: 'sub-A4'},
{text: 'sub-A5', state: 'closed', children: [
{text:'sub-A5A', children:[
{text:'sub-A5A1'}
]},
{text:'sub_A5B'}
]},
{text: 'sub-A6'},
{text: 'sub-A7'},
{text: 'sub-A8'},
{text: 'sub-A9', state: 'closed', children: [
{text:'sub-A9A'},
{text:'sub-A9B'}
]},
{text: 'sub-A10'},
{text: 'sub-A11'},
{text: 'sub-A12'}
]},
{text: 'rootB', state:'closed', children: [
{text:'sub-B1'},
{text:'sub-B2'},
{text:'sub-B3'}
]}
];
var tree = new tui.Tree('#tree', {
data: data,
nodeDefaultState: 'opened',
template: {
internalNode:
'<div class="tui-tree-content-wrapper" style="padding-left: {{indent}}px">' +
'<button type="button" class="tui-tree-toggle-btn tui-js-tree-toggle-btn">' +
'<span class="tui-ico-tree"></span>' +
'{{stateLabel}}' +
'</button>' +
'<span class="tui-tree-text tui-js-tree-text">' +
'<label class="tui-checkbox">' +
'<span class="tui-ico-check"><input type="checkbox" class="tui-tree-checkbox"></span>' +
'</label>' +
'<span class="tui-tree-ico tui-ico-folder"></span>' +
'{{text}}' +
'</span>' +
'</div>' +
'<ul class="tui-tree-subtree tui-js-tree-subtree">{{children}}</ul>',
leafNode:
'<div class="tui-tree-content-wrapper" style="padding-left: {{indent}}px">' +
'<span class="tui-tree-text tui-js-tree-text">' +
'<label class="tui-checkbox">' +
'<span class="tui-ico-check"><input type="checkbox" class="tui-tree-checkbox"></span>' +
'</label>' +
'<span class="tui-tree-ico tui-ico-file"></span>' +
'{{text}}' +
'</span>' +
'</div>'
}
}).enableFeature('Checkbox', {
checkboxClassName: 'tui-tree-checkbox',
});
var checkBtn = document.getElementById('checkBtn');
var uncheckBtn = document.getElementById('uncheckBtn');
var checkedValue = document.getElementById('checkedValue');
var rootNodeId = tree.getRootNodeId();
var firstChildId = tree.getChildIds(rootNodeId)[0];
tree.on('check', function(eventData) {
var nodeData = tree.getNodeData(eventData.nodeId);
checkedValue.value = 'checked : ' + nodeData.text;
});
tree.on('uncheck', function(eventData) {
var nodeData = tree.getNodeData(eventData.nodeId);
checkedValue.value = 'unchecked : ' + nodeData.text;
});
util.addEventListener(checkBtn, 'click', function() {
tree.check(firstChildId);
});
util.addEventListener(uncheckBtn, 'click', function() {
tree.uncheck(firstChildId);
});
</script>
</body>
</html>
@@ -0,0 +1,183 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tree with "Ajax" feature</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="./css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
<link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui.context-menu/v2.1.6/tui-context-menu.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<h3>To create / update / remove a tree node, please right-click.</h3>
<p>ContextMenu feature does not support IE8.</p>
<textarea id="before-ajax-request" readonly></textarea>
<textarea id="success-ajax-response" readonly></textarea>
</div>
<script src="https://uicdn.toast.com/tui.context-menu/v2.1.6/tui-context-menu.min.js"></script>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
// Create Tree component
var tree = new tui.Tree('#tree', {
nodeDefaultState: 'opened',
template: {
internalNode: // Change to Mustache's format
'<div class="tui-tree-content-wrapper" style="padding-left: {{indent}}px">' + // Example for using indent value
'<button type="button" class="tui-tree-toggle-btn tui-js-tree-toggle-btn">' +
'<span class="tui-ico-tree"></span>' +
'{{stateLabel}}' +
'</button>' +
'<span class="tui-tree-text tui-js-tree-text">' +
'<span class="tui-tree-ico tui-ico-folder"></span>' +
'{{text}}' +
'</span>' +
'</div>' +
'<ul class="tui-tree-subtree tui-js-tree-subtree">' +
'{{children}}' + // Mustache's format
'</ul>',
leafNode:
'<div class="tui-tree-content-wrapper" style="padding-left: {{indent}}px">' + // Example for using indent value
'<span class="tui-tree-text {{textClass}}">' +
'<span class="tui-tree-ico tui-ico-file"></span>' +
'{{text}}' +
'</span>' +
'</div>'
}
});
// Bind custom event
tree.on('selectContextMenu', function(e) {
var command = e.command;
var nodeId = e.nodeId;
switch (command) {
case 'create':
tree.createChildNode(nodeId);
break;
case 'update':
tree.editNode(nodeId);
break;
case 'remove':
tree.remove(nodeId);
break;
}
});
tree.on('beforeCreateChildNode', function(event) {
if (event.cause === 'blur') {
tree.finishEditing();
tree.remove(event.nodeId);
return false;
}
return confirm('Are you sure you want to create?');
});
tree.on('beforeEditNode', function(event) {
if (event.cause === 'blur') {
tree.finishEditing();
return false;
}
return confirm('Are you sure you want to edit?');
});
tree.on('beforeMove', function() {
console.log('before move');
});
tree.on('beforeAjaxRequest', function(eventData) {
print('beforeAjaxRequest', eventData);
})
tree.on('successAjaxResponse', function(eventData) {
print('successAjaxResponse', eventData);
});
function print(title, jsonData) {
var textareaId = title.replace(/([A-Z])/g, '-$1').toLowerCase();
var message = title + ' : \n';
message += JSON.stringify(jsonData, null, 8);
document.getElementById(textareaId).value = message
}
// Add features
tree.enableFeature('Selectable')
.enableFeature('Editable', {
dataKey: 'text'
}).enableFeature('Draggable', {
isSortable: true
}).enableFeature('ContextMenu', {
menuData: [
{title: 'create', command: 'create'},
{title: 'update', command: 'update'},
{title: 'remove', command: 'remove'}
]
}).enableFeature('Ajax', {
command: {
read: {
url: 'data/tree.json',
contentType: 'application/json',
params: function(treeData) {
return {
productId: tree.getNodeData(treeData.nodeId).pid
};
}
},
create: {
url: 'data/response.json',
contentType: 'application/json',
params: function(treeData) {
return {
targetId: tree.getNodeData(treeData.parentId).pid,
productName: treeData.data.text
};
}
},
update: {
url: 'data/response.json',
contentType: 'application/json',
params: function(treeData) {
return {
productId: tree.getNodeData(treeData.nodeId).pid,
productName: treeData.data.text
};
}
},
remove: {
url: 'data/response.json',
contentType: 'application/json',
params: function(treeData) {
return {
productId: tree.getNodeData(treeData.nodeId).pid
};
}
},
move: {
url: 'data/response.json',
contentType: 'application/json',
params: function(treeData) {
return {
productId: tree.getNodeData(treeData.nodeId).pid,
targetId: tree.getNodeData(treeData.newParentId).pid,
moveIdx: treeData.index
};
}
}
},
parseData: function(command, responseData) {
if (command === 'read') {
return responseData.tree;
} else {
return true;
}
}
});
</script>
</body>
</html>
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Tree with depth label using template option</title>
<link rel="stylesheet" type="text/css" href="./css/tui-example-style.css" />
<link rel="stylesheet" type="text/css" href="./css/docs.css" />
<link rel="stylesheet" type="text/css" href="../dist/tui-tree.css" />
</head>
<body>
<div class="code-html">
<div id="tree" class="tui-tree-wrap"></div>
</div>
<div class="explain">
<p>Set the labels for each depth: [' building', ' floor', ' section', ' block']<br />
- 1Depth : node's text + " building"<br />
- 2Depth : node's text + " floor"<br />
- 3Depth : node's text + " section"<br />
- 4Depth : node's text + " block"<br />
<p>The label is automatically appended when a tree node is created.</p>
</div>
<script src="../dist/tui-tree.js"></script>
<script class="code-js">
var data = [
{text: 'rootA', children: [
{text: 'root-1A'},
{text: 'root-1B'},
{text: 'root-1C'},
{text: 'root-1D'},
{text: 'root-2A', children: [
{text:'sub_1A', children:[
{text:'sub_sub_1A'}
]},
{text:'sub_2A'}
]},
{text: 'root-2B'},
{text: 'root-2C'},
{text: 'root-2D'},
{text: 'root-3A', children: [
{text:'sub3_a'},
{text:'sub3_b'}
]},
{text: 'root-3B'},
{text: 'root-3C'},
{text: 'root-3D'}
]},
{text: 'rootB', children: [
{text:'B_sub1'},
{text:'B_sub2'}
]}
];
var depthLabels = [' building', ' floor', ' section', ' block'];
var tree = new tui.Tree('#tree', {
data: data,
nodeDefaultState: 'opened',
template: {
internalNode:
'<div class="tui-tree-content-wrapper" style="padding-left: {{indent}}px">' +
'<button type="button" class="tui-tree-toggle-btn tui-js-tree-toggle-btn">' +
'<span class="tui-ico-tree"></span>' +
'{{stateLabel}}' +
'</button>' +
'</button>' +
'<span class="tui-tree-text tui-js-tree-text">' +
'<span class="tui-tree-ico tui-ico-folder"></span>' +
'{{text}}<b>{{depthLabel}}</b>' +
'</span>' +
'</div>' +
'<ul class="tui-tree-subtree tui-js-tree-subtree">{{children}}</ul>',
leafNode:
'<div class="tui-tree-content-wrapper" style="padding-left: {{indent}}px">' +
'<span class="tui-tree-text tui-js-tree-text">' +
'<span class="tui-tree-ico tui-ico-file"></span>' +
'{{text}}<b>{{depthLabel}}</b>' +
'</span>' +
'</div>'
}
}).enableFeature('Draggable', {
useHelper: true
});
tree.on('beforeDraw', function(evt) {
tree.each(function(child, childId) {
var depth = tree.getDepth(childId);
depth = Math.min(depth, depthLabels.length);
child.setData({
depthLabel: depthLabels[depth - 1] || ''
});
}, evt.nodeId);
});
tree.refresh();
</script>
</body>
</html>
@@ -0,0 +1,9 @@
/* eslint-disable strict */
module.exports = {
moduleFileExtensions: ['js'],
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/**/*.spec.js'],
transformIgnorePatterns: ['<rootDir>/node_modules/'],
clearMocks: true,
setupFilesAfterEnv: ['./test/setup-globals.js']
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
{
"name": "tui-tree",
"version": "4.0.9",
"main": "dist/tui-tree",
"files": [
"dist"
],
"scripts": {
"test": "jest",
"bundle": "webpack --mode=production & webpack --mode=production --minify",
"serve": "webpack-dev-server --inline --hot -d",
"serve:ie8": "webpack-dev-server -d",
"doc:serve": "tuidoc --serv",
"doc": "tuidoc",
"note": "tui-note"
},
"description": "TOAST UI Component: Tree",
"repository": "https://github.com/nhn/tui.tree.git",
"keywords": [
"nhn",
"tui",
"component",
"tree"
],
"author": "NHN FE Development Lab <dl_javascript@nhn.com>",
"license": "MIT",
"devDependencies": {
"@toast-ui/release-notes": "^2.0.1",
"css-loader": "^0.26.1",
"eslint": "^5.12.0",
"eslint-config-prettier": "^6.2.0",
"eslint-config-tui": "^2.2.0",
"eslint-loader": "^3.0.0",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-prettier": "^3.1.2",
"file-loader": "^0.11.2",
"jest": "^27.0.4",
"mini-css-extract-plugin": "^0.8.0",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"prettier": "^1.18.2",
"terser-webpack-plugin": "^2.3.0",
"tui-code-snippet": "^2.3.1",
"url-loader": "^0.5.7",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.7",
"webpack-dev-server": "^3.8.0"
},
"dependencies": {
"tui-context-menu": "^2.1.9"
}
}
@@ -0,0 +1,364 @@
@charset "utf-8";
.tui-ico-check,
.tui-ico-checked,
.tui-ico-file,
.tui-ico-folder,
.tui-ico-root,
.tui-ico-tree {
overflow: hidden;
display: inline-block;
width: 1px;
height: 1px;
line-height: 300px;
vertical-align: top;
background: url(../img/sp_tui_tree.png) no-repeat;
}
.tui-ico-check {
width: 14px;
height: 14px;
background-position: 0px 0px;
}
.tui-ico-checked {
width: 14px;
height: 14px;
background-position: -16px 0px;
}
.tui-ico-file {
width: 9px;
height: 12px;
background-position: 0px -32px;
}
.tui-ico-folder {
width: 12px;
height: 10px;
background-position: -16px -16px;
}
.tui-ico-root {
width: 14px;
height: 14px;
background-position: 0px -16px;
}
.tui-ico-tree {
width: 5px;
height: 8px;
background-position: -32px -31px;
}
.tui-tree-wrap {
width: 300px;
height: auto;
padding: 30px 0;
margin: 0;
font-size: 11px;
background-color: #f9f9f9;
}
.tui-tree-root {
overflow-y: auto;
}
.tui-tree-wrap .tui-tree,
.tui-tree-wrap .tui-tree-subtree {
margin: 0;
padding-left: 0;
}
.tui-tree-wrap .tui-tree-node {
list-style: none;
padding: 0;
margin: 0;
min-height: 24px;
cursor: pointer;
}
.tui-tree-wrap .tui-tree-node {
position: relative;
}
.tui-tree-wrap .tui-tree-toggle-btn {
float: left;
margin: 6px 4px 0 0;
width: 13px;
height: 12px;
font-size: 0;
padding: 0;
color: #000000;
vertical-align: top;
border: 0;
border-radius: 0;
background-color: transparent;
outline: none;
cursor: pointer;
}
.tui-tree-root > .tui-tree-node > .tui-tree-toggle-btn {
margin: 11px 1px 0 0;
}
.tui-tree-content-wrapper .tui-tree-ico {
margin: 0 8px 0 0;
}
.tui-tree-content-wrapper .tui-ico-folder {
margin: 2px 8px 0 0;
}
.tui-tree-content-wrapper .tui-ico-file {
margin: 0 9px 0 2px;
}
.tui-tree-text {
overflow: hidden;
position: relative;
display: block;
padding: 5px 0;
font-size: 12px;
font-weight: 400;
text-overflow: ellipsis;
color: #777;
*display: inline;
zoom: 1;
}
.tui-tree-root .tui-tree-rootBtn .tui-tree-text {
font-size: 14px;
}
.tui-tree-root .tui-tree-rootBtn .tui-tree-toggle-btn {
margin-top: 7px;
}
.tui-tree-opened > .tui-tree-content-wrapper .tui-tree-text {
font-weight: 700;
color: #333;
}
.tui-tree-root > .tui-tree-opened > .tui-tree-content-wrapper .tui-tree-text {
color: #333;
}
.tui-tree-opened .tui-ico-folder {
background-position: -32px 0px;
}
.tui-tree-opened .tui-ico-tree {
width: 8px;
height: 5px;
background-position: -22px -28px;
}
.tui-tree-closed .tui-ico-tree {
width: 5px;
height: 8px;
background-position: -22px -35px;
}
.tui-tree-closed .tui-ico-folder {
background-position: -16px -16px;
}
.tui-tree-leaf > .tui-tree-content-wrapper .tui-tree-text {
padding-left: 17px;
}
.tui-tree-input {
position: absolute;
top: -2px;
width: 128px;
margin: 0 0 0 34px;
padding: 7px 9px;
height: 12px;
border: 1px solid #aaa;
outline: none;
}
.tui-tree-line {
margin: 0;
padding: 0;
width: 300px;
height: 1px;
background-color: #4b96e6;
font-size: 0;
}
.tui-tree-drop {
padding: 5px 22px 5px 10px;
height: 12px;
font-weight: normal;
background-color: #fff;
border: 1px solid #4b96e6;
color: #4b96e6;
}
.tui-tree-drop .tui-ico-folder,
.tui-tree-drop .tui-ico-file {
margin-right: 6px;
}
.tui-tree-selector {
position: absolute;
width: 100%;
height: 24px;
top: 0;
left: 0;
right: 0;
background-color: #fff;
}
.tui-tree-loader {
position: absolute;
margin-left: -16px;
margin-top: -16px;
left: 50%;
top: 50%;
padding: 0;
width: 32px;
height: 32px;
background: url('../img/ajax-loader.gif') no-repeat left;
background-position: 0 0;
font-size: 0;
}
.tui-tree-selected > .tui-tree-content-wrapper {
background: #e7eff7;
background-color: rgba(75, 150, 230, 0.1);
}
.tui-tree-selected > .tui-tree-content-wrapper .tui-tree-text {
font-weight: bold;
color: #4b96e6;
}
.tui-tree-selected > .tui-tree-content-wrapper .tui-ico-folder,
.tui-tree-drop .tui-ico-folder {
background-position: -32px -12px;
}
.tui-tree-selected.tui-tree-opened > .tui-tree-content-wrapper .tui-ico-folder {
background-position: -32px -24px;
}
.tui-tree-selected > .tui-tree-content-wrapper .tui-ico-file,
.tui-tree-drop .tui-ico-file {
background-position: -11px -32px;
}
.tui-tree-content-wrapper {
width: 300px;
overflow: hidden;
padding-left: 26px;
padding-right: 25px;
white-space: nowrap;
text-overflow: ellipsis;
}
.tui-tree-content-wrapper:after {
display: block;
clear: both;
content: '';
}
.tui-tree-content-wrapper:hover,
.tui-tree-hover > .tui-tree-content-wrapper {
background: #e7eff7;
background-color: rgba(75, 150, 230, 0.1);
}
.tui-tree-subtree .tui-tree-content-wrapper {
padding-left: 46px;
}
.tui-tree-subtree .tui-tree-subtree .tui-tree-content-wrapper {
padding-left: 69px;
}
.tui-tree-subtree .tui-tree-subtree .tui-tree-subtree .tui-tree-content-wrapper {
padding-left: 93px;
}
.tui-tree-wrap-checkbox .tui-tree-toggle-btn {
margin-right: 4px;
}
.tui-ico-check {
width: 14px;
height: 14px;
background-position: 0px 0px;
}
.tui-ico-check input {
position: absolute;
left: -9999px;
}
.tui-checkbox {
position: relative;
overflow: hidden;
display: inline-block;
margin-right: 8px;
height: 14px;
font-size: 0;
cursor: pointer;
vertical-align: top;
}
.tui-checkbox.tui-is-checked .tui-ico-check {
background-position: -16px 0;
}
.tui-tree-rootBtn .tui-checkbox-root {
margin: 1px 10px 0 0;
}
.tui-checkbox-root.tui-is-checked .tui-ico-check {
background-position: 0 -16px;
}
.tui-tree-node.tui-tree-drag > .tui-tree-content-wrapper .tui-tree-text {
color: #ddd;
}
.tui-tree-node.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-file {
background-position: -46px -24px;
}
.tui-tree-closed.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-folder {
background-position: -46px 0;
}
.tui-tree-opened.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-folder {
background-position: -46px -12px;
}
.tui-tree-closed.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-tree {
background-position: -29px -35px;
}
.tui-tree-opened.tui-tree-drag > .tui-tree-content-wrapper .tui-ico-tree {
background-position: -36px -35px;
}
.tui-tree-node.tui-tree-drag > .tui-tree-content-wrapper .tui-checkbox .tui-ico-check {
background-position: 0 -46px;
}
.tui-tree-node.tui-tree-drag
> .tui-tree-content-wrapper
.tui-checkbox.tui-is-checked
.tui-ico-check {
background-position: -16px -46px;
}
.tui-tree-node.tui-tree-drag
> .tui-tree-content-wrapper
.tui-checkbox-root.tui-is-checked
.tui-ico-check {
background-position: -32px -46px;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,17 @@
/**
* @fileoverview Set each command name using in Ajax feature
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
/**
* Ajax comman in tree
* @type {Object.<string, string>}
*/
module.exports = {
READ: 'read',
CREATE: 'create',
UPDATE: 'update',
DELETE: 'remove',
DELETE_ALL_CHILDREN: 'removeAllChildren',
MOVE: 'move'
};
@@ -0,0 +1,69 @@
/**
* @fileoverview Set default value of options
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
/**
* A default values for tree
* @const
* @type {Object}
* @property {array} data - A data to be used on tree
* @property {string} nodeDefaultState - Node state
* @property {string} nodeIdPrefix - Node id prefix
* @property {object} stateLabel - State label in node
* @property {string} stateLabel.opened - '-'
* @property {string} stateLabel.closed - '+'
* @property {object} template - Template html for the nodes.
* @property {string} template.internalNode - Template html for internal node.
* @property {string} template.leafNode - Template html for leaf node.
* @property {object} classNames - Class names of elements in tree
* @property {string} openedClass - Class name for opened node
* @property {string} closedClass - Class name for closed node
* @property {string} nodeClass - Class name for node
* @property {string} leafClass - Class name for leaf node
* @property {string} subtreeClass - Class name for subtree in internal node
* @property {string} toggleBtnClass - Class name for toggle button in internal node
* @property {string} textClass - Class name for text element in a node
*/
module.exports = {
data: [],
nodeDefaultState: 'closed',
stateLabels: {
opened: '-',
closed: '+'
},
nodeIdPrefix: 'tui-tree-node-',
classNames: {
nodeClass: 'tui-tree-node',
leafClass: 'tui-tree-leaf',
openedClass: 'tui-tree-opened',
closedClass: 'tui-tree-closed',
subtreeClass: 'tui-js-tree-subtree',
toggleBtnClass: 'tui-js-tree-toggle-btn',
textClass: 'tui-js-tree-text',
btnClass: 'tui-tree-content-wrapper'
},
template: {
internalNode:
'<div class="tui-tree-content-wrapper">' +
'<button type="button" class="tui-tree-toggle-btn {{toggleBtnClass}}">' +
'<span class="tui-ico-tree"></span>' +
'{{stateLabel}}' +
'</button>' +
'<span class="tui-tree-text {{textClass}}">' +
'<span class="tui-tree-ico tui-ico-folder"></span>' +
'{{text}}' +
'</span>' +
'</div>' +
'<ul class="tui-tree-subtree {{subtreeClass}}">{{children}}</ul>',
leafNode:
'<div class="tui-tree-content-wrapper">' +
'<span class="tui-tree-text {{textClass}}">' +
'<span class="tui-tree-ico tui-ico-file"></span>' +
'{{text}}' +
'</span>' +
'</div>'
},
indent: 23, // value of default css,
usageStatistics: true
};
@@ -0,0 +1,17 @@
/**
* @fileoverview Set error messages
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
/**
* Messages for tree
* @type {Object.<string, string>}
*/
module.exports = {
INVALID_CONTAINER_ELEMENT: '"tui-tree": The container element is invalid.',
INVALID_API: '"tui-tree": INVALID_API',
INVALID_API_SELECTABLE: '"tui-tree": The feature-"Selectable" is not enabled.',
INVALID_API_EDITABLE: '"tui-tree": The feature-"Editable" is not enabled.',
INVALID_API_DRAGGABLE: '"tui-tree": The feature-"Draggable" is not enabled.',
INVALID_API_CHECKBOX: '"tui-tree": The feature-"Checkbox" is not enabled.'
};
@@ -0,0 +1,14 @@
/**
* @fileoverview Set outer template
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
/**
* Outer template
* @type {{internalNode: string, leafNode: string}}
*/
module.exports = {
ROOT: '<ul class="tui-tree tui-tree-root"></ul>',
INTERNAL_NODE: '<li id="{{id}}" class="{{nodeClass}} {{stateClass}}">{{innerTemplate}}</li>',
LEAF_NODE: '<li id="{{id}}" class="{{nodeClass}} {{leafClass}}">{{innerTemplate}}</li>'
};
@@ -0,0 +1,19 @@
/**
* @fileoverview Set default value of toggle button
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
/**
* States in tree
* @type {Object.<string, string>}
*/
module.exports = {
/**
* States of node
* @type {{OPENED: string, CLOSED: string}}
*/
node: {
OPENED: 'opened',
CLOSED: 'closed'
}
};
@@ -0,0 +1,307 @@
/**
* @fileoverview Feature that tree action is enable to communicate server
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var ajax = require('tui-code-snippet/ajax/index.js');
var defineClass = require('tui-code-snippet/defineClass/defineClass');
var extend = require('tui-code-snippet/object/extend');
var isFunction = require('tui-code-snippet/type/isFunction');
var isUndefined = require('tui-code-snippet/type/isUndefined');
var bind = require('../util').bind;
var API_LIST = [];
var LOADER_CLASSNAME = 'tui-tree-loader';
/**
* Set Ajax feature on tree
* @class Ajax
* @param {Tree} tree - Tree
* @param {Object} options - Options
* @param {Object} options.command - Each Ajax request command options
* @param {Function} [options.parseData] - Function to parse and return the response data
* @param {string} [options.loaderClassName] - Classname of loader element
* @param {boolean} [options.isLoadRoot] - Whether load data from root node or not
* @ignore
*/
var Ajax = defineClass(
/** @lends Ajax.prototype */ {
static: {
/**
* @static
* @memberof Ajax
* @returns {Array.<string>} API list of Ajax
*/
getAPIList: function() {
return API_LIST.slice();
}
},
init: function(tree, options) {
options = extend({}, options);
/**
* Tree
* @type {Tree}
*/
this.tree = tree;
/**
* Option for each request command
* @type {Object}
*/
this.command = options.command;
/**
* Callback for parsing the response data
* @type {?Function}
*/
this.parseData = options.parseData || null;
/**
* Classname of loader element
* @type {string}
*/
this.loaderClassName = options.loaderClassName || LOADER_CLASSNAME;
/**
* State of loading root data or not
* @type {boolean}
*/
this.isLoadRoot = !isUndefined(options.isLoadRoot) ? options.isLoadRoot : true;
/**
* Loader element
* @type {HTMLElement}
*/
this.loader = null;
this._createLoader();
tree.on('initFeature', bind(this._onInitFeature, this));
},
/**
* Custom event handler "initFeature"
* @private
*/
_onInitFeature: function() {
if (!this.isLoadRoot) {
return;
}
this.tree.resetAllData();
},
/**
* Disable this module
*/
destroy: function() {
var tree = this.tree;
this._removeLoader();
tree.off(this);
},
/**
* Load data to request server
* @param {string} command - Command type
* @param {Function} callback - Executed function after response
* @param {Object} [data] - Values to make "data" property using request
*/
loadData: function(command, callback, data) {
var options;
if (!this.command || !this.command[command] || !this.command[command].url) {
return;
}
options = this._getDefaultRequestOptions(command, data);
/**
* @event Tree#beforeAjaxRequest
* @type {object} evt - Event data
* @property {string} command - Command type
* @property {object} [data] - Request data
* @example
* tree.on('beforeAjaxRequest', function(evt) {
* console.log('before ' + evt.command + ' request!');
* return false; // It cancels request
* // return true; // It fires request
* });
*/
if (
!this.tree.invoke('beforeAjaxRequest', {
command: command,
data: data
})
) {
return;
}
this._showLoader();
options.success = bind(function(response) {
this._responseSuccess(command, callback, response.data);
}, this);
options.error = bind(function(error) {
this._responseError(command, error);
}, this);
ajax(options);
},
/**
* Processing when response is success
* @param {string} command - Command type
* @param {Function} callback - Executed function after response
* @param {Object|boolean} [responseData] - Response data from server or return value of "parseData"
* @private
*/
_responseSuccess: function(command, callback, responseData) {
var tree = this.tree;
var data;
this._hideLoader();
if (this.parseData) {
responseData = this.parseData(command, responseData);
}
if (responseData) {
data = callback(responseData);
/**
* @event Tree#successAjaxResponse
* @type {object} evt - Event data
* @property {string} command - Command type
* @property {object} [data] - Return value of executed command callback
* @example
* tree.on('successAjaxResponse', function(evt) {
* console.log(evt.command + ' response is success!');
* if (data) {
* console.log('data:' + evt.data);
* }
* });
*/
tree.fire('successAjaxResponse', {
command: command,
data: data
});
} else {
/**
* @event Tree#failAjaxResponse
* @type {object} evt - Event data
* @property {string} command - Command type
* @example
* tree.on('failAjaxResponse', function(evt) {
* console.log(evt.command + ' response is fail!');
* });
*/
tree.fire('failAjaxResponse', {
command: command
});
}
},
/**
* Processing when response is error
* @param {string} command - Command type
* @private
*/
_responseError: function(command, error) {
this._hideLoader();
/**
* @event Tree#errorAjaxResponse
* @type {object} evt - Event data
* @property {string} command - Command type
* @property {number} status - Error status code
* @property {string} statusText - Error status text
* @example
* tree.on('errorAjaxResponse', function(evt) {
* console.log(evt.command + ' response is error!');
* });
*/
this.tree.fire('errorAjaxResponse', {
command: command,
status: error.status,
statusText: error.statusText
});
},
/**
* Get default request options
* @param {string} type - Command type
* @param {Object} [data] - Value of request option "data"
* @returns {Object} Default options to request
* @private
*/
_getDefaultRequestOptions: function(type, data) {
var options = extend({}, this.command[type]);
if (isFunction(options.url)) {
// for restful API url
options.url = options.url(data);
}
if (isFunction(options.params)) {
// for custom request data
options.params = options.params(data);
}
options.method = options.method || 'GET';
options.contentType = options.contentType || 'application/json';
return options;
},
/**
* Create loader element
* @private
*/
_createLoader: function() {
var tree = this.tree;
var loader = document.createElement('span');
loader.className = this.loaderClassName;
loader.style.display = 'none';
tree.rootElement.parentNode.appendChild(loader);
this.loader = loader;
},
/**
* Remove loader element
* @private
*/
_removeLoader: function() {
var tree = this.tree;
var loader = this.loader;
tree.rootElement.parentNode.removeChild(loader);
this.loader = null;
},
/**
* Show loader element on tree
* @private
*/
_showLoader: function() {
this.loader.style.display = 'block';
},
/**
* Hide loader element on tree
* @private
*/
_hideLoader: function() {
this.loader.style.display = 'none';
}
}
);
module.exports = Ajax;
@@ -0,0 +1,642 @@
/**
* @fileoverview Feature that each tree node is possible to check and uncheck
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var inArray = require('tui-code-snippet/array/inArray');
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var CustomEvents = require('tui-code-snippet/customEvents/customEvents');
var defineClass = require('tui-code-snippet/defineClass/defineClass');
var getTarget = require('tui-code-snippet/domEvent/getTarget');
var once = require('tui-code-snippet/domEvent/once');
var addClass = require('tui-code-snippet/domUtil/addClass');
var removeClass = require('tui-code-snippet/domUtil/removeClass');
var extend = require('tui-code-snippet/object/extend');
var util = require('../util.js');
var API_LIST = [
'check',
'uncheck',
'toggleCheck',
'isChecked',
'isIndeterminate',
'isUnchecked',
'getCheckedList',
'getTopCheckedList',
'getBottomCheckedList'
];
/* Checkbox tri-states */
var STATE_CHECKED = 1;
var STATE_UNCHECKED = 2;
var STATE_INDETERMINATE = 3;
var DATA_KEY_FOR_CHECKBOX_STATE = '__CheckBoxState__';
var DATA = {};
var CHECKED_CLASSNAME = 'tui-is-checked';
var INDETERMINATE_CLASSNAME = 'tui-checkbox-root';
/* Checkbox cascade-states */
var CASCADE_UP = 'up';
var CASCADE_DOWN = 'down';
var CASCADE_BOTH = 'both';
var CASCADE_NONE = false;
/**
* Set the checkbox-api
* @class Checkbox
* @param {Tree} tree - Tree
* @param {Object} option - Option
* @param {string} option.checkboxClassName - Classname of checkbox element
* @param {string|boolean} [option.checkboxCascade='both'] - 'up', 'down', 'both', false
* @ignore
*/
var Checkbox = defineClass(
/** @lends Checkbox.prototype */ {
static: {
/**
* @static
* @memberof Checkbox
* @returns {Array.<string>} API list of checkbox
*/
getAPIList: function() {
return API_LIST.slice();
}
},
init: function(tree, option) {
option = extend({}, option);
this.tree = tree;
this.checkboxClassName = option.checkboxClassName;
this.checkboxCascade = this._initCascadeOption(option.checkboxCascade);
this.checkedList = [];
this.rootCheckbox = document.createElement('INPUT');
this.rootCheckbox.type = 'checkbox';
this._setAPIs();
this._attachEvents();
},
/**
* Disable this module
*/
destroy: function() {
var tree = this.tree;
tree.off(this);
forEachArray(API_LIST, function(apiName) {
delete tree[apiName];
});
},
/**
* @param {string|boolean} cascadeOption - Cascade option
* @returns {string|boolean} Cascade option
* @private
*/
_initCascadeOption: function(cascadeOption) {
var cascadeOptions = [CASCADE_UP, CASCADE_DOWN, CASCADE_BOTH, CASCADE_NONE];
if (inArray(cascadeOption, cascadeOptions) === -1) {
cascadeOption = CASCADE_BOTH;
}
return cascadeOption;
},
/**
* Set apis of checkbox tree
* @private
*/
_setAPIs: function() {
var tree = this.tree;
forEachArray(
API_LIST,
function(apiName) {
tree[apiName] = util.bind(this[apiName], this);
},
this
);
},
/**
* Attach event to tree instance
* @private
*/
_attachEvents: function() {
this.tree.on(
{
singleClick: function(event) {
var target = getTarget(event);
if (target.querySelector('.' + this.checkboxClassName)) {
this._changeCustomCheckbox(target);
}
},
afterDraw: function(ev) {
if (this.tree.isMovingNode) {
return;
}
this._reflectChanges(ev.nodeId);
},
move: function(data) {
// @TODO - Optimization
this._reflectChanges(data.originalParentId);
this._reflectChanges(data.newParentId);
}
},
this
);
},
/**
* Change custom checkbox
* @param {HTMLElement} target - Label element
*/
_changeCustomCheckbox: function(target) {
var nodeId = this.tree.getNodeIdFromElement(target);
var inputElement = target.getElementsByTagName('input')[0];
once(
inputElement,
'change propertychange',
util.bind(function() {
var state = this._getStateFromCheckbox(inputElement);
this._continuePostprocessing(nodeId, state);
}, this)
);
},
/**
* Reflect the changes on node.
* @param {string} nodeId - Node id
* @private
*/
_reflectChanges: function(nodeId) {
this.tree.each(
function(descendant, descendantId) {
this._setState(descendantId, this._getState(descendantId), true);
},
nodeId,
this
);
this._judgeOwnState(nodeId);
this._updateAllAncestorsState(nodeId);
},
/**
* Set checkbox attributes (checked, indeterminate)
* @param {Element} checkbox - Checkbox element
* @param {boolean} isChecked - "checked"
* @param {boolean} isIndeterminate - "indeterminate"
* @private
*/
_setCheckboxAttr: function(checkbox, isChecked, isIndeterminate) {
checkbox.indeterminate = isIndeterminate;
checkbox.checked = isChecked;
},
/**
* Get checking state of node
* @param {string} nodeId - Node id
* @param {number} state - State for checkbox
* @param {boolean} [stopPropagation] - If true, stop changing state propagation
* @private
*/
_setState: function(nodeId, state, stopPropagation) {
var checkbox = this._getCheckboxElement(nodeId);
if (!checkbox) {
return;
}
switch (state) {
case STATE_CHECKED:
this._setCheckboxAttr(checkbox, true, false);
break;
case STATE_UNCHECKED:
this._setCheckboxAttr(checkbox, false, false);
break;
case STATE_INDETERMINATE:
this._setCheckboxAttr(checkbox, false, true);
break;
default:
// no more process if the state is invalid
return;
}
this._continuePostprocessing(nodeId, state, stopPropagation);
},
/**
* Get checking state of node
* @param {string} nodeId - Node id
* @returns {number} Checking state
* @private
*/
_getState: function(nodeId) {
var tree = this.tree;
var state = tree.getNodeData(nodeId)[DATA_KEY_FOR_CHECKBOX_STATE];
var checkbox;
if (!state) {
checkbox = this._getCheckboxElement(nodeId);
state = this._getStateFromCheckbox(checkbox);
}
return state;
},
/**
* Get checking state of node element
* @private
* @param {Element} checkbox - Checkbox element
* @returns {?number} Checking state
*/
_getStateFromCheckbox: function(checkbox) {
var state;
if (!checkbox) {
return null;
}
if (checkbox.checked) {
state = STATE_CHECKED;
} else if (checkbox.indeterminate) {
state = STATE_INDETERMINATE;
} else {
state = STATE_UNCHECKED;
}
return state;
},
/**
* Continue post-processing from changing:checkbox-state
* @param {string} nodeId - Node id
* @param {number} state - Checkbox state
* @param {boolean} [stopPropagation] - If true, stop update-propagation
* @private
*/
_continuePostprocessing: function(nodeId, state, stopPropagation) {
var tree = this.tree;
var checkedList = this.checkedList;
var eventName;
/* Prevent duplicated node id */
util.removeItemFromArray(nodeId, checkedList);
if (state === STATE_CHECKED) {
checkedList.push(nodeId);
/**
* @event Tree#check
* @type {object} evt - Event data
* @property {string} nodeId - Checked node id
* @example
* tree.on('check', function(evt) {
* console.log('checked: ' + evt.nodeId);
* });
*/
eventName = 'check';
} else if (state === STATE_UNCHECKED) {
/**
* @event Tree#uncheck
* @type {object} evt - Event data
* @property {string} nodeId - Unchecked node id
* @example
* tree.on('uncheck', function(evt) {
* console.log('unchecked: ' + evt.nodeId);
* });
*/
eventName = 'uncheck';
}
DATA[DATA_KEY_FOR_CHECKBOX_STATE] = state;
tree.setNodeData(nodeId, DATA, {
isSilent: true
});
this._setClassName(nodeId, state);
if (!stopPropagation) {
this._propagateState(nodeId, state);
tree.fire(eventName, { nodeId: nodeId });
}
},
/**
* Set class name on label element
* @param {string} nodeId - Node id for finding input element
* @param {number} state - Checked state number
*/
_setClassName: function(nodeId, state) {
var parentElement = this._getCheckboxElement(nodeId).parentNode;
var labelElement;
if (parentElement && parentElement.parentNode) {
labelElement = parentElement.parentNode;
removeClass(labelElement, INDETERMINATE_CLASSNAME);
removeClass(labelElement, CHECKED_CLASSNAME);
if (state === 1) {
addClass(labelElement, CHECKED_CLASSNAME);
} else if (state === 3) {
addClass(labelElement, INDETERMINATE_CLASSNAME);
addClass(labelElement, CHECKED_CLASSNAME);
}
}
},
/**
* Propagate a node state to descendants and ancestors for updating their states
* @param {string} nodeId - Node id
* @param {number} state - Checkbox state
* @private
*/
_propagateState: function(nodeId, state) {
if (state === STATE_INDETERMINATE) {
return;
}
if (inArray(this.checkboxCascade, [CASCADE_DOWN, CASCADE_BOTH]) > -1) {
this._updateAllDescendantsState(nodeId, state);
}
if (inArray(this.checkboxCascade, [CASCADE_UP, CASCADE_BOTH]) > -1) {
this._updateAllAncestorsState(nodeId);
}
},
/**
* Update all descendants state
* @param {string} nodeId - Node id
* @param {number} state - State for checkbox
* @private
*/
_updateAllDescendantsState: function(nodeId, state) {
this.tree.each(
function(descendant, descendantId) {
this._setState(descendantId, state, true);
},
nodeId,
this
);
},
/**
* Update all ancestors state
* @param {string} nodeId - Node id
* @private
*/
_updateAllAncestorsState: function(nodeId) {
var tree = this.tree;
var parentId = tree.getParentId(nodeId);
while (parentId) {
this._judgeOwnState(parentId);
parentId = tree.getParentId(parentId);
}
},
/**
* Judge own state from child node is changed
* @param {string} nodeId - Node id
* @private
*/
_judgeOwnState: function(nodeId) {
var tree = this.tree;
var childIds = tree.getChildIds(nodeId);
var checked = true;
var unchecked = true;
if (!childIds.length) {
checked = this.isChecked(nodeId);
} else {
forEachArray(
childIds,
function(childId) {
var state = this._getState(childId);
checked = checked && state === STATE_CHECKED;
unchecked = unchecked && state === STATE_UNCHECKED;
return checked || unchecked;
},
this
);
}
if (checked) {
this._setState(nodeId, STATE_CHECKED, true);
} else if (unchecked) {
this._setState(nodeId, STATE_UNCHECKED, true);
} else {
this._setState(nodeId, STATE_INDETERMINATE, true);
}
},
/**
* Get checkbox element of node
* @param {string} nodeId - Node id
* @returns {?HTMLElement} Checkbox element
* @private
*/
_getCheckboxElement: function(nodeId) {
var tree = this.tree;
var el, nodeEl;
if (nodeId === tree.getRootNodeId()) {
el = this.rootCheckbox;
} else {
nodeEl = document.getElementById(nodeId);
if (!nodeEl) {
return null;
}
el = nodeEl.querySelector('.' + this.checkboxClassName);
}
return el;
},
/**
* Check node
* @memberof Tree.prototype
* @param {string} nodeId - Node id
* @example
* var nodeId = 'tui-tree-node-3';
* tree.check(nodeId);
*/
check: function(nodeId) {
if (!this.isChecked(nodeId)) {
this._setState(nodeId, STATE_CHECKED);
}
},
/**
* Uncheck node
* @memberof Tree.prototype
* @param {string} nodeId - Node id
* @example
* var nodeId = 'tui-tree-node-3';
* tree.uncheck(nodeId);
*/
uncheck: function(nodeId) {
if (!this.isUnchecked(nodeId)) {
this._setState(nodeId, STATE_UNCHECKED);
}
},
/**
* Toggle node checking
* @memberof Tree.prototype
* @param {string} nodeId - Node id
* @example
* var nodeId = 'tui-tree-node-3';
* tree.toggleCheck(nodeId);
*/
toggleCheck: function(nodeId) {
if (!this.isChecked(nodeId)) {
this.check(nodeId);
} else {
this.uncheck(nodeId);
}
},
/**
* Whether the node is checked
* @memberof Tree.prototype
* @param {string} nodeId - Node id
* @returns {boolean} True if node is indeterminate
* @example
* var nodeId = 'tui-tree-node-3';
* tree.check(nodeId);
* console.log(tree.isChecked(nodeId)); // true
*/
isChecked: function(nodeId) {
return STATE_CHECKED === this._getState(nodeId);
},
/**
* Whether the node is indeterminate
* @memberof Tree.prototype
* @param {string} nodeId - Node id
* @returns {boolean} True if node is indeterminate
* @example
* var nodeId = 'tui-tree-node-3';
* tree.check(nodeId);
* console.log(tree.isIndeterminate(nodeId)); // false
*/
isIndeterminate: function(nodeId) {
return STATE_INDETERMINATE === this._getState(nodeId);
},
/**
* Whether the node is unchecked or not
* @memberof Tree.prototype
* @param {string} nodeId - Node id
* @returns {boolean} True if node is unchecked.
* @example
* var nodeId = 'tui-tree-node-3';
* tree.uncheck(nodeId);
* console.log(tree.isUnchecked(nodeId)); // true
*/
isUnchecked: function(nodeId) {
return STATE_UNCHECKED === this._getState(nodeId);
},
/**
* Get checked list
* @memberof Tree.prototype
* @param {string} [parentId] - Node id (default: rootNode id)
* @returns {Array.<string>} Checked node ids
* @example
* // ├─ node1 (v)
* // │ ├─ node2 (v)
* // │ └─ node3 (v)
* // ├─ node4 ( )
* // │ └─ node5 (v)
* // └─ node6 ( )
* // ├─ node7 (v)
* // │ └─ node8 (v)
* // └─ node9 ( )
*
* var allCheckedList = tree.getCheckedList(); // ['node1', 'node2', 'node3' ,....]
* var descendantsCheckedList = tree.getCheekedList('node6'); // ['node7', 'node8']
*/
getCheckedList: function(parentId) {
var tree = this.tree;
var checkedList = this.checkedList;
if (!parentId) {
return checkedList.slice();
}
return util.filter(checkedList, function(nodeId) {
return tree.contains(parentId, nodeId);
});
},
/**
* Get top checked list
* @memberof Tree.prototype
* @param {string} [parentId] - Node id (default: rootNode id)
* @returns {Array.<string>} Checked node ids
* @example
* // ├─ node1 (v)
* // │ ├─ node2 (v)
* // │ └─ node3 (v)
* // ├─ node4 ( )
* // │ └─ node5 (v)
* // └─ node6 ( )
* // ├─ node7 (v)
* // │ └─ node8 (v)
* // └─ node9 ( )
*
* var allTopCheckedList = tree.getTopCheckedList(); // ['node1', 'node5', 'node7']
* var descendantsTopCheckedList = tree.getTopCheekedList('node6'); // ['node7']
*/
getTopCheckedList: function(parentId) {
var tree = this.tree;
var checkedList = [];
var state;
parentId = parentId || tree.getRootNodeId();
state = this._getState(parentId);
if (state === STATE_CHECKED) {
checkedList = tree.getChildIds(parentId);
} else if (state === STATE_INDETERMINATE) {
checkedList = this.getCheckedList(parentId);
checkedList = util.filter(
checkedList,
function(nodeId) {
return !this.isChecked(tree.getParentId(nodeId));
},
this
);
}
return checkedList;
},
/**
* Get bottom checked list
* @memberof Tree.prototype
* @param {string} [parentId] - Node id (default: rootNode id)
* @returns {Array.<string>} Checked node ids
* @example
* var allBottomCheckedList = tree.getBottomCheckedList(); // ['node2', 'node3', 'node5', 'node8']
* var descendantsBottomCheckedList = tree.getBottomCheekedList('node6'); // ['node8']
*/
getBottomCheckedList: function(parentId) {
var tree = this.tree;
var checkedList;
parentId = parentId || tree.getRootNodeId();
checkedList = this.getCheckedList(parentId);
return util.filter(checkedList, function(nodeId) {
return tree.isLeaf(nodeId);
});
}
}
);
CustomEvents.mixin(Checkbox);
module.exports = Checkbox;
@@ -0,0 +1,245 @@
/**
* @fileoverview Feature that each tree node is possible to have context-menu
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var defineClass = require('tui-code-snippet/defineClass/defineClass');
var getTarget = require('tui-code-snippet/domEvent/getTarget');
var disableTextSelection = require('tui-code-snippet/domUtil/disableTextSelection');
var enableTextSelection = require('tui-code-snippet/domUtil/enableTextSelection');
var util = require('./../util');
var TuiContextMenu = require('tui-context-menu');
var API_LIST = ['changeContextMenu'];
/**
* Set ContextMenu feature on tree
* @class ContextMenu
* @param {Tree} tree - Tree
* @param {Object} options - Options
* @param {Array.<Object>} options.menuData - Context menu data
* @param {boolean} options.usageStatistics - Whether to send the hostname to GA
* @ignore
*/
var ContextMenu = defineClass(
/** @lends ContextMenu.prototype */ {
static: {
/**
* @static
* @memberof ContextMenu
* @returns {Array.<string>} API list of ContextMenu
*/
getAPIList: function() {
return API_LIST.slice();
}
},
init: function(tree, options) {
var containerId = tree.rootElement.parentNode.id;
options = options || {};
/**
* Tree data
* @type {Tree}
*/
this.tree = tree;
/**
* Tree selector for context menu
*/
this.treeSelector = '#' + containerId;
/**
* Id of floating layer in tree
* @type {string}
*/
this.flId = containerId + '-fl';
/**
* Floating layer element
* @type {HTMLElement}
*/
this.flElement = document.getElementById(this.flId);
/**
* Info of context menu in tree
* @type {Object}
*/
this.menu = this._generateContextMenu(options.usageStatistics);
/**
* Id of selected tree item
* @type {string}
*/
this.selectedNodeId = null;
this.menu.register(this.treeSelector, util.bind(this._onSelect, this), options.menuData);
this.tree.on('contextmenu', this._onContextMenu, this);
this._preventTextSelection();
this._setAPIs();
},
/**
* Change current context-menu view
* @memberof Tree.prototype
* @requires ContextMenu
* @param {Array.<Object>} newMenuData - New context menu data
* @example
* tree.changeContextMenu([
* {title: 'menu1'},
* {title: 'menu2', disable: true},
* {title: 'menu3', menu: [
* {title: 'submenu1', disable: true},
* {title: 'submenu2'}
* ]}
* ]);
*/
changeContextMenu: function(newMenuData) {
this.menu.unregister(this.treeSelector);
this.menu.register(this.treeSelector, util.bind(this._onSelect, this), newMenuData);
},
/**
* Disable ContextMenu feature
*/
destroy: function() {
var tree = this.tree;
this.menu.destroy();
this._restoreTextSelection();
this._removeFloatingLayer();
tree.off(this);
forEachArray(API_LIST, function(apiName) {
delete tree[apiName];
});
},
/**
* Create floating layer for context menu
* @private
*/
_createFloatingLayer: function() {
this.flElement = document.createElement('div');
this.flElement.id = this.flId;
document.body.appendChild(this.flElement);
},
/**
* Remove floating layer for context menu
* @private
*/
_removeFloatingLayer: function() {
document.body.removeChild(this.flElement);
this.flElement = null;
},
/**
* Generate context menu in tree
* @returns {TuiContextMenu} Instance of TuiContextMenu
* @param {boolean} usageStatistics - Let us know the hostname.
* @private
*/
_generateContextMenu: function(usageStatistics) {
if (!this.flElement) {
this._createFloatingLayer();
}
return new TuiContextMenu(this.flElement, {
usageStatistics: usageStatistics
});
},
/**
* Prevent text selection on selected tree item
* @private
*/
_preventTextSelection: function() {
disableTextSelection(this.tree.rootElement);
},
/**
* Restore text selection on selected tree item
* @private
*/
_restoreTextSelection: function() {
enableTextSelection(this.tree.rootElement);
},
/**
* Event handler on tree item
* @param {MouseEvent} e - Mouse event
* @private
*/
_onContextMenu: function(e) {
var target = getTarget(e);
this.selectedNodeId = this.tree.getNodeIdFromElement(target);
/**
* @event Tree#beforeOpenContextMenu
* @type {object} evt - Event data
* @property {string} nodeId - Current selected node id
* @example
* tree.on('beforeOpenContextMenu', function(evt) {
* console.log('nodeId: ' + evt.nodeId);
* });
*/
this.tree.fire('beforeOpenContextMenu', {
nodeId: this.selectedNodeId
});
},
/**
* Event handler on context menu
* @param {MouseEvent} e - Mouse event
* @param {string} command - Options value of selected context menu ("title"|"command")
* @private
*/
_onSelect: function(e, command) {
/**
* @event Tree#selectContextMenu
* @type {object} evt - Event data
* @property {string} command - Command type
* @property {string} nodeId - Node id
* @example
* tree.on('selectContextMenu', function(evt) {
* var command = treeEvent.command; // key of context menu's data
* var nodeId = treeEvent.nodeId;
*
* console.log(evt.command, evt.nodeId);
* });
*/
this.tree.fire('selectContextMenu', {
command: command,
nodeId: this.selectedNodeId
});
},
/**
* Set API of ContextMenu feature
* @private
*/
_setAPIs: function() {
var tree = this.tree;
forEachArray(
API_LIST,
function(apiName) {
tree[apiName] = util.bind(this[apiName], this);
},
this
);
}
}
);
module.exports = ContextMenu;
@@ -0,0 +1,649 @@
/**
* @fileoverview Feature that each tree node is possible to drag and drop
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var inArray = require('tui-code-snippet/array/inArray');
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var defineClass = require('tui-code-snippet/defineClass/defineClass');
var getMouseButton = require('tui-code-snippet/domEvent/getMouseButton');
var getMousePosition = require('tui-code-snippet/domEvent/getMousePosition');
var getTarget = require('tui-code-snippet/domEvent/getTarget');
var off = require('tui-code-snippet/domEvent/off');
var on = require('tui-code-snippet/domEvent/on');
var preventDefault = require('tui-code-snippet/domEvent/preventDefault');
var addClass = require('tui-code-snippet/domUtil/addClass');
var disableTextSelection = require('tui-code-snippet/domUtil/disableTextSelection');
var enableTextSelection = require('tui-code-snippet/domUtil/enableTextSelection');
var hasClass = require('tui-code-snippet/domUtil/hasClass');
var removeClass = require('tui-code-snippet/domUtil/removeClass');
var removeElement = require('tui-code-snippet/domUtil/removeElement');
var extend = require('tui-code-snippet/object/extend');
var defaultOptions = {
useHelper: true,
helperPos: {
y: 2,
x: 5
},
helperClassName: 'tui-tree-drop',
dragItemClassName: 'tui-tree-drag',
hoverClassName: 'tui-tree-hover',
lineClassName: 'tui-tree-line',
lineBoundary: {
top: 4,
bottom: 4
},
autoOpenDelay: 1500,
isSortable: false
};
var rejectedTagNames = ['INPUT', 'BUTTON', 'UL'];
var API_LIST = [];
/**
* Set the tree draggable
* @class Draggable
* @param {Tree} tree - Tree
* @param {Object} options - Options
* @param {boolean} options.useHelper - Using helper flag
* @param {{x: number, y:number}} options.helperPos - Helper position (each minimum value is 4)
* @param {Array.<string>} options.rejectedTagNames - No draggable tag names
* @param {Array.<string>} options.rejectedClassNames - No draggable class names
* @param {number} options.autoOpenDelay - Delay time while dragging to be opened
* @param {boolean} options.isSortable - Flag of whether using sortable dragging
* @param {string} options.hoverClassName - Class name for hovered node
* @param {string} options.lineClassName - Class name for moving position line
* @param {string} options.helperClassName - Class name for helper's outer element
* @param {string} options.helperTemplate - Template string for helper's inner contents
* @param {{top: number, bottom: number}} options.lineBoundary - Boundary value for visible moving line
* @ignore
*/
var Draggable = defineClass(
/** @lends Draggable.prototype */ {
static: {
/**
* @static
* @memberof Draggable
* @returns {Array.<string>} API list of Draggable
*/
getAPIList: function() {
return API_LIST.slice();
}
},
init: function(tree, options) {
options = extend({}, defaultOptions, options);
/**
* Tree data
* @type {Tree}
*/
this.tree = tree;
/**
* Drag helper element
* @type {HTMLElement}
*/
this.helperElement = null;
/**
* Dragging element's node id
* @type {string}
*/
this.currentNodeId = null;
/**
* Current mouse overed element
* @type {HTMLElement}
*/
this.hoveredElement = null;
/**
* Moving line type ("top" or "bottom")
* @type {string}
*/
this.movingLineType = null;
/**
* Invoking time for setTimeout()
* @type {number}
*/
this.timer = null;
/**
* Last mouse hovered nodeId
* @type {string | null}
*/
this.lastHoverNodeId = null;
/**
* Tag list for rejecting to drag
* @param {Array.<string>}
*/
this.rejectedTagNames = rejectedTagNames.concat(options.rejectedTagNames);
/**
* Class name list for rejecting to drag
* @param {Array.<string>}
*/
this.rejectedClassNames = [].concat(options.rejectedClassNames);
/**
* Using helper flag
* @type {boolean}
*/
this.useHelper = options.useHelper;
/**
* Helper position
* @type {Object}
*/
this.helperPos = options.helperPos;
/**
* Delay time while dragging to be opened
* @type {number}
*/
this.autoOpenDelay = options.autoOpenDelay;
/**
* Flag of whether using sortable dragging
* @type {boolean}
*/
this.isSortable = options.isSortable;
/**
* Class name for mouse overed node
* @type {string}
*/
this.hoverClassName = options.hoverClassName;
/**
* Class name for moving position line
* @type {string}
*/
this.lineClassName = options.lineClassName;
/**
* Boundary value for visible moving line
* @type {Object}
*/
this.lineBoundary = options.lineBoundary;
/**
* Helper's outer element class name
* @type {string}
*/
this.helperClassName = options.helperClassName;
this._initHelper();
if (this.isSortable) {
this._initMovingLine();
}
this._attachMousedown();
},
/**
* Disable this module (remove attached elements and unbind event)
*/
destroy: function() {
removeElement(this.helperElement);
removeElement(this.lineElement);
this._restoreTextSelection();
this._detachMousedown();
},
/**
* Change helper element position
* @param {object} mousePos - Current mouse position
* @private
*/
_changeHelperPosition: function(mousePos) {
var mousePosX = mousePos[0];
var mousePosY = mousePos[1];
var helperStyle = this.helperElement.style;
helperStyle.top = mousePosY + window.pageYOffset + this.helperPos.y + 'px';
helperStyle.left = mousePosX + window.pageXOffset + this.helperPos.x + 'px';
helperStyle.display = '';
},
/**
* Init helper element
* @private
*/
_initHelper: function() {
var helperElement = document.createElement('span');
var helperStyle = helperElement.style;
helperStyle.position = 'absolute';
helperStyle.display = 'none';
addClass(helperElement, this.helperClassName);
this.tree.rootElement.parentNode.appendChild(helperElement);
this.helperElement = helperElement;
},
/**
* Init moving line element
* @private
*/
_initMovingLine: function() {
var lineElement = document.createElement('div');
var lineStyle = lineElement.style;
lineStyle.position = 'absolute';
lineStyle.display = 'none';
addClass(lineElement, this.lineClassName);
this.tree.rootElement.parentNode.appendChild(lineElement);
this.lineElement = lineElement;
},
/**
* Set helper contents
* @param {string} contents - Helper contents
* @private
*/
_setHelper: function(contents) {
this.helperElement.innerHTML = contents;
removeElement(this.helperElement.getElementsByTagName('label')[0]);
},
/**
* Attach mouse down event
* @private
*/
_attachMousedown: function() {
this._preventTextSelection();
this.tree.on('mousedown', this._onMousedown, this);
},
/**
* Detach mousedown event
* @private
*/
_detachMousedown: function() {
this.tree.off(this);
},
/**
* Prevent text-selection
* @private
*/
_preventTextSelection: function() {
on(this.tree.rootElement, 'selectstart', preventDefault);
disableTextSelection(this.tree.rootElement);
},
/**
* Restore text-selection
* @private
*/
_restoreTextSelection: function() {
off(this.tree.rootElement, 'selectstart', preventDefault);
enableTextSelection(this.tree.rootElement);
},
/**
* Return whether the target element is in rejectedTagNames or in rejectedClassNames
* @param {HTMLElement} target - Target element
* @returns {boolean} Whether the target is not draggable or draggable
* @private
*/
_isNotDraggable: function(target) {
var tagName = target.tagName.toUpperCase();
var classNames = target.className.split(/\s+/);
var result;
if (inArray(tagName, this.rejectedTagNames) !== -1) {
return true;
}
forEachArray(
classNames,
function(className) {
result = inArray(className, this.rejectedClassNames) !== -1;
return !result;
},
this
);
return result;
},
/**
* Event handler - mousedown
* @param {MouseEvent} event - Mouse event
* @private
*/
_onMousedown: function(event) {
var tree = this.tree;
var target = getTarget(event);
var isRightButton = getMouseButton(event) === 2;
var isEditing = tree.enabledFeatures.Editable && tree.enabledFeatures.Editable.inputElement;
var nodeElement;
if (isRightButton || this._isNotDraggable(target) || isEditing) {
return;
}
preventDefault(event);
this.currentNodeId = tree.getNodeIdFromElement(target);
if (this.useHelper) {
nodeElement = document.querySelector(
'#' + this.currentNodeId + ' .' + tree.classNames.textClass
);
this._setHelper(nodeElement.innerHTML);
}
tree.on(
{
mousemove: this._onMousemove,
mouseup: this._onMouseup
},
this
);
},
/**
* Event handler - mousemove
* @param {MouseEvent} event - Mouse event
* @private
*/
_onMousemove: function(event) {
var mousePos = getMousePosition(event);
var target = getTarget(event);
var nodeId;
if (!this.useHelper) {
return;
}
this._setClassNameOnDragItem('add');
this._changeHelperPosition(mousePos);
nodeId = this.tree.getNodeIdFromElement(target);
if (nodeId) {
this.lastHoverNodeId = nodeId;
this._applyMoveAction(nodeId, mousePos);
}
},
/**
* Event handler - mouseup
* @param {MouseEvent} event - Mouse event
* @private
*/
_onMouseup: function(event) {
var tree = this.tree;
var nodeId = this.currentNodeId;
var target = getTarget(event);
var targetId = this._getTargetNodeId(target);
var index = this._getIndexToInsert(targetId);
var newParentId;
if (index === -1) {
// When the node is created as a child after moving
newParentId = targetId;
} else {
newParentId = tree.getParentId(targetId);
}
if (nodeId !== newParentId) {
// Don't fire beforeMove event
tree.move(nodeId, newParentId, index);
}
this.lastHoverNodeId = null;
this._reset();
},
/**
* Get id of the target element on which the moved item is placed
* @param {HTMLElement} target - Target element
* @returns {string} Id of target element
* @private
*/
_getTargetNodeId: function(target) {
var tree = this.tree;
var movingType = this.movingLineType;
var nodeId = tree.getNodeIdFromElement(target);
var childIds;
if (nodeId) {
return nodeId;
}
childIds = tree.getChildIds(tree.getRootNodeId());
if (movingType === 'top') {
nodeId = childIds[0];
} else {
nodeId = childIds[childIds.length - 1];
}
if (this._isGuideLineElement(target)) {
nodeId = this.lastHoverNodeId;
}
return nodeId;
},
/**
* Get a index number to insert the moved item
* @param {number} nodeId - Id of moved item
* @returns {number} Index number
* @private
*/
_getIndexToInsert: function(nodeId) {
var movingType = this.movingLineType;
var index;
if (!movingType) {
return -1;
}
index = this.tree.getNodeIndex(nodeId);
if (movingType === 'bottom') {
index += 1;
}
return index;
},
/**
* Apply move action that are delay effect and sortable moving node
* @param {string} nodeId - Selected tree node id
* @param {object} mousePos - Current mouse position
* @private
*/
_applyMoveAction: function(nodeId, mousePos) {
var currentElement = document.getElementById(nodeId);
var targetPos = currentElement.getBoundingClientRect();
var isHover = hasClass(currentElement, this.hoverClassName);
var isContain = this._isContain(targetPos, mousePos);
var boundaryType;
if (!this.hoveredElement && isContain) {
this.hoveredElement = currentElement;
this._hover(nodeId);
} else if (!isHover) {
this._unhover();
} else if (!isContain) {
this._unhover();
}
if (this.isSortable) {
boundaryType = this._getBoundaryType(targetPos, mousePos);
this._drawBoundaryLine(targetPos, boundaryType);
}
},
/**
* Act to hover on tree item
* @param {string} nodeId - Tree node id
* @private
*/
_hover: function(nodeId) {
var tree = this.tree;
addClass(this.hoveredElement, this.hoverClassName);
if (tree.isLeaf(nodeId)) {
return;
}
this.timer = setTimeout(function() {
tree.open(nodeId);
}, this.autoOpenDelay);
},
/**
* Act to unhover on tree item
* @private
*/
_unhover: function() {
clearTimeout(this.timer);
if (this.hoveredElement) {
removeClass(this.hoveredElement, this.hoverClassName);
}
this.hoveredElement = null;
this.timer = null;
},
/**
* Check contained state of current target
* @param {object} targetPos - Position of tree item
* @param {object} mousePos - Position of moved mouse
* @returns {boolean} Contained state
* @private
*/
_isContain: function(targetPos, mousePos) {
var top = targetPos.top;
var bottom = targetPos.bottom;
var mousePosX = mousePos[0];
var mousePosY = mousePos[1];
if (this.isSortable) {
top += this.lineBoundary.top;
bottom -= this.lineBoundary.bottom;
}
return (
targetPos.left < mousePosX &&
targetPos.right > mousePosX &&
top < mousePosY &&
bottom > mousePosY
);
},
/**
* Get boundary type by mouse position
* @param {object} targetPos - Position of tree item
* @param {object} mousePos - Position of moved mouse
* @returns {string} Position type in boundary
* @private
*/
_getBoundaryType: function(targetPos, mousePos) {
var type;
if (mousePos[1] < targetPos.top + this.lineBoundary.top) {
type = 'top';
} else if (mousePos[1] > targetPos.bottom - this.lineBoundary.bottom) {
type = 'bottom';
}
return type;
},
/**
* Draw boundary line on tree
* @param {object} targetPos - Position of tree item
* @param {string} boundaryType - Position type in boundary
* @private
*/
_drawBoundaryLine: function(targetPos, boundaryType) {
var style = this.lineElement.style;
var parentNodeOfRoot = this.tree.rootElement.parentNode;
var scrollTop;
if (boundaryType) {
scrollTop = parentNodeOfRoot.getBoundingClientRect().top - parentNodeOfRoot.offsetTop;
style.top = targetPos[boundaryType] - scrollTop + 'px';
style.display = 'block';
this.movingLineType = boundaryType;
} else {
style.display = 'none';
this.movingLineType = null;
}
},
/**
* _reset properties and remove event
* @private
*/
_reset: function() {
if (this.isSortable) {
this.lineElement.style.display = 'none';
}
if (this.hoveredElement) {
removeClass(this.hoveredElement, this.hoverClassName);
this.hoveredElement = null;
}
this._setClassNameOnDragItem('remove');
this.helperElement.style.display = 'none';
this.currentNodeId = null;
this.movingLineType = null;
this.tree.off(this, 'mousemove');
this.tree.off(this, 'mouseup');
},
/**
* Set class name on drag item's element
* @param {string} type - Set type ('add' or 'remove')
*/
_setClassNameOnDragItem: function(type) {
var dragItemElement = document.getElementById(this.currentNodeId);
var dragItemClassName = defaultOptions.dragItemClassName;
if (dragItemElement) {
if (type === 'add') {
addClass(dragItemElement, dragItemClassName);
} else {
removeClass(dragItemElement, dragItemClassName);
}
}
},
/**
* Check if an element is a GuideLineElement
* @param {HTMLElement} element - target element
* @returns {boolean}
* @private
*/
_isGuideLineElement: function(element) {
return element && hasClass(element, this.lineClassName);
}
}
);
module.exports = Draggable;
@@ -0,0 +1,467 @@
/**
* @fileoverview Feature that each tree node is possible to edit as double click
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var defineClass = require('tui-code-snippet/defineClass/defineClass');
var getTarget = require('tui-code-snippet/domEvent/getTarget');
var off = require('tui-code-snippet/domEvent/off');
var on = require('tui-code-snippet/domEvent/on');
var addClass = require('tui-code-snippet/domUtil/addClass');
var hasClass = require('tui-code-snippet/domUtil/hasClass');
var removeElement = require('tui-code-snippet/domUtil/removeElement');
var extend = require('tui-code-snippet/object/extend');
var util = require('./../util');
var ajaxCommand = require('./../consts/ajaxCommand');
var states = require('./../consts/states');
var API_LIST = ['createChildNode', 'editNode', 'finishEditing'];
var EDIT_TYPE = {
CREATE: 'create',
UPDATE: 'update'
};
var WRAPPER_CLASSNAME = 'tui-input-wrap';
var INPUT_CLASSNAME = 'tui-tree-input';
/**
* Set the tree selectable
* @class Editable
* @param {Tree} tree - Tree
* @param {Object} options - Options
* @param {string} options.editableClassName - Classname of editable element
* @param {string} options.dataKey - Key of node data to set value
* @param {string} [options.dataValue] - Value of node data to set value (Use "createNode" API)
* @param {string} [options.inputClassName] - Classname of input element
* @ignore
*/
var Editable = defineClass(
/** @lends Editable.prototype */ {
static: {
/**
* @static
* @memberof Selectable
* @returns {Array.<string>} API list of Editable
*/
getAPIList: function() {
return API_LIST.slice();
}
},
init: function(tree, options) {
options = extend({}, options);
/**
* Tree
* @type {Tree}
*/
this.tree = tree;
/**
* Classname of editable element
* @type {string}
*/
this.editableClassName = options.editableClassName || tree.classNames.textClass;
/**
* Key of node data to set value
* @type {string}
*/
this.dataKey = options.dataKey;
/**
* Default value for creating node
* @type {string}
*/
this.defaultValue = options.defaultValue || '';
/**
* Input element for create or edit
* @type {HTMLElement}
*/
this.inputElement = null;
/**
* Action mode - create or edit
* @type {string}
*/
this.mode = null;
/**
* For block blur when unintentional blur event occur when alert popup
* @type {Boolean}
*/
this._blockBlur = false;
/**
* Keyup event handler
* @type {Function}
*/
this.boundOnKeyup = util.bind(this._onKeyup, this);
/**
* Blur event handler
* @type {Function}
*/
this.boundOnBlur = util.bind(this._onBlur, this);
tree.on('doubleClick', this._onDoubleClick, this);
this._setAPIs();
},
/**
* Disable this module
*/
destroy: function() {
var tree = this.tree;
this._detachInputElement();
tree.off(this);
forEachArray(API_LIST, function(apiName) {
delete tree[apiName];
});
},
/**
* Create child node
* @memberof Tree.prototype
* @requires Editable
* @param {string} parentId - Parent node id to create new node
* @example
* tree.createChildNode('tui-tree-node-1');
*/
createChildNode: function(parentId) {
var tree = this.tree;
var useAjax = tree.enabledFeatures.Ajax;
var nodeId;
this.mode = EDIT_TYPE.CREATE;
if (useAjax) {
tree.on('successAjaxResponse', this._onSuccessResponse, this);
}
if (!tree.isLeaf(parentId) && tree.getState(parentId) === states.node.CLOSED) {
tree.open(parentId);
} else {
nodeId = tree._add({}, parentId)[0];
this._attachInputElement(nodeId);
}
},
/**
* Edit node
* @memberof Tree.prototype
* @requires Editable
* @param {string} nodeId - Node id
* @example
* tree.editNode('tui-tree-node-1');
*/
editNode: function(nodeId) {
this.mode = EDIT_TYPE.UPDATE;
this._attachInputElement(nodeId);
},
/**
* Exit edit though remove input tag
* @memberof Tree.prototype
* @requires Editable
* @example
* tree.finishEditing();
*/
finishEditing: function() {
if (this.inputElement) {
this._detachInputElement();
}
},
/**
* Custom event handler "successResponse"
* @param {string} type - Ajax command type
* @param {Array.<string>} nodeIds - Added node ids on tree
* @private
*/
_onSuccessResponse: function(type, nodeIds) {
var tree = this.tree;
var parentId, nodeId;
if (type === ajaxCommand.READ && nodeIds) {
parentId = tree.getParentId(nodeIds[0]);
nodeId = tree._add({}, parentId)[0];
this._attachInputElement(nodeId);
}
},
/**
* Custom event handler "doubleClick"
* @param {MouseEvent} event - Mouse event
* @private
*/
_onDoubleClick: function(event) {
var target = getTarget(event);
var nodeId;
if (hasClass(target, this.editableClassName)) {
nodeId = this.tree.getNodeIdFromElement(target);
this.editNode(nodeId);
}
},
/**
* InputElement is keep going
* @private
*/
_keepEdit: function() {
if (this.inputElement) {
this.inputElement.focus();
}
},
/**
* Invoke 'beforeCreateChildNode'
* @param {Object} event - Information of 'beforeCreateChildNode'
* @returns {boolean} Result of invoke event
* @private
*/
_invokeBeforeCreateChildNode: function(event) {
/**
* @event Tree#beforeCreateChildNode
* @type {object} evt - Event data
* @property {string} value - Return value of creating input element
* @property {string} nodeId - Return id of creating node
* @property {string} cause - Return 'blur' or 'enter' according cause of the event
* @example
* tree
* .enableFeature('Editable')
* .on('beforeCreateChildNode', function(evt) {
* console.log(evt.value);
* console.log(evt.nodeId);
* console.log(evt.cause);
* return false; // It cancels
* // return true; // It execute next
* });
*/
return this.tree.invoke('beforeCreateChildNode', event);
},
/**
* Invoke 'beforeEditNode'
* @param {Event} event - Information of 'beforeEditNode'
* @returns {boolean} Result of invoke event
* @private
*/
_invokeBeforeEditNode: function(event) {
/**
* @event Tree#beforeEditNode
* @type {object} evt - Event data
* @property {string} value - Return value of creating input element
* @property {string} nodeId - Return id of editing node
* @property {string} cause - Return 'blur' or 'enter' according cause of the event
* @example
* tree
* .enableFeature('Editable')
* .on('beforeEditNode', function(evt) {
* console.log(evt.value);
* console.log(evt.nodeId);
* console.log(evt.cause);
* return false; // It cancels
* // return true; // It execute next
* });
*/
return this.tree.invoke('beforeEditNode', event);
},
/**
* Reflect the value of inputElement to node for creating or editing
* @param {string} cause - how finish editing ('blur' or 'enter')
* @returns {boolean} Result of submit input result
* @private
*/
_submitInputResult: function(cause) {
var tree = this.tree;
var nodeId = tree.getNodeIdFromElement(this.inputElement);
var value = this.inputElement.value;
var event = {
value: value,
nodeId: nodeId,
cause: cause
};
if (this.mode === EDIT_TYPE.CREATE) {
if (!this._invokeBeforeCreateChildNode(event)) {
this._keepEdit();
return false;
}
this._addData(nodeId, value);
} else {
if (!this._invokeBeforeEditNode(event)) {
this._keepEdit();
return false;
}
this._setData(nodeId, value);
}
this._detachInputElement();
return true;
},
/**
* Event handler: keyup - input element
* @param {Event} event - Key event
* @private
*/
_onKeyup: function(event) {
if (util.getKeyCode(event) === 13) {
this._blockBlur = true;
this._submitInputResult('enter');
}
},
/**
* Event handler: blur - input element
* @private
*/
_onBlur: function() {
if (this._blockBlur) {
this._blockBlur = false;
} else {
this._blockBlur = !this._submitInputResult('blur');
}
},
/**
* Create input element
* @returns {HTMLElement} Input element
* @private
*/
_createInputElement: function() {
var element = document.createElement('INPUT');
element.setAttribute('type', 'text');
addClass(element, INPUT_CLASSNAME);
return element;
},
/**
* Attach input element on tree
* @param {string} nodeId - Node id
* @private
*/
_attachInputElement: function(nodeId) {
var tree = this.tree;
var target = document.getElementById(nodeId);
var wrapperElement = document.createElement('DIV');
var inputElement = this._createInputElement();
if (!target) {
return;
}
wrapperElement = target.querySelector('.' + WRAPPER_CLASSNAME);
if (!wrapperElement) {
wrapperElement = document.createElement('DIV');
inputElement = this._createInputElement();
addClass(wrapperElement, WRAPPER_CLASSNAME);
wrapperElement.style.paddingLeft = tree.getIndentWidth(nodeId) + 'px';
inputElement.value = tree.getNodeData(nodeId)[this.dataKey] || '';
wrapperElement.appendChild(inputElement);
target.appendChild(wrapperElement);
on(inputElement, {
keyup: this.boundOnKeyup,
blur: this.boundOnBlur
});
if (this.inputElement) {
this.inputElement.blur();
}
this.inputElement = inputElement;
}
this._blockBlur = false;
this.inputElement.focus();
},
/**
* Detach input element on tree
* @private
*/
_detachInputElement: function() {
var tree = this.tree;
var inputElement = this.inputElement;
var wrapperElement = this.inputElement.parentNode;
off(inputElement, {
keyup: this.boundOnKeyup,
blur: this.boundOnBlur
});
removeElement(wrapperElement);
if (tree.enabledFeatures.Ajax) {
tree.off(this, 'successAjaxResponse');
}
this.inputElement = null;
},
/**
* Add data of input element to node and detach input element on tree
* @param {string} nodeId - Node id to add
* @param {string} value - Content for that node
* @private
*/
_addData: function(nodeId, value) {
var tree = this.tree;
var parentId = tree.getParentId(nodeId);
var data = {};
if (nodeId) {
data[this.dataKey] = value || this.defaultValue;
tree._remove(nodeId);
tree.add(data, parentId);
}
},
/**
* Set data of input element to node and detach input element on tree
* @param {string} nodeId - Node id to change
* @param {string} value - Content for that node
* @private
*/
_setData: function(nodeId, value) {
var tree = this.tree;
var data = {};
if (nodeId) {
data[this.dataKey] = value;
tree.setNodeData(nodeId, data);
}
},
/**
* Set apis of selectable tree
* @private
*/
_setAPIs: function() {
var tree = this.tree;
forEachArray(
API_LIST,
function(apiName) {
tree[apiName] = util.bind(this[apiName], this);
},
this
);
}
}
);
module.exports = Editable;
@@ -0,0 +1,251 @@
/**
* @fileoverview Feature that each tree node is possible to select as click
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var defineClass = require('tui-code-snippet/defineClass/defineClass');
var getTarget = require('tui-code-snippet/domEvent/getTarget');
var addClass = require('tui-code-snippet/domUtil/addClass');
var removeClass = require('tui-code-snippet/domUtil/removeClass');
var extend = require('tui-code-snippet/object/extend');
var util = require('./../util');
var API_LIST = ['select', 'getSelectedNodeId', 'deselect'];
var defaults = {
selectedClassName: 'tui-tree-selected'
};
/**
* Set the tree selectable
* @class Selectable
* @param {Tree} tree - Tree
* @param {Object} options
* @param {string} options.selectedClassName - Classname for selected node.
* @ignore
*/
var Selectable = defineClass(
/** @lends Selectable.prototype */ {
static: {
/**
* @static
* @memberof Selectable
* @returns {Array.<string>} API list of Selectable
*/
getAPIList: function() {
return API_LIST.slice();
}
},
init: function(tree, options) {
options = extend({}, defaults, options);
this.tree = tree;
this.selectedClassName = options.selectedClassName;
this.selectedNodeId = null;
tree.on(
{
singleClick: this.onSingleClick,
afterDraw: this.onAfterDraw
},
this
);
this._setAPIs();
},
/**
* Set apis of selectable tree
* @private
*/
_setAPIs: function() {
var tree = this.tree;
forEachArray(
API_LIST,
function(apiName) {
tree[apiName] = util.bind(this[apiName], this);
},
this
);
},
/**
* Disable this module
*/
destroy: function() {
var tree = this.tree;
var nodeElement = this.getPrevElement();
if (nodeElement) {
removeClass(nodeElement, this.selectedClassName);
}
tree.off(this);
forEachArray(API_LIST, function(apiName) {
delete tree[apiName];
});
},
/**
* Custom event handler "singleClick"
* @param {MouseEvent} event - Mouse event
*/
onSingleClick: function(event) {
var target = getTarget(event);
var nodeId = this.tree.getNodeIdFromElement(target);
this._select(nodeId, target);
},
/**
* Select node if the feature-"Selectable" is enabled.
* @memberof Tree.prototype
* @requires Selectable
* @param {string} nodeId - Node id
* @param {object} [target] - target
* @private
*/
_select: function(nodeId, target) {
var tree, root, prevElement, nodeElement, selectedClassName, prevNodeId, invokeResult;
if (!nodeId) {
return;
}
tree = this.tree;
root = tree.rootElement;
selectedClassName = this.selectedClassName;
prevNodeId = this.selectedNodeId;
/**
* @event Tree#beforeSelect
* @type {object} evt - Event data
* @property {string} nodeId - Selected node id
* @property {string} prevNodeId - Previous selected node id
* @property {HTMLElement|undefined} target - Target element
* @example
* tree
* .enableFeature('Selectable')
* .on('beforeSelect', function(evt) {
* console.log('selected node: ' + evt.nodeId);
* console.log('previous selected node: ' + evt.prevNodeId);
* console.log('target element: ' + evt.target);
* return false; // It cancels "select"
* // return true; // It fires "select"
* });
*/
invokeResult = tree.invoke('beforeSelect', {
nodeId: nodeId,
prevNodeId: prevNodeId,
target: target
});
prevElement = this.getPrevElement();
nodeElement = root.querySelector('#' + nodeId);
if (invokeResult) {
if (prevElement) {
removeClass(prevElement, selectedClassName);
}
addClass(nodeElement, selectedClassName);
this.selectedNodeId = nodeId;
/**
* @event Tree#select
* @type {object} evt - Event data
* @property {string} nodeId - Selected node id
* @property {string} prevNodeId - Previous selected node id
* @property {HTMLElement|undefined} target - Target element
* @example
* tree
* .enableFeature('Selectable')
* .on('select', function(evt) {
* console.log('selected node: ' + evt.nodeId);
* console.log('previous selected node: ' + evt.prevNodeId);
* console.log('target element: ' + evt.target);
* });
*/
tree.fire('select', {
nodeId: nodeId,
prevNodeId: prevNodeId,
target: target
});
}
},
/**
* Select node if the feature-"Selectable" is enabled.
* @memberof Tree.prototype
* @requires Selectable
* @param {string} nodeId - Node id
* @example
* tree.select('tui-tree-node-3');
*/
select: function(nodeId) {
this._select(nodeId);
},
/**
* Get previous selected node element
* @returns {HTMLElement} Node element
*/
getPrevElement: function() {
return document.getElementById(this.selectedNodeId);
},
/**
* Get selected node id
* @memberof Tree.prototype
* @returns {string} selected node id
*/
getSelectedNodeId: function() {
return this.selectedNodeId;
},
/**
* Deselect node by id
* @memberof Tree.prototype
* @requires Selectable
* @param {string} nodeId - Node id
* @example
* tree.deselect('tui-tree-node-3');
*/
deselect: function() {
var nodeId = this.selectedNodeId;
var nodeElement = document.getElementById(nodeId);
var tree = this.tree;
if (!nodeElement) {
return;
}
removeClass(nodeElement, this.selectedClassName);
this.selectedNodeId = null;
/**
* @event Tree#deselect
* @type {object} evt - Event data
* @property {string} nodeId - Deselected node id
* @example
* tree
* .enableFeature('Selectable')
* .on('deselect', function(evt) {
* console.log('deselected node: ' + evt.nodeId);
* });
*/
tree.fire('deselect', { nodeId: nodeId });
},
/**
* Custom event handler - "afterDraw"
*/
onAfterDraw: function() {
var nodeElement = this.getPrevElement();
if (nodeElement) {
addClass(nodeElement, this.selectedClassName);
}
}
}
);
module.exports = Selectable;
@@ -0,0 +1,3 @@
require('../css/tree.css');
module.exports = require('./tree');
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,517 @@
/**
* @fileoverview Update view and control tree data
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var forEachOwnProperties = require('tui-code-snippet/collection/forEachOwnProperties');
var CustomEvents = require('tui-code-snippet/customEvents/customEvents');
var defineClass = require('tui-code-snippet/defineClass/defineClass');
var extend = require('tui-code-snippet/object/extend');
var isArray = require('tui-code-snippet/type/isArray');
var util = require('./util');
var TreeNode = require('./treeNode');
/**
* Tree model
* @class TreeModel
* @param {Array} data - Data
* @param {Object} options - Options for defaultState and nodeIdPrefix
* @ignore
*/
var TreeModel = defineClass(
/** @lends TreeModel.prototype */ {
init: function(options) {
TreeNode.setIdPrefix(options.nodeIdPrefix);
/**
* Default state of node
* @type {String}
*/
this.nodeDefaultState = options.nodeDefaultState;
/**
* Root node
* @type {TreeNode}
*/
this.rootNode = new TreeNode(
{
state: 'opened'
},
null
);
/**
* Tree hash having all nodes
* @type {object.<string, TreeNode>}
*/
this.treeHash = {};
this._setData(options.data);
},
/**
* Return prefix of node id
* @returns {string} Prefix
*/
getNodeIdPrefix: function() {
return TreeNode.idPrefix;
},
/**
* Set model with tree data
* @param {Array} data - Tree data
*/
_setData: function(data) {
var root = this.rootNode;
var rootId = root.getId();
this.treeHash[rootId] = root;
this._makeTreeHash(data, root);
},
/**
* Make tree hash from data and parentNode
* @param {Array} data - Tree data
* @param {TreeNode} parent - Parent node id
* @returns {Array.<string>} Added node ids
* @private
*/
_makeTreeHash: function(data, parent) {
var parentId = parent.getId();
var ids = [];
forEachArray(
data || [],
function(datum) {
var childrenData = datum.children;
var node = this._createNode(datum, parentId);
var nodeId = node.getId();
ids.push(nodeId);
this.treeHash[nodeId] = node;
parent.addChildId(nodeId);
this._makeTreeHash(childrenData, node);
},
this
);
return ids;
},
/**
* Create node
* @param {object} nodeData - Datum of node
* @param {string} parentId - Parent id
* @returns {TreeNode} TreeNode
*/
_createNode: function(nodeData, parentId) {
nodeData = extend(
{
state: this.nodeDefaultState
},
nodeData
);
return new TreeNode(nodeData, parentId);
},
/**
* Get children
* @param {string} nodeId - Node id
* @returns {?Array.<TreeNode>} children
*/
getChildren: function(nodeId) {
var childIds = this.getChildIds(nodeId);
if (!childIds) {
return null;
}
return util.map(
childIds,
function(childId) {
return this.getNode(childId);
},
this
);
},
/**
* Get child ids
* @param {string} nodeId - Node id
* @returns {?Array.<string>} Child ids
*/
getChildIds: function(nodeId) {
var node = this.getNode(nodeId);
if (!node) {
return null;
}
return node.getChildIds();
},
/**
* Get the number of nodes
* @returns {number} The number of nodes
*/
getCount: function() {
var treeHash = this.treeHash;
var length = 0;
forEachOwnProperties(treeHash, function() {
length += 1;
});
return length;
},
/**
* Get last depth
* @returns {number} The last depth
*/
getLastDepth: function() {
var depths = util.map(
this.treeHash,
function(node) {
return this.getDepth(node.getId());
},
this
);
return Math.max.apply(null, depths);
},
/**
* Find node
* @param {string} id - A node id to find
* @returns {?TreeNode} Node
*/
getNode: function(id) {
return this.treeHash[id];
},
/**
* Get depth from node id
* @param {string} id - A node id to find
* @returns {?number} Depth
*/
getDepth: function(id) {
var node = this.getNode(id);
var depth = 0;
var parent;
if (!node) {
return null;
}
parent = this.getNode(node.getParentId());
while (parent) {
depth += 1;
parent = this.getNode(parent.getParentId());
}
return depth;
},
/**
* Return parent id of node
* @param {string} id - Node id
* @returns {?string} Parent id
*/
getParentId: function(id) {
var node = this.getNode(id);
if (!node) {
return null;
}
return node.getParentId();
},
/**
* Return parents ids of node
* @param {string} id - Node id
* @returns {Array.<string>} Parents node ids
*/
getParentIds: function(id) {
var parentsNodeList = [];
var node = this.getNode(id);
var parentNodeId = node.getParentId();
while (parentNodeId) {
node = this.getNode(parentNodeId);
parentNodeId = node.getParentId();
parentsNodeList.push(node);
}
return util.map(parentsNodeList, function(parentsNode) {
return parentsNode.getId();
});
},
/**
* Remove a node with children.
* - The update event will be fired with parent node.
* @param {string} id - Node id to remove
* @param {boolean} [isSilent] - If true, it doesn't trigger the 'update' event
*/
remove: function(id, isSilent) {
var node = this.getNode(id);
var parent;
if (!node) {
return;
}
parent = this.getNode(node.getParentId());
forEachArray(
node.getChildIds(),
function(childId) {
this.remove(childId, true);
},
this
);
parent.removeChildId(id);
delete this.treeHash[id];
if (!isSilent) {
this.fire('update', parent.getId());
}
},
/**
* Add node(s).
* - If the parentId is falsy, the node will be appended to rootNode.
* - The update event will be fired with parent node.
* @param {Array|object} data - Raw-data
* @param {string} parentId - Parent id
* @param {boolean} [isSilent] - If true, it doesn't trigger the 'update' event
* @returns {Array.<string>} New added node ids
*/
add: function(data, parentId, isSilent) {
var parent = this.getNode(parentId) || this.rootNode;
var ids;
data = [].concat(data);
ids = this._makeTreeHash(data, parent);
if (!isSilent) {
this.fire('update', parent.getId());
}
return ids;
},
/**
* Set data properties of a node
* @param {string} id - Node id
* @param {object} props - Properties
* @param {boolean} [isSilent] - If true, it doesn't trigger the 'update' event
*/
setNodeData: function(id, props, isSilent) {
var node = this.getNode(id);
if (!node || !props) {
return;
}
node.setData(props);
if (!isSilent) {
this.fire('update', id);
}
},
/**
* Remove node data
* @param {string} id - Node id
* @param {string|Array} names - Names of properties
* @param {boolean} [isSilent] - If true, it doesn't trigger the 'update' event
*/
removeNodeData: function(id, names, isSilent) {
var node = this.getNode(id);
if (!node || !names) {
return;
}
if (isArray(names)) {
node.removeData.apply(node, names);
} else {
node.removeData(names);
}
if (!isSilent) {
this.fire('update', id);
}
},
/**
* Move a node to new parent's child
* @param {string} nodeId - Node id
* @param {string} newParentId - New parent id
* @param {number} [index] - Start index number for inserting
* @param {boolean} [isSilent] - If true, it doesn't trigger the 'update' event
*/
/* eslint-disable complexity*/
move: function(nodeId, newParentId, index, isSilent) {
var node = this.getNode(nodeId);
var originalParentId, newParent, sameParent;
if (!node) {
return;
}
newParent = this.getNode(newParentId) || this.rootNode;
newParentId = newParent.getId();
originalParentId = node.getParentId();
sameParent = index === -1 && originalParentId === newParentId;
if (nodeId === newParentId || sameParent || this.contains(nodeId, newParentId)) {
return;
}
this._changeOrderOfIds(nodeId, newParentId, originalParentId, index);
if (!isSilent) {
this.fire('move', nodeId, originalParentId, newParentId, index);
}
} /* eslint-enable complexity*/,
/**
* Change order of ids
* @param {string} nodeId - Node id
* @param {string} newParentId - New parent id
* @param {string} originalParentId - Original parent id
* @param {number} index - Moving index (When child node is moved on parent node, the value is -1)
* @private
*/
_changeOrderOfIds: function(nodeId, newParentId, originalParentId, index) {
var node = this.getNode(nodeId);
var newParent = this.getNode(newParentId) || this.rootNode;
var originalParent = this.getNode(originalParentId);
var isSameParentIds = newParentId === originalParentId;
if (index !== -1) {
if (isSameParentIds) {
newParent.moveChildId(nodeId, index);
} else {
newParent.insertChildId(nodeId, index);
originalParent.removeChildId(nodeId);
}
} else if (!isSameParentIds) {
newParent.addChildId(nodeId);
originalParent.removeChildId(nodeId);
}
node.setParentId(newParentId);
},
/**
* Whether a node is a ancestor of another node.
* @param {string} containerId - Id of a node that may contain the other node
* @param {string} containedId - Id of a node that may be contained by the other node
* @returns {boolean} Whether a node contains another node
*/
contains: function(containerId, containedId) {
var parentId = this.getParentId(containedId);
var isContained = false;
while (!isContained && parentId) {
isContained = containerId === parentId;
parentId = this.getParentId(parentId);
}
return isContained;
},
/**
* Sort nodes
* @param {Function} comparator - Comparator function
* @param {string} [parentId] - Id of a node to sort partially
*/
sort: function(comparator, parentId) {
var iteratee = function(node, nodeId) {
var children = this.getChildren(nodeId);
var childIds;
if (children.length > 1) {
children.sort(comparator);
childIds = util.map(children, function(child) {
return child.getId();
});
node.replaceChildIds(childIds);
}
};
var node;
if (parentId) {
node = this.getNode(parentId);
iteratee.call(this, node, parentId);
} else {
this.eachAll(iteratee, this);
}
},
/**
* Get node data (all)
* @param {string} nodeId - Node id
* @returns {?object} Node data
*/
getNodeData: function(nodeId) {
var node = this.getNode(nodeId);
if (!node) {
return null;
}
return node.getAllData();
},
/**
* Traverse this tree iterating over all nodes.
* @param {Function} iteratee - Iteratee function
* @param {object} [context] - Context of iteratee
*/
eachAll: function(iteratee, context) {
context = context || this;
forEachOwnProperties(this.treeHash, function() {
iteratee.apply(context, arguments);
});
},
/**
* Traverse this tree iterating over all descendants of a node.
* @param {Function} iteratee - Iteratee function
* @param {string} parentId - Parent node id
* @param {object} [context] - Context of iteratee
*/
each: function(iteratee, parentId, context) {
// depth-first
var stack, nodeId, node;
node = this.getNode(parentId);
if (!node) {
return;
}
stack = node.getChildIds();
context = context || this;
while (stack.length) {
nodeId = stack.pop();
node = this.getNode(nodeId);
iteratee.call(context, node, nodeId);
stack = stack.concat(node.getChildIds());
}
}
}
);
CustomEvents.mixin(TreeModel);
module.exports = TreeModel;
@@ -0,0 +1,309 @@
/**
* @fileoverview Control each tree node's data
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var inArray = require('tui-code-snippet/array/inArray');
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var forEachOwnProperties = require('tui-code-snippet/collection/forEachOwnProperties');
var defineClass = require('tui-code-snippet/defineClass/defineClass');
var extend = require('tui-code-snippet/object/extend');
var isFalsy = require('tui-code-snippet/type/isFalsy');
var states = require('./consts/states').node;
var util = require('./util');
var lastIndex = 0;
var getNextIndex = function() {
var index = lastIndex;
lastIndex += 1;
return index;
};
var RESERVED_PROPERTIES = {
id: '',
state: 'setState',
children: ''
};
/**
* TreeNode
* @Class TreeNode
* @param {Object} nodeData - Node data
* @param {string} [parentId] - Parent node id
* @ignore
*/
var TreeNode = defineClass(
/** @lends TreeNode.prototype */ {
static: {
/**
* Set prefix of id
* @param {string} prefix - Prefix of id
*/
setIdPrefix: function(prefix) {
this.idPrefix = prefix || this.idPrefix;
},
/**
* Prefix of id
* @type {string}
*/
idPrefix: ''
},
init: function(nodeData, parentId) {
/**
* Node id
* @type {string}
* @private
*/
this._id = this.constructor.idPrefix + getNextIndex();
/**
* Parent node id
* @type {string}
* @private
*/
this._parentId = parentId;
/**
* Id list of children
* @type {Array.<number>}
* @private
*/
this._childIds = [];
/**
* Node data
* @type {object}
* @private
*/
this._data = {};
/**
* Node state
* @type {string}
* @private
*/
this._state = states.CLOSED;
this.setData(nodeData);
},
/**
* Set reserved properties from data
* @param {object} data - Node data
* @returns {object} Node data
* @private
*/
_setReservedProperties: function(data) {
forEachOwnProperties(
RESERVED_PROPERTIES,
function(setter, name) {
var value = data[name];
if (value && setter) {
this[setter](value);
}
delete data[name];
},
this
);
return data;
},
/**
* Toggle state
*/
toggleState: function() {
if (this._state === states.CLOSED) {
this._state = states.OPENED;
} else {
this._state = states.CLOSED;
}
},
/**
* Set state
* @param {string} state - State of node ('closed', 'opened')
*/
setState: function(state) {
state = String(state);
this._state = states[state.toUpperCase()] || this._state;
},
/**
* Get state
* @returns {string} state ('opened' or 'closed')
*/
getState: function() {
return this._state;
},
/**
* Get id
* @returns {string} Node id
*/
getId: function() {
return this._id;
},
/**
* Get parent id
* @returns {string} Parent node id
*/
getParentId: function() {
return this._parentId;
},
/**
* Set parent id
* @param {string} parentId - Parent node id
*/
setParentId: function(parentId) {
this._parentId = parentId;
},
/**
* Replace childIds
* @param {Array.<number>} childIds - Id list of children
*/
replaceChildIds: function(childIds) {
this._childIds = childIds;
},
/**
* Get id list of children
* @returns {Array.<number>} Id list of children
*/
getChildIds: function() {
return this._childIds.slice();
},
/**
* Add child id
* @param {string} id - Child node id
*/
addChildId: function(id) {
var childIds = this._childIds;
if (inArray(childIds, id) === -1) {
childIds.push(id);
}
},
/**
* Remove child id
* @param {string} id - Child node id
*/
removeChildId: function(id) {
util.removeItemFromArray(id, this._childIds);
},
/**
* Get data
* @param {string} name - Property name of data
* @returns {*} Data
*/
getData: function(name) {
return this._data[name];
},
/**
* Get all data
* @returns {Object} Data
*/
getAllData: function() {
return extend({}, this._data);
},
/**
* Set data
* @param {Object} data - Data for adding
*/
setData: function(data) {
data = this._setReservedProperties(data);
extend(this._data, data);
},
/**
* Remove data
* @param {...string} names - Names of data
*/
removeData: function() {
forEachArray(
arguments,
function(name) {
delete this._data[name];
},
this
);
},
/**
* Return true if this node has a provided child id.
* @param {string} id - Node id
* @returns {boolean} - Whether this node has a provided child id.
*/
hasChild: function(id) {
return inArray(id, this._childIds) !== -1;
},
/**
* Return whether this node is leaf.
* @returns {boolean} Node is leaf or not.
*/
isLeaf: function() {
return !this._childIds.length && !this.getData('hasChild');
},
/**
* Return whether this node is root.
* @returns {boolean} Node is root or not.
*/
isRoot: function() {
return isFalsy(this._parentId);
},
/**
* Get index of child
* @param {string} id - Node id
* @returns {number} Index of child in children list
*/
getChildIndex: function(id) {
return inArray(id, this._childIds);
},
/**
* Insert child id
* @param {string} id - Child node id
* @param {number} index - Index number of insert position
*/
insertChildId: function(id, index) {
var childIds = this._childIds;
if (inArray(id, childIds) === -1) {
childIds.splice(index, 0, id);
}
},
/**
* Move child id
* @param {string} id - Child node id
* @param {number} index - Index number of insert position
*/
moveChildId: function(id, index) {
var childIds = this._childIds;
var originIdx = this.getChildIndex(id);
if (inArray(id, childIds) !== -1) {
if (originIdx < index) {
index -= 1;
}
childIds.splice(index, 0, childIds.splice(originIdx, 1)[0]);
}
}
}
);
module.exports = TreeNode;
@@ -0,0 +1,126 @@
/**
* @fileoverview Helper object to make easy tree elements
* @author NHN. FE dev Lab <dl_javascript@nhn.com>
*/
var inArray = require('tui-code-snippet/array/inArray');
var forEach = require('tui-code-snippet/collection/forEach');
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var sendHostname = require('tui-code-snippet/request/sendHostname');
/**
* @ignore
*/
var util = {
/**
* Remove first specified item from array, if it exists
* @param {*} item Item to look for
* @param {Array} arr Array to query
*/
removeItemFromArray: function(item, arr) {
var index = inArray(item, arr);
if (index > -1) {
arr.splice(index, 1);
}
},
/**
* Get key code from event object
* @param {Event} e Event object
* @returns {Number} KeyCode
*/
getKeyCode: function(e) {
e = e || window.event;
return e.which || e.keyCode;
},
/**
* Get first text node in target element
* @param {HTMLElement} element - Target element to find
* @returns {HTMLElement} Text node
*/
getFirstTextNode: function(element) {
var firstTextNode = '';
forEachArray(element.childNodes, function(childElement) {
if (childElement.nodeName === '#text') {
firstTextNode = childElement;
return false;
}
return true;
});
return firstTextNode;
},
/**
* Create a new function that, when called, has its this keyword set to the provided value.
* @param {function} fn A original function before binding
* @param {Object} obj context of function in arguments[0]
* @returns {function} A new bound function with context that is in arguments[1]
*/
bind: function(fn, context) {
var slice = Array.prototype.slice;
var args;
if (fn.bind) {
return fn.bind.apply(fn, slice.call(arguments, 1));
}
args = slice.call(arguments, 2);
return function() {
return fn.apply(context, args.length ? args.concat(slice.call(arguments)) : arguments);
};
},
/**
* Construct a new array with elements that pass the test by the provided callback function.
* @param {Array|NodeList|Arguments} arr - array to be traversed
* @param {function} iteratee - callback function
* @param {Object} context - context of callback function
* @returns {Array}
*/
filter: function(arr, iteratee, context) {
var result = [];
forEachArray(arr, function(elem) {
if (iteratee.apply(context || null, arguments)) {
result.push(elem);
}
});
return result;
},
/**
* Constructs a new array by executing the provided callback function.
* @param {Object|Array} obj - object or array to be traversed
* @param {function} iteratee - callback function
* @param {Object} context - context of callback function
* @returns {Array}
*/
map: function(obj, iteratee, context) {
var result = [];
forEach(obj, function() {
result.push(iteratee.apply(context || null, arguments));
});
return result;
},
/**
* send host name
* @ignore
*/
sendHostName: function() {
sendHostname('tree', 'UA-129987462-1');
}
};
module.exports = util;
@@ -0,0 +1,5 @@
module.exports = {
rules: {
'max-nested-callbacks': 0
}
};
@@ -0,0 +1,529 @@
var Tree = require('../../src/js/tree');
describe('Ajax feature', function() {
var tree;
beforeEach(function() {
loadFixtures('basicFixture.html');
xhrMock.install();
tree = new Tree('#tree', {
rootElement: 'treeRoot'
});
});
afterEach(function() {
xhrMock.uninstall();
});
it('loader should be created in the tree when Ajax feature is enabled', function() {
var className;
tree.enableFeature('Ajax');
className = tree.enabledFeatures.Ajax.loaderClassName;
expect(document.querySelectorAll('.' + className).length).toBe(1);
});
it('loader should be removed from the tree when Ajax feature is disabled', function() {
var className;
tree.enableFeature('Ajax');
className = tree.enabledFeatures.Ajax.loaderClassName;
tree.disableFeature('Ajax');
expect(document.querySelectorAll('.' + className).length).toBe(0);
});
describe('Options', function() {
var treeAjax;
beforeEach(function() {
tree.enableFeature('Ajax', {
command: {
read: {
url: function(data) {
var path = data && data.path ? data.path : '';
return 'api/id' + path;
},
params: function(data) {
data = data || { id: 1 };
return { someId: data.id };
}
}
},
isLoadRoot: false
});
treeAjax = tree.enabledFeatures.Ajax;
});
it('"method" should be "GET" when default command option does not have it', function() {
expect(treeAjax._getDefaultRequestOptions('read').method).toBe('GET');
});
it('"contentType" should be "application/json" when default command option does not have it', function() {
expect(treeAjax._getDefaultRequestOptions('read').contentType).toBe('application/json');
});
it('"url" should be the return value when "url" property of the default command option is function', function() {
expect(treeAjax._getDefaultRequestOptions('read').url).toBe('api/id');
expect(treeAjax._getDefaultRequestOptions('read', { path: '/tree' }).url).toBe('api/id/tree');
});
it('"params" should be the return value when "params" property of the default command option is function', function() {
expect(treeAjax._getDefaultRequestOptions('read').params).toEqual({
someId: 1
});
expect(treeAjax._getDefaultRequestOptions('read', { id: 5 }).params).toEqual({
someId: 5
});
});
it('should not request to a server on init when "isLoadRoot" is false', function() {
tree.resetAllData = jest.fn();
tree.on('initFeature');
expect(tree.resetAllData).not.toHaveBeenCalled();
});
});
describe('loadData() Ajax request - ', function() {
var treeAjax;
beforeEach(function() {
tree.enableFeature('Ajax', {
command: {
remove: {
url: 'api/test'
}
}
});
treeAjax = tree.enabledFeatures.Ajax;
});
it('request should not be execute when request url is empty', function() {
treeAjax.loadData('read');
expect(xhrMock.open).not.toHaveBeenCalled();
});
it('request should be executed when request options are valid', function() {
treeAjax.loadData('remove');
expect(xhrMock.open).toHaveBeenCalledWith('GET', 'api/test');
});
it('request url should include query strings when request is "GET" with parameters', function() {
var expected = 'api/test?param1=a&param2=b';
treeAjax._getDefaultRequestOptions = jest.fn().mockReturnValue({
url: 'api/test',
method: 'GET',
contentType: 'application/json',
params: {
param1: 'a',
param2: 'b'
}
});
treeAjax.loadData('remove');
expect(xhrMock.open).toHaveBeenCalledWith('GET', expected);
});
it('request property should not be null when request is "POST" with parameters', function() {
var parmas = {
param1: 'a',
param2: 'b'
};
treeAjax._getDefaultRequestOptions = jest.fn().mockReturnValue({
url: 'api/test',
method: 'POST',
contentType: 'application/json',
params: parmas
});
treeAjax.loadData('remove');
expect(xhrMock.send).toHaveBeenCalledWith(JSON.stringify(parmas));
});
});
describe('loadData() Ajax response - ', function() {
var callback, treeAjax;
beforeEach(function() {
tree.enableFeature('Ajax', {
command: {
read: {
url: 'api/test'
}
}
});
treeAjax = tree.enabledFeatures.Ajax;
callback = jest.fn();
});
it('callback function should be executed when response is success', function() {
xhrMock.setResponse({
status: 200,
responseText: '{}'
});
treeAjax.loadData('read', callback);
expect(callback).toHaveBeenCalled();
});
it('the loader should be hidden when response is success', function() {
var className = treeAjax.loaderClassName;
xhrMock.setResponse({
status: 200,
responseText: '{}'
});
treeAjax._showLoader();
treeAjax.loadData('read', callback);
expect(document.querySelector('.' + className).style.display).toBe('none');
});
it('the Ajax loader should be hidden when response is failed', function() {
var className = treeAjax.loaderClassName;
treeAjax._showLoader();
treeAjax.loadData('read', callback);
xhrMock.setResponse({
status: 404
});
expect(document.querySelector('.' + className).style.display).toBe('none');
});
it('the "errorAjaxResponse" custom event should be fired when response is failed', function() {
var handler = jest.fn();
xhrMock.setResponse({
status: 404
});
tree.on('errorAjaxResponse', handler);
treeAjax.loadData('read', callback);
expect(handler).toHaveBeenCalled();
});
});
describe('READ command - ', function() {
var rootNodeId, newChildIds;
beforeEach(function() {
rootNodeId = tree.getRootNodeId();
xhrMock.setResponse({
status: 200,
responseText: '[{"text":"A","state":"opened","hasChild":true},{"text":"B"}]',
headers: {
'Content-Type': 'application/json'
}
});
tree.enableFeature('Ajax', {
command: {
read: {
url: 'api/test'
}
}
});
tree.on('successAjaxResponse', function(evt) {
newChildIds = evt.data;
});
});
it('1 depth nodes should be added when Ajax feature is enabled', function() {
expect(tree.getChildIds(rootNodeId).length).toBe(2);
});
it('children nodes should be added when state label is opened', function() {
var nodeId = tree.getChildIds(rootNodeId)[0];
xhrMock.setResponse({
status: 200,
responseText: '[{"text":"A","state":"opened","hasChild":true},{"text":"B"}]',
headers: {
'Content-Type': 'application/json'
}
});
tree.close(nodeId);
tree.toggle(nodeId);
expect(tree.getChildIds(nodeId)).toEqual(newChildIds);
});
});
describe('CREATE command - ', function() {
var parentId, newChildIds;
beforeEach(function() {
tree.enableFeature('Ajax', {
command: {
create: {
url: 'api/test'
}
}
});
parentId = tree.getRootNodeId();
});
it('new node should be created when response data is success', function() {
xhrMock.setResponse({
status: 200,
responseText: 'true',
headers: {
'Content-Type': 'application/json'
}
});
tree.on('successAjaxResponse', function(evt) {
newChildIds = evt.data;
});
tree.add({ text: 'C' }, parentId);
expect(tree.getChildIds(parentId)).toEqual(newChildIds);
});
it('new node should not be created when response data is error', function() {
xhrMock.setResponse({
status: 200,
responseText: 'false',
headers: {
'Content-Type': 'application/json'
}
});
tree.add({ text: 'C' }, parentId);
expect(tree.getChildIds(parentId).length).toBe(0);
});
});
describe('REMOVE command - ', function() {
var children, parentId, nodeId;
beforeEach(function() {
children = [{ text: 'A' }, { text: 'B' }];
tree.add(children);
tree.enableFeature('Ajax', {
command: {
remove: {
url: 'api/test'
}
}
});
parentId = tree.getRootNodeId();
nodeId = tree.getChildIds(parentId)[0];
});
it('selected node should be removed when response data is success', function() {
xhrMock.setResponse({
status: 200,
responseText: 'true',
headers: {
'Content-Type': 'application/json'
}
});
tree.remove(nodeId);
expect(tree.getChildIds(parentId).length).toBe(children.length - 1);
});
it('selected node should not be removed when response data is error', function() {
xhrMock.setResponse({
status: 200,
responseText: 'false',
headers: {
'Content-Type': 'application/json'
}
});
tree.remove(nodeId);
expect(tree.getChildIds(parentId).length).toBe(children.length);
});
});
describe('UPDATE command - ', function() {
var children, nodeId, changedData;
beforeEach(function() {
children = [{ text: 'A', propA: 'aa', propB: 'bb' }];
tree.add(children);
tree.enableFeature('Ajax', {
command: {
update: {
url: 'api/test'
}
}
});
nodeId = tree.getChildIds(tree.getRootNodeId())[0];
changedData = { text: 'B' };
});
it('selected node data should be updated when response data is success', function() {
xhrMock.setResponse({
status: 200,
responseText: 'true',
headers: {
'Content-Type': 'application/json'
}
});
tree.setNodeData(nodeId, changedData);
expect(tree.getNodeData(nodeId).text).toBe(changedData.text);
});
it('selected node data should not be updated when response data is error', function() {
xhrMock.setResponse({
status: 200,
responseText: 'false',
headers: {
'Content-Type': 'application/json'
}
});
tree.setNodeData(nodeId, changedData);
expect(tree.getNodeData(nodeId).text).not.toBe(changedData.text);
});
it('deleted node data should be updated when response data is success', function() {
xhrMock.setResponse({
status: 200,
responseText: 'true',
headers: {
'Content-Type': 'application/json'
}
});
tree.removeNodeData(nodeId, 'propA');
expect(tree.getNodeData(nodeId).propA).toBeUndefined();
});
it('deleted node data should not be updated when response data is error', function() {
xhrMock.setResponse({
status: 200,
responseText: 'false',
headers: {
'Content-Type': 'application/json'
}
});
tree.removeNodeData(nodeId, 'propA');
expect(tree.getNodeData(nodeId).propA).toBe('aa');
});
});
describe('REMOVE_ALL_CHILDREN command - ', function() {
var children, nodeId;
beforeEach(function() {
children = [{ text: 'A' }, { text: 'B' }, { text: 'C' }];
tree.add(children);
tree.enableFeature('Ajax', {
command: {
removeAllChildren: {
url: 'api/test'
}
}
});
nodeId = tree.getRootNodeId();
});
it('all children nodes should be removed when response data is success', function() {
xhrMock.setResponse({
status: 200,
responseText: 'true',
headers: {
'Content-Type': 'application/json'
}
});
tree.removeAllChildren(nodeId);
expect(tree.getChildIds(nodeId).length).toBe(0);
});
it('all children nodes should not be removed when response data is faild', function() {
xhrMock.setResponse({
status: 200,
responseText: 'false',
headers: {
'Content-Type': 'application/json'
}
});
tree.removeAllChildren(nodeId);
expect(tree.getChildIds(nodeId).length).toBe(children.length);
});
});
describe('MOVE command - ', function() {
var children, rootNodeId, nodeId, newParentId;
beforeEach(function() {
children = [
{ text: 'A', children: [{ text: 'aa' }, { text: 'bb' }] },
{ text: 'B' },
{ text: 'C' }
];
tree.add(children);
tree.enableFeature('Ajax', {
command: {
move: {
url: 'api/test'
}
}
});
rootNodeId = tree.getRootNodeId();
nodeId = tree.getChildIds(rootNodeId)[0];
newParentId = tree.getChildIds(rootNodeId)[1];
});
it('node should be moved when response data is success', function() {
xhrMock.setResponse({
status: 200,
responseText: 'true',
headers: {
'Content-Type': 'application/json'
}
});
tree.move(nodeId, newParentId);
expect(tree.getParentId(nodeId)).toBe(newParentId);
});
it('node should not be moved when response data is error', function() {
xhrMock.setResponse({
status: 200,
responseText: 'false',
headers: {
'Content-Type': 'application/json'
}
});
tree.move(nodeId, newParentId);
expect(tree.getParentId(nodeId)).toBe(rootNodeId);
});
});
});
@@ -0,0 +1,284 @@
var forEachArray = require('tui-code-snippet/collection/forEachArray');
var Tree = require('../../src/js/tree');
var messages = require('../../src/js/consts/messages');
var Checkbox = require('../../src/js/features/checkbox');
describe('checkbox feature', function() {
var data = [
{
title: 'A',
children: [
{ title: '1' },
{ title: '2' },
{ title: '3' },
{ title: '4' },
{
title: '5',
children: [{ title: '가', children: [{ title: '*' }] }, { title: '나' }]
},
{ title: '6' },
{ title: '7' },
{ title: '8' },
{ title: '9', children: [{ title: '가' }, { title: '나' }] },
{ title: '10' },
{ title: '11' },
{ title: '12' }
]
},
{
title: 'B',
children: [{ title: '1' }, { title: '2' }, { title: '3' }]
}
];
var tree;
beforeEach(function() {
loadFixtures('basicFixture.html');
tree = new Tree('#tree', {
rootElement: 'treeRoot',
data: data,
template: {
internalNode:
'<button type="button" class="{{toggleBtnClass}}">{{stateLabel}}</button>' +
'<input type="checkbox" class="tui-tree-checkbox">' +
'<span class="{{textClass}}">{{text}}</span>' +
'<ul class="{{subtreeClass}}">{{children}}</ul>',
leafNode:
'<input type="checkbox" class="tui-tree-checkbox">' +
'<span class="{{textClass}}">{{text}}</span>'
}
});
tree.enableFeature('Checkbox', {
checkboxClassName: 'tui-tree-checkbox'
});
});
it('should have implemented apis about checkbox if enabled', function() {
var apiList = Checkbox.getAPIList();
forEachArray(apiList, function(name) {
expect(tree[name]).not.toThrowError(messages.INVALID_API_CHECKBOX);
});
tree.disableFeature('Checkbox');
forEachArray(apiList, function(name) {
expect(tree[name]).toThrowError(messages.INVALID_API_CHECKBOX);
});
});
it('should fire "check"-event when a node is checked', function() {
var firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
var spy = jest.fn();
tree.on('check', spy);
tree.check(firstChildId);
expect(spy).toHaveBeenCalledWith({
nodeId: firstChildId
});
});
it('should fire "uncheck"-event when a node is unchecked', function() {
var firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
var spy = jest.fn();
tree.on('uncheck', spy);
tree.uncheck(firstChildId);
expect(spy).not.toHaveBeenCalled();
tree.check(firstChildId);
tree.uncheck(firstChildId);
expect(spy).toHaveBeenCalledWith({
nodeId: firstChildId
});
});
it('should check with all descendants', function() {
var firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
tree.check(firstChildId);
tree.each(function(node, nodeId) {
expect(tree.isChecked(nodeId)).toBe(true);
}, firstChildId);
});
it('should uncheck with all descendants', function() {
var firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
tree.check(firstChildId);
tree.uncheck(firstChildId);
tree.each(function(node, nodeId) {
expect(tree.isUnchecked(nodeId)).toBe(true);
}, firstChildId);
});
it('should toggle with all descendants', function() {
var firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
tree.check(firstChildId);
tree.toggleCheck(firstChildId);
expect(tree.isUnchecked(firstChildId)).toBe(true);
tree.each(function(node, nodeId) {
expect(tree.isUnchecked(nodeId)).toBe(true);
}, firstChildId);
});
it('should be indeterminate if some children are checked', function() {
var baseNodeId = tree.getChildIds(tree.getRootNodeId())[0];
var childIds = tree.getChildIds(baseNodeId);
tree.check(childIds[0]);
expect(tree.isIndeterminate(baseNodeId)).toBe(true);
});
it('should be checked if all descendants are checked', function() {
var baseNodeId = tree.getChildIds(tree.getRootNodeId())[0];
tree.each(function(node, nodeId) {
tree.check(nodeId);
}, baseNodeId);
expect(tree.isChecked(baseNodeId)).toBe(true);
});
it('should be indeterminate if some children are unchecked from all checked', function() {
var baseNodeId = tree.getChildIds(tree.getRootNodeId())[0];
var childIds = tree.getChildIds(baseNodeId);
tree.each(function(node, nodeId) {
tree.check(nodeId);
}, baseNodeId);
tree.uncheck(childIds[0]);
expect(tree.isIndeterminate(baseNodeId)).toBe(true);
});
it('should reflect states when a node is changed with children', function() {
var rootChildIds = tree.getChildIds(tree.getRootNodeId());
/**
* ======== Set node states ========
* a-1(v)
* a-1-1(v)
* a-1-2(v)
* a-1-3(v)
* ....
* a-2(-)
* a-2-1(v)
* a-2-2( )
* a-2-3(v)
*/
tree.check(rootChildIds[0]);
tree.check(rootChildIds[1]);
tree.uncheck(tree.getChildIds(rootChildIds[1])[1]);
/**
* ======== Reflect the changes ========
* a-1(-)
* a-1-1(v)
* a-1-2(v)
* a-1-3(v)
* ....
* a-2-2( ) <-- moved to "a-1" from "a-2"
* a-2(v)
* a-2-1(v)
* a-2-3(v)
*/
tree.move(tree.getChildIds(rootChildIds[1])[1], rootChildIds[0]);
expect(tree.isIndeterminate(rootChildIds[0])).toBe(true);
expect(tree.isChecked(rootChildIds[1])).toBe(true);
});
describe('When cascade option is both', function() {
beforeEach(function() {
tree.enableFeature('Checkbox', {
checkboxClassName: 'tui-tree-checkbox',
checkboxCascade: 'both'
});
this.firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
this.childIds = tree.getChildIds(this.firstChildId);
});
it('should check with all descendants', function() {
tree.check(this.firstChildId);
tree.each(function(node, nodeId) {
expect(tree.isChecked(nodeId)).toBe(true);
}, this.firstChildId);
});
it('should be indeterminate if some children are checked', function() {
tree.check(this.childIds[0]);
expect(tree.isIndeterminate(this.firstChildId)).toBe(true);
});
});
describe('When cascade option is false', function() {
beforeEach(function() {
tree.enableFeature('Checkbox', {
checkboxClassName: 'tui-tree-checkbox',
checkboxCascade: false
});
this.firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
this.childIds = tree.getChildIds(this.firstChildId);
});
it('should children remain unchecked even if parents are checked', function() {
tree.check(this.firstChildId);
tree.each(function(node, nodeId) {
expect(tree.isChecked(nodeId)).toBe(false);
}, this.firstChildId);
});
it('should not be indeterminate even if some children are checked', function() {
tree.check(this.childIds[0]);
expect(tree.isIndeterminate(this.firstChildId)).toBe(false);
});
});
describe('When cascade option is up', function() {
beforeEach(function() {
tree.enableFeature('Checkbox', {
checkboxClassName: 'tui-tree-checkbox',
checkboxCascade: 'up'
});
this.firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
this.childIds = tree.getChildIds(this.firstChildId);
});
it('should children remain unchecked even if parents are checked', function() {
tree.check(this.firstChildId);
tree.each(function(node, nodeId) {
expect(tree.isChecked(nodeId)).toBe(false);
}, this.firstChildId);
});
it('should be indeterminate if some children are checked', function() {
tree.check(this.childIds[0]);
expect(tree.isIndeterminate(this.firstChildId)).toBe(true);
});
});
describe('When cascade option is down', function() {
beforeEach(function() {
tree.enableFeature('Checkbox', {
checkboxClassName: 'tui-tree-checkbox',
checkboxCascade: 'down'
});
this.firstChildId = tree.getChildIds(tree.getRootNodeId())[0];
this.childIds = tree.getChildIds(this.firstChildId);
});
it('should check with all descendants', function() {
tree.check(this.firstChildId);
tree.each(function(node, nodeId) {
expect(tree.isChecked(nodeId)).toBe(true);
}, this.firstChildId);
});
it('should not be indeterminate even if some children are checked', function() {
tree.check(this.childIds[0]);
expect(tree.isIndeterminate(this.firstChildId)).toBe(false);
});
});
});
@@ -0,0 +1,157 @@
var Tree = require('../../src/js/tree');
var TuiContextMenu = require('tui-context-menu');
function clearBody() {
var flElement = document.getElementById('tree-fl');
var treeElement = document.getElementById('tree');
if (flElement && flElement.parentNode) {
flElement.parentNode.removeChild(flElement);
}
if (treeElement && treeElement.parentNode) {
treeElement.parentNode.removeChild(treeElement);
}
}
describe('contextMenu feature', function() {
var rootElement, tree, contextMenu, menuData;
var data = [
{
text: 'A',
children: [
{ text: '1' },
{ text: '2' },
{ text: '3' },
{ text: '4' },
{
text: '5',
children: [{ text: '가', children: [{ text: '*' }] }, { text: '나' }]
},
{ text: '6' },
{ text: '7' },
{ text: '8' },
{ text: '9', children: [{ text: '가' }, { text: '나' }] },
{ text: '10' },
{ text: '11' },
{ text: '12' }
]
},
{
text: 'B',
children: [{ text: '1' }, { text: '2' }, { text: '3' }, { text: '4' }, { text: '5' }]
}
];
beforeEach(function() {
loadFixtures('basicFixture.html');
tree = new Tree('#tree', {
rootElement: 'treeRoot',
data: data
});
menuData = [
{ title: 'menu1' },
{ title: 'menu2' },
{
title: 'menu3',
menu: [{ title: 'submenu1' }, { title: 'submenu2' }]
}
];
tree.enableFeature('ContextMenu', {
menuData: menuData
});
rootElement = tree.rootElement;
contextMenu = tree.enabledFeatures.ContextMenu;
});
afterEach(function() {
clearBody();
});
describe('When _generateContextMenu() is called,', function() {
it('should not generate new floating layer, if flElement exist', function() {
var flElement = document.createElement('div');
flElement.id = 'tree-fl';
clearBody();
loadFixtures('basicFixture.html');
document.body.appendChild(flElement);
tree = new Tree('#tree', {
rootElement: 'treeRoot',
data: data
});
tree.enableFeature('ContextMenu', {
menuData: menuData
});
expect(tree.enabledFeatures.ContextMenu.flElement).toEqual(flElement);
});
it('new floating layer should be generated', function() {
contextMenu.flElement = null;
contextMenu._createFloatingLayer = jest.fn();
contextMenu._generateContextMenu();
expect(contextMenu._createFloatingLayer).toHaveBeenCalled();
});
it('should generate and return instance of ContextMenu', function() {
var menu = contextMenu._generateContextMenu();
expect(menu instanceof TuiContextMenu).toEqual(true);
});
});
it('id of selected tree item should set value when "contextmenu" event is fired', function() {
var target = rootElement.querySelector('li');
var nodeId = target.getAttribute('id');
tree.getNodeIdFromElement = jest.fn().mockReturnValue(nodeId);
tree._onContextMenu({
target: target
});
expect(contextMenu.selectedNodeId).toEqual(nodeId);
});
it('custom event as "selectContextMenu" should be fired when the context menu is selected', function() {
var spyListener = jest.fn();
var mock = {
command: 'test',
nodeId: null
};
tree.on('selectContextMenu', spyListener);
contextMenu._onSelect({}, 'test'); // select context menu
expect(spyListener).toHaveBeenCalledWith(mock);
});
describe('When context-menu feature is disabled,', function() {
beforeEach(function() {
contextMenu._restoreTextSelection = jest.fn();
tree.disableFeature('ContextMenu');
});
it('events should be removed', function() {
var spyListener = jest.fn();
tree.on('selectContextMenu', spyListener);
expect(spyListener).not.toHaveBeenCalled();
});
it('text selection property should restore', function() {
expect(contextMenu._restoreTextSelection).toHaveBeenCalled();
});
});
});
@@ -0,0 +1,275 @@
var hasClass = require('tui-code-snippet/domUtil/hasClass');
var Tree = require('../../src/js/tree');
describe('draggable feature', function() {
var rootElement, tree, treeDraggable;
var data = [
{
title: 'A',
children: [
{ title: '1' },
{ title: '2' },
{ title: '3' },
{ title: '4' },
{
title: '5',
children: [{ title: '가', children: [{ title: '*' }] }, { title: '나' }]
},
{ title: '6' },
{ title: '7' },
{ title: '8' },
{ title: '9', children: [{ title: '가' }, { title: '나' }] },
{ title: '10' },
{ title: '11' },
{ title: '12' }
]
},
{
title: 'B',
children: [{ title: '1' }, { title: '2' }, { title: '3' }]
}
];
beforeEach(function() {
loadFixtures('basicFixture.html');
tree = new Tree('#tree', {
rootElement: 'treeRoot',
data: data
});
rootElement = tree.rootElement;
tree.enableFeature('Draggable', {
isSortable: true,
lineBoundary: {
top: 5,
bottom: 5
}
});
treeDraggable = tree.enabledFeatures.Draggable;
});
describe('when _applyMoveAction() is called,', function() {
var currentElement, nodeId, mousePos;
beforeEach(function() {
currentElement = rootElement.querySelector('li');
nodeId = tree.getNodeIdFromElement(currentElement);
mousePos = [10, 20];
});
it('mouse overed tree item should be changed to hover style', function() {
treeDraggable._isContain = jest.fn().mockReturnValue(true);
treeDraggable._hover = jest.fn();
treeDraggable._applyMoveAction(nodeId, mousePos);
expect(treeDraggable._hover).toHaveBeenCalledWith(nodeId);
});
it('mouse overed tree item should be changed to unhover style', function() {
treeDraggable._isContain = jest.fn().mockReturnValue(false);
treeDraggable._unhover = jest.fn();
treeDraggable._applyMoveAction(nodeId, mousePos);
expect(treeDraggable._unhover).toHaveBeenCalled();
});
it('should draw boundary line on tree items when drag type is sortable', function() {
treeDraggable._getBoundaryType = jest.fn().mockReturnValue('top');
treeDraggable._drawBoundaryLine = jest.fn();
treeDraggable.isSortable = true;
treeDraggable._applyMoveAction(nodeId, mousePos);
expect(treeDraggable._drawBoundaryLine).toHaveBeenCalled();
});
});
it('Mouse position should be contained tree item, _hover() add hover style', function() {
var currentElement = rootElement.querySelector('li');
var nodeId = tree.getNodeIdFromElement(currentElement);
tree.isLeaf = jest.fn().mockReturnValue(true);
treeDraggable.hoveredElement = currentElement;
treeDraggable._hover(nodeId);
expect(hasClass(currentElement, treeDraggable.hoverClassName)).toEqual(true);
});
it('Mouse position should be out of tree item, _unhover() remove hover style', function() {
var currentElement = rootElement.querySelector('li');
var nodeId = tree.getNodeIdFromElement(currentElement);
treeDraggable._unhover(nodeId);
expect(hasClass(currentElement, treeDraggable.hoverClassName)).toEqual(false);
});
describe('when _isContain method is called,', function() {
var targetPos, mousePos, state;
beforeEach(function() {
targetPos = {
left: 0,
top: 0,
right: 100,
bottom: 100
};
});
it('should return true state when mouse position is in enable touched area', function() {
mousePos = [10, 20];
state = treeDraggable._isContain(targetPos, mousePos);
expect(state).toEqual(true);
});
it('should return false state when mouse position is not in enable touched area', function() {
mousePos = [2, 3];
state = treeDraggable._isContain(targetPos, mousePos);
expect(state).toEqual(false);
});
});
describe('when _getBoundaryType method is called,', function() {
var targetPos, mousePos, type;
beforeEach(function() {
targetPos = {
left: 0,
top: 0,
right: 100,
bottom: 100
};
});
it('boundary type should be "top" when mouse postion is above selected tree item', function() {
mousePos = [3, 3];
type = treeDraggable._getBoundaryType(targetPos, mousePos);
expect(type).toEqual('top');
});
it('boundary type should be "bottom" when mouse postion is above selected tree item', function() {
mousePos = [97, 97];
type = treeDraggable._getBoundaryType(targetPos, mousePos);
expect(type).toEqual('bottom');
});
});
it('_drawBoundaryLine() should draw boundary line on tree when boundary type has value', function() {
var targetPos = {
left: 10,
top: 10,
right: 10,
bottom: 10
};
var boundaryType = 'top';
treeDraggable._initMovingLine();
treeDraggable._drawBoundaryLine(targetPos, boundaryType);
expect(treeDraggable.lineElement.style.display).toEqual('block');
});
describe('when _getIndexForInserting() is called,', function() {
var currentElements, helperId, nodeId, index;
beforeEach(function() {
currentElements = rootElement.querySelectorAll('li');
});
it('index number should be same when drag item is moving from bottom to top', function() {
helperId = tree.getNodeIdFromElement(currentElements[1]);
nodeId = tree.getNodeIdFromElement(currentElements[3]);
treeDraggable.currentNodeId = helperId;
treeDraggable.movingLineType = 'top';
index = treeDraggable._getIndexToInsert(nodeId);
expect(index).toEqual(2);
});
it('index number should increase when drag item is moving from top to bottom', function() {
helperId = tree.getNodeIdFromElement(currentElements[3]); // index: 2
nodeId = tree.getNodeIdFromElement(currentElements[1]); // index: 0
treeDraggable.currentNodeId = helperId;
treeDraggable.movingLineType = 'bottom';
index = treeDraggable._getIndexToInsert(nodeId);
expect(index).toEqual(1);
});
});
it('dragging action should be cancel when invoking "beforeMove"', function() {
var eventMock = {
target: null
};
tree.model.move = jest.fn();
tree.on('beforeMove', function() {
return false;
});
treeDraggable._onMouseup(eventMock);
expect(tree.model.move).not.toHaveBeenCalled();
});
describe('when "_setClassNameOnDragItem" should', function() {
var firstChildId, firstChildElement, eventMock;
var className = 'tui-tree-drag';
beforeEach(function() {
firstChildId = tree.model.rootNode.getChildIds()[0];
firstChildElement = document.getElementById(firstChildId);
eventMock = {
target: firstChildElement,
clientX: 10,
clientY: 10
};
});
it('should add class name on dragging item element while dragging', function() {
treeDraggable._onMousedown(eventMock);
treeDraggable._onMousemove(eventMock);
expect(hasClass(firstChildElement, className)).toBe(true);
});
});
it('the contents of the helper element should be same as the contents of the dragging node when the node is dragging', function() {
var firstChildId, selectedElement, dragItemElement, eventMock;
firstChildId = tree.model.rootNode.getChildIds()[0];
selectedElement = document.getElementById(firstChildId);
eventMock = {
target: selectedElement
};
treeDraggable._onMousedown(eventMock);
dragItemElement = selectedElement.querySelector('.' + tree.classNames.textClass);
expect(treeDraggable.helperElement.innerHTML).toBe(dragItemElement.innerHTML);
});
});
@@ -0,0 +1,130 @@
var Tree = require('../../src/js/tree');
describe('editable feature', function() {
var tree, treeEditable, rootNodeId;
var firstChildId, lastChildId;
var firstChildElement;
var data = [
{ title: 'A', state: 'closed', children: [{ title: '1' }, { title: '2' }, { title: '3' }] },
{ title: 'B' }
];
var WRAPPER_CLASSNAME = 'tui-input-wrap';
beforeEach(function() {
loadFixtures('basicFixture.html');
tree = new Tree('#tree', {
rootElement: 'treeRoot',
data: data,
template: {
internalNode:
'<button type="button" class="{{toggleBtnClass}}">{{stateLabel}}</button>' +
'<span class="{{textClass}}">{{title}}</span>' +
'<ul>{{children}}</ul>',
leafNode: '<span class="{{textClass}}">{{title}}</span>'
}
});
rootNodeId = tree.getRootNodeId();
firstChildId = tree.getChildIds(rootNodeId)[0];
lastChildId = tree.getChildIds(rootNodeId)[1];
firstChildElement = document.getElementById(firstChildId);
tree.enableFeature('Editable', {
dataKey: 'text'
});
treeEditable = tree.enabledFeatures.Editable;
});
describe('createChildNode()', function() {
it('should show children nodes when parent node is not leaf node', function() {
var stateLabel;
var toggleBtnClass = tree.classNames.toggleBtnClass;
var expected = tree.stateLabels.closed;
tree.createChildNode(firstChildId);
stateLabel = firstChildElement.querySelector('.' + toggleBtnClass);
expect(stateLabel.innerHTML).toBe(expected);
});
it('should add toggle button when parent node is leaf node', function() {
var stateLabels;
var toggleBtnClass = tree.classNames.toggleBtnClass;
var expected = tree.stateLabels.closed;
tree.createChildNode(lastChildId);
stateLabels = firstChildElement.querySelectorAll('.' + toggleBtnClass);
expect(stateLabels.length).toBe(1);
expect(stateLabels[0].innerHTML).toBe(expected);
});
it('should fire "successResponse" when Ajax feature is enabled', function() {
xhrMock.install();
treeEditable._onSuccessResponse = jest.fn();
tree.enableFeature('Ajax', {
command: {
read: {
url: 'api/test'
}
}
});
tree.createChildNode(lastChildId);
tree.fire('successAjaxResponse');
expect(treeEditable._onSuccessResponse).toHaveBeenCalled();
xhrMock.uninstall();
});
});
it('"editNode()" should create input element in selected node', function() {
var inputElements = firstChildElement.getElementsByTagName('input');
tree.editNode(firstChildId);
expect(inputElements.length).toBe(1);
});
it('"_attachInputElement" should attach the input element to selected node', function() {
treeEditable._attachInputElement(firstChildId);
expect(firstChildElement.querySelectorAll('.' + WRAPPER_CLASSNAME).length).toBe(1);
});
it("should calculate input element's paddingLeft by it's depth when editable feature is enabled", function() {
/* expected padding-left: 23 */
var nodeElements = tree.rootElement.querySelectorAll('.tui-tree-node');
var inputWrapper, result;
tree.editNode(nodeElements[0].id); // depth 1
inputWrapper = nodeElements[0].querySelector('.' + WRAPPER_CLASSNAME);
result = tree.getIndentWidth(nodeElements[0].id);
expect(inputWrapper.style.paddingLeft).toBe(result + 'px');
tree.editNode(nodeElements[1].id); // depth 2
inputWrapper = nodeElements[1].querySelector('.' + WRAPPER_CLASSNAME);
result = tree.getIndentWidth(nodeElements[1].id);
expect(inputWrapper.style.paddingLeft).toBe(result + 'px');
});
it('"finishEditing()" should remove input element of editing node', function() {
var inputElements;
tree.editNode(firstChildId);
inputElements = firstChildElement.getElementsByTagName('input');
expect(inputElements.length).toBe(1);
tree.finishEditing();
inputElements = firstChildElement.getElementsByTagName('input');
expect(inputElements.length).toBe(0);
});
});
@@ -0,0 +1,190 @@
var hasClass = require('tui-code-snippet/domUtil/hasClass');
var Tree = require('../../src/js/tree');
var messages = require('../../src/js/consts/messages');
describe('selectable feature', function() {
var data = [
{
title: 'A',
children: [
{ title: '1' },
{ title: '2' },
{ title: '3' },
{ title: '4' },
{
title: '5',
children: [{ title: '가', children: [{ title: '*' }] }, { title: '나' }]
},
{ title: '6' },
{ title: '7' },
{ title: '8' },
{ title: '9', children: [{ title: '가' }, { title: '나' }] },
{ title: '10' },
{ title: '11' },
{ title: '12' }
]
},
{ title: 'B', children: [{ title: '1' }, { title: '2' }, { title: '3' }] }
];
var rootElement, tree, treeSelection;
beforeEach(function() {
loadFixtures('basicFixture.html');
tree = new Tree('#tree', {
rootElement: 'treeRoot',
data: data
});
rootElement = tree.rootElement;
tree.enableFeature('Selectable');
// "tree.enabledFeatures.Selectable" is not constructor but instance.
treeSelection = tree.enabledFeatures.Selectable;
});
describe('events', function() {
var eventMock, target;
beforeEach(function() {
target = rootElement.querySelectorAll('.tui-tree-node')[2];
eventMock = {
target: target
};
});
it('should not invoke "beforeSelect" if the selected node does not exist', function() {
var spyListener = jest.fn();
eventMock = {
target: null
};
tree.on('beforeSelect', spyListener);
tree.fire('singleClick', eventMock);
expect(spyListener).not.toHaveBeenCalled();
});
it('should invoke "beforeSelect" if the selected node exists', function() {
var beforeSelectListenerSpy = jest.fn();
tree.on('beforeSelect', beforeSelectListenerSpy);
treeSelection.onSingleClick(eventMock);
expect(beforeSelectListenerSpy).toHaveBeenCalled();
});
it('should fire "select" event if the "beforeSelect"-listener does not return false', function() {
var selectListenerSpy = jest.fn();
tree.on('beforeSelect', function() {
return '';
});
tree.on('select', selectListenerSpy);
treeSelection.onSingleClick(eventMock);
expect(selectListenerSpy).toHaveBeenCalled();
});
it('should not fire "select" event if the "beforeSelect"-listener returns false', function() {
var selectListenerSpy = jest.fn();
tree.on('beforeSelect', function() {
return false;
});
tree.on('select', selectListenerSpy);
treeSelection.onSingleClick(eventMock);
expect(selectListenerSpy).not.toHaveBeenCalled();
});
it('should fire custom events with args containing "nodeId" and "prevNodeId"', function() {
var beforeSelectListenerSpy = jest.fn();
var selectListenerSpy = jest.fn();
var curNodeId = target.id;
var prevNodeId = 'previousNodeId';
var expected;
treeSelection.selectedNodeId = prevNodeId;
tree.on('beforeSelect', beforeSelectListenerSpy);
tree.on('select', selectListenerSpy);
treeSelection.onSingleClick(eventMock);
expected = {
nodeId: curNodeId,
prevNodeId: prevNodeId,
target: target
};
expect(beforeSelectListenerSpy).toHaveBeenCalledWith(expected);
expect(selectListenerSpy).toHaveBeenCalledWith(expected);
});
});
describe('API', function() {
it('should throw error when the feature-"Selectable" is not enabled', function() {
tree.disableFeature('Selectable');
expect(tree.select).toThrowError(messages.INVALID_API_SELECTABLE);
expect(tree.getSelectedNodeId).toThrowError(messages.INVALID_API_SELECTABLE);
});
it('should invoke "beforeSelect" and fire "select"', function() {
var beforeSelectListenerSpy = jest.fn();
var selectListenerSpy = jest.fn();
var targetId = rootElement.querySelectorAll('.tui-tree-node')[2].id;
tree.on({
beforeSelect: beforeSelectListenerSpy,
select: selectListenerSpy
});
tree.select(targetId);
expect(beforeSelectListenerSpy).toHaveBeenCalled();
expect(selectListenerSpy).toHaveBeenCalled();
});
it('should have "getSelectedNodeId" api', function() {
var targetId = rootElement.querySelectorAll('.tui-tree-node')[2].id;
tree.select(targetId);
expect(tree.getSelectedNodeId()).toEqual(targetId);
});
it('deselect() should reset node state after selecting', function() {
var nodeId = rootElement.querySelectorAll('.tui-tree-node')[0].id;
var className = treeSelection.selectedClassName;
var selectedNode;
tree.select(nodeId);
tree.deselect();
selectedNode = rootElement.querySelector('#' + nodeId);
expect(hasClass(selectedNode, className)).toBe(false);
});
it('deselect() should invoke "deselect" event', function() {
var nodeId = rootElement.querySelector('.tui-tree-node').id;
var handler = jest.fn();
tree.select(nodeId);
tree.on('deselect', handler);
tree.deselect();
expect(handler).toHaveBeenCalled();
});
it('deselect() should not invoke "deselect" event when node is removed', function() {
var nodeId = rootElement.querySelector('.tui-tree-node').id;
var handler = jest.fn();
tree.select(nodeId);
tree.remove(nodeId);
tree.on('deselect', handler);
tree.deselect();
expect(handler).not.toHaveBeenCalled();
});
});
});
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Toast UI Component: Tree</title>
</head>
<body>
<div id="tree">
<ul id="treeRoot"></ul>
</div>
</body>
</html>
@@ -0,0 +1,70 @@
var path = require('path');
var fs = require('fs');
var fixturesPath = './fixtures';
global.loadFixtures = function(fileName) {
var data = '';
var dir = path.resolve(__dirname, fixturesPath, fileName);
try {
data = fs.readFileSync(dir, 'utf8'); // eslint-disable-line no-sync
} catch (err) {
// do nothing
} finally {
document.body.innerHTML = data;
}
};
global.xhrMock = {
open: jest.fn(),
send: jest.fn().mockImplementation(function() {
this.readyState = 4;
this.onreadystatechange();
}),
setRequestHeader: jest.fn(),
getResponseHeader: jest.fn().mockImplementation(function(key) {
return this.responseHeaders && this.responseHeaders[key];
}),
getAllResponseHeaders: jest.fn().mockImplementation(function() {
var result, i, keys, length;
if (this.responseHeaders) {
result = '';
keys = Object.keys(this.responseHeaders);
length = keys.length;
for (i = 0; i < length; i += 1) {
result += keys[i] + ': ' + this.responseHeaders[keys[i]] + '\r\n';
}
}
return result;
}),
setResponse: function(options) {
this.xhr.status = options.status;
this.xhr.statusText = options.statusText;
this.xhr.responseText = options.responseText || '\r\n';
this.xhr.responseHeaders = options.headers || '\r\n';
},
install: function() {
var xhr = {
open: this.open,
send: this.send,
setRequestHeader: this.setRequestHeader,
getResponseHeader: this.getResponseHeader,
getAllResponseHeaders: this.getAllResponseHeaders,
status: 200
};
this.xhr = xhr;
this.setResponse({});
this.originalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
return xhr;
};
},
uninstall: function() {
window.XMLHttpRequest = this.originalXHR;
}
};
@@ -0,0 +1,355 @@
var template = require('tui-code-snippet/domUtil/template');
var Tree = require('../src/js/tree');
var util = require('../src/js/util');
var messages = require('../src/js/consts/messages');
describe('Tree', function() {
var data = [
{
text: 'A',
children: [
{ text: '1' },
{ text: '2' },
{ text: '3' },
{ text: '4' },
{ text: '5', children: [{ text: '가', children: [{ text: '*' }] }, { text: '나' }] },
{ text: '6' },
{ text: '7' },
{ text: '8' },
{ text: '9', children: [{ text: '가' }, { text: '나' }] },
{ text: '10' },
{ text: '11' },
{ text: '12' }
]
},
{
text: 'B',
children: [{ text: '1' }, { text: '2' }, { text: '3' }, { text: '4' }, { text: '5' }]
}
];
var rootElement, tree, firstChildId, lastChildId, grandChildId, firstChild, container;
describe('function', function() {
beforeEach(function() {
loadFixtures('basicFixture.html');
container = '#tree';
tree = new Tree(container, {
rootElement: 'treeRoot',
data: data,
template: {
leafNode:
'<span class="tui-tree-leaf-label"></span>' +
'<span class="{{textClass}}">{{text}}</span>'
}
});
rootElement = tree.rootElement;
firstChildId = tree.model.rootNode.getChildIds()[0];
firstChild = tree.model.getNode(firstChildId);
lastChildId = tree.model.rootNode.getChildIds().slice(-1)[0]; // slice(-1) returns a value of last index
grandChildId = tree.model.getNode(firstChildId).getChildIds()[0];
});
it('should run once per node ', function() {
jest.spyOn(tree, '_setNodeClassNameAndVisibility');
tree.refresh();
// root 1 + data 24
expect(tree._setNodeClassNameAndVisibility).toHaveBeenCalledTimes(25);
});
it('should throw an error if has invalid root element', function() {
expect(function() {
return new Tree('#tree2');
}).toThrowError(messages.INVALID_CONTAINER_ELEMENT);
});
it('should have a root element', function() {
// Node.ELEMENT_NODE === 1
// in IE8,
// element instanceof HTMLElement --> error
// element.nodeType === Node.ELEMENT_NODE --> error
expect(tree.rootElement.nodeType).toEqual(1);
});
it('should make correct DOM', function() {
var textElement = rootElement.querySelector('.tui-tree-node .tui-tree-text');
var textNode = util.getFirstTextNode(textElement);
expect(textNode.nodeValue).toEqual('A');
});
it('should make DOM from optional-template', function() {
var leafNodes = rootElement.querySelectorAll('.tui-tree-leaf');
var leafLabels = rootElement.querySelectorAll('.tui-tree-leaf .tui-tree-leaf-label');
expect(leafLabels.length).toEqual(leafNodes.length);
});
it('"open()" should change state of a node to "opened"', function() {
tree.open(firstChildId);
expect(firstChild.getState()).toEqual('opened');
});
it('"close()" should change state of a node to "closed"', function() {
tree.close(firstChildId);
expect(firstChild.getState()).toEqual('closed');
});
it('"toggle()" should toggle state of a node', function() {
tree.open(firstChildId);
tree.toggle(firstChildId);
expect(firstChild.getState()).toEqual('closed');
tree.toggle(firstChildId);
expect(firstChild.getState()).toEqual('opened');
});
it('should fire singleClick event', function() {
var handler = jest.fn();
var eventMock = {
target: document.createElement('DIV')
};
jest.useFakeTimers();
tree.on('singleClick', handler);
tree.clickTimer = null; // No clicked
tree._onClick(eventMock); // Single click
jest.advanceTimersByTime(401);
expect(handler).toHaveBeenCalled();
jest.useRealTimers();
});
it('"open(), close()" should change button label', function() {
var firstChildElement = document.getElementById(firstChildId);
var btnElement = firstChildElement.querySelector('.' + tree.classNames.toggleBtnClass);
var textNode = util.getFirstTextNode(btnElement);
tree.close(firstChildId);
expect(textNode.nodeValue).toEqual(tree.stateLabels.closed);
tree.open(firstChildId);
expect(textNode.nodeValue).toEqual(tree.stateLabels.opened);
});
describe('"open(), close()" recursive action', function() {
var oneDepthId;
var twoDepthId;
var lastDepthId;
var oneDepthNode;
var twoDepthNode;
var lastDepthNode;
beforeEach(function() {
oneDepthId = tree.model.rootNode.getChildIds()[0];
oneDepthNode = tree.model.getNode(oneDepthId);
twoDepthId = oneDepthNode.getChildIds()[4];
twoDepthNode = tree.model.getNode(twoDepthId);
lastDepthId = twoDepthNode.getChildIds()[0];
lastDepthNode = tree.model.getNode(lastDepthId);
});
it('"open()" using recursive option should change the state of all parent nodes to "open"', function() {
tree.open(lastDepthId, true);
expect(oneDepthNode.getState()).toBe('opened');
expect(twoDepthNode.getState()).toBe('opened');
expect(lastDepthNode.getState()).toBe('opened');
});
it('"close()" using recursive option should change the state of all children nodes to "close"', function() {
tree.open(tree.model.rootNode.getChildIds()[0]);
tree.open(oneDepthNode.getChildIds()[4]);
tree.open(twoDepthNode.getChildIds()[0]);
tree.close(tree.model.rootNode.getChildIds()[0], true);
expect(oneDepthNode.getState()).toBe('closed');
expect(twoDepthNode.getState()).toBe('closed');
expect(lastDepthNode.getState()).toBe('closed');
});
});
it('should fire doubleClick event', function() {
var handler = jest.fn();
var eventMock = {
target: document.createElement('DIV')
};
tree.on('doubleClick', handler);
tree._onDoubleClick(eventMock);
expect(handler).toHaveBeenCalled();
});
it('"add() with no "isSilent" flag" should redraw nodes', function() {
var childCount = firstChild.getChildIds().length;
var subtreeElement;
var testData = [{ text: 'hello world' }, { text: 'new world' }];
jest.spyOn(tree, '_draw');
tree.add(testData, firstChildId);
subtreeElement = document.getElementById(firstChildId).lastChild;
expect(firstChild.getChildIds().length).toEqual(childCount + 2);
expect(subtreeElement.childNodes.length).toEqual(childCount + 2);
});
it('"add()" should return node ids of new added nodes', function() {
var testData = [
{ text: 'hello world', children: [{ text: 'foo' }, { text: 'bar' }] },
{ text: 'new world' }
];
var ids = tree.add(testData, firstChildId, false, false);
expect(ids).toEqual(expect.any(Array));
expect(ids.length).toEqual(2);
});
it('"remove()" should redraw nodes without removed node(s)', function() {
var childCount = firstChild.getChildIds().length;
var idForRemoving = firstChild.getChildIds()[0];
var subtreeElement;
tree.remove(idForRemoving);
subtreeElement = document.getElementById(firstChildId).lastChild;
expect(document.getElementById(idForRemoving)).toBeFalsy();
expect(firstChild.getChildIds().length).toEqual(childCount - 1);
expect(subtreeElement.childNodes.length).toEqual(childCount - 1);
});
it('"move()" should redraw nodes with a moved node', function() {
var firstChildElement, lastChildElement, grandChildElement;
tree.move(grandChildId, lastChildId);
firstChildElement = document.getElementById(firstChildId);
lastChildElement = document.getElementById(lastChildId);
grandChildElement = document.getElementById(grandChildId);
expect(firstChildElement.contains(grandChildElement)).toBe(false);
expect(lastChildElement.contains(grandChildElement)).toBe(true);
});
it('"move()" should fire "move" event with some related ids', function() {
var mock = jest.fn();
var index = -1;
tree.on('move', mock);
tree.move(grandChildId, lastChildId, index);
expect(mock).toHaveBeenCalledWith({
nodeId: grandChildId,
originalParentId: firstChildId,
newParentId: lastChildId,
index: index
});
});
it('"search()" should return array of node ids', function() {
var result = tree.search({
text: '5'
});
expect(result).toEqual(expect.any(Array));
expect(result.length).toEqual(2);
result = tree.search(function(node) {
var text = node.getData('text');
return text === '가' || text === '나';
});
expect(result).toEqual(expect.any(Array));
expect(result.length).toEqual(4);
});
it('option:"renderTemplate" should override template renderer', function() {
var templateRenderer = jest.fn().mockImplementation(function(source, props) {
return template(source, props);
});
tree = new Tree(container, {
rootElement: 'treeRoot',
data: data,
template: {
leafNode:
'<span class="tui-tree-leaf-label"></span>' +
'<span class="{{textClass}}">{{text}}</span>'
},
renderTemplate: templateRenderer
});
expect(templateRenderer).toHaveBeenCalled();
});
it('"removeAllChildren()" should remove all child nodes', function() {
var rootNodeId = tree.getRootNodeId();
tree.removeAllChildren(firstChildId);
expect(tree.getChildIds(firstChildId).length).toBe(0);
expect(tree.getChildIds(rootNodeId).length).toBe(2);
tree.removeAllChildren(rootNodeId);
expect(tree.getChildIds(rootNodeId).length).toBe(0);
});
it('"resetAllData()" should reset all nodes from new data', function() {
var testData = [{ text: 'hello' }, { text: 'wolrd' }];
var newChildIds = tree.resetAllData(testData);
var rootNodeId = tree.getRootNodeId();
expect(newChildIds.length).toBe(2);
expect(tree.getChildIds(rootNodeId)).toEqual(newChildIds);
expect(rootElement.children.length).toBe(2);
});
it('"getNodeIndex()" should return index number of selected node in children list', function() {
var testData = [{ text: 'child1' }, { text: 'child2' }];
var newChildIds = tree.resetAllData(testData);
var prevNodeIdx = tree.getNodeIndex(newChildIds[1]);
expect(prevNodeIdx).toEqual(1);
});
it('it should reset children data of nodeId when "resetAllData()" have nodeId parameter', function() {
var testData = [{ text: 'A' }, { text: 'B' }];
var nodeId = tree.getChildIds(tree.getRootNodeId())[0];
var newChildIds = tree.resetAllData(testData, nodeId);
expect(tree.getNodeData(newChildIds[0])).toEqual({ text: 'A' });
expect(tree.getNodeData(newChildIds[1])).toEqual({ text: 'B' });
});
});
describe('option', function() {
beforeEach(function() {
util.sendHostName = jest.fn();
loadFixtures('basicFixture.html');
container = '#tree';
});
it('should send hostname by default', function() {
tree = new Tree(container, {
rootElement: 'treeRoot',
data: data
});
expect(util.sendHostName).toHaveBeenCalled();
});
it('should not send hostname on usageStatistics option false', function() {
tree = new Tree(container, {
rootElement: 'treeRoot',
data: data,
usageStatistics: false
});
expect(util.sendHostName).not.toHaveBeenCalled();
});
});
});
@@ -0,0 +1,200 @@
var TreeModel = require('../src/js/treeModel');
describe('TreeModel', function() {
var treeModel;
var data = [
{
text: 'A',
hiddenValue: 'Child of root',
children: [
{ text: '1' },
{ text: '2' },
{ text: '3' },
{ text: '4' },
{
text: '5',
children: [{ text: '가', children: [{ text: '*' }] }, { text: '나' }]
},
{ text: '6' },
{ text: '7' },
{ text: '8' },
{ text: '9', children: [{ text: '가' }, { text: '나' }] },
{ text: '10' },
{ text: '11' },
{ text: '12' }
]
},
{
text: 'B',
state: 'opened',
children: [{ text: '1' }, { text: '2' }, { text: '3' }]
},
{ text: 'C', children: [{ text: '1' }] }
];
beforeEach(function() {
treeModel = new TreeModel({
data: data,
defaultState: 'closed',
nodeIdPrefix: 'tree-node-'
});
});
it('should have the rootNode', function() {
expect(treeModel.rootNode).toBeDefined();
});
it('should have the length of all nodes within rootNode', function() {
expect(treeModel.getCount()).toEqual(25);
});
it('Find method should return a node', function() {
var id = treeModel.rootNode.getChildIds()[0];
var node = treeModel.getNode(id);
expect(node.getData('hiddenValue')).toEqual('Child of root');
expect(node.getData('text')).toEqual('A');
expect(node.getAllData()).toEqual({
text: 'A',
hiddenValue: 'Child of root'
});
});
it('- Add method should append a node to a specific parent node', function() {
var parentId = treeModel.rootNode.getChildIds()[0];
var parentNode = treeModel.getNode(parentId);
var testData = {
text: 'This node will be added to the first node'
};
var id, node;
treeModel.add(testData, parentId);
id = parentNode.getChildIds()[12];
node = treeModel.getNode(id);
expect(node.getData('text')).toEqual('This node will be added to the first node');
expect(node.getParentId()).toEqual(parentId);
});
it('- Remove method should remove a node in treeModel', function() {
var id = treeModel.rootNode.getChildIds()[0];
var childIds = treeModel.getNode(id).getChildIds();
var searched;
treeModel.remove(id);
expect(treeModel.rootNode.getChildIds()).not.toContain(id);
searched = treeModel.getNode(childIds[0]);
expect(searched).toBeFalsy();
searched = treeModel.getNode(childIds[11]);
expect(searched).toBeFalsy();
});
it('- Each(all) method should iterate over all descendants of a (root or specific)node', function() {
var iteratee = jest.fn();
var firstChildIdOfRoot = treeModel.rootNode.getChildIds()[0];
treeModel.eachAll(iteratee);
expect(iteratee).toHaveBeenCalledTimes(25);
iteratee.mockReset();
treeModel.each(iteratee, firstChildIdOfRoot);
expect(iteratee).toHaveBeenCalledTimes(17);
});
describe('- Sort method', function() {
var comparator;
beforeEach(function() {
comparator = function(nodeA, nodeB) {
var aValue = nodeA.getData('text');
var bValue = nodeB.getData('text');
if (!bValue.localeCompare) {
return 0;
}
return bValue.localeCompare(aValue);
};
});
it('called basically', function() {
var childIds = treeModel.rootNode.getChildIds();
treeModel.sort(comparator);
expect(childIds.reverse()).toEqual(treeModel.rootNode.getChildIds());
});
it('called with id of a node to sort partially', function() {
var parentId = treeModel.rootNode.getChildIds()[1];
var childIds = treeModel.getNode(parentId).getChildIds();
treeModel.sort(comparator, false, parentId);
expect(childIds.reverse()).toEqual(treeModel.getNode(parentId).getChildIds());
});
});
it('should support initial state for each node', function() {
var openedNodeId = treeModel.rootNode.getChildIds()[1];
var node = treeModel.getNode(openedNodeId);
expect(node.getState()).toEqual('opened');
});
it('should fire update event when a node added', function() {
var handler = jest.fn();
var rootId = treeModel.rootNode.getId();
treeModel.on('update', handler);
treeModel.add({ title: 'new node' }, rootId);
expect(handler).toHaveBeenCalledWith(rootId);
});
it('should fire update event when a node removed', function() {
var handler = jest.fn();
var rootId = treeModel.rootNode.getId();
var firstChildId = treeModel.rootNode.getChildIds()[0];
treeModel.on('update', handler);
treeModel.remove(firstChildId);
expect(handler).toHaveBeenCalledWith(rootId);
});
it('should fire update event when node data changed', function() {
var handler = jest.fn();
var firstChildId = treeModel.rootNode.getChildIds()[0];
treeModel.on('update', handler);
treeModel.setNodeData(firstChildId, { hiddenValue: 'new hidden' });
expect(handler).toHaveBeenCalledWith(firstChildId);
expect(treeModel.getNode(firstChildId).getData('hiddenValue')).toEqual('new hidden');
});
it('should fire move event when a node moved', function() {
var handler = jest.fn();
var rootId = treeModel.rootNode.getId();
var firstChildId = treeModel.rootNode.getChildIds()[0];
var grandChildId = treeModel.getNode(firstChildId).getChildIds()[0];
treeModel.on('move', handler);
treeModel.move(grandChildId, rootId, -1);
expect(handler).toHaveBeenCalledWith(grandChildId, firstChildId, rootId, -1);
expect(treeModel.getNode(grandChildId).getParentId()).toEqual(rootId);
});
it('getParentIds should return an array of parent nodes of that node', function() {
var rootNodeId = treeModel.rootNode.getId();
var oneDepthId = treeModel.rootNode.getChildIds()[0];
var twoDepthId = treeModel.getNode(oneDepthId).getChildIds()[4];
var lastDepthId = treeModel.getNode(twoDepthId).getChildIds()[1];
expect(treeModel.getParentIds(lastDepthId)).toEqual([twoDepthId, oneDepthId, rootNodeId]);
});
});
@@ -0,0 +1,124 @@
var TreeNode = require('../src/js/treeNode');
describe('TreeNode', function() {
var node;
var parentId = 0;
var data = {
title: 'My node',
state: 'closed'
};
beforeEach(function() {
node = new TreeNode(data, parentId);
});
it('should set/get/toggle its state', function() {
expect(node.getState()).toEqual('closed');
node.setState('opened');
expect(node.getState()).toEqual('opened');
node.setState('asdf');
expect(node.getState()).toEqual('opened');
node.toggleState();
expect(node.getState()).toEqual('closed');
});
it('should add/remove/replace by childId(s)', function() {
node.addChildId(4);
expect(node.getChildIds()).toContain(4);
node.removeChildId(4);
expect(node.getChildIds()).not.toContain(4);
node.replaceChildIds([4, 5, 6]);
expect(node.getChildIds()).toEqual([4, 5, 6]);
});
it('should add/remove a node by data', function() {
expect(node.getAllData()).toEqual(
expect.objectContaining({
title: 'My node'
})
);
node.setData({ newData: 'data2' });
expect(node.getAllData()).toEqual(
expect.objectContaining({
title: 'My node',
newData: 'data2'
})
);
node.removeData('title', 'newData');
expect(node.getAllData().title).toBeUndefined();
expect(node.getAllData().newData).toBeUndefined();
});
it('"isLeaf" should return true if a node is leaf', function() {
expect(node.isLeaf()).toBe(true);
node.addChildId(4);
expect(node.isLeaf()).toBe(false);
});
it('"isRoot" should return true if a node is root', function() {
expect(node.isRoot()).toBe(false);
node.setParentId(null);
expect(node.isRoot()).toBe(true);
});
it('"constructor.setIdPrefix and _stampId" should set node id', function() {
TreeNode.setIdPrefix('new-tree-node-');
node = new TreeNode(data, parentId);
expect(node.getId()).toContain('new-tree-node-');
});
it('children property in "setData" should be ignored', function() {
node.setData({ children: [] });
expect(node.getData('children')).toBeUndefined();
});
it('"getChildIndex()" should return index number of child node', function() {
var index;
node.replaceChildIds(['1', '2', '3']);
index = node.getChildIndex('2');
expect(index).toEqual(1);
});
it('"insertChildId()" should add the node id in the position corresponding to the index ', function() {
var ids;
node.replaceChildIds(['node-a', 'node-b', 'node-c']);
node.insertChildId('node-d', 1);
ids = node.getChildIds();
expect(ids).toEqual(['node-a', 'node-d', 'node-b', 'node-c']);
});
it('"moveChildId()" should change the node id position', function() {
var ids;
node.replaceChildIds(['node-a', 'node-b', 'node-c', 'node-d']);
node.moveChildId('node-d', 1);
ids = node.getChildIds();
expect(ids).toEqual(['node-a', 'node-d', 'node-b', 'node-c']);
node.moveChildId('node-a', 3);
ids = node.getChildIds();
expect(ids).toEqual(['node-d', 'node-b', 'node-a', 'node-c']);
node.moveChildId('node-c', 0);
ids = node.getChildIds();
expect(ids).toEqual(['node-c', 'node-d', 'node-b', 'node-a']);
});
});
@@ -0,0 +1,25 @@
var util = require('../src/js/util.js');
describe('Util', function() {
it('"removeItemFromArray" should remove element if in array', function() {
var arr = [1, 3, 5];
util.removeItemFromArray(3, arr);
expect(arr).toEqual([1, 5]);
arr = [1, 2, 5];
util.removeItemFromArray(3, arr);
expect(arr).toEqual([1, 2, 5]);
});
it('"getFirstText" should return first text of element', function() {
var element = document.createElement('div');
var firstTextNode;
element.innerHTML = '<span></span>firstText';
firstTextNode = util.getFirstTextNode(element);
expect(firstTextNode.nodeValue).toBe('firstText');
});
});
@@ -0,0 +1,15 @@
/* eslint-env es6 */
module.exports = {
downloads: ({ name, version }) => {
const extensions = ['.css', '.js', '.min.css', '.min.js'];
const result = {};
extensions.forEach(ext => {
const filename = name + ext;
result[filename] = `https://uicdn.toast.com/${name}/v${version}/${filename}`;
});
return result;
}
};
@@ -0,0 +1,43 @@
{
"header": {
"logo": {
"src": "https://uicdn.toast.com/toastui/img/tui-component-bi-white.png"
},
"title": {
"text": "Tree",
"linkUrl": "https://github.com/nhn/tui.tree"
}
},
"footer": [
{
"title": "NHN",
"linkUrl": "https://github.com/nhn"
},
{
"title": "FE Development Lab",
"linkUrl": "https://ui.toast.com"
}
],
"main": {
"filePath": "README.md"
},
"api": {
"filePath": "src/js/**",
"permalink": true
},
"examples": {
"filePath": "examples",
"titles": {
"example01-basic": "1. Basic",
"example02-template": "2. Using template",
"example03-selectable": "3. Selectable feature",
"example04-draggable": "4. Draggable feature",
"example05-editable": "5. Editable feature",
"example06-contextmenu": "6. ContextMenu feature",
"example07-checkbox": "7. Checkbox feature",
"example08-ajax": "8. Ajax feature",
"example09-customizing": "9. Customizing - Make depth label"
}
},
"pathPrefix": "tui.tree"
}
@@ -0,0 +1,95 @@
/**
* Configs file for bundling
* @author NHN. FE Development Lab <dl_javascript@nhn.com>
*/
var path = require('path');
var pkg = require('./package.json');
var webpack = require('webpack');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var TerserPlugin = require('terser-webpack-plugin');
var OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
function getOptimization(isMinified) {
if (isMinified) {
return {
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: false,
extractComments: false
}),
new OptimizeCSSAssetsPlugin()
]
};
}
return {
minimize: false
};
}
module.exports = function(env, argv) {
var isProduction = argv.mode === 'production';
var isMinified = !!argv.minify;
var FILENAME = pkg.name + (isMinified ? '.min' : '');
var BANNER = [
'TOAST UI Tree',
'@version ' + pkg.version,
'@author ' + pkg.author,
'@license ' + pkg.license
].join('\n');
return {
mode: 'development',
entry: './src/js/index.js',
output: {
library: ['tui', 'Tree'],
libraryTarget: 'umd',
path: path.resolve(__dirname, 'dist'),
publicPath: 'dist/',
filename: FILENAME + '.js'
},
externals: {
'tui-context-menu': {
commonjs: 'tui-context-menu',
commonjs2: 'tui-context-menu',
amd: 'tui-context-menu',
root: ['tui', 'ContextMenu']
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(dist|node_modules)/,
loader: 'eslint-loader',
enforce: 'pre',
options: {
failOnError: isProduction
}
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /[.png|.gif]$/,
loader: 'url-loader'
}
]
},
plugins: [
new MiniCssExtractPlugin({ filename: FILENAME + '.css' }),
new webpack.BannerPlugin(BANNER)
],
optimization: getOptimization(isMinified),
devServer: {
historyApiFallback: false,
progress: true,
host: '0.0.0.0',
disableHostCheck: true
}
};
};