├── .eslintrc
├── .gitattributes
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── help-with-something.md
├── .gitignore
├── LICENSE
├── README.md
├── dist
├── react.tagify.jsx
├── react.tagify.jsx.map
├── tagify.css
├── tagify.esm.js
├── tagify.esm.js.map
├── tagify.js
├── tagify.js.map
├── tagify.polyfills.min.js
├── tagify.polyfills.min.js.map
└── tagify.vue
├── docs
├── codepen.css
├── codepen.js
├── demo.gif
├── demo2.apng
├── examples
│ ├── dist
│ │ ├── RTL.html
│ │ ├── advance-options.html
│ │ ├── basic.html
│ │ ├── different-look.html
│ │ ├── disabled-user-input.html
│ │ ├── disabled.html
│ │ ├── drag-sort.html
│ │ ├── extra-properties.html
│ │ ├── input-suggestions-styled-as-tags.html
│ │ ├── input.html
│ │ ├── manual-suggestions.html
│ │ ├── mix.html
│ │ ├── mode-select.html
│ │ ├── outside-of-the-box.html
│ │ ├── readonly-mixed.html
│ │ ├── readonly.html
│ │ ├── textarea.html
│ │ └── users-list.html
│ └── src
│ │ ├── RTL
│ │ ├── RTL.css
│ │ ├── RTL.html
│ │ └── RTL.js
│ │ ├── advance-options
│ │ ├── advance-options.css
│ │ ├── advance-options.html
│ │ └── advance-options.js
│ │ ├── basic
│ │ ├── basic.html
│ │ └── basic.js
│ │ ├── different-look
│ │ ├── different-look.css
│ │ ├── different-look.html
│ │ └── different-look.js
│ │ ├── disabled-user-input
│ │ ├── disabled-user-input.css
│ │ ├── disabled-user-input.html
│ │ └── disabled-user-input.js
│ │ ├── disabled
│ │ ├── disabled.html
│ │ └── disabled.js
│ │ ├── drag-sort
│ │ ├── drag-sort.html
│ │ └── drag-sort.js
│ │ ├── example-template.html
│ │ ├── extra-properties
│ │ ├── extra-properties.css
│ │ ├── extra-properties.html
│ │ └── extra-properties.js
│ │ ├── input-suggestions-styled-as-tags
│ │ ├── input-suggestions-styled-as-tags.css
│ │ ├── input-suggestions-styled-as-tags.html
│ │ └── input-suggestions-styled-as-tags.js
│ │ ├── input
│ │ ├── input.html
│ │ └── input.js
│ │ ├── manual-suggestions
│ │ ├── manual-suggestions.css
│ │ ├── manual-suggestions.html
│ │ └── manual-suggestions.js
│ │ ├── mix
│ │ ├── mix.html
│ │ └── mix.js
│ │ ├── mode-select
│ │ ├── mode-select.css
│ │ ├── mode-select.html
│ │ └── mode-select.js
│ │ ├── outside-of-the-box
│ │ ├── outside-of-the-box.css
│ │ ├── outside-of-the-box.html
│ │ └── outside-of-the-box.js
│ │ ├── readonly-mixed
│ │ ├── readonly-mixed.css
│ │ ├── readonly-mixed.html
│ │ └── readonly-mixed.js
│ │ ├── readonly
│ │ ├── readonly.html
│ │ └── readonly.js
│ │ ├── textarea
│ │ ├── textarea.html
│ │ └── textarea.js
│ │ └── users-list
│ │ ├── users-list.css
│ │ ├── users-list.html
│ │ └── users-list.js
├── homepage
│ ├── head.html
│ ├── header.html
│ ├── index.html
│ ├── scripts.html
│ └── section.njk
├── logo.svg
├── mix2.gif
├── mix3.gif
├── readme-header.svg
└── suggestions-list.apng
├── gulpfile.js
├── index.html
├── package.json
├── playwright.config.js
├── pnpm-lock.yaml
├── roadmap.md
├── src
├── parts
│ ├── EventDispatcher.js
│ ├── constants.js
│ ├── defaults.js
│ ├── dropdown.js
│ ├── events.js
│ ├── helpers.js
│ ├── persist.js
│ ├── suggestions.js
│ ├── templates.js
│ └── texts.js
├── polyfills
│ ├── Array.findIndex.js
│ ├── Array.includes.js
│ ├── Array.some.js
│ ├── AutoUrlDetect.js
│ ├── Element.classList.js
│ ├── Element.closest.js
│ ├── Element.matches.js
│ ├── Event.js
│ ├── NodeList.forEach.js
│ ├── Object.assign.js
│ ├── String.includes.js
│ ├── String.trim.js
│ └── es6-promise.js
├── react-compat-layer.js
├── react.tagify.jsx
├── tagify.js
├── tagify.polyfills.js
└── tagify.scss
├── test
├── basic-RTL.html
├── basic.html
├── dragsort.html
├── easy-to-customize.html
├── manual-suggestions.html
├── mix.html
├── mode-select.html
├── scroll-tracking.html
├── tagify.test.js
├── test.html
├── test2.html
├── text-input.html
├── textarea.html
├── users-list.html
└── validate.html
└── tests
└── basic.spec.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "no-console" : ["error", { "allow": ["warn", "error"] }],
4 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
5 | "block-spacing" : [2, "always"],
6 | "comma-style" : [2, "last"],
7 | "no-debugger" : [1],
8 | "no-alert" : [1],
9 | "indent" : [1, 4, {"SwitchCase":1}],
10 | "strict" : 0,
11 | "no-undef" : 1,
12 | "no-dupe-args" : 2,
13 | "no-const-assign" : 2,
14 | "no-extra-semi" : 2,
15 | "no-dupe-class-members" : 2,
16 | "no-dupe-else-if" : 2,
17 | "no-dupe-keys" : 2,
18 | "no-invalid-regexp" : 2,
19 | "valid-typeof" : 2
20 | },
21 | "parserOptions": {
22 | "ecmaVersion" : 2020,
23 | "sourceType": "module",
24 | "ecmaFeatures": {
25 | "modules": true,
26 | "sourceType": "module"
27 | }
28 | },
29 | "env": {
30 | "es6": true,
31 | "browser": true,
32 | "node": true
33 | },
34 | "globals": {
35 | "jQuery": "readonly"
36 | }
37 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: "⚠️ Please make a DEMO page if possible"
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Prerequisites
11 |
12 | - [x] I am running the latest version
13 | - [ ] I checked the documentation and found no answer
14 | - [ ] I checked to make sure that this issue has not already been filed
15 |
16 | ### 💥 Demo Page
17 |
18 |
19 | https://jsbin.com/jekuqap/edit?html,js,output
20 |
21 | ***React*** issue template:
22 | https://codesandbox.io/s/tagify-react-issue-template-4ub1r?file=/src/index.js
23 |
24 | ### Explanation
25 |
26 | - **What is the expected behavior?**
27 |
28 | - **What is happening instead?**
29 |
30 | - **What error message are you getting?**
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: 'Suggest an idea '
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/help-with-something.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Help with something
3 | about: "⚠️ Please make a DEMO page if possible"
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Prerequisites
11 |
12 | - [x] I am running the latest version
13 | - [ ] I checked the documentation and found no answer
14 | - [ ] I checked to make sure that this issue has not already been filed
15 |
16 | ### Demo Page - *clone one of the below:*
17 |
18 |
19 | https://jsbin.com/jekuqap/edit?html,js,output
20 |
21 | ***React*** issue template:
22 | https://codesandbox.io/s/tagify-react-issue-template-4ub1r?file=/src/index.js
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Windows image file caches
2 | Thumbs.db
3 | ehthumbs.db
4 |
5 | # Folder config file
6 | Desktop.ini
7 |
8 | # Recycle Bin used on file shares
9 | $RECYCLE.BIN/
10 |
11 | # Windows Installer files
12 | *.cab
13 | *.msi
14 | *.msm
15 | *.msp
16 |
17 | # Windows shortcuts
18 | *.lnk
19 |
20 | # =========================
21 | # Operating System Files
22 | # =========================
23 |
24 | # OSX
25 | # =========================
26 |
27 | .DS_Store
28 | .AppleDouble
29 | .LSOverride
30 |
31 | # Thumbnails
32 | ._*
33 |
34 | # Files that might appear in the root of a volume
35 | .DocumentRevisions-V100
36 | .fseventsd
37 | .Spotlight-V100
38 | .TemporaryItems
39 | .Trashes
40 | .VolumeIcon.icns
41 |
42 | # Directories potentially created on remote AFP share
43 | .AppleDB
44 | .AppleDesktop
45 | Network Trash Folder
46 | Temporary Items
47 | .apdisk
48 | /node_modules
49 | /test.html
50 | /.npmrc
51 | /.babelrc
52 | /test2.html
53 | /package-lock.json
54 | /react
55 | /react-demo.html
56 | .idea
57 | /.vscode/bookmarks.json
58 | /.vscode/launch.json
59 | /.vscode/settings.json
60 | /111.html
61 | /logo.psd
62 | /test/_temp.html
63 | /test/test-1.html
64 | /test-results/
65 | /playwright-report/
66 | /blob-report/
67 | /playwright/.cache/
68 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a copy
2 | of this software and associated documentation files (the "Software"), to deal
3 | in the Software without restriction, including without limitation the rights
4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5 | copies of the Software, and to permit persons to whom the Software is
6 | furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in
9 | all copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
17 | THE SOFTWARE.
18 |
19 | This Software may not be rebranded and sold as a library under any other name
20 | other than "Tagify" (by owner) or as part of another library.
--------------------------------------------------------------------------------
/dist/tagify.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/docs/codepen.css:
--------------------------------------------------------------------------------
1 | body{
2 | height: 100vh;
3 | display: flex;
4 | flex-flow: column;
5 | align-items: center;
6 | justify-content: center;
7 | padding: 12em 0;
8 | box-sizing: border-box;
9 | }
10 |
11 | .pen-intro{
12 |
13 | }
14 |
15 | .repoLink{
16 | position: absolute;
17 | top: 0;
18 | right: 0;
19 | width: 18%;
20 | max-width: 360px;
21 | min-width: 160px;
22 | }
23 |
24 | label[for='checkbox-tagify-show-input']{
25 | position: absolute;
26 | top: .85em;
27 | left: 2.5em;
28 |
29 | }
30 |
31 | #checkbox-tagify-show-input{
32 | position: absolute;
33 | top: 1em;
34 | left: 1em;
35 | transform: scale(1.4);
36 | }
37 |
38 | #checkbox-tagify-show-input:checked ~ input,
39 | #checkbox-tagify-show-input:checked ~ textarea{
40 | display: block !important;
41 | position: static !important;
42 | transform: none !important;
43 | width: 100%;
44 | max-width: 700px;
45 | margin-top: 1em;
46 | padding: .5em;
47 | }
48 |
49 | #checkbox-tagify-show-input:checked ~ textarea{
50 | min-height: 11ch;
51 | }
--------------------------------------------------------------------------------
/docs/codepen.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | var repoLinkContent = ` `
3 | repoLinkContent = ` `
4 |
5 | var repoLink = `
6 | ${repoLinkContent}
7 | `
8 |
9 | var html = `
10 | Show original input
11 |
12 | `
13 |
14 | setTimeout(() => {
15 | document.body.insertAdjacentHTML('afterbegin', html.trim())
16 | }, 200);
17 | })()
18 |
--------------------------------------------------------------------------------
/docs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yairEO/tagify/3c49093fba26029602a5dc56676af3504e74dd3a/docs/demo.gif
--------------------------------------------------------------------------------
/docs/demo2.apng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yairEO/tagify/3c49093fba26029602a5dc56676af3504e74dd3a/docs/demo2.apng
--------------------------------------------------------------------------------
/docs/examples/dist/RTL.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | RTL
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
83 |
88 |
111 |
112 |
--------------------------------------------------------------------------------
/docs/examples/dist/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | basic
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
76 |
77 |
--------------------------------------------------------------------------------
/docs/examples/dist/different-look.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | different-look
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
136 |
137 | +
138 |
139 |
175 |
176 |
--------------------------------------------------------------------------------
/docs/examples/dist/disabled-user-input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | disabled-user-input
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
76 |
77 |
--------------------------------------------------------------------------------
/docs/examples/dist/disabled.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | disabled
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
73 |
74 |
--------------------------------------------------------------------------------
/docs/examples/dist/drag-sort.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | drag-sort
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
86 |
87 |
--------------------------------------------------------------------------------
/docs/examples/dist/input-suggestions-styled-as-tags.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | input-suggestions-styled-as-tags
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
96 |
97 |
98 |
99 |
113 |
114 |
--------------------------------------------------------------------------------
/docs/examples/dist/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | input
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
70 | Remove all these tags ⬆
71 |
72 |
165 |
166 |
--------------------------------------------------------------------------------
/docs/examples/dist/manual-suggestions.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | manual-suggestions
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
78 |
79 |
80 |
☝ Add items from below list:
81 |
82 |
126 |
127 |
--------------------------------------------------------------------------------
/docs/examples/dist/mix.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mix
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
148 |
149 |
--------------------------------------------------------------------------------
/docs/examples/dist/mode-select.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | mode-select
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
90 |
91 |
--------------------------------------------------------------------------------
/docs/examples/dist/outside-of-the-box.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | outside-of-the-box
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
85 |
86 |
87 |
88 |
100 |
101 |
--------------------------------------------------------------------------------
/docs/examples/dist/readonly-mixed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | readonly-mixed
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
86 |
87 |
91 |
92 |
--------------------------------------------------------------------------------
/docs/examples/dist/readonly.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | readonly
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
73 |
74 |
--------------------------------------------------------------------------------
/docs/examples/dist/textarea.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | textarea
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
81 |
82 |
--------------------------------------------------------------------------------
/docs/examples/src/RTL/RTL.css:
--------------------------------------------------------------------------------
1 | .tagify__dropdown--rtl-example {
2 | max-width: none !important;
3 | }
4 |
5 | /* each suggestion item in the dropdown should be rendered as a single line of text */
6 | .tagify__dropdown--rtl-example .tagify__dropdown__item {
7 | white-space: nowrap;
8 | }
9 |
10 | /* just for fun */
11 | .tagify__dropdown--rtl-example .tagify__dropdown__item > em {
12 | font-weight: 700;
13 | }
14 |
15 | /* in this example the tagify itself is narrower than the other examples */
16 | .tagify--rtl-exmaple {
17 | min-width: 300px;
18 | }
--------------------------------------------------------------------------------
/docs/examples/src/RTL/RTL.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/docs/examples/src/RTL/RTL.js:
--------------------------------------------------------------------------------
1 | // The DOM element you wish to replace with Tagify
2 | var input = document.querySelector('input[name=rtl-example]');
3 |
4 | // initialize Tagify on the above input node reference
5 | new Tagify(input, {
6 | whitelist: [
7 | { value: "מיכאל כהן", full: "מיכאל כהן - פיתוח תוכנה מתקדם ויישום טכנולוגיות חדשניות בתחום התעשייה והייצור" },
8 | { value: "שרה לוי", full: "שרה לוי - ניהול ופיתוח פתרונות אקולוגיים וסביבתיים למתן יתרון תחרותי לעסקים" },
9 | { value: "אברהם גולן", full: "אברהם גולן - יישום ופיתוח טכנולוגיות מתקדמות לשיפור פרודוקטיביות ויצירתיות בארגונים" },
10 | { value: "רחל רביבו", full: "רחל רביבו - מחקר ופיתוח טכנולוגי בתחום החדשנות והיזמות לקידום עסקים ותעשיות" },
11 | { value: "דוד כהן", full: "דוד כהן - פיתוח ויישום טכנולוגיות מתקדמות לשיפור תשתיות מידע עסקיות" },
12 | { value: "רבקה אריאל", full: "רבקה אריאל - ייזום ופיתוח מוצרים חדשניים עבור תעשיות יצירתיות ומתקדמות" }
13 | ],
14 | dropdown: {
15 | mapValueTo: 'full',
16 | classname: 'tagify__dropdown--rtl-example',
17 | enabled: 0, // shows the suggestiosn dropdown once field is focused
18 | RTL: true,
19 | escapeHTML: false // allows HTML inside each suggestion item
20 | }
21 | })
--------------------------------------------------------------------------------
/docs/examples/src/advance-options/advance-options.css:
--------------------------------------------------------------------------------
1 | .advance-options .tagify__tag{
2 | --tag-hover: var(--tag-bg);
3 | }
--------------------------------------------------------------------------------
/docs/examples/src/advance-options/advance-options.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/advance-options/advance-options.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input.advance-options'),
2 | tagify = new Tagify(input, {
3 | pattern : /^.{0,20}$/, // Validate typed tag(s) by Regex. Here maximum chars length is defined as "20"
4 | delimiters : ",| ", // add new tags when a comma or a space character is entered
5 | trim : false, // if "delimiters" setting is using space as a delimeter, then "trim" should be set to "false"
6 | keepInvalidTags : true, // do not remove invalid tags (but keep them marked as invalid)
7 | // createInvalidTags: false,
8 | editTags : {
9 | clicks: 2, // single click to edit a tag
10 | keepInvalid: false // if after editing, tag is invalid, auto-revert
11 | },
12 | maxTags : 6,
13 | blacklist : ["foo", "bar", "baz"],
14 | whitelist : ["temple","stun","detective","sign","passion","routine","deck","discriminate","relaxation","fraud","attractive","soft","forecast","point","thank","stage","eliminate","effective","flood","passive","skilled","separation","contact","compromise","reality","district","nationalist","leg","porter","conviction","worker","vegetable","commerce","conception","particle","honor","stick","tail","pumpkin","core","mouse","egg","population","unique","behavior","onion","disaster","cute","pipe","sock","dialect","horse","swear","owner","cope","global","improvement","artist","shed","constant","bond","brink","shower","spot","inject","bowel","homosexual","trust","exclude","tough","sickness","prevalence","sister","resolution","cattle","cultural","innocent","burial","bundle","thaw","respectable","thirsty","exposure","team","creed","facade","calendar","filter","utter","dominate","predator","discover","theorist","hospitality","damage","woman","rub","crop","unpleasant","halt","inch","birthday","lack","throne","maximum","pause","digress","fossil","policy","instrument","trunk","frame","measure","hall","support","convenience","house","partnership","inspector","looting","ranch","asset","rally","explicit","leak","monarch","ethics","applied","aviation","dentist","great","ethnic","sodium","truth","constellation","lease","guide","break","conclusion","button","recording","horizon","council","paradox","bride","weigh","like","noble","transition","accumulation","arrow","stitch","academy","glimpse","case","researcher","constitutional","notion","bathroom","revolutionary","soldier","vehicle","betray","gear","pan","quarter","embarrassment","golf","shark","constitution","club","college","duty","eaux","know","collection","burst","fun","animal","expectation","persist","insure","tick","account","initiative","tourist","member","example","plant","river","ratio","view","coast","latest","invite","help","falsify","allocation","degree","feel","resort","means","excuse","injury","pupil","shaft","allow","ton","tube","dress","speaker","double","theater","opposed","holiday","screw","cutting","picture","laborer","conservation","kneel","miracle","brand","nomination","characteristic","referral","carbon","valley","hot","climb","wrestle","motorist","update","loot","mosquito","delivery","eagle","guideline","hurt","feedback","finish","traffic","competence","serve","archive","feeling","hope","seal","ear","oven","vote","ballot","study","negative","declaration","particular","pattern","suburb","intervention","brake","frequency","drink","affair","contemporary","prince","dry","mole","lazy","undermine","radio","legislation","circumstance","bear","left","pony","industry","mastermind","criticism","sheep","failure","chain","depressed","launch","script","green","weave","please","surprise","doctor","revive","banquet","belong","correction","door","image","integrity","intermediate","sense","formal","cane","gloom","toast","pension","exception","prey","random","nose","predict","needle","satisfaction","establish","fit","vigorous","urgency","X-ray","equinox","variety","proclaim","conceive","bulb","vegetarian","available","stake","publicity","strikebreaker","portrait","sink","frog","ruin","studio","match","electron","captain","channel","navy","set","recommend","appoint","liberal","missile","sample","result","poor","efflux","glance","timetable","advertise","personality","aunt","dog"],
15 | transformTag : transformTag,
16 | backspace : "edit",
17 | placeholder : "Type something",
18 | dropdown : {
19 | enabled: 1, // show suggestion after 1 typed character
20 | fuzzySearch: false, // match only suggestions that starts with the typed characters
21 | position: 'text', // position suggestions list next to typed text
22 | caseSensitive: true, // allow adding duplicate items if their case is different
23 | },
24 | templates: {
25 | dropdownItemNoMatch: function(data) {
26 | return `No suggestion found for: ${data.value}
`
27 | }
28 | }
29 | })
30 |
31 | tagify.on('change', updatePlaceholderByTagsCount);
32 |
33 | function updatePlaceholderByTagsCount() {
34 | tagify.setPlaceholder(`${tagify.value.length || 'no'} tags added`)
35 | }
36 |
37 | updatePlaceholderByTagsCount()
38 |
39 | // generate a random color (in HSL format, which I like to use)
40 | function getRandomColor(){
41 | function rand(min, max) {
42 | return min + Math.random() * (max - min);
43 | }
44 |
45 | var h = rand(1, 360)|0,
46 | s = rand(40, 70)|0,
47 | l = rand(65, 72)|0;
48 |
49 | return 'hsl(' + h + ',' + s + '%,' + l + '%)';
50 | }
51 |
52 | function transformTag( tagData ){
53 | tagData.color = getRandomColor();
54 | tagData.style = "--tag-bg:" + tagData.color;
55 |
56 | if( tagData.value.toLowerCase() == 'shit' )
57 | tagData.value = 's✲✲t'
58 | }
59 |
60 | tagify.on('add', function(e){
61 | console.log(e.detail)
62 | })
63 |
64 | tagify.on('invalid', function(e){
65 | console.log(e, e.detail);
66 | })
67 |
68 | var clickDebounce;
69 |
70 | tagify.on('click', function(e){
71 | const {tag:tagElm, data:tagData} = e.detail;
72 |
73 | // a delay is needed to distinguish between regular click and double-click.
74 | // this allows enough time for a possible double-click, and noly fires if such
75 | // did not occur.
76 | clearTimeout(clickDebounce);
77 | clickDebounce = setTimeout(() => {
78 | tagData.color = getRandomColor();
79 | tagData.style = "--tag-bg:" + tagData.color;
80 | tagify.replaceTag(tagElm, tagData);
81 | }, 200);
82 | })
83 |
84 | tagify.on('dblclick', function(e){
85 | // when souble clicking, do not change the color of the tag
86 | clearTimeout(clickDebounce);
87 | })
--------------------------------------------------------------------------------
/docs/examples/src/basic/basic.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/basic/basic.js:
--------------------------------------------------------------------------------
1 | // The DOM element you wish to replace with Tagify
2 | var input = document.querySelector('input[name=basic]');
3 |
4 | // initialize Tagify on the above input node reference
5 | new Tagify(input)
--------------------------------------------------------------------------------
/docs/examples/src/different-look/different-look.css:
--------------------------------------------------------------------------------
1 | .customLook {
2 | --tag-bg : #0052BF;
3 | --tag-hover : #CE0078;
4 | --tag-text-color : #FFF;
5 | --tags-border-color : silver;
6 | --tag-text-color--edit : #111;
7 | --tag-remove-bg : var(--tag-hover);
8 | --tag-pad : .6em 1em;
9 | --tag-inset-shadow-size : 1.4em; /* compensate for the larger --tag-pad value */
10 | --tag-remove-btn-color : white;
11 | --tag-remove-btn-bg--hover: black;
12 |
13 | display: inline-block;
14 | min-width: 0;
15 | border: none;
16 | }
17 |
18 | .customLook .tagify__tag {
19 | margin-top: 0;
20 | }
21 |
22 | .customLook .tagify__tag>div {
23 | border-radius: 25px;
24 | }
25 |
26 | .customLook .tagify__tag:not(:only-of-type):not(.tagify__tag--editable):hover .tagify__tag-text {
27 | margin-inline-end: -1px;
28 | }
29 |
30 | /* Do not show the "remove tag" (x) button when only a single tag remains */
31 | .customLook .tagify__tag:only-of-type .tagify__tag__removeBtn {
32 | display: none;
33 | }
34 |
35 | .customLook .tagify__tag__removeBtn {
36 | opacity: 0;
37 | transform: translateX(-100%) scale(.5);
38 | margin-inline: -20px 6px;
39 | /* very specific on purpose */
40 | text-align: right;
41 | transition: .12s;
42 | }
43 |
44 | .customLook .tagify__tag:not(.tagify__tag--editable):hover .tagify__tag__removeBtn {
45 | transform: none;
46 | opacity: 1;
47 | }
48 |
49 | .customLook+button {
50 | color: #0052BF;
51 | font: bold 1.4em/1.65 Arial;
52 | border: 0;
53 | background: none;
54 | box-shadow: 0 0 0 2px inset currentColor;
55 | border-radius: 50%;
56 | width: 1.65em;
57 | height: 1.65em;
58 | cursor: pointer;
59 | outline: none;
60 | transition: .1s ease-out;
61 | margin: 0 0 0 5px;
62 | vertical-align: top;
63 | }
64 |
65 | .customLook+button:hover {
66 | box-shadow: 0 0 0 5px inset currentColor;
67 | }
68 |
69 | .customLook .tagify__input {
70 | display: none;
71 | }
--------------------------------------------------------------------------------
/docs/examples/src/different-look/different-look.html:
--------------------------------------------------------------------------------
1 | +
--------------------------------------------------------------------------------
/docs/examples/src/different-look/different-look.js:
--------------------------------------------------------------------------------
1 | // generate random whilist items (for the demo)
2 | var randomStringsArr = Array.apply(null, Array(100)).map(function () {
3 | return Array.apply(null, Array(~~(Math.random() * 10 + 3))).map(function () {
4 | return String.fromCharCode(Math.random() * (123 - 97) + 97)
5 | }).join('') + '@gmail.com'
6 | })
7 |
8 | var input = document.querySelector('.customLook'),
9 | button = input.nextElementSibling,
10 | tagify = new Tagify(input, {
11 | editTags: {
12 | keepInvalid: false, // better to auto-remove invalid tags which are in edit-mode (on blur)
13 | },
14 | // email address validation (https://stackoverflow.com/a/46181/104380)
15 | pattern: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
16 | whitelist: randomStringsArr,
17 | callbacks: {
18 | "invalid": onInvalidTag
19 | },
20 | dropdown: {
21 | position: 'text',
22 | enabled: 1 // show suggestions dropdown after 1 typed character
23 | }
24 | }); // "add new tag" action-button
25 |
26 | button.addEventListener("click", onAddButtonClick)
27 |
28 | function onAddButtonClick() {
29 | tagify.addEmptyTag()
30 | }
31 |
32 | function onInvalidTag(e) {
33 | console.log("invalid", e.detail)
34 | }
--------------------------------------------------------------------------------
/docs/examples/src/disabled-user-input/disabled-user-input.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yairEO/tagify/3c49093fba26029602a5dc56676af3504e74dd3a/docs/examples/src/disabled-user-input/disabled-user-input.css
--------------------------------------------------------------------------------
/docs/examples/src/disabled-user-input/disabled-user-input.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/disabled-user-input/disabled-user-input.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[name=disabled-user-input]'),
2 | tagify = new Tagify(input, {
3 | whitelist: [1,2,3,4,5],
4 | userInput: false
5 | })
--------------------------------------------------------------------------------
/docs/examples/src/disabled/disabled.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/disabled/disabled.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[disabled]'),
2 | tagify = new Tagify(input);
--------------------------------------------------------------------------------
/docs/examples/src/drag-sort/drag-sort.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/drag-sort/drag-sort.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[name=drag-sort]'),
2 | tagify = new Tagify(input);
3 |
4 | // using 3-party script "dragsort"
5 | // https://github.com/yairEO/dragsort
6 | var dragsort = new DragSort(tagify.DOM.scope, {
7 | selector:'.' + tagify.settings.classNames.tag,
8 | callbacks: {
9 | dragEnd: onDragEnd
10 | }
11 | })
12 |
13 | function onDragEnd(elm){
14 | tagify.updateValueByDOMTags()
15 | }
--------------------------------------------------------------------------------
/docs/examples/src/example-template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{NAME}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
72 |
73 |
--------------------------------------------------------------------------------
/docs/examples/src/extra-properties/extra-properties.css:
--------------------------------------------------------------------------------
1 | .tagify__dropdown.extra-properties .tagify__dropdown__item > img{
2 | display: inline-block;
3 | vertical-align: middle;
4 | height: 20px;
5 | transform: scale(.75);
6 | margin-right: 5px;
7 | border-radius: 2px;
8 | transition: .12s ease-out;
9 | }
10 |
11 | .tagify__dropdown.extra-properties .tagify__dropdown__item--active > img,
12 | .tagify__dropdown.extra-properties .tagify__dropdown__item:hover > img{
13 | transform: none;
14 | margin-right: 12px;
15 | }
16 |
17 | .tagify.countries .tagify__input{ min-width:175px; }
18 |
19 | .tagify.countries tag{ white-space:nowrap; }
20 | .tagify.countries tag img{
21 | display: inline-block;
22 | height: 16px;
23 | margin-right: 3px;
24 | border-radius: 2px;
25 | pointer-events: none;
26 | }
--------------------------------------------------------------------------------
/docs/examples/src/extra-properties/extra-properties.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/input-suggestions-styled-as-tags/input-suggestions-styled-as-tags.css:
--------------------------------------------------------------------------------
1 | .tags-look .tagify__dropdown__item{
2 | display: inline-block;
3 | vertical-align: middle;
4 | border-radius: 3px;
5 | padding: .3em .5em;
6 | border: 1px solid #CCC;
7 | background: #F3F3F3;
8 | margin: .2em;
9 | font-size: .85em;
10 | color: black;
11 | transition: 0s;
12 | }
13 |
14 | .tags-look .tagify__dropdown__item--active{
15 | border-color: black;
16 | }
17 |
18 | .tags-look .tagify__dropdown__item:hover{
19 | background: lightyellow;
20 | border-color: gold;
21 | }
22 |
23 | .tags-look .tagify__dropdown__item--hidden {
24 | max-width: 0;
25 | max-height: initial;
26 | padding: .3em 0;
27 | margin: .2em 0;
28 | white-space: nowrap;
29 | text-indent: -20px;
30 | border: 0;
31 | }
--------------------------------------------------------------------------------
/docs/examples/src/input-suggestions-styled-as-tags/input-suggestions-styled-as-tags.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/input-suggestions-styled-as-tags/input-suggestions-styled-as-tags.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[name="input-custom-dropdown"]'),
2 | // init Tagify script on the above inputs
3 | tagify = new Tagify(input, {
4 | whitelist: ["A# .NET", "A# (Axiom)", "A-0 System", "A+", "A++", "ABAP", "ABC", "ABC ALGOL", "ABSET", "ABSYS", "ACC", "Accent", "Ace DASL", "ACL2", "Avicsoft", "ACT-III", "Action!", "ActionScript", "Ada", "Adenine", "Agda", "Agilent VEE", "Agora", "AIMMS", "Alef", "ALF", "ALGOL 58", "ALGOL 60", "ALGOL 68", "ALGOL W", "Alice", "Alma-0", "AmbientTalk", "Amiga E", "AMOS", "AMPL", "Apex (Salesforce.com)", "APL", "AppleScript", "Arc", "ARexx", "Argus", "AspectJ", "Assembly language", "ATS", "Ateji PX", "AutoHotkey", "Autocoder", "AutoIt", "AutoLISP / Visual LISP", "Averest", "AWK", "Axum", "Active Server Pages", "ASP.NET", "B", "Babbage", "Bash", "BASIC", "bc", "BCPL", "BeanShell", "Batch (Windows/Dos)", "Bertrand", "BETA", "Bigwig", "Bistro", "BitC", "BLISS", "Blockly", "BlooP", "Blue", "Boo", "Boomerang", "Bourne shell (including bash and ksh)", "BREW", "BPEL", "B", "C--", "C++ – ISO/IEC 14882", "C# – ISO/IEC 23270", "C/AL", "Caché ObjectScript", "C Shell", "Caml", "Cayenne", "CDuce", "Cecil", "Cesil", "Céu", "Ceylon", "CFEngine", "CFML", "Cg", "Ch", "Chapel", "Charity", "Charm", "Chef", "CHILL", "CHIP-8", "chomski", "ChucK", "CICS", "Cilk", "Citrine (programming language)", "CL (IBM)", "Claire", "Clarion", "Clean", "Clipper", "CLIPS", "CLIST", "Clojure", "CLU", "CMS-2", "COBOL – ISO/IEC 1989", "CobolScript – COBOL Scripting language", "Cobra", "CODE", "CoffeeScript", "ColdFusion", "COMAL", "Combined Programming Language (CPL)", "COMIT", "Common Intermediate Language (CIL)", "Common Lisp (also known as CL)", "COMPASS", "Component Pascal", "Constraint Handling Rules (CHR)", "COMTRAN", "Converge", "Cool", "Coq", "Coral 66", "Corn", "CorVision", "COWSEL", "CPL", "CPL", "Cryptol", "csh", "Csound", "CSP", "CUDA", "Curl", "Curry", "Cybil", "Cyclone", "Cython", "Java", "Javascript", "M2001", "M4", "M#", "Machine code", "MAD (Michigan Algorithm Decoder)", "MAD/I", "Magik", "Magma", "make", "Maple", "MAPPER now part of BIS", "MARK-IV now VISION:BUILDER", "Mary", "MASM Microsoft Assembly x86", "MATH-MATIC", "Mathematica", "MATLAB", "Maxima (see also Macsyma)", "Max (Max Msp – Graphical Programming Environment)", "Maya (MEL)", "MDL", "Mercury", "Mesa", "Metafont", "Microcode", "MicroScript", "MIIS", "Milk (programming language)", "MIMIC", "Mirah", "Miranda", "MIVA Script", "ML", "Model 204", "Modelica", "Modula", "Modula-2", "Modula-3", "Mohol", "MOO", "Mortran", "Mouse", "MPD", "Mathcad", "MSIL – deprecated name for CIL", "MSL", "MUMPS", "Mystic Programming L"],
5 | maxTags: 10,
6 | dropdown: {
7 | maxItems: 20, // <- mixumum allowed rendered suggestions
8 | classname: 'tags-look', // <- custom classname for this dropdown, so it could be targeted
9 | enabled: 0, // <- show suggestions on focus
10 | closeOnSelect: false // <- do not hide the suggestions dropdown once an item has been selected
11 | }
12 | })
--------------------------------------------------------------------------------
/docs/examples/src/input/input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Remove all these tags ⬆
--------------------------------------------------------------------------------
/docs/examples/src/input/input.js:
--------------------------------------------------------------------------------
1 | var inputElm = document.querySelector('input[name=input]'),
2 | whitelist = ["A# .NET", "A# (Axiom)", "A-0 System", "A+", "A++", "ABAP", "ABC", "ABC ALGOL", "ABSET", "ABSYS", "ACC", "Accent", "Ace DASL", "ACL2", "Avicsoft", "ACT-III", "Action!", "ActionScript", "Ada", "Adenine", "Agda", "Agilent VEE", "Agora", "AIMMS", "Alef", "ALF", "ALGOL 58", "ALGOL 60", "ALGOL 68", "ALGOL W", "Alice", "Alma-0", "AmbientTalk", "Amiga E", "AMOS", "AMPL", "Apex (Salesforce.com)", "APL", "AppleScript", "Arc", "ARexx", "Argus", "AspectJ", "Assembly language", "ATS", "Ateji PX", "AutoHotkey", "Autocoder", "AutoIt", "AutoLISP / Visual LISP", "Averest", "AWK", "Axum", "Active Server Pages", "ASP.NET", "B", "Babbage", "Bash", "BASIC", "bc", "BCPL", "BeanShell", "Batch (Windows/Dos)", "Bertrand", "BETA", "Bigwig", "Bistro", "BitC", "BLISS", "Blockly", "BlooP", "Blue", "Boo", "Boomerang", "Bourne shell (including bash and ksh)", "BREW", "BPEL", "B", "C--", "C++ – ISO/IEC 14882", "C# – ISO/IEC 23270", "C/AL", "Caché ObjectScript", "C Shell", "Caml", "Cayenne", "CDuce", "Cecil", "Cesil", "Céu", "Ceylon", "CFEngine", "CFML", "Cg", "Ch", "Chapel", "Charity", "Charm", "Chef", "CHILL", "CHIP-8", "chomski", "ChucK", "CICS", "Cilk", "Citrine (programming language)", "CL (IBM)", "Claire", "Clarion", "Clean", "Clipper", "CLIPS", "CLIST", "Clojure", "CLU", "CMS-2", "COBOL – ISO/IEC 1989", "CobolScript – COBOL Scripting language", "Cobra", "CODE", "CoffeeScript", "ColdFusion", "COMAL", "Combined Programming Language (CPL)", "COMIT", "Common Intermediate Language (CIL)", "Common Lisp (also known as CL)", "COMPASS", "Component Pascal", "Constraint Handling Rules (CHR)", "COMTRAN", "Converge", "Cool", "Coq", "Coral 66", "Corn", "CorVision", "COWSEL", "CPL", "CPL", "Cryptol", "csh", "Csound", "CSP", "CUDA", "Curl", "Curry", "Cybil", "Cyclone", "Cython", "Java", "Javascript", "M2001", "M4", "M#", "Machine code", "MAD (Michigan Algorithm Decoder)", "MAD/I", "Magik", "Magma", "make", "Maple", "MAPPER now part of BIS", "MARK-IV now VISION:BUILDER", "Mary", "MASM Microsoft Assembly x86", "MATH-MATIC", "Mathematica", "MATLAB", "Maxima (see also Macsyma)", "Max (Max Msp – Graphical Programming Environment)", "Maya (MEL)", "MDL", "Mercury", "Mesa", "Metafont", "Microcode", "MicroScript", "MIIS", "Milk (programming language)", "MIMIC", "Mirah", "Miranda", "MIVA Script", "ML", "Model 204", "Modelica", "Modula", "Modula-2", "Modula-3", "Mohol", "MOO", "Mortran", "Mouse", "MPD", "Mathcad", "MSIL – deprecated name for CIL", "MSL", "MUMPS", "Mystic Programming L"];
3 |
4 |
5 | // initialize Tagify on the above input node reference
6 | var tagify = new Tagify(inputElm, {
7 | enforceWhitelist: true,
8 | whitelist: inputElm.value.trim().split(/\s*,\s*/) // Array of values. stackoverflow.com/a/43375571/104380
9 | })
10 |
11 |
12 |
13 | // "remove all tags" button event listener
14 | document.querySelector('.tags--removeAllBtn')
15 | .addEventListener('click', tagify.removeAllTags.bind(tagify))
16 |
17 | // Chainable event listeners
18 | tagify.on('add', onAddTag)
19 | .on('remove', onRemoveTag)
20 | .on('input', onInput)
21 | .on('edit', onTagEdit)
22 | .on('invalid', onInvalidTag)
23 | .on('click', onTagClick)
24 | .on('focus', onTagifyFocusBlur)
25 | .on('blur', onTagifyFocusBlur)
26 | .on('dropdown:hide dropdown:show', e => console.log(e.type))
27 | .on('dropdown:select', onDropdownSelect)
28 |
29 | var mockAjax = (function mockAjax(){
30 | var timeout;
31 | return function(duration){
32 | clearTimeout(timeout); // abort last request
33 | return new Promise(function(resolve, reject){
34 | timeout = setTimeout(resolve, duration || 700, whitelist)
35 | })
36 | }
37 | })()
38 |
39 | // tag added callback
40 | function onAddTag(e){
41 | console.log("onAddTag: ", e.detail);
42 | console.log("original input value: ", inputElm.value)
43 | tagify.off('add', onAddTag) // exmaple of removing a custom Tagify event
44 | }
45 |
46 | // tag remvoed callback
47 | function onRemoveTag(e){
48 | console.log("onRemoveTag:", e.detail, "tagify instance value:", tagify.value)
49 | }
50 |
51 | // on character(s) added/removed (user is typing/deleting)
52 | function onInput(e){
53 | console.log("onInput: ", e.detail);
54 | tagify.whitelist = null; // reset current whitelist
55 | tagify.loading(true) // show the loader animation
56 |
57 | // get new whitelist from a delayed mocked request (Promise)
58 | mockAjax()
59 | .then(function(result){
60 | tagify.settings.whitelist = result.concat(tagify.value) // add already-existing tags to the new whitelist array
61 |
62 | tagify
63 | .loading(false)
64 | // render the suggestions dropdown.
65 | .dropdown.show(e.detail.value);
66 | })
67 | .catch(err => tagify.dropdown.hide())
68 | }
69 |
70 | function onTagEdit(e){
71 | console.log("onTagEdit: ", e.detail);
72 | }
73 |
74 | // invalid tag added callback
75 | function onInvalidTag(e){
76 | console.log("onInvalidTag: ", e.detail);
77 | }
78 |
79 | // invalid tag added callback
80 | function onTagClick(e){
81 | console.log(e.detail);
82 | console.log("onTagClick: ", e.detail);
83 | }
84 |
85 | function onTagifyFocusBlur(e){
86 | console.log(e.type, "event fired")
87 | }
88 |
89 | function onDropdownSelect(e){
90 | console.log("onDropdownSelect: ", e.detail)
91 | }
--------------------------------------------------------------------------------
/docs/examples/src/manual-suggestions/manual-suggestions.css:
--------------------------------------------------------------------------------
1 | .customSuggestionsList > div{
2 | max-height: 300px;
3 | min-height: 50px;
4 | border: 2px solid pink;
5 | overflow: auto;
6 | }
7 |
8 | .customSuggestionsList .empty{
9 | color: #999;
10 | font-size: 20px;
11 | text-align: center;
12 | padding: 1em;
13 | }
--------------------------------------------------------------------------------
/docs/examples/src/manual-suggestions/manual-suggestions.html:
--------------------------------------------------------------------------------
1 |
2 | ☝ Add items from below list:
--------------------------------------------------------------------------------
/docs/examples/src/manual-suggestions/manual-suggestions.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[name=tags-manual-suggestions]'),
2 | // init Tagify script on the above inputs
3 | tagify = new Tagify(input, {
4 | whitelist: ["A# .NET", "A# (Axiom)", "A-0 System", "A+", "A++", "ABAP", "ABC", "ABC ALGOL", "ABSET", "ABSYS", "ACC", "Accent", "Ace DASL", "ACL2", "Avicsoft", "ACT-III", "Action!", "ActionScript", "Ada", "Adenine", "Agda", "Agilent VEE", "Agora", "AIMMS", "Alef", "ALF", "ALGOL 58", "ALGOL 60", "ALGOL 68", "ALGOL W", "Alice", "Alma-0", "AmbientTalk", "Amiga E", "AMOS", "AMPL", "Apex (Salesforce.com)", "APL", "AppleScript", "Arc", "ARexx", "Argus", "AspectJ", "Assembly language", "ATS", "Ateji PX", "AutoHotkey", "Autocoder", "AutoIt", "AutoLISP / Visual LISP", "Averest", "AWK", "Axum", "Active Server Pages", "ASP.NET", "B", "Babbage", "Bash", "BASIC", "bc", "BCPL", "BeanShell", "Batch (Windows/Dos)", "Bertrand", "BETA", "Bigwig", "Bistro", "BitC", "BLISS", "Blockly", "BlooP", "Blue", "Boo", "Boomerang", "Bourne shell (including bash and ksh)", "BREW", "BPEL", "B", "C--", "C++ – ISO/IEC 14882", "C# – ISO/IEC 23270", "C/AL", "Caché ObjectScript", "C Shell", "Caml", "Cayenne", "CDuce", "Cecil", "Cesil", "Céu", "Ceylon", "CFEngine", "CFML", "Cg", "Ch", "Chapel", "Charity", "Charm", "Chef", "CHILL", "CHIP-8", "chomski", "ChucK", "CICS", "Cilk", "Citrine (programming language)", "CL (IBM)", "Claire", "Clarion", "Clean", "Clipper", "CLIPS", "CLIST", "Clojure", "CLU", "CMS-2", "COBOL – ISO/IEC 1989", "CobolScript – COBOL Scripting language", "Cobra", "CODE", "CoffeeScript", "ColdFusion", "COMAL", "Combined Programming Language (CPL)", "COMIT", "Common Intermediate Language (CIL)", "Common Lisp (also known as CL)", "COMPASS", "Component Pascal", "Constraint Handling Rules (CHR)", "COMTRAN", "Converge", "Cool", "Coq", "Coral 66", "Corn", "CorVision", "COWSEL", "CPL", "CPL", "Cryptol", "csh", "Csound", "CSP", "CUDA", "Curl", "Curry", "Cybil", "Cyclone", "Cython", "Java", "Javascript", "M2001", "M4", "M#", "Machine code", "MAD (Michigan Algorithm Decoder)", "MAD/I", "Magik", "Magma", "make", "Maple", "MAPPER now part of BIS", "MARK-IV now VISION:BUILDER", "Mary", "MASM Microsoft Assembly x86", "MATH-MATIC", "Mathematica", "MATLAB", "Maxima (see also Macsyma)", "Max (Max Msp – Graphical Programming Environment)", "Maya (MEL)", "MDL", "Mercury", "Mesa", "Metafont", "Microcode", "MicroScript", "MIIS", "Milk (programming language)", "MIMIC", "Mirah", "Miranda", "MIVA Script", "ML", "Model 204", "Modelica", "Modula", "Modula-2", "Modula-3", "Mohol", "MOO", "Mortran", "Mouse", "MPD", "Mathcad", "MSIL – deprecated name for CIL", "MSL", "MUMPS", "Mystic Programming L"],
5 | dropdown: {
6 | position: "manual",
7 | maxItems: Infinity,
8 | enabled: 0,
9 | classname: "customSuggestionsList"
10 | },
11 | templates: {
12 | dropdownItemNoMatch() {
13 | return `Nothing Found
`;
14 | }
15 | },
16 | enforceWhitelist: true
17 | })
18 |
19 | tagify.on("dropdown:show", onSuggestionsListUpdate)
20 | .on("dropdown:hide", onSuggestionsListHide)
21 | .on('dropdown:scroll', onDropdownScroll)
22 |
23 | renderSuggestionsList() // defined down below
24 |
25 | // ES2015 argument destructuring
26 | function onSuggestionsListUpdate({ detail: suggestionsElm }) {
27 | console.log(suggestionsElm)
28 | }
29 |
30 | function onSuggestionsListHide() {
31 | console.log("hide dropdown")
32 | }
33 |
34 | function onDropdownScroll(e) {
35 | console.log(e.detail)
36 | }
37 |
38 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement
39 | function renderSuggestionsList() {
40 | tagify.dropdown.show() // load the list
41 | tagify.DOM.scope.parentNode.appendChild(tagify.DOM.dropdown)
42 | }
--------------------------------------------------------------------------------
/docs/examples/src/mix/mix.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/mix/mix.js:
--------------------------------------------------------------------------------
1 | // Define two types of whitelists, each used for the dropdown suggestions menu,
2 | // depending on the prefix pattern typed (@/#). See settings below.
3 | var whitelist_1 = [
4 | { value: 100, text: 'kenny', title: 'Kenny McCormick' },
5 | { value: 200, text: 'cartman', title: 'Eric Cartman' },
6 | { value: 300, text: 'kyle', title: 'Kyle Broflovski' },
7 | { value: 400, text: 'token', title: 'Token Black' },
8 | { value: 500, text: 'jimmy', title: 'Jimmy Valmer' },
9 | { value: 600, text: 'butters', title: 'Butters Stotch' },
10 | { value: 700, text: 'stan', title: 'Stan Marsh' },
11 | { value: 800, text: 'randy', title: 'Randy Marsh' },
12 | { value: 900, text: 'Mr. Garrison', title: 'POTUS' },
13 | { value: 1000, text: 'Mr. Mackey', title: "M'Kay" }
14 | ]
15 |
16 | // Second whitelist, which is shown only when starting to type "#".
17 | // Below whitelist is the simplest possible format.
18 | var whitelist_2 = ['Homer simpson', 'Marge simpson', 'Bart', 'Lisa', 'Maggie', 'Mr. Burns', 'Ned', 'Milhouse', 'Moe'];
19 |
20 |
21 | // initialize Tagify
22 | var input = document.querySelector('[name=mix]'),
23 | // init Tagify script on the above inputs
24 | tagify = new Tagify(input, {
25 | // mixTagsInterpolator: ["{{", "}}"],
26 | mode: 'mix', // <-- Enable mixed-content
27 | pattern: /@|#/, // <-- Text starting with @ or # (if single, String can be used here)
28 | tagTextProp: 'text', // <-- the default property (from whitelist item) for the text to be rendered in a tag element.
29 | // Array for initial interpolation, which allows only these tags to be used
30 | whitelist: whitelist_1.concat(whitelist_2).map(function(item){
31 | return typeof item == 'string' ? {value:item} : item
32 | }),
33 |
34 | // custom validation - no special characters
35 | validate(data){
36 | return !/[^a-zA-Z0-9 ]/.test(data.value)
37 | },
38 |
39 | dropdown : {
40 | enabled: 1,
41 | position: 'text', // <-- render the suggestions list next to the typed text ("caret")
42 | mapValueTo: 'text', // <-- similar to above "tagTextProp" setting, but for the dropdown items
43 | highlightFirst: true // automatically highlights first sugegstion item in the dropdown
44 | },
45 | callbacks: {
46 | add: console.log, // callback when adding a tag
47 | remove: console.log // callback when removing a tag
48 | }
49 | })
50 |
51 |
52 | // A good place to pull server suggestion list accoring to the prefix/value
53 | tagify.on('input', function(e){
54 | var prefix = e.detail.prefix;
55 |
56 | // first, clean the whitlist array, because the below code, while not, might be async,
57 | // therefore it should be up to you to decide WHEN to render the suggestions dropdown
58 | // tagify.settings.whitelist.length = 0;
59 |
60 | if( prefix ){
61 | if( prefix == '@' )
62 | tagify.whitelist = whitelist_1;
63 |
64 | if( prefix == '#' )
65 | tagify.whitelist = whitelist_2;
66 |
67 | if( e.detail.value.length > 1 )
68 | tagify.dropdown.show(e.detail.value);
69 | }
70 |
71 | console.log( tagify.value )
72 | console.log('mix-mode "input" event value: ', e.detail)
73 | })
74 |
75 | tagify.on('add', function(e){
76 | console.log(e)
77 | })
--------------------------------------------------------------------------------
/docs/examples/src/mode-select/mode-select.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yairEO/tagify/3c49093fba26029602a5dc56676af3504e74dd3a/docs/examples/src/mode-select/mode-select.css
--------------------------------------------------------------------------------
/docs/examples/src/mode-select/mode-select.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/mode-select/mode-select.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[name=mode-select]'),
2 | tagify = new Tagify(input, {
3 | enforceWhitelist: true,
4 | mode : "select",
5 | whitelist: ["first option", "second option", "third option"],
6 | blacklist: ['foo', 'bar'],
7 | })
8 |
9 | // bind events
10 | tagify.on('add', onAddTag)
11 | tagify.DOM.input.addEventListener('focus', onSelectFocus)
12 |
13 | function onAddTag(e){
14 | console.log(e.detail)
15 | }
16 |
17 | function onSelectFocus(e){
18 | console.log(e)
19 | }
--------------------------------------------------------------------------------
/docs/examples/src/outside-of-the-box/outside-of-the-box.css:
--------------------------------------------------------------------------------
1 | .tagify--outside {
2 | border: 0;
3 | }
4 |
5 | .tagify--outside .tagify__input {
6 | order: -1;
7 | flex: 100%;
8 | border: 1px solid var(--tags-border-color);
9 | margin-bottom: 1em;
10 | transition: .1s;
11 | }
12 |
13 | .tagify--outside .tagify__input:hover {
14 | border-color: var(--tags-hover-border-color);
15 | }
16 |
17 | .tagify--outside.tagify--focus .tagify__input {
18 | transition: 0s;
19 | border-color: var(--tags-focus-border-color);
20 | }
--------------------------------------------------------------------------------
/docs/examples/src/outside-of-the-box/outside-of-the-box.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/outside-of-the-box/outside-of-the-box.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[name=tags-outside]')
2 |
3 | var tagify = new Tagify(input, {
4 | whitelist: ['foo', 'bar', 'baz'],
5 | focusable: false,
6 | dropdown: {
7 | position: 'input',
8 | enabled: 0 // always opens dropdown when input gets focus
9 | }
10 | })
--------------------------------------------------------------------------------
/docs/examples/src/readonly-mixed/readonly-mixed.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yairEO/tagify/3c49093fba26029602a5dc56676af3504e74dd3a/docs/examples/src/readonly-mixed/readonly-mixed.css
--------------------------------------------------------------------------------
/docs/examples/src/readonly-mixed/readonly-mixed.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/readonly-mixed/readonly-mixed.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[name=readonly-mix]'),
2 | tagify = new Tagify(input)
--------------------------------------------------------------------------------
/docs/examples/src/readonly/readonly.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/readonly/readonly.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('input[readonly]'),
2 | tagify = new Tagify(input);
--------------------------------------------------------------------------------
/docs/examples/src/textarea/textarea.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/examples/src/textarea/textarea.js:
--------------------------------------------------------------------------------
1 | var input = document.querySelector('textarea[name=tags2]'),
2 | tagify = new Tagify(input, {
3 | enforceWhitelist : true,
4 | delimiters : null,
5 | whitelist : ["The Shawshank Redemption", "The Godfather", "The Godfather: Part II", "The Dark Knight", "12 Angry Men", "Schindler's List", "Pulp Fiction", "The Lord of the Rings: The Return of the King", "The Good, the Bad and the Ugly", "Fight Club", "The Lord of the Rings: The Fellowship of the Ring", "Star Wars: Episode V - The Empire Strikes Back", "Forrest Gump", "Inception", "The Lord of the Rings: The Two Towers", "One Flew Over the Cuckoo's Nest", "Goodfellas", "The Matrix", "Seven Samurai", "Star Wars: Episode IV - A New Hope", "City of God", "Se7en", "The Silence of the Lambs", "It's a Wonderful Life", "The Usual Suspects", "Life Is Beautiful", "Léon: The Professional", "Spirited Away", "Saving Private Ryan", "La La Land", "Once Upon a Time in the West", "American History X", "Interstellar", "Casablanca", "Psycho", "City Lights", "The Green Mile", "Raiders of the Lost Ark", "The Intouchables", "Modern Times", "Rear Window", "The Pianist", "The Departed", "Terminator 2: Judgment Day", "Back to the Future", "Whiplash", "Gladiator", "Memento", "Apocalypse Now", "The Prestige", "The Lion King", "Alien", "Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb", "Sunset Boulevard", "The Great Dictator", "Cinema Paradiso", "The Lives of Others", "Paths of Glory", "Grave of the Fireflies", "Django Unchained", "The Shining", "WALL·E", "American Beauty", "The Dark Knight Rises", "Princess Mononoke", "Aliens", "Oldboy", "Once Upon a Time in America", "Citizen Kane", "Das Boot", "Witness for the Prosecution", "North by Northwest", "Vertigo", "Star Wars: Episode VI - Return of the Jedi", "Reservoir Dogs", "M", "Braveheart", "Amélie", "Requiem for a Dream", "A Clockwork Orange", "Taxi Driver", "Lawrence of Arabia", "Like Stars on Earth", "Double Indemnity", "To Kill a Mockingbird", "Eternal Sunshine of the Spotless Mind", "Toy Story 3", "Amadeus", "My Father and My Son", "Full Metal Jacket", "The Sting", "2001: A Space Odyssey", "Singin' in the Rain", "Bicycle Thieves", "Toy Story", "Dangal", "The Kid", "Inglourious Basterds", "Snatch", "Monty Python and the Holy Grail", "Hacksaw Ridge", "3 Idiots", "L.A. Confidential", "For a Few Dollars More", "Scarface", "Rashomon", "The Apartment", "The Hunt", "Good Will Hunting", "Indiana Jones and the Last Crusade", "A Separation", "Metropolis", "Yojimbo", "All About Eve", "Batman Begins", "Up", "Some Like It Hot", "The Treasure of the Sierra Madre", "Unforgiven", "Downfall", "Raging Bull", "The Third Man", "Die Hard", "Children of Heaven", "The Great Escape", "Heat", "Chinatown", "Inside Out", "Pan's Labyrinth", "Ikiru", "My Neighbor Totoro", "On the Waterfront", "Room", "Ran", "The Gold Rush", "The Secret in Their Eyes", "The Bridge on the River Kwai", "Blade Runner", "Mr. Smith Goes to Washington", "The Seventh Seal", "Howl's Moving Castle", "Lock, Stock and Two Smoking Barrels", "Judgment at Nuremberg", "Casino", "The Bandit", "Incendies", "A Beautiful Mind", "A Wednesday", "The General", "The Elephant Man", "Wild Strawberries", "Arrival", "V for Vendetta", "Warrior", "The Wolf of Wall Street", "Manchester by the Sea", "Sunrise", "The Passion of Joan of Arc", "Gran Torino", "Rang De Basanti", "Trainspotting", "Dial M for Murder", "The Big Lebowski", "The Deer Hunter", "Tokyo Story", "Gone with the Wind", "Fargo", "Finding Nemo", "The Sixth Sense", "The Thing", "Hera Pheri", "Cool Hand Luke", "Andaz Apna Apna", "Rebecca", "No Country for Old Men", "How to Train Your Dragon", "Munna Bhai M.B.B.S.", "Sholay", "Kill Bill: Vol. 1", "Into the Wild", "Mary and Max", "Gone Girl", "There Will Be Blood", "Come and See", "It Happened One Night", "Life of Brian", "Rush", "Hotel Rwanda", "Platoon", "Shutter Island", "Network", "The Wages of Fear", "Stand by Me", "Wild Tales", "In the Name of the Father", "Spotlight", "Star Wars: The Force Awakens", "The Nights of Cabiria", "The 400 Blows", "Butch Cassidy and the Sundance Kid", "Mad Max: Fury Road", "The Maltese Falcon", "12 Years a Slave", "Ben-Hur", "The Grand Budapest Hotel", "Persona", "Million Dollar Baby", "Amores Perros", "Jurassic Park", "The Princess Bride", "Hachi: A Dog's Tale", "Memories of Murder", "Stalker", "Nausicaä of the Valley of the Wind", "Drishyam", "The Truman Show", "The Grapes of Wrath", "Before Sunrise", "Touch of Evil", "Annie Hall", "The Message", "Rocky", "Gandhi", "Harry Potter and the Deathly Hallows: Part 2", "The Bourne Ultimatum", "Diabolique", "Donnie Darko", "Monsters, Inc.", "Prisoners", "8½", "The Terminator", "The Wizard of Oz", "Catch Me If You Can", "Groundhog Day", "Twelve Monkeys", "Zootopia", "La Haine", "Barry Lyndon", "Jaws", "The Best Years of Our Lives", "Infernal Affairs", "Udaan", "The Battle of Algiers", "Strangers on a Train", "Dog Day Afternoon", "Sin City", "Kind Hearts and Coronets", "Gangs of Wasseypur", "The Help"],
6 | callbacks : {
7 | add : console.log, // callback when adding a tag
8 | remove : console.log // callback when removing a tag
9 | }
10 | })
--------------------------------------------------------------------------------
/docs/examples/src/users-list/users-list.css:
--------------------------------------------------------------------------------
1 | /* Suggestions items */
2 | :root {
3 | --tagify-dd-item-pad: .5em .7em;
4 | }
5 |
6 | .tagify__dropdown.users-list .tagify__dropdown__item {
7 | display: grid;
8 | grid-template-columns: auto 1fr;
9 | gap: 0 1em;
10 | grid-template-areas: "avatar name"
11 | "avatar email";
12 | }
13 |
14 | .tagify__dropdown.users-list header.tagify__dropdown__item {
15 | grid-template-areas: "add remove-tags"
16 | "remaning .";
17 | }
18 |
19 | .tagify__dropdown.users-list .tagify__dropdown__item:hover .tagify__dropdown__item__avatar-wrap {
20 | transform: scale(1.2);
21 | }
22 |
23 | .tagify__dropdown.users-list .tagify__dropdown__item__avatar-wrap {
24 | grid-area: avatar;
25 | width: 36px;
26 | height: 36px;
27 | border-radius: 50%;
28 | overflow: hidden;
29 | background: #EEE;
30 | transition: .1s ease-out;
31 | }
32 |
33 | .tagify__dropdown.users-list img {
34 | width: 100%;
35 | vertical-align: top;
36 | }
37 |
38 | .tagify__dropdown.users-list header.tagify__dropdown__item>div,
39 | .tagify__dropdown.users-list .tagify__dropdown__item strong {
40 | grid-area: name;
41 | width: 100%;
42 | align-self: center;
43 | }
44 |
45 | .tagify__dropdown.users-list span {
46 | grid-area: email;
47 | width: 100%;
48 | font-size: .9em;
49 | opacity: .6;
50 | }
51 |
52 | .tagify__dropdown.users-list .tagify__dropdown__item__addAll {
53 | border-bottom: 1px solid #DDD;
54 | gap: 0;
55 | }
56 |
57 | .tagify__dropdown.users-list .remove-all-tags {
58 | grid-area: remove-tags;
59 | justify-self: self-end;
60 | font-size: .8em;
61 | padding: .2em .3em;
62 | border-radius: 3px;
63 | user-select: none;
64 | }
65 |
66 | .tagify__dropdown.users-list .remove-all-tags:hover {
67 | color: white;
68 | background: salmon;
69 | }
70 |
71 |
72 | /* Tags items */
73 | .users-list .tagify__tag {
74 | white-space: nowrap;
75 | }
76 |
77 | .users-list .tagify__tag img {
78 | width: 100%;
79 | vertical-align: top;
80 | pointer-events: none;
81 | }
82 |
83 |
84 | .users-list .tagify__tag:hover .tagify__tag__avatar-wrap {
85 | transform: scale(1.6) translateX(-10%);
86 | }
87 |
88 | .users-list .tagify__tag .tagify__tag__avatar-wrap {
89 | width: 16px;
90 | height: 16px;
91 | white-space: normal;
92 | border-radius: 50%;
93 | background: silver;
94 | margin-right: 5px;
95 | transition: .12s ease-out;
96 | }
97 |
98 | .users-list .tagify__dropdown__itemsGroup:empty {
99 | display: none;
100 | }
101 |
102 | .users-list .tagify__dropdown__itemsGroup::before {
103 | content: attr(data-title);
104 | display: inline-block;
105 | font-size: .9em;
106 | padding: 4px 6px;
107 | margin: var(--tagify-dd-item-pad);
108 | font-style: italic;
109 | border-radius: 4px;
110 | background: #00ce8d;
111 | color: white;
112 | font-weight: 600;
113 | }
114 |
115 | .users-list .tagify__dropdown__itemsGroup:not(:first-of-type) {
116 | border-top: 1px solid #DDD;
117 | }
--------------------------------------------------------------------------------
/docs/examples/src/users-list/users-list.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/homepage/scripts.html:
--------------------------------------------------------------------------------
1 |
14 |
15 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/docs/homepage/section.njk:
--------------------------------------------------------------------------------
1 | {% macro section(name, codepen, opts) %}
2 | {% set assetPath = 'examples/src/' + name + '/' + name %}
3 |
4 |
5 |
26 |
27 |
35 |
36 |
41 |
42 | {% endmacro %}
--------------------------------------------------------------------------------
/docs/mix2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yairEO/tagify/3c49093fba26029602a5dc56676af3504e74dd3a/docs/mix2.gif
--------------------------------------------------------------------------------
/docs/mix3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yairEO/tagify/3c49093fba26029602a5dc56676af3504e74dd3a/docs/mix3.gif
--------------------------------------------------------------------------------
/docs/suggestions-list.apng:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yairEO/tagify/3c49093fba26029602a5dc56676af3504e74dd3a/docs/suggestions-list.apng
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@yaireo/tagify",
3 | "version": "4.35.1",
4 | "homepage": "https://github.com/yairEO/tagify",
5 | "description": "lightweight, efficient Tags input component in Vanilla JS / React / Angular [super customizable, tiny size & top performance]",
6 | "keywords": [
7 | "tags",
8 | "tagging",
9 | "component",
10 | "tag",
11 | "ui"
12 | ],
13 | "license": "MIT",
14 | "browserslist": [
15 | ">1%",
16 | "not dead",
17 | "not ie < 11",
18 | "not IE_Mob 11",
19 | "not op_mini all"
20 | ],
21 | "engines": {
22 | "npm": ">=9.0.0",
23 | "node": ">=16.15.0"
24 | },
25 | "np": {
26 | "yarn": false,
27 | "yolo": true
28 | },
29 | "scripts": {
30 | "start": "gulp --dev",
31 | "build": "gulp",
32 | "version": "gulp build && git add .",
33 | "prepublishOnly": "pjv",
34 | "test": "pnpm exec playwright test",
35 | "serve": "npx http-server -o index.html -c-1"
36 | },
37 | "jest": {
38 | "preset": "jest-puppeteer"
39 | },
40 | "author": "Yair Even-Or ",
41 | "main": "./dist/tagify.js",
42 | "exports": {
43 | ".": {
44 | "import": "./dist/tagify.esm.js",
45 | "require": "./dist/tagify.js"
46 | },
47 | "./react": "./src/react.tagify",
48 | "./react.tagify": "./src/react.tagify",
49 | "./dist/react.tagify": "./src/react.tagify",
50 | "./dist/react.tagify.jsx": "./src/react.tagify",
51 | "./dist/tagify.min.js": "./dist/tagify.js",
52 | "./dist/tagify.min": "./dist/tagify.js",
53 | "./dist/tagify.esm.js": "./dist/tagify.esm.js",
54 | "./dist/tagify.polyfills.min.js": "./dist/tagify.polyfills.min.js",
55 | "./dist/tagify.vue": "./dist/tagify.vue",
56 | "./vue": "./dist/tagify.vue",
57 | "./dist/tagify.css": "./dist/tagify.css",
58 | "./src/tagify.scss": "./src/tagify.scss"
59 | },
60 | "repository": {
61 | "type": "git",
62 | "url": "git+https://github.com/yairEO/tagify.git"
63 | },
64 | "bugs": {
65 | "url": "https://github.com/yaireo/tagify/issues"
66 | },
67 | "files": [
68 | "/dist",
69 | "/src"
70 | ],
71 | "peerDependencies": {
72 | "prop-types": ">15.5.7",
73 | "react": "*",
74 | "react-dom": "*"
75 | },
76 | "devDependencies": {
77 | "@playwright/test": "^1.43.1",
78 | "@rollup/plugin-terser": "^0.4.4",
79 | "@rollup/stream": "^3.0.1",
80 | "@swc/core": "^1.4.12",
81 | "@types/node": "^20.12.8",
82 | "beepbeep": "^1.3.0",
83 | "gulp": "^5.0.0",
84 | "gulp-autoprefixer": "^8.0.0",
85 | "gulp-bump": "^3.2.0",
86 | "gulp-cached": "^1.1.1",
87 | "gulp-clean-css": "^4.3.0",
88 | "gulp-combine-mq": "^0.4.0",
89 | "gulp-concat": "^2.6.1",
90 | "gulp-css-globbing": "^0.2.2",
91 | "gulp-header-comment": "^0.10.0",
92 | "gulp-insert": "^0.5.0",
93 | "gulp-load-plugins": "^2.0.8",
94 | "gulp-nunjucks": "^6.0.0",
95 | "gulp-rename": "^2.0.0",
96 | "gulp-replace": "^1.1.4",
97 | "gulp-sass": "^5.1.0",
98 | "gulp-sourcemaps": "^3.0.0",
99 | "gulp-streamify": "^1.0.2",
100 | "gulp-swc": "^2.0.0",
101 | "gulp-tag-version": "^1.3.1",
102 | "gulp-tap": "^2.0.0",
103 | "gulp-terser": "^2.1.0",
104 | "gulp-umd": "^2.0.0",
105 | "gulp-util": "^3.0.8",
106 | "gulp-watch": "^5.0.1",
107 | "path": "^0.12.7",
108 | "rollup": "2.79.2",
109 | "rollup-plugin-banner2": "^1.2.3",
110 | "rollup-plugin-swc3": "^0.11.0",
111 | "run-sequence": "^2.2.1",
112 | "sass": "^1.74.1",
113 | "semver": "^7.6.0",
114 | "vinyl-buffer": "^1.0.1",
115 | "vinyl-source-stream": "^2.0.0"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/playwright.config.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | const { defineConfig, devices } = require('@playwright/test');
3 |
4 | /**
5 | * Read environment variables from file.
6 | * https://github.com/motdotla/dotenv
7 | */
8 | // require('dotenv').config();
9 |
10 | /**
11 | * @see https://playwright.dev/docs/test-configuration
12 | */
13 | module.exports = defineConfig({
14 | testDir: './tests',
15 | /* Run tests in files in parallel */
16 | fullyParallel: true,
17 | /* Fail the build on CI if you accidentally left test.only in the source code. */
18 | forbidOnly: !!process.env.CI,
19 | /* Retry on CI only */
20 | retries: process.env.CI ? 2 : 0,
21 | /* Opt out of parallel tests on CI. */
22 | workers: process.env.CI ? 1 : undefined,
23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
24 | reporter: 'html',
25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
26 | use: {
27 | /* Base URL to use in actions like `await page.goto('/')`. */
28 | // baseURL: 'http://127.0.0.1:3000',
29 |
30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
31 | trace: 'on-first-retry',
32 | },
33 |
34 | /* Configure projects for major browsers */
35 | projects: [
36 | {
37 | name: 'chromium',
38 | use: { ...devices['Desktop Chrome'] },
39 | },
40 |
41 | // {
42 | // name: 'firefox',
43 | // use: { ...devices['Desktop Firefox'] },
44 | // },
45 |
46 | // {
47 | // name: 'webkit',
48 | // use: { ...devices['Desktop Safari'] },
49 | // },
50 |
51 | /* Test against mobile viewports. */
52 | // {
53 | // name: 'Mobile Chrome',
54 | // use: { ...devices['Pixel 5'] },
55 | // },
56 | // {
57 | // name: 'Mobile Safari',
58 | // use: { ...devices['iPhone 12'] },
59 | // },
60 |
61 | /* Test against branded browsers. */
62 | // {
63 | // name: 'Microsoft Edge',
64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
65 | // },
66 | // {
67 | // name: 'Google Chrome',
68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
69 | // },
70 | ],
71 |
72 | /* Run your local dev server before starting the tests */
73 | // webServer: {
74 | // command: 'npm run start',
75 | // url: 'http://127.0.0.1:3000',
76 | // reuseExistingServer: !process.env.CI,
77 | // },
78 | });
79 |
80 |
--------------------------------------------------------------------------------
/roadmap.md:
--------------------------------------------------------------------------------
1 | - [x] add SCSS variable for input color (not tag color)
2 | - [x] Make demo page *mobile-friendly* using *media-queries*
3 | - [x] dragable sortable tags
4 | - [ ] Make (regular-mode) tagify accessible by keyboard by navigating left/right arrow and able to delete tags
5 | - [ ] when "addTagOnBlur" is set to false and there is a text in the input and Tagify gets focus, the caret is not placed at the end of the input.
6 | (need to check if the "focus" event was fired programatically and if it, place the caret at the end)
7 | - [ ] mix-mode: maybe convert `settings.pattern` to always be a regex. Currently it may be a String (this fails "validateTag" method)
8 | - [ ] mix-mode: add "prefix" to tags, so when double-clicking to edit, it will show it
9 | - [ ] maybe trigger the "invalid" event also for edited tags. need to think when exactly. probbaly not on "input" event, it's too much
10 | - [ ] use DOM mutation to detect changes and update "this.value" automatically
11 | - [ ] add examples of added tag CSS effects
12 | - [ ] allow templates to return DOM nodes and not just Strings
13 | - [ ] Make *readonly* tags work in *mix-mode*
14 |
--------------------------------------------------------------------------------
/src/parts/EventDispatcher.js:
--------------------------------------------------------------------------------
1 | import { extend, logger } from './helpers'
2 |
3 | export default function EventDispatcher( instance ){
4 | // Create a DOM EventTarget object
5 | var target = document.createTextNode(''),
6 | // keep track of all binded events & their callbacks to be able to completely remove all listeners of a speicific type
7 | callbacksPerType = {}
8 |
9 | function addRemove(op, events, cb){
10 | if( cb )
11 | events.split(/\s+/g).forEach(ev => target[op + 'EventListener'].call(target, ev, cb))
12 | }
13 |
14 | // Pass EventTarget interface calls to DOM EventTarget object
15 | return {
16 | // unbinds all events
17 | removeAllCustomListeners(){
18 | Object.entries(callbacksPerType).forEach(([ev, cbArr]) => {
19 | cbArr.forEach(cb => addRemove('remove', ev, cb))
20 | })
21 |
22 | callbacksPerType = {}
23 | },
24 |
25 | off(events, cb){
26 | if( events ) {
27 | if( cb )
28 | addRemove('remove', events, cb)
29 | else
30 | // if `cb` argument was not specified then remove all listeners for the given event(s) types
31 | events.split(/\s+/g).forEach(ev => {
32 | callbacksPerType[ev]?.forEach(cb => addRemove('remove', ev, cb))
33 | delete callbacksPerType[ev]
34 | })
35 | }
36 |
37 | return this
38 | },
39 |
40 | on(events, cb){
41 | if(cb && typeof cb == 'function') {
42 | //track events callbacks to be able to remove them altogehter
43 | events.split(/\s+/g).forEach(ev => {
44 | if (Array.isArray(callbacksPerType[ev]) )
45 | callbacksPerType[ev].push(cb)
46 | else
47 | callbacksPerType[ev] = [cb]
48 | })
49 |
50 | addRemove('add', events, cb)
51 | }
52 |
53 | return this
54 | },
55 |
56 | trigger(eventName, data, opts){
57 | var e;
58 |
59 | opts = opts || {
60 | cloneData:true
61 | }
62 |
63 | if( !eventName ) return;
64 |
65 | if( instance.settings.isJQueryPlugin ){
66 | if( eventName == 'remove' ) eventName = 'removeTag' // issue #222
67 | jQuery(instance.DOM.originalInput).triggerHandler(eventName, [data])
68 | }
69 | else{
70 | try {
71 | var eventData = typeof data === 'object'
72 | ? data
73 | : {value:data};
74 |
75 | eventData = opts.cloneData ? extend({}, eventData) : eventData
76 | eventData.tagify = this
77 |
78 | if( data.event )
79 | eventData.event = this.cloneEvent(data.event)
80 |
81 | // TODO: move the below to the "extend" function
82 | if( data instanceof Object )
83 | for( var prop in data )
84 | if(data[prop] instanceof HTMLElement)
85 | eventData[prop] = data[prop]
86 |
87 | e = new CustomEvent(eventName, {"detail":eventData})
88 | }
89 | catch(err){ logger.warn(err) }
90 |
91 | target.dispatchEvent(e);
92 | }
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/parts/constants.js:
--------------------------------------------------------------------------------
1 | export var ZERO_WIDTH_CHAR = '\u200B';
2 | export var ZERO_WIDTH_UNICODE_CHAR = ``
--------------------------------------------------------------------------------
/src/parts/defaults.js:
--------------------------------------------------------------------------------
1 | export default {
2 | delimiters : ",", // [RegEx] split tags by any of these delimiters ("null" to cancel) Example: ",| |."
3 | pattern : null, // RegEx pattern to validate input by. Ex: /[1-9]/
4 | tagTextProp : 'value', // tag data Object property which will be displayed as the tag's text
5 | maxTags : Infinity, // Maximum number of tags
6 | callbacks : {}, // Exposed callbacks object to be triggered on certain events
7 | addTagOnBlur : true, // automatically adds the text which was inputed as a tag when blur event happens
8 | addTagOn : ['blur', 'tab', 'enter'], // if the tagify field (in a normal mode) has any non-tag input in it, convert it to a tag on any of these events: blur away from the field, click "tab"/"enter" key
9 | onChangeAfterBlur : true, // By default, the native way of inputs' onChange events is kept, and it only fires when the field is blured.
10 | duplicates : false, // "true" - allow duplicate tags
11 | whitelist : [], // Array of tags to suggest as the user types (can be used along with "enforceWhitelist" setting)
12 | blacklist : [], // A list of non-allowed tags
13 | enforceWhitelist : false, // Only allow tags from the whitelist
14 | userInput : true, // disable manually typing/pasting/editing tags (tags may only be added from the whitelist)
15 | focusable : true, // Allow the component as a whole to recieve focus. There are implementations of Tagify without external border and so 'focusability' causes unwanted behaviour
16 | focusInputOnRemove : true, // Refocus the input when a tag is removed
17 | keepInvalidTags : false, // if true, do not remove tags which did not pass validation
18 | createInvalidTags : true, // if false, do not create invalid tags from invalid user input
19 | mixTagsAllowedAfter : /,|\.|\:|\s/, // RegEx - Define conditions in which mix-tags content allows a tag to be added after
20 | mixTagsInterpolator : ['[[', ']]'], // Interpolation for mix mode. Everything between these will become a tag, if is a valid Object
21 | backspace : true, // false / true / "edit"
22 | skipInvalid : false, // If `true`, do not add invalid, temporary, tags before automatically removing them
23 | pasteAsTags : true, // automatically converts pasted text into tags. if "false", allows for further text editing
24 |
25 | editTags : {
26 | clicks : 2, // clicks to enter "edit-mode": 1 for single click. any other value is considered as double-click
27 | keepInvalid : true // keeps invalid edits as-is until `esc` is pressed while in focus
28 | }, // 1 or 2 clicks to edit a tag. false/null for not allowing editing
29 | transformTag : ()=>{}, // Takes a tag input string as argument and returns a transformed value
30 | trim : true, // whether or not the value provided should be trimmed, before being added as a tag
31 | a11y: {
32 | focusableTags: false
33 | },
34 |
35 | mixMode: {
36 | insertAfterTag : '\u00A0', // String/Node to inject after a tag has been added (see #588)
37 | },
38 |
39 | autoComplete: {
40 | enabled: true, // Tries to suggest the input's value while typing (match from whitelist) by adding the rest of term as grayed-out text
41 | rightKey: false, // If `true`, when Right key is pressed, use the suggested value to create a tag, else just auto-completes the input. in mixed-mode this is set to "true"
42 | tabKey: false, // If 'true`, pressing `tab` key would only auto-complete but not also convert to a tag (like `rightKey` does).
43 | },
44 |
45 | classNames: {
46 | namespace : 'tagify',
47 | mixMode : 'tagify--mix',
48 | selectMode : 'tagify--select',
49 | input : 'tagify__input',
50 | focus : 'tagify--focus',
51 | tagNoAnimation : 'tagify--noAnim',
52 | tagInvalid : 'tagify--invalid',
53 | tagNotAllowed : 'tagify--notAllowed',
54 | scopeLoading : 'tagify--loading',
55 | hasMaxTags : 'tagify--hasMaxTags',
56 | hasNoTags : 'tagify--noTags',
57 | empty : 'tagify--empty',
58 | inputInvalid : 'tagify__input--invalid',
59 | dropdown : 'tagify__dropdown',
60 | dropdownWrapper : 'tagify__dropdown__wrapper',
61 | dropdownHeader : 'tagify__dropdown__header',
62 | dropdownFooter : 'tagify__dropdown__footer',
63 | dropdownItem : 'tagify__dropdown__item',
64 | dropdownItemActive : 'tagify__dropdown__item--active',
65 | dropdownItemHidden : 'tagify__dropdown__item--hidden',
66 | dropdownItemSelected : 'tagify__dropdown__item--selected',
67 | dropdownInital : 'tagify__dropdown--initial',
68 | tag : 'tagify__tag',
69 | tagText : 'tagify__tag-text',
70 | tagX : 'tagify__tag__removeBtn',
71 | tagLoading : 'tagify__tag--loading',
72 | tagEditing : 'tagify__tag--editable',
73 | tagFlash : 'tagify__tag--flash',
74 | tagHide : 'tagify__tag--hide',
75 |
76 | },
77 |
78 | dropdown: {
79 | classname : '',
80 | enabled : 2, // minimum input characters to be typed for the suggestions dropdown to show
81 | maxItems : 10,
82 | searchKeys : ["value", "searchBy"],
83 | fuzzySearch : true,
84 | caseSensitive : false,
85 | accentedSearch : true,
86 | includeSelectedTags: false, // Should the suggestions list Include already-selected tags (after filtering)
87 | escapeHTML : true, // escapes HTML entities in the suggestions' rendered text
88 | highlightFirst : true, // highlights first-matched item in the list
89 | closeOnSelect : true, // closes the dropdown after selecting an item, if `enabled:0` (which means always show dropdown)
90 | clearOnSelect : true, // after selecting a suggetion, should the typed text input remain or be cleared
91 | position : 'all', // 'manual' / 'text' / 'all'
92 | appendTarget : null // defaults to document.body once DOM has been loaded
93 | },
94 |
95 | hooks: {
96 | beforeRemoveTag: () => Promise.resolve(),
97 | beforePaste: () => Promise.resolve(),
98 | suggestionClick: () => Promise.resolve(),
99 | beforeKeyDown: () => Promise.resolve(),
100 | }
101 | }
--------------------------------------------------------------------------------
/src/parts/persist.js:
--------------------------------------------------------------------------------
1 | const VERSION = 1; // current version of persisted data. if code change breaks persisted data, verison number should be bumped.
2 | const STORE_KEY = '@yaireo/tagify/'
3 |
4 | export const getPersistedData = id => key => {
5 | if( !id ) return;
6 |
7 | // if "persist" is "false", do not save to localstorage
8 | let customKey = '/'+key,
9 | persistedData,
10 | currentStorageVersion = localStorage?.getItem(STORE_KEY + id + '/v')
11 |
12 | if( currentStorageVersion === VERSION){
13 | try{ persistedData = JSON.parse(localStorage[STORE_KEY + id + customKey]) }
14 | catch(err){}
15 | }
16 |
17 | return persistedData
18 | }
19 |
20 | export const setPersistedData = id => {
21 | if( !id ) return () => {};
22 |
23 | // for storage invalidation
24 | localStorage?.setItem(STORE_KEY + id + '/v', VERSION)
25 |
26 | return (data, key) => {
27 | let customKey = '/'+key,
28 | persistedData = JSON.stringify(data)
29 |
30 | if( data && key ){
31 | localStorage?.setItem(STORE_KEY + id + customKey, persistedData)
32 | dispatchEvent( new Event('storage') )
33 | }
34 | }
35 | }
36 |
37 | export const clearPersistedData = id => key => {
38 | const base = STORE_KEY + '/' + id + '/';
39 |
40 | // delete specific key in the storage
41 | if( key )
42 | localStorage.removeItem(base + key)
43 |
44 | // delete all keys in the storage with a specific tagify id
45 | else {
46 | for(let k in localStorage)
47 | if( k.includes(base) )
48 | localStorage.removeItem(k)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/parts/templates.js:
--------------------------------------------------------------------------------
1 | import {ZERO_WIDTH_UNICODE_CHAR} from './constants'
2 |
3 | export default {
4 | /**
5 | *
6 | * @param {DOM Object} input Original input DOm element
7 | * @param {Object} settings Tagify instance settings Object
8 | */
9 | wrapper(input, _s){
10 | return `
16 | ${this.settings.templates.input.call(this)}
17 | ${ZERO_WIDTH_UNICODE_CHAR}
18 | `
19 | },
20 |
21 | input() {
22 | var _s = this.settings,
23 | placeholder = _s.placeholder || ZERO_WIDTH_UNICODE_CHAR;
24 |
25 | return ` `
32 | },
33 |
34 | tag(tagData, {settings: _s}){
35 | return `
40 |
41 |
42 | ${tagData[_s.tagTextProp] || tagData.value}
43 |
44 | `
45 | },
46 |
47 | dropdown(settings){
48 | var _sd = settings.dropdown,
49 | isManual = _sd.position == 'manual';
50 |
51 | return ``
54 | },
55 |
56 | dropdownContent(HTMLContent) {
57 | var _t = this.settings.templates,
58 | suggestions = this.state.dropdown.suggestions;
59 |
60 | return `
61 | ${_t.dropdownHeader.call(this, suggestions)}
62 | ${HTMLContent}
63 | ${_t.dropdownFooter.call(this, suggestions)}
64 | `
65 | },
66 |
67 | dropdownItem(item){
68 | return `${item.mappedValue || item.value}
`
72 | },
73 |
74 | /**
75 | * @param {Array} suggestions An array of all the matched suggested items, including those which were sliced away due to the "dropdown.maxItems" setting
76 | */
77 | dropdownHeader(suggestions){
78 | return ``
79 | },
80 |
81 | dropdownFooter(suggestions){
82 | var hasMore = suggestions.length - this.settings.dropdown.maxItems;
83 |
84 | return hasMore > 0
85 | ? ``
88 | : '';
89 | },
90 |
91 | dropdownItemNoMatch: null
92 | }
93 |
--------------------------------------------------------------------------------
/src/parts/texts.js:
--------------------------------------------------------------------------------
1 | export default {
2 | empty : "empty",
3 | exceed : "number of tags exceeded",
4 | pattern : "pattern mismatch",
5 | duplicate : "already exists",
6 | notAllowed : "not allowed"
7 | }
--------------------------------------------------------------------------------
/src/polyfills/Array.findIndex.js:
--------------------------------------------------------------------------------
1 | if (!Array.prototype.findIndex) {
2 | Object.defineProperty(Array.prototype, 'findIndex', {
3 | value: function(predicate) {
4 | if (this == null)
5 | throw new TypeError('"this" is null or not defined');
6 |
7 | var o = Object(this), len = o.length >>> 0;
8 |
9 | if (typeof predicate !== 'function') {
10 | throw new TypeError('predicate must be a function');
11 | }
12 |
13 | var thisArg = arguments[1], k = 0;
14 |
15 | while (k < len) {
16 | var kValue = o[k];
17 | if (predicate.call(thisArg, kValue, k, o)) {
18 | return k;
19 | }
20 | k++;
21 | }
22 |
23 | return -1;
24 | },
25 | configurable: true,
26 | writable: true
27 | })
28 | }
--------------------------------------------------------------------------------
/src/polyfills/Array.includes.js:
--------------------------------------------------------------------------------
1 | if (!Array.prototype.includes) {
2 | Array.prototype.includes = function(search){
3 | return !!~this.indexOf(search)
4 | }
5 | }
--------------------------------------------------------------------------------
/src/polyfills/Array.some.js:
--------------------------------------------------------------------------------
1 | // Production steps of ECMA-262, Edition 5, 15.4.4.17
2 | // Reference: http://es5.github.io/#x15.4.4.17
3 | if (!Array.prototype.some) {
4 | Array.prototype.some = function(fun, thisArg) {
5 | 'use strict';
6 |
7 | if (this == null) {
8 | throw new TypeError('Array.prototype.some called on null or undefined');
9 | }
10 |
11 | if (typeof fun !== 'function') {
12 | throw new TypeError();
13 | }
14 |
15 | var t = Object(this);
16 | var len = t.length >>> 0;
17 |
18 | for (var i = 0; i < len; i++) {
19 | if (i in t && fun.call(thisArg, t[i], i, t)) {
20 | return true;
21 | }
22 | }
23 |
24 | return false;
25 | };
26 | }
--------------------------------------------------------------------------------
/src/polyfills/AutoUrlDetect.js:
--------------------------------------------------------------------------------
1 | // Avoid transformation text to link ie contentEditable mode
2 | // https://stackoverflow.com/q/7556007/104380
3 | document.execCommand("AutoUrlDetect", false, false);
--------------------------------------------------------------------------------
/src/polyfills/Element.classList.js:
--------------------------------------------------------------------------------
1 | /*
2 | * classList.js: Cross-browser full element.classList implementation.
3 | * 1.2.20171210
4 | *
5 | * By Eli Grey, http://eligrey.com
6 | * License: Dedicated to the public domain.
7 | * See https://github.com/eligrey/classList.js/blob/master/LICENSE.md
8 | */
9 |
10 | /*global self, document, DOMException */
11 |
12 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
13 |
14 | if ("document" in self) {
15 |
16 | // Full polyfill for browsers with no classList support
17 | // Including IE < Edge missing SVGElement.classList
18 | if (
19 | !("classList" in document.createElement("_"))
20 | || document.createElementNS
21 | && !("classList" in document.createElementNS("http://www.w3.org/2000/svg","g"))
22 | ) {
23 |
24 | (function (view) {
25 |
26 | "use strict";
27 |
28 | if (!('Element' in view)) return;
29 |
30 | var
31 | classListProp = "classList"
32 | , protoProp = "prototype"
33 | , elemCtrProto = view.Element[protoProp]
34 | , objCtr = Object
35 | , strTrim = String[protoProp].trim || function () {
36 | return this.replace(/^\s+|\s+$/g, "");
37 | }
38 | , arrIndexOf = Array[protoProp].indexOf || function (item) {
39 | var
40 | i = 0
41 | , len = this.length
42 | ;
43 | for (; i < len; i++) {
44 | if (i in this && this[i] === item) {
45 | return i;
46 | }
47 | }
48 | return -1;
49 | }
50 | // Vendors: please allow content code to instantiate DOMExceptions
51 | , DOMEx = function (type, message) {
52 | this.name = type;
53 | this.code = DOMException[type];
54 | this.message = message;
55 | }
56 | , checkTokenAndGetIndex = function (classList, token) {
57 | if (token === "") {
58 | throw new DOMEx(
59 | "SYNTAX_ERR"
60 | , "The token must not be empty."
61 | );
62 | }
63 | if (/\s/.test(token)) {
64 | throw new DOMEx(
65 | "INVALID_CHARACTER_ERR"
66 | , "The token must not contain space characters."
67 | );
68 | }
69 | return arrIndexOf.call(classList, token);
70 | }
71 | , ClassList = function (elem) {
72 | var
73 | trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
74 | , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
75 | , i = 0
76 | , len = classes.length
77 | ;
78 | for (; i < len; i++) {
79 | this.push(classes[i]);
80 | }
81 | this._updateClassName = function () {
82 | elem.setAttribute("class", this.toString());
83 | };
84 | }
85 | , classListProto = ClassList[protoProp] = []
86 | , classListGetter = function () {
87 | return new ClassList(this);
88 | }
89 | ;
90 | // Most DOMException implementations don't allow calling DOMException's toString()
91 | // on non-DOMExceptions. Error's toString() is sufficient here.
92 | DOMEx[protoProp] = Error[protoProp];
93 | classListProto.item = function (i) {
94 | return this[i] || null;
95 | };
96 | classListProto.contains = function (token) {
97 | return ~checkTokenAndGetIndex(this, token + "");
98 | };
99 | classListProto.add = function () {
100 | var
101 | tokens = arguments
102 | , i = 0
103 | , l = tokens.length
104 | , token
105 | , updated = false
106 | ;
107 | do {
108 | token = tokens[i] + "";
109 | if (!~checkTokenAndGetIndex(this, token)) {
110 | this.push(token);
111 | updated = true;
112 | }
113 | }
114 | while (++i < l);
115 |
116 | if (updated) {
117 | this._updateClassName();
118 | }
119 | };
120 | classListProto.remove = function () {
121 | var
122 | tokens = arguments
123 | , i = 0
124 | , l = tokens.length
125 | , token
126 | , updated = false
127 | , index
128 | ;
129 | do {
130 | token = tokens[i] + "";
131 | index = checkTokenAndGetIndex(this, token);
132 | while (~index) {
133 | this.splice(index, 1);
134 | updated = true;
135 | index = checkTokenAndGetIndex(this, token);
136 | }
137 | }
138 | while (++i < l);
139 |
140 | if (updated) {
141 | this._updateClassName();
142 | }
143 | };
144 | classListProto.toggle = function (token, force) {
145 | var
146 | result = this.contains(token)
147 | , method = result ?
148 | force !== true && "remove"
149 | :
150 | force !== false && "add"
151 | ;
152 |
153 | if (method) {
154 | this[method](token);
155 | }
156 |
157 | if (force === true || force === false) {
158 | return force;
159 | } else {
160 | return !result;
161 | }
162 | };
163 | classListProto.replace = function (token, replacement_token) {
164 | var index = checkTokenAndGetIndex(token + "");
165 | if (~index) {
166 | this.splice(index, 1, replacement_token);
167 | this._updateClassName();
168 | }
169 | }
170 | classListProto.toString = function () {
171 | return this.join(" ");
172 | };
173 |
174 | if (objCtr.defineProperty) {
175 | var classListPropDesc = {
176 | get: classListGetter
177 | , enumerable: true
178 | , configurable: true
179 | };
180 | try {
181 | objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
182 | } catch (ex) { // IE 8 doesn't support enumerable:true
183 | // adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36
184 | // modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected
185 | if (ex.number === undefined || ex.number === -0x7FF5EC54) {
186 | classListPropDesc.enumerable = false;
187 | objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
188 | }
189 | }
190 | } else if (objCtr[protoProp].__defineGetter__) {
191 | elemCtrProto.__defineGetter__(classListProp, classListGetter);
192 | }
193 |
194 | }(self));
195 |
196 | }
197 |
198 | // There is full or partial native classList support, so just check if we need
199 | // to normalize the add/remove and toggle APIs.
200 |
201 | (function () {
202 | "use strict";
203 |
204 | var testElement = document.createElement("_");
205 |
206 | testElement.classList.add("c1", "c2");
207 |
208 | // Polyfill for IE 10/11 and Firefox <26, where classList.add and
209 | // classList.remove exist but support only one argument at a time.
210 | if (!testElement.classList.contains("c2")) {
211 | var createMethod = function(method) {
212 | var original = DOMTokenList.prototype[method];
213 |
214 | DOMTokenList.prototype[method] = function(token) {
215 | var i, len = arguments.length;
216 |
217 | for (i = 0; i < len; i++) {
218 | token = arguments[i];
219 | original.call(this, token);
220 | }
221 | };
222 | };
223 | createMethod('add');
224 | createMethod('remove');
225 | }
226 |
227 | testElement.classList.toggle("c3", false);
228 |
229 | // Polyfill for IE 10 and Firefox <24, where classList.toggle does not
230 | // support the second argument.
231 | if (testElement.classList.contains("c3")) {
232 | var _toggle = DOMTokenList.prototype.toggle;
233 |
234 | DOMTokenList.prototype.toggle = function(token, force) {
235 | if (1 in arguments && !this.contains(token) === !force) {
236 | return force;
237 | } else {
238 | return _toggle.call(this, token);
239 | }
240 | };
241 |
242 | }
243 |
244 | // replace() polyfill
245 | if (!("replace" in document.createElement("_").classList)) {
246 | DOMTokenList.prototype.replace = function (token, replacement_token) {
247 | var
248 | tokens = this.toString().split(" ")
249 | , index = tokens.indexOf(token + "")
250 | ;
251 | if (~index) {
252 | tokens = tokens.slice(index);
253 | this.remove.apply(this, tokens);
254 | this.add(replacement_token);
255 | this.add.apply(this, tokens.slice(1));
256 | }
257 | }
258 | }
259 |
260 | testElement = null;
261 | }());
262 |
263 | }
--------------------------------------------------------------------------------
/src/polyfills/Element.closest.js:
--------------------------------------------------------------------------------
1 | if (!Element.prototype.closest) {
2 | Element.prototype.closest = function(s) {
3 | var el = this;
4 | if (!document.documentElement.contains(el)) return null;
5 | do {
6 | if (el.matches(s)) return el;
7 | el = el.parentElement || el.parentNode;
8 | } while (el !== null && el.nodeType === 1);
9 | return null;
10 | };
11 | }
--------------------------------------------------------------------------------
/src/polyfills/Element.matches.js:
--------------------------------------------------------------------------------
1 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
2 | if (!Element.prototype.matches)
3 | Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
--------------------------------------------------------------------------------
/src/polyfills/Event.js:
--------------------------------------------------------------------------------
1 | // https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
2 | function CustomEventPolyfill ( event, params ) {
3 | params = params || { bubbles: false, cancelable: false, detail: undefined };
4 | var evt = document.createEvent( 'CustomEvent' );
5 | evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
6 | return evt;
7 | }
8 |
9 | CustomEventPolyfill.prototype = window.Event.prototype;
10 |
11 | if ( typeof window.CustomEvent !== "function" ){
12 | window.CustomEvent = CustomEventPolyfill;
13 | }
--------------------------------------------------------------------------------
/src/polyfills/NodeList.forEach.js:
--------------------------------------------------------------------------------
1 | if( window.NodeList && !NodeList.prototype.forEach ){
2 | NodeList.prototype.forEach = Array.prototype.forEach;
3 | }
--------------------------------------------------------------------------------
/src/polyfills/Object.assign.js:
--------------------------------------------------------------------------------
1 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
2 | //
3 | if (typeof Object.assign != 'function') {
4 | // Must be writable: true, enumerable: false, configurable: true
5 | Object.defineProperty(Object, "assign", {
6 | value: function assign(target, varArgs) { // .length of function is 2
7 | if (target == null) { // TypeError if undefined or null
8 | throw new TypeError('Cannot convert undefined or null to object');
9 | }
10 |
11 | var to = Object(target);
12 |
13 | for (var index = 1; index < arguments.length; index++) {
14 | var nextSource = arguments[index];
15 |
16 | if (nextSource != null) { // Skip over if undefined or null
17 | for (var nextKey in nextSource) {
18 | // Avoid bugs when hasOwnProperty is shadowed
19 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
20 | to[nextKey] = nextSource[nextKey];
21 | }
22 | }
23 | }
24 | }
25 | return to;
26 | },
27 | writable: true,
28 | configurable: true
29 | });
30 | }
--------------------------------------------------------------------------------
/src/polyfills/String.includes.js:
--------------------------------------------------------------------------------
1 | if( !String.prototype.includes ){
2 | String.prototype.includes = function(search, start) {
3 | if (typeof start !== 'number')
4 | start = 0;
5 |
6 | if (start + search.length > this.length)
7 | return false;
8 |
9 | else
10 | return this.indexOf(search, start) !== -1;
11 | };
12 | }
--------------------------------------------------------------------------------
/src/polyfills/String.trim.js:
--------------------------------------------------------------------------------
1 | // 1. String.prototype.trim polyfill
2 | if (!"".trim) String.prototype.trim = function(){ return this.replace(/^[\s]+|[\s]+$/g, ''); };
--------------------------------------------------------------------------------
/src/react-compat-layer.js:
--------------------------------------------------------------------------------
1 | function renderToStaticMarkup(element) {
2 | if (typeof element === 'string') {
3 | return element;
4 | }
5 |
6 | if (Array.isArray(element)) {
7 | return element.map(renderToStaticMarkup).join('');
8 | }
9 |
10 | if (typeof element === 'object' && element !== null) {
11 | if (typeof element.type === 'function') {
12 | // For function components
13 | return renderToStaticMarkup(element.type(element.props));
14 | }
15 |
16 | if (typeof element.type === 'string') {
17 | // For DOM elements
18 | const attrs = Object.entries(element.props || {})
19 | .filter(([key]) => key !== 'children')
20 | .map(([key, value]) => `${key}="${value}"`)
21 | .join(' ');
22 |
23 | const children = element.props.children
24 | ? renderToStaticMarkup(element.props.children)
25 | : '';
26 |
27 | return `<${element.type} ${attrs}>${children}${element.type}>`;
28 | }
29 | }
30 |
31 | return '';
32 | }
33 |
34 | export { renderToStaticMarkup };
--------------------------------------------------------------------------------
/src/tagify.polyfills.js:
--------------------------------------------------------------------------------
1 | import "./polyfills/String.trim"
2 | import "./polyfills/NodeList.forEach"
3 | import "./polyfills/Array.findIndex"
4 | import "./polyfills/Array.includes"
5 | import "./polyfills/Array.some"
6 | import "./polyfills/String.includes"
7 | import "./polyfills/Object.assign"
8 | import "./polyfills/Event"
9 | import "./polyfills/Element.matches"
10 | import "./polyfills/Element.closest"
11 | import "./polyfills/AutoUrlDetect"
12 | import "./polyfills/Element.classList"
13 | import "./polyfills/es6-promise"
--------------------------------------------------------------------------------
/test/dragsort.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - basic
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
40 |
41 |
42 |
43 |
44 |
63 |
64 |
--------------------------------------------------------------------------------
/test/easy-to-customize.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - pupetteer tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
100 |
101 |
102 |
103 |
106 |
107 |
142 |
143 |
--------------------------------------------------------------------------------
/test/manual-suggestions.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - manual suggestions
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
38 |
39 | Please select from the list:
40 |
41 |
42 |
83 |
84 |
--------------------------------------------------------------------------------
/test/mix.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - mix-mode
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
38 |
39 |
52 |
53 |
54 |
101 |
102 |
103 |
104 |
238 |
239 |
--------------------------------------------------------------------------------
/test/mode-select.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - Select
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
85 |
86 |
--------------------------------------------------------------------------------
/test/scroll-tracking.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - pupetteer tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
38 |
39 | FullScreen
40 |
43 |
44 |
53 |
54 |
--------------------------------------------------------------------------------
/test/test2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - pupetteer tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
52 |
53 |
54 |
55 |
58 |
59 |
76 |
77 |
--------------------------------------------------------------------------------
/test/text-input.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - pupetteer tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
38 |
39 |
44 |
45 |
141 |
142 |
--------------------------------------------------------------------------------
/test/textarea.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - textarea
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
38 |
39 | mixed-mode [[example]] with [[{"value":"tags"}]].
40 |
41 |
42 | [[{"value":200, "text":" cartman ", "title":"Eric Cartman", "prefix":"@"}]] and [[{"value":1000, "text":"cartman", "title":"xxx", "prefix":"@", "readonly":true}]] [[kyle]] do not know [[homer simpson]] because he's a
43 | and [[{"value": "readonly", "readonly":true}]]
44 |
45 |
46 |
47 |
48 |
49 |
207 |
208 |
--------------------------------------------------------------------------------
/test/validate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Tagify - pupetteer tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | submit and validate
44 |
45 |
46 |
62 |
63 |
--------------------------------------------------------------------------------