forked from Bitmessage/virtpool
Compare commits
1 Commits
85eec52f1a
...
5d01c4278d
Author | SHA1 | Date | |
---|---|---|---|
5d01c4278d |
46
node_modules/@yaireo/tagify/.eslintrc
generated
vendored
46
node_modules/@yaireo/tagify/.eslintrc
generated
vendored
|
@ -1,46 +0,0 @@
|
||||||
{
|
|
||||||
"rules": {
|
|
||||||
"no-mixed-spaces-and-tabs" : [2, "smart-tabs"],
|
|
||||||
"block-spacing" : [2, "always"],
|
|
||||||
"comma-style" : [2, "last"],
|
|
||||||
"no-debugger" : [1],
|
|
||||||
"no-alert" : [1],
|
|
||||||
"indent" : [1, 4, {"SwitchCase":1}],
|
|
||||||
"strict" : 0,
|
|
||||||
"no-undef" : 1
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion" : 9,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"modules": true,
|
|
||||||
"sourceTyp": "module",
|
|
||||||
"experimentalObjectRestSpread": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"globals" : {
|
|
||||||
"FB": "readonly",
|
|
||||||
"ga": "readonly",
|
|
||||||
"jQuery": "readonly",
|
|
||||||
"$":"readonly",
|
|
||||||
"_": "readonly",
|
|
||||||
"d3": "readonly",
|
|
||||||
"Router": "readonly",
|
|
||||||
"ttip": "readonly",
|
|
||||||
"Cookies": "readonly",
|
|
||||||
"fastdom": "readonly",
|
|
||||||
"describe": "readonly",
|
|
||||||
"beforeEach": "readonly",
|
|
||||||
"it": "readonly",
|
|
||||||
"expect": "readonly",
|
|
||||||
"assert": "readonly",
|
|
||||||
"done": "readonly",
|
|
||||||
"dataLayer": "readonly",
|
|
||||||
"validator": "readonly"
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"es6": true,
|
|
||||||
"browser": true,
|
|
||||||
"node": true
|
|
||||||
}
|
|
||||||
}
|
|
17
node_modules/@yaireo/tagify/.gitattributes
generated
vendored
17
node_modules/@yaireo/tagify/.gitattributes
generated
vendored
|
@ -1,17 +0,0 @@
|
||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
||||||
|
|
||||||
# Custom for Visual Studio
|
|
||||||
*.cs diff=csharp
|
|
||||||
|
|
||||||
# Standard to msysgit
|
|
||||||
*.doc diff=astextplain
|
|
||||||
*.DOC diff=astextplain
|
|
||||||
*.docx diff=astextplain
|
|
||||||
*.DOCX diff=astextplain
|
|
||||||
*.dot diff=astextplain
|
|
||||||
*.DOT diff=astextplain
|
|
||||||
*.pdf diff=astextplain
|
|
||||||
*.PDF diff=astextplain
|
|
||||||
*.rtf diff=astextplain
|
|
||||||
*.RTF diff=astextplain
|
|
19
node_modules/@yaireo/tagify/LICENSE
generated
vendored
19
node_modules/@yaireo/tagify/LICENSE
generated
vendored
|
@ -1,19 +0,0 @@
|
||||||
Copyright 2019 Yair Even-Or
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
888
node_modules/@yaireo/tagify/README.md
generated
vendored
888
node_modules/@yaireo/tagify/README.md
generated
vendored
|
@ -1,888 +0,0 @@
|
||||||
<h1 align="center">
|
|
||||||
<a href='https://yaireo.github.io/tagify'><img src="/docs/readme-header.svg" width="320" height="160"><a/>
|
|
||||||
<br><br>
|
|
||||||
<a href='https://yaireo.github.io/tagify'>Tagify</a> - <em>tags</em> input component
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
Transforms an input field or a textarea into a <em>Tags component</em>, in an easy, customizable way,
|
|
||||||
with great performance and small code footprint, exploded with features.
|
|
||||||
<br>
|
|
||||||
<strong>Vanilla</strong> ⚡ <strong>React</strong> ⚡ <strong>Vue</strong> ⚡ <strong>Angular</strong>
|
|
||||||
<p>
|
|
||||||
|
|
||||||
<h3 align="center">
|
|
||||||
👉 <a href="https://yaireo.github.io/tagify">See Demos</a> 👈
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href='https://www.npmjs.com/package/@yaireo/tagify'>
|
|
||||||
<img src="https://img.shields.io/npm/v/@yaireo/tagify.svg" />
|
|
||||||
</a>
|
|
||||||
<a href='https://simple.wikipedia.org/wiki/MIT_License'>
|
|
||||||
<img src="https://img.shields.io/badge/license-MIT-lightgrey" />
|
|
||||||
</a>
|
|
||||||
<img src="https://img.shields.io/bundlephobia/minzip/@yaireo/tagify" />
|
|
||||||
<img src="https://img.shields.io/npm/dw/@yaireo/tagify" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="/docs/mix3.gif?sanitize=true" />
|
|
||||||
<img src="/docs/demo.gif?sanitize=true" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
<!--ts-->
|
|
||||||
* [Installation](#installation)
|
|
||||||
* [What can Tagify do](#features)
|
|
||||||
* [Building the project](#building-the-project)
|
|
||||||
* [Adding tags dynamically](#adding-tags-dynamically)
|
|
||||||
* [Output value](#output-value)
|
|
||||||
* [Ajax whitelist](#ajax-whitelist)
|
|
||||||
* [Edit tags](#edit-tags)
|
|
||||||
* [Drag & Sort](#drag--sort)
|
|
||||||
* [DOM Templates](#dom-templates)
|
|
||||||
* [Suggestions selectbox](#suggestions-selectbox)
|
|
||||||
* [Tags mixed with text](#mixed-content)
|
|
||||||
* [Single-Value Only](#single-value)
|
|
||||||
* [React wrapper](#react)
|
|
||||||
* [Angular wrapper](#angular)
|
|
||||||
* [Vue Example](https://codesandbox.io/s/tagify-tags-component-vue-example-l8ok4)
|
|
||||||
* [jQuery version](#jquery-version)
|
|
||||||
* [FAQ](#FAQ)
|
|
||||||
* [CSS Variables](#css-variables)
|
|
||||||
* [Methods](#methods)
|
|
||||||
* [Events](#events)
|
|
||||||
* [Hooks](#hooks)
|
|
||||||
* [Settings](#settings)
|
|
||||||
<!--te-->
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm i @yaireo/tagify --save
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage (in your bundle):
|
|
||||||
|
|
||||||
[live demo using Parcel as bundler](https://codesandbox.io/s/simple-tagify-setup-6pfi2)
|
|
||||||
|
|
||||||
```js
|
|
||||||
import Tagify from '@yaireo/tagify'
|
|
||||||
|
|
||||||
var tagify = new Tagify(...)
|
|
||||||
```
|
|
||||||
|
|
||||||
> Don't forget to **include `tagify.css`** file in your project.
|
|
||||||
> CSS location: `@yaireo/tagify/dist/tagify.css`
|
|
||||||
> SCSS location: `@yaireo/tagify/src/tagify.scss`
|
|
||||||
> [See SCSS usecase & example](https://github.com/yairEO/tagify/pull/282)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
* Can be applied on input & textarea elements
|
|
||||||
* Supports [mix content](#mixed-content) (text and tags together)
|
|
||||||
* Supports [single-value](#single-value) mode (like `<select>`)
|
|
||||||
* Supports whitelist/blacklist
|
|
||||||
* Supports Templates for: <em>component wrapper</em>, <em>tag items</em>, <em>suggestion list</em> & <em>suggestion items</em>
|
|
||||||
* Shows suggestions selectbox (flexiable settings & styling) at *full (component) width* or *next to* the typed texted (caret)
|
|
||||||
* Allows setting suggestions' [aliases](#example-for-a-suggestion-item-alias) for easier fuzzy-searching
|
|
||||||
* Auto-suggest input as-you-type with ability to auto-complete
|
|
||||||
* Can paste in multiple values: `tag 1, tag 2, tag 3` or even newline-separated tags
|
|
||||||
* Tags can be created by Regex delimiter or by pressing the "Enter" key / focusing of the input
|
|
||||||
* Validate tags by Regex pattern
|
|
||||||
* Tags may be [editable](#edit-tags) (double-click)
|
|
||||||
* <del>ARIA accessibility support</del>(Component too generic for any meaningful ARIA)
|
|
||||||
* Supports read-only mode to the whole componenet or per-tag
|
|
||||||
* Each tag can have any properties desired (class, data-whatever, readonly...)
|
|
||||||
* Automatically disallow duplicate tags (vis "settings" object)
|
|
||||||
* Has built-in CSS loader, if needed (Ex. <em>AJAX</em> whitelist pulling)
|
|
||||||
* Tags can be trimmed via `hellip` by giving `max-width` to the `tag` element in your `CSS`
|
|
||||||
* Easily change direction to RTL (via the SCSS file)
|
|
||||||
* Internet Explorer - A polyfill script should be used: `tagify.polyfills.min.js` (in `/dist`)
|
|
||||||
* Many useful custom [events](#events)
|
|
||||||
* Original input/textarea element values kept in sync with Tagify
|
|
||||||
|
|
||||||
## Building the project
|
|
||||||
Simply run `gulp` in your terminal, from the project's path ([Gulp](https://gulpjs.com) should be installed first).
|
|
||||||
|
|
||||||
Source files are this path: `/src/`
|
|
||||||
|
|
||||||
Output files, which are automatically generated using Gulp, are in: `/dist/`
|
|
||||||
|
|
||||||
The rest of the files are most likely irrelevant.
|
|
||||||
|
|
||||||
## Adding tags dynamically
|
|
||||||
```javascript
|
|
||||||
var tagify = new Tagify(...);
|
|
||||||
|
|
||||||
tagify.addTags(["banana", "orange", "apple"])
|
|
||||||
|
|
||||||
// or add tags with pre-defined propeties
|
|
||||||
|
|
||||||
tagify.addTags([{value:"banana", color:"yellow"}, {value:"apple", color:"red"}, {value:"watermelon", color:"green"}])
|
|
||||||
```
|
|
||||||
|
|
||||||
## Output value
|
|
||||||
There are two possible ways to get the value of the tags:
|
|
||||||
|
|
||||||
1. Access the tagify's instance's `value` prop: `tagify.value` (Array of tags)
|
|
||||||
2. Access the *original* input's value: `inputElm.value` (Stringified Array of tags)
|
|
||||||
|
|
||||||
The most common way is to simply listen to the `change` event on the *original input*
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var inputElm = document.querySelector,
|
|
||||||
tagify = new Tagify (inputElm);
|
|
||||||
|
|
||||||
inputElm.addEventListener('change', onChange)
|
|
||||||
|
|
||||||
function onChange(e){
|
|
||||||
// outputs a String
|
|
||||||
console.log(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### [Modify original input value format](https://jsbin.com/paxijaj/edit?html,js,output)
|
|
||||||
|
|
||||||
Default format is a JSON string:<br>
|
|
||||||
`'[{"value":"cat"}, {"value":"dog"}]'`
|
|
||||||
|
|
||||||
I **recommend** keeping this because some situations might have values such as addresses (tags contain commas):<br>
|
|
||||||
`'[{"value":"Apt. 2A, Jacksonville, FL 39404"}, {"value":"Forrest Ray, 191-103 Integer Rd., Corona New Mexico"}]'`
|
|
||||||
|
|
||||||
Another example for complex tags state might be disabled tags, or ones with custom identifier *class*:<br>
|
|
||||||
*(tags can be clicked, so delevopers can choose to use this to disable/enable tags)*<br>
|
|
||||||
`'[{"value":"cat", "disabled":true}, {"value":"dog"}, {"value":"bird", "class":"color-green"}]'`
|
|
||||||
|
|
||||||
To change the format, assuming your tags have no commas and are fairly simple:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var tagify = new Tagify(inputElm, {
|
|
||||||
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(',')
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output:**<br>
|
|
||||||
`"cat,dog"`
|
|
||||||
|
|
||||||
|
|
||||||
## Ajax whitelist
|
|
||||||
Dynamically-loaded suggestions list (*whitelist*) from the server (as the user types) is a frequent need to many.
|
|
||||||
|
|
||||||
Tagify comes with its own loading animation, which is a very lightweight CSS-only code, and the <em>loading</em>
|
|
||||||
state is controlled by the method `tagify.loading` which accepts `true` or `false` as arguments.
|
|
||||||
|
|
||||||
Below is a basic example using the `fetch` API. I advise to abort the last request on any input before starting a new request.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example:</summary>
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var input = document.querySelector('input'),
|
|
||||||
tagify = new Tagify(input, {whitelist:[]}),
|
|
||||||
controller; // for aborting the call
|
|
||||||
|
|
||||||
// listen to any keystrokes which modify tagify's input
|
|
||||||
tagify.on('input', onInput)
|
|
||||||
|
|
||||||
function onInput( e ){
|
|
||||||
var value = e.detail.value;
|
|
||||||
tagify.settings.whitelist.length = 0; // reset the whitelist
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort
|
|
||||||
controller && controller.abort();
|
|
||||||
controller = new AbortController();
|
|
||||||
|
|
||||||
// show loading animation and hide the suggestions dropdown
|
|
||||||
tagify.loading(true).dropdown.hide.call(tagify)
|
|
||||||
|
|
||||||
fetch('http://get_suggestions.com?value=' + value, {signal:controller.signal})
|
|
||||||
.then(RES => RES.json())
|
|
||||||
.then(function(whitelist){
|
|
||||||
// update inwhitelist Array in-place
|
|
||||||
tagify.settings.whitelist.splice(0, whitelist.length, ...whitelist)
|
|
||||||
tagify.loading(false).dropdown.show.call(tagify, value); // render the suggestions dropdown
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Edit tags
|
|
||||||
Tags which aren't `read-only` can be edited by double-clicking them (by default)
|
|
||||||
or by changing the `editTags` *setting* to `1`, making tags editable by single-clicking them.
|
|
||||||
|
|
||||||
The value is saved on `blur` or by pressing `enter` key. Pressing `Escape` will revert the change trigger `blur`.
|
|
||||||
<kbd>ctrl</kbd><kbd>z</kbd> will revert the change if an edited tag was marked as not valid (perhaps duplicate or blacklisted)
|
|
||||||
|
|
||||||
To prevent *all* tags from being allowed to be editable, set the `editTags` setting to `false` (or `null`).<br>
|
|
||||||
To do the same but for specific tag(s), set those tags' data with `editable` property set to `false`:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<input value='[{"value":"foo", "editable":false}, {"value":"bar"}]'>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Drag & Sort
|
|
||||||
|
|
||||||
To be able to sort tags by draging, a 3rd-party script is needed.
|
|
||||||
|
|
||||||
I have made a very simple *drag & drop* (~`11kb` *unminified*) script which uses [HTML5 native API](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API) and
|
|
||||||
it is available to download via [NPM](https://www.npmjs.com/package/@yaireo/dragsort) or [Github](https://github.com/yairEO/dragsort)
|
|
||||||
but any other *drag & drop* script may possibly work. I could not find in the whole internet a decent lightweight script.
|
|
||||||
|
|
||||||
### [Integration example](https://codepen.io/vsync/pen/jOqYOVJ):
|
|
||||||
|
|
||||||
```js
|
|
||||||
var tagify = new Tagify(inputElement)
|
|
||||||
|
|
||||||
// bind "DragSort" to Tagify's main element and tell
|
|
||||||
// it that all the items with the below "selector" are "draggable"
|
|
||||||
var dragsort = new DragSort(tagify.DOM.scope, {
|
|
||||||
selector: '.'+tagify.settings.classNames.tag,
|
|
||||||
callbacks: {
|
|
||||||
dragEnd: onDragEnd
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// must update Tagify's value according to the re-ordered nodes in the DOM
|
|
||||||
function onDragEnd(elm){
|
|
||||||
tagify.updateValueByDOMTags()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## DOM Templates
|
|
||||||
It's possible to control the templates for some of the HTML elements tagify is using by
|
|
||||||
modifying the `settings.templates` Object with your own custom functions which **must return** an *HTML string*.
|
|
||||||
|
|
||||||
Available templates are: `wrapper`, `tag`, `dropdown`, `dropdownItem` and the optional `dropdownItemNoMatch`
|
|
||||||
which is a special template for rendering a suggestion item (in the dropdown list) only if there were no matches found for the typed input.
|
|
||||||
|
|
||||||
[View templates](https://github.com/yairEO/tagify/blob/master/src/parts/templates.js)
|
|
||||||
|
|
||||||
## Suggestions selectbox
|
|
||||||
The suggestions selectbox is shown is a whitelist Array of Strings or Objects was passed in the settings when the Tagify instance was created.
|
|
||||||
Suggestions list will only be rendered if there are at least two matching suggestions (case-insensitive).
|
|
||||||
|
|
||||||
The selectbox dropdown will be appended to the document's `<body>` element and will be rendered by default in a position below (bottom of) the Tagify element.
|
|
||||||
Using the keyboard arrows up/down will highlight an option from the list, and hitting the Enter key to select.
|
|
||||||
|
|
||||||
It is possible to tweak the selectbox dropdown via 2 settings:
|
|
||||||
|
|
||||||
- `enabled` - this is a numeral value which tells Tagify when to show the suggestions dropdown, when a minimum of N characters were typed.
|
|
||||||
- `maxItems` - Limits the number of items the suggestions selectbox will render
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var input = document.querySelector('input'),
|
|
||||||
tagify = new Tagify(input, {
|
|
||||||
whitelist : ['aaa', 'aaab', 'aaabb', 'aaabc', 'aaabd', 'aaabe', 'aaac', 'aaacc'],
|
|
||||||
dropdown : {
|
|
||||||
classname : "color-blue",
|
|
||||||
enabled : 0, // show the dropdown immediately on focus
|
|
||||||
maxItems : 5,
|
|
||||||
position : "text", // place the dropdown near the typed text
|
|
||||||
closeOnSelect : false, // keep the dropdown open after selecting a suggestion
|
|
||||||
highlightFirst: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
<p align="center"><b>Will render</b></p>
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="tagify__dropdown tagify__dropdown--text" style="left:993.5px; top:106.375px; width:616px;">
|
|
||||||
<div class="tagify__dropdown__wrapper">
|
|
||||||
<div class="tagify__dropdown__item tagify__dropdown__item--active" value="aaab">aaab</div>
|
|
||||||
<div class="tagify__dropdown__item" value="aaabb">aaabb</div>
|
|
||||||
<div class="tagify__dropdown__item" value="aaabc">aaabc</div>
|
|
||||||
<div class="tagify__dropdown__item" value="aaabd">aaabd</div>
|
|
||||||
<div class="tagify__dropdown__item" value="aaabe">aaabe</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
By default searching the suggestions is using [fuzzy-search](https://en.wikipedia.org/wiki/Approximate_string_matching) (see [settings](#settings)).
|
|
||||||
|
|
||||||
If you wish to assign *alias* to items (in your suggestion list), add the `searchBy` property to *whitelist* items you wish
|
|
||||||
to have an *alias* for.
|
|
||||||
|
|
||||||
In the below example, typing a part of a string which is included in the `searchBy` property, for example *`land midd"`* -
|
|
||||||
the suggested item which match the value "Israel" will be rendered in the suggestions (dropdown) list.
|
|
||||||
|
|
||||||
### [Example](https://yaireo.github.io/tagify/#section-extra-properties) for a suggestion item alias
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
whitelist = [
|
|
||||||
...
|
|
||||||
{ value:'Israel', code:'IL', searchBy:'holy land, desert, middle east' },
|
|
||||||
...
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Another handy setting is `dropdown.searchKeys` which, like the above `dropdown.searchBy` setting, allows
|
|
||||||
expanding the search of any typed terms to more than the `value` property of the whitelist items (if items are a *Collection*).
|
|
||||||
|
|
||||||
### Example whitelist:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
[
|
|
||||||
{
|
|
||||||
value : 123456,
|
|
||||||
nickname : "foo",
|
|
||||||
email : "foo@mail.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value : 987654,
|
|
||||||
nickname : "bar",
|
|
||||||
email : "bar@mail.com"
|
|
||||||
},
|
|
||||||
...more..
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
// setting to search in other keys:
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
dropdown: {
|
|
||||||
searchKeys: ["nickname", "email"] // fuzzy-search matching for those whitelist items' properties
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mixed-Content
|
|
||||||
|
|
||||||
> To use this feature it must be toggled - see [settings](#settings).
|
|
||||||
|
|
||||||
When mixing text with tags, the original textarea (or input) element will have a value as follows:
|
|
||||||
|
|
||||||
[[cartman]] and [[kyle]] do not know [[Homer simpson]]
|
|
||||||
|
|
||||||
If the inital value of the textarea or input is formatted as the above example, tagify will try to
|
|
||||||
automatically convert everything between `[[` & `]]` to a tag, if tag exists in the *whitelist*, so make
|
|
||||||
sure when the Tagify instance is initialized, that it has tags with the correct `value` property that match
|
|
||||||
the same values that appear between `[[` & `]]`.
|
|
||||||
|
|
||||||
Applying the setting `dropdown.position:"text"` is encouraged for mixed-content tags, because the suggestions list
|
|
||||||
will be rendered right next to the caret location and not the the bottom of the Tagify componenet, which might look
|
|
||||||
weird when there is already a lot of content at multiple lines.
|
|
||||||
|
|
||||||
If a tag does not exists in the *whitelist*, it may be created by the user and all you should do is listen to the `add` event and update your local/remote state.
|
|
||||||
|
|
||||||
## Single-Value
|
|
||||||
|
|
||||||
Similar to native `<Select>` element, but allows typing text as value.
|
|
||||||
|
|
||||||
## React
|
|
||||||
|
|
||||||
See [**live demo**](https://codesandbox.io/s/tagify-react-wrapper-oempc) for React integration examples.
|
|
||||||
|
|
||||||
A Tagify React component is exported from [`react.tagify.js`](https://github.com/yairEO/tagify/blob/master/dist/react.tagify.js):
|
|
||||||
|
|
||||||
> Note: You will need to inport Tagify's CSS also, either by javasceript or by SCSS `@import` (which is preferable)
|
|
||||||
```javascript
|
|
||||||
import Tags from "@yaireo/tagify/dist/react.tagify" // React-wrapper file
|
|
||||||
import "@yaireo/tagify/dist/tagify.css" // Tagify CSS
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
return (
|
|
||||||
<Tags
|
|
||||||
tagifyRef={tagifyRef} // optional Ref object for the Tagify instance itself, to get access to inner-methods
|
|
||||||
settings={settings} // tagify settings object
|
|
||||||
value="a,b,c"
|
|
||||||
{...tagifyProps} // dynamic props such as "loading", "showDropdown:'abc'", "value"
|
|
||||||
onChange={e => (e.persist(), console.log("CHANGED:", e.target.value))}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
To gain full access to Tagify's (instance) inner methods, A custom `ref` can be used:
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
<Tags tagifyRef={tagifyRef} ... />
|
|
||||||
|
|
||||||
// or mix-mode
|
|
||||||
<MixedTags
|
|
||||||
settings={...}
|
|
||||||
onChange={...}
|
|
||||||
value={`This is a textarea which mixes text with [[{"value":"tags"}]].`}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
`<MixedTags>` component is a shorthand for `<Tags InputMode="textarea">`
|
|
||||||
|
|
||||||
#### Updating the component's state
|
|
||||||
|
|
||||||
The `settings` prop is **only used once** in the initialization process, please do not update it afterwards.
|
|
||||||
|
|
||||||
---
|
|
||||||
<details>
|
|
||||||
<summary>📖 List of (React) props for the <code><Tags/></code> component</summary>
|
|
||||||
|
|
||||||
|
|
||||||
Prop | Type | Updatable | Info
|
|
||||||
----------------------- | ------------------------- |:---------:| -----------------------------------------------------------
|
|
||||||
settings | <sub>Object</sub> | | See [*settings* section](#settings)
|
|
||||||
name | <sub>String</sub> | ✔ | `<input>`'s element `name` attribute
|
|
||||||
value | <sub>String/Array</sub> | ✔ | Initial value.
|
|
||||||
defaultValue | <sub>String/Array</sub> | | Only affects the hidden `<input>` element
|
|
||||||
placeholder | <sub>String</sub> | ✔ | placeholder text for the component
|
|
||||||
readOnly | <sub>Boolean</sub> | ✔ | Toggles `readonly` state. With capital `O`.
|
|
||||||
tagifyRef | <sub>Object</sub> | | `useRef` hook refference for the component inner instance of vailla *Tagify* (for methods access)
|
|
||||||
showFilteredDropdown | <sub>Boolean/String</sub> | ✔ | if `true` shows the suggestions dropdown. if assigned a String, show the dropdown pre-filtered.
|
|
||||||
loading | <sub>Boolean</sub> | ✔ | Toggles `loading` state for the whole component
|
|
||||||
whitelist | <sub>Array</sub> | ✔ | Sets the `whitelist` which is the basis for the suggestions dropdown & autocomplete
|
|
||||||
className | <sub>String</sub> | | Component's optional class name to be added
|
|
||||||
InputMode | <sub>String</sub> | | `"textarea"` will create a `<textarea>` (hidden) element instead of the default `<input>` and automatically make Tagify act as [*"mix mode"*](#mixed-content)
|
|
||||||
autoFocus | <sub>Boolean</sub> | | Should the component have focus on mount. Must be unique, per-page.
|
|
||||||
children | <sub>String/Nodes</sub> | | `value` prop is prefered of this
|
|
||||||
onChange | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onInput | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onAdd | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onRemove | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onEdit | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onInvalid | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onClick | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onKeydown | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onFocus | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
onBlur | <sub>Function</sub> | | See [*events* section](#events)
|
|
||||||
</details>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Angular
|
|
||||||
|
|
||||||
**TagifyComponent** which will be used by your template as `<tagify>`
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example:</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
<div>
|
|
||||||
testing tagify wrapper
|
|
||||||
<tagify [settings]="settings"
|
|
||||||
(add)="onAdd($event)"
|
|
||||||
(remove)="onRemove($event)">
|
|
||||||
</tagify>
|
|
||||||
<button (click)="clearTags()">clear</button>
|
|
||||||
<button (click)="addTags()">add Tags</button>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**TagifyService**
|
|
||||||
|
|
||||||
> (The tagifyService is a singletone injected by angular, do not create a new instance of it)
|
|
||||||
Remember to add `TagifyService` to your module definition.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example:</summary>
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {Component, OnDestroy} from '@angular/core';
|
|
||||||
import {TagifyService} from '@yaireo/tagify';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
templateUrl: './app.component.html',
|
|
||||||
styleUrls: ['./app.component.css']
|
|
||||||
})
|
|
||||||
export class AppComponent implements OnDestroy {
|
|
||||||
|
|
||||||
constructor(private tagifyService: TagifyService) {}
|
|
||||||
public settings = { blacklist: ['fucking', 'shit']};
|
|
||||||
|
|
||||||
onAdd(tagify) {
|
|
||||||
console.log('added a tag', tagify);
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove(tags) {
|
|
||||||
console.log('removed a tag', tags);
|
|
||||||
}
|
|
||||||
clearTags() {
|
|
||||||
this.tagifyService.removeAll();
|
|
||||||
}
|
|
||||||
addTags() {
|
|
||||||
this.tagifyService.addTags(['this', 'is', 'cool']);
|
|
||||||
}
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.tagifyService.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## jQuery version
|
|
||||||
|
|
||||||
`jQuery.tagify.js`
|
|
||||||
|
|
||||||
A jQuery wrapper verison is also available, but I advise not using it because it's basically the exact same as the "normal"
|
|
||||||
script (non-jqueryfied) and all the jQuery's wrapper does is allowing to chain the event listeners for ('add', 'remove', 'invalid')
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
$('[name=tags]')
|
|
||||||
.tagify()
|
|
||||||
.on('add', function(e, tagData){
|
|
||||||
console.log('added', ...tagData) // data, index, and DOM node
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Accessing methods can be done via the [`.data('tagify')`](https://api.jquery.com/data):
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
$('[name=tags]').tagify();
|
|
||||||
// get tags from the server (ajax) and add them:
|
|
||||||
$('[name=tags]').data('tagify').addTags('aaa, bbb, ccc')
|
|
||||||
````
|
|
||||||
|
|
||||||
## HTML input & textarea attributes
|
|
||||||
|
|
||||||
The below list of *attributes* affect *Tagify*.<br>
|
|
||||||
These can also be set by Tagify [settings](#settings) Object manually, and not *declerativly* (via attributes).
|
|
||||||
|
|
||||||
Attribute | Example | Info
|
|
||||||
----------------- | ----------------------------------------------------- | --------------------
|
|
||||||
[pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern) | <pre lang=html>`<input pattern='^[A-Za-z_✲ ]{1,15}$'>`</pre> | Tag Regex pattern which tag input is validated by.
|
|
||||||
[placeholder](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefplaceholder) | <pre lang=html>`<input placeholder='please type your tags'>`</pre> | This attribute's value will be used as a constant placeholder, which is visible unless something is being typed.
|
|
||||||
readOnly | <pre lang=html>`<input readOnly>`</pre> | No user-interaction (add/remove/edit) allowed.
|
|
||||||
autofocus | <pre lang=html>`<input autofocus>`</pre> | Automatically focus the the Tagify component when the component is loaded
|
|
||||||
required | <pre lang=html>`<input required>`</pre> | Adds a `required` attribute to the Tagify wrapper element. Does nothing more.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
List of questions & scenarios which might come up during development with Tagify:
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>tags/whitelist data strcture</strong></summary>
|
|
||||||
|
|
||||||
Tagify does not accept just *any* kind of data structure.<br>
|
|
||||||
If a tag data is represented as an `Object`, it **must** contain a **unique** property `value`
|
|
||||||
which Tagify uses to check if a tag already exists, among other things, so make sure it is present.
|
|
||||||
|
|
||||||
**Incorrect**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
[{ "id":1, "name":"foo bar" }]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Correct**
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
[{ "id":1, "value": 1, "name":"foo bar" }]
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
[{ "value":1, "name":"foo bar" }]
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
[{ "value":"foo bar" }]
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// ad a simple array of Strings
|
|
||||||
["foo bar"]
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Save changes (Ex. to a server)</strong></summary>
|
|
||||||
|
|
||||||
In framework-less projects, the developer should save the state of the Tagify component (somewhere), and
|
|
||||||
the question is:<br/>
|
|
||||||
**when should the state be saved?**
|
|
||||||
On every change made to *Tagify's* internal state (`tagify.value` via the `update()` method).<br>
|
|
||||||
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var tagify = new Tagify(...)
|
|
||||||
|
|
||||||
// listen to "change" events on the "original" input/textarea element
|
|
||||||
tagify.DOM.originalInput.addEventListener('change', onTagsChange)
|
|
||||||
|
|
||||||
// This example uses async/await but you can use Promises, of course, if you prefer.
|
|
||||||
async function onTagsChange(e){
|
|
||||||
const {name, value} = e.target
|
|
||||||
// "imaginary" async function "saveToServer" should get the field's name & value
|
|
||||||
await saveToServer(name, value)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are using *React/Vue/Angular* or any "modern" framework, then you already know how to
|
|
||||||
attach "onChange" event listeners to your `<input>`/`<textarea>` elements, so the above is irrelevant.
|
|
||||||
</details>
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Render tags in one single line</strong></summary>
|
|
||||||
|
|
||||||
Stopping tags from wrapping to new lines, add this to your `.tagify` *selector CSS Rule*:
|
|
||||||
|
|
||||||
```css
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
````
|
|
||||||
</details>
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><strong>Submit on `Enter` key</strong></summary>
|
|
||||||
|
|
||||||
Tagify internally has `state` property, per `Tagify` instance
|
|
||||||
and this may be useful for a variety of things when implementing a specific scenario.
|
|
||||||
|
|
||||||
```js
|
|
||||||
var tagify = new Tagify(...)
|
|
||||||
var formElm = document.forms[0]; // just an example
|
|
||||||
|
|
||||||
tagify.on('keydown', onTagifyKeyDown)
|
|
||||||
|
|
||||||
function onTagifyKeyDown(e){
|
|
||||||
if( e.key == 'Enter' && // "enter" key pressed
|
|
||||||
!tagify.state.inputText && // assuming user is not in the middle oy adding a tag
|
|
||||||
!tagify.state.editing // user not editing a tag
|
|
||||||
){
|
|
||||||
setTimeout(() => formElm.submit()) // put some buffer to make sure tagify has done with whatever, to be on the safe-side
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
* [Double-click tag fires both "edit" & "click" custom events](https://github.com/yairEO/tagify/issues/247)
|
|
||||||
* [Manualy open the suggestions dropdown](https://github.com/yairEO/tagify/issues/254)
|
|
||||||
* [Render your own suggestions dropdown](https://github.com/yairEO/tagify/issues/244)
|
|
||||||
* [Allow max length on mix mode](https://github.com/yairEO/tagify/issues/252)
|
|
||||||
* [Always show dropdown](https://github.com/yairEO/tagify/issues/253)
|
|
||||||
* [Limit the length of a tag value (minimum & maximum)](https://github.com/yairEO/tagify/issues/245)
|
|
||||||
* [Mixed mode initial value](https://github.com/yairEO/tagify/issues/237)
|
|
||||||
* [Random colors for each tag](https://github.com/yairEO/tagify/issues/223)
|
|
||||||
* [Format input value for server side](https://github.com/yairEO/tagify/issues/220)
|
|
||||||
* [Writing to tagify textarea](https://github.com/yairEO/tagify/issues/294)
|
|
||||||
* [Scroll all tags within one line, instead of growing vertically](https://github.com/yairEO/tagify/issues/145)
|
|
||||||
* [Insert emoji at caret location when editing a tag](https://github.com/yairEO/tagify/issues/365)
|
|
||||||
* [propagate `change` event](https://github.com/yairEO/tagify/issues/413)
|
|
||||||
* [Manually update tag data after it was added](https://github.com/yairEO/tagify/issues/433)
|
|
||||||
* [Ajax Whitelist with "enforceWhitelist" setting enabled](https://github.com/yairEO/tagify/issues/465)
|
|
||||||
* [Custom (multiple) tag valitation & AJAX](https://github.com/yairEO/tagify/issues/474)
|
|
||||||
* [Make tags from pasted multi-line text](https://github.com/yairEO/tagify/issues/160)
|
|
||||||
|
|
||||||
## CSS Variables
|
|
||||||
|
|
||||||
> Learn more about [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties)) (custom properties)
|
|
||||||
|
|
||||||
Tagify's utilizes *CSS variables* which allow easy customization without the need to manually write CSS.
|
|
||||||
If you do wish to heavily style your Tagify components, then you can (and should) use the below variables within
|
|
||||||
your modified styles as much as you can.
|
|
||||||
|
|
||||||
For a *live* example, see the [demos page](https://yaireo.github.io/tagify/#section-different-look).
|
|
||||||
|
|
||||||
Name | Info
|
|
||||||
------------------------------- | --------------------------------
|
|
||||||
--tags-border-color | The outer border color which surrounds tagify
|
|
||||||
--tags-hover-border-color | *hover* state
|
|
||||||
--tags-focus-border-color | *focus* state
|
|
||||||
--tag-bg | Tag background color
|
|
||||||
--tag-hover | Tag background color on hover (mouse)
|
|
||||||
--tag-text-color | Tag text color
|
|
||||||
--tag-text-color--edit | Tag text color when a Tag is being edited
|
|
||||||
--tag-pad | Tag padding, from all sides. Ex. `.3em .5em`
|
|
||||||
--tag--min-width | Minimum Tag width
|
|
||||||
--tag--max-width | Maximum tag width, which gets trimmed with *hellip* after
|
|
||||||
--tag-inset-shadow-size | This is the inner shadow size, which dictates the color of the Tags.<br>It's important the size fits *exactly* to the tag.<br>Change this if you change the `--tag-pad` or fontsize.
|
|
||||||
--tag-invalid-color | For border color of edited tags with invalid value being typed into them
|
|
||||||
--tag-invalid-bg | Background color for invalid Tags.
|
|
||||||
--tag-remove-bg | Tag background color when hovering the `×` button.
|
|
||||||
--tag-remove-btn-color | Remove (`×`) button text color
|
|
||||||
--tag-remove-btn-bg | Remove (`×`) button background color
|
|
||||||
--tag-remove-btn-bg--hover | Remove (`×`) button hover background color
|
|
||||||
--loader-size | Loading animation size. `1em` is pretty big, default is a bit less.
|
|
||||||
--tag-hide-transition | Controls the transition property when a tag is removed. default is '.3s'
|
|
||||||
--placeholder-color | Placeholder text color
|
|
||||||
--placeholder-color-focus | Placeholder text color when Tagify has focus and no input was typed
|
|
||||||
--input-color | Input text color
|
|
||||||
|
|
||||||
### Full list of Tagify's [SCSS variables](https://github.com/yairEO/tagify/blob/master/src/tagify.scss#L9-L24)
|
|
||||||
|
|
||||||
|
|
||||||
## Methods
|
|
||||||
|
|
||||||
`Tagify` is [prototype](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) based and There are many methods, but I've chosen to list the most relevant ones:
|
|
||||||
|
|
||||||
Name | Parameters | Info
|
|
||||||
-------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------
|
|
||||||
`destroy` | | Reverts the input element back as it was before Tagify was applied
|
|
||||||
`removeAllTags` | | Removes all tags and resets the original input tag's value property
|
|
||||||
`addTags` | <ol><li>`Array`/`String`/`Object` tag(s) to add</li><li>`Boolean` clear input after adding</li><li>`Boolean` - skip adding invalids</li><ol> | Accepts a String (word, single or multiple with a delimiter), an Array of Objects (see above) or Strings
|
|
||||||
`removeTags` | <ol><li>`Array`/`HTMLElement`/`String` tag(s) to remove</li><li>`silent` does not update the component's value</li><li>`tranDuration` Transition duration (in `ms`)</li></ul> | (#502) Remove single/multiple Tags. When nothing passed, removes last tag. <ul><li>`silent` - A flag, which when turned on, does not remove any value and does not update the original input value but simply removes the tag from tagify</li><li>`tranDuration` - delay for animation, after which the tag will be removed from the DOM</li></ul>
|
|
||||||
`addEmptyTag` | `Object` <sub>(`tagData`)</sub> | Create an empty tag (optionally with pre-defined data) and enters "edit" mode directly. [See demo](https://yaireo.github.io/tagify#section-different-look)
|
|
||||||
`loadOriginalValues` | `String`/`Array` | Converts the input's value into tags. This method gets called automatically when instansiating Tagify. Also works for mixed-tags
|
|
||||||
`getWhitelistItemsByValue` | `Object` | `{value}` - return an Array of found matching items (case-insensitive)
|
|
||||||
`getTagIndexByValue` | `String` | Returns the index of a specific tag, by value
|
|
||||||
`getTagElmByValue` | `String` | Returns the first matched tag node, if found
|
|
||||||
`isTagDuplicate` | `String` | Returns how many tags already exists with that value
|
|
||||||
`parseMixTags` | `String` | Converts a String argument (`[[foo]] and [[bar]] are..`) into HTML with mixed tags & texts
|
|
||||||
`getTagElms` | | Returns a DOM nodes list of all the tags
|
|
||||||
`getTagElmByValue` | `String` | Returns a specific tag DOM node by value
|
|
||||||
`tagData` | `HTMLElement`, `Object` | set/get tag data on a tag element (has`.tagify__tag` class by default)
|
|
||||||
`editTag` | `HTMLElement` | Goes to edit-mode in a specific tag
|
|
||||||
`replaceTag` | `tagElm`, `Object` <sub>(`tagData`)</sub> | Exit a tag's edit-mode. if "tagData" exists, replace the tag element with new data and update Tagify value
|
|
||||||
`loading` | `Boolean` | Toogle loading state on/off (Ex. AJAX whitelist pulling)
|
|
||||||
`tagLoading` | `HTMLElement`, Boolean | same as above but for a specific tag element
|
|
||||||
`createTagElem` | `Object` <sub>(`tagData`)</sub> | Returns a tag element from the supplied tag data
|
|
||||||
`injectAtCaret` | `HTMLElement` <sub>(`injectedNode`)</sub>, `Object` <sub>(`range`)</sub> | Injects text or HTML node at last caret position. `range` parameter is *optional*
|
|
||||||
`placeCaretAfterNode` | `HTMLElement` | Places the caret after a given node
|
|
||||||
`insertAfterTag` | `HTMLElement` <sub>(tag element)</sub>, `HTMLElement`/`String` <sub>(whatever to insert after)</sub> |
|
|
||||||
`toggleClass` | `Boolean` | Toggles `class` on the main *tagify* container (`scope`)
|
|
||||||
`dropdown.selectAll` | | Add **all** whitelist items as tags and close the suggestion dropdown
|
|
||||||
`updateValueByDOMTags` | | Iterate tag DOM nodes and re-build the `tagify.value` array (call this if tags get sorted manually)
|
|
||||||
`parseTemplate` | `String`/`Function` <sub>(template name or function)</sub>, `Array` <sub>(data)</sub> | converts a template string (by selecting one from the `settings.templates` by name or supplying a template function which returns a String) into a DOM node
|
|
||||||
`setReadonly` | `Boolean` | Toggles "readonly" mode on/off
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
All triggered events return the instance's scope (tagify).<br>
|
|
||||||
See `e.detail` for custom event additional data.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example 1</summary>
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var tagify = new Tagify(...)
|
|
||||||
|
|
||||||
// events can be chainable, and multiple events may be binded for the same callback
|
|
||||||
tagify
|
|
||||||
.on('input', e => console.log(e.detail))
|
|
||||||
.on('edit:input edit:updated edit:start edit:keydown', e => console.log(e.type, e.detail))
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Example 2</summary>
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var tagify = new Tagify(inputNode, {
|
|
||||||
callbacks: {
|
|
||||||
"change": (e) => console.log(e.detail))
|
|
||||||
"dropdown:show": (e) => console.log(e.detail))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
Name | Info
|
|
||||||
------------------ | --------------------------------------------------------------------------
|
|
||||||
change | Any change to the value has occured. `e.details.value` callback listener argument is a *String*
|
|
||||||
add | A tag has been added
|
|
||||||
remove | A tag has been removed ([use `removeTag`](https://github.com/yairEO/tagify/issues/222) instead with *jQuery*)
|
|
||||||
invalid | A tag has been added but did not pass vaildation. See [event detail](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events)
|
|
||||||
input | [Input](https://developer.mozilla.org/en-US/docs/Web/Events/input) event, when a tag is being typed/edited. `e.detail` exposes `value`, `inputElm` & `isValid`
|
|
||||||
click | Clicking a tag. Exposes the tag element, its index & data
|
|
||||||
dblclick | Double-clicking a tag
|
|
||||||
keydown | When tagify input has focus and a key was pressed
|
|
||||||
focus | The component currently has focus
|
|
||||||
blur | The component lost focus
|
|
||||||
edit:input | Typing inside an edited tag
|
|
||||||
edit:beforeUpdate | Just before a tag has been updated, while still in "edit" mode
|
|
||||||
edit:updated | A tag as been updated (changed view editing or by directly calling the `replaceTag()` method)
|
|
||||||
edit:start | A tag is now in "edit mode"
|
|
||||||
edit:keydown | keydown event while an edited tag is in focus
|
|
||||||
dropdown:show | Suggestions dropdown is to be rendered. The dropdown DOM node is passed in the callback, [see demo](https://yaireo.github.io/tagify/#section-basic).
|
|
||||||
dropdown:hide | Suggestions dropdown has been removed from the DOM
|
|
||||||
dropdown:select | Suggestions dropdown item selected (by mouse/keyboard/touch)
|
|
||||||
dropdown:scroll | Tells the percentage scrolled. (`event.detail.percentage`)
|
|
||||||
dropdown:noMatch | No whitelist suggestion item matched for the the typed input. At this point it is possible to manually set `tagify.suggestedListItems` to any possible custom value, for example: `[{ value:"default" }]`
|
|
||||||
|
|
||||||
|
|
||||||
## Hooks
|
|
||||||
|
|
||||||
**Promise**-based hooks for *async* program flow scenarios.
|
|
||||||
|
|
||||||
Allows to "hook" (intervene) at certain points of the program, which were selected as a suitable place to
|
|
||||||
**pause** the program flow and wait for further instructions on how/if to procceed.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>For example, if a developer wishes to add a (native) confirmation popup before a tag is removed (by a user action):
|
|
||||||
</summary>
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var input = document.querySelector('input')
|
|
||||||
var tagify = new Tagify(input,{
|
|
||||||
hooks: {
|
|
||||||
/**
|
|
||||||
* Removes a tag
|
|
||||||
* @param {Array} tags [Array of Objects [{node:..., data:...}, {...}, ...]]
|
|
||||||
*/
|
|
||||||
beforeRemoveTag : function( tags ){
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
confirm("Remove " + tags[0].data.value + "?")
|
|
||||||
? resolve()
|
|
||||||
: reject()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
```
|
|
||||||
</details>
|
|
||||||
|
|
||||||
|
|
||||||
Name | Parameters | Info
|
|
||||||
---------------------- | ------------------------------------------- | --------------------------------------------------------------------------
|
|
||||||
beforeRemoveTag | Array <sub>(of Objects)</sub> | [Example](https://jsbin.com/xoseyux/edit?html,js,output)
|
|
||||||
suggestionClick | Object <sub>(click event data)</sub> | [Example](https://jsbin.com/tuwihuf/edit?html,js,output)
|
|
||||||
|
|
||||||
## [Settings](https://github.com/yairEO/tagify/blob/master/src/parts/defaults.js#L1)
|
|
||||||
|
|
||||||
Name | Type | Default | Info
|
|
||||||
----------------------- | ---------------------------- | ------------------------------------------- | --------------------------------------------------------------------------
|
|
||||||
tagTextProp | <sub>String</sub> | `value` | Tag data Object property which will be displayed as the tag's text. Remember to keep "value" property <em>unique</em>.
|
|
||||||
placeholder | <sub>String</sub> | | Placeholder text. If this attribute is set on an input/textarea element it will override this setting
|
|
||||||
delimiters | <sub>String</sub> | `,` | [RegEx **string**] split tags by any of these delimiters. Example delimeters: ",|.| " (*comma*, *dot* or *whitespace*)
|
|
||||||
pattern | <sub>String/RegEx</sub> | null | Validate input by RegEx pattern (can also be applied on the input itself as an attribute) Ex: `/[1-9]/`
|
|
||||||
mode | <sub>String</sub> | null | Use `select` for single-value dropdown-like select box. See `mix` as value to allow mixed-content. The 'pattern' setting must be set to some character.
|
|
||||||
mixTagsInterpolator | <sub>Array</sub> | <sub>`['[[', ']]']`</sub> | Interpolation for mix mode. Everything between these will become a tag
|
|
||||||
mixTagsAllowedAfter | <sub>RegEx</sub> | <sub>`/,\|\.\|\:\|\s/`</sub> | Define conditions in which typed mix-tags content is allowing a tag to be created after.
|
|
||||||
duplicates | <sub>Boolean</sub> | false | Should duplicate tags be allowed or not
|
|
||||||
trim | <sub>Boolean</sub> | true | If `true` trim the tag's value (remove before/after whitespaces)
|
|
||||||
enforceWhitelist | <sub>Boolean</sub> | false | Should ONLY use tags allowed in whitelist.<br>In `mix-mode`, setting it to `false` will not allow creating new tags.
|
|
||||||
autoComplete.enabled | <sub>Boolean</sub> | true | Tries to suggest the input's value while typing (match from whitelist) by adding the rest of term as grayed-out text
|
|
||||||
autoComplete.rightKey | <sub>Boolean</sub> | false | If `true`, when `→` is pressed, use the suggested value to create a tag, else just auto-completes the input. In mixed-mode this is ignored and treated as "true"
|
|
||||||
whitelist | <sub>Array</sub> | [] | An array of allowed tags (*Strings* or *Objects*). Also used for auto-completion when `autoCompletion.enabled` is `true`
|
|
||||||
blacklist | <sub>Array</sub> | [] | An array of tags which aren't allowed
|
|
||||||
addTagOnBlur | <sub>Boolean</sub> | true | Automatically adds the text which was inputed as a tag when blur event happens
|
|
||||||
callbacks | <sub>Object</sub> | {} | Exposed callbacks object to be triggered on events: `'add'` / `'remove'` tags
|
|
||||||
maxTags | <sub>Number</sub> | Infinity | Maximum number of allowed tags. when reached, adds a class "tagify--hasMaxTags" to `<Tags>`
|
|
||||||
editTags | <sub>Object/Number</sub> | {} | `false` or `null` will disallow editing
|
|
||||||
editTags.clicks | <sub>Number</sub> | 2 | Number of clicks to enter "edit-mode": 1 for single click. Any other value is considered as double-click
|
|
||||||
editTags.keepInvalid | <sub>Boolean</sub> | true | keeps invalid edits as-is until `esc` is pressed while in focus
|
|
||||||
templates | <sub>Object</sub> | <sub>`wrapper`, `tag`, `dropdownItem`</sub> | Object consisting of functions which return template strings
|
|
||||||
transformTag | <sub>Function</sub> | undefined | Takes a tag data as argument and allows mutating it before a tag is created or edited.<br>Should not `return` anything, only **mutate**.
|
|
||||||
keepInvalidTags | <sub>Boolean</sub> | false | If `true`, do not remove tags which did not pass validation
|
|
||||||
skipInvalid | <sub>Boolean</sub> | false | If `true`, do not add invalid, temporary, tags before automatically removing them
|
|
||||||
backspace | <sub>*</sub> | true | On pressing backspace key:<br> `true` - remove last tag <br>`edit` - edit last tag
|
|
||||||
originalInputValueFormat| <sub>Function</sub> | | If you wish your original input/textarea `value` property format to other than the default (which I recommend keeping) you may use this and make sure it returns a *string*.
|
|
||||||
mixMode.insertAfterTag | <sub>Node/String</sub> | `\u00A0` | `node` or `string` to add after a tag added |
|
|
||||||
dropdown.enabled | <sub>Number</sub> | 2 | Minimum characters input for showing a suggestions list. `false` will not render a suggestions list.
|
|
||||||
dropdown.caseSensitive | <sub>Boolean</sub> | false | if `true`, match **exact** item when a suggestion is selected (from the dropdown) and also more strict matching for dulpicate items. **Ensure** `fuzzySearch` is `false` for this to work.
|
|
||||||
dropdown.maxItems | <sub>Number</sub> | 10 | Maximum items to show in the suggestions list
|
|
||||||
dropdown.classname | <sub>String</sub> | `""` | Custom *classname* for the dropdown suggestions selectbox
|
|
||||||
dropdown.fuzzySearch | <sub>Boolean</sub> | true | Enables filtering dropdown items values' by string *containing* and not only *beginning*
|
|
||||||
dropdown.accentedSearch | <sub>Boolean</sub> | true | Enable searching for <em>accented</em> items in the whitelist without typing exact match (#491)
|
|
||||||
dropdown.position | <sub>String</sub> | null | <ul><li>`manual` - will not render the dropdown, and you would need to do it yourself. [See demo](https://yaireo.github.io/tagify/#section-manual-suggestions)</li><li>`text` - will place the dropdown next to the caret</li><li>`input` - will place the dropdown next to the input</li><li>`all` - normal, full-width design</li></ul>
|
|
||||||
dropdown.highlightFirst | <sub>Boolean</sub> | false | When a suggestions list is shown, highlight the first item, and also suggest it in the input (The suggestion can be accepted with <kbd>→</kbd> key)
|
|
||||||
dropdown.closeOnSelect | <sub>Boolean</sub> | true | close the dropdown after selecting an item, if `enabled:0` is set (which means always show dropdown on focus)
|
|
||||||
dropdown.clearOnSelect | <sub>Boolean</sub> | true | Keep typed text after selecting a suggestion
|
|
||||||
dropdown.mapValueTo | <sub>Function/String</sub> | | if whitelist is an Array of Objects:<br>Ex. `[{value:'foo', email:'foo@a.com'},...]`)<br> this setting controlls which data <em>key</em> will be printed in the dropdown.<br> Ex. `mapValueTo: data => "To:" + data.email`<br>Ex. `mapValueTo: "email"`
|
|
||||||
dropdown.searchKeys | <sub>Array</sub> | <sub>`["value", "searchBy"]`</sub> | When a user types something and trying to match the whitelist items for suggestions, this setting allows matching other keys of a whitelist objects
|
|
||||||
dropdown.appendTarget | <sub>HTMLNode</sub> | `document.body` | Target-Node which the *suggestions dropdown* is appended to (*only when rendered*)
|
|
119
node_modules/@yaireo/tagify/package.json
generated
vendored
119
node_modules/@yaireo/tagify/package.json
generated
vendored
|
@ -1,119 +0,0 @@
|
||||||
{
|
|
||||||
"_from": "@yaireo/tagify",
|
|
||||||
"_id": "@yaireo/tagify@3.21.0",
|
|
||||||
"_inBundle": false,
|
|
||||||
"_integrity": "sha512-pTr85RlPa7skaUgWCZkaqKpwsAkAiNi5FdLbXFnrhTo+oDMZb0w2rndKIca2bggwWUSsHC+TP2BY7ju8KZu9Gg==",
|
|
||||||
"_location": "/@yaireo/tagify",
|
|
||||||
"_npmUser": {
|
|
||||||
"name": "vsync",
|
|
||||||
"email": "vsync.design@gmail.com"
|
|
||||||
},
|
|
||||||
"_phantomChildren": {},
|
|
||||||
"_requested": {
|
|
||||||
"type": "tag",
|
|
||||||
"registry": true,
|
|
||||||
"raw": "@yaireo/tagify",
|
|
||||||
"name": "@yaireo/tagify",
|
|
||||||
"escapedName": "@yaireo%2ftagify",
|
|
||||||
"scope": "@yaireo",
|
|
||||||
"rawSpec": "",
|
|
||||||
"saveSpec": null,
|
|
||||||
"fetchSpec": "latest"
|
|
||||||
},
|
|
||||||
"_requiredBy": [
|
|
||||||
"#USER",
|
|
||||||
"/"
|
|
||||||
],
|
|
||||||
"_resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-3.21.0.tgz",
|
|
||||||
"_shasum": "4864edc827911d397b519d702791987445f97031",
|
|
||||||
"_spec": "@yaireo/tagify",
|
|
||||||
"_where": "/home/watney/Downloads/virtpool-master/virtpool",
|
|
||||||
"author": {
|
|
||||||
"name": "Yair Even-Or",
|
|
||||||
"email": "vsync.design@gmail.com"
|
|
||||||
},
|
|
||||||
"browserslist": [
|
|
||||||
">1%",
|
|
||||||
"not dead",
|
|
||||||
"not ie < 11",
|
|
||||||
"not IE_Mob 11",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/yaireo/tagify/issues"
|
|
||||||
},
|
|
||||||
"bundleDependencies": false,
|
|
||||||
"deprecated": false,
|
|
||||||
"description": "lightweight, efficient Tags input component in Vanilla JS / React / Angular [super customizable, tiny size & top performance]",
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.5.5",
|
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.9.6",
|
|
||||||
"@babel/plugin-transform-destructuring": "^7.9.5",
|
|
||||||
"@babel/preset-env": "^7.9.6",
|
|
||||||
"@babel/preset-react": "^7.9.4",
|
|
||||||
"@rollup/plugin-babel": "^5.1.0",
|
|
||||||
"@rollup/stream": "git+https://github.com/andremacola/stream.git",
|
|
||||||
"beepbeep": "^1.2.2",
|
|
||||||
"gulp": "^4.0.2",
|
|
||||||
"gulp-autoprefixer": "7.0.1",
|
|
||||||
"gulp-babel": "^8.0.0",
|
|
||||||
"gulp-bump": "^3.1.3",
|
|
||||||
"gulp-cached": "^1.1.1",
|
|
||||||
"gulp-clean-css": "^4.3.0",
|
|
||||||
"gulp-combine-mq": "^0.4.0",
|
|
||||||
"gulp-concat": "^2.6.1",
|
|
||||||
"gulp-css-globbing": "^0.2.2",
|
|
||||||
"gulp-eslint": "^6.0.0",
|
|
||||||
"gulp-filter": "^6.0.0",
|
|
||||||
"gulp-git": "^2.10.1",
|
|
||||||
"gulp-header-comment": "^0.8.0",
|
|
||||||
"gulp-insert": "^0.5.0",
|
|
||||||
"gulp-load-plugins": "^2.0.1",
|
|
||||||
"gulp-rename": "^1.4.0",
|
|
||||||
"gulp-replace": "^1.0.0",
|
|
||||||
"gulp-sass": "^4.1.0",
|
|
||||||
"gulp-sourcemaps": "^2.6.5",
|
|
||||||
"gulp-streamify": "^1.0.2",
|
|
||||||
"gulp-tag-version": "^1.3.1",
|
|
||||||
"gulp-tap": "^2.0.0",
|
|
||||||
"gulp-uglify": "^3.0.2",
|
|
||||||
"gulp-umd": "^2.0.0",
|
|
||||||
"gulp-util": "^3.0.8",
|
|
||||||
"gulp-watch": "^5.0.1",
|
|
||||||
"path": "^0.12.7",
|
|
||||||
"puppeteer": "^1.20.0",
|
|
||||||
"rollup": "^2.22.1",
|
|
||||||
"rollup-plugin-banner": "^0.2.1",
|
|
||||||
"rollup-plugin-terser": "^6.1.0",
|
|
||||||
"rollup-plugin-uglify": "^6.0.4",
|
|
||||||
"run-sequence": "^2.2.1",
|
|
||||||
"semver": "^7.3.2",
|
|
||||||
"vinyl-source-stream": "^2.0.0"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/yairEO/tagify",
|
|
||||||
"jest": {
|
|
||||||
"preset": "jest-puppeteer"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"tags",
|
|
||||||
"tagging",
|
|
||||||
"component",
|
|
||||||
"tag",
|
|
||||||
"ui"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"main": "./dist/tagify.min.js",
|
|
||||||
"name": "@yaireo/tagify",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/yairEO/tagify.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"push": "git push origin",
|
|
||||||
"push:tags": "git push origin --tags",
|
|
||||||
"serve": "npx http-server -o index.html -c-1",
|
|
||||||
"start": "npx gulp@4.0.2",
|
|
||||||
"test": "jest"
|
|
||||||
},
|
|
||||||
"version": "3.21.0"
|
|
||||||
}
|
|
13
node_modules/@yaireo/tagify/roadmap.md
generated
vendored
13
node_modules/@yaireo/tagify/roadmap.md
generated
vendored
|
@ -1,13 +0,0 @@
|
||||||
- [x] add SCSS variable for input color (not tag color)
|
|
||||||
- [x] Make demo page *mobile-friendly* using *media-queries*
|
|
||||||
- [x] dragable sortable tags
|
|
||||||
- [ ] Make (regular-mode) tagify accessible by keyboard by navigating left/right arrow and able to delete tags
|
|
||||||
- [ ] 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.
|
|
||||||
(need to check if the "focus" event was fired programatically and if it, place the caret at the end)
|
|
||||||
- [ ] mix-mode: maybe convert `settings.pattern` to always be a regex. Currently it may be a String (this fails "validateTag" method)
|
|
||||||
- [ ] mix-mode: add "prefix" to tags, so when double-clicking to edit, it will show it
|
|
||||||
- [ ] maybe trigger the "invalid" event also for edited tags. need to think when exactly. probbaly not on "input" event, it's too much
|
|
||||||
- [ ] use DOM mutation to detect changes and update "this.value" automatically
|
|
||||||
- [ ] add examples of added tag CSS effects
|
|
||||||
- [ ] allow templates to return DOM nodes and not just Strings
|
|
||||||
- [ ] Make *readonly* tags work in *mix-mode*
|
|
28
node_modules/@yaireo/tagify/src/polyfills/Array.findIndex.js
generated
vendored
28
node_modules/@yaireo/tagify/src/polyfills/Array.findIndex.js
generated
vendored
|
@ -1,28 +0,0 @@
|
||||||
if (!Array.prototype.findIndex) {
|
|
||||||
Object.defineProperty(Array.prototype, 'findIndex', {
|
|
||||||
value: function(predicate) {
|
|
||||||
if (this == null)
|
|
||||||
throw new TypeError('"this" is null or not defined');
|
|
||||||
|
|
||||||
var o = Object(this), len = o.length >>> 0;
|
|
||||||
|
|
||||||
if (typeof predicate !== 'function') {
|
|
||||||
throw new TypeError('predicate must be a function');
|
|
||||||
}
|
|
||||||
|
|
||||||
var thisArg = arguments[1], k = 0;
|
|
||||||
|
|
||||||
while (k < len) {
|
|
||||||
var kValue = o[k];
|
|
||||||
if (predicate.call(thisArg, kValue, k, o)) {
|
|
||||||
return k;
|
|
||||||
}
|
|
||||||
k++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
},
|
|
||||||
configurable: true,
|
|
||||||
writable: true
|
|
||||||
})
|
|
||||||
}
|
|
26
node_modules/@yaireo/tagify/src/polyfills/Array.some.js
generated
vendored
26
node_modules/@yaireo/tagify/src/polyfills/Array.some.js
generated
vendored
|
@ -1,26 +0,0 @@
|
||||||
// Production steps of ECMA-262, Edition 5, 15.4.4.17
|
|
||||||
// Reference: http://es5.github.io/#x15.4.4.17
|
|
||||||
if (!Array.prototype.some) {
|
|
||||||
Array.prototype.some = function(fun, thisArg) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
if (this == null) {
|
|
||||||
throw new TypeError('Array.prototype.some called on null or undefined');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof fun !== 'function') {
|
|
||||||
throw new TypeError();
|
|
||||||
}
|
|
||||||
|
|
||||||
var t = Object(this);
|
|
||||||
var len = t.length >>> 0;
|
|
||||||
|
|
||||||
for (var i = 0; i < len; i++) {
|
|
||||||
if (i in t && fun.call(thisArg, t[i], i, t)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
3
node_modules/@yaireo/tagify/src/polyfills/AutoUrlDetect.js
generated
vendored
3
node_modules/@yaireo/tagify/src/polyfills/AutoUrlDetect.js
generated
vendored
|
@ -1,3 +0,0 @@
|
||||||
// Avoid transformation text to link ie contentEditable mode
|
|
||||||
// https://stackoverflow.com/q/7556007/104380
|
|
||||||
document.execCommand("AutoUrlDetect", false, false);
|
|
263
node_modules/@yaireo/tagify/src/polyfills/Element.classList.js
generated
vendored
263
node_modules/@yaireo/tagify/src/polyfills/Element.classList.js
generated
vendored
|
@ -1,263 +0,0 @@
|
||||||
/*
|
|
||||||
* classList.js: Cross-browser full element.classList implementation.
|
|
||||||
* 1.2.20171210
|
|
||||||
*
|
|
||||||
* By Eli Grey, http://eligrey.com
|
|
||||||
* License: Dedicated to the public domain.
|
|
||||||
* See https://github.com/eligrey/classList.js/blob/master/LICENSE.md
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*global self, document, DOMException */
|
|
||||||
|
|
||||||
/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
|
|
||||||
|
|
||||||
if ("document" in self) {
|
|
||||||
|
|
||||||
// Full polyfill for browsers with no classList support
|
|
||||||
// Including IE < Edge missing SVGElement.classList
|
|
||||||
if (
|
|
||||||
!("classList" in document.createElement("_"))
|
|
||||||
|| document.createElementNS
|
|
||||||
&& !("classList" in document.createElementNS("http://www.w3.org/2000/svg","g"))
|
|
||||||
) {
|
|
||||||
|
|
||||||
(function (view) {
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
if (!('Element' in view)) return;
|
|
||||||
|
|
||||||
var
|
|
||||||
classListProp = "classList"
|
|
||||||
, protoProp = "prototype"
|
|
||||||
, elemCtrProto = view.Element[protoProp]
|
|
||||||
, objCtr = Object
|
|
||||||
, strTrim = String[protoProp].trim || function () {
|
|
||||||
return this.replace(/^\s+|\s+$/g, "");
|
|
||||||
}
|
|
||||||
, arrIndexOf = Array[protoProp].indexOf || function (item) {
|
|
||||||
var
|
|
||||||
i = 0
|
|
||||||
, len = this.length
|
|
||||||
;
|
|
||||||
for (; i < len; i++) {
|
|
||||||
if (i in this && this[i] === item) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
// Vendors: please allow content code to instantiate DOMExceptions
|
|
||||||
, DOMEx = function (type, message) {
|
|
||||||
this.name = type;
|
|
||||||
this.code = DOMException[type];
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
, checkTokenAndGetIndex = function (classList, token) {
|
|
||||||
if (token === "") {
|
|
||||||
throw new DOMEx(
|
|
||||||
"SYNTAX_ERR"
|
|
||||||
, "The token must not be empty."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (/\s/.test(token)) {
|
|
||||||
throw new DOMEx(
|
|
||||||
"INVALID_CHARACTER_ERR"
|
|
||||||
, "The token must not contain space characters."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return arrIndexOf.call(classList, token);
|
|
||||||
}
|
|
||||||
, ClassList = function (elem) {
|
|
||||||
var
|
|
||||||
trimmedClasses = strTrim.call(elem.getAttribute("class") || "")
|
|
||||||
, classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []
|
|
||||||
, i = 0
|
|
||||||
, len = classes.length
|
|
||||||
;
|
|
||||||
for (; i < len; i++) {
|
|
||||||
this.push(classes[i]);
|
|
||||||
}
|
|
||||||
this._updateClassName = function () {
|
|
||||||
elem.setAttribute("class", this.toString());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
, classListProto = ClassList[protoProp] = []
|
|
||||||
, classListGetter = function () {
|
|
||||||
return new ClassList(this);
|
|
||||||
}
|
|
||||||
;
|
|
||||||
// Most DOMException implementations don't allow calling DOMException's toString()
|
|
||||||
// on non-DOMExceptions. Error's toString() is sufficient here.
|
|
||||||
DOMEx[protoProp] = Error[protoProp];
|
|
||||||
classListProto.item = function (i) {
|
|
||||||
return this[i] || null;
|
|
||||||
};
|
|
||||||
classListProto.contains = function (token) {
|
|
||||||
return ~checkTokenAndGetIndex(this, token + "");
|
|
||||||
};
|
|
||||||
classListProto.add = function () {
|
|
||||||
var
|
|
||||||
tokens = arguments
|
|
||||||
, i = 0
|
|
||||||
, l = tokens.length
|
|
||||||
, token
|
|
||||||
, updated = false
|
|
||||||
;
|
|
||||||
do {
|
|
||||||
token = tokens[i] + "";
|
|
||||||
if (!~checkTokenAndGetIndex(this, token)) {
|
|
||||||
this.push(token);
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (++i < l);
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
this._updateClassName();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
classListProto.remove = function () {
|
|
||||||
var
|
|
||||||
tokens = arguments
|
|
||||||
, i = 0
|
|
||||||
, l = tokens.length
|
|
||||||
, token
|
|
||||||
, updated = false
|
|
||||||
, index
|
|
||||||
;
|
|
||||||
do {
|
|
||||||
token = tokens[i] + "";
|
|
||||||
index = checkTokenAndGetIndex(this, token);
|
|
||||||
while (~index) {
|
|
||||||
this.splice(index, 1);
|
|
||||||
updated = true;
|
|
||||||
index = checkTokenAndGetIndex(this, token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (++i < l);
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
this._updateClassName();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
classListProto.toggle = function (token, force) {
|
|
||||||
var
|
|
||||||
result = this.contains(token)
|
|
||||||
, method = result ?
|
|
||||||
force !== true && "remove"
|
|
||||||
:
|
|
||||||
force !== false && "add"
|
|
||||||
;
|
|
||||||
|
|
||||||
if (method) {
|
|
||||||
this[method](token);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (force === true || force === false) {
|
|
||||||
return force;
|
|
||||||
} else {
|
|
||||||
return !result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
classListProto.replace = function (token, replacement_token) {
|
|
||||||
var index = checkTokenAndGetIndex(token + "");
|
|
||||||
if (~index) {
|
|
||||||
this.splice(index, 1, replacement_token);
|
|
||||||
this._updateClassName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
classListProto.toString = function () {
|
|
||||||
return this.join(" ");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (objCtr.defineProperty) {
|
|
||||||
var classListPropDesc = {
|
|
||||||
get: classListGetter
|
|
||||||
, enumerable: true
|
|
||||||
, configurable: true
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
|
|
||||||
} catch (ex) { // IE 8 doesn't support enumerable:true
|
|
||||||
// adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36
|
|
||||||
// modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected
|
|
||||||
if (ex.number === undefined || ex.number === -0x7FF5EC54) {
|
|
||||||
classListPropDesc.enumerable = false;
|
|
||||||
objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (objCtr[protoProp].__defineGetter__) {
|
|
||||||
elemCtrProto.__defineGetter__(classListProp, classListGetter);
|
|
||||||
}
|
|
||||||
|
|
||||||
}(self));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// There is full or partial native classList support, so just check if we need
|
|
||||||
// to normalize the add/remove and toggle APIs.
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var testElement = document.createElement("_");
|
|
||||||
|
|
||||||
testElement.classList.add("c1", "c2");
|
|
||||||
|
|
||||||
// Polyfill for IE 10/11 and Firefox <26, where classList.add and
|
|
||||||
// classList.remove exist but support only one argument at a time.
|
|
||||||
if (!testElement.classList.contains("c2")) {
|
|
||||||
var createMethod = function(method) {
|
|
||||||
var original = DOMTokenList.prototype[method];
|
|
||||||
|
|
||||||
DOMTokenList.prototype[method] = function(token) {
|
|
||||||
var i, len = arguments.length;
|
|
||||||
|
|
||||||
for (i = 0; i < len; i++) {
|
|
||||||
token = arguments[i];
|
|
||||||
original.call(this, token);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
createMethod('add');
|
|
||||||
createMethod('remove');
|
|
||||||
}
|
|
||||||
|
|
||||||
testElement.classList.toggle("c3", false);
|
|
||||||
|
|
||||||
// Polyfill for IE 10 and Firefox <24, where classList.toggle does not
|
|
||||||
// support the second argument.
|
|
||||||
if (testElement.classList.contains("c3")) {
|
|
||||||
var _toggle = DOMTokenList.prototype.toggle;
|
|
||||||
|
|
||||||
DOMTokenList.prototype.toggle = function(token, force) {
|
|
||||||
if (1 in arguments && !this.contains(token) === !force) {
|
|
||||||
return force;
|
|
||||||
} else {
|
|
||||||
return _toggle.call(this, token);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace() polyfill
|
|
||||||
if (!("replace" in document.createElement("_").classList)) {
|
|
||||||
DOMTokenList.prototype.replace = function (token, replacement_token) {
|
|
||||||
var
|
|
||||||
tokens = this.toString().split(" ")
|
|
||||||
, index = tokens.indexOf(token + "")
|
|
||||||
;
|
|
||||||
if (~index) {
|
|
||||||
tokens = tokens.slice(index);
|
|
||||||
this.remove.apply(this, tokens);
|
|
||||||
this.add(replacement_token);
|
|
||||||
this.add.apply(this, tokens.slice(1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testElement = null;
|
|
||||||
}());
|
|
||||||
|
|
||||||
}
|
|
11
node_modules/@yaireo/tagify/src/polyfills/Element.closest.js
generated
vendored
11
node_modules/@yaireo/tagify/src/polyfills/Element.closest.js
generated
vendored
|
@ -1,11 +0,0 @@
|
||||||
if (!Element.prototype.closest) {
|
|
||||||
Element.prototype.closest = function(s) {
|
|
||||||
var el = this;
|
|
||||||
if (!document.documentElement.contains(el)) return null;
|
|
||||||
do {
|
|
||||||
if (el.matches(s)) return el;
|
|
||||||
el = el.parentElement || el.parentNode;
|
|
||||||
} while (el !== null && el.nodeType === 1);
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
}
|
|
3
node_modules/@yaireo/tagify/src/polyfills/Element.matches.js
generated
vendored
3
node_modules/@yaireo/tagify/src/polyfills/Element.matches.js
generated
vendored
|
@ -1,3 +0,0 @@
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
|
|
||||||
if (!Element.prototype.matches)
|
|
||||||
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
|
|
13
node_modules/@yaireo/tagify/src/polyfills/Event.js
generated
vendored
13
node_modules/@yaireo/tagify/src/polyfills/Event.js
generated
vendored
|
@ -1,13 +0,0 @@
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
|
|
||||||
function CustomEventPolyfill ( event, params ) {
|
|
||||||
params = params || { bubbles: false, cancelable: false, detail: undefined };
|
|
||||||
var evt = document.createEvent( 'CustomEvent' );
|
|
||||||
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
|
|
||||||
return evt;
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomEventPolyfill.prototype = window.Event.prototype;
|
|
||||||
|
|
||||||
if ( typeof window.CustomEvent !== "function" ){
|
|
||||||
window.CustomEvent = CustomEventPolyfill;
|
|
||||||
}
|
|
3
node_modules/@yaireo/tagify/src/polyfills/NodeList.forEach.js
generated
vendored
3
node_modules/@yaireo/tagify/src/polyfills/NodeList.forEach.js
generated
vendored
|
@ -1,3 +0,0 @@
|
||||||
if( window.NodeList && !NodeList.prototype.forEach ){
|
|
||||||
NodeList.prototype.forEach = Array.prototype.forEach;
|
|
||||||
}
|
|
30
node_modules/@yaireo/tagify/src/polyfills/Object.assign.js
generated
vendored
30
node_modules/@yaireo/tagify/src/polyfills/Object.assign.js
generated
vendored
|
@ -1,30 +0,0 @@
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
|
|
||||||
//
|
|
||||||
if (typeof Object.assign != 'function') {
|
|
||||||
// Must be writable: true, enumerable: false, configurable: true
|
|
||||||
Object.defineProperty(Object, "assign", {
|
|
||||||
value: function assign(target, varArgs) { // .length of function is 2
|
|
||||||
if (target == null) { // TypeError if undefined or null
|
|
||||||
throw new TypeError('Cannot convert undefined or null to object');
|
|
||||||
}
|
|
||||||
|
|
||||||
var to = Object(target);
|
|
||||||
|
|
||||||
for (var index = 1; index < arguments.length; index++) {
|
|
||||||
var nextSource = arguments[index];
|
|
||||||
|
|
||||||
if (nextSource != null) { // Skip over if undefined or null
|
|
||||||
for (var nextKey in nextSource) {
|
|
||||||
// Avoid bugs when hasOwnProperty is shadowed
|
|
||||||
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
|
||||||
to[nextKey] = nextSource[nextKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return to;
|
|
||||||
},
|
|
||||||
writable: true,
|
|
||||||
configurable: true
|
|
||||||
});
|
|
||||||
}
|
|
12
node_modules/@yaireo/tagify/src/polyfills/String.includes.js
generated
vendored
12
node_modules/@yaireo/tagify/src/polyfills/String.includes.js
generated
vendored
|
@ -1,12 +0,0 @@
|
||||||
if( !String.prototype.includes ){
|
|
||||||
String.prototype.includes = function(search, start) {
|
|
||||||
if (typeof start !== 'number')
|
|
||||||
start = 0;
|
|
||||||
|
|
||||||
if (start + search.length > this.length)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
else
|
|
||||||
return this.indexOf(search, start) !== -1;
|
|
||||||
};
|
|
||||||
}
|
|
2
node_modules/@yaireo/tagify/src/polyfills/String.trim.js
generated
vendored
2
node_modules/@yaireo/tagify/src/polyfills/String.trim.js
generated
vendored
|
@ -1,2 +0,0 @@
|
||||||
// 1. String.prototype.trim polyfill
|
|
||||||
if (!"".trim) String.prototype.trim = function(){ return this.replace(/^[\s]+|[\s]+$/g, ''); };
|
|
1172
node_modules/@yaireo/tagify/src/polyfills/es6-promise.js
generated
vendored
1172
node_modules/@yaireo/tagify/src/polyfills/es6-promise.js
generated
vendored
|
@ -1,1172 +0,0 @@
|
||||||
/*!
|
|
||||||
* @overview es6-promise - a tiny implementation of Promises/A+.
|
|
||||||
* @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
|
|
||||||
* @license Licensed under MIT license
|
|
||||||
* See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
|
|
||||||
* @version v4.2.8+1e68dce6
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function (global, factory) {
|
|
||||||
global.Promise = factory()
|
|
||||||
}(window, (function () { 'use strict';
|
|
||||||
|
|
||||||
function objectOrFunction(x) {
|
|
||||||
var type = typeof x;
|
|
||||||
return x !== null && (type === 'object' || type === 'function');
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFunction(x) {
|
|
||||||
return typeof x === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var _isArray = void 0;
|
|
||||||
if (Array.isArray) {
|
|
||||||
_isArray = Array.isArray;
|
|
||||||
} else {
|
|
||||||
_isArray = function (x) {
|
|
||||||
return Object.prototype.toString.call(x) === '[object Array]';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var isArray = _isArray;
|
|
||||||
|
|
||||||
var len = 0;
|
|
||||||
var vertxNext = void 0;
|
|
||||||
var customSchedulerFn = void 0;
|
|
||||||
|
|
||||||
var asap = function asap(callback, arg) {
|
|
||||||
queue[len] = callback;
|
|
||||||
queue[len + 1] = arg;
|
|
||||||
len += 2;
|
|
||||||
if (len === 2) {
|
|
||||||
// If len is 2, that means that we need to schedule an async flush.
|
|
||||||
// If additional callbacks are queued before the queue is flushed, they
|
|
||||||
// will be processed by this flush that we are scheduling.
|
|
||||||
if (customSchedulerFn) {
|
|
||||||
customSchedulerFn(flush);
|
|
||||||
} else {
|
|
||||||
scheduleFlush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function setScheduler(scheduleFn) {
|
|
||||||
customSchedulerFn = scheduleFn;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setAsap(asapFn) {
|
|
||||||
asap = asapFn;
|
|
||||||
}
|
|
||||||
|
|
||||||
var browserWindow = typeof window !== 'undefined' ? window : undefined;
|
|
||||||
var browserGlobal = browserWindow || {};
|
|
||||||
var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
|
|
||||||
var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && {}.toString.call(process) === '[object process]';
|
|
||||||
|
|
||||||
// test for web worker but not in IE10
|
|
||||||
var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined';
|
|
||||||
|
|
||||||
// node
|
|
||||||
function useNextTick() {
|
|
||||||
// node version 0.10.x displays a deprecation warning when nextTick is used recursively
|
|
||||||
// see https://github.com/cujojs/when/issues/410 for details
|
|
||||||
return function () {
|
|
||||||
return process.nextTick(flush);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// vertx
|
|
||||||
function useVertxTimer() {
|
|
||||||
if (typeof vertxNext !== 'undefined') {
|
|
||||||
return function () {
|
|
||||||
vertxNext(flush);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return useSetTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
function useMutationObserver() {
|
|
||||||
var iterations = 0;
|
|
||||||
var observer = new BrowserMutationObserver(flush);
|
|
||||||
var node = document.createTextNode('');
|
|
||||||
observer.observe(node, { characterData: true });
|
|
||||||
|
|
||||||
return function () {
|
|
||||||
node.data = iterations = ++iterations % 2;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// web worker
|
|
||||||
function useMessageChannel() {
|
|
||||||
var channel = new MessageChannel();
|
|
||||||
channel.port1.onmessage = flush;
|
|
||||||
return function () {
|
|
||||||
return channel.port2.postMessage(0);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSetTimeout() {
|
|
||||||
// Store setTimeout reference so es6-promise will be unaffected by
|
|
||||||
// other code modifying setTimeout (like sinon.useFakeTimers())
|
|
||||||
var globalSetTimeout = setTimeout;
|
|
||||||
return function () {
|
|
||||||
return globalSetTimeout(flush, 1);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var queue = new Array(1000);
|
|
||||||
function flush() {
|
|
||||||
for (var i = 0; i < len; i += 2) {
|
|
||||||
var callback = queue[i];
|
|
||||||
var arg = queue[i + 1];
|
|
||||||
|
|
||||||
callback(arg);
|
|
||||||
|
|
||||||
queue[i] = undefined;
|
|
||||||
queue[i + 1] = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
len = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function attemptVertx() {
|
|
||||||
try {
|
|
||||||
var vertx = Function('return this')().require('vertx');
|
|
||||||
vertxNext = vertx.runOnLoop || vertx.runOnContext;
|
|
||||||
return useVertxTimer();
|
|
||||||
} catch (e) {
|
|
||||||
return useSetTimeout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var scheduleFlush = void 0;
|
|
||||||
// Decide what async method to use to triggering processing of queued callbacks:
|
|
||||||
if (isNode) {
|
|
||||||
scheduleFlush = useNextTick();
|
|
||||||
} else if (BrowserMutationObserver) {
|
|
||||||
scheduleFlush = useMutationObserver();
|
|
||||||
} else if (isWorker) {
|
|
||||||
scheduleFlush = useMessageChannel();
|
|
||||||
} else if (browserWindow === undefined && typeof require === 'function') {
|
|
||||||
scheduleFlush = attemptVertx();
|
|
||||||
} else {
|
|
||||||
scheduleFlush = useSetTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
function then(onFulfillment, onRejection) {
|
|
||||||
var parent = this;
|
|
||||||
|
|
||||||
var child = new this.constructor(noop);
|
|
||||||
|
|
||||||
if (child[PROMISE_ID] === undefined) {
|
|
||||||
makePromise(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
var _state = parent._state;
|
|
||||||
|
|
||||||
|
|
||||||
if (_state) {
|
|
||||||
var callback = arguments[_state - 1];
|
|
||||||
asap(function () {
|
|
||||||
return invokeCallback(_state, child, callback, parent._result);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
subscribe(parent, child, onFulfillment, onRejection);
|
|
||||||
}
|
|
||||||
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
`Promise.resolve` returns a promise that will become resolved with the
|
|
||||||
passed `value`. It is shorthand for the following:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let promise = new Promise(function(resolve, reject){
|
|
||||||
resolve(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.then(function(value){
|
|
||||||
// value === 1
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Instead of writing the above, your code now simply becomes the following:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let promise = Promise.resolve(1);
|
|
||||||
|
|
||||||
promise.then(function(value){
|
|
||||||
// value === 1
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
@method resolve
|
|
||||||
@static
|
|
||||||
@param {Any} value value that the returned promise will be resolved with
|
|
||||||
Useful for tooling.
|
|
||||||
@return {Promise} a promise that will become fulfilled with the given
|
|
||||||
`value`
|
|
||||||
*/
|
|
||||||
function resolve$1(object) {
|
|
||||||
/*jshint validthis:true */
|
|
||||||
var Constructor = this;
|
|
||||||
|
|
||||||
if (object && typeof object === 'object' && object.constructor === Constructor) {
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
|
|
||||||
var promise = new Constructor(noop);
|
|
||||||
resolve(promise, object);
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
var PROMISE_ID = Math.random().toString(36).substring(2);
|
|
||||||
|
|
||||||
function noop() {}
|
|
||||||
|
|
||||||
var PENDING = void 0;
|
|
||||||
var FULFILLED = 1;
|
|
||||||
var REJECTED = 2;
|
|
||||||
|
|
||||||
function selfFulfillment() {
|
|
||||||
return new TypeError("You cannot resolve a promise with itself");
|
|
||||||
}
|
|
||||||
|
|
||||||
function cannotReturnOwn() {
|
|
||||||
return new TypeError('A promises callback cannot return that same promise.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function tryThen(then$$1, value, fulfillmentHandler, rejectionHandler) {
|
|
||||||
try {
|
|
||||||
then$$1.call(value, fulfillmentHandler, rejectionHandler);
|
|
||||||
} catch (e) {
|
|
||||||
return e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleForeignThenable(promise, thenable, then$$1) {
|
|
||||||
asap(function (promise) {
|
|
||||||
var sealed = false;
|
|
||||||
var error = tryThen(then$$1, thenable, function (value) {
|
|
||||||
if (sealed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sealed = true;
|
|
||||||
if (thenable !== value) {
|
|
||||||
resolve(promise, value);
|
|
||||||
} else {
|
|
||||||
fulfill(promise, value);
|
|
||||||
}
|
|
||||||
}, function (reason) {
|
|
||||||
if (sealed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sealed = true;
|
|
||||||
|
|
||||||
reject(promise, reason);
|
|
||||||
}, 'Settle: ' + (promise._label || ' unknown promise'));
|
|
||||||
|
|
||||||
if (!sealed && error) {
|
|
||||||
sealed = true;
|
|
||||||
reject(promise, error);
|
|
||||||
}
|
|
||||||
}, promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOwnThenable(promise, thenable) {
|
|
||||||
if (thenable._state === FULFILLED) {
|
|
||||||
fulfill(promise, thenable._result);
|
|
||||||
} else if (thenable._state === REJECTED) {
|
|
||||||
reject(promise, thenable._result);
|
|
||||||
} else {
|
|
||||||
subscribe(thenable, undefined, function (value) {
|
|
||||||
return resolve(promise, value);
|
|
||||||
}, function (reason) {
|
|
||||||
return reject(promise, reason);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMaybeThenable(promise, maybeThenable, then$$1) {
|
|
||||||
if (maybeThenable.constructor === promise.constructor && then$$1 === then && maybeThenable.constructor.resolve === resolve$1) {
|
|
||||||
handleOwnThenable(promise, maybeThenable);
|
|
||||||
} else {
|
|
||||||
if (then$$1 === undefined) {
|
|
||||||
fulfill(promise, maybeThenable);
|
|
||||||
} else if (isFunction(then$$1)) {
|
|
||||||
handleForeignThenable(promise, maybeThenable, then$$1);
|
|
||||||
} else {
|
|
||||||
fulfill(promise, maybeThenable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolve(promise, value) {
|
|
||||||
if (promise === value) {
|
|
||||||
reject(promise, selfFulfillment());
|
|
||||||
} else if (objectOrFunction(value)) {
|
|
||||||
var then$$1 = void 0;
|
|
||||||
try {
|
|
||||||
then$$1 = value.then;
|
|
||||||
} catch (error) {
|
|
||||||
reject(promise, error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleMaybeThenable(promise, value, then$$1);
|
|
||||||
} else {
|
|
||||||
fulfill(promise, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function publishRejection(promise) {
|
|
||||||
if (promise._onerror) {
|
|
||||||
promise._onerror(promise._result);
|
|
||||||
}
|
|
||||||
|
|
||||||
publish(promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fulfill(promise, value) {
|
|
||||||
if (promise._state !== PENDING) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
promise._result = value;
|
|
||||||
promise._state = FULFILLED;
|
|
||||||
|
|
||||||
if (promise._subscribers.length !== 0) {
|
|
||||||
asap(publish, promise);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reject(promise, reason) {
|
|
||||||
if (promise._state !== PENDING) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
promise._state = REJECTED;
|
|
||||||
promise._result = reason;
|
|
||||||
|
|
||||||
asap(publishRejection, promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribe(parent, child, onFulfillment, onRejection) {
|
|
||||||
var _subscribers = parent._subscribers;
|
|
||||||
var length = _subscribers.length;
|
|
||||||
|
|
||||||
|
|
||||||
parent._onerror = null;
|
|
||||||
|
|
||||||
_subscribers[length] = child;
|
|
||||||
_subscribers[length + FULFILLED] = onFulfillment;
|
|
||||||
_subscribers[length + REJECTED] = onRejection;
|
|
||||||
|
|
||||||
if (length === 0 && parent._state) {
|
|
||||||
asap(publish, parent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function publish(promise) {
|
|
||||||
var subscribers = promise._subscribers;
|
|
||||||
var settled = promise._state;
|
|
||||||
|
|
||||||
if (subscribers.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var child = void 0,
|
|
||||||
callback = void 0,
|
|
||||||
detail = promise._result;
|
|
||||||
|
|
||||||
for (var i = 0; i < subscribers.length; i += 3) {
|
|
||||||
child = subscribers[i];
|
|
||||||
callback = subscribers[i + settled];
|
|
||||||
|
|
||||||
if (child) {
|
|
||||||
invokeCallback(settled, child, callback, detail);
|
|
||||||
} else {
|
|
||||||
callback(detail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
promise._subscribers.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function invokeCallback(settled, promise, callback, detail) {
|
|
||||||
var hasCallback = isFunction(callback),
|
|
||||||
value = void 0,
|
|
||||||
error = void 0,
|
|
||||||
succeeded = true;
|
|
||||||
|
|
||||||
if (hasCallback) {
|
|
||||||
try {
|
|
||||||
value = callback(detail);
|
|
||||||
} catch (e) {
|
|
||||||
succeeded = false;
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (promise === value) {
|
|
||||||
reject(promise, cannotReturnOwn());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value = detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (promise._state !== PENDING) {
|
|
||||||
// noop
|
|
||||||
} else if (hasCallback && succeeded) {
|
|
||||||
resolve(promise, value);
|
|
||||||
} else if (succeeded === false) {
|
|
||||||
reject(promise, error);
|
|
||||||
} else if (settled === FULFILLED) {
|
|
||||||
fulfill(promise, value);
|
|
||||||
} else if (settled === REJECTED) {
|
|
||||||
reject(promise, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializePromise(promise, resolver) {
|
|
||||||
try {
|
|
||||||
resolver(function resolvePromise(value) {
|
|
||||||
resolve(promise, value);
|
|
||||||
}, function rejectPromise(reason) {
|
|
||||||
reject(promise, reason);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
reject(promise, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = 0;
|
|
||||||
function nextId() {
|
|
||||||
return id++;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makePromise(promise) {
|
|
||||||
promise[PROMISE_ID] = id++;
|
|
||||||
promise._state = undefined;
|
|
||||||
promise._result = undefined;
|
|
||||||
promise._subscribers = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function validationError() {
|
|
||||||
return new Error('Array Methods must be provided an Array');
|
|
||||||
}
|
|
||||||
|
|
||||||
var Enumerator = function () {
|
|
||||||
function Enumerator(Constructor, input) {
|
|
||||||
this._instanceConstructor = Constructor;
|
|
||||||
this.promise = new Constructor(noop);
|
|
||||||
|
|
||||||
if (!this.promise[PROMISE_ID]) {
|
|
||||||
makePromise(this.promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isArray(input)) {
|
|
||||||
this.length = input.length;
|
|
||||||
this._remaining = input.length;
|
|
||||||
|
|
||||||
this._result = new Array(this.length);
|
|
||||||
|
|
||||||
if (this.length === 0) {
|
|
||||||
fulfill(this.promise, this._result);
|
|
||||||
} else {
|
|
||||||
this.length = this.length || 0;
|
|
||||||
this._enumerate(input);
|
|
||||||
if (this._remaining === 0) {
|
|
||||||
fulfill(this.promise, this._result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reject(this.promise, validationError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Enumerator.prototype._enumerate = function _enumerate(input) {
|
|
||||||
for (var i = 0; this._state === PENDING && i < input.length; i++) {
|
|
||||||
this._eachEntry(input[i], i);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Enumerator.prototype._eachEntry = function _eachEntry(entry, i) {
|
|
||||||
var c = this._instanceConstructor;
|
|
||||||
var resolve$$1 = c.resolve;
|
|
||||||
|
|
||||||
|
|
||||||
if (resolve$$1 === resolve$1) {
|
|
||||||
var _then = void 0;
|
|
||||||
var error = void 0;
|
|
||||||
var didError = false;
|
|
||||||
try {
|
|
||||||
_then = entry.then;
|
|
||||||
} catch (e) {
|
|
||||||
didError = true;
|
|
||||||
error = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_then === then && entry._state !== PENDING) {
|
|
||||||
this._settledAt(entry._state, i, entry._result);
|
|
||||||
} else if (typeof _then !== 'function') {
|
|
||||||
this._remaining--;
|
|
||||||
this._result[i] = entry;
|
|
||||||
} else if (c === Promise$1) {
|
|
||||||
var promise = new c(noop);
|
|
||||||
if (didError) {
|
|
||||||
reject(promise, error);
|
|
||||||
} else {
|
|
||||||
handleMaybeThenable(promise, entry, _then);
|
|
||||||
}
|
|
||||||
this._willSettleAt(promise, i);
|
|
||||||
} else {
|
|
||||||
this._willSettleAt(new c(function (resolve$$1) {
|
|
||||||
return resolve$$1(entry);
|
|
||||||
}), i);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._willSettleAt(resolve$$1(entry), i);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Enumerator.prototype._settledAt = function _settledAt(state, i, value) {
|
|
||||||
var promise = this.promise;
|
|
||||||
|
|
||||||
|
|
||||||
if (promise._state === PENDING) {
|
|
||||||
this._remaining--;
|
|
||||||
|
|
||||||
if (state === REJECTED) {
|
|
||||||
reject(promise, value);
|
|
||||||
} else {
|
|
||||||
this._result[i] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._remaining === 0) {
|
|
||||||
fulfill(promise, this._result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Enumerator.prototype._willSettleAt = function _willSettleAt(promise, i) {
|
|
||||||
var enumerator = this;
|
|
||||||
|
|
||||||
subscribe(promise, undefined, function (value) {
|
|
||||||
return enumerator._settledAt(FULFILLED, i, value);
|
|
||||||
}, function (reason) {
|
|
||||||
return enumerator._settledAt(REJECTED, i, reason);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return Enumerator;
|
|
||||||
}();
|
|
||||||
|
|
||||||
/**
|
|
||||||
`Promise.all` accepts an array of promises, and returns a new promise which
|
|
||||||
is fulfilled with an array of fulfillment values for the passed promises, or
|
|
||||||
rejected with the reason of the first passed promise to be rejected. It casts all
|
|
||||||
elements of the passed iterable to promises as it runs this algorithm.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let promise1 = resolve(1);
|
|
||||||
let promise2 = resolve(2);
|
|
||||||
let promise3 = resolve(3);
|
|
||||||
let promises = [ promise1, promise2, promise3 ];
|
|
||||||
|
|
||||||
Promise.all(promises).then(function(array){
|
|
||||||
// The array here would be [ 1, 2, 3 ];
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
If any of the `promises` given to `all` are rejected, the first promise
|
|
||||||
that is rejected will be given as an argument to the returned promises's
|
|
||||||
rejection handler. For example:
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let promise1 = resolve(1);
|
|
||||||
let promise2 = reject(new Error("2"));
|
|
||||||
let promise3 = reject(new Error("3"));
|
|
||||||
let promises = [ promise1, promise2, promise3 ];
|
|
||||||
|
|
||||||
Promise.all(promises).then(function(array){
|
|
||||||
// Code here never runs because there are rejected promises!
|
|
||||||
}, function(error) {
|
|
||||||
// error.message === "2"
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
@method all
|
|
||||||
@static
|
|
||||||
@param {Array} entries array of promises
|
|
||||||
@param {String} label optional string for labeling the promise.
|
|
||||||
Useful for tooling.
|
|
||||||
@return {Promise} promise that is fulfilled when all `promises` have been
|
|
||||||
fulfilled, or rejected if any of them become rejected.
|
|
||||||
@static
|
|
||||||
*/
|
|
||||||
function all(entries) {
|
|
||||||
return new Enumerator(this, entries).promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
`Promise.race` returns a new promise which is settled in the same way as the
|
|
||||||
first passed promise to settle.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let promise1 = new Promise(function(resolve, reject){
|
|
||||||
setTimeout(function(){
|
|
||||||
resolve('promise 1');
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
let promise2 = new Promise(function(resolve, reject){
|
|
||||||
setTimeout(function(){
|
|
||||||
resolve('promise 2');
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.race([promise1, promise2]).then(function(result){
|
|
||||||
// result === 'promise 2' because it was resolved before promise1
|
|
||||||
// was resolved.
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
`Promise.race` is deterministic in that only the state of the first
|
|
||||||
settled promise matters. For example, even if other promises given to the
|
|
||||||
`promises` array argument are resolved, but the first settled promise has
|
|
||||||
become rejected before the other promises became fulfilled, the returned
|
|
||||||
promise will become rejected:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let promise1 = new Promise(function(resolve, reject){
|
|
||||||
setTimeout(function(){
|
|
||||||
resolve('promise 1');
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
|
|
||||||
let promise2 = new Promise(function(resolve, reject){
|
|
||||||
setTimeout(function(){
|
|
||||||
reject(new Error('promise 2'));
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.race([promise1, promise2]).then(function(result){
|
|
||||||
// Code here never runs
|
|
||||||
}, function(reason){
|
|
||||||
// reason.message === 'promise 2' because promise 2 became rejected before
|
|
||||||
// promise 1 became fulfilled
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
An example real-world use case is implementing timeouts:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
Promise.race([ajax('foo.json'), timeout(5000)])
|
|
||||||
```
|
|
||||||
|
|
||||||
@method race
|
|
||||||
@static
|
|
||||||
@param {Array} promises array of promises to observe
|
|
||||||
Useful for tooling.
|
|
||||||
@return {Promise} a promise which settles in the same way as the first passed
|
|
||||||
promise to settle.
|
|
||||||
*/
|
|
||||||
function race(entries) {
|
|
||||||
/*jshint validthis:true */
|
|
||||||
var Constructor = this;
|
|
||||||
|
|
||||||
if (!isArray(entries)) {
|
|
||||||
return new Constructor(function (_, reject) {
|
|
||||||
return reject(new TypeError('You must pass an array to race.'));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return new Constructor(function (resolve, reject) {
|
|
||||||
var length = entries.length;
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
Constructor.resolve(entries[i]).then(resolve, reject);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
`Promise.reject` returns a promise rejected with the passed `reason`.
|
|
||||||
It is shorthand for the following:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let promise = new Promise(function(resolve, reject){
|
|
||||||
reject(new Error('WHOOPS'));
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.then(function(value){
|
|
||||||
// Code here doesn't run because the promise is rejected!
|
|
||||||
}, function(reason){
|
|
||||||
// reason.message === 'WHOOPS'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Instead of writing the above, your code now simply becomes the following:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
let promise = Promise.reject(new Error('WHOOPS'));
|
|
||||||
|
|
||||||
promise.then(function(value){
|
|
||||||
// Code here doesn't run because the promise is rejected!
|
|
||||||
}, function(reason){
|
|
||||||
// reason.message === 'WHOOPS'
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
@method reject
|
|
||||||
@static
|
|
||||||
@param {Any} reason value that the returned promise will be rejected with.
|
|
||||||
Useful for tooling.
|
|
||||||
@return {Promise} a promise rejected with the given `reason`.
|
|
||||||
*/
|
|
||||||
function reject$1(reason) {
|
|
||||||
/*jshint validthis:true */
|
|
||||||
var Constructor = this;
|
|
||||||
var promise = new Constructor(noop);
|
|
||||||
reject(promise, reason);
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
function needsResolver() {
|
|
||||||
throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
|
|
||||||
}
|
|
||||||
|
|
||||||
function needsNew() {
|
|
||||||
throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Promise objects represent the eventual result of an asynchronous operation. The
|
|
||||||
primary way of interacting with a promise is through its `then` method, which
|
|
||||||
registers callbacks to receive either a promise's eventual value or the reason
|
|
||||||
why the promise cannot be fulfilled.
|
|
||||||
|
|
||||||
Terminology
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- `promise` is an object or function with a `then` method whose behavior conforms to this specification.
|
|
||||||
- `thenable` is an object or function that defines a `then` method.
|
|
||||||
- `value` is any legal JavaScript value (including undefined, a thenable, or a promise).
|
|
||||||
- `exception` is a value that is thrown using the throw statement.
|
|
||||||
- `reason` is a value that indicates why a promise was rejected.
|
|
||||||
- `settled` the final resting state of a promise, fulfilled or rejected.
|
|
||||||
|
|
||||||
A promise can be in one of three states: pending, fulfilled, or rejected.
|
|
||||||
|
|
||||||
Promises that are fulfilled have a fulfillment value and are in the fulfilled
|
|
||||||
state. Promises that are rejected have a rejection reason and are in the
|
|
||||||
rejected state. A fulfillment value is never a thenable.
|
|
||||||
|
|
||||||
Promises can also be said to *resolve* a value. If this value is also a
|
|
||||||
promise, then the original promise's settled state will match the value's
|
|
||||||
settled state. So a promise that *resolves* a promise that rejects will
|
|
||||||
itself reject, and a promise that *resolves* a promise that fulfills will
|
|
||||||
itself fulfill.
|
|
||||||
|
|
||||||
|
|
||||||
Basic Usage:
|
|
||||||
------------
|
|
||||||
|
|
||||||
```js
|
|
||||||
let promise = new Promise(function(resolve, reject) {
|
|
||||||
// on success
|
|
||||||
resolve(value);
|
|
||||||
|
|
||||||
// on failure
|
|
||||||
reject(reason);
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.then(function(value) {
|
|
||||||
// on fulfillment
|
|
||||||
}, function(reason) {
|
|
||||||
// on rejection
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Advanced Usage:
|
|
||||||
---------------
|
|
||||||
|
|
||||||
Promises shine when abstracting away asynchronous interactions such as
|
|
||||||
`XMLHttpRequest`s.
|
|
||||||
|
|
||||||
```js
|
|
||||||
function getJSON(url) {
|
|
||||||
return new Promise(function(resolve, reject){
|
|
||||||
let xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
xhr.open('GET', url);
|
|
||||||
xhr.onreadystatechange = handler;
|
|
||||||
xhr.responseType = 'json';
|
|
||||||
xhr.setRequestHeader('Accept', 'application/json');
|
|
||||||
xhr.send();
|
|
||||||
|
|
||||||
function handler() {
|
|
||||||
if (this.readyState === this.DONE) {
|
|
||||||
if (this.status === 200) {
|
|
||||||
resolve(this.response);
|
|
||||||
} else {
|
|
||||||
reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getJSON('/posts.json').then(function(json) {
|
|
||||||
// on fulfillment
|
|
||||||
}, function(reason) {
|
|
||||||
// on rejection
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Unlike callbacks, promises are great composable primitives.
|
|
||||||
|
|
||||||
```js
|
|
||||||
Promise.all([
|
|
||||||
getJSON('/posts'),
|
|
||||||
getJSON('/comments')
|
|
||||||
]).then(function(values){
|
|
||||||
values[0] // => postsJSON
|
|
||||||
values[1] // => commentsJSON
|
|
||||||
|
|
||||||
return values;
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
@class Promise
|
|
||||||
@param {Function} resolver
|
|
||||||
Useful for tooling.
|
|
||||||
@constructor
|
|
||||||
*/
|
|
||||||
|
|
||||||
var Promise$1 = function () {
|
|
||||||
function Promise(resolver) {
|
|
||||||
this[PROMISE_ID] = nextId();
|
|
||||||
this._result = this._state = undefined;
|
|
||||||
this._subscribers = [];
|
|
||||||
|
|
||||||
if (noop !== resolver) {
|
|
||||||
typeof resolver !== 'function' && needsResolver();
|
|
||||||
this instanceof Promise ? initializePromise(this, resolver) : needsNew();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
The primary way of interacting with a promise is through its `then` method,
|
|
||||||
which registers callbacks to receive either a promise's eventual value or the
|
|
||||||
reason why the promise cannot be fulfilled.
|
|
||||||
```js
|
|
||||||
findUser().then(function(user){
|
|
||||||
// user is available
|
|
||||||
}, function(reason){
|
|
||||||
// user is unavailable, and you are given the reason why
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Chaining
|
|
||||||
--------
|
|
||||||
The return value of `then` is itself a promise. This second, 'downstream'
|
|
||||||
promise is resolved with the return value of the first promise's fulfillment
|
|
||||||
or rejection handler, or rejected if the handler throws an exception.
|
|
||||||
```js
|
|
||||||
findUser().then(function (user) {
|
|
||||||
return user.name;
|
|
||||||
}, function (reason) {
|
|
||||||
return 'default name';
|
|
||||||
}).then(function (userName) {
|
|
||||||
// If `findUser` fulfilled, `userName` will be the user's name, otherwise it
|
|
||||||
// will be `'default name'`
|
|
||||||
});
|
|
||||||
findUser().then(function (user) {
|
|
||||||
throw new Error('Found user, but still unhappy');
|
|
||||||
}, function (reason) {
|
|
||||||
throw new Error('`findUser` rejected and we're unhappy');
|
|
||||||
}).then(function (value) {
|
|
||||||
// never reached
|
|
||||||
}, function (reason) {
|
|
||||||
// if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'.
|
|
||||||
// If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'.
|
|
||||||
});
|
|
||||||
```
|
|
||||||
If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream.
|
|
||||||
```js
|
|
||||||
findUser().then(function (user) {
|
|
||||||
throw new PedagogicalException('Upstream error');
|
|
||||||
}).then(function (value) {
|
|
||||||
// never reached
|
|
||||||
}).then(function (value) {
|
|
||||||
// never reached
|
|
||||||
}, function (reason) {
|
|
||||||
// The `PedgagocialException` is propagated all the way down to here
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Assimilation
|
|
||||||
------------
|
|
||||||
Sometimes the value you want to propagate to a downstream promise can only be
|
|
||||||
retrieved asynchronously. This can be achieved by returning a promise in the
|
|
||||||
fulfillment or rejection handler. The downstream promise will then be pending
|
|
||||||
until the returned promise is settled. This is called *assimilation*.
|
|
||||||
```js
|
|
||||||
findUser().then(function (user) {
|
|
||||||
return findCommentsByAuthor(user);
|
|
||||||
}).then(function (comments) {
|
|
||||||
// The user's comments are now available
|
|
||||||
});
|
|
||||||
```
|
|
||||||
If the assimliated promise rejects, then the downstream promise will also reject.
|
|
||||||
```js
|
|
||||||
findUser().then(function (user) {
|
|
||||||
return findCommentsByAuthor(user);
|
|
||||||
}).then(function (comments) {
|
|
||||||
// If `findCommentsByAuthor` fulfills, we'll have the value here
|
|
||||||
}, function (reason) {
|
|
||||||
// If `findCommentsByAuthor` rejects, we'll have the reason here
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Simple Example
|
|
||||||
--------------
|
|
||||||
Synchronous Example
|
|
||||||
```javascript
|
|
||||||
let result;
|
|
||||||
try {
|
|
||||||
result = findResult();
|
|
||||||
// success
|
|
||||||
} catch(reason) {
|
|
||||||
// failure
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Errback Example
|
|
||||||
```js
|
|
||||||
findResult(function(result, err){
|
|
||||||
if (err) {
|
|
||||||
// failure
|
|
||||||
} else {
|
|
||||||
// success
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Promise Example;
|
|
||||||
```javascript
|
|
||||||
findResult().then(function(result){
|
|
||||||
// success
|
|
||||||
}, function(reason){
|
|
||||||
// failure
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Advanced Example
|
|
||||||
--------------
|
|
||||||
Synchronous Example
|
|
||||||
```javascript
|
|
||||||
let author, books;
|
|
||||||
try {
|
|
||||||
author = findAuthor();
|
|
||||||
books = findBooksByAuthor(author);
|
|
||||||
// success
|
|
||||||
} catch(reason) {
|
|
||||||
// failure
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Errback Example
|
|
||||||
```js
|
|
||||||
function foundBooks(books) {
|
|
||||||
}
|
|
||||||
function failure(reason) {
|
|
||||||
}
|
|
||||||
findAuthor(function(author, err){
|
|
||||||
if (err) {
|
|
||||||
failure(err);
|
|
||||||
// failure
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
findBoooksByAuthor(author, function(books, err) {
|
|
||||||
if (err) {
|
|
||||||
failure(err);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
foundBooks(books);
|
|
||||||
} catch(reason) {
|
|
||||||
failure(reason);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch(error) {
|
|
||||||
failure(err);
|
|
||||||
}
|
|
||||||
// success
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
Promise Example;
|
|
||||||
```javascript
|
|
||||||
findAuthor().
|
|
||||||
then(findBooksByAuthor).
|
|
||||||
then(function(books){
|
|
||||||
// found books
|
|
||||||
}).catch(function(reason){
|
|
||||||
// something went wrong
|
|
||||||
});
|
|
||||||
```
|
|
||||||
@method then
|
|
||||||
@param {Function} onFulfilled
|
|
||||||
@param {Function} onRejected
|
|
||||||
Useful for tooling.
|
|
||||||
@return {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
`catch` is simply sugar for `then(undefined, onRejection)` which makes it the same
|
|
||||||
as the catch block of a try/catch statement.
|
|
||||||
```js
|
|
||||||
function findAuthor(){
|
|
||||||
throw new Error('couldn't find that author');
|
|
||||||
}
|
|
||||||
// synchronous
|
|
||||||
try {
|
|
||||||
findAuthor();
|
|
||||||
} catch(reason) {
|
|
||||||
// something went wrong
|
|
||||||
}
|
|
||||||
// async with promises
|
|
||||||
findAuthor().catch(function(reason){
|
|
||||||
// something went wrong
|
|
||||||
});
|
|
||||||
```
|
|
||||||
@method catch
|
|
||||||
@param {Function} onRejection
|
|
||||||
Useful for tooling.
|
|
||||||
@return {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
Promise.prototype.catch = function _catch(onRejection) {
|
|
||||||
return this.then(null, onRejection);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
`finally` will be invoked regardless of the promise's fate just as native
|
|
||||||
try/catch/finally behaves
|
|
||||||
|
|
||||||
Synchronous example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
findAuthor() {
|
|
||||||
if (Math.random() > 0.5) {
|
|
||||||
throw new Error();
|
|
||||||
}
|
|
||||||
return new Author();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return findAuthor(); // succeed or fail
|
|
||||||
} catch(error) {
|
|
||||||
return findOtherAuther();
|
|
||||||
} finally {
|
|
||||||
// always runs
|
|
||||||
// doesn't affect the return value
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Asynchronous example:
|
|
||||||
|
|
||||||
```js
|
|
||||||
findAuthor().catch(function(reason){
|
|
||||||
return findOtherAuther();
|
|
||||||
}).finally(function(){
|
|
||||||
// author was either found, or not
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
@method finally
|
|
||||||
@param {Function} callback
|
|
||||||
@return {Promise}
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
Promise.prototype.finally = function _finally(callback) {
|
|
||||||
var promise = this;
|
|
||||||
var constructor = promise.constructor;
|
|
||||||
|
|
||||||
if (isFunction(callback)) {
|
|
||||||
return promise.then(function (value) {
|
|
||||||
return constructor.resolve(callback()).then(function () {
|
|
||||||
return value;
|
|
||||||
});
|
|
||||||
}, function (reason) {
|
|
||||||
return constructor.resolve(callback()).then(function () {
|
|
||||||
throw reason;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.then(callback, callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
return Promise;
|
|
||||||
}();
|
|
||||||
|
|
||||||
Promise$1.prototype.then = then;
|
|
||||||
Promise$1.all = all;
|
|
||||||
Promise$1.race = race;
|
|
||||||
Promise$1.resolve = resolve$1;
|
|
||||||
Promise$1.reject = reject$1;
|
|
||||||
Promise$1._setScheduler = setScheduler;
|
|
||||||
Promise$1._setAsap = setAsap;
|
|
||||||
Promise$1._asap = asap;
|
|
||||||
|
|
||||||
/*global self*/
|
|
||||||
function polyfill() {
|
|
||||||
var local = void 0;
|
|
||||||
|
|
||||||
if (typeof global !== 'undefined') {
|
|
||||||
local = global;
|
|
||||||
} else if (typeof self !== 'undefined') {
|
|
||||||
local = self;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
local = Function('return this')();
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error('polyfill failed because global object is unavailable in this environment');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var P = local.Promise;
|
|
||||||
|
|
||||||
if (P) {
|
|
||||||
var promiseToString = null;
|
|
||||||
try {
|
|
||||||
promiseToString = Object.prototype.toString.call(P.resolve());
|
|
||||||
} catch (e) {
|
|
||||||
// silently ignored
|
|
||||||
}
|
|
||||||
|
|
||||||
if (promiseToString === '[object Promise]' && !P.cast) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
local.Promise = Promise$1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strange compat..
|
|
||||||
Promise$1.polyfill = polyfill;
|
|
||||||
Promise$1.Promise = Promise$1;
|
|
||||||
|
|
||||||
return Promise$1;
|
|
||||||
|
|
||||||
})));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//# sourceMappingURL=es6-promise.map
|
|
190
node_modules/@yaireo/tagify/src/react.tagify.js
generated
vendored
190
node_modules/@yaireo/tagify/src/react.tagify.js
generated
vendored
|
@ -1,190 +0,0 @@
|
||||||
import React, {useMemo, useEffect, useRef} from "react"
|
|
||||||
import {renderToStaticMarkup} from "react-dom/server"
|
|
||||||
import {string, array, func, bool, object, element, oneOfType} from "prop-types"
|
|
||||||
import Tagify from "./tagify.min.js"
|
|
||||||
|
|
||||||
const noop = _ => _
|
|
||||||
|
|
||||||
// if a template is a React component, it should be outputed as a String (and not as a React component)
|
|
||||||
function templatesToString(templates) {
|
|
||||||
if (templates) {
|
|
||||||
for (let templateName in templates) {
|
|
||||||
let isReactComp = String(templates[templateName]).includes(".createElement")
|
|
||||||
|
|
||||||
if (isReactComp) {
|
|
||||||
let Template = templates[templateName]
|
|
||||||
templates[templateName] = data => renderToStaticMarkup(<Template {...data} />)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TagifyWrapper = ({
|
|
||||||
name,
|
|
||||||
value = "",
|
|
||||||
loading = false,
|
|
||||||
onInput = noop,
|
|
||||||
onAdd = noop,
|
|
||||||
onRemove = noop,
|
|
||||||
onEdit = noop,
|
|
||||||
onInvalid = noop,
|
|
||||||
onClick = noop,
|
|
||||||
onKeydown = noop,
|
|
||||||
onFocus = noop,
|
|
||||||
onBlur = noop,
|
|
||||||
onChange = noop,
|
|
||||||
readOnly,
|
|
||||||
children,
|
|
||||||
settings = {},
|
|
||||||
InputMode = "input",
|
|
||||||
autoFocus,
|
|
||||||
className,
|
|
||||||
whitelist,
|
|
||||||
tagifyRef,
|
|
||||||
placeholder = "",
|
|
||||||
defaultValue,
|
|
||||||
showDropdown
|
|
||||||
}) => {
|
|
||||||
const mountedRef = useRef()
|
|
||||||
const inputElmRef = useRef()
|
|
||||||
const tagify = useRef()
|
|
||||||
|
|
||||||
const handleRef = elm => {
|
|
||||||
inputElmRef.current = elm
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputAttrs = useMemo(() => ({
|
|
||||||
ref: handleRef,
|
|
||||||
name,
|
|
||||||
value: children
|
|
||||||
? children
|
|
||||||
: typeof value === "string"
|
|
||||||
? value
|
|
||||||
: JSON.stringify(value),
|
|
||||||
className,
|
|
||||||
readOnly,
|
|
||||||
onChange,
|
|
||||||
autoFocus,
|
|
||||||
placeholder,
|
|
||||||
defaultValue
|
|
||||||
}), [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
templatesToString(settings.templates)
|
|
||||||
|
|
||||||
if (InputMode == "textarea")
|
|
||||||
settings.mode = "mix"
|
|
||||||
|
|
||||||
// "whitelist" prop takes precedence
|
|
||||||
if( whitelist && whitelist.length )
|
|
||||||
settings.whitelist = whitelist
|
|
||||||
|
|
||||||
const t = new Tagify(inputElmRef.current, settings)
|
|
||||||
|
|
||||||
onInput && t.on("input" , onInput)
|
|
||||||
onAdd && t.on("add" , onAdd)
|
|
||||||
onRemove && t.on("remove" , onRemove)
|
|
||||||
onEdit && t.on("edit" , onEdit)
|
|
||||||
onInvalid && t.on("invalid", onInvalid)
|
|
||||||
onKeydown && t.on("keydown", onKeydown)
|
|
||||||
onFocus && t.on("focus" , onFocus)
|
|
||||||
onBlur && t.on("blur" , onBlur)
|
|
||||||
onClick && t.on("click" , onClick)
|
|
||||||
|
|
||||||
// Bridge Tagify instance with parent component
|
|
||||||
if (tagifyRef) {
|
|
||||||
tagifyRef.current = t
|
|
||||||
}
|
|
||||||
|
|
||||||
tagify.current = t
|
|
||||||
|
|
||||||
// cleanup
|
|
||||||
return () => {
|
|
||||||
t.destroy()
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mountedRef.current) {
|
|
||||||
tagify.current.settings.whitelist.length = 0
|
|
||||||
|
|
||||||
// replace whitelist array items
|
|
||||||
whitelist && whitelist.length && tagify.current.settings.whitelist.push(...whitelist)
|
|
||||||
}
|
|
||||||
}, [whitelist])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mountedRef.current) {
|
|
||||||
tagify.current.loadOriginalValues(value)
|
|
||||||
}
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mountedRef.current) {
|
|
||||||
tagify.current.toggleClass(className)
|
|
||||||
}
|
|
||||||
}, [className])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mountedRef.current) {
|
|
||||||
tagify.current.loading(loading)
|
|
||||||
}
|
|
||||||
}, [loading])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (mountedRef.current) {
|
|
||||||
tagify.current.setReadonly(readOnly)
|
|
||||||
}
|
|
||||||
}, [readOnly])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const t = tagify.current
|
|
||||||
|
|
||||||
if (mountedRef.current) {
|
|
||||||
if (showDropdown) {
|
|
||||||
t.dropdown.show.call(t, showDropdown)
|
|
||||||
t.toggleFocusClass(true)
|
|
||||||
} else {
|
|
||||||
t.dropdown.hide.call(t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [showDropdown])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
mountedRef.current = true
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
// a wrapper must be used because Tagify will appened inside it it's component,
|
|
||||||
// keeping the virtual-DOM out of the way
|
|
||||||
<div className="tags-input">
|
|
||||||
<InputMode {...inputAttrs} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TagifyWrapper.propTypes = {
|
|
||||||
name: string,
|
|
||||||
value: oneOfType([string, array]),
|
|
||||||
loading: bool,
|
|
||||||
children: element,
|
|
||||||
onChange: func,
|
|
||||||
readOnly: bool,
|
|
||||||
settings: object,
|
|
||||||
InputMode: string,
|
|
||||||
autoFocus: bool,
|
|
||||||
className: string,
|
|
||||||
tagifyRef: object,
|
|
||||||
whitelist: array,
|
|
||||||
placeholder: string,
|
|
||||||
defaultValue: oneOfType([string, array]),
|
|
||||||
showDropdown: oneOfType([string, bool])
|
|
||||||
}
|
|
||||||
|
|
||||||
const Tags = React.memo(TagifyWrapper)
|
|
||||||
Tags.displayName = "Tags"
|
|
||||||
|
|
||||||
export const MixedTags = ({ children, ...rest }) =>
|
|
||||||
<Tags InputMode="textarea" {...rest}>{children}</Tags>
|
|
||||||
|
|
||||||
export default Tags
|
|
1478
node_modules/@yaireo/tagify/src/tagify.js
generated
vendored
1478
node_modules/@yaireo/tagify/src/tagify.js
generated
vendored
|
@ -1,1478 +0,0 @@
|
||||||
import { sameStr, removeCollectionProp, isObject, parseHTML, removeTextChildNodes, escapeHTML, extend } from './parts/helpers'
|
|
||||||
import dropdownMethods from './parts/dropdown'
|
|
||||||
import DEFAULTS from './parts/defaults'
|
|
||||||
import templates from './parts/templates'
|
|
||||||
import EventDispatcher from './parts/EventDispatcher'
|
|
||||||
import events, { triggerChangeEvent } from './parts/events'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @constructor
|
|
||||||
* @param {Object} input DOM element
|
|
||||||
* @param {Object} settings settings object
|
|
||||||
*/
|
|
||||||
function Tagify( input, settings ){
|
|
||||||
if( !input ){
|
|
||||||
console.warn('Tagify: ', 'input element not found', input)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
if( input.previousElementSibling && input.previousElementSibling.classList.contains('tagify') ){
|
|
||||||
console.warn('Tagify: ', 'input element is already Tagified', input)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
extend(this, EventDispatcher(this))
|
|
||||||
this.isFirefox = typeof InstallTrigger !== 'undefined'
|
|
||||||
this.isIE = window.document.documentMode; // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility
|
|
||||||
|
|
||||||
this.applySettings(input, settings||{})
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
inputText: '',
|
|
||||||
editing : false,
|
|
||||||
actions : {}, // UI actions for state-locking
|
|
||||||
mixMode : {},
|
|
||||||
dropdown: {},
|
|
||||||
flaggedTags: {} // in mix-mode, when a string is detetced as potential tag, and the user has chocen to close the suggestions dropdown, keep the record of the tasg here
|
|
||||||
}
|
|
||||||
|
|
||||||
this.value = [] // tags' data
|
|
||||||
|
|
||||||
// events' callbacks references will be stores here, so events could be unbinded
|
|
||||||
this.listeners = {}
|
|
||||||
|
|
||||||
this.DOM = {} // Store all relevant DOM elements in an Object
|
|
||||||
|
|
||||||
this.build(input)
|
|
||||||
this.getCSSVars()
|
|
||||||
this.loadOriginalValues()
|
|
||||||
|
|
||||||
this.events.customBinding.call(this);
|
|
||||||
this.events.binding.call(this)
|
|
||||||
input.autofocus && this.DOM.input.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
Tagify.prototype = {
|
|
||||||
dropdown: dropdownMethods,
|
|
||||||
|
|
||||||
TEXTS : {
|
|
||||||
empty : "empty",
|
|
||||||
exceed : "number of tags exceeded",
|
|
||||||
pattern : "pattern mismatch",
|
|
||||||
duplicate : "already exists",
|
|
||||||
notAllowed : "not allowed"
|
|
||||||
},
|
|
||||||
|
|
||||||
DEFAULTS,
|
|
||||||
|
|
||||||
customEventsList : ['change', 'add', 'remove', 'invalid', 'input', 'click', 'keydown', 'focus', 'blur', 'edit:input', 'edit:updated', 'edit:start', 'edit:keydown', 'dropdown:show', 'dropdown:hide', 'dropdown:select', 'dropdown:updated', 'dropdown:noMatch'],
|
|
||||||
|
|
||||||
trim(text){
|
|
||||||
return this.settings.trim ? text.trim() : text
|
|
||||||
},
|
|
||||||
|
|
||||||
// expose this handy utility function
|
|
||||||
parseHTML,
|
|
||||||
|
|
||||||
templates,
|
|
||||||
|
|
||||||
parseTemplate(template, data){
|
|
||||||
template = this.settings.templates[template] || template;
|
|
||||||
return this.parseHTML( template.apply(this, data) )
|
|
||||||
},
|
|
||||||
|
|
||||||
applySettings( input, settings ){
|
|
||||||
this.DEFAULTS.templates = this.templates;
|
|
||||||
|
|
||||||
var _s = this.settings = extend({}, this.DEFAULTS, settings);
|
|
||||||
_s.readonly = input.hasAttribute('readonly') // if "readonly" do not include an "input" element inside the Tags component
|
|
||||||
_s.placeholder = input.getAttribute('placeholder') || _s.placeholder || ""
|
|
||||||
_s.required = input.hasAttribute('required')
|
|
||||||
|
|
||||||
if( this.isIE )
|
|
||||||
_s.autoComplete = false; // IE goes crazy if this isn't false
|
|
||||||
|
|
||||||
["whitelist", "blacklist"].forEach(name => {
|
|
||||||
var attrVal = input.getAttribute('data-' + name)
|
|
||||||
if( attrVal ){
|
|
||||||
attrVal = attrVal.split(_s.delimiters)
|
|
||||||
if( attrVal instanceof Array )
|
|
||||||
_s[name] = attrVal
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// backward-compatibility for old version of "autoComplete" setting:
|
|
||||||
if( "autoComplete" in settings && !isObject(settings.autoComplete) ){
|
|
||||||
_s.autoComplete = this.DEFAULTS.autoComplete
|
|
||||||
_s.autoComplete.enabled = settings.autoComplete
|
|
||||||
}
|
|
||||||
|
|
||||||
if( _s.mode == 'mix' ){
|
|
||||||
_s.autoComplete.rightKey = true
|
|
||||||
_s.delimiters = settings.delimiters || null // default dlimiters in mix-mode must be NULL
|
|
||||||
}
|
|
||||||
|
|
||||||
if( input.pattern )
|
|
||||||
try { _s.pattern = new RegExp(input.pattern) }
|
|
||||||
catch(e){}
|
|
||||||
|
|
||||||
// Convert the "delimiters" setting into a REGEX object
|
|
||||||
if( this.settings.delimiters ){
|
|
||||||
try { _s.delimiters = new RegExp(this.settings.delimiters, "g") }
|
|
||||||
catch(e){}
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the dropdown will be shown on "focus" and not only after typing something (in "select" mode)
|
|
||||||
if( _s.mode == 'select' )
|
|
||||||
_s.dropdown.enabled = 0
|
|
||||||
|
|
||||||
_s.dropdown.appendTarget = settings.dropdown && settings.dropdown.appendTarget
|
|
||||||
? settings.dropdown.appendTarget
|
|
||||||
: document.body
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string of HTML element attributes
|
|
||||||
* @param {Object} data [Tag data]
|
|
||||||
*/
|
|
||||||
getAttributes( data ){
|
|
||||||
// only items which are objects have properties which can be used as attributes
|
|
||||||
if( Object.prototype.toString.call(data) != "[object Object]" )
|
|
||||||
return '';
|
|
||||||
|
|
||||||
var keys = Object.keys(data),
|
|
||||||
s = "", propName, i;
|
|
||||||
|
|
||||||
for( i=keys.length; i--; ){
|
|
||||||
propName = keys[i];
|
|
||||||
if( propName != 'class' && data.hasOwnProperty(propName) && data[propName] !== undefined )
|
|
||||||
s += " " + propName + (data[propName] !== undefined ? `="${data[propName]}"` : "");
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the caret position relative to the viewport
|
|
||||||
* https://stackoverflow.com/q/58985076/104380
|
|
||||||
*
|
|
||||||
* @returns {object} left, top distance in pixels
|
|
||||||
*/
|
|
||||||
getCaretGlobalPosition(){
|
|
||||||
const sel = document.getSelection()
|
|
||||||
|
|
||||||
if( sel.rangeCount ){
|
|
||||||
const r = sel.getRangeAt(0)
|
|
||||||
const node = r.startContainer
|
|
||||||
const offset = r.startOffset
|
|
||||||
let rect, r2;
|
|
||||||
|
|
||||||
if (offset > 0) {
|
|
||||||
r2 = document.createRange()
|
|
||||||
r2.setStart(node, offset - 1)
|
|
||||||
r2.setEnd(node, offset)
|
|
||||||
rect = r2.getBoundingClientRect()
|
|
||||||
return {left:rect.right, top:rect.top, bottom:rect.bottom}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( node.getBoundingClientRect )
|
|
||||||
return node.getBoundingClientRect()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {left:-9999, top:-9999}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get specific CSS variables which are relevant to this script and parse them as needed.
|
|
||||||
* The result is saved on the instance in "this.CSSVars"
|
|
||||||
*/
|
|
||||||
getCSSVars(){
|
|
||||||
var compStyle = getComputedStyle(this.DOM.scope, null)
|
|
||||||
|
|
||||||
const getProp = name => compStyle.getPropertyValue('--'+name)
|
|
||||||
|
|
||||||
function seprateUnitFromValue(a){
|
|
||||||
if( !a ) return {}
|
|
||||||
a = a.trim().split(' ')[0]
|
|
||||||
var unit = a.split(/\d+/g).filter(n=>n).pop().trim(),
|
|
||||||
value = +a.split(unit).filter(n=>n)[0].trim()
|
|
||||||
return {value, unit}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.CSSVars = {
|
|
||||||
tagHideTransition: (({value, unit}) => unit=='s' ? value * 1000 : value)(seprateUnitFromValue(getProp('tag-hide-transition')))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* builds the HTML of this component
|
|
||||||
* @param {Object} input [DOM element which would be "transformed" into "Tags"]
|
|
||||||
*/
|
|
||||||
build( input ){
|
|
||||||
var DOM = this.DOM;
|
|
||||||
|
|
||||||
if( this.settings.mixMode.integrated ){
|
|
||||||
DOM.originalInput = null;
|
|
||||||
DOM.scope = input;
|
|
||||||
DOM.input = input;
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
DOM.originalInput = input
|
|
||||||
DOM.scope = this.parseTemplate('wrapper', [input, this.settings])
|
|
||||||
DOM.input = DOM.scope.querySelector('.' + this.settings.classNames.input)
|
|
||||||
input.parentNode.insertBefore(DOM.scope, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
if( this.settings.dropdown.enabled >= 0 )
|
|
||||||
this.dropdown.init.call(this)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* revert any changes made by this component
|
|
||||||
*/
|
|
||||||
destroy(){
|
|
||||||
this.DOM.scope.parentNode.removeChild(this.DOM.scope)
|
|
||||||
this.dropdown.hide.call(this, true)
|
|
||||||
clearTimeout(this.dropdownHide__bindEventsTimeout)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* if the original input had any values, add them as tags
|
|
||||||
*/
|
|
||||||
loadOriginalValues( value ){
|
|
||||||
var lastChild,
|
|
||||||
_s = this.settings;
|
|
||||||
|
|
||||||
value = value || (_s.mixMode.integrated
|
|
||||||
? this.DOM.input.textContent
|
|
||||||
: this.DOM.originalInput.value)
|
|
||||||
|
|
||||||
if( value ){
|
|
||||||
this.removeAllTags()
|
|
||||||
|
|
||||||
if( _s.mode == 'mix' ){
|
|
||||||
this.parseMixTags(value.trim())
|
|
||||||
|
|
||||||
lastChild = this.DOM.input.lastChild;
|
|
||||||
|
|
||||||
if( !lastChild || lastChild.tagName != 'BR' )
|
|
||||||
this.DOM.input.insertAdjacentHTML('beforeend', '<br>')
|
|
||||||
}
|
|
||||||
|
|
||||||
else{
|
|
||||||
try{
|
|
||||||
if( JSON.parse(value) instanceof Array )
|
|
||||||
value = JSON.parse(value)
|
|
||||||
}
|
|
||||||
catch(err){}
|
|
||||||
this.addTags(value).forEach(tag => tag && tag.classList.add(_s.classNames.tagNoAnimation))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
this.postUpdate()
|
|
||||||
|
|
||||||
this.state.lastOriginalValueReported = _s.mixMode.integrated ? '' : this.DOM.originalInput.value
|
|
||||||
this.state.loadedOriginalValues = true
|
|
||||||
},
|
|
||||||
|
|
||||||
cloneEvent(e){
|
|
||||||
var clonedEvent = {}
|
|
||||||
for( var v in e )
|
|
||||||
clonedEvent[v] = e[v]
|
|
||||||
return clonedEvent
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toogle global loading state on/off
|
|
||||||
* Useful when fetching async whitelist while user is typing
|
|
||||||
* @param {Boolean} isLoading
|
|
||||||
*/
|
|
||||||
loading( isLoading ){
|
|
||||||
this.state.isLoading = isLoading
|
|
||||||
// IE11 doesn't support toggle with second parameter
|
|
||||||
this.DOM.scope.classList[isLoading?"add":"remove"](this.settings.classNames.scopeLoading)
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toogle specieif tag loading state on/off
|
|
||||||
* @param {Boolean} isLoading
|
|
||||||
*/
|
|
||||||
tagLoading( tagElm, isLoading ){
|
|
||||||
if( tagElm )
|
|
||||||
// IE11 doesn't support toggle with second parameter
|
|
||||||
tagElm.classList[isLoading?"add":"remove"](this.settings.classNames.tagLoading)
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles class on the main tagify container ("scope")
|
|
||||||
* @param {String} className
|
|
||||||
* @param {Boolean} force
|
|
||||||
*/
|
|
||||||
toggleClass( className, force ){
|
|
||||||
// force = force === undefined ?
|
|
||||||
this.DOM.scope.classList.toggle(className, force)
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleFocusClass( force ){
|
|
||||||
this.toggleClass(this.settings.classNames.focus, !!force)
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerChangeEvent,
|
|
||||||
|
|
||||||
events,
|
|
||||||
|
|
||||||
fixFirefoxLastTagNoCaret(){
|
|
||||||
return // seems to be fixed in newer version of FF, so retiring below code (for now)
|
|
||||||
var inputElm = this.DOM.input
|
|
||||||
|
|
||||||
if( this.isFirefox && inputElm.childNodes.length && inputElm.lastChild.nodeType == 1 ){
|
|
||||||
inputElm.appendChild(document.createTextNode("\u200b"))
|
|
||||||
this.setRangeAtStartEnd(true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
placeCaretAfterNode( node ){
|
|
||||||
if( !node ) return
|
|
||||||
|
|
||||||
var nextSibling = node.nextSibling,
|
|
||||||
sel = window.getSelection(),
|
|
||||||
range = sel.getRangeAt(0);
|
|
||||||
|
|
||||||
if (sel.rangeCount) {
|
|
||||||
range.setStartBefore(nextSibling || node);
|
|
||||||
range.setEndBefore(nextSibling || node);
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(range);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
insertAfterTag( tagElm, newNode ){
|
|
||||||
newNode = newNode || this.settings.mixMode.insertAfterTag;
|
|
||||||
|
|
||||||
if( !tagElm || !newNode ) return
|
|
||||||
|
|
||||||
newNode = typeof newNode == 'string'
|
|
||||||
? document.createTextNode(newNode)
|
|
||||||
: newNode
|
|
||||||
|
|
||||||
tagElm.appendChild(newNode)
|
|
||||||
tagElm.parentNode.insertBefore(newNode, tagElm.nextSibling)
|
|
||||||
return newNode
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enters a tag into "edit" mode
|
|
||||||
* @param {Node} tagElm the tag element to edit. if nothing specified, use last last
|
|
||||||
*/
|
|
||||||
editTag( tagElm, opts ){
|
|
||||||
tagElm = tagElm || this.getLastTag()
|
|
||||||
opts = opts || {}
|
|
||||||
|
|
||||||
this.dropdown.hide.call(this)
|
|
||||||
|
|
||||||
var _s = this.settings;
|
|
||||||
|
|
||||||
function getEditableElm(){
|
|
||||||
return tagElm.querySelector('.' + _s.classNames.tagText)
|
|
||||||
}
|
|
||||||
|
|
||||||
var editableElm = getEditableElm(),
|
|
||||||
tagIdx = this.getNodeIndex(tagElm),
|
|
||||||
tagData = this.tagData(tagElm),
|
|
||||||
_CB = this.events.callbacks,
|
|
||||||
that = this,
|
|
||||||
isValid = true,
|
|
||||||
delayed_onEditTagBlur = function(){
|
|
||||||
setTimeout(() => _CB.onEditTagBlur.call(that, getEditableElm()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !editableElm ){
|
|
||||||
console.warn('Cannot find element in Tag template: .', _s.classNames.tagText);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( tagData instanceof Object && "editable" in tagData && !tagData.editable )
|
|
||||||
return
|
|
||||||
|
|
||||||
editableElm.setAttribute('contenteditable', true)
|
|
||||||
tagElm.classList.add( _s.classNames.tagEditing )
|
|
||||||
|
|
||||||
// cache the original data, on the DOM node, before any modification ocurs, for possible revert
|
|
||||||
this.tagData(tagElm, {
|
|
||||||
__originalData: extend({}, tagData),
|
|
||||||
__originalHTML: tagElm.innerHTML
|
|
||||||
})
|
|
||||||
|
|
||||||
editableElm.addEventListener('focus', _CB.onEditTagFocus.bind(this, tagElm))
|
|
||||||
editableElm.addEventListener('blur', delayed_onEditTagBlur)
|
|
||||||
editableElm.addEventListener('input', _CB.onEditTagInput.bind(this, editableElm))
|
|
||||||
editableElm.addEventListener('keydown', e => _CB.onEditTagkeydown.call(this, e, tagElm))
|
|
||||||
|
|
||||||
editableElm.focus()
|
|
||||||
this.setRangeAtStartEnd(false, editableElm)
|
|
||||||
|
|
||||||
if( !opts.skipValidation )
|
|
||||||
isValid = this.editTagToggleValidity(tagElm, tagData.value)
|
|
||||||
|
|
||||||
editableElm.originalIsValid = isValid
|
|
||||||
|
|
||||||
this.trigger("edit:start", { tag:tagElm, index:tagIdx, data:tagData, isValid })
|
|
||||||
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
|
|
||||||
editTagToggleValidity( tagElm, value ){
|
|
||||||
var tagData = this.tagData(tagElm),
|
|
||||||
toggleState;
|
|
||||||
|
|
||||||
if( !tagData ){
|
|
||||||
console.warn("tag has no data: ", tagElm, tagData)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleState = !!(tagData.__isValid && tagData.__isValid != true);
|
|
||||||
|
|
||||||
//this.validateTag(tagData);
|
|
||||||
|
|
||||||
tagElm.classList.toggle(this.settings.classNames.tagInvalid, toggleState)
|
|
||||||
return tagData.__isValid
|
|
||||||
},
|
|
||||||
|
|
||||||
onEditTagDone(tagElm, tagData){
|
|
||||||
tagElm = tagElm || this.state.editing.scope
|
|
||||||
tagData = tagData || {}
|
|
||||||
|
|
||||||
var eventData = { tag:tagElm, index:this.getNodeIndex(tagElm), previousData:this.tagData(tagElm), data:tagData };
|
|
||||||
|
|
||||||
this.trigger("edit:beforeUpdate", eventData)
|
|
||||||
|
|
||||||
this.state.editing = false;
|
|
||||||
delete tagData.__originalData
|
|
||||||
delete tagData.__originalHTML
|
|
||||||
|
|
||||||
if( tagElm && tagData[this.settings.tagTextProp] ){
|
|
||||||
this.editTagToggleValidity(tagElm)
|
|
||||||
this.replaceTag(tagElm, tagData)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(tagElm)
|
|
||||||
this.removeTags(tagElm)
|
|
||||||
|
|
||||||
this.trigger("edit:updated", eventData)
|
|
||||||
this.dropdown.hide.call(this)
|
|
||||||
|
|
||||||
// check if any of the current tags which might have been marked as "duplicate" should be now un-marked
|
|
||||||
if( this.settings.keepInvalidTags )
|
|
||||||
this.reCheckInvalidTags()
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replaces an exisitng tag with a new one. Used for updating a tag's data
|
|
||||||
* @param {Object} tagElm [DOM node to replace]
|
|
||||||
* @param {Object} tagData [data to create new tag from]
|
|
||||||
*/
|
|
||||||
replaceTag(tagElm, tagData){
|
|
||||||
if( !tagData || !tagData.value )
|
|
||||||
tagData = tagElm.__tagifyTagData
|
|
||||||
|
|
||||||
// if tag is invalid, make the according changes in the newly created element
|
|
||||||
if( tagData.__isValid && tagData.__isValid != true )
|
|
||||||
extend( tagData, this.getInvalidTagAttrs(tagData, tagData.__isValid) )
|
|
||||||
|
|
||||||
var newTag = this.createTagElem(tagData)
|
|
||||||
|
|
||||||
// update DOM
|
|
||||||
tagElm.parentNode.replaceChild(newTag, tagElm)
|
|
||||||
this.updateValueByDOMTags()
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update "value" (Array of Objects) by traversing all valid tags
|
|
||||||
*/
|
|
||||||
updateValueByDOMTags(){
|
|
||||||
this.value.length = 0;
|
|
||||||
|
|
||||||
[].forEach.call(this.getTagElms(), node => {
|
|
||||||
if( node.classList.contains(this.settings.classNames.tagNotAllowed) ) return
|
|
||||||
this.value.push( this.tagData(node) )
|
|
||||||
})
|
|
||||||
|
|
||||||
this.update()
|
|
||||||
},
|
|
||||||
|
|
||||||
/** https://stackoverflow.com/a/59156872/104380
|
|
||||||
* @param {Boolean} start indicating where to place it (start or end of the node)
|
|
||||||
* @param {Object} node DOM node to place the caret at
|
|
||||||
*/
|
|
||||||
setRangeAtStartEnd( start, node ){
|
|
||||||
start = typeof start == 'number' ? start : !!start
|
|
||||||
node = node || this.DOM.input;
|
|
||||||
node = node.lastChild || node;
|
|
||||||
var sel = document.getSelection()
|
|
||||||
|
|
||||||
try{
|
|
||||||
if( sel.rangeCount >= 1 ){
|
|
||||||
['Start', 'End'].forEach(pos =>
|
|
||||||
sel.getRangeAt(0)["set" + pos](node, start ? start : node.length)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch(err){
|
|
||||||
console.warn("Tagify: ", err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* injects nodes/text at caret position, which is saved on the "state" when "blur" event gets triggered
|
|
||||||
* @param {Node} injectedNode [the node to inject at the caret position]
|
|
||||||
* @param {Object} selection [optional selection Object. must have "anchorNode" & "anchorOffset"]
|
|
||||||
*/
|
|
||||||
injectAtCaret( injectedNode, range ){
|
|
||||||
range = range || this.state.selection.range
|
|
||||||
|
|
||||||
if( !range ) return;
|
|
||||||
|
|
||||||
if( typeof injectedNode == 'string' )
|
|
||||||
injectedNode = document.createTextNode(injectedNode);
|
|
||||||
|
|
||||||
range.deleteContents()
|
|
||||||
|
|
||||||
range.insertNode(injectedNode)
|
|
||||||
|
|
||||||
this.setRangeAtStartEnd(false, injectedNode)
|
|
||||||
|
|
||||||
this.updateValueByDOMTags() // updates internal "this.value"
|
|
||||||
this.update() // updates original input/textarea
|
|
||||||
|
|
||||||
return this
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* input bridge for accessing & setting
|
|
||||||
* @type {Object}
|
|
||||||
*/
|
|
||||||
input : {
|
|
||||||
set( s = '', updateDOM = true ){
|
|
||||||
var hideDropdown = this.settings.dropdown.closeOnSelect
|
|
||||||
this.state.inputText = s
|
|
||||||
|
|
||||||
if( updateDOM )
|
|
||||||
this.DOM.input.innerHTML = s;
|
|
||||||
|
|
||||||
if( !s && hideDropdown )
|
|
||||||
this.dropdown.hide.bind(this)
|
|
||||||
|
|
||||||
this.input.autocomplete.suggest.call(this);
|
|
||||||
this.input.validate.call(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the tagify's input as "invalid" if the value did not pass "validateTag()"
|
|
||||||
*/
|
|
||||||
validate(){
|
|
||||||
var isValid = !this.state.inputText || this.validateTag({value:this.state.inputText}) === true;
|
|
||||||
|
|
||||||
this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid, !isValid)
|
|
||||||
|
|
||||||
return isValid
|
|
||||||
},
|
|
||||||
|
|
||||||
// remove any child DOM elements that aren't of type TEXT (like <br>)
|
|
||||||
normalize( node ){
|
|
||||||
var clone = node || this.DOM.input, //.cloneNode(true),
|
|
||||||
v = [];
|
|
||||||
|
|
||||||
// when a text was pasted in FF, the "this.DOM.input" element will have <br> but no newline symbols (\n), and this will
|
|
||||||
// result in tags not being properly created if one wishes to create a separate tag per newline.
|
|
||||||
clone.childNodes.forEach(n => n.nodeType==3 && v.push(n.nodeValue))
|
|
||||||
v = v.join("\n")
|
|
||||||
|
|
||||||
try{
|
|
||||||
// "delimiters" might be of a non-regex value, where this will fail ("Tags With Properties" example in demo page):
|
|
||||||
v = v.replace(/(?:\r\n|\r|\n)/g, this.settings.delimiters.source.charAt(0))
|
|
||||||
}
|
|
||||||
catch(err){}
|
|
||||||
|
|
||||||
v = v.replace(/\s/g, ' ') // replace NBSPs with spaces characters
|
|
||||||
|
|
||||||
if( this.settings.trim )
|
|
||||||
v = v.replace(/^\s+/, '') // trimLeft
|
|
||||||
|
|
||||||
return v
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* suggest the rest of the input's value (via CSS "::after" using "content:attr(...)")
|
|
||||||
* @param {String} s [description]
|
|
||||||
*/
|
|
||||||
autocomplete : {
|
|
||||||
suggest( data ){
|
|
||||||
if( !this.settings.autoComplete.enabled ) return;
|
|
||||||
|
|
||||||
data = data || {}
|
|
||||||
|
|
||||||
if( typeof data == 'string' )
|
|
||||||
data = {value:data}
|
|
||||||
|
|
||||||
var suggestedText = data.value ? ''+data.value : '',
|
|
||||||
suggestionStart = suggestedText.substr(0, this.state.inputText.length).toLowerCase(),
|
|
||||||
suggestionTrimmed = suggestedText.substring(this.state.inputText.length);
|
|
||||||
|
|
||||||
if( !suggestedText || !this.state.inputText || suggestionStart != this.state.inputText.toLowerCase() ){
|
|
||||||
this.DOM.input.removeAttribute("data-suggest");
|
|
||||||
delete this.state.inputSuggestion
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
this.DOM.input.setAttribute("data-suggest", suggestionTrimmed);
|
|
||||||
this.state.inputSuggestion = data
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* sets the suggested text as the input's value & cleanup the suggestion autocomplete.
|
|
||||||
* @param {String} s [text]
|
|
||||||
*/
|
|
||||||
set( s ){
|
|
||||||
var dataSuggest = this.DOM.input.getAttribute('data-suggest'),
|
|
||||||
suggestion = s || (dataSuggest ? this.state.inputText + dataSuggest : null);
|
|
||||||
|
|
||||||
if( suggestion ){
|
|
||||||
if( this.settings.mode == 'mix' ){
|
|
||||||
this.replaceTextWithNode( document.createTextNode(this.state.tag.prefix + suggestion) )
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
this.input.set.call(this, suggestion);
|
|
||||||
this.setRangeAtStartEnd()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.input.autocomplete.suggest.call(this);
|
|
||||||
this.dropdown.hide.call(this);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns the index of the the tagData within the "this.value" array collection.
|
|
||||||
* since values should be unique, it is suffice to only search by "value" property
|
|
||||||
* @param {Object} tagData
|
|
||||||
*/
|
|
||||||
getTagIdx( tagData ){
|
|
||||||
return this.value.findIndex(item => item.value == tagData.value )
|
|
||||||
},
|
|
||||||
|
|
||||||
getNodeIndex( node ){
|
|
||||||
var index = 0;
|
|
||||||
|
|
||||||
if( node )
|
|
||||||
while( (node = node.previousElementSibling) )
|
|
||||||
index++;
|
|
||||||
|
|
||||||
return index;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTagElms( ...classess ){
|
|
||||||
var classname = ['.' + this.settings.classNames.tag, ...classess].join('.')
|
|
||||||
return [].slice.call(this.DOM.scope.querySelectorAll(classname)) // convert nodeList to Array - https://stackoverflow.com/a/3199627/104380
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gets the last non-readonly, not-in-the-proccess-of-removal tag
|
|
||||||
*/
|
|
||||||
getLastTag(){
|
|
||||||
var lastTag = this.DOM.scope.querySelectorAll(`.${this.settings.classNames.tag}:not(.${this.settings.classNames.tagHide}):not([readonly])`);
|
|
||||||
return lastTag[lastTag.length - 1];
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Setter/Getter
|
|
||||||
* Each tag DOM node contains a custom property called "__tagifyTagData" which hosts its data
|
|
||||||
* @param {Node} tagElm
|
|
||||||
* @param {Object} data
|
|
||||||
*/
|
|
||||||
tagData(tagElm, data, override){
|
|
||||||
if( !tagElm ){
|
|
||||||
console.warn("tag elment doesn't exist",tagElm, data)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
if( data )
|
|
||||||
tagElm.__tagifyTagData = override
|
|
||||||
? data
|
|
||||||
: extend({}, tagElm.__tagifyTagData || {}, data)
|
|
||||||
|
|
||||||
return tagElm.__tagifyTagData
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches if any tag with a certain value already exis
|
|
||||||
* @param {String/Object} v [text value / tag data object]
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
isTagDuplicate( value, caseSensitive ){
|
|
||||||
var duplications,
|
|
||||||
_s = this.settings;
|
|
||||||
|
|
||||||
// duplications are irrelevant for this scenario
|
|
||||||
if( _s.mode == 'select' )
|
|
||||||
return false
|
|
||||||
|
|
||||||
duplications = this.value.reduce((acc, item) =>
|
|
||||||
sameStr( this.trim(""+value), item.value, caseSensitive || _s.dropdown.caseSensitive )
|
|
||||||
? acc+1
|
|
||||||
: acc
|
|
||||||
, 0)
|
|
||||||
|
|
||||||
return duplications
|
|
||||||
},
|
|
||||||
|
|
||||||
getTagIndexByValue( value ){
|
|
||||||
var indices = [];
|
|
||||||
|
|
||||||
this.getTagElms().forEach((tagElm, i) => {
|
|
||||||
if( sameStr( this.trim(tagElm.textContent), value, this.settings.dropdown.caseSensitive ) )
|
|
||||||
indices.push(i)
|
|
||||||
})
|
|
||||||
|
|
||||||
return indices;
|
|
||||||
},
|
|
||||||
|
|
||||||
getTagElmByValue( value ){
|
|
||||||
var tagIdx = this.getTagIndexByValue(value)[0]
|
|
||||||
return this.getTagElms()[tagIdx]
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Temporarily marks a tag element (by value or Node argument)
|
|
||||||
* @param {Object} tagElm [a specific "tag" element to compare to the other tag elements siblings]
|
|
||||||
*/
|
|
||||||
flashTag( tagElm ){
|
|
||||||
if( tagElm ){
|
|
||||||
tagElm.classList.add(this.settings.classNames.tagFlash)
|
|
||||||
setTimeout(() => { tagElm.classList.remove(this.settings.classNames.tagFlash) }, 100)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* checks if text is in the blacklist
|
|
||||||
*/
|
|
||||||
isTagBlacklisted( v ){
|
|
||||||
v = this.trim(v.toLowerCase());
|
|
||||||
return this.settings.blacklist.filter(x => (""+x).toLowerCase() == v).length;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* checks if text is in the whitelist
|
|
||||||
*/
|
|
||||||
isTagWhitelisted( v ){
|
|
||||||
return !!this.getWhitelistItem(v)
|
|
||||||
/*
|
|
||||||
return this.settings.whitelist.some(item =>
|
|
||||||
typeof v == 'string'
|
|
||||||
? sameStr(this.trim(v), (item.value || item))
|
|
||||||
: sameStr(JSON.stringify(item), JSON.stringify(v))
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the first whitelist item matched, by value (if match found)
|
|
||||||
* @param {String} value [text to match by]
|
|
||||||
*/
|
|
||||||
getWhitelistItem( value, prop, whitelist ){
|
|
||||||
var result,
|
|
||||||
prop = prop || 'value',
|
|
||||||
_s = this.settings,
|
|
||||||
whitelist = whitelist || _s.whitelist,
|
|
||||||
_cs = _s.dropdown.caseSensitive;
|
|
||||||
|
|
||||||
whitelist.some(_wi => {
|
|
||||||
var _wiv = typeof _wi == 'string' ? _wi : _wi[prop],
|
|
||||||
isSameStr = sameStr(_wiv, value, _cs)
|
|
||||||
|
|
||||||
if( isSameStr ){
|
|
||||||
result = typeof _wi == 'string' ? {value:_wi} : _wi
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// first iterate the whitelist, try find maches by "value" and if that fails
|
|
||||||
// and a "tagTextProp" is set to be other than "value", try that also
|
|
||||||
if( !result && prop == 'value' && _s.tagTextProp != 'value' ){
|
|
||||||
// if found, adds the first which matches
|
|
||||||
result = this.getWhitelistItem(value, _s.tagTextProp, whitelist)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* validate a tag object BEFORE the actual tag will be created & appeneded
|
|
||||||
* @param {String} s
|
|
||||||
* @param {String} uid [unique ID, to not inclue own tag when cheking for duplicates]
|
|
||||||
* @return {Boolean/String} ["true" if validation has passed, String for a fail]
|
|
||||||
*/
|
|
||||||
validateTag( tagData ){
|
|
||||||
var _s = this.settings,
|
|
||||||
// when validating a tag in edit-mode, need to take "tagTextProp" into consideration
|
|
||||||
prop = "value" in tagData ? "value" : _s.tagTextProp,
|
|
||||||
v = this.trim(tagData[prop] + "");
|
|
||||||
|
|
||||||
// check for definitive empty value
|
|
||||||
if( !(tagData[prop]+"").trim() )
|
|
||||||
return this.TEXTS.empty;
|
|
||||||
|
|
||||||
// check if pattern should be used and if so, use it to test the value
|
|
||||||
if( _s.pattern && _s.pattern instanceof RegExp && !(_s.pattern.test(v)) )
|
|
||||||
return this.TEXTS.pattern;
|
|
||||||
|
|
||||||
// if duplicates are not allowed and there is a duplicate
|
|
||||||
if( !_s.duplicates && this.isTagDuplicate(v, this.state.editing) )
|
|
||||||
return this.TEXTS.duplicate;
|
|
||||||
|
|
||||||
if( this.isTagBlacklisted(v) || (_s.enforceWhitelist && !this.isTagWhitelisted(v)) )
|
|
||||||
return this.TEXTS.notAllowed;
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
|
|
||||||
getInvalidTagAttrs(tagData, validation){
|
|
||||||
return {
|
|
||||||
"aria-invalid" : true,
|
|
||||||
"class": `${tagData.class || ''} ${this.settings.classNames.tagNotAllowed}`.trim(),
|
|
||||||
"title": validation
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hasMaxTags(){
|
|
||||||
if( this.value.length >= this.settings.maxTags )
|
|
||||||
return this.TEXTS.exceed;
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
setReadonly( isReadonly ){
|
|
||||||
var _s = this.settings;
|
|
||||||
|
|
||||||
document.activeElement.blur() // exists possible edit-mode
|
|
||||||
_s.readonly = isReadonly
|
|
||||||
this.DOM.scope[(isReadonly?'set':'remove') + 'Attribute']('readonly', true)
|
|
||||||
|
|
||||||
if( _s.mode == 'mix' ){
|
|
||||||
this.DOM.input.contentEditable = !isReadonly
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words
|
|
||||||
* so each item should be iterated on and a tag created for.
|
|
||||||
* @return {Array} [Array of Objects]
|
|
||||||
*/
|
|
||||||
normalizeTags( tagsItems ){
|
|
||||||
var {whitelist, delimiters, mode, tagTextProp, enforceWhitelist} = this.settings,
|
|
||||||
whitelistMatches = [],
|
|
||||||
whitelistWithProps = whitelist ? whitelist[0] instanceof Object : false,
|
|
||||||
// checks if this is a "collection", meanning an Array of Objects
|
|
||||||
isArray = tagsItems instanceof Array,
|
|
||||||
mapStringToCollection = s => (s+"").split(delimiters).filter(n => n).map(v => ({ [tagTextProp]:this.trim(v) }))
|
|
||||||
|
|
||||||
if( typeof tagsItems == 'number' )
|
|
||||||
tagsItems = tagsItems.toString()
|
|
||||||
|
|
||||||
// if the argument is a "simple" String, ex: "aaa, bbb, ccc"
|
|
||||||
if( typeof tagsItems == 'string' ){
|
|
||||||
if( !tagsItems.trim() ) return [];
|
|
||||||
|
|
||||||
// go over each tag and add it (if there were multiple ones)
|
|
||||||
tagsItems = mapStringToCollection(tagsItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
// is is an Array of Strings, convert to an Array of Objects
|
|
||||||
else if( isArray ){
|
|
||||||
// flatten the 2D array
|
|
||||||
tagsItems = [].concat(...tagsItems.map(item => item.value
|
|
||||||
? item // mapStringToCollection(item.value).map(newItem => ({...item,...newItem}))
|
|
||||||
: mapStringToCollection(item)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// search if the tag exists in the whitelist as an Object (has props),
|
|
||||||
// to be able to use its properties
|
|
||||||
if( whitelistWithProps ){
|
|
||||||
tagsItems.forEach(item => {
|
|
||||||
var whitelistMatchesValues = whitelistMatches.map(a=>a.value)
|
|
||||||
|
|
||||||
// if suggestions are shown, they are already filtered, so it's easier to use them,
|
|
||||||
// because the whitelist might also include items which have already been added
|
|
||||||
var filteredList = this.dropdown.filterListItems.call(this, item[tagTextProp], { exact:true })
|
|
||||||
// also filter out items which have already been matches in previous iterations
|
|
||||||
.filter(filteredItem => !whitelistMatchesValues.includes(filteredItem.value))
|
|
||||||
|
|
||||||
// get the best match out of list of possible matches.
|
|
||||||
// if there was a single item in the filtered list, use that one
|
|
||||||
var matchObj = filteredList.length > 1
|
|
||||||
? this.getWhitelistItem(item[tagTextProp], tagTextProp, filteredList)
|
|
||||||
: filteredList[0]
|
|
||||||
|
|
||||||
if( matchObj && matchObj instanceof Object ){
|
|
||||||
whitelistMatches.push( matchObj ) // set the Array (with the found Object) as the new value
|
|
||||||
}
|
|
||||||
else if( mode != 'mix' && !enforceWhitelist ){
|
|
||||||
if( item.value == undefined )
|
|
||||||
item.value = item[tagTextProp]
|
|
||||||
whitelistMatches.push(item)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// if( whitelistMatches.length )
|
|
||||||
tagsItems = whitelistMatches
|
|
||||||
}
|
|
||||||
|
|
||||||
return tagsItems;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the initial value of a textarea (or input) element and generate mixed text w/ tags
|
|
||||||
* https://stackoverflow.com/a/57598892/104380
|
|
||||||
* @param {String} s
|
|
||||||
*/
|
|
||||||
parseMixTags( s ){
|
|
||||||
var {mixTagsInterpolator, duplicates, transformTag, enforceWhitelist, maxTags, tagTextProp} = this.settings,
|
|
||||||
tagsDataSet = [];
|
|
||||||
|
|
||||||
s = s.split(mixTagsInterpolator[0]).map((s1, i) => {
|
|
||||||
var s2 = s1.split(mixTagsInterpolator[1]),
|
|
||||||
preInterpolated = s2[0],
|
|
||||||
maxTagsReached = tagsDataSet.length == maxTags,
|
|
||||||
textProp,
|
|
||||||
tagData,
|
|
||||||
tagElm;
|
|
||||||
|
|
||||||
try{
|
|
||||||
// skip numbers and go straight to the "catch" statement
|
|
||||||
if( preInterpolated == +preInterpolated )
|
|
||||||
throw Error
|
|
||||||
tagData = JSON.parse(preInterpolated)
|
|
||||||
} catch(err){
|
|
||||||
tagData = this.normalizeTags(preInterpolated)[0] //{value:preInterpolated}
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !maxTagsReached &&
|
|
||||||
s2.length > 1 &&
|
|
||||||
(!enforceWhitelist || this.isTagWhitelisted(tagData.value)) &&
|
|
||||||
!(!duplicates && this.isTagDuplicate(tagData.value)) ){
|
|
||||||
transformTag.call(this, tagData)
|
|
||||||
|
|
||||||
// in case "tagTextProp" setting is set to other than "value" and this tag does not have this prop
|
|
||||||
textProp = tagData[tagTextProp] ? tagTextProp : 'value'
|
|
||||||
tagData[textProp] = this.trim(tagData[textProp])
|
|
||||||
|
|
||||||
tagElm = this.createTagElem(tagData)
|
|
||||||
tagsDataSet.push( tagData )
|
|
||||||
tagElm.classList.add(this.settings.classNames.tagNoAnimation)
|
|
||||||
|
|
||||||
s2[0] = tagElm.outerHTML //+ "⁠" // put a zero-space at the end so the caret won't jump back to the start (when the last input's child element is a tag)
|
|
||||||
this.value.push(tagData)
|
|
||||||
}
|
|
||||||
else if(s1)
|
|
||||||
return i ? mixTagsInterpolator[0] + s1 : s1
|
|
||||||
|
|
||||||
return s2.join('')
|
|
||||||
}).join('')
|
|
||||||
|
|
||||||
this.DOM.input.innerHTML = s
|
|
||||||
this.DOM.input.appendChild(document.createTextNode(''))
|
|
||||||
this.DOM.input.normalize()
|
|
||||||
this.getTagElms().forEach((elm, idx) => this.tagData(elm, tagsDataSet[idx]))
|
|
||||||
this.update({withoutChangeEvent:true})
|
|
||||||
return s
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For mixed-mode: replaces a text starting with a prefix with a wrapper element (tag or something)
|
|
||||||
* First there *has* to be a "this.state.tag" which is a string that was just typed and is staring with a prefix
|
|
||||||
*/
|
|
||||||
replaceTextWithNode( newWrapperNode, strToReplace ){
|
|
||||||
if( !this.state.tag && !strToReplace ) return;
|
|
||||||
|
|
||||||
strToReplace = strToReplace || this.state.tag.prefix + this.state.tag.value;
|
|
||||||
var idx, nodeToReplace,
|
|
||||||
selection = window.getSelection(),
|
|
||||||
nodeAtCaret = selection.anchorNode,
|
|
||||||
firstSplitOffset = this.state.tag.delimiters ? this.state.tag.delimiters.length : 0;
|
|
||||||
|
|
||||||
// STEP 1: ex. replace #ba with the tag "bart" where "|" is where the caret is:
|
|
||||||
// CURRENT STATE: "foo #ba #ba| #ba"
|
|
||||||
|
|
||||||
// split the text node at the index of the caret
|
|
||||||
nodeAtCaret.splitText(selection.anchorOffset - firstSplitOffset)
|
|
||||||
|
|
||||||
// node 0: "foo #ba #ba|"
|
|
||||||
// node 1: " #ba"
|
|
||||||
|
|
||||||
// get index of LAST occurence of "#ba"
|
|
||||||
idx = nodeAtCaret.nodeValue.lastIndexOf(strToReplace)
|
|
||||||
|
|
||||||
nodeToReplace = nodeAtCaret.splitText(idx)
|
|
||||||
|
|
||||||
// node 0: "foo #ba "
|
|
||||||
// node 1: "#ba" <- nodeToReplace
|
|
||||||
|
|
||||||
newWrapperNode && nodeAtCaret.parentNode.replaceChild(newWrapperNode, nodeToReplace)
|
|
||||||
|
|
||||||
// must NOT normalize contenteditable or it will cause unwanetd issues:
|
|
||||||
// https://monosnap.com/file/ZDVmRvq5upYkidiFedvrwzSswegWk7
|
|
||||||
// nodeAtCaret.parentNode.normalize()
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For selecting a single option (not used for multiple tags, but for "mode:select" only)
|
|
||||||
* @param {Object} tagElm Tag DOM node
|
|
||||||
* @param {Object} tagData Tag data
|
|
||||||
*/
|
|
||||||
selectTag( tagElm, tagData ){
|
|
||||||
if( this.settings.enforceWhitelist && !this.isTagWhitelisted(tagData.value) )
|
|
||||||
return
|
|
||||||
|
|
||||||
this.input.set.call(this, tagData.value, true)
|
|
||||||
|
|
||||||
// place the caret at the end of the input, only if a dropdown option was selected (and not by manually typing another value and clicking "TAB")
|
|
||||||
if( this.state.actions.selectOption )
|
|
||||||
setTimeout(this.setRangeAtStartEnd.bind(this))
|
|
||||||
|
|
||||||
if( this.getLastTag() )
|
|
||||||
this.replaceTag(this.getLastTag(), tagData)
|
|
||||||
else
|
|
||||||
this.appendTag(tagElm)
|
|
||||||
|
|
||||||
this.value[0] = tagData
|
|
||||||
this.trigger('add', { tag:tagElm, data:tagData })
|
|
||||||
this.update()
|
|
||||||
|
|
||||||
return [tagElm]
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add an empty "tag" element in an editable state
|
|
||||||
*/
|
|
||||||
addEmptyTag( initialData ){
|
|
||||||
var tagData = extend({ value:"" }, initialData || {}),
|
|
||||||
tagElm = this.createTagElem(tagData)
|
|
||||||
|
|
||||||
this.tagData(tagElm, tagData)
|
|
||||||
|
|
||||||
// add the tag to the component's DOM
|
|
||||||
this.appendTag(tagElm)
|
|
||||||
this.editTag(tagElm, {skipValidation:true})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* add a "tag" element to the "tags" component
|
|
||||||
* @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings]
|
|
||||||
* @param {Boolean} clearInput [flag if the input's value should be cleared after adding tags]
|
|
||||||
* @param {Boolean} skipInvalid [do not add, mark & remove invalid tags]
|
|
||||||
* @return {Array} Array of DOM elements (tags)
|
|
||||||
*/
|
|
||||||
addTags( tagsItems, clearInput, skipInvalid = this.settings.skipInvalid ){
|
|
||||||
var tagElems = [], _s = this.settings;
|
|
||||||
|
|
||||||
if( !tagsItems || tagsItems.length == 0 ){
|
|
||||||
// is mode is "select" clean all tags
|
|
||||||
if( _s.mode == 'select' )
|
|
||||||
this.removeAllTags()
|
|
||||||
return tagElems
|
|
||||||
}
|
|
||||||
|
|
||||||
// converts Array/String/Object to an Array of Objects
|
|
||||||
tagsItems = this.normalizeTags(tagsItems)
|
|
||||||
|
|
||||||
if( _s.mode == 'mix' ){
|
|
||||||
return this.addMixTags(tagsItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
if( _s.mode == 'select' )
|
|
||||||
clearInput = false
|
|
||||||
|
|
||||||
this.DOM.input.removeAttribute('style')
|
|
||||||
|
|
||||||
tagsItems.forEach(tagData => {
|
|
||||||
var tagElm,
|
|
||||||
tagElmParams = {},
|
|
||||||
originalData = Object.assign({}, tagData, {value:tagData.value+""});
|
|
||||||
|
|
||||||
// shallow-clone tagData so later modifications will not apply to the source
|
|
||||||
tagData = Object.assign({}, originalData)
|
|
||||||
tagData.__isValid = this.hasMaxTags() || this.validateTag(tagData)
|
|
||||||
|
|
||||||
_s.transformTag.call(this, tagData)
|
|
||||||
|
|
||||||
if( tagData.__isValid !== true ){
|
|
||||||
if( skipInvalid )
|
|
||||||
return
|
|
||||||
|
|
||||||
// originalData is kept because it might be that this tag is invalid because it is a duplicate of another,
|
|
||||||
// and if that other tags is edited/deleted, this one should be re-validated and if is no more a duplicate - restored
|
|
||||||
extend(tagElmParams, this.getInvalidTagAttrs(tagData, tagData.__isValid), {__preInvalidData:originalData})
|
|
||||||
|
|
||||||
if( tagData.__isValid == this.TEXTS.duplicate )
|
|
||||||
// mark, for a brief moment, the tag (this this one) which THIS CURRENT tag is a duplcate of
|
|
||||||
this.flashTag( this.getTagElmByValue(tagData.value) )
|
|
||||||
}
|
|
||||||
/////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
|
|
||||||
if( tagData.readonly )
|
|
||||||
tagElmParams["aria-readonly"] = true
|
|
||||||
|
|
||||||
// Create tag HTML element
|
|
||||||
tagElm = this.createTagElem( extend({}, tagData, tagElmParams) )
|
|
||||||
tagElems.push(tagElm)
|
|
||||||
|
|
||||||
// mode-select overrides
|
|
||||||
if( _s.mode == 'select' ){
|
|
||||||
return this.selectTag(tagElm, tagData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the tag to the component's DOM
|
|
||||||
this.appendTag(tagElm)
|
|
||||||
|
|
||||||
if( tagData.__isValid && tagData.__isValid === true ){
|
|
||||||
// update state
|
|
||||||
this.value.push(tagData)
|
|
||||||
this.update()
|
|
||||||
this.trigger('add', {tag:tagElm, index:this.value.length - 1, data:tagData})
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
this.trigger("invalid", {data:tagData, index:this.value.length, tag:tagElm, message:tagData.__isValid})
|
|
||||||
if( !_s.keepInvalidTags )
|
|
||||||
// remove invalid tags (if "keepInvalidTags" is set to "false")
|
|
||||||
setTimeout(() => this.removeTags(tagElm, true), 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dropdown.position.call(this) // reposition the dropdown because the just-added tag might cause a new-line
|
|
||||||
})
|
|
||||||
|
|
||||||
if( tagsItems.length && clearInput ){
|
|
||||||
this.input.set.call(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dropdown.refilter.call(this)
|
|
||||||
return tagElems
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a mix-content tag
|
|
||||||
* @param {String/Array} tagsItems [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings]
|
|
||||||
*/
|
|
||||||
addMixTags( tagsItems ){
|
|
||||||
var _s = this.settings,
|
|
||||||
tagElm,
|
|
||||||
createdFromDelimiters = this.state.tag.delimiters
|
|
||||||
|
|
||||||
_s.transformTag.call(this, tagsItems[0])
|
|
||||||
|
|
||||||
tagsItems[0].prefix = tagsItems[0].prefix || this.state.tag ? this.state.tag.prefix : (_s.pattern.source||_s.pattern)[0];
|
|
||||||
|
|
||||||
// TODO: should check if the tag is valid
|
|
||||||
tagElm = this.createTagElem(tagsItems[0])
|
|
||||||
|
|
||||||
// tries to replace a taged textNode with a tagElm, and if not able,
|
|
||||||
// insert the new tag to the END if "addTags" was called from outside
|
|
||||||
if( !this.replaceTextWithNode(tagElm) ){
|
|
||||||
this.DOM.input.appendChild(tagElm)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(()=> tagElm.classList.add(this.settings.classNames.tagNoAnimation), 300)
|
|
||||||
|
|
||||||
this.value.push(tagsItems[0])
|
|
||||||
this.update()
|
|
||||||
|
|
||||||
// fixes a firefox bug where if the last child of the input is a tag and not a text, the input cannot get focus (by Tab key)
|
|
||||||
!createdFromDelimiters && setTimeout(() => {
|
|
||||||
var elm = this.insertAfterTag(tagElm) || tagElm;
|
|
||||||
this.placeCaretAfterNode(elm)
|
|
||||||
}, this.isFirefox ? 100 : 0)
|
|
||||||
|
|
||||||
this.state.tag = null
|
|
||||||
this.trigger('add', extend({}, {tag:tagElm}, {data:tagsItems[0]}))
|
|
||||||
|
|
||||||
return tagElm
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* appened (validated) tag to the component's DOM scope
|
|
||||||
*/
|
|
||||||
appendTag(tagElm){
|
|
||||||
var insertBeforeNode = this.DOM.scope.lastElementChild;
|
|
||||||
|
|
||||||
if( insertBeforeNode === this.DOM.input )
|
|
||||||
this.DOM.scope.insertBefore(tagElm, insertBeforeNode);
|
|
||||||
else
|
|
||||||
this.DOM.scope.appendChild(tagElm);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* creates a DOM tag element and injects it into the component (this.DOM.scope)
|
|
||||||
* @param {Object} tagData [text value & properties for the created tag]
|
|
||||||
* @return {Object} [DOM element]
|
|
||||||
*/
|
|
||||||
createTagElem( tagData ){
|
|
||||||
var tagElm,
|
|
||||||
templateData = extend({}, tagData, { value:escapeHTML(tagData.value+"") });
|
|
||||||
|
|
||||||
if( this.settings.readonly )
|
|
||||||
tagData.readonly = true
|
|
||||||
|
|
||||||
tagElm = this.parseTemplate('tag', [templateData])
|
|
||||||
|
|
||||||
// crucial for proper caret placement when deleting content. if textNodes are allowed as children of
|
|
||||||
// a tag element, a browser bug casues the caret to misplaced inside the tag element (especcially affects "readonly" tags)
|
|
||||||
removeTextChildNodes(tagElm)
|
|
||||||
|
|
||||||
// while( tagElm.lastChild.nodeType == 3 )
|
|
||||||
// tagElm.lastChild.parentNode.removeChild(tagElm.lastChild)
|
|
||||||
|
|
||||||
this.tagData(tagElm, tagData)
|
|
||||||
|
|
||||||
return tagElm;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* find all invalid tags and re-check them
|
|
||||||
*/
|
|
||||||
reCheckInvalidTags(){
|
|
||||||
var _s = this.settings,
|
|
||||||
selector = `.${_s.classNames.tag}.${_s.classNames.tagNotAllowed}`,
|
|
||||||
tagElms = this.DOM.scope.querySelectorAll(selector);
|
|
||||||
|
|
||||||
[].forEach.call(tagElms, node => {
|
|
||||||
var tagData = this.tagData(node),
|
|
||||||
wasNodeDuplicate = node.getAttribute('title') == this.TEXTS.duplicate,
|
|
||||||
isNodeValid = this.validateTag(tagData) === true;
|
|
||||||
|
|
||||||
// if this tag node was marked as a dulpicate, unmark. (might have been marked as "notAllowed" for other reasons)
|
|
||||||
if( wasNodeDuplicate && isNodeValid ){
|
|
||||||
if( tagData.__preInvalidData )
|
|
||||||
tagData = tagData.__preInvalidData
|
|
||||||
else
|
|
||||||
// start fresh
|
|
||||||
tagData = {value:tagData.value}
|
|
||||||
|
|
||||||
this.replaceTag(node, tagData)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a tag
|
|
||||||
* @param {Array|Node|String} tagElms [DOM element(s) or a String value. if undefined or null, remove last added tag]
|
|
||||||
* @param {Boolean} silent [A flag, which when turned on, does not remove any value and does not update the original input value but simply removes the tag from tagify]
|
|
||||||
* @param {Number} tranDuration [Transition duration in MS]
|
|
||||||
* TODO: Allow multiple tags to be removed at-once
|
|
||||||
*/
|
|
||||||
removeTags( tagElms, silent, tranDuration ){
|
|
||||||
var tagsToRemove;
|
|
||||||
|
|
||||||
tagElms = tagElms && tagElms instanceof HTMLElement
|
|
||||||
? [tagElms]
|
|
||||||
: tagElms instanceof Array
|
|
||||||
? tagElms
|
|
||||||
: tagElms
|
|
||||||
? [tagElms]
|
|
||||||
: [this.getLastTag()]
|
|
||||||
|
|
||||||
// normalize tagElms array values:
|
|
||||||
// 1. removing invalid items
|
|
||||||
// 2, if an item is String try to get the matching Tag HTML node
|
|
||||||
// 3. get the tag data
|
|
||||||
// 4. return a collection of Objects
|
|
||||||
tagsToRemove = tagElms.reduce((elms, tagElm) => {
|
|
||||||
if( tagElm && typeof tagElm == 'string')
|
|
||||||
tagElm = this.getTagElmByValue(tagElm)
|
|
||||||
|
|
||||||
if( tagElm )
|
|
||||||
// because the DOM node might be removed by async animation, the state will be updated while
|
|
||||||
// the node might still be in the DOM, so the "update" method should know which nodes to ignore
|
|
||||||
elms.push({
|
|
||||||
node: tagElm,
|
|
||||||
idx: this.getTagIdx(this.tagData(tagElm)), // this.getNodeIndex(tagElm); // this.getTagIndexByValue(tagElm.textContent)
|
|
||||||
data: this.tagData(tagElm, {'__removed':true})
|
|
||||||
})
|
|
||||||
|
|
||||||
return elms
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
tranDuration = typeof tranDuration == "number" ? tranDuration : this.CSSVars.tagHideTransition
|
|
||||||
|
|
||||||
if( this.settings.mode == 'select' ){
|
|
||||||
tranDuration = 0;
|
|
||||||
this.input.set.call(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if only a single tag is to be removed
|
|
||||||
if( tagsToRemove.length == 1 ){
|
|
||||||
if( tagsToRemove[0].node.classList.contains(this.settings.classNames.tagNotAllowed) )
|
|
||||||
silent = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !tagsToRemove.length )
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.settings.hooks.beforeRemoveTag(tagsToRemove, {tagify:this})
|
|
||||||
.then(() => {
|
|
||||||
function removeNode( tag ){
|
|
||||||
if( !tag.node.parentNode ) return
|
|
||||||
|
|
||||||
tag.node.parentNode.removeChild(tag.node)
|
|
||||||
|
|
||||||
if( !silent ){
|
|
||||||
// this.removeValueById(tagData.__uid)
|
|
||||||
this.trigger('remove', { tag:tag.node, index:tag.idx, data:tag.data })
|
|
||||||
this.dropdown.refilter.call(this)
|
|
||||||
this.dropdown.position.call(this)
|
|
||||||
this.DOM.input.normalize() // best-practice when in mix-mode (safe to do always anyways)
|
|
||||||
|
|
||||||
// check if any of the current tags which might have been marked as "duplicate" should be now un-marked
|
|
||||||
if( this.settings.keepInvalidTags )
|
|
||||||
this.reCheckInvalidTags()
|
|
||||||
}
|
|
||||||
else if( this.settings.keepInvalidTags )
|
|
||||||
this.trigger('remove', { tag:tag.node, index:tag.idx })
|
|
||||||
}
|
|
||||||
|
|
||||||
function animation( tag ){
|
|
||||||
tag.node.style.width = parseFloat(window.getComputedStyle(tag.node).width) + 'px'
|
|
||||||
document.body.clientTop // force repaint for the width to take affect before the "hide" class below
|
|
||||||
tag.node.classList.add(this.settings.classNames.tagHide)
|
|
||||||
|
|
||||||
// manual timeout (hack, since transitionend cannot be used because of hover)
|
|
||||||
setTimeout(removeNode.bind(this), tranDuration, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if( tranDuration && tranDuration > 10 && tagsToRemove.length == 1 )
|
|
||||||
animation.call(this, tagsToRemove[0])
|
|
||||||
else
|
|
||||||
tagsToRemove.forEach(removeNode.bind(this))
|
|
||||||
|
|
||||||
// update state regardless of animation
|
|
||||||
if( !silent ){
|
|
||||||
tagsToRemove.forEach(tag => {
|
|
||||||
// remove "__removed" so the comparison in "getTagIdx" could work
|
|
||||||
var tagData = Object.assign({}, tag.data) // shallow clone
|
|
||||||
delete tagData.__removed
|
|
||||||
|
|
||||||
var tagIdx = this.getTagIdx(tagData)
|
|
||||||
if( tagIdx > -1 )
|
|
||||||
this.value.splice(tagIdx, 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
// that.removeValueById(tagData.__uid)
|
|
||||||
this.update() // update the original input with the current value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch(reason => {})
|
|
||||||
},
|
|
||||||
|
|
||||||
removeAllTags(){
|
|
||||||
this.value = []
|
|
||||||
|
|
||||||
if( this.settings.mode == 'mix' )
|
|
||||||
this.DOM.input.innerHTML = ''
|
|
||||||
else
|
|
||||||
Array.prototype.slice.call(this.getTagElms()).forEach(elm => elm.parentNode.removeChild(elm))
|
|
||||||
|
|
||||||
this.dropdown.position.call(this)
|
|
||||||
|
|
||||||
if( this.settings.mode == 'select' )
|
|
||||||
this.input.set.call(this)
|
|
||||||
|
|
||||||
this.update()
|
|
||||||
},
|
|
||||||
|
|
||||||
postUpdate(){
|
|
||||||
var classNames = this.settings.classNames,
|
|
||||||
hasValue = this.settings.mode == 'mix'
|
|
||||||
? this.settings.mixMode.integrated
|
|
||||||
? this.DOM.input.textContent
|
|
||||||
: this.DOM.originalInput.value
|
|
||||||
: this.value.length;
|
|
||||||
|
|
||||||
this.toggleClass(classNames.hasMaxTags, this.value.length >= this.settings.maxTags)
|
|
||||||
this.toggleClass(classNames.hasNoTags, !this.value.length)
|
|
||||||
this.toggleClass(classNames.empty, !hasValue)
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* update the origianl (hidden) input field's value
|
|
||||||
* see - https://stackoverflow.com/q/50957841/104380
|
|
||||||
*/
|
|
||||||
update( args ){
|
|
||||||
var inputElm = this.DOM.originalInput,
|
|
||||||
{ withoutChangeEvent } = args || {},
|
|
||||||
value = removeCollectionProp(this.value, ['__isValid', '__removed']);
|
|
||||||
|
|
||||||
if( !this.settings.mixMode.integrated ){
|
|
||||||
inputElm.value = this.settings.mode == 'mix'
|
|
||||||
? this.getMixedTagsAsString(value)
|
|
||||||
: value.length
|
|
||||||
? this.settings.originalInputValueFormat
|
|
||||||
? this.settings.originalInputValueFormat(value)
|
|
||||||
: JSON.stringify(value)
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
this.postUpdate()
|
|
||||||
|
|
||||||
if( !withoutChangeEvent && this.state.loadedOriginalValues )
|
|
||||||
this.triggerChangeEvent()
|
|
||||||
},
|
|
||||||
|
|
||||||
getMixedTagsAsString(){
|
|
||||||
var result = "",
|
|
||||||
that = this,
|
|
||||||
i = 0,
|
|
||||||
_interpolator = this.settings.mixTagsInterpolator;
|
|
||||||
|
|
||||||
function iterateChildren(rootNode){
|
|
||||||
rootNode.childNodes.forEach((node) => {
|
|
||||||
if( node.nodeType == 1 ){
|
|
||||||
if( node.classList.contains(that.settings.classNames.tag) && that.tagData(node) ){
|
|
||||||
if( that.tagData(node).__removed )
|
|
||||||
return;
|
|
||||||
else
|
|
||||||
result += _interpolator[0] + JSON.stringify( node.__tagifyTagData ) + _interpolator[1]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if( node.tagName == 'BR' && (node.parentNode == that.DOM.input || node.parentNode.childNodes.length == 1 ) ){
|
|
||||||
result += "\r\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
else if( node.tagName == 'DIV' || node.tagName == 'P' ){
|
|
||||||
result += "\r\n";
|
|
||||||
iterateChildren(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
result += node.textContent;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
iterateChildren(this.DOM.input)
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// legacy support for changed methods names
|
|
||||||
Tagify.prototype.removeTag = Tagify.prototype.removeTags
|
|
||||||
|
|
||||||
export default Tagify
|
|
12
node_modules/@yaireo/tagify/src/tagify.polyfills.js
generated
vendored
12
node_modules/@yaireo/tagify/src/tagify.polyfills.js
generated
vendored
|
@ -1,12 +0,0 @@
|
||||||
import "./polyfills/String.trim"
|
|
||||||
import "./polyfills/NodeList.forEach"
|
|
||||||
import "./polyfills/Array.findIndex"
|
|
||||||
import "./polyfills/Array.some"
|
|
||||||
import "./polyfills/String.includes"
|
|
||||||
import "./polyfills/Object.assign"
|
|
||||||
import "./polyfills/Event"
|
|
||||||
import "./polyfills/Element.matches"
|
|
||||||
import "./polyfills/Element.closest"
|
|
||||||
import "./polyfills/AutoUrlDetect"
|
|
||||||
import "./polyfills/Element.classList"
|
|
||||||
import "./polyfills/es6-promise"
|
|
674
node_modules/@yaireo/tagify/src/tagify.scss
generated
vendored
674
node_modules/@yaireo/tagify/src/tagify.scss
generated
vendored
|
@ -1,674 +0,0 @@
|
||||||
:root {
|
|
||||||
--tagify-dd-color-primary: rgb(53,149,246); // should be same as "$tags-focus-border-color"
|
|
||||||
--tagify-dd-bg-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tagify{
|
|
||||||
// SCSS "default" allows overriding variables BEFORE they are set in the below lines of code
|
|
||||||
$self: &;
|
|
||||||
$tags-border-color : #DDD !default;
|
|
||||||
$tags-hover-border-color : #CCC !default;
|
|
||||||
$tags-focus-border-color : #3595f6 !default;
|
|
||||||
$tagMargin : 5px !default;
|
|
||||||
$tag-pad : .3em .5em !default;
|
|
||||||
$tag-min-width : 1ch !default;
|
|
||||||
$tag-max-width : auto !default;
|
|
||||||
$tag-text-color : black !default;
|
|
||||||
$tag-text-color--edit : black !default;
|
|
||||||
$tag-bg : #E5E5E5 !default;
|
|
||||||
$tag-hover : #D3E2E2 !default;
|
|
||||||
$tag-remove : #D39494 !default;
|
|
||||||
$tag-remove-btn-color : $tag-text-color !default;
|
|
||||||
$tag-remove-btn-bg : none !default;
|
|
||||||
$tag-remove-btn-bg--hover: darken($tag-remove, 8) !default;
|
|
||||||
$tag-invalid-color : $tag-remove !default;
|
|
||||||
$tag-invalid-bg : rgba($tag-remove, .5) !default;
|
|
||||||
$tag-inset-shadow-size : 1.1em !default;
|
|
||||||
$tag-hide-transition : .3s !default;
|
|
||||||
$placeholder-color : rgba($tag-text-color, .4) !default;
|
|
||||||
$placeholder-color-focus : rgba($tag-text-color, .25) !default;
|
|
||||||
$input-color : inherit !default;
|
|
||||||
$tagify-dd-bg-color : white !default;
|
|
||||||
$tagify-dd-color-primary : rgb(53,149,246) !default;
|
|
||||||
|
|
||||||
// CSS variables
|
|
||||||
--tags-border-color : #{$tags-border-color};
|
|
||||||
--tags-hover-border-color : #{$tags-hover-border-color};
|
|
||||||
--tags-focus-border-color : #{$tags-focus-border-color};
|
|
||||||
--tag-bg : #{$tag-bg};
|
|
||||||
--tag-hover : #{$tag-hover};
|
|
||||||
--tag-text-color : #{$tag-text-color};
|
|
||||||
--tag-text-color--edit : #{$tag-text-color--edit};
|
|
||||||
--tag-pad : #{$tag-pad};
|
|
||||||
--tag-inset-shadow-size : #{$tag-inset-shadow-size};
|
|
||||||
--tag-invalid-color : #{$tag-invalid-color};
|
|
||||||
--tag-invalid-bg : #{$tag-invalid-bg};
|
|
||||||
--tag-remove-bg : #{rgba($tag-remove, .3)};
|
|
||||||
--tag-remove-btn-color : #{$tag-remove-btn-color};
|
|
||||||
--tag-remove-btn-bg : #{$tag-remove-btn-bg};
|
|
||||||
--tag-remove-btn-bg--hover : #{$tag-remove-btn-bg--hover};
|
|
||||||
--input-color : #{$input-color};
|
|
||||||
--tag--min-width : #{$tag-min-width};
|
|
||||||
--tag--max-width : #{$tag-max-width};
|
|
||||||
--tag-hide-transition : #{$tag-hide-transition};
|
|
||||||
--placeholder-color : #{$placeholder-color};
|
|
||||||
--placeholder-color-focus : #{$placeholder-color-focus};
|
|
||||||
--loader-size : .8em;
|
|
||||||
|
|
||||||
@mixin firefox {
|
|
||||||
@at-root {
|
|
||||||
@-moz-document url-prefix() {
|
|
||||||
& { @content; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin placeholder( $show:true ){
|
|
||||||
transition: .2s ease-out;
|
|
||||||
|
|
||||||
@if $show == true {
|
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
@else {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translatex(6px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin loader(){
|
|
||||||
content: '';
|
|
||||||
vertical-align: middle;
|
|
||||||
opacity: 1;
|
|
||||||
width: .7em;
|
|
||||||
height: .7em;
|
|
||||||
width: var(--loader-size);
|
|
||||||
height: var(--loader-size);
|
|
||||||
border: 3px solid;
|
|
||||||
border-color: #EEE #BBB #888 transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: rotateLoader .4s infinite linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin tagReadonlyBG{
|
|
||||||
background: linear-gradient(45deg, var(--tag-bg) 25%,
|
|
||||||
transparent 25%,
|
|
||||||
transparent 50%,
|
|
||||||
var(--tag-bg) 50%,
|
|
||||||
var(--tag-bg) 75%,
|
|
||||||
transparent 75%,
|
|
||||||
transparent) 0/5px 5px;
|
|
||||||
box-shadow: none;
|
|
||||||
filter: brightness(.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes tags--bump{
|
|
||||||
30% { transform: scale(1.2); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes rotateLoader {
|
|
||||||
to{ transform: rotate(1turn) }
|
|
||||||
}
|
|
||||||
|
|
||||||
display : flex;
|
|
||||||
align-items : flex-start;
|
|
||||||
flex-wrap : wrap;
|
|
||||||
border : 1px solid $tags-border-color;
|
|
||||||
border : 1px solid var(--tags-border-color);
|
|
||||||
padding : 0;
|
|
||||||
line-height : 1.1;
|
|
||||||
cursor : text;
|
|
||||||
outline : none;
|
|
||||||
position : relative;
|
|
||||||
box-sizing : border-box;
|
|
||||||
transition : .1s;
|
|
||||||
|
|
||||||
&:hover{
|
|
||||||
border-color: $tags-hover-border-color;
|
|
||||||
border-color: var(--tags-hover-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.tagify--focus{
|
|
||||||
transition: 0s;
|
|
||||||
border-color: $tags-focus-border-color;
|
|
||||||
border-color: var(--tags-focus-border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global "read-only" mode (no input button)
|
|
||||||
&[readonly]{
|
|
||||||
&:not(.tagify--mix){
|
|
||||||
cursor: default;
|
|
||||||
> #{ $self }__input{
|
|
||||||
visibility: hidden;
|
|
||||||
width: 0;
|
|
||||||
margin: $tagMargin 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#{ $self }__tag > div{
|
|
||||||
padding: $tag-pad;
|
|
||||||
padding: var(--tag-pad);
|
|
||||||
&::before{
|
|
||||||
@include tagReadonlyBG;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#{ $self }__tag__removeBtn{ display:none; }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
&--loading{
|
|
||||||
#{ $self }__input{
|
|
||||||
&::before{ content:none; }
|
|
||||||
&::after{
|
|
||||||
@include loader;
|
|
||||||
margin: -2px 0 -2px .5em;
|
|
||||||
}
|
|
||||||
&:empty{
|
|
||||||
&::after{
|
|
||||||
margin-left:0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////
|
|
||||||
// Hides originals
|
|
||||||
+ input,
|
|
||||||
+ textarea{ display:none !important; }
|
|
||||||
|
|
||||||
&__tag{
|
|
||||||
display : inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
margin : $tagMargin 0 $tagMargin $tagMargin;
|
|
||||||
position : relative;
|
|
||||||
z-index : 1;
|
|
||||||
outline : none;
|
|
||||||
cursor : default;
|
|
||||||
transition : .13s ease-out;
|
|
||||||
|
|
||||||
> div{ // :not([contenteditable])
|
|
||||||
vertical-align : top;
|
|
||||||
box-sizing : border-box;
|
|
||||||
max-width : 100%;
|
|
||||||
padding : $tag-pad;
|
|
||||||
padding : var(--tag-pad, $tag-pad);
|
|
||||||
color : $tag-text-color;
|
|
||||||
color : var(--tag-text-color, $tag-text-color);
|
|
||||||
line-height : inherit;
|
|
||||||
border-radius : 3px;
|
|
||||||
// user-select : none; // should allow selecting text if the user wishes to copy something
|
|
||||||
white-space : nowrap;
|
|
||||||
transition : .13s ease-out;
|
|
||||||
|
|
||||||
> *{
|
|
||||||
white-space : pre-wrap;
|
|
||||||
overflow : hidden;
|
|
||||||
text-overflow : ellipsis;
|
|
||||||
display : inline-block;
|
|
||||||
vertical-align : top;
|
|
||||||
min-width : $tag-min-width;
|
|
||||||
max-width : $tag-max-width;
|
|
||||||
min-width : var(--tag--min-width, $tag-min-width);
|
|
||||||
max-width : var(--tag--max-width, $tag-max-width);
|
|
||||||
transition : .8s ease, .1s color;
|
|
||||||
|
|
||||||
|
|
||||||
&[contenteditable]{
|
|
||||||
outline: none;
|
|
||||||
user-select: text;
|
|
||||||
cursor: text;
|
|
||||||
// fix: sometimes the caret after the last character wasn't visible (when setting {backspace:"edit"})
|
|
||||||
margin: -2px;
|
|
||||||
padding: 2px;
|
|
||||||
max-width: 350px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before{
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
border-radius: inherit;
|
|
||||||
left:0; top:0; right:0; bottom:0;
|
|
||||||
z-index: -1;
|
|
||||||
pointer-events:none;
|
|
||||||
transition: 120ms ease;
|
|
||||||
animation : tags--bump .3s ease-out 1;
|
|
||||||
|
|
||||||
box-shadow: 0 0 0 $tag-inset-shadow-size $tag-bg inset;
|
|
||||||
box-shadow: 0 0 0 var(--tag-inset-shadow-size, $tag-inset-shadow-size) var(--tag-bg, $tag-bg) inset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover:not([readonly]){
|
|
||||||
div{ // :not([contenteditable])
|
|
||||||
&::before{
|
|
||||||
$size: -$tagMargin/2;
|
|
||||||
$size: -2px;
|
|
||||||
top:$size; right:$size; bottom:$size; left:$size;
|
|
||||||
box-shadow: 0 0 0 $tag-inset-shadow-size $tag-hover inset;
|
|
||||||
box-shadow: 0 0 0 var(--tag-inset-shadow-size, $tag-inset-shadow-size) var(--tag-hover, $tag-hover) inset;
|
|
||||||
// box-shadow: 0 0 0 0 $tag-remove inset
|
|
||||||
}
|
|
||||||
// background:nth($tagColor,2);
|
|
||||||
//background:none;
|
|
||||||
// box-shadow: 0 0 0 2px $tag-hover inset;
|
|
||||||
// transition:50ms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--loading{
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
.tagify__tag__removeBtn{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after{
|
|
||||||
--loader-size: .4em;
|
|
||||||
@include loader;
|
|
||||||
margin: 0 .5em 0 -.1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--flash{
|
|
||||||
div::before{ animation:none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
&--hide{
|
|
||||||
width : 0 !important;
|
|
||||||
padding-left : 0;
|
|
||||||
padding-right : 0;
|
|
||||||
margin-left : 0;
|
|
||||||
margin-right : 0;
|
|
||||||
opacity : 0;
|
|
||||||
transform : scale(0);
|
|
||||||
transition : $tag-hide-transition;
|
|
||||||
transition : var(--tag-hide-transition, $tag-hide-transition);
|
|
||||||
pointer-events : none;
|
|
||||||
|
|
||||||
> div > *{
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&#{ $self }{
|
|
||||||
&--noAnim{
|
|
||||||
> div::before{
|
|
||||||
animation:none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--notAllowed:not(.tagify__tag--editable){
|
|
||||||
div{
|
|
||||||
> span{ opacity:.5; } // filter:blur(.2px);
|
|
||||||
&::before{
|
|
||||||
box-shadow: 0 0 0 $tag-inset-shadow-size $tag-invalid-bg inset !important;
|
|
||||||
box-shadow: 0 0 0 var(--tag-inset-shadow-size, $tag-inset-shadow-size) var(--tag-invalid-bg, $tag-invalid-bg) inset !important;
|
|
||||||
transition: .2s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[readonly]{
|
|
||||||
#{ $self }__tag__removeBtn{ display:none; }
|
|
||||||
> div{// padding: $tag-pad;
|
|
||||||
&::before{
|
|
||||||
@include tagReadonlyBG;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--editable{
|
|
||||||
> div{
|
|
||||||
color : $tag-text-color--edit;
|
|
||||||
color : var(--tag-text-color--edit, $tag-text-color--edit);
|
|
||||||
|
|
||||||
&::before{
|
|
||||||
box-shadow: 0 0 0 2px $tag-hover inset !important;
|
|
||||||
box-shadow: 0 0 0 2px var(--tag-hover, $tag-hover) inset !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> #{$self}__tag__removeBtn{
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&::after{
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(100%) translateX(5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.tagify--invalid{
|
|
||||||
> div{
|
|
||||||
&::before{
|
|
||||||
box-shadow: 0 0 0 2px $tag-invalid-color inset !important;
|
|
||||||
box-shadow: 0 0 0 2px var(--tag-invalid-color, $tag-invalid-color) inset !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__removeBtn{
|
|
||||||
$size: 14px;
|
|
||||||
|
|
||||||
order : 5;
|
|
||||||
display : inline-flex;
|
|
||||||
align-items : center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius : 50px;
|
|
||||||
cursor : pointer;
|
|
||||||
font : #{$size}/1 Arial;
|
|
||||||
background : $tag-remove-btn-bg;
|
|
||||||
background : var(--tag-remove-btn-bg, $tag-remove-btn-bg);
|
|
||||||
color : $tag-remove-btn-color;
|
|
||||||
color : var(--tag-remove-btn-color, $tag-remove-btn-color);
|
|
||||||
|
|
||||||
width : $size;
|
|
||||||
height : $size;
|
|
||||||
margin-right : $size/3;
|
|
||||||
margin-left : -$size/3;
|
|
||||||
|
|
||||||
overflow : hidden;
|
|
||||||
transition : .2s ease-out;
|
|
||||||
|
|
||||||
&::after{
|
|
||||||
content: "\00D7";
|
|
||||||
transition: .3s, color 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover{
|
|
||||||
color: white;
|
|
||||||
background: $tag-remove-btn-bg--hover;
|
|
||||||
background: var(--tag-remove-btn-bg--hover, $tag-remove-btn-bg--hover);
|
|
||||||
// + span{ box-shadow: 0 0 0 2px $tag-remove inset; transition:.2s; }
|
|
||||||
+ div{
|
|
||||||
> span{ opacity:.5; } // filter:blur(.2px);
|
|
||||||
&::before{
|
|
||||||
box-shadow: 0 0 0 $tag-inset-shadow-size rgba($tag-remove, .3) inset !important;
|
|
||||||
box-shadow: 0 0 0 var(--tag-inset-shadow-size, $tag-inset-shadow-size) var(--tag-remove-bg, rgba($tag-remove, .3)) inset !important;
|
|
||||||
transition: box-shadow .2s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
&:not(#{$self}--mix){
|
|
||||||
#{ $self }__input{
|
|
||||||
// https://stackoverflow.com/a/13470210/104380
|
|
||||||
br { display:none; }
|
|
||||||
* { display:inline; white-space:nowrap; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////
|
|
||||||
// Holds the placeholder & the tags input
|
|
||||||
&__input{
|
|
||||||
$placeholder-width : 110px;
|
|
||||||
flex-grow: 1;
|
|
||||||
display: inline-block;
|
|
||||||
min-width: $placeholder-width;
|
|
||||||
margin: $tagMargin;
|
|
||||||
padding: $tag-pad;
|
|
||||||
padding: var(--tag-pad, $tag-pad);
|
|
||||||
line-height: inherit;
|
|
||||||
position: relative;
|
|
||||||
white-space: pre-wrap; // #160 Line break (\n) as delimeter
|
|
||||||
color: $input-color;
|
|
||||||
color: var(--input-color, $input-color);
|
|
||||||
box-sizing: inherit;
|
|
||||||
|
|
||||||
&:empty{
|
|
||||||
@include firefox {
|
|
||||||
// clicking twice on the input (not fast) disallows typing (bug) only when the input has "display:flex".
|
|
||||||
// disabled the below rule for the above reason:
|
|
||||||
// display: flex; // https://bugzilla.mozilla.org/show_bug.cgi?id=904846#c45
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before{
|
|
||||||
@include placeholder;
|
|
||||||
display: inline-block;
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
#{ $self }--mix &{
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus{
|
|
||||||
outline:none;
|
|
||||||
|
|
||||||
&::before{
|
|
||||||
@include placeholder(false);
|
|
||||||
|
|
||||||
/* ALL MS BROWSERS: hide placeholder (on focus) otherwise the caret is places after it, which is weird */
|
|
||||||
/* IE10+ CSS styles go here */
|
|
||||||
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
/* IE Edge 12+ CSS styles go here */
|
|
||||||
@supports ( -ms-ime-align:auto ) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:empty{
|
|
||||||
&::before{
|
|
||||||
@include placeholder(true);
|
|
||||||
|
|
||||||
// Seems to be fixed! no need for the below hack
|
|
||||||
// @include firefox {
|
|
||||||
// // remove ":after" pseudo element: https://bugzilla.mozilla.org/show_bug.cgi?id=904846#c45
|
|
||||||
// content: unset;
|
|
||||||
// // display:inline-block;
|
|
||||||
// }
|
|
||||||
|
|
||||||
color: $placeholder-color-focus;
|
|
||||||
color: var(--placeholder-color-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after{
|
|
||||||
@include firefox {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before{
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
height: 1em;
|
|
||||||
line-height: 1em;
|
|
||||||
margin: auto 0;
|
|
||||||
z-index: 1;
|
|
||||||
color: $placeholder-color;
|
|
||||||
color: var(--placeholder-color);
|
|
||||||
white-space: nowrap;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
#{$self}--mix &{
|
|
||||||
display: none;
|
|
||||||
position: static;
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Seems firefox newer versions don't need this any more
|
|
||||||
@supports ( -moz-appearance:none ){
|
|
||||||
&::before{
|
|
||||||
line-height: inherit;
|
|
||||||
position:relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// tries to suggest the rest of the value from the first item in the whitelist which matches it
|
|
||||||
&::after{
|
|
||||||
content: attr(data-suggest);
|
|
||||||
display: inline-block;
|
|
||||||
white-space: pre; /* allows spaces at the beginning */
|
|
||||||
color: $tag-text-color;
|
|
||||||
opacity: .3;
|
|
||||||
pointer-events:none;
|
|
||||||
max-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// &--invalid{
|
|
||||||
// // color: $invalid-input-color;
|
|
||||||
// }
|
|
||||||
|
|
||||||
/*
|
|
||||||
in "mix mode" the tags are inside the "input" element
|
|
||||||
*/
|
|
||||||
#{ $self }__tag{
|
|
||||||
margin: 0; // a developer can choose to have automatic horizontal margin ("1ch" advised) between tags or use manual keyboard spaces
|
|
||||||
// line-height: 1.1;
|
|
||||||
|
|
||||||
> div{
|
|
||||||
padding-top:0; padding-bottom:0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--mix {
|
|
||||||
display: block; // display:flex makes Chrome generates <div><br></div> when pressing ENTER key
|
|
||||||
|
|
||||||
#{ $self }__input{
|
|
||||||
padding: $tagMargin;
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
&::before{ height:auto; }
|
|
||||||
|
|
||||||
// no suggested-complete are shown in mix-mode while higilighting dropdown options
|
|
||||||
&::after{ content:none; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--select{
|
|
||||||
&::after{
|
|
||||||
$size: 16px;
|
|
||||||
content: '>';
|
|
||||||
opacity: .5;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
font: $size monospace;
|
|
||||||
line-height: $size/2;
|
|
||||||
height: $size/2;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translate(-150%, -50%) scaleX(1.2) rotate(90deg);
|
|
||||||
transition: .2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-expanded=true]{
|
|
||||||
&::after{
|
|
||||||
transform: translate(-150%, -50%) rotate(270deg) scaleY(1.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#{$self}__tag{
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 1.8em;
|
|
||||||
bottom: 0;
|
|
||||||
div{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#{$self}__input{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--invalid{
|
|
||||||
--tags-border-color : #{$tag-invalid-color};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since the dropdown is an external element, which is positioned directly on the body element
|
|
||||||
// it cannot ingerit the CSS variables applied on the ".Tagify" element
|
|
||||||
&__dropdown{
|
|
||||||
$dropdown: &;
|
|
||||||
$trans: .25s cubic-bezier(0,1,.5,1);
|
|
||||||
position: absolute;
|
|
||||||
z-index: 9999;
|
|
||||||
transform: translateY(1px);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&[placement="top"]{
|
|
||||||
margin-top: 0;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
#{$dropdown}__wrapper{
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-bottom-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// when the dropdown shows next to the caret while typing
|
|
||||||
&[position="text"]{
|
|
||||||
box-shadow: 0 0 0 3px rgba(var(--tagify-dd-color-primary), .1);
|
|
||||||
font-size: .9em;
|
|
||||||
#{$dropdown}__wrapper{
|
|
||||||
border-width: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__wrapper{
|
|
||||||
max-height: 300px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: $tagify-dd-bg-color;
|
|
||||||
background: var(--tagify-dd-bg-color);
|
|
||||||
border: 1px solid $tags-focus-border-color;
|
|
||||||
border-color: var(--tagify-dd-color-primary);
|
|
||||||
border-top-width: 0;
|
|
||||||
box-shadow: 0 2px 4px -2px rgba(black,.2);
|
|
||||||
// box-sizing: border-box;
|
|
||||||
transition: $trans;
|
|
||||||
|
|
||||||
&:hover{
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// intial state, pre-rendered
|
|
||||||
&--initial{
|
|
||||||
#{$dropdown}__wrapper{
|
|
||||||
max-height: 20px;
|
|
||||||
transform: translateY(-1em);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[placement="top"]{
|
|
||||||
#{$dropdown}__wrapper{
|
|
||||||
transform: translateY(2em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__item{
|
|
||||||
box-sizing: inherit;
|
|
||||||
padding: $tag-pad;
|
|
||||||
margin: 1px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 2px;
|
|
||||||
position: relative;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
&--active{
|
|
||||||
background: $tagify-dd-color-primary;
|
|
||||||
background: var(--tagify-dd-color-primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
&:active{
|
|
||||||
filter: brightness(105%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"requires": true,
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"dependencies": {
|
|
||||||
"@yaireo/tagify": {
|
|
||||||
"version": "3.21.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-3.21.0.tgz",
|
|
||||||
"integrity": "sha512-pTr85RlPa7skaUgWCZkaqKpwsAkAiNi5FdLbXFnrhTo+oDMZb0w2rndKIca2bggwWUSsHC+TP2BY7ju8KZu9Gg=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user