Compare commits

...

2 Commits

27 changed files with 5768 additions and 3 deletions

46
node_modules/@yaireo/tagify/.eslintrc generated vendored Normal file
View File

@ -0,0 +1,46 @@
{
"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 Normal file
View File

@ -0,0 +1,17 @@
# 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 Normal file
View File

@ -0,0 +1,19 @@
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 Normal file
View File

@ -0,0 +1,888 @@
<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>&lt;Tags/&gt;</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: ",&#124;.&#124; " (*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 Normal file
View File

@ -0,0 +1,119 @@
{
"_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 Normal file
View File

@ -0,0 +1,13 @@
- [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*

View File

@ -0,0 +1,28 @@
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
})
}

View File

@ -0,0 +1,26 @@
// 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;
};
}

View File

@ -0,0 +1,3 @@
// Avoid transformation text to link ie contentEditable mode
// https://stackoverflow.com/q/7556007/104380
document.execCommand("AutoUrlDetect", false, false);

View File

@ -0,0 +1,263 @@
/*
* 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;
}());
}

View File

@ -0,0 +1,11 @@
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;
};
}

View File

@ -0,0 +1,3 @@
// 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 Normal file
View File

@ -0,0 +1,13 @@
// 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;
}

View File

@ -0,0 +1,3 @@
if( window.NodeList && !NodeList.prototype.forEach ){
NodeList.prototype.forEach = Array.prototype.forEach;
}

View File

@ -0,0 +1,30 @@
// 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
});
}

View File

@ -0,0 +1,12 @@
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;
};
}

View File

@ -0,0 +1,2 @@
// 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 Normal file
View File

@ -0,0 +1,1172 @@
/*!
* @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 Normal file
View File

@ -0,0 +1,190 @@
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 Normal file
View File

@ -0,0 +1,1478 @@
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 //+ "&#8288;" // 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 Normal file
View File

@ -0,0 +1,12 @@
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 Normal file
View File

@ -0,0 +1,674 @@
: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 Normal file
View File

@ -0,0 +1,11 @@
{
"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=="
}
}
}

5
tags.sql Normal file
View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS tags(
id INTEGER PRIMARY KEY,
tag VARCHAR,
)

View File

@ -0,0 +1,55 @@
.bootstrap-tagsinput {
background-color: #fff;
border: 1px solid #ccc;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
display: inline-block;
padding: 4px 6px;
color: #555;
vertical-align: middle;
border-radius: 4px;
max-width: 100%;
line-height: 22px;
cursor: text;
}
.bootstrap-tagsinput input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0 6px;
margin: 0;
width: auto;
max-width: inherit;
}
.bootstrap-tagsinput.form-control input::-moz-placeholder {
color: #777;
opacity: 1;
}
.bootstrap-tagsinput.form-control input:-ms-input-placeholder {
color: #777;
}
.bootstrap-tagsinput.form-control input::-webkit-input-placeholder {
color: #777;
}
.bootstrap-tagsinput input:focus {
border: none;
box-shadow: none;
}
.bootstrap-tagsinput .tag {
margin-right: 2px;
color: white;
}
.bootstrap-tagsinput .tag [data-role="remove"] {
margin-left: 8px;
cursor: pointer;
}
.bootstrap-tagsinput .tag [data-role="remove"]:after {
content: "x";
padding: 0px 2px;
}
.bootstrap-tagsinput .tag [data-role="remove"]:hover {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
}
.bootstrap-tagsinput .tag [data-role="remove"]:hover:active {
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}

View File

@ -0,0 +1,646 @@
(function ($) {
"use strict";
var defaultOptions = {
tagClass: function(item) {
return 'label label-info';
},
itemValue: function(item) {
return item ? item.toString() : item;
},
itemText: function(item) {
return this.itemValue(item);
},
itemTitle: function(item) {
return null;
},
freeInput: true,
addOnBlur: true,
maxTags: undefined,
maxChars: undefined,
confirmKeys: [13, 44],
delimiter: ',',
delimiterRegex: null,
cancelConfirmKeysOnEmpty: true,
onTagExists: function(item, $tag) {
$tag.hide().fadeIn();
},
trimValue: false,
allowDuplicates: false
};
/**
* Constructor function
*/
function TagsInput(element, options) {
this.itemsArray = [];
this.$element = $(element);
this.$element.hide();
this.isSelect = (element.tagName === 'SELECT');
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
this.objectItems = options && options.itemValue;
this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : '';
this.inputSize = Math.max(1, this.placeholderText.length);
this.$container = $('<div class="bootstrap-tagsinput"></div>');
this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
this.$element.before(this.$container);
this.build(options);
}
TagsInput.prototype = {
constructor: TagsInput,
/**
* Adds the given item as a new tag. Pass true to dontPushVal to prevent
* updating the elements val()
*/
add: function(item, dontPushVal, options) {
var self = this;
if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags)
return;
// Ignore falsey values, except false
if (item !== false && !item)
return;
// Trim value
if (typeof item === "string" && self.options.trimValue) {
item = $.trim(item);
}
// Throw an error when trying to add an object while the itemValue option was not set
if (typeof item === "object" && !self.objectItems)
throw("Can't add objects when itemValue option is not set");
// Ignore strings only containg whitespace
if (item.toString().match(/^\s*$/))
return;
// If SELECT but not multiple, remove current tag
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
self.remove(self.itemsArray[0]);
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter;
var items = item.split(delimiter);
if (items.length > 1) {
for (var i = 0; i < items.length; i++) {
this.add(items[i], true);
}
if (!dontPushVal)
self.pushVal();
return;
}
}
var itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item),
itemTitle = self.options.itemTitle(item);
// Ignore items allready added
var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0];
if (existing && !self.options.allowDuplicates) {
// Invoke onTagExists
if (self.options.onTagExists) {
var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; });
self.options.onTagExists(item, $existingTag);
}
return;
}
// if length greater than limit
if (self.items().toString().length + item.length + 1 > self.options.maxInputLength)
return;
// raise beforeItemAdd arg
var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options});
self.$element.trigger(beforeItemAddEvent);
if (beforeItemAddEvent.cancel)
return;
// register item in internal array and map
self.itemsArray.push(item);
// add a tag element
var $tag = $('<span class="tag ' + htmlEncode(tagClass) + (itemTitle !== null ? ('" title="' + itemTitle) : '') + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>');
$tag.data('item', item);
self.findInputWrapper().before($tag);
$tag.after(' ');
// add <option /> if item represents a value not present in one of the <select />'s options
if (self.isSelect && !$('option[value="' + encodeURIComponent(itemValue) + '"]',self.$element)[0]) {
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
$option.data('item', item);
$option.attr('value', itemValue);
self.$element.append($option);
}
if (!dontPushVal)
self.pushVal();
// Add class when reached maxTags
if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength)
self.$container.addClass('bootstrap-tagsinput-max');
self.$element.trigger($.Event('itemAdded', { item: item, options: options }));
},
/**
* Removes the given item. Pass true to dontPushVal to prevent updating the
* elements val()
*/
remove: function(item, dontPushVal, options) {
var self = this;
if (self.objectItems) {
if (typeof item === "object")
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } );
else
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } );
item = item[item.length-1];
}
if (item) {
var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options });
self.$element.trigger(beforeItemRemoveEvent);
if (beforeItemRemoveEvent.cancel)
return;
$('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
if($.inArray(item, self.itemsArray) !== -1)
self.itemsArray.splice($.inArray(item, self.itemsArray), 1);
}
if (!dontPushVal)
self.pushVal();
// Remove class when reached maxTags
if (self.options.maxTags > self.itemsArray.length)
self.$container.removeClass('bootstrap-tagsinput-max');
self.$element.trigger($.Event('itemRemoved', { item: item, options: options }));
},
/**
* Removes all items
*/
removeAll: function() {
var self = this;
$('.tag', self.$container).remove();
$('option', self.$element).remove();
while(self.itemsArray.length > 0)
self.itemsArray.pop();
self.pushVal();
},
/**
* Refreshes the tags so they match the text/value of their corresponding
* item.
*/
refresh: function() {
var self = this;
$('.tag', self.$container).each(function() {
var $tag = $(this),
item = $tag.data('item'),
itemValue = self.options.itemValue(item),
itemText = self.options.itemText(item),
tagClass = self.options.tagClass(item);
// Update tag's class and inner text
$tag.attr('class', null);
$tag.addClass('tag ' + htmlEncode(tagClass));
$tag.contents().filter(function() {
return this.nodeType == 3;
})[0].nodeValue = htmlEncode(itemText);
if (self.isSelect) {
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
option.attr('value', itemValue);
}
});
},
/**
* Returns the items added as tags
*/
items: function() {
return this.itemsArray;
},
/**
* Assembly value by retrieving the value of each item, and set it on the
* element.
*/
pushVal: function() {
var self = this,
val = $.map(self.items(), function(item) {
return self.options.itemValue(item).toString();
});
self.$element.val(val, true).trigger('change');
},
/**
* Initializes the tags input behaviour on the element
*/
build: function(options) {
var self = this;
self.options = $.extend({}, defaultOptions, options);
// When itemValue is set, freeInput should always be false
if (self.objectItems)
self.options.freeInput = false;
makeOptionItemFunction(self.options, 'itemValue');
makeOptionItemFunction(self.options, 'itemText');
makeOptionFunction(self.options, 'tagClass');
// Typeahead Bootstrap version 2.3.2
if (self.options.typeahead) {
var typeahead = self.options.typeahead || {};
makeOptionFunction(typeahead, 'source');
self.$input.typeahead($.extend({}, typeahead, {
source: function (query, process) {
function processItems(items) {
var texts = [];
for (var i = 0; i < items.length; i++) {
var text = self.options.itemText(items[i]);
map[text] = items[i];
texts.push(text);
}
process(texts);
}
this.map = {};
var map = this.map,
data = typeahead.source(query);
if ($.isFunction(data.success)) {
// support for Angular callbacks
data.success(processItems);
} else if ($.isFunction(data.then)) {
// support for Angular promises
data.then(processItems);
} else {
// support for functions and jquery promises
$.when(data)
.then(processItems);
}
},
updater: function (text) {
self.add(this.map[text]);
return this.map[text];
},
matcher: function (text) {
return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
},
sorter: function (texts) {
return texts.sort();
},
highlighter: function (text) {
var regex = new RegExp( '(' + this.query + ')', 'gi' );
return text.replace( regex, "<strong>$1</strong>" );
}
}));
}
// typeahead.js
if (self.options.typeaheadjs) {
var typeaheadConfig = null;
var typeaheadDatasets = {};
// Determine if main configurations were passed or simply a dataset
var typeaheadjs = self.options.typeaheadjs;
if ($.isArray(typeaheadjs)) {
typeaheadConfig = typeaheadjs[0];
typeaheadDatasets = typeaheadjs[1];
} else {
typeaheadDatasets = typeaheadjs;
}
self.$input.typeahead(typeaheadConfig, typeaheadDatasets).on('typeahead:selected', $.proxy(function (obj, datum) {
if (typeaheadDatasets.valueKey)
self.add(datum[typeaheadDatasets.valueKey]);
else
self.add(datum);
self.$input.typeahead('val', '');
}, self));
}
self.$container.on('click', $.proxy(function(event) {
if (! self.$element.attr('disabled')) {
self.$input.removeAttr('disabled');
}
self.$input.focus();
}, self));
if (self.options.addOnBlur && self.options.freeInput) {
self.$input.on('focusout', $.proxy(function(event) {
// HACK: only process on focusout when no typeahead opened, to
// avoid adding the typeahead text as tag
if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) {
self.add(self.$input.val());
self.$input.val('');
}
}, self));
}
self.$container.on('keydown', 'input', $.proxy(function(event) {
var $input = $(event.target),
$inputWrapper = self.findInputWrapper();
if (self.$element.attr('disabled')) {
self.$input.attr('disabled', 'disabled');
return;
}
switch (event.which) {
// BACKSPACE
case 8:
if (doGetCaretPosition($input[0]) === 0) {
var prev = $inputWrapper.prev();
if (prev.length) {
self.remove(prev.data('item'));
}
}
break;
// DELETE
case 46:
if (doGetCaretPosition($input[0]) === 0) {
var next = $inputWrapper.next();
if (next.length) {
self.remove(next.data('item'));
}
}
break;
// LEFT ARROW
case 37:
// Try to move the input before the previous tag
var $prevTag = $inputWrapper.prev();
if ($input.val().length === 0 && $prevTag[0]) {
$prevTag.before($inputWrapper);
$input.focus();
}
break;
// RIGHT ARROW
case 39:
// Try to move the input after the next tag
var $nextTag = $inputWrapper.next();
if ($input.val().length === 0 && $nextTag[0]) {
$nextTag.after($inputWrapper);
$input.focus();
}
break;
default:
// ignore
}
// Reset internal input's size
var textLength = $input.val().length,
wordSpace = Math.ceil(textLength / 5),
size = textLength + wordSpace + 1;
$input.attr('size', Math.max(this.inputSize, $input.val().length));
}, self));
self.$container.on('keypress', 'input', $.proxy(function(event) {
var $input = $(event.target);
if (self.$element.attr('disabled')) {
self.$input.attr('disabled', 'disabled');
return;
}
var text = $input.val(),
maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars;
if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) {
// Only attempt to add a tag if there is data in the field
if (text.length !== 0) {
self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text);
$input.val('');
}
// If the field is empty, let the event triggered fire as usual
if (self.options.cancelConfirmKeysOnEmpty === false) {
event.preventDefault();
}
}
// Reset internal input's size
var textLength = $input.val().length,
wordSpace = Math.ceil(textLength / 5),
size = textLength + wordSpace + 1;
$input.attr('size', Math.max(this.inputSize, $input.val().length));
}, self));
// Remove icon clicked
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
if (self.$element.attr('disabled')) {
return;
}
self.remove($(event.target).closest('.tag').data('item'));
}, self));
// Only add existing value as tags when using strings as tags
if (self.options.itemValue === defaultOptions.itemValue) {
if (self.$element[0].tagName === 'INPUT') {
self.add(self.$element.val());
} else {
$('option', self.$element).each(function() {
self.add($(this).attr('value'), true);
});
}
}
},
/**
* Removes all tagsinput behaviour and unregsiter all event handlers
*/
destroy: function() {
var self = this;
// Unbind events
self.$container.off('keypress', 'input');
self.$container.off('click', '[role=remove]');
self.$container.remove();
self.$element.removeData('tagsinput');
self.$element.show();
},
/**
* Sets focus on the tagsinput
*/
focus: function() {
this.$input.focus();
},
/**
* Returns the internal input element
*/
input: function() {
return this.$input;
},
/**
* Returns the element which is wrapped around the internal input. This
* is normally the $container, but typeahead.js moves the $input element.
*/
findInputWrapper: function() {
var elt = this.$input[0],
container = this.$container[0];
while(elt && elt.parentNode !== container)
elt = elt.parentNode;
return $(elt);
}
};
/**
* Register JQuery plugin
*/
$.fn.tagsinput = function(arg1, arg2, arg3) {
var results = [];
this.each(function() {
var tagsinput = $(this).data('tagsinput');
// Initialize a new tags input
if (!tagsinput) {
tagsinput = new TagsInput(this, arg1);
$(this).data('tagsinput', tagsinput);
results.push(tagsinput);
if (this.tagName === 'SELECT') {
$('option', $(this)).attr('selected', 'selected');
}
// Init tags from $(this).val()
$(this).val($(this).val());
} else if (!arg1 && !arg2) {
// tagsinput already exists
// no function, trying to init
results.push(tagsinput);
} else if(tagsinput[arg1] !== undefined) {
// Invoke function on existing tags input
if(tagsinput[arg1].length === 3 && arg3 !== undefined){
var retVal = tagsinput[arg1](arg2, null, arg3);
}else{
var retVal = tagsinput[arg1](arg2);
}
if (retVal !== undefined)
results.push(retVal);
}
});
if ( typeof arg1 == 'string') {
// Return the results from the invoked function calls
return results.length > 1 ? results : results[0];
} else {
return results;
}
};
$.fn.tagsinput.Constructor = TagsInput;
/**
* Most options support both a string or number as well as a function as
* option value. This function makes sure that the option with the given
* key in the given options is wrapped in a function
*/
function makeOptionItemFunction(options, key) {
if (typeof options[key] !== 'function') {
var propertyName = options[key];
options[key] = function(item) { return item[propertyName]; };
}
}
function makeOptionFunction(options, key) {
if (typeof options[key] !== 'function') {
var value = options[key];
options[key] = function() { return value; };
}
}
/**
* HtmlEncodes the given value
*/
var htmlEncodeContainer = $('<div />');
function htmlEncode(value) {
if (value) {
return htmlEncodeContainer.text(value).html();
} else {
return '';
}
}
/**
* Returns the position of the caret in the given input field
* http://flightschool.acylt.com/devnotes/caret-position-woes/
*/
function doGetCaretPosition(oField) {
var iCaretPos = 0;
if (document.selection) {
oField.focus ();
var oSel = document.selection.createRange();
oSel.moveStart ('character', -oField.value.length);
iCaretPos = oSel.text.length;
} else if (oField.selectionStart || oField.selectionStart == '0') {
iCaretPos = oField.selectionStart;
}
return (iCaretPos);
}
/**
* Returns boolean indicates whether user has pressed an expected key combination.
* @param object keyPressEvent: JavaScript event object, refer
* http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
* @param object lookupList: expected key combinations, as in:
* [13, {which: 188, shiftKey: true}]
*/
function keyCombinationInList(keyPressEvent, lookupList) {
var found = false;
$.each(lookupList, function (index, keyCombination) {
if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) {
found = true;
return false;
}
if (keyPressEvent.which === keyCombination.which) {
var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey,
shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey,
ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey;
if (alt && shift && ctrl) {
found = true;
return false;
}
}
});
return found;
}
/**
* Initialize tagsinput behaviour on inputs and selects which have
* data-role=tagsinput
*/
$(function() {
$("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
});
})(window.jQuery);

