├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── stale.yml └── workflows │ └── test_on_pull_request.yml ├── .gitignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bower.json ├── docs ├── COMMIT_MESSAGE_CONVENTION.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md └── getting-started.md ├── examples ├── autoConfig.js ├── css │ ├── default.css │ └── tui-example-style.css ├── example01-basic.html ├── example02-toggle-autocompletion.html ├── example03-dynamic-search.html ├── img │ ├── btn_off.jpg │ ├── btn_on.jpg │ ├── off.jpg │ └── on.jpg └── mock.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── js │ ├── autoComplete.js │ └── manager │ ├── data.js │ ├── input.js │ └── result.js ├── test ├── .eslintrc.js ├── autoConfig.js ├── autocomplete.spec.js ├── datamanager.spec.js ├── fixtures │ └── expand.html ├── inputmanager.spec.js ├── mock.js ├── resultmanager.spec.js └── setup-globals.js ├── tuidoc.config.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['tui', 'prettier'], 3 | parserOptions: { 4 | sourceType: 'module' 5 | }, 6 | env: { 7 | browser: true, 8 | jest: true, 9 | jquery: true, 10 | commonjs: true 11 | }, 12 | globals: { 13 | tui: true, 14 | loadFixtures: true 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: Bug 6 | assignees: '' 7 | --- 8 | 9 | ## Describe the bug 10 | A clear and concise description of what the bug is. 11 | 12 | ## To Reproduce 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | ## Expected behavior 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## Screenshots 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | ## Desktop (please complete the following information): 26 | - OS: [e.g. iOS] 27 | - Browser: [e.g. chrome, safari] 28 | - Version: [e.g. 22] 29 | 30 | ## Smartphone (please complete the following information): 31 | - Device: [e.g. iPhone6] 32 | - OS: [e.g. iOS8.1] 33 | - Browser: [e.g. stock browser, safari] 34 | - Version: [e.g. 22] 35 | 36 | ## Additional context 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: Enhancement, Need Discussion 6 | assignees: '' 7 | --- 8 | 9 | 15 | 16 | ## Version 17 | Write the version that you are currently using. 18 | 19 | ## Development Environment 20 | Write the browser type, OS and so on. 21 | 22 | ## Current Behavior 23 | Write a description of the current operation. You can add sample code, 'CodePen' or 'jsfiddle' links. 24 | 25 | ```js 26 | // Write example code 27 | ``` 28 | 29 | ## Expected Behavior 30 | Write a description of the future action. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Create a question about the DatePicker 4 | title: '' 5 | labels: Question 6 | assignees: '' 7 | --- 8 | 9 | 18 | 19 | ## Summary 20 | A clear and concise description of what the question is. 21 | 22 | ## Screenshots 23 | If applicable, add screenshots to help explain your question. 24 | 25 | ## Version 26 | Write the version of the Editor you are currently using. 27 | 28 | ## Additional context 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | 2 | # Configuration for probot-stale - https://github.com/probot/stale 3 | 4 | # Number of days of inactivity before an Issue or Pull Request becomes stale 5 | daysUntilStale: 30 6 | 7 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 8 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 9 | daysUntilClose: 7 10 | 11 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 12 | exemptLabels: 13 | - Type: Feature 14 | - Type: Enhancement 15 | - Type: Bug 16 | - NHN Cloud 17 | 18 | # Label to use when marking as stale 19 | staleLabel: inactive 20 | 21 | # Comment to post when marking as stale. Set to `false` to disable 22 | markComment: > 23 | This issue has been automatically marked as inactive because there hasn’t been much going on it lately. 24 | It is going to be closed after 7 days. Thanks! 25 | # Comment to post when closing a stale Issue or Pull Request. 26 | closeComment: > -------------------------------------------------------------------------------- /.github/workflows/test_on_pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Jest Test 2 | on: pull_request 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Install modules 9 | run: npm install 10 | - name: Run tests 11 | run: npm run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Compiled files 11 | dist 12 | 13 | # Dependency directory 14 | node_modules 15 | 16 | # Bower Components 17 | bower_components 18 | 19 | #JSDOC 20 | doc 21 | 22 | # IDEA 23 | .idea 24 | *.iml 25 | 26 | # Window 27 | Thumbs.db 28 | Desktop.ini 29 | 30 | # MACgi 31 | .DS_Store 32 | 33 | # eclipse 34 | .project 35 | .metadata 36 | 37 | # Atom 38 | tags 39 | .ctags 40 | .tern-project 41 | 42 | # etc 43 | .agignore 44 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dl_javascript@nhn.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to TOAST UI 2 | 3 | First off, thanks for taking the time to contribute! 🎉 😘 ✨ 4 | 5 | The following is a set of guidelines for contributing to TOAST UI. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## Reporting Bugs 8 | Bugs are tracked as GitHub issues. Search the list and try reproduce on [demo][demo] before you create an issue. When you create an issue, please provide the following information by filling in the template. 9 | 10 | Explain the problem and include additional details to help maintainers reproduce the problem: 11 | 12 | * **Use a clear and descriptive title** for the issue to identify the problem. 13 | * **Describe the exact steps which reproduce the problem** in as many details as possible. Don't just say what you did, but explain how you did it. For example, if you moved the cursor to the end of a line, explain if you used a mouse or a keyboard. 14 | * **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets on the issue, use Markdown code blocks. 15 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 16 | * **Explain which behavior you expected to see instead and why.** 17 | * **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. 18 | 19 | ## Suggesting Enhancements 20 | In case you want to suggest for a TOAST UI product, please follow this guideline to help maintainers and the community understand your suggestion. 21 | Before creating suggestions, please check [issue list](../../../labels/feature%20request) if there's already a request. 22 | 23 | Create an issue and provide the following information: 24 | 25 | * **Use a clear and descriptive title** for the issue to identify the suggestion. 26 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 27 | * **Provide specific examples to demonstrate the steps.** Include copy/pasteable snippets which you use in those examples, as Markdown code blocks. 28 | * **Include screenshots and animated GIFs** which helps demonstrate the steps or point out the part of a TOAST UI product which the suggestion is related to. 29 | * **Explain why this enhancement would be useful** to most TOAST UI users. 30 | * **List some other products where this enhancement exists.** 31 | 32 | ## First Code Contribution 33 | 34 | Unsure where to begin contributing to TOAST UI? You can start by looking through these `document`, `good first issue` and `help wanted` issues: 35 | 36 | * **document issues**: issues which should be reviewed or improved. 37 | * **good first issues**: issues which should only require a few lines of code, and a test or two. 38 | * **help wanted issues**: issues which should be a bit more involved than beginner issues. 39 | 40 | ## Pull Requests 41 | 42 | ### Development WorkFlow 43 | - Set up your development environment 44 | - Make change from a right branch 45 | - Be sure the code passes `npm run test` 46 | - Make a pull request 47 | 48 | ### Development environment 49 | - Prepare your machine node and it's packages installed. 50 | - Checkout our repository 51 | - Install dependencies by `npm install && bower install` 52 | - Start webpack-dev-server by `npm run serve` 53 | 54 | ### Make changes 55 | #### Checkout a branch 56 | - **develop**: PR base branch. merge features, updates for next minor or major release 57 | - **master**: bug fix or document update for next patch release. develop branch will rebase every time master branch update. so keep code change to a minimum. 58 | - **production**: lastest release branch with distribution files. never make a PR on this 59 | - **gh-pages**: API docs, examples and demo 60 | 61 | #### Check Code Style 62 | Run `npm run eslint` and make sure all the tests pass. 63 | 64 | #### Test 65 | Run `npm run test` and verify all the tests pass. 66 | If you are adding new commands or features, they must include tests. 67 | If you are changing functionality, update the tests if you need to. 68 | 69 | #### Commit 70 | Follow our [commit message conventions](./docs/COMMIT_MESSAGE_CONVENTION.md). 71 | 72 | ### Yes! Pull request 73 | Make your pull request, then describe your changes. 74 | #### Title 75 | Follow other PR title format on below. 76 | ``` 77 | : Short Description (fix #111) 78 | : Short Description (fix #123, #111, #122) 79 | : Short Description (ref #111) 80 | ``` 81 | * capitalize first letter of Type 82 | * use present tense: 'change' not 'changed' or 'changes' 83 | 84 | #### Description 85 | If it has related to issues, add links to the issues(like `#123`) in the description. 86 | Fill in the [Pull Request Template](./docs/PULL_REQUEST_TEMPLATE.md) by check your case. 87 | 88 | ## Code of Conduct 89 | This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to dl_javascript@nhn.com. 90 | 91 | > This Guide is base on [atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), [CocoaPods](http://guides.cocoapods.org/contributing/contribute-to-cocoapods.html) and [ESLint](http://eslint.org/docs/developer-guide/contributing/pull-requests) 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2019 NHN Cloud Corp. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TOAST UI Component : Auto Complete 2 | > Component that displays suggested words when you enter characters and you can quickly complete the word. 3 | 4 | [![GitHub release](https://img.shields.io/github/release/nhn/tui.auto-complete.svg)](https://github.com/nhn/tui.auto-complete/releases/latest) 5 | [![npm](https://img.shields.io/npm/v/tui-auto-complete.svg)](https://www.npmjs.com/package/tui-auto-complete) 6 | [![GitHub license](https://img.shields.io/github/license/nhn/tui.auto-complete.svg)](https://github.com/nhn/tui.auto-complete/blob/production/LICENSE) 7 | [![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg)](https://github.com/nhn/tui.project-name/labels/help%20wanted) 8 | [![code with hearth by NHN Cloud](https://img.shields.io/badge/%3C%2F%3E%20with%20%E2%99%A5%20by-NHN_Cloud-ff1414.svg)](https://github.com/nhn) 9 | 10 | 11 | ## 🚩 Table of Contents 12 | 13 | - [Collect statistics on the use of open source](#collect-statistics-on-the-use-of-open-source) 14 | - [📙 Documents](#-documents) 15 | - [🎨 Features](#-features) 16 | - [🐾 Examples](#-examples) 17 | - [💾 Install](#-install) 18 | - [Via Package Manager](#via-package-manager) 19 | - [npm](#npm) 20 | - [bower](#bower) 21 | - [Via Contents Delivery Network (CDN)](#via-contents-delivery-network-cdn) 22 | - [Download Source Files](#download-source-files) 23 | - [🔨 Usage](#-usage) 24 | - [HTML](#html) 25 | - [JavaScript](#javascript) 26 | - [Using namespace in browser environment](#using-namespace-in-browser-environment) 27 | - [Using module format in node environment](#using-module-format-in-node-environment) 28 | - [🔩 Dependency](#-dependency) 29 | - [🌏 Browser Support](#-browser-support) 30 | - [🔧 Pull Request Steps](#-pull-request-steps) 31 | - [Setup](#setup) 32 | - [Develop](#develop) 33 | - [Running dev server](#running-dev-server) 34 | - [Running test](#running-test) 35 | - [Pull Request](#pull-request) 36 | - [💬 Contributing](#-contributing) 37 | - [🍞 TOAST UI Family](#-toast-ui-family) 38 | - [📜 License](#-license) 39 | 40 | 41 | ## Collect statistics on the use of open source 42 | 43 | TOAST UI AutoComplete applies Google Analytics (GA) to collect statistics on the use of open source, in order to identify how widely TOAST UI AutoComplete 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` options when creating the instance. 44 | 45 | ```js 46 | const options = { 47 | ... 48 | usageStatistics: false 49 | } 50 | const instance = new AutoComplete(options); 51 | ``` 52 | 53 | Or, include `tui-code-snippet.js` (**v1.5.0** or **later**) and then immediately write the options as follows: 54 | 55 | ```js 56 | tui.usageStatistics = false; 57 | ``` 58 | 59 | 60 | ## 📙 Documents 61 | * [Getting Started](https://github.com/nhn/tui.auto-complete/blob/production/docs/getting-started.md) 62 | * [Tutorials](https://github.com/nhn/tui.auto-complete/tree/production/docs) 63 | * [APIs](https://nhn.github.io/tui.auto-complete/latest) 64 | 65 | You can also see the older versions of API page on the [releases page](https://github.com/nhn/tui.auto-complete/releases). 66 | 67 | 68 | ## 🎨 Features 69 | * Displays suggested words using Ajax with the server API. 70 | * Makes autocomplation enable or disable. 71 | * Moves the list of suggested words using the keyboard. 72 | * Supports templates. 73 | 74 | 75 | ## 🐾 Examples 76 | * [Basic](https://nhn.github.io/tui.auto-complete/latest/tutorial-example01-basic) : Example using default options. 77 | * [Toggle autocompletion](https://nhn.github.io/tui.auto-complete/latest/tutorial-example02-toggle-autocompletion) : Example of enabling or disabling autocompletion. 78 | 79 | More examples can be found on the left sidebar of each example page, and have fun with it. 80 | 81 | 82 | ## 💾 Install 83 | 84 | TOAST UI products can be used by using the package manager or downloading the source directly. 85 | However, we highly recommend using the package manager. 86 | 87 | ### Via Package Manager 88 | 89 | TOAST UI products are registered in two package managers, [npm](https://www.npmjs.com/) and [bower](https://bower.io/). 90 | You can conveniently install it using the commands provided by each package manager. 91 | When using npm, be sure to use it in the environment [Node.js](https://nodejs.org/ko/) is installed. 92 | 93 | #### npm 94 | 95 | ``` sh 96 | $ npm install --save tui-auto-complete # Latest version 97 | $ npm install --save tui-auto-complete@ # Specific version 98 | ``` 99 | 100 | #### bower 101 | 102 | ``` sh 103 | $ bower install tui-auto-complete # Latest version 104 | $ bower install tui-auto-complete# # Specific version 105 | ``` 106 | 107 | ### Via Contents Delivery Network (CDN) 108 | TOAST UI products are available over the CDN powered by [TOAST Cloud](https://www.toast.com). 109 | 110 | You can use the CDN as below. 111 | 112 | ```html 113 | 114 | ``` 115 | 116 | If you want to use a specific version, use the tag name instead of `latest` in the url's path. 117 | 118 | The CDN directory has the following structure. 119 | 120 | ``` 121 | tui-auto-complete/ 122 | ├─ latest/ 123 | │ ├─ tui-auto-complete.js 124 | │ └─ tui-auto-complete.min.js 125 | ├─ v2.1.0/ 126 | │ ├─ ... 127 | ``` 128 | 129 | ### Download Source Files 130 | * [Download bundle files](https://github.com/nhn/tui.auto-complete/tree/production/dist) 131 | * [Download all sources for each version](https://github.com/nhn/tui.auto-complete/releases) 132 | 133 | 134 | ## 🔨 Usage 135 | 136 | ### HTML 137 | 138 | Add the container element to create the component as an option. 139 | See [here](https://nhn.github.io/tui.auto-complete/latest/tutorial-example01-basic) for information about the added element. 140 | 141 | 142 | ### JavaScript 143 | 144 | This component can be used by creating an instance with the constructor function. 145 | To get the constructor function, you should import the module using one of the following ways depending on your environment. 146 | 147 | #### Using namespace in browser environment 148 | ``` javascript 149 | const AutoComplete = tui.AutoComplete; 150 | ``` 151 | 152 | #### Using module format in node environment 153 | ``` javascript 154 | const AutoComplete = require('tui-auto-complete'); /* CommonJS */ 155 | ``` 156 | 157 | ``` javascript 158 | import {AutoComplete} from 'tui-auto-complete'; /* ES6 */ 159 | ``` 160 | 161 | You can create an instance with [options](https://github.com/nhn/tui.auto-complete/blob/production/examples/autoConfig.js) and call various APIs after creating an instance. 162 | 163 | ``` javascript 164 | const instance = new AutoComplete({ ... }); 165 | 166 | instance.getValue(); 167 | ``` 168 | 169 | For more information about the API, please see [here](http://nhn.github.io/tui.auto-complete/latest/AutoComplete). 170 | 171 | 172 | ## 🔩 Dependency 173 | * [tui-code-snippet](https://github.com/nhn/tui.code-snippet) >=1.5.0 174 | * [js-cookie](https://github.com/js-cookie/js-cookie) >=1.2.0 (If use 1.3.0 or more, You need to include the [JSON-js polyfill](https://github.com/douglascrockford/JSON-js)) 175 | * [jquery](https://github.com/jquery/jquery) >=1.11.0 176 | 177 | 178 | ## 🌏 Browser Support 179 | | Chrome Chrome | IE Internet Explorer | Edge Edge | Safari Safari | Firefox Firefox | 180 | | :---------: | :---------: | :---------: | :---------: | :---------: | 181 | | Yes | 8+ | Yes | Yes | Yes | 182 | 183 | 184 | ## 🔧 Pull Request Steps 185 | 186 | TOAST UI products are open source, so you can create a pull request(PR) after you fix issues. 187 | Run npm scripts and develop yourself with the following process. 188 | 189 | ### Setup 190 | 191 | Fork `develop` branch into your personal repository. 192 | Clone it to local computer. Install node modules. 193 | Before starting development, you should check if there are any errors. 194 | 195 | ``` sh 196 | $ git clone https://github.com/{your-personal-repo}/tui.auto-complete.git 197 | $ cd tui.auto-complete 198 | $ npm install 199 | $ npm run test 200 | ``` 201 | 202 | ### Develop 203 | 204 | Let's start development! 205 | You can see your code reflected as soon as you save the code by running a server. 206 | Don't miss adding test cases and then make green rights. 207 | 208 | #### Running dev server 209 | 210 | ``` sh 211 | $ npm run serve 212 | $ npm run serve:ie8 # Run on Internet Explorer 8 213 | ``` 214 | 215 | #### Running test 216 | 217 | ``` sh 218 | $ npm run test 219 | ``` 220 | 221 | ### Pull Request 222 | 223 | Before uploading your PR, run test one last time to check if there are any errors. 224 | If it has no errors, commit and then push it! 225 | 226 | For more information on PR's steps, please see links in the Contributing section. 227 | 228 | 229 | ## 💬 Contributing 230 | * [Code of Conduct](https://github.com/nhn/tui.auto-complete/blob/production/CODE_OF_CONDUCT.md) 231 | * [Contributing guideline](https://github.com/nhn/tui.auto-complete/blob/production/CONTRIBUTING.md) 232 | * [Issue guideline](https://github.com/nhn/tui.auto-complete/blob/production/docs/ISSUE_TEMPLATE.md) 233 | * [Commit convention](https://github.com/nhn/tui.auto-complete/blob/production/docs/COMMIT_MESSAGE_CONVENTION.md) 234 | 235 | 236 | ## 🍞 TOAST UI Family 237 | 238 | * [TOAST UI Editor](https://github.com/nhn/tui.editor) 239 | * [TOAST UI Calendar](https://github.com/nhn/tui.calendar) 240 | * [TOAST UI Chart](https://github.com/nhn/tui.chart) 241 | * [TOAST UI Image-Editor](https://github.com/nhn/tui.image-editor) 242 | * [TOAST UI Grid](https://github.com/nhn/tui.grid) 243 | * [TOAST UI Components](https://github.com/nhn) 244 | 245 | 246 | ## 📜 License 247 | 248 | This software is licensed under the [MIT](https://github.com/nhn/tui.auto-complete/blob/production/LICENSE) © [NHN Cloud](https://github.com/nhn). 249 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tui-auto-complete", 3 | "authors": [ 4 | "NHN Cloud. FE Dev Lab " 5 | ], 6 | "main": [ 7 | "dist/tui-auto-complete.js" 8 | ], 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "src", 16 | "examples", 17 | "jsdoc.conf.json", 18 | "jest.config.js", 19 | "package.json", 20 | "webpack.config.js" 21 | ], 22 | "dependencies": { 23 | "jquery" : "^1.11.0", 24 | "js-cookie": "^1.2.0", 25 | "tui-code-snippet" : "^1.3.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/COMMIT_MESSAGE_CONVENTION.md: -------------------------------------------------------------------------------- 1 | # Commit Message Convention 2 | 3 | ## Commit Message Format 4 | 5 | ``` 6 | : Short description (fix #1234) 7 | 8 | Longer description here if necessary 9 | 10 | BREAKING CHANGE: only contain breaking change 11 | ``` 12 | * Any line of the commit message cannot be longer 100 characters! 13 | 14 | ## Revert 15 | ``` 16 | revert: commit 17 | 18 | This reverts commit 19 | More description if needed 20 | ``` 21 | 22 | ## Type 23 | Must be one of the following: 24 | 25 | * **feat**: A new feature 26 | * **fix**: A bug fix 27 | * **docs**: Documentation only changes 28 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 29 | * **refactor**: A code change that neither fixes a bug nor adds a feature 30 | * **perf**: A code change that improves performance 31 | * **test**: Adding missing or correcting existing tests 32 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation generation 33 | 34 | ## Subject 35 | * use the imperative, __present__ tense: "change" not "changed" nor "changes" 36 | * don't capitalize the first letter 37 | * no dot (.) at the end 38 | * reference GitHub issues at the end. If the commit doesn’t completely fix the issue, then use `(refs #1234)` instead of `(fixes #1234)`. 39 | 40 | ## Body 41 | 42 | * use the imperative, __present__ tense: "change" not "changed" nor "changes". 43 | * the motivation for the change and contrast this with previous behavior. 44 | 45 | ## BREAKING CHANGE 46 | * This commit contains breaking change(s). 47 | * start with the word BREAKING CHANGE: with a space or two newlines. The rest of the commit message is then used for this. 48 | 49 | This convention is based on [AngularJS](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) and [ESLint](https://eslint.org/docs/developer-guide/contributing/pull-requests#step2) 50 | -------------------------------------------------------------------------------- /docs/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 27 | 28 | ### Please check if the PR fulfills these requirements 29 | - [ ] It's submitted to right branch according to our branching model 30 | - [ ] It's right issue type on title 31 | - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number) 32 | - [ ] The commit message follows our guidelines 33 | - [ ] Tests for the changes have been added (for bug fixes/features) 34 | - [ ] Docs have been added/updated (for bug fixes/features) 35 | - [ ] It does not introduce a breaking change or has description for the breaking change 36 | 37 | ### Description 38 | 39 | 40 | 41 | --- 42 | Thank you for your contribution to TOAST UI product. 🎉 😘 ✨ 43 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Tutorials 2 | 3 | - [Getting Started](getting-started.md) 4 | 5 | ## Documents 6 | 7 | - [Code of Conduct](../CODE_OF_CONDUCT.md) 8 | - [Contributing Guide](../CONTRIBUTING.md) 9 | - [Commit Message Convention](COMMIT_MESSAGE_CONVENTION.md) 10 | - [API & Examples](https://nhn.github.io/tui.auto-complete/latest) 11 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Dependencies 2 | * [tui-code-snippet](https://github.com/nhn/tui.code-snippet) >=1.3.0 3 | * [js-cookie](https://github.com/js-cookie/js-cookie) >=1.2.0 (If use 1.3.0 or more, You need to include the [JSON-js polyfill](https://github.com/douglascrockford/JSON-js)) 4 | * [jquery](https://github.com/jquery/jquery) >=1.11.0 5 | 6 | ## Load 7 | 8 | ### HTML 9 | 10 | ```html 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 |
22 | 25 | 26 |
27 |
28 |
29 | ``` 30 | 31 | ### Scripts 32 | ```js 33 | 34 | 35 | 36 | 37 | 45 | ``` 46 | 47 |
48 | 49 | ## Configuration 50 | 51 | ### Required Options 52 | 53 | * `resultListElement`: Element for displaying the search result 54 | * `searchBoxElement`: Input element for searching 55 | * `orgQueryElement`: Hidden input element for request (use jquery selector) 56 | * `subQuerySet`: Set of keys of subqueries
57 | ```js 58 | /* Example */ 59 | subQuerySet: [ 60 | ['key1', 'key2', 'key3'], 61 | ['dep1', 'dep2', 'dep3'], 62 | ['ch1', 'ch2', 'ch3'], 63 | ['cid'] 64 | ] 65 | ``` 66 | * `template` : Markup templates for each collection, the expression is `@key@`
67 | ```js 68 | /* Example */ 69 | template: { 70 | department: { 71 | element: '
  • ' + 72 | 'Shop the ' + 73 | '@subject@ ' + 74 | 'Store' + 75 | '
  • ', 76 | attr: ['subject'] 77 | }, 78 | srch: { 79 | element: '
  • @subject@
  • ', 80 | attr: ['subject'] 81 | }, 82 | srch_in_department: { 83 | element: '
  • ' + 84 | '@subject@ ' + 85 | 'in ' + 86 | '@department@' + 87 | '
  • ', 88 | attr: ['subject', 'department'] 89 | }, 90 | title: { 91 | element: '
  • @title@
  • ', 92 | attr: ['title'] 93 | }, 94 | defaults: { 95 | element: '
  • @subject@
  • ', 96 | attr: ['subject'] 97 | } 98 | }, 99 | ``` 100 | * `listConfig` : Configurations for each collection. 101 | ```js 102 | /* Example */ 103 | listConfig: { 104 | '0': { 105 | 'template': 'department', 106 | 'subQuerySet' : 0, 107 | 'action': 0 108 | }, 109 | '1': { 110 | 'template': 'srch_in_department', 111 | 'subQuerySet' : 1, 112 | 'action': 0 113 | }, 114 | '2': { 115 | 'template': 'srch_in_department', 116 | 'subQuerySet' : 2, 117 | 'action': 1, 118 | 'staticParams': 0 119 | }, 120 | '3': { 121 | 'template': 'department', 122 | 'subQuerySet' : 0, 123 | 'action': 1, 124 | 'staticParams': 1 125 | } 126 | }, 127 | ``` 128 | * `actions` : Form actions for each colection 129 | ```js 130 | /* Example */ 131 | actions: [ 132 | "http://0.0.0.0:3000/searc1.aspx", 133 | "http://0.0.0.0:3000/search2.aspx" 134 | ], 135 | ``` 136 | * `formElement`: From element wrapping searchbox 137 | * `searchUrl`: Search URL 138 | 139 | ## Caution 140 | * This component uses JSONP 141 | -------------------------------------------------------------------------------- /examples/autoConfig.js: -------------------------------------------------------------------------------- 1 | var Default = { // 설정용가능한 항목을 모두 설정한 config 2 | // 자동완성 결과를 보여주는 엘리먼트 3 | resultListElement: '._resultBox', 4 | 5 | // 검색어를 입력하는 input 엘리먼트 6 | searchBoxElement: '#ac_input1', 7 | 8 | // 입력한 검색어를 넘기기 위한 hidden element 9 | orgQueryElement: '#org_query', 10 | 11 | // on,off 버튼 엘리먼트 12 | toggleBtnElement: $('#onoffBtn'), 13 | 14 | // on,off 상태를 알리는 엘리먼트 15 | onoffTextElement: $('.baseBox .bottom'), 16 | 17 | // on, off상태일때 변경 이미지 경로 18 | toggleImg: { 19 | on: 'img/btn_on.jpg', 20 | off: 'img/btn_off.jpg' 21 | }, 22 | 23 | // 컬렉션아이템별 보여질 리스트 겟수 24 | viewCount: 3, 25 | 26 | // 서브쿼리로 넘어갈 키 값들의 배열(컬렉션 명 별로 지정 할 수 있다, 따로 지정하지 않으면 defaults가 적용된다.) 27 | subQuerySet: [ 28 | ['cid', 'cid2', 'cid3'], 29 | ['cid', 'dep2', 'dep3'], 30 | ['ch1', 'ch2', 'ch3'], 31 | ['cid'] 32 | ], 33 | 34 | // 컬렉션 인덱스별 자동완성 리스트의 config를 설정한다. 35 | listConfig: { 36 | '0': { 37 | template: 'department', 38 | subQuerySet: 0, 39 | action: 0 40 | }, 41 | '1': { 42 | template: 'srch_in_department', 43 | subQuerySet: 1, 44 | action: 0 45 | }, 46 | '2': { 47 | template: 'srch', 48 | subQuerySet: 2, 49 | action: 1, 50 | staticParams: 0 51 | }, 52 | '3': { 53 | template: 'department', 54 | subQuerySet: 0, 55 | action: 1, 56 | staticParams: 1 57 | } 58 | }, 59 | 60 | // 컬렉션 타입 별로 표시될 마크업, 데이터가 들어갈 부분은 @key@ 형식으로 사용한다.(지정하지 않으면, defaults가 적용된다.) 61 | // 형식은 수정 가능하지만, keyword-field는 키워드가 들어가는 부분에 필수로 들어가야함. 단 title에는 들어가면 안됨. 62 | template: { 63 | department: { 64 | element: '
  • ' + 65 | 'Shop the ' + 66 | '@subject@ ' + 67 | 'Store' + 68 | '
  • ', 69 | attr: ['subject'] 70 | }, 71 | srch: { 72 | element: '
  • @subject@
  • ', 73 | attr: ['subject'] 74 | }, 75 | 'srch_in_department': { 76 | element: '
  • ' + 77 | '@subject@ ' + 78 | 'in ' + 79 | '@department@' + 80 | '
  • ', 81 | attr: ['subject', 'department'] 82 | }, 83 | title: { 84 | element: '
  • @title@
  • ', 85 | attr: ['title'] 86 | }, 87 | defaults: { 88 | element: '
  • @subject@
  • ', 89 | attr: ['subject'] 90 | } 91 | }, 92 | 93 | // 컬렉션 타입별 form action 을 지정한다. (지정하지 않으면 defaults가 적용된다) 94 | 'actions': [ 95 | '//www.fashiongo.net/catalog.aspx', 96 | '//www.fashiongo.net/search2.aspx' 97 | ], 98 | 99 | // 컬렉션 타입별 추가 스테틱한 옵션들을 설정 100 | 'staticParams': [ 101 | 'qt=ProductName', 102 | 'at=TEST,bt=ACT' 103 | ], 104 | 105 | // 타이틀을 보일지 여부 106 | 'useTitle': true, 107 | 108 | // 검색창을 감싸고 있는 form앨리먼트 109 | 'formElement': '#ac_form1', 110 | 111 | // 자동완성을 끄고 켤때 사용되는 쿠키명 112 | 'cookieName': 'usecookie', 113 | 114 | // 선택된 엘리먼트에 추가되는 클래스명 115 | 'mouseOverClass': 'emp', 116 | 117 | // 자동완성API url 118 | 'searchUrl': 'mock.js', 119 | 120 | // 자동완성API request 설정 121 | 'searchApi': { 122 | 'st': 1111, 123 | 'r_lt': 1111, 124 | 'r_enc': 'UTF-8', 125 | 'q_enc': 'UTF-8', 126 | 'r_format': 'json' 127 | } 128 | }; 129 | 130 | var Plane = { // 필수 항목만 나열한 config 131 | // 자동완성 결과를 보여주는 엘리먼트 132 | resultListElement: '._resultBox', 133 | 134 | // 검색어를 입력하는 input 엘리먼트 135 | searchBoxElement: '#ac_input1', 136 | 137 | // 입력한 검색어를 넘기기 위한 hidden element 138 | orgQueryElement: '#org_query', 139 | 140 | // 컬렉션아이템별 보여질 리스트 겟수 141 | viewCount: 2, 142 | 143 | // 서브쿼리로 넘어갈 키 값들의 배열(컬렉션 명 별로 지정 할 수 있다, 따로 지정하지 않으면 defaults가 적용된다.) 144 | subQuerySet: [ 145 | ['cid', 'cid2', 'cid3'], 146 | ['cid', 'dep2', 'dep3'], 147 | ['ch1', 'ch2', 'ch3'], 148 | ['cid'] 149 | ], 150 | 151 | // 컬렉션 인덱스별 자동완성 리스트의 config를 설정한다. 152 | listConfig: { 153 | '0': { 154 | 'template': 'department', 155 | 'subQuerySet': 0, 156 | 'action': 0 157 | }, 158 | '1': { 159 | 'template': 'srch_in_department', 160 | 'subQuerySet': 1, 161 | 'action': 0 162 | }, 163 | '2': { 164 | 'template': 'srch', 165 | 'subQuerySet': 2, 166 | 'action': 1, 167 | 'staticParams': 0 168 | }, 169 | '3': { 170 | 'template': 'department', 171 | 'subQuerySet': 0, 172 | 'action': 1, 173 | 'staticParams': 1 174 | } 175 | }, 176 | 177 | // 컬렉션 타입 별로 표시될 마크업, 데이터가 들어갈 부분은 @key@ 형식으로 사용한다.(지정하지 않으면, defaults가 적용된다.) 178 | // 형식은 수정 가능하지만, keyword-field는 키워드가 들어가는 부분에 필수로 들어가야함. 단 title에는 들어가면 안됨. 179 | template: { 180 | department: { 181 | element: '
  • ' + 182 | 'Shop the ' + 183 | '@subject@ ' + 184 | 'Store' + 185 | '
  • ', 186 | attr: ['subject'] 187 | }, 188 | srch: { 189 | element: '
  • @subject@
  • ', 190 | attr: ['subject'] 191 | }, 192 | 'srch_in_department': { 193 | element: '
  • ' + 194 | '@subject@ ' + 195 | 'in ' + 196 | '@department@' + 197 | '
  • ', 198 | attr: ['subject', 'department'] 199 | }, 200 | title: { 201 | element: '
  • @title@
  • ', 202 | attr: ['title'] 203 | }, 204 | defaults: { 205 | element: '
  • @subject@
  • ', 206 | attr: ['subject'] 207 | } 208 | }, 209 | 210 | // 컬렉션 타입별 form action 을 지정한다. (지정하지 않으면 defaults가 적용된다) 211 | actions: [ 212 | '//www.fashiongo.net/catalog.aspx', 213 | '//www.fashiongo.net/search2.aspx' 214 | ], 215 | 216 | // 컬렉션 타입별 추가 스테틱한 옵션들을 설정 217 | staticParams: [ 218 | 'qt=ProductName', 219 | 'at=TEST,bt=ACT' 220 | ], 221 | 222 | // 검색창을 감싸고 있는 form앨리먼트 223 | formElement: '#ac_form1', 224 | 225 | // 자동완성API url 226 | searchUrl: 'mock.js', 227 | 228 | // 자동완성API request 설정 229 | searchApi: { 230 | st: 1111, 231 | 'r_lt': 1111, 232 | 'r_enc': 'UTF-8', 233 | 'q_enc': 'UTF-8', 234 | 'r_format': 'json' 235 | } 236 | }; 237 | -------------------------------------------------------------------------------- /examples/css/default.css: -------------------------------------------------------------------------------- 1 | @charset "utf-8"; 2 | 3 | /* inputBox */ 4 | .inputBox {position:absolute;top:1px;left:0;width:200px;padding:1px 1px 1px 1px;} 5 | .inputBox .inputBorder{line-height:18px;font-weight:bold;font-size:0.87em;color:#000;*ime-mode:active;outline:none;width:258px;_width /**/:272px;height:26px;_height /**/:40px;margin-right:5px;padding-left:3px;border:4px solid #FF5E00;background-color:#fff} 6 | .inputBox .onoff{position:absolute;top:8px;left:240px;} 7 | 8 | /* suggestbox */ 9 | .suggestBox {position:absolute;top:33px;left:1px;z-index:9999;width:100%;padding:0 0 0 0;} 10 | .suggestBox {font-size:12px;line-height:27px;font-family:'굴림',gulim,sans-serif;background-color:#fff} 11 | .suggestBox .list {padding:1px 0 1px;border-right:1px solid #a5a6ac;border-left:1px solid #a5a6ac;color:#000} 12 | 13 | .suggestBox .baseBox {/* position:relative; *//* overflow:hidden; */border:1px solid #e5333d;/* border-top:none; */background-color:#fff;color: #e5333d;zoom:1} 14 | .suggestBox .bottom {overflow:hidden;text-align:right;clear:both;width:100%;padding:6px 0;border:1px solid #e1e1e1;border-width:1px 0 0;background:#f5f5f5;color:#5d5d5d;font:11px/1.3em Dotum,Helvetica,sans-serif;} 15 | 16 | .suggestBox .baseBox li {overflow:hidden;height:19px;padding:2px 9px;line-height:22px;text-align:left;cursor:pointer} 17 | .suggestBox .baseBox li strong {color:#e5333d} 18 | .suggestBox .baseBox li.emp {background-color:#e7e7e7} 19 | .suggestBox .baseBox li.lastitem { border-bottom: 1px solid goldenrod; } 20 | .suggestBox .baseBox li.title {color:#0000ff;} 21 | 22 | .sampleWarp, .link {height:500px; } 23 | .sampleWarp {width:500px; float:left; } 24 | .sampleWarp .formWrap { margin-left:50px; position:relative; margin-top:50px;} 25 | .sampleWarp form {margin:0 auto; display: block;} 26 | .link { width:400px; float:left; margin-left: 15px; text-align:center; } 27 | .link ul {margin:60px auto;} 28 | .link li {font-size:12px; text-decoration: underline;display:block;} 29 | .link h4 {display:block; margin:60px auto;} 30 | -------------------------------------------------------------------------------- /examples/css/tui-example-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .code-description { 7 | padding: 22px 52px; 8 | background-color: rgba(81, 92, 230, 0.1); 9 | line-height: 1.4em; 10 | } 11 | 12 | .code-description, 13 | .code-description a { 14 | font-family: Arial; 15 | font-size: 14px; 16 | color: #515ce6; 17 | } 18 | 19 | .code-html { 20 | padding: 20px 52px; 21 | } 22 | -------------------------------------------------------------------------------- /examples/example01-basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 1. Basic 10 | 11 | 12 |
    13 | See the real services. FashionGo 14 |
    15 |
    16 |
    17 | 18 |
    19 | 20 |
    21 | 22 | 23 |
    24 | 25 | 26 |
    27 |
    28 | 31 | 32 |
    33 |
    34 |
    35 |
    36 |
    37 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/example02-toggle-autocompletion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Autocomplete - On/Off the auto-completion 10 | 11 | 12 |
    13 | See the real services. FashionGo 14 |
    15 |
    16 |
    17 | 18 |
    19 | 20 |
    21 | 22 | 23 | 24 |
    25 | 26 | 27 |
    28 |
    29 | 32 | 33 |
    34 |
    35 | 36 |
    37 |
    38 |
    39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/example03-dynamic-search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 3. Dynamic search 10 | 13 | 14 | 15 |
    16 | 23 |
    24 | 25 |
    26 | 27 |
    28 | 29 | 30 | 31 |
    32 | 33 | 34 |
    35 |
    36 | 39 | 40 |
    41 |
    42 |
    43 |
    44 |
    45 | 46 | 47 | 48 | 49 | 50 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /examples/img/btn_off.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhn/tui.auto-complete/420a9571c62b2a8c7cfd7b59c34173bb924c780c/examples/img/btn_off.jpg -------------------------------------------------------------------------------- /examples/img/btn_on.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhn/tui.auto-complete/420a9571c62b2a8c7cfd7b59c34173bb924c780c/examples/img/btn_on.jpg -------------------------------------------------------------------------------- /examples/img/off.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhn/tui.auto-complete/420a9571c62b2a8c7cfd7b59c34173bb924c780c/examples/img/off.jpg -------------------------------------------------------------------------------- /examples/img/on.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nhn/tui.auto-complete/420a9571c62b2a8c7cfd7b59c34173bb924c780c/examples/img/on.jpg -------------------------------------------------------------------------------- /examples/mock.js: -------------------------------------------------------------------------------- 1 | dataCallback({ 2 | 'collections': [ 3 | { 4 | 'index': 0, 5 | 'items': [ 6 | ['Shrugs & Cardigans', '27', '30'], 7 | ['Sweaters', '27', '30'], 8 | ['Sets', '27', '30'], 9 | ['Shirts & Blouse', '27', '30'], 10 | ['Shorts', '27', '30'], 11 | ['Shoes', '27', '30'], 12 | ['Semiformal', '27', '30'], 13 | ['Seamless', '27', '30'], 14 | ['Special Occasion', '27', '30'], 15 | ['Socks\\/Footwear', '27', '30'] 16 | ], 17 | 'title': 'Category', 18 | 'type': 'department' 19 | }, 20 | { 21 | 'index': 1, 22 | 'items': [ 23 | ['Solid Dress', 'Casual', 'c3,44'], 24 | ['Stretch Bracelet', 'Bracelets', 'c3=153'], 25 | ['Striped Top', 'Fashion Tops', 'c3=254'], 26 | ['stripe cardigan', 'Shrugs & Cardigans', 'c3=27'], 27 | ['SOLID TOP', 'Dressy Tops', 'c3=18'], 28 | ['Sweater top', 'Fashion Tops', 'c3=254'], 29 | ['SOLID TOP', 'Casual', 'c3=210'], 30 | ['SLEEVELESS TOP', 'Fashion Tops', 'c3=254'], 31 | ['SOLID TOP', 'Fashion Tops', 'c3=254'], 32 | ['STRIPED TOP', 'Casual', 'c3=210'] 33 | ], 34 | 'title': 'Product in Category', 35 | 'type': 'srch_in_department' 36 | }, 37 | { 38 | 'index': 2, 39 | 'items': [ 40 | ['Skinny jeans'], 41 | ['shirt dress'], 42 | ['SWEATSHIRT'], 43 | ['striped dress'], 44 | ['sweat pants'], 45 | ['stripe CARDIGAN'], 46 | ['SunGlasses'], 47 | ['short sleeve'], 48 | ['Shopwtd'], 49 | ['shorts'] 50 | ], 51 | 'title': 'Product', 52 | 'type': 'srch' 53 | } 54 | ], 55 | 'query': ['s'], 56 | 'ver': '1.0' 57 | } 58 | ); 59 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable strict */ 2 | module.exports = { 3 | moduleFileExtensions: ['js'], 4 | testEnvironment: 'jsdom', 5 | testMatch: ['/**/*.spec.js'], 6 | transformIgnorePatterns: ['/node_modules/'], 7 | clearMocks: true, 8 | setupFilesAfterEnv: ['./test/setup-globals.js'] 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tui-auto-complete", 3 | "version": "2.1.6", 4 | "description": "TOAST UI Component: AutoComplete", 5 | "main": "dist/tui-auto-complete", 6 | "files": [ 7 | "dist" 8 | ], 9 | "scripts": { 10 | "test": "jest", 11 | "bundle": "webpack && webpack -p", 12 | "serve": "webpack-dev-server --inline --hot -d", 13 | "serve:ie8": "webpack-dev-server -d", 14 | "doc:serve": "tuidoc --serv", 15 | "doc": "tuidoc", 16 | "note": "tuie --tag=$(git describe --tags)" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/nhn/tui.auto-complete.git" 21 | }, 22 | "keywords": [ 23 | "nhn", 24 | "tui", 25 | "ui component", 26 | "auto-complete", 27 | "auto complete", 28 | "nhn cloud" 29 | ], 30 | "author": "NHN Cloud FE Development Lab ", 31 | "license": "MIT", 32 | "devDependencies": { 33 | "eslint": "^5.12.0", 34 | "eslint-config-prettier": "^6.2.0", 35 | "eslint-config-tui": "^2.2.0", 36 | "eslint-loader": "^3.0.0", 37 | "eslint-plugin-jest": "^24.3.6", 38 | "jest": "^27.0.4", 39 | "prettier": "^1.18.2", 40 | "tui-release-notes": "github:nhn/toast-ui.release-notes#v1.0.0", 41 | "webpack": "^4.39.3", 42 | "webpack-cli": "^3.3.7", 43 | "webpack-dev-server": "^3.8.0" 44 | }, 45 | "dependencies": { 46 | "jquery": "^1.11.0", 47 | "js-cookie": "^1.2.0", 48 | "tui-code-snippet": "^1.5.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/js/autoComplete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Auto complete's Core element. All of auto complete objects belong with this object. 3 | */ 4 | var snippet = require('tui-code-snippet'); 5 | var Cookies = require('js-cookie'); 6 | var $ = require('jquery'); 7 | var DataManager = require('./manager/data'), 8 | InputManager = require('./manager/input'), 9 | ResultManager = require('./manager/result'); 10 | 11 | var DEFAULT_COOKIE_NAME = '_atcp_use_cookie'; 12 | 13 | var requiredOptions = [ 14 | 'resultListElement', 15 | 'searchBoxElement', 16 | 'orgQueryElement', 17 | 'formElement', 18 | 'subQuerySet', 19 | 'template', 20 | 'listConfig', 21 | 'actions', 22 | 'searchUrl' 23 | ], 24 | rIsElementOption = /element/i; 25 | 26 | /** 27 | * @constructor 28 | * @param {Object} options 29 | * @param {Boolean} [options.usageStatistics=true] - Let us know the hostname. If you don't want to send the hostname, please set to false. 30 | * @example CommonJS 31 | * const AutoComplete = require('tui-auto-complete'); 32 | * const autoComplete = new AutoComplete({'config': 'Default'}); 33 | * @example Global Namespace 34 | * const autoComplete = new tui.AutoComplete({"config" : "Default"}); 35 | * @example Arguments of AutoComplete Constructor 36 | * SAMPLE FILE: [AutoConfig.json]{@link https://github.com/nhn/tui.auto-complete/blob/master/src/js/autoComplete.js} 37 | */ 38 | var AutoComplete = snippet.defineClass( 39 | /** @lends AutoComplete.prototype */ { 40 | init: function(options) { 41 | options = snippet.extend( 42 | { 43 | usageStatistics: true 44 | }, 45 | options 46 | ); 47 | 48 | this.options = {}; 49 | this.isUse = true; 50 | this.queries = null; 51 | this.isIdle = true; 52 | 53 | this._checkValidation(options); 54 | this._setOptions(options); 55 | 56 | this.dataManager = new DataManager(this, this.options); 57 | this.inputManager = new InputManager(this, this.options); 58 | this.resultManager = new ResultManager(this, this.options); 59 | 60 | this.setToggleBtnImg(this.isUse); 61 | this.setCookieValue(this.isUse); 62 | 63 | if (options.usageStatistics) { 64 | snippet.sendHostname('auto-complete', 'UA-129987462-1'); 65 | } 66 | }, 67 | 68 | /** 69 | * Direction value for key 70 | * @static 71 | * @private 72 | */ 73 | flowMap: { 74 | NEXT: 'next', 75 | PREV: 'prev', 76 | FIRST: 'first', 77 | LAST: 'last' 78 | }, 79 | 80 | /** 81 | * Interval for check update input 82 | * @type {number} 83 | * @default 300 84 | */ 85 | watchInterval: 300, 86 | 87 | /** 88 | * Check required fields and validate fields. 89 | * @param {Object} options component configurations 90 | * @private 91 | */ 92 | _checkValidation: function(options) { 93 | var isExisty = snippet.isExisty, 94 | config = options.config; 95 | 96 | if (!isExisty(config)) { 97 | throw new Error('No configuration #' + config); 98 | } 99 | 100 | snippet.forEach(requiredOptions, function(name) { 101 | if (!isExisty(config[name])) { 102 | throw new Error(name + 'does not not exist.'); 103 | } 104 | }); 105 | }, 106 | 107 | /** 108 | * Set component options 109 | * @param {Object} options component configurations 110 | * @private 111 | */ 112 | _setOptions: function(options) { 113 | var config = options.config, 114 | cookieValue; 115 | 116 | if (!config.toggleImg || !config.onoffTextElement) { 117 | this.isUse = true; 118 | delete config.onoffTextElement; 119 | } else { 120 | cookieValue = Cookies.get(config.cookieName); 121 | this.isUse = cookieValue === 'use' || !cookieValue; 122 | } 123 | config.cookieName = config.cookieName || DEFAULT_COOKIE_NAME; 124 | 125 | if (snippet.isFalsy(config.watchInterval)) { 126 | config.watchInterval = this.watchInterval; 127 | } 128 | 129 | snippet.forEach( 130 | config, 131 | function(value, name) { 132 | if (rIsElementOption.test(name)) { 133 | this.options[name] = $(value); 134 | } else { 135 | this.options[name] = value; 136 | } 137 | }, 138 | this 139 | ); 140 | }, 141 | 142 | /** 143 | * Request data at api server with keyword 144 | * @param {String} keyword The key word to send to Auto complete API 145 | */ 146 | request: function(keyword) { 147 | this.dataManager.request(keyword); 148 | }, 149 | 150 | /** 151 | * Return string in input element. 152 | * @returns {String} 153 | */ 154 | getValue: function() { 155 | return this.inputManager.getValue(); 156 | }, 157 | 158 | /** 159 | * Set inputManager's value to show at search element 160 | * @param {String} keyword The string to show up at search element 161 | */ 162 | setValue: function(keyword) { 163 | this.inputManager.setValue(keyword); 164 | }, 165 | 166 | /** 167 | * Set additional parameters at inputManager. 168 | * @param {string} paramStr String to be addition parameters.(saperator '&') 169 | * @param {string} index The index for setting key value 170 | */ 171 | setParams: function(paramStr, index) { 172 | this.inputManager.setParams(paramStr, index); 173 | }, 174 | 175 | /** 176 | * Request to draw result at resultManager with data from api server. 177 | * @param {Array} dataArr Data array from api server 178 | */ 179 | setServerData: function(dataArr) { 180 | this.resultManager.draw(dataArr); 181 | }, 182 | 183 | /** 184 | * Set Cookie value with whether use auto complete or not 185 | * @param {Boolean} isUse Whether use auto complete or not 186 | */ 187 | setCookieValue: function(isUse) { 188 | Cookies.set(this.options.cookieName, isUse ? 'use' : 'notUse'); 189 | this.isUse = isUse; 190 | this.setToggleBtnImg(isUse); 191 | }, 192 | 193 | /** 194 | * Save matched queries from server. 195 | * @param {Array} queries Result queries 196 | */ 197 | setQueries: function(queries) { 198 | this.queries = [].concat(queries); 199 | }, 200 | 201 | /** 202 | * Get whether use auto complete or not 203 | * @api 204 | * @returns {Boolean} 205 | * @example 206 | * autoComplete.isUseAutoComplete(); => true|false 207 | */ 208 | isUseAutoComplete: function() { 209 | return this.isUse; 210 | }, 211 | 212 | /** 213 | * Whether show the result list area or not. 214 | * @returns {Boolean} 215 | */ 216 | isShowResultList: function() { 217 | return this.resultManager.isShowResultList(); 218 | }, 219 | 220 | /** 221 | * Change toggle button image by auto complete state 222 | * @param {Boolean} isUse whether use auto complete or not 223 | * @private 224 | */ 225 | setToggleBtnImg: function(isUse) { 226 | this.inputManager.setToggleBtnImg(isUse); 227 | }, 228 | 229 | /** 230 | * Hide search result list area 231 | */ 232 | hideResultList: function() { 233 | this.resultManager.hideResultList(); 234 | }, 235 | 236 | /** 237 | * Show search result list area 238 | */ 239 | showResultList: function() { 240 | if (this.isUseAutoComplete()) { 241 | this.resultManager.showResultList(); 242 | } 243 | }, 244 | 245 | /** 246 | * Move to next item in result list. 247 | * @param {string} flow Direction to move. 248 | * @private 249 | */ 250 | moveNextResult: function(flow) { 251 | this.resultManager.moveNextResult(flow); 252 | }, 253 | 254 | /** 255 | * Set text to auto complete switch 256 | * @param {Boolean} isUse Whether use auto complete or not 257 | * @private 258 | */ 259 | changeOnOffText: function(isUse) { 260 | this.resultManager.changeOnOffText(isUse); 261 | }, 262 | 263 | /** 264 | * Reset serachApi 265 | * @api 266 | * @param {Object} options searchApi option 267 | * @example 268 | * autoComplete.setSearchApi({ 269 | * 'st' : 111, 270 | * 'r_lt' : 111, 271 | * 'r_enc' : 'UTF-8', 272 | * 'q_enc' : 'UTF-8', 273 | * 'r_format' : 'json' 274 | * }); 275 | */ 276 | setSearchApi: function(options) { 277 | snippet.extend(this.options.searchApi, options); 278 | }, 279 | 280 | /** 281 | * clear ready value and set idle state 282 | */ 283 | clearReadyValue: function() { 284 | if (snippet.isExisty(this.readyValue)) { 285 | this.request(this.readyValue); 286 | } else { 287 | this.isIdle = true; 288 | } 289 | this.readyValue = null; 290 | } 291 | } 292 | ); 293 | 294 | snippet.CustomEvents.mixin(AutoComplete); 295 | 296 | module.exports = AutoComplete; 297 | -------------------------------------------------------------------------------- /src/js/manager/data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Data is kind of manager module to request data at API with input queries. 3 | */ 4 | var snippet = require('tui-code-snippet'); 5 | var $ = require('jquery'); 6 | var CALLBACK_NAME = 'dataCallback', 7 | SERACH_QUERY_IDENTIFIER = 'q'; 8 | 9 | var forEach = snippet.forEach, 10 | map = snippet.map, 11 | isEmpty = snippet.isEmpty, 12 | extend = snippet.extend; 13 | 14 | /** 15 | * Unit of auto complete connecting server. 16 | * @ignore 17 | * @constructor 18 | */ 19 | var Data = snippet.defineClass( 20 | /** @lends Data.prototype */ { 21 | init: function(autoCompleteObj, options) { 22 | this.autoCompleteObj = autoCompleteObj; 23 | this.options = options; 24 | }, 25 | 26 | /** 27 | * Request data at api server use jsonp 28 | * @param {String} keyword String to request at server 29 | */ 30 | request: function(keyword) { 31 | var rsKeyWrod = keyword.replace(/\s/g, ''), 32 | acObj = this.autoCompleteObj, 33 | keyData; 34 | 35 | if (!keyword || !rsKeyWrod) { 36 | acObj.hideResultList(); 37 | 38 | return; 39 | } 40 | 41 | this.options.searchApi[SERACH_QUERY_IDENTIFIER] = keyword; 42 | $.ajax(this.options.searchUrl, { 43 | dataType: 'jsonp', 44 | jsonpCallback: CALLBACK_NAME, 45 | data: this.options.searchApi, 46 | type: 'get', 47 | success: $.proxy(function(dataObj) { 48 | try { 49 | keyData = this._getCollectionData(dataObj); 50 | acObj.setQueries(dataObj.query); 51 | acObj.setServerData(keyData); 52 | acObj.clearReadyValue(); 53 | } catch (e) { 54 | throw new Error('[DataManager] invalid response data.', e); 55 | } 56 | }, this) 57 | }); 58 | }, 59 | 60 | /** 61 | * Make collection data to display 62 | * @param {object} dataObj Collection data 63 | * @returns {Array} 64 | * @private 65 | */ 66 | _getCollectionData: function(dataObj) { 67 | var collection = dataObj.collections, 68 | itemDataList = []; 69 | 70 | forEach( 71 | collection, 72 | function(itemSet) { 73 | var keys; 74 | 75 | if (isEmpty(itemSet.items)) { 76 | return; 77 | } 78 | 79 | keys = this._getRedirectData(itemSet); 80 | itemDataList.push({ 81 | type: 'title', 82 | values: [itemSet.title] 83 | }); 84 | itemDataList = itemDataList.concat(keys); 85 | }, 86 | this 87 | ); 88 | 89 | return itemDataList; 90 | }, 91 | 92 | /** 93 | * Make item of collection to display 94 | * @param {object} itemSet Item of collection data 95 | * @private 96 | * @returns {Array} 97 | */ 98 | _getRedirectData: function(itemSet) { 99 | var defaultData = { 100 | type: itemSet.type, 101 | index: itemSet.index, 102 | dest: itemSet.destination 103 | }, 104 | items = itemSet.items.slice(0, this.options.viewCount - 1); 105 | 106 | items = map(items, function(item) { 107 | return extend( 108 | { 109 | values: item 110 | }, 111 | defaultData 112 | ); 113 | }); 114 | 115 | return items; 116 | } 117 | } 118 | ); 119 | 120 | module.exports = Data; 121 | -------------------------------------------------------------------------------- /src/js/manager/input.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Input is kind of manager module to support input element events and all of input functions. 3 | */ 4 | var snippet = require('tui-code-snippet'); 5 | var $ = require('jquery'); 6 | 7 | /** 8 | * Unit of auto complete component that belong with input element. 9 | * @ignore 10 | * @constructor 11 | */ 12 | var Input = snippet.defineClass( 13 | /** @lends Input.prototype */ { 14 | /** 15 | * keyboard Input KeyCode enum 16 | */ 17 | keyCodeMap: { 18 | TAB: 9, 19 | UP_ARROW: 38, 20 | DOWN_ARROW: 40, 21 | ESC: 27 22 | }, 23 | 24 | /** 25 | * Initialize 26 | * @param {Object} autoCompleteObj AutoComplete instance 27 | * @param {object} options auto complete options 28 | */ 29 | init: function(autoCompleteObj, options) { 30 | this.autoCompleteObj = autoCompleteObj; 31 | this.options = options; 32 | 33 | /** 34 | * Flag to distinguish new changed inputValue from moving-value in resultList 35 | * @type {boolean} 36 | */ 37 | this.isKeyMoving = false; 38 | 39 | // Save elements from configuration. 40 | this.$searchBox = this.options.searchBoxElement; 41 | this.$toggleBtn = this.options.toggleBtnElement; 42 | this.$orgQuery = this.options.orgQueryElement; 43 | this.$formElement = this.options.formElement; 44 | this.prevValue = ''; 45 | 46 | this._attachEvent(); 47 | }, 48 | 49 | /** 50 | * Return input element value 51 | * @returns {String} Searchbox value 52 | */ 53 | getValue: function() { 54 | return this.$searchBox.val(); 55 | }, 56 | 57 | /** 58 | * Set keyword to input element 59 | * @param {String} str The keyword to set value. 60 | */ 61 | setValue: function(str) { 62 | this.$searchBox.val(str); 63 | }, 64 | 65 | /** 66 | * Read config files parameter option and set parameter. 67 | * @param {Array|string} subQueryValues The subQueryValues from resultList 68 | * @param {number|string} index The index for subQuerySet in config 69 | */ 70 | setParams: function(subQueryValues, index) { 71 | if (subQueryValues && snippet.isString(subQueryValues)) { 72 | subQueryValues = subQueryValues.split(','); 73 | } 74 | 75 | if (!subQueryValues || snippet.isEmpty(subQueryValues)) { 76 | return; 77 | } 78 | this._createParamSetByType(subQueryValues, index); 79 | }, 80 | 81 | /** 82 | * Create inputElement by type 83 | * @param {Array|string} subQueryValues The subQueryValues from resultList 84 | * @param {number|string} index The index for subQuerySet in config 85 | * @private 86 | */ 87 | _createParamSetByType: function(subQueryValues, index) { 88 | var options = this.options, 89 | listConfig = options.listConfig[index], 90 | subQuerySetIndex = listConfig.subQuerySet, 91 | staticParamsIndex = listConfig.staticParams, 92 | subQueryKeys = options.subQuerySet[subQuerySetIndex], 93 | staticParams = options.staticParams[staticParamsIndex]; 94 | 95 | if (!this.hiddens) { 96 | this._createParamContainer(); 97 | } 98 | 99 | snippet.forEach( 100 | subQueryValues, 101 | function(value, idx) { 102 | var key = subQueryKeys[idx]; 103 | 104 | this.hiddens.append( 105 | $('') 106 | ); 107 | }, 108 | this 109 | ); 110 | 111 | this._createStaticParams(staticParams); 112 | }, 113 | 114 | /** 115 | * Create static parameters 116 | * @param {string} staticParams Static parameters 117 | * @private 118 | */ 119 | _createStaticParams: function(staticParams) { 120 | if (!staticParams) { 121 | return; 122 | } 123 | 124 | staticParams = staticParams.split(','); 125 | snippet.forEach( 126 | staticParams, 127 | function(value) { 128 | var val = value.split('='); 129 | 130 | this.hiddens.append( 131 | $('') 132 | ); 133 | }, 134 | this 135 | ); 136 | }, 137 | 138 | /** 139 | * Create wrapper that become container of hidden elements. 140 | * @private 141 | */ 142 | _createParamContainer: function() { 143 | this.hiddens = $('
    ') 144 | .hide() 145 | .appendTo(this.$formElement); 146 | }, 147 | 148 | /** 149 | * Change toggle button image. 150 | * @param {Boolean} isUse 자동완성 사용 여부 151 | */ 152 | setToggleBtnImg: function(isUse) { 153 | if (!this.options.toggleImg || snippet.isEmpty(this.$toggleBtn)) { 154 | return; 155 | } 156 | 157 | if (isUse) { 158 | this.$toggleBtn.attr('src', this.options.toggleImg.on); 159 | } else { 160 | this.$toggleBtn.attr('src', this.options.toggleImg.off); 161 | } 162 | }, 163 | 164 | /** 165 | * Event binding 166 | * @private 167 | */ 168 | _attachEvent: function() { 169 | this.$searchBox.on({ 170 | focus: $.proxy(this._onFocus, this), 171 | blur: $.proxy(this._onBlur, this), 172 | keydown: $.proxy(this._onKeyDown, this), 173 | click: $.proxy(this._onClick, this) 174 | }); 175 | 176 | if (!snippet.isEmpty(this.$toggleBtn)) { 177 | this.$toggleBtn.on('click', $.proxy(this._onClickToggle, this)); 178 | } 179 | }, 180 | 181 | /** 182 | * Save user query into hidden element. 183 | * @param {String} str The string typed by user 184 | * @private 185 | */ 186 | _setOrgQuery: function(str) { 187 | this.$orgQuery.val(str); 188 | }, 189 | 190 | /** 191 | * Input element onclick event handler 192 | * @private 193 | * @param {MouseEvent} event Mouse event 194 | * @returns {boolean} False if no input-keyword or not use auto-complete 195 | */ 196 | _onClick: function(event) { 197 | // 입력된 키워드가 없거나 자동완성 기능 사용하지 않으면 펼칠 필요 없으므로 그냥 리턴하고 끝. 198 | if (!this.autoCompleteObj.getValue() || !this.autoCompleteObj.isUseAutoComplete()) { 199 | return false; 200 | } 201 | 202 | if (!this.autoCompleteObj.isShowResultList()) { 203 | this.autoCompleteObj.showResultList(); 204 | } 205 | event.stopPropagation(); 206 | 207 | return true; 208 | }, 209 | 210 | /** 211 | * Input element focus event handler 212 | * @private 213 | */ 214 | _onFocus: function() { 215 | // setInterval 설정해서 일정 시간 주기로 _onWatch 함수를 실행한다. 216 | this.intervalId = setInterval($.proxy(this._onWatch, this), this.options.watchInterval); 217 | }, 218 | 219 | /** 220 | * Roop for check update input element 221 | * @private 222 | */ 223 | _onWatch: function() { 224 | var searchBoxValue = this.getValue(); 225 | 226 | if (!searchBoxValue) { 227 | this.autoCompleteObj.hideResultList(); 228 | this.prevValue = ''; 229 | this._setOrgQuery(''); 230 | 231 | return; 232 | } 233 | 234 | if (this.isKeyMoving) { 235 | this._setOrgQuery(searchBoxValue); 236 | this.prevValue = searchBoxValue; 237 | } else if (this.prevValue !== searchBoxValue) { 238 | this._onChange(); 239 | } 240 | }, 241 | 242 | /** 243 | * Input element onchange event handler 244 | * @private 245 | */ 246 | _onChange: function() { 247 | var acObj = this.autoCompleteObj, 248 | searchBoxValue = this.getValue(); 249 | 250 | if (!this.autoCompleteObj.isUseAutoComplete()) { 251 | return; 252 | } 253 | 254 | if (acObj.isIdle) { 255 | acObj.isIdle = false; 256 | acObj.request(searchBoxValue); 257 | } else { 258 | acObj.readyValue = searchBoxValue; 259 | acObj.showResultList(); 260 | } 261 | this.prevValue = searchBoxValue; 262 | }, 263 | 264 | /** 265 | * Input element blur event handler 266 | * @private 267 | */ 268 | _onBlur: function() { 269 | clearInterval(this.intervalId); 270 | this.intervalId = null; 271 | }, 272 | 273 | /** 274 | * Input element keydown event handler 275 | * Set actino by input value 276 | * @param {Event} event keyDown Event instance 277 | * @private 278 | */ 279 | /* eslint-disable complexity */ 280 | _onKeyDown: function(event) { 281 | var acObj = this.autoCompleteObj, 282 | flow, 283 | codeMap, 284 | flowMap; 285 | 286 | if (!acObj.isUseAutoComplete() || !acObj.isShowResultList()) { 287 | return; 288 | } 289 | 290 | codeMap = this.keyCodeMap; 291 | flowMap = acObj.flowMap; 292 | switch (event.keyCode) { 293 | case codeMap.TAB: 294 | event.preventDefault(); 295 | flow = event.shiftKey ? flowMap.NEXT : flowMap.PREV; 296 | break; 297 | case codeMap.DOWN_ARROW: 298 | flow = flowMap.NEXT; 299 | break; 300 | case codeMap.UP_ARROW: 301 | flow = flowMap.PREV; 302 | break; 303 | case codeMap.ESC: 304 | acObj.hideResultList(); 305 | break; 306 | default: 307 | break; 308 | } 309 | 310 | if (flow) { 311 | this.isKeyMoving = true; 312 | acObj.moveNextResult(flow); 313 | } else { 314 | this.isKeyMoving = false; 315 | } 316 | }, 317 | /* eslint-enable complexity */ 318 | 319 | /** 320 | * Toggle button click event handler 321 | * @param {MouseEvent} event Mouse click event 322 | * @private 323 | */ 324 | _onClickToggle: function(event) { 325 | var curValue = this.getValue(); 326 | 327 | event.stopPropagation(); 328 | 329 | if (!this.autoCompleteObj.isUseAutoComplete()) { 330 | this.autoCompleteObj.setCookieValue(true); 331 | this.autoCompleteObj.changeOnOffText(true); 332 | if (!curValue) { 333 | return; 334 | } 335 | if (this.prevValue !== curValue) { 336 | this.autoCompleteObj.request(curValue); 337 | } else { 338 | this.autoCompleteObj.showResultList(); 339 | } 340 | } else { 341 | this.autoCompleteObj.setCookieValue(false); 342 | this.autoCompleteObj.changeOnOffText(false); 343 | this.autoCompleteObj.hideResultList(); 344 | } 345 | } 346 | } 347 | ); 348 | 349 | module.exports = Input; 350 | -------------------------------------------------------------------------------- /src/js/manager/result.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Result is kind of managing module to draw auto complete result list from server and apply template. 3 | */ 4 | var snippet = require('tui-code-snippet'); 5 | var $ = require('jquery'); 6 | var DEFAULT_VIEW_COUNT = 10, 7 | WHITE_SPACES = '[\\s]*'; 8 | 9 | var isEmpty = snippet.isEmpty, 10 | forEach = snippet.forEach, 11 | map = snippet.map; 12 | 13 | var rIsSpeicalCharacters = /[\\^$.*+?()[\]{}|]/, 14 | rWhiteSpace = '/s+/g'; 15 | 16 | /** 17 | * Unit of auto complete that belong with search result list. 18 | * Handle the submit data from resultList. 19 | * See {@link Result.prototype._orderElement} which set the request data from arrow-key input 20 | * @ignore 21 | * @constructor 22 | */ 23 | var Result = snippet.defineClass( 24 | /** @lends Result.prototype */ { 25 | init: function(autoCompleteObj, options) { 26 | this.autoCompleteObj = autoCompleteObj; 27 | this.options = options; 28 | 29 | this.$resultList = options.resultListElement; 30 | this.viewCount = options.viewCount || DEFAULT_VIEW_COUNT; 31 | this.$onOffTxt = options.onoffTextElement; 32 | this.mouseOverClass = options.mouseOverClass; 33 | this.flowMap = autoCompleteObj.flowMap; 34 | 35 | this._attachEvent(); 36 | this.$selectedElement = $(); 37 | }, 38 | 39 | /** 40 | * Delete last result list 41 | * @private 42 | */ 43 | _deleteBeforeElement: function() { 44 | this.$selectedElement = $(); 45 | this.$resultList.hide().html(''); 46 | }, 47 | 48 | /** 49 | * Draw result form api server 50 | * @param {Array} dataArr Result data 51 | */ 52 | draw: function(dataArr) { 53 | var len = dataArr.length; 54 | 55 | this._deleteBeforeElement(); 56 | if (len < 1) { 57 | this._hideBottomArea(); 58 | } else { 59 | this._makeResultList(dataArr, len); 60 | } 61 | this.showResultList(); 62 | }, 63 | 64 | /** 65 | * Make search result list element 66 | * @param {Array} dataArr - Data array 67 | * @param {number} len - Length of dataArray 68 | * @private 69 | */ 70 | _makeResultList: function(dataArr, len) { 71 | var template = this.options.template, 72 | listConfig = this.options.listConfig, 73 | useTitle = this.options.useTitle && !!template.title, 74 | tmpl, 75 | index, 76 | tmplValue, 77 | i, 78 | data; 79 | 80 | for (i = 0; i < len; i += 1) { 81 | data = dataArr[i]; 82 | index = data.index; 83 | tmpl = listConfig[index] ? template[listConfig[index].template] : template.defaults; 84 | 85 | if (data.type === 'title') { 86 | tmpl = template.title; 87 | if (!useTitle) { 88 | continue; 89 | } 90 | } 91 | tmplValue = this._getTmplData(tmpl.attr, data); 92 | $(this._applyTemplate(tmpl.element, tmplValue)) 93 | .data({ 94 | params: tmplValue.params, 95 | index: index 96 | }) 97 | .appendTo(this.$resultList); 98 | } 99 | }, 100 | 101 | /** 102 | * Make template data 103 | * @param {Array} attrs Template attributes 104 | * @param {string|Object} data The data to make template 105 | * @returns {Object} Template data 106 | * @private 107 | */ 108 | _getTmplData: function(attrs, data) { 109 | var tmplValue = {}, 110 | values = data.values || null; 111 | 112 | if (snippet.isString(data)) { 113 | tmplValue[attrs[0]] = data; 114 | 115 | return tmplValue; 116 | } 117 | 118 | forEach(attrs, function(attr, idx) { 119 | tmplValue[attr] = values[idx]; 120 | }); 121 | if (attrs.length < values.length) { 122 | tmplValue.params = values.slice(attrs.length); 123 | } 124 | 125 | return tmplValue; 126 | }, 127 | 128 | /** 129 | * Return whether result list show or not 130 | * @returns {Boolean} 131 | */ 132 | isShowResultList: function() { 133 | return this.$resultList.css('display') === 'block'; 134 | }, 135 | 136 | /** 137 | * Hide result list area 138 | */ 139 | hideResultList: function() { 140 | this.$resultList.hide(); 141 | this._hideBottomArea(); 142 | this.autoCompleteObj.isIdle = true; 143 | 144 | /** 145 | * Fired when hide the result list 146 | * @event AutoComplete#close 147 | */ 148 | this.autoCompleteObj.fire('close'); 149 | }, 150 | 151 | /** 152 | * Show result list area 153 | */ 154 | showResultList: function() { 155 | this.$resultList.show(); 156 | this._showBottomArea(); 157 | }, 158 | 159 | /** 160 | * Move focus to next item, change input element value as focus value. 161 | * @param {string} flow Direction by key code 162 | */ 163 | moveNextResult: function(flow) { 164 | var $selectEl = this.$selectedElement, 165 | keyword; 166 | 167 | if (!isEmpty($selectEl)) { 168 | $selectEl.removeClass(this.mouseOverClass); 169 | } 170 | $selectEl = this.$selectedElement = this._orderElement(flow); 171 | 172 | keyword = $selectEl.find('.keyword-field').text(); 173 | if (keyword) { 174 | $selectEl.addClass(this.mouseOverClass); 175 | this.autoCompleteObj.setValue(keyword); 176 | this._setSubmitOption(); 177 | } else { 178 | this.moveNextResult(flow); 179 | } 180 | }, 181 | 182 | /** 183 | * Chage text by whether auto complete use or not 184 | * @param {Boolean} isUse on/off 여부 185 | */ 186 | changeOnOffText: function(isUse) { 187 | if (isUse) { 188 | this.$onOffTxt.text('자동완성 끄기'); 189 | } else { 190 | this.$onOffTxt.text('자동완성 켜기'); 191 | } 192 | }, 193 | 194 | /** 195 | * Attach auto complete event belongs with result list 196 | * @private 197 | */ 198 | _attachEvent: function() { 199 | this.$resultList.on({ 200 | mouseover: $.proxy(this._onMouseOver, this), 201 | click: $.proxy(this._onClick, this) 202 | }); 203 | 204 | if (this.$onOffTxt) { 205 | this.$onOffTxt.on( 206 | 'click', 207 | $.proxy(function() { 208 | this._useAutoComplete(); 209 | }, this) 210 | ); 211 | } 212 | 213 | $(document).on( 214 | 'click', 215 | $.proxy(function() { 216 | this.hideResultList(); 217 | }, this) 218 | ); 219 | }, 220 | 221 | /** 222 | * Highlight key word 223 | * @param {string} tmplStr Template string 224 | * @param {Object} dataObj Replace string map 225 | * @returns {string} 226 | * @private 227 | */ 228 | _applyTemplate: function(tmplStr, dataObj) { 229 | snippet.forEach( 230 | dataObj, 231 | function(value, key) { 232 | if (key === 'subject') { 233 | value = this._highlight(value); 234 | } 235 | tmplStr = tmplStr.replace(new RegExp('@' + key + '@', 'g'), value); 236 | }, 237 | this 238 | ); 239 | 240 | return tmplStr; 241 | }, 242 | 243 | /** 244 | * Return applied highlight effect key word 245 | * (text: Nike air / query : [Nike] / Result : Nike air 246 | * text : 'rhdiddl와 고양이' / query : [rhdiddl, 고양이] / 리턴결과 rhdiddl고양이 247 | * @param {String} text Input string 248 | * @returns {String} 249 | * @private 250 | */ 251 | _highlight: function(text) { 252 | var queries = this.autoCompleteObj.queries, 253 | returnStr; 254 | 255 | snippet.forEach( 256 | queries, 257 | function(query) { 258 | if (!returnStr) { 259 | returnStr = text; 260 | } 261 | returnStr = this._makeStrong(returnStr, query); 262 | }, 263 | this 264 | ); 265 | 266 | return returnStr || text; 267 | }, 268 | 269 | /** 270 | * Contain text by strong tag 271 | * @param {String} text Recommend search data 추천검색어 데이터 272 | * @param {String} query Input keyword 273 | * @returns {String} 274 | * @private 275 | */ 276 | _makeStrong: function(text, query) { 277 | var tmpArr, regQuery; 278 | 279 | if (!query || query.length < 1) { 280 | return text; 281 | } 282 | 283 | tmpArr = query.replace(rWhiteSpace, '').split(''); 284 | tmpArr = map(tmpArr, function(char) { 285 | if (rIsSpeicalCharacters.test(char)) { 286 | return '\\' + char; 287 | } 288 | 289 | return char; 290 | }); 291 | regQuery = new RegExp(tmpArr.join(WHITE_SPACES), 'gi'); 292 | 293 | return text.replace(regQuery, function(match) { 294 | return '' + match + ''; 295 | }); 296 | }, 297 | 298 | /** 299 | * Return the first result item 300 | * @returns {jQuery} 301 | * @private 302 | */ 303 | _getFirst: function() { 304 | return this._orderStage(this.flowMap.FIRST); 305 | }, 306 | 307 | /** 308 | * Return the last result item 309 | * @returns {jQuery} 310 | * @private 311 | */ 312 | _getLast: function() { 313 | return this._orderStage(this.flowMap.LAST); 314 | }, 315 | 316 | /** 317 | * Return whether first or last 318 | * @param {string} type First/end element type 319 | * @returns {jQuery} 320 | * @private 321 | */ 322 | _orderStage: function(type) { 323 | var flowMap = this.flowMap; 324 | var $children = this.$resultList.children(); 325 | var reuslt = null; 326 | 327 | if (type === flowMap.FIRST) { 328 | reuslt = $children.first(); 329 | } else if (type === flowMap.LAST) { 330 | reuslt = $children.last(); 331 | } 332 | 333 | return reuslt; 334 | }, 335 | 336 | /** 337 | * Return previous or next element from resultList by direction 338 | * @param {string} type The direction type for finding element 339 | * @returns {jQuery} 340 | * @private 341 | */ 342 | _orderElement: function(type) { 343 | var $selectedElement = this.$selectedElement, 344 | $order; 345 | 346 | if (type === this.flowMap.NEXT) { 347 | $order = $selectedElement.next(); 348 | 349 | return $order.length ? $order : this._getFirst(); 350 | } 351 | $order = $selectedElement.prev(); 352 | 353 | return $order.length ? $order : this._getLast(); 354 | }, 355 | 356 | /** 357 | * Set whether auto complete use or not and change switch's state. 358 | * @private 359 | */ 360 | _useAutoComplete: function() { 361 | var isUse = this.autoCompleteObj.isUseAutoComplete(); 362 | 363 | this.changeOnOffText(isUse); 364 | this.autoCompleteObj.setCookieValue(isUse); 365 | }, 366 | 367 | /** 368 | * Show auto complete switch area 369 | * @private 370 | */ 371 | _showBottomArea: function() { 372 | if (this.$onOffTxt) { 373 | this.$onOffTxt.show(); 374 | } 375 | }, 376 | 377 | /** 378 | * Hide auto complete switch area 379 | * @private 380 | */ 381 | _hideBottomArea: function() { 382 | if (this.$onOffTxt) { 383 | this.$onOffTxt.hide(); 384 | } 385 | }, 386 | 387 | /** 388 | * Change action attribute of form element and set addition values in hidden type elements. 389 | * (Called when click the
  • ) 390 | * @param {element} [$target] Submit options target 391 | * @private 392 | * 393 | */ 394 | _setSubmitOption: function($target) { 395 | var $selectField = $target ? $($target).closest('li') : this.$selectedElement, 396 | paramsString = $selectField.data('params'), 397 | index = $selectField.data('index'), 398 | config = this.options.listConfig[index], 399 | action = this.options.actions[config.action], 400 | $formElement = this.options.formElement; 401 | 402 | $formElement.attr('action', action); 403 | this._clearSubmitOption(); 404 | this.autoCompleteObj.setParams(paramsString, index); 405 | 406 | /** 407 | * Fired when the user's selected element in result list is changed 408 | * @event AutoComplete#change 409 | * @param {Object} data - Data for submit 410 | * @param {string} data.index - Index of collection 411 | * @param {string} data.action - Form action 412 | * @param {string} data.params - Parameters 413 | */ 414 | this.autoCompleteObj.fire('change', { 415 | index: index, 416 | action: action, 417 | params: paramsString 418 | }); 419 | }, 420 | 421 | /** 422 | * Reset form element. 423 | * @private 424 | */ 425 | _clearSubmitOption: function() { 426 | var $formElement = this.options.formElement; 427 | 428 | $formElement.find('.hidden-inputs').html(''); 429 | }, 430 | 431 | /** 432 | * Result list mouseover event handler 433 | * @param {Event} event Event instanse 434 | * @private 435 | */ 436 | _onMouseOver: function(event) { 437 | var $target = $(event.target), 438 | $arr = this.$resultList.find('li'), 439 | $selectedItem = $target.closest('li'); 440 | 441 | $arr.removeClass(this.mouseOverClass); 442 | if ($selectedItem.find('.keyword-field').length) { 443 | $selectedItem.addClass(this.mouseOverClass); 444 | } 445 | this.$selectedElement = $target; 446 | }, 447 | 448 | /** 449 | * Result list click evnet handler 450 | * Submit form element. 451 | * @param {Event} event Event instanse 452 | * @private 453 | */ 454 | _onClick: function(event) { 455 | var $target = $(event.target), 456 | $formElement = this.options.formElement, 457 | $selectField = $target.closest('li'), 458 | $keywordField = $selectField.find('.keyword-field'), 459 | selectedKeyword = $keywordField.text(); 460 | 461 | this.autoCompleteObj.setValue(selectedKeyword); 462 | if (selectedKeyword) { 463 | this._setSubmitOption($target); 464 | $formElement.submit(); 465 | } 466 | } 467 | } 468 | ); 469 | 470 | module.exports = Result; 471 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | camelcase: 0 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /test/autoConfig.js: -------------------------------------------------------------------------------- 1 | var Default = { 2 | // 설정용가능한 항목을 모두 설정한 config 3 | // 자동완성 결과를 보여주는 엘리먼트 4 | resultListElement: '._resultBox', 5 | 6 | // 검색어를 입력하는 input 엘리먼트 7 | searchBoxElement: '#ac_input1', 8 | 9 | // 입력한 검색어를 넘기기 위한 hidden element 10 | orgQueryElement: '#org_query', 11 | 12 | // on,off 버튼 엘리먼트 13 | toggleBtnElement: '#onoffBtn', 14 | 15 | // on,off 상태를 알리는 엘리먼트 16 | onoffTextElement: '.baseBox .bottom', 17 | 18 | // on, off상태일때 변경 이미지 경로 19 | toggleImg: { 20 | on: '../img/btn_on.jpg', 21 | off: '../img/btn_off.jpg' 22 | }, 23 | 24 | // 컬렉션아이템별 보여질 리스트 겟수 25 | viewCount: 3, 26 | 27 | // 서브쿼리로 넘어갈 키 값들의 배열(컬렉션 명 별로 지정 할 수 있다, 따로 지정하지 않으면 defaults가 적용된다.) 28 | subQuerySet: [['key1', 'key2', 'key3'], ['dep1', 'dep2', 'dep3'], ['ch1', 'ch2', 'ch3'], ['cid']], 29 | 30 | // 컬렉션 인덱스별 자동완성 리스트의 config를 설정한다. 31 | listConfig: { 32 | '0': { 33 | template: 'department', 34 | subQuerySet: 0, 35 | action: 0 36 | }, 37 | '1': { 38 | template: 'srch_in_department', 39 | subQuerySet: 1, 40 | action: 0 41 | }, 42 | '2': { 43 | template: 'srch_in_department', 44 | subQuerySet: 2, 45 | action: 1, 46 | staticParams: 0 47 | }, 48 | '3': { 49 | template: 'department', 50 | subQuerySet: 0, 51 | action: 1, 52 | staticParams: 1 53 | } 54 | }, 55 | 56 | // 컬렉션 타입 별로 표시될 마크업, 데이터가 들어갈 부분은 @key@ 형식으로 사용한다.(지정하지 않으면, defaults가 적용된다.) 57 | // 형식은 수정 가능하지만, keyword-field는 키워드가 들어가는 부분에 필수로 들어가야함. 단 title에는 들어가면 안됨. 58 | template: { 59 | department: { 60 | element: 61 | '
  • ' + 62 | 'Shop the ' + 63 | '@subject@ ' + 64 | 'Store' + 65 | '
  • ', 66 | attr: ['subject'] 67 | }, 68 | srch: { 69 | element: '
  • @subject@
  • ', 70 | attr: ['subject'] 71 | }, 72 | srch_in_department: { 73 | element: 74 | '
  • ' + 75 | '@subject@ ' + 76 | 'in ' + 77 | '@department@' + 78 | '
  • ', 79 | attr: ['subject', 'department'] 80 | }, 81 | title: { 82 | element: '
  • @title@
  • ', 83 | attr: ['title'] 84 | }, 85 | defaults: { 86 | element: '
  • @subject@
  • ', 87 | attr: ['subject'] 88 | } 89 | }, 90 | 91 | // 컬렉션 타입별 form action 을 지정한다. (지정하지 않으면 defaults가 적용된다) 92 | actions: ['http://www.fashiongo.net/catalog.aspx', 'http://www.fashiongo.net/search2.aspx'], 93 | 94 | // 컬렉션 타입별 추가 스테틱한 옵션들을 설정 95 | staticParams: ['qt=ProductName', 'at=TEST,bt=ACT'], 96 | 97 | // 타이틀을 보일지 여부 98 | useTitle: true, 99 | 100 | // 검색창을 감싸고 있는 form앨리먼트 101 | formElement: '#ac_form1', 102 | 103 | // 자동완성을 끄고 켤때 사용되는 쿠키명 104 | cookieName: 'usecookie', 105 | 106 | // 선택된 엘리먼트에 추가되는 클래스명 107 | mouseOverClass: 'emp', 108 | 109 | // 자동완성API url 110 | searchUrl: 'http://12.123.123.123:20011/ac', 111 | 112 | // 자동완성API request 설정 113 | searchApi: { 114 | st: 1111, 115 | r_lt: 1111, 116 | r_enc: 'UTF-8', 117 | q_enc: 'UTF-8', 118 | r_format: 'json' 119 | } 120 | }; 121 | var Plane = { 122 | // 필수 항목만 나열한 config 123 | // 자동완성 결과를 보여주는 엘리먼트 124 | resultListElement: '._resultBox', 125 | 126 | // 검색어를 입력하는 input 엘리먼트 127 | searchBoxElement: '#ac_input1', 128 | 129 | // 입력한 검색어를 넘기기 위한 hidden element 130 | orgQueryElement: '#org_query', 131 | 132 | // 컬렉션아이템별 보여질 리스트 겟수 133 | viewCount: 2, 134 | 135 | // 서브쿼리로 넘어갈 키 값들의 배열(컬렉션 명 별로 지정 할 수 있다, 따로 지정하지 않으면 defaults가 적용된다.) 136 | subQuerySet: [['key1', 'key2', 'key3'], ['dep1', 'dep2', 'dep3'], ['ch1', 'ch2', 'ch3'], ['cid']], 137 | 138 | // 컬렉션 인덱스별 자동완성 리스트의 config를 설정한다. 139 | listConfig: { 140 | '0': { 141 | template: 'department', 142 | subQuerySet: 0, 143 | action: 0 144 | }, 145 | '1': { 146 | template: 'srch_in_department', 147 | subQuerySet: 1, 148 | action: 0 149 | }, 150 | '2': { 151 | template: 'srch_in_department', 152 | subQuerySet: 2, 153 | action: 1, 154 | staticParams: 0 155 | }, 156 | '3': { 157 | template: 'department', 158 | subQuerySet: 0, 159 | action: 1, 160 | staticParams: 1 161 | } 162 | }, 163 | 164 | useTitle: false, 165 | 166 | // 컬렉션 타입 별로 표시될 마크업, 데이터가 들어갈 부분은 @key@ 형식으로 사용한다.(지정하지 않으면, defaults가 적용된다.) 167 | // 형식은 수정 가능하지만, keyword-field는 키워드가 들어가는 부분에 필수로 들어가야함. 단 title에는 들어가면 안됨. 168 | template: { 169 | department: { 170 | element: 171 | '
  • ' + 172 | 'Shop the ' + 173 | '@subject@ ' + 174 | 'Store' + 175 | '
  • ', 176 | attr: ['subject'] 177 | }, 178 | srch: { 179 | element: '
  • @subject@
  • ', 180 | attr: ['subject'] 181 | }, 182 | srch_in_department: { 183 | element: 184 | '
  • ' + 185 | '@subject@ ' + 186 | 'in ' + 187 | '@department@' + 188 | '
  • ', 189 | attr: ['subject', 'department'] 190 | }, 191 | title: { 192 | element: '
  • @title@
  • ', 193 | attr: ['title'] 194 | }, 195 | defaults: { 196 | element: '
  • @subject@
  • ', 197 | attr: ['subject'] 198 | } 199 | }, 200 | 201 | // 컬렉션 타입별 form action 을 지정한다. (지정하지 않으면 defaults가 적용된다) 202 | actions: ['http://www.fashiongo.net/catalog.aspx', 'http://www.fashiongo.net/search2.aspx'], 203 | 204 | // 컬렉션 타입별 추가 스테틱한 옵션들을 설정 205 | staticParams: ['qt=ProductName', 'at=TEST,bt=ACT'], 206 | 207 | // 검색창을 감싸고 있는 form앨리먼트 208 | formElement: '#ac_form1', 209 | 210 | // 자동완성API url 211 | searchUrl: 'http://12.123.123.123:20011/ac', 212 | 213 | // 자동완성API request 설정 214 | searchApi: { 215 | st: 1111, 216 | r_lt: 1111, 217 | r_enc: 'UTF-8', 218 | q_enc: 'UTF-8', 219 | r_format: 'json' 220 | } 221 | }; 222 | 223 | module.exports = { 224 | Default: Default, 225 | Plane: Plane 226 | }; 227 | -------------------------------------------------------------------------------- /test/autocomplete.spec.js: -------------------------------------------------------------------------------- 1 | var AutoComplete = require('../src/js/autoComplete'); 2 | var snippet = require('tui-code-snippet'); 3 | 4 | describe('자동완성 컴포넌트를 생성하고 기능을 테스트한다.', function() { 5 | var autoComplete, 6 | resultManager, 7 | inputManager, 8 | global = tui.test.global; 9 | 10 | describe('functions', function() { 11 | beforeEach(function() { 12 | loadFixtures('expand.html'); 13 | $('#ac_input1').val('운동화'); 14 | 15 | // 객체 생성 16 | autoComplete = new AutoComplete({ config: global.Default }); 17 | resultManager = autoComplete.resultManager; 18 | inputManager = autoComplete.inputManager; 19 | }); 20 | 21 | // OK 22 | it('AutoComplete, Manager 객체가 생성되는지 테스트한다.', function() { 23 | var A = new AutoComplete({ 24 | config: global.Default 25 | }); 26 | 27 | expect(A).toEqual(expect.any(Object)); 28 | 29 | // 객체 생성 판단 30 | resultManager = A.resultManager; 31 | inputManager = A.inputManager; 32 | 33 | expect(inputManager).toBeTruthy(); 34 | expect(resultManager).toBeTruthy(); 35 | }); 36 | 37 | it('키워드 하이라이팅이 제대로 되는가.', function() { 38 | resultManager = autoComplete.resultManager; 39 | 40 | // 검색어 입력 41 | autoComplete.setValue('운동화'); 42 | autoComplete.request('운동화'); 43 | 44 | // 키워드 하이라이트 처리 테스트 45 | autoComplete.queries = ['나이키']; 46 | expect(resultManager._highlight('나이키 에어')).toBe('나이키 에어'); 47 | autoComplete.queries = ['TEST']; 48 | expect(resultManager._highlight('나이키 에어')).toBe('나이키 에어'); 49 | }); 50 | 51 | // OK 52 | it('자동완성 기능을 사용안함으로 설정되는가.', function() { 53 | // 자동완성 기능 사용 안함 설정 54 | autoComplete.setCookieValue(false); 55 | expect(autoComplete.isUseAutoComplete()).toBeFalsy(); 56 | autoComplete.hideResultList(); 57 | }); 58 | 59 | it('(검색어 결과가 있는 경우)검색어 입력 후, 검색 결과가 있는가.', function() { 60 | var eventMock = { 61 | stopPropagation: function() {} 62 | }; 63 | 64 | autoComplete.setCookieValue(true); 65 | autoComplete.setValue('운동화'); 66 | 67 | expect($('._resultBox')).not.toBeEmpty(); 68 | expect($('._resultBox > li')).not.toBeEmpty(); 69 | expect(inputManager).toBeDefined(); 70 | 71 | autoComplete.setCookieValue(false); 72 | inputManager._onClickToggle(eventMock); 73 | }); 74 | 75 | it('자동완성 끄기/켜기 기능이 제대로 동작하는가.', function() { 76 | var $onOffTxt = $('.baseBox .bottom'); 77 | 78 | resultManager.changeOnOffText(true); 79 | expect($('#onofftext').text()).toEqual('자동완성 끄기'); 80 | 81 | resultManager.changeOnOffText(false); 82 | expect($onOffTxt.css('display')).toEqual('none'); 83 | 84 | resultManager._useAutoComplete(); 85 | expect(resultManager.isShowResultList()).toBeFalsy(); 86 | }); 87 | }); 88 | 89 | describe('usageStatistics', function() { 90 | beforeEach(function() { 91 | snippet.sendHostname = jest.fn(); 92 | }); 93 | 94 | it('should send hostname by default', function() { 95 | autoComplete = new AutoComplete({ config: global.Default }); 96 | 97 | expect(snippet.sendHostname).toHaveBeenCalled(); 98 | }); 99 | 100 | it('should not send hostname on usageStatistics option false', function() { 101 | autoComplete = new AutoComplete({ 102 | config: global.Default, 103 | usageStatistics: false 104 | }); 105 | 106 | expect(snippet.sendHostname).not.toHaveBeenCalled(); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/datamanager.spec.js: -------------------------------------------------------------------------------- 1 | var AutoComplete = require('../src/js/autoComplete'), 2 | DataManager = require('../src/js/manager/data'); 3 | 4 | describe('DataManager 생성 및 테스트', function() { 5 | var dm1, 6 | dm2, 7 | dm3, 8 | global = tui.test.global; 9 | 10 | beforeEach(function() { 11 | var ac; 12 | 13 | loadFixtures('expand.html'); 14 | ac = new AutoComplete({ 15 | config: global.Plane 16 | }); 17 | dm1 = ac.dataManager; 18 | 19 | ac = new AutoComplete({ 20 | config: global.Default 21 | }); 22 | dm2 = ac.dataManager; 23 | 24 | dm3 = new DataManager(global.Default); 25 | }); 26 | 27 | it('to be defined', function() { 28 | expect(dm1).toBeDefined(); 29 | expect(dm2).toBeDefined(); 30 | expect(dm1.options).toBeDefined(); 31 | expect(dm2.options).toBeDefined(); 32 | }); 33 | 34 | it('to be defined but not normally', function() { 35 | expect(dm3).toBeDefined(); 36 | expect(dm3.options).not.toBeDisabled(); 37 | }); 38 | 39 | it('getCollectionData return array of collection data', function() { 40 | var arr = dm1._getCollectionData(global.mock), 41 | arr2 = dm2._getCollectionData(global.n_mock); 42 | 43 | expect(arr).toBeDefined(); 44 | expect(arr.length).toBeDefined(); 45 | expect(arr2).toBeDefined(); 46 | expect(arr2.length).toBe(0); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/fixtures/expand.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | 5 | 6 |
    7 | 8 |
    9 |
    10 | 13 | 14 |
    15 |
    16 |
    17 | -------------------------------------------------------------------------------- /test/inputmanager.spec.js: -------------------------------------------------------------------------------- 1 | var AutoComplete = require('../src/js/autoComplete'); 2 | 3 | describe('InputManager', function() { 4 | var im1, 5 | global = tui.test.global; 6 | 7 | beforeEach(function() { 8 | var autocom; 9 | 10 | loadFixtures('expand.html'); 11 | 12 | autocom = new AutoComplete({ 13 | config: global.Default 14 | }); 15 | im1 = autocom.inputManager; 16 | }); 17 | 18 | it('to be defined', function() { 19 | expect(im1).toBeDefined(); 20 | expect(im1.options).toBeDefined(); 21 | }); 22 | 23 | it('getValue', function() { 24 | im1.$searchBox.val('s'); 25 | expect(im1.getValue()).toBe('s'); 26 | }); 27 | 28 | it('setParams with array', function() { 29 | var opt = ['a', 'b'], 30 | index = '0', 31 | inputs; 32 | 33 | im1.setParams(opt, index); 34 | inputs = im1.hiddens.find('input'); 35 | 36 | expect(inputs.length).toBe(2); 37 | }); 38 | 39 | it('setParams with string and staticParams', function() { 40 | var opt = 'a,b', 41 | index = '2', 42 | inputs; 43 | 44 | im1.setParams(opt, index); 45 | inputs = im1.hiddens.find('input'); 46 | 47 | expect(inputs.length).toBe(3); 48 | }); 49 | 50 | it('setParams with noting', function() { 51 | var opt = '', 52 | index = '0'; 53 | 54 | im1.setParams(opt, index); 55 | 56 | expect(im1.hiddens).not.toBeDefined(); 57 | }); 58 | 59 | it('_setOrgQuery', function() { 60 | var query = 'asdf'; 61 | 62 | im1._setOrgQuery(query); 63 | 64 | expect(im1.$orgQuery.val()).toBe(query); 65 | }); 66 | 67 | it('검색창 클릭시 리스트 영역 동작.', function() { 68 | var autocon = im1.autoCompleteObj, 69 | eventMock = { 70 | stopPropagation: function() {} 71 | }; 72 | 73 | autocon.showResultList = jest.fn(); 74 | 75 | im1.setValue('asdf'); 76 | im1.autoCompleteObj.isUse = true; 77 | 78 | im1._onClick(eventMock); 79 | expect(autocon.showResultList).toHaveBeenCalled(); 80 | 81 | autocon.resultManager.$resultList.css({ 82 | // Modify: (v1.1.2) Do not hide 83 | display: 'block' 84 | }); 85 | im1._onClick(eventMock); 86 | expect(autocon.showResultList).toHaveBeenCalled(); 87 | }); 88 | 89 | it('자동완성 목록 사용하지 않을 경우 동작하지 않음.', function() { 90 | var autocon = im1.autoCompleteObj; 91 | 92 | autocon.showResultList = jest.fn(); 93 | autocon.hideResultList = jest.fn(); 94 | 95 | im1.setValue('asdf'); 96 | im1.autoCompleteObj.isUse = false; 97 | 98 | im1._onClick(); 99 | 100 | expect(autocon.showResultList).not.toHaveBeenCalled(); 101 | expect(autocon.hideResultList).not.toHaveBeenCalled(); 102 | }); 103 | 104 | it('_onFocus, onWatch', function(done) { 105 | im1.$searchBox.val('focus'); 106 | 107 | im1._onWatch = jest.fn(); 108 | 109 | im1._onFocus(); 110 | 111 | setTimeout(function() { 112 | expect(im1._onWatch).toHaveBeenCalled(); 113 | im1._onBlur(); 114 | done(); 115 | }, 500); 116 | }); 117 | 118 | it('onWatch', function() { 119 | im1._onChange = jest.fn(); 120 | 121 | im1.$searchBox.val('focus'); 122 | im1._onWatch(); 123 | 124 | im1.$searchBox.val(''); 125 | im1._onWatch(); 126 | 127 | expect(im1._onChange).toHaveBeenCalled(); 128 | }); 129 | 130 | it('onWatch runned with resultManger moved flag', function() { 131 | im1._onChange = jest.fn(); 132 | 133 | im1.$searchBox.val('asdf'); 134 | im1._onWatch(); 135 | 136 | im1.autoCompleteObj.resultManager.isMoved = false; 137 | 138 | im1.$searchBox.val('asdf'); 139 | im1._onWatch(); 140 | 141 | expect(im1._onChange).toHaveBeenCalled(); 142 | }); 143 | 144 | it('_onKeyDown with up key', function() { 145 | var autocon = im1.autoCompleteObj; 146 | 147 | autocon.isUse = true; 148 | autocon.resultManager.$resultList.css({ 149 | display: 'block' 150 | }); 151 | 152 | autocon.moveNextResult = jest.fn(); 153 | 154 | im1._onKeyDown({ 155 | keyCode: 38 156 | }); 157 | expect(im1.isKeyMoving).toBe(true); 158 | expect(autocon.moveNextResult).toHaveBeenCalled(); 159 | }); 160 | 161 | it('_onKeyDown with down key', function() { 162 | var autocon = im1.autoCompleteObj; 163 | 164 | autocon.isUse = true; 165 | autocon.resultManager.$resultList.css({ 166 | display: 'block' 167 | }); 168 | 169 | autocon.moveNextResult = jest.fn(); 170 | 171 | im1._onKeyDown({ 172 | keyCode: 40 173 | }); 174 | expect(im1.isKeyMoving).toBe(true); 175 | expect(autocon.moveNextResult).toHaveBeenCalled(); 176 | }); 177 | 178 | it('_onKeyDown with tab key', function() { 179 | var autocon = im1.autoCompleteObj; 180 | 181 | autocon.isUse = true; 182 | autocon.resultManager.$resultList.css({ 183 | display: 'block' 184 | }); 185 | 186 | autocon.moveNextResult = jest.fn(); 187 | 188 | im1._onKeyDown({ 189 | keyCode: 9, 190 | preventDefault: function() {} 191 | }); 192 | expect(im1.isKeyMoving).toBe(true); 193 | expect(autocon.moveNextResult).toHaveBeenCalled(); 194 | }); 195 | 196 | it('_onKeyDown with other key', function() { 197 | var autocon = im1.autoCompleteObj; 198 | 199 | autocon.isUse = true; 200 | autocon.resultManager.$resultList.css({ 201 | display: 'block' 202 | }); 203 | 204 | autocon.moveNextResult = jest.fn(); 205 | 206 | im1._onKeyDown({ 207 | keyCode: 93 208 | }); 209 | expect(im1.isKeyMoving).toBe(false); 210 | expect(autocon.moveNextResult).not.toHaveBeenCalled(); 211 | }); 212 | 213 | it('_onKeyDown with down key, but not show resultList', function() { 214 | var autocon = im1.autoCompleteObj; 215 | 216 | autocon.isUse = true; 217 | autocon.resultManager.$resultList.css({ 218 | display: 'none' 219 | }); 220 | 221 | autocon.moveNextResult = jest.fn(); 222 | 223 | im1._onKeyDown({ 224 | keyCode: 40 225 | }); 226 | 227 | expect(im1.isKeyMoving).toBe(false); 228 | expect(autocon.moveNextResult).not.toHaveBeenCalled(); 229 | }); 230 | 231 | it('_onClickToggle when autoComplete is using, turn off autoComplete', function() { 232 | var eventMock = { 233 | stopPropagation: function() {} 234 | }, 235 | autocon = im1.autoCompleteObj; 236 | 237 | autocon.isUse = true; 238 | im1._onClickToggle(eventMock); 239 | 240 | expect(autocon.isUse).toBe(false); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /test/mock.js: -------------------------------------------------------------------------------- 1 | var mock = { 2 | collections: [ 3 | { 4 | index: 0, 5 | items: [ 6 | ['Shrugs & Cardigans', '27', '30'], 7 | ['Sweaters', '27', '30'], 8 | ['Sets', '27', '30'], 9 | ['Shirts & Blouse', '27', '30'], 10 | ['Shorts', '27', '30'], 11 | ['Shoes', '27', '30'], 12 | ['Semiformal', '27', '30'], 13 | ['Seamless', '27', '30'], 14 | ['Special Occasion', '27', '30'], 15 | ['Socks\\/Footwear', '27', '30'] 16 | ], 17 | title: 'Category', 18 | type: 'department' 19 | }, 20 | { 21 | index: 1, 22 | items: [ 23 | ['Solid Dress', 'Casual', 'c3,44'], 24 | ['Stretch Bracelet', 'Bracelets', 'c3=153'], 25 | ['Striped Top', 'Fashion Tops', 'c3=254'], 26 | ['stripe cardigan', 'Shrugs & Cardigans', 'c3=27'], 27 | ['SOLID TOP', 'Dressy Tops', 'c3=18'], 28 | ['Sweater top', 'Fashion Tops', 'c3=254'], 29 | ['SOLID TOP', 'Casual', 'c3=210'], 30 | ['SLEEVELESS TOP', 'Fashion Tops', 'c3=254'], 31 | ['SOLID TOP', 'Fashion Tops', 'c3=254'], 32 | ['STRIPED TOP', 'Casual', 'c3=210'] 33 | ], 34 | title: 'Product in Category', 35 | type: 'srch_in_department' 36 | }, 37 | { 38 | index: 2, 39 | items: [ 40 | ['Skinny jeans'], 41 | ['shirt dress'], 42 | ['SWEATSHIRT'], 43 | ['striped dress'], 44 | ['sweat pants'], 45 | ['stripe CARDIGAN'], 46 | ['SunGlasses'], 47 | ['short sleeve'], 48 | ['Shopwtd'], 49 | ['shorts'] 50 | ], 51 | title: 'Product', 52 | type: 'srch' 53 | } 54 | ], 55 | query: ['s'], 56 | ver: '1.0' 57 | }; 58 | var nMock = { 59 | collections: [ 60 | { 61 | index: 0, 62 | items: [], 63 | title: 'Category', 64 | type: 'department' 65 | }, 66 | { 67 | index: 1, 68 | items: [], 69 | title: 'Product in Category', 70 | type: 'srch_in_department' 71 | }, 72 | { 73 | index: 2, 74 | items: [], 75 | title: 'Product', 76 | type: 'srch' 77 | } 78 | ], 79 | query: ['s'], 80 | ver: '1.0' 81 | }; 82 | 83 | module.exports = { 84 | mock: mock, 85 | n_mock: nMock 86 | }; 87 | -------------------------------------------------------------------------------- /test/resultmanager.spec.js: -------------------------------------------------------------------------------- 1 | var AutoComplete = require('../src/js/autoComplete'); 2 | 3 | describe('ResultManager', function() { 4 | var rm1, 5 | rm2, 6 | global = tui.test.global; 7 | 8 | beforeEach(function() { 9 | var ac; 10 | 11 | loadFixtures('expand.html'); 12 | ac = new AutoComplete({ 13 | config: global.Default 14 | }); 15 | rm1 = ac.resultManager; 16 | 17 | ac = new AutoComplete({ 18 | config: global.Plane 19 | }); 20 | rm2 = ac.resultManager; 21 | }); 22 | 23 | it('to be defined', function() { 24 | expect(rm1).toBeDefined(); 25 | expect(rm2).toBeDefined(); 26 | }); 27 | 28 | it('draw data with Title', function() { 29 | var autocon = rm1.autoCompleteObj, 30 | dm = autocon.dataManager, 31 | data = dm._getCollectionData(global.mock); 32 | 33 | rm1.draw(data); 34 | expect(autocon.isShowResultList()).toBeTruthy(); 35 | }); 36 | 37 | it('draw data with no Title', function() { 38 | var autocon = rm2.autoCompleteObj, 39 | dm = autocon.dataManager, 40 | data = dm._getCollectionData(global.mock); 41 | 42 | rm2.draw(data); 43 | expect(autocon.isShowResultList()).toBeTruthy(); 44 | }); 45 | 46 | it('_getTmplData', function() { 47 | var td = rm1._getTmplData(['a', 'b', 'c'], { values: ['v1', 'v2', 'v3'] }), 48 | td2 = rm1._getTmplData(['a', 'b', 'c'], 'v1'); 49 | 50 | expect(td.a).toBe('v1'); 51 | expect(td.b).toBe('v2'); 52 | expect(td.c).toBe('v3'); 53 | expect(td2.a).toBe('v1'); 54 | }); 55 | 56 | // Modify - v1.1.2: remove "isMoved" flag 57 | it('moveNextResult will set the selectedElement to first-child of resultList', function() { 58 | var autocon = rm1.autoCompleteObj, 59 | dm = autocon.dataManager, 60 | data = dm._getCollectionData(global.mock); 61 | 62 | rm1.draw(data); 63 | rm1.moveNextResult('next'); 64 | 65 | expect(rm1.$selectedElement[0]).not.toBe(rm1.$resultList.children().first()); 66 | }); 67 | 68 | it('moveNextResult with selectedElement', function() { 69 | var autocon = rm1.autoCompleteObj, 70 | dm = autocon.dataManager, 71 | data = dm._getCollectionData(global.mock), 72 | bSel, 73 | aSel; 74 | 75 | rm1.draw(data); 76 | rm1.moveNextResult('next'); 77 | 78 | bSel = rm1.$selectedElement; 79 | rm1.moveNextResult('next'); 80 | aSel = rm1.$selectedElement; 81 | 82 | expect(bSel.next()[0]).toBe(aSel[0]); 83 | }); 84 | 85 | it('makeStrong', function() { 86 | var text = 'dkfjdkfj 65 _ 2 * G+ T 9"76asdl65g65_2Gt965_2*G+t9', 87 | query = '65_2*G+t9', 88 | result; 89 | 90 | result = rm1._makeStrong(text, query); 91 | expect(result).toEqual( 92 | 'dkfjdkfj 65 _ 2 * G+ T 9"' + 93 | '76asdl65g65_2Gt9' + 94 | '65_2*G+t9' 95 | ); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/setup-globals.js: -------------------------------------------------------------------------------- 1 | var snippet = require('tui-code-snippet'); 2 | var Mock = require('./mock'); 3 | var config = require('./autoConfig'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var $ = require('jquery'); 7 | var fixturesPath = './fixtures'; 8 | 9 | snippet.defineNamespace('tui.test.global', { 10 | Default: config.Default, 11 | Plane: config.Plane, 12 | mock: Mock.mock, 13 | n_mock: Mock.n_mock, 14 | }); 15 | 16 | global.$ = global.jQuery = $; 17 | 18 | global.loadFixtures = function (fileName) { 19 | var data = ''; 20 | var dir = path.resolve(__dirname, fixturesPath, fileName); 21 | 22 | try { 23 | data = fs.readFileSync(dir, 'utf8'); // eslint-disable-line no-sync 24 | } catch (err) {} // eslint-disable-line no-empty 25 | finally { 26 | document.body.innerHTML = data; 27 | } 28 | }; 29 | 30 | expect.extend({ 31 | toBeEmpty: function (actual) { 32 | var result; 33 | try { 34 | result = $(actual).is(':empty'); 35 | 36 | return { 37 | pass: result, 38 | message: function () { 39 | return `recived value is ${result ? '' : 'not '}empty.`; 40 | }, 41 | }; 42 | } catch (error) { 43 | return { 44 | pass: false, 45 | message: function () { 46 | return 'received value cannot resolve to jQuery'; 47 | }, 48 | }; 49 | } 50 | }, 51 | toBeDisabled: function (actual) { 52 | var result; 53 | try { 54 | result = $(actual).is(':disabled'); 55 | 56 | return { 57 | pass: result, 58 | message: function () { 59 | return `recived value is ${result ? '' : 'not '}disabled.`; 60 | }, 61 | }; 62 | } catch (error) { 63 | return { 64 | pass: false, 65 | message: function () { 66 | return 'received value cannot resolve to jQuery'; 67 | }, 68 | }; 69 | } 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /tuidoc.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "logo": { 4 | "src": "https://uicdn.toast.com/toastui/img/tui-component-bi-white.png" 5 | }, 6 | "title": { 7 | "text": "Auto Complete", 8 | "linkUrl": "https://github.com/nhn/tui.auto-complete" 9 | } 10 | }, 11 | "footer": [ 12 | { 13 | "title": "NHN Cloud", 14 | "linkUrl": "https://github.com/nhn" 15 | }, 16 | { 17 | "title": "FE Development Lab", 18 | "linkUrl": "https://ui.toast.com/" 19 | } 20 | ], 21 | "main": { 22 | "filePath": "README.md" 23 | }, 24 | "api": { 25 | "filePath": "src/js/**", 26 | "permalink": true 27 | }, 28 | "examples": { 29 | "filePath": "examples", 30 | "titles": { 31 | "example01-basic": "1. Basic", 32 | "example02-toggle-autocompletion": "2. Toggle auto completion", 33 | "example03-dynamic-search": "3. Dynamic search" 34 | } 35 | }, 36 | "pathPrefix": "tui.auto-complete" 37 | } 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-process-env */ 2 | /** 3 | * Configs file for bundling 4 | */ 5 | var path = require('path'); 6 | var pkg = require('./package.json'); 7 | var webpack = require('webpack'); 8 | 9 | module.exports = function(env, argv) { 10 | var isProduction = argv.mode === 'production'; 11 | var FILENAME = pkg.name + (isProduction ? '.min.js' : '.js'); 12 | var BANNER = [ 13 | 'TOAST UI Auto Complete', 14 | '@version ' + pkg.version, 15 | '@author ' + pkg.author, 16 | '@license ' + pkg.license 17 | ].join('\n'); 18 | 19 | return { 20 | mode: 'development', 21 | entry: './src/js/autoComplete.js', 22 | output: { 23 | library: ['tui', 'AutoComplete'], 24 | libraryTarget: 'umd', 25 | path: path.resolve(__dirname, 'dist'), 26 | publicPath: 'dist/', 27 | filename: FILENAME 28 | }, 29 | externals: { 30 | 'tui-code-snippet': { 31 | commonjs: 'tui-code-snippet', 32 | commonjs2: 'tui-code-snippet', 33 | amd: 'tui-code-snippet', 34 | root: ['tui', 'util'] 35 | }, 36 | jquery: { 37 | commonjs: 'jquery', 38 | commonjs2: 'jquery', 39 | amd: 'jquery', 40 | root: '$' 41 | }, 42 | 'js-cookie': { 43 | commonjs: 'js-cookie', 44 | commonjs2: 'js-cookie', 45 | amd: 'js-cookie', 46 | root: 'Cookies' 47 | } 48 | }, 49 | module: { 50 | rules: [ 51 | { 52 | test: /\.js$/, 53 | exclude: /(node_modules|bower_components)/, 54 | loader: 'eslint-loader', 55 | enforce: 'pre', 56 | options: { 57 | failOnError: isProduction 58 | } 59 | } 60 | ] 61 | }, 62 | plugins: [new webpack.BannerPlugin(BANNER)], 63 | devServer: { 64 | historyApiFallback: false, 65 | progress: true, 66 | host: '0.0.0.0', 67 | disableHostCheck: true 68 | } 69 | }; 70 | } 71 | --------------------------------------------------------------------------------