View File

@ -9,8 +9,11 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ STATIC_PREFIX }}assets/css/bootstrap-tagsinput.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="{{ STATIC_PREFIX }}assets/js/bootstrap-tagsinput.js"></script>
<style>
body {
color: #566787;
@ -277,10 +280,15 @@
}
</style>
<script>
//import Tagify from '@yaireo/tagify';
$(document).ready(function(){
//window.onload= function(){
// Activate tooltip
$('[data-toggle="tooltip"]').tooltip();
//$('input[name=tags]').data('tagify').addTags('aaa, bbb, ccc');
//new Tagify(input)
// Select/Deselect checkboxes
var checkbox = $('table tbody input[type="checkbox"]');
@ -383,7 +391,10 @@ $(document).ready(function(){
}
elem.checked= false;
}
}
else if(elem.placeholder=="Tags"){
requestData.data.push(elem.value)
}
else{
requestData.data.push(elem.value)
row += `<td>${elem.value}</td>`
@ -451,7 +462,10 @@ $(document).ready(function(){
$(elem).prop('checked', false)
}
}
}
else if(elem.placeholder=="Tags"){
}
else{
elem.value= data[index]
}
@ -486,7 +500,10 @@ $(document).ready(function(){
}
elem.checked= false;
}
}
else if(elem.placeholder=="Tags"){
requestData.data.push(elem.value)
}
else{
requestData.data.push(elem.value)
row += `<td>${elem.value}</td>`
@ -703,6 +720,11 @@ $(document).ready(function(){
<label for="selectAll"></label>
</span-->
</div>
<div class="form-group">
<label>Tags</label>
<input type="text" value="" data-role="tagsinput" placeholder="Tags" />
</div>
<div class="form-group">
<label>Power Option</label>
<select class="browser-default custom-select sel-add" type="select">
@ -760,6 +782,10 @@ $(document).ready(function(){
<label for="selectAll"></label>
</span-->
</div>
<div class="form-group">
<label>Tags</label>
<input type="text" value="hello, world" data-role="tagsinput" placeholder="Tags" />
</div>
<div class="form-group">
<label>Power Option</label>
<select class="browser-default custom-select sel-edit" type="select">