diff --git a/build.gradle b/build.gradle
index 38123b0..4a6fbfd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -30,6 +30,11 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
+ implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.9'
+ implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.7.1'
+ implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
+
+
implementation 'com.querydsl:querydsl-jpa'
implementation 'com.github.node-gradle:gradle-node-plugin:3.1.0'
@@ -44,6 +49,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
+ testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.2.0'
}
test {
diff --git a/node_modules/@yaireo/tagify/LICENSE b/node_modules/@yaireo/tagify/LICENSE
new file mode 100644
index 0000000..4110d3c
--- /dev/null
+++ b/node_modules/@yaireo/tagify/LICENSE
@@ -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.
\ No newline at end of file
diff --git a/node_modules/@yaireo/tagify/README.md b/node_modules/@yaireo/tagify/README.md
new file mode 100644
index 0000000..36febc0
--- /dev/null
+++ b/node_modules/@yaireo/tagify/README.md
@@ -0,0 +1,995 @@
+
+
+
+ Tagify - tags input component
+
+
+
+ Transforms an input field or a textarea into a Tags component , in an easy, customizable way,
+ with great performance and small code footprint, exploded with features.
+
+ Vanilla ⚡ React ⚡ Vue ⚡ Angular
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Table of Contents
+
+
+- [Table of Contents](#table-of-contents)
+- [Installation](#installation)
+ - [Option 1 - import from CDN:](#option-1---import-from-cdn)
+ - [option 2 - import as a *Node module*:](#option-2---import-as-a-node-module)
+ - [Usage (in your bundle):](#usage-in-your-bundle)
+- [Features](#features)
+- [Building the project](#building-the-project)
+- [Adding tags dynamically](#adding-tags-dynamically)
+- [Output value](#output-value)
+ - [Modify original input value format](#modify-original-input-value-format)
+- [Ajax whitelist](#ajax-whitelist)
+- [Edit tags](#edit-tags)
+- [Validations](#validations)
+- [Drag & Sort](#drag--sort)
+ - [Integration example:](#integration-example)
+- [DOM Templates](#dom-templates)
+ - [Example of overriding the `tag` template:](#example-of-overriding-the-tag-template)
+- [Suggestions list](#suggestions-list)
+ - [Example for a suggestion item alias](#example-for-a-suggestion-item-alias)
+ - [Example whitelist:](#example-whitelist)
+- [Mixed-Content](#mixed-content)
+- [Single-Value](#single-value)
+- [React](#react)
+ - [Update regarding `onChange` prop:](#update-regarding-onchange-prop)
+ - [Updating the component's state](#updating-the-components-state)
+- [jQuery version](#jquery-version)
+- [CSS Variables](#css-variables)
+ - [Full list of Tagify's SCSS variables](#full-list-of-tagifys-scss-variables)
+- [Methods](#methods)
+- [Events](#events)
+- [Hooks](#hooks)
+- [Settings](#settings)
+
+
+## Installation
+
+### Option 1 - import from CDN:
+
+Place these lines before any other code which is (or will be) using *Tagify* ([Example here](https://jsbin.com/jekuqap/edit?html))
+```html
+
+
+
+```
+
+`Tagify` will then be available globally.
+To load specific version use `@` - for example: `unpkg.com/@yaireo/tagify@3.1.0`
+
+### option 2 - import as a *Node module*:
+```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 ``)
+* Supports whitelist/blacklist
+* Supports Templates for: component wrapper , tag items , suggestion list & suggestion items
+* Shows suggestions list (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* or by function
+* Tags may be [editable](#edit-tags) (double-click)
+* ARIA accessibility support(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. AJAX 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`) ***(IE support has been dropped)***
+* 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:
+`'[{"value":"cat"}, {"value":"dog"}]'`
+
+I **recommend** keeping this because some situations might have values such as addresses (tags contain commas):
+`'[{"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*:
+*(tags can be clicked, so delevopers can choose to use this to disable/enable tags)*
+`'[{"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:**
+`"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 loading
+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.
+
+
+ Example:
+
+```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.whitelist = null // 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()
+
+ fetch('http://get_suggestions.com?value=' + value, {signal:controller.signal})
+ .then(RES => RES.json())
+ .then(function(newWhitelist){
+ tagify.whitelist = newWhitelist // update inwhitelist Array in-place
+ tagify.loading(false).dropdown.show(value) // render the suggestions dropdown
+ })
+}
+```
+
+
+## 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`.
+ctrl z 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`).
+To do the same but for specific tag(s), set those tags' data with `editable` property set to `false`:
+
+```html
+
+```
+
+
+## Validations
+For "regular" tags (not *mix-mode* or *select-mode*) the easiest way is to use the `pattern` setting and use a Regex, or
+apply the `pattern` attribute directly on the `input` which will be "transformed" into a *Tagify* component (for vanilla code where the `input` tag is fully accessible to develops).
+
+If the `pattern` setting does not meet your needs, use the [`validate` setting](#settings), which recieves a *tag data object* as an argument and should return `true` if validaiton is passing, or `false`/`string` of not.
+A *string* may be returned as the reason of the validation failure so it would be printed as the `title` attribute of the invalid tag.
+
+Note - there is a setting to keep invalid tags ([`keepInvalidTags`](#settings)) and if it's set to `true`, the user can see the reason for the invalidation by
+hovering the tag and see the browser's native tooltip via the `title` attribute:
+
+```js
+{
+ empty : "empty",
+ exceed : "number of tags exceeded",
+ pattern : "pattern mismatch",
+ duplicate : "already exists",
+ notAllowed : "not allowed"
+}
+```
+
+The texts for those (invalid tags) *titles* can be customized from the settings:
+
+```js
+new Tagify(inputElement, {
+ texts: {
+ duplicate: "Duplicates are not allowed"
+ }
+})
+```
+
+Or by directly manipulating the *Tagify* function *prototype*:
+
+```js
+Tagify.prototype.TEXTS = {...Tagify.prototype.TEXTS, {duplicate: "Duplicates are not allowed"}}
+```
+
+## 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)
+
+### Example of overriding the `tag` template:
+
+Each template function automaticaly gets binded with `this` pointing to the current *Tagify* instance.
+It is imperative to preserve the class names and also the `this.getAttributes(tagData)` for proper functionality.
+
+```js
+new Tagify(inputElem, {
+ templates: {
+ tag(tagData, tagify){
+ return `
+
+
+ ${tagData[this.settings.tagTextProp] || tagData.value}
+
+ `
+ }
+})
+```
+
+## Suggestions list
+
+
+
+
+
+The suggestions list is a *whitelist Array* of *Strings* or *Objects* which was set in the [settings](#settings) Object when the Tagify instance was created, and can be set latet directly on the instance: `tagifyInstance.whitelist = ["tag1", "tag2", ...]`.
+
+The suggestions dropdown will be appended to the document's `` 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 list 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 list 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
+ }
+ });
+```
+
+Will render
+
+```html
+
+
+
aaab
+
aaabb
+
aaabc
+
aaabd
+
aaabe
+
+
+```
+
+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
+
+[See demo here](https://yaireo.github.io/tagify/#section-mix)
+
+This feature must be toggled using these [settings](#settings):
+
+```js
+{
+ // mixTagsInterpolator: ["{{", "}}"], // optional: interpolation before & after string
+ mode: 'mix', // <-- Enable mixed-content
+ pattern: /@|#/ // <-- Text starting with @ or # (if single, String can be used here instead of Regex)
+}
+```
+
+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
+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 `` element, but allows typing text as value.
+
+## React
+
+See [**live demo**](https://codesandbox.io/s/tagify-react-wrapper-oempc) for React integration examples.
+⚠️ Tagify is **not** a [controlled component](https://github.com/yairEO/tagify/issues/489#issuecomment-629316093).
+
+A Tagify React component is exported from [`react.tagify.js`](https://github.com/yairEO/tagify/blob/master/dist/react.tagify.js):
+
+---
+### Update regarding `onChange` prop:
+
+I have changed how the `onChange` works internally within the Wrapper of Tagify
+so as of *March 30, 2021* the `e` argument will include a `detail` parameter with the value as string.
+There is no more `e.target`, and to access the original DOM input element, do this: `e.detail.tagify.DOM.originalInput`.
+
+----
+
+> Note: You will need to inport Tagify's CSS also, either by javasceript or by SCSS `@import` (which is preferable)
+> Also note that you will need to use [*dart-sass*](https://www.npmjs.com/package/sass) and not *node-sass* in order to compile the file.
+
+```javascript
+import Tags from "@yaireo/tagify/dist/react.tagify" // React-wrapper file
+import "@yaireo/tagify/dist/tagify.css" // Tagify CSS
+
+// on tag add/edit/remove
+const onChange = useCallback((e) => {
+ console.log("CHANGED:"
+ , e.detail.tagify.value // Array where each tag includes tagify's (needed) extra properties
+ , e.detail.tagify.getCleanValue()) // Same as above, without the extra properties
+ , e.detail.value // a string representing the tags
+ )
+}, [])
+
+const App = () => {
+ return (
+
+ )
+})
+```
+
+To gain full access to Tagify's (instance) inner methods, A custom `ref` can be used:
+
+```jsx
+...
+const tagifyRef = useRef()
+...
+
+
+// or mix-mode
+
+```
+
+`` component is a shorthand for ``
+
+#### Updating the component's state
+
+The `settings` prop is **only used once** in the initialization process, please do not update it afterwards.
+
+---
+
+ 📖 List of (React) props for the <Tags/> component
+
+
+Prop | Type | Updatable | Info
+----------------------- | ------------------------- |:---------:| -----------------------------------------------------------
+settings | Object | | See [*settings* section](#settings)
+name | String | ✔ | ` `'s element `name` attribute
+value | String/Array | ✔ | Initial value.
+defaultValue | String/Array | | Same as `value prop
+placeholder | String | ✔ | placeholder text for the component
+readOnly | Boolean | ✔ | Toggles `readonly` state. With capital `O`.
+tagifyRef | Object | | `useRef` hook refference for the component inner instance of vailla *Tagify* (for methods access)
+showFilteredDropdown | Boolean/String | ✔ | if `true` shows the suggestions dropdown. if assigned a String, show the dropdown pre-filtered.
+loading | Boolean | ✔ | Toggles `loading` state for the whole component
+whitelist | Array | ✔ | Sets the `whitelist` which is the basis for the suggestions dropdown & autocomplete
+className | String | | Component's optional class name to be added
+InputMode | String | | `"textarea"` will create a `
+
+---
+
+
+## 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*.
+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) | ` ` | Tag Regex pattern which tag input is validated by.
+[placeholder](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefplaceholder) | ` ` | This attribute's value will be used as a constant placeholder, which is visible unless something is being typed.
+readOnly | ` ` | No user-interaction (add/remove/edit) allowed.
+autofocus | ` ` | Automatically focus the the Tagify component when the component is loaded
+required | ` ` | 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:
+
+
+ Dynamic whitelist
+The whitelist initial value is set like so:
+
+```javascript
+const tagify = new Tagify(tagNode, {
+ whitelist: ["a", "b", "c"]
+})
+```
+
+If changes to the whitelist are needed, they should be done like so:
+
+**Incorrect:**
+
+```js
+tagify.settings.whitelist = ["foo", "bar"]
+```
+
+**Correct:**
+```js
+// set the whitelist directly on the instance and not on the "settings" property
+tagify.whitelist = ["foo", "bar"]
+```
+
+
+---
+
+
+ tags/whitelist data structure
+
+Tagify does not accept just *any* kind of data structure.
+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"]
+```
+
+
+---
+
+
+ Save changes (Ex. to a server)
+
+In framework-less projects, the developer should save the state of the Tagify component (somewhere), and
+the question is:
+**when should the state be saved?**
+On every change made to *Tagify's* internal state (`tagify.value` via the `update()` method).
+
+
+```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 ` `/`
+
+----
+
+
+ Render tags in one single line
+
+Stopping tags from wrapping to new lines, add this to your `.tagify` *selector CSS Rule*:
+
+```css
+flex-wrap: nowrap;
+````
+
+
+----
+
+
+ Submit on `Enter` key
+
+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
+ }
+
+}
+```
+
+
+---
+
+* [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 validation & AJAX](https://github.com/yairEO/tagify/issues/474)
+* [Make tags from pasted multi-line text](https://github.com/yairEO/tagify/issues/160)
+* [Add a tag at *caret* position in *mixed mode*](https://github.com/yairEO/tagify/issues/524#issuecomment-699140465)
+* [Change automatic title tooltips for invalid tags]([https0465](https://github.com/yairEO/tagify/issues/862))
+
+## 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. It's important the size fits *exactly* to the tag. 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` | `Array`/`String`/`Object` tag(s) to add `Boolean` clear input after adding `Boolean` - skip adding invalids | Accepts a String (word, single or multiple with a delimiter), an Array of Objects (see above) or Strings.
+`addMixTags` | `Array`/`String` | Bypasses the normalization process in `addTags`, forcefully adding tags at the last caret location or at the end, if there's no last caret location saved (at `tagify.state.selection`)
+`removeTags` | `Array`/`HTMLElement`/`String` tag(s) to remove `silent` does not update the component's value `tranDuration` Transition duration (in `ms`) | (#502) Remove single/multiple Tags. When nothing passed, removes last tag. `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 `tranDuration` - delay for animation, after which the tag will be removed from the DOM
+`addEmptyTag` | `Object` (`tagData`) | 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` (`tagData`) | Exit a tag's edit-mode. if "tagData" exists, replace the tag element with new data and update Tagify value
+`loading` | `Boolean` | toggle loading state on/off (Ex. AJAX whitelist pulling)
+`tagLoading` | `HTMLElement`, Boolean | same as above but for a specific tag element
+`createTagElem` | `Object` (`tagData`) | Returns a tag element from the supplied tag data
+`injectAtCaret` | `HTMLElement` (`injectedNode`) , `Object` (`range`) | Injects text or HTML node at last caret position. `range` parameter is *optional*
+`placeCaretAfterNode` | `HTMLElement` | Places the caret after a given node
+`insertAfterTag` | `HTMLElement` (tag element) , `HTMLElement`/`String` (whatever to insert after) |
+`toggleClass` | `Boolean` | Toggles `class` on the main *tagify* container (`scope`)
+`dropdown.selectAll` | | Add **all** whitelist items as tags and close the suggestion dropdown
+`dropdown.show` | `String` | Shows the sugegstions list dropdown. A string paramater allows filtering the results
+`dropdown.hide` | `Boolean` | Hides the suggestions list dropdown (if it's not managed manually by the developer)
+`dropdown.toggle` | `Boolean` | Toggles dropdown show/hide. the boolean parameter will force-show
+`updateValueByDOMTags` | | Iterate tag DOM nodes and re-build the `tagify.value` array (call this if tags get sorted manually)
+`parseTemplate` | `String`/`Function` (template name or function) , `Array` (data) | 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
+`setDisabled` | `Boolean` | Toggles "disabled" mode on/off
+
+
+## Events
+
+All triggered events return the instance's scope (tagify).
+See `e.detail` for custom event additional data.
+
+
+ Example 1
+
+```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))
+```
+
+
+
+ Example 2
+
+```javascript
+var tagify = new Tagify(inputNode, {
+ callbacks: {
+ "change": (e) => console.log(e.detail))
+ "dropdown:show": (e) => console.log(e.detail))
+ }
+})
+```
+
+
+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" }]`
+dropdown:updated | Fired when the dropdown list is re-filtered while suggestions list is visible and a tag was removed so it was re-added as a suggestion
+
+## 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.
+
+
+ For example, if a developer wishes to add a (native) confirmation popup before a tag is removed (by a user action):
+
+
+```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()
+ })
+ }
+ }
+})
+```
+
+
+
+Name | Parameters | Info
+---------------------- | ------------------------------------------- | --------------------------------------------------------------------------
+beforeRemoveTag | Array (of Objects) | [Example](https://jsbin.com/xoseyux/edit?html,js,output)
+suggestionClick | Object (click event data) | [Example](https://jsbin.com/tuwihuf/edit?html,js,output)
+beforePaste | `tagify`, `pastedText`, `clipboardData` | Before pasted text was added to Tagify. *Resolve* with new paste value if needed
+
+## [Settings](https://github.com/yairEO/tagify/blob/master/src/parts/defaults.js#L1)
+
+Name | Type | Default | Info
+------------------------- | ---------------------------- | ------------------------------------------- | --------------------------------------------------------------------------
+tagTextProp | String | `"value"` | Tag data Object property which will be displayed as the tag's text. Remember to keep "value" property unique . See Also: `dropdown.mapValueTo`, `dropdown.searchKeys`
+placeholder | String | | Placeholder text. If this attribute is set on an input/textarea element it will override this setting
+delimiters | String | `","` | [RegEx **string**] split tags by any of these delimiters. Example delimeters: ",|.| " (*comma*, *dot* or *whitespace*)
+pattern | String/RegEx | null | Validate input by RegEx pattern (can also be applied on the input itself as an attribute) Ex: `/[1-9]/`
+mode | String | 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 | Array | `['[[', ']]']` | Interpolation for mix mode. Everything between these will become a tag
+mixTagsAllowedAfter | RegEx | `/,\|\.\|\:\|\s/` | Define conditions in which typed mix-tags content is allowing a tag to be created after.
+duplicates | Boolean | false | Should duplicate tags be allowed or not
+trim | Boolean | true | If `true` trim the tag's value (remove before/after whitespaces)
+enforceWhitelist | Boolean | false | Should ONLY use tags allowed in whitelist. In `mix-mode`, setting it to `false` will not allow creating new tags.
+userInput | Boolean | true | Disable manually typing/pasting/editing tags (tags may only be added from the whitelist)
+autoComplete.enabled | Boolean | 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 | Boolean | 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 | Array | [] | An array of allowed tags (*Strings* or *Objects*). When using *Objects* in the *whitelist* array a `value` property is a must & should be unique. Also, the *whitelist used for auto-completion when `autoCompletion.enabled` is `true`
+blacklist | Array | [] | An array of tags which aren't allowed
+addTagOnBlur | Boolean | true | Automatically adds the text which was inputed as a tag when blur event happens
+pasteAsTags | Boolean | true | Automatically converts pasted text into tags
+callbacks | Object | {} | Exposed callbacks object to be triggered on events: `'add'` / `'remove'` tags
+maxTags | Number | Infinity | Maximum number of allowed tags. when reached, adds a class "tagify--hasMaxTags" to ``
+editTags | Object/Number | {} | `false` or `null` will disallow editing
+editTags.*clicks* | Number | 2 | Number of clicks to enter "edit-mode": 1 for single click. Any other value is considered as double-click
+editTags.*keepInvalid* | Boolean | true | keeps invalid edits as-is until `esc` is pressed while in focus
+templates | Object | `wrapper`, `tag`, `dropdownItem` | Object consisting of functions which return template strings
+validate | Function | | If the `pattern` setting does not meet your needs, use this function, which recieves *tag data object* as an argument and should return `true` if validaiton passed or `false`/`string` of not. A *string* may be returned as the reason of the validation failure.
+transformTag | Function | | Takes a tag data as argument and allows mutating it before a tag is created or edited. Should not `return` anything, only **mutate**.
+keepInvalidTags | Boolean | false | If `true`, do not remove tags which did not pass validation
+skipInvalid | Boolean | false | If `true`, do not add invalid, temporary, tags before automatically removing them
+backspace | * | true | On pressing backspace key: `true` - remove last tag `edit` - edit last tag `false` - do nothing (useful for outside style)
+originalInputValueFormat | Function | | 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* | Node/String | `\u00A0` | `node` or `string` to add after a tag added |
+a11y.*focusableTags* | Boolean | false | allows tags to get focus, and also to be deleted via Backspace
+dropdown.*enabled* | Number | 2 | Minimum characters input for showing a suggestions list. `false` will not render a suggestions list.
+dropdown.*caseSensitive* | Boolean | 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* | Number | 10 | Maximum items to show in the suggestions list
+dropdown.*classname* | String | `""` | Custom *classname* for the dropdown suggestions list
+dropdown.*fuzzySearch* | Boolean | true | Enables filtering dropdown items values' by string *containing* and not only *beginning*
+dropdown.*sortby* | String/Function | | If set as `startsWith` string, the suggestions list will be sorted with matched items which starts with the query shown first, and *exact* matches shown before all. If this setting is defined as a `function`, it recieves two arguments: the array of filtered items and the query and it must return an Array. (*default sorting order is same as the whitelist's*)
+dropdown.*accentedSearch* | Boolean | true | Enable searching for accented items in the whitelist without typing exact match (#491)
+dropdown.*position* | String | `"all"` | `manual` - will not render the dropdown, and you would need to do it yourself. [See demo](https://yaireo.github.io/tagify/#section-manual-suggestions) `text` - places the dropdown next to the caret `input` - places the dropdown next to the input (useful in rare situations) `all` - normal, full-width design
+dropdown.*highlightFirst* | Boolean | false | When a suggestions list is shown, highlight the first item, and also suggest it in the input (The suggestion can be accepted with → key)
+dropdown.*closeOnSelect* | Boolean | true | close the dropdown after selecting an item, if `enabled:0` is set (which means always show dropdown on focus)
+dropdown.*clearOnSelect* | Boolean | true | Keep typed text after selecting a suggestion
+dropdown.*mapValueTo* | Function/String | | If whitelist is an Array of Objects: Ex. `[{value:'foo', email:'foo@a.com'},...]`) this setting controlls which data key will be printed in the dropdown. Ex.1: `mapValueTo: data => "To:" + data.email` Ex.2: `mapValueTo: "email"`
+dropdown.*searchKeys* | Array | `["value", "searchBy"]` | 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* | HTMLNode | `document.body` | Target-Node which the *suggestions dropdown* is appended to (*only when rendered*)
+dropdown.*placeAbove* | Boolean | | If defined, will force the placement of the dropdown in respect to the Boolean value: `true` will always show the suggestions dropdown above the input field and `false` will always show it below. By default this setting it not defined and the placement of the dropdown is automatically decided according to the space availble, where opening it *below* the input is preferred.
diff --git a/node_modules/@yaireo/tagify/dist/jQuery.tagify.min.js b/node_modules/@yaireo/tagify/dist/jQuery.tagify.min.js
new file mode 100644
index 0000000..99b517d
--- /dev/null
+++ b/node_modules/@yaireo/tagify/dist/jQuery.tagify.min.js
@@ -0,0 +1,32 @@
+/**
+ * Tagify (v 4.8.1) - tags input component
+ * By Yair Even-Or
+ * Don't sell this code. (c)
+ * https://github.com/yairEO/tagify
+ */
+
+;(function($){
+ // just a jQuery wrapper for the vanilla version of this component
+ $.fn.tagify = function(settings = {}){
+ return this.each(function() {
+ var $input = $(this),
+ tagify;
+
+ if( $input.data("tagify") ) // don't continue if already "tagified"
+ return this;
+
+ settings.isJQueryPlugin = true;
+ tagify = new Tagify($input[0], settings);
+ $input.data("tagify", tagify);
+ });
+ }
+
+/**
+ * Tagify (v 4.8.1) - tags input component
+ * By Yair Even-Or
+ * Don't sell this code. (c)
+ * https://github.com/yairEO/tagify
+ */
+
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Tagify=e()}(this,(function(){"use strict";function t(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function e(e){for(var s=1;s(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase());function a(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function n(t){var e=document.createElement("div");return t.replace(/\?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function o(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function r(t){return"string"==typeof t?t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/`|'/g,"'"):t}function l(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function d(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(l(e[i])){l(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(Array.isArray(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function h(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var g=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);function p(t){return t&&t.classList&&t.classList.contains(this.settings.classNames.tag)}var c={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,userInput:!0,keepInvalidTags:!1,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,pasteAsTags:!0,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tag:"tagify__tag",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",inputInvalid:"tagify__input--invalid",tagX:"tagify__tag__removeBtn",tagText:"tagify__tag-text",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownInital:"tagify__dropdown--initial",scopeLoading:"tagify--loading",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve()}};function u(){this.dropdown={};for(let t in this._dropdown)this.dropdown[t]="function"==typeof this._dropdown[t]?this._dropdown[t].bind(this):this._dropdown[t];this.settings.dropdown.enabled>=0&&this.dropdown.init()}var m,v={init(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector(this.settings.classNames.dropdownWrapperSelector)},show(t){var e,i,a,n=this.settings,o="mix"==n.mode&&!n.enforceWhitelist,r=!n.whitelist||!n.whitelist.length,d="manual"==n.dropdown.position;if(t=void 0===t?this.state.inputText:t,(!r||o||n.templates.dropdownItemNoMatch)&&!1!==n.dropdown.enable&&!this.state.isLoading){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems(t),t&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",t),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:t}))),!a){if(this.suggestedListItems.length)t&&o&&!this.state.editing.scope&&!s(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!o||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide();this.suggestedListItems=[{value:t}]}i=""+(l(e=this.suggestedListItems[0])?e.value:e),n.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill(a),n.dropdown.highlightFirst&&this.dropdown.highlightOption(this.DOM.dropdown.content.children[0]),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.setStateSelection(),d||setTimeout((()=>{this.dropdown.position(),this.dropdown.render()})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},toggle(t){this.dropdown[this.state.dropdown.visible&&!t?"hide":"show"]()},render(){var t,e,i,s=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),a=this.settings;return this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(a.classNames.dropdownInital),this.dropdown.position(s),a.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(a.classNames.dropdownInital)))),this},fill(t){var e;t="string"==typeof t?t:this.dropdown.createListHTML(t||this.suggestedListItems),this.DOM.dropdown.content.innerHTML=(e=t)?e.replace(/\>[\r\n ]+\<").replace(/(<.*?>)|\s+/g,((t,e)=>e||" ")):""},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems(t),this.dropdown.fill(),this.suggestedListItems.length||this.dropdown.hide(),this.trigger("dropdown:updated",this.DOM.dropdown)},position(t){var e=this.settings.dropdown;if("manual"!=e.position){var i,s,a,n,o,r,l=this.DOM.dropdown,d=e.placeAbove,h=document.documentElement.clientHeight,g=Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)>480?e.position:"all",p=this.DOM["input"==g?"input":"scope"];t=t||l.clientHeight,this.state.dropdown.visible&&("text"==g?(a=(i=this.getCaretGlobalPosition()).bottom,s=i.top,n=i.left,o="auto"):(r=function(t){for(var e=0,i=0;t;)e+=t.offsetLeft||0,i+=t.offsetTop||0,t=t.parentNode;return{left:e,top:i}}(this.settings.dropdown.appendTarget),s=(i=p.getBoundingClientRect()).top-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+"px"),s=Math.floor(s),a=Math.ceil(a),d=void 0===d?h-i.bottom{if(e)return this.dropdown.selectOption(e);this.dropdown.hide(),"mix"!=this.settings.mode&&this.addTags(this.state.inputText.trim(),!0)})).catch((t=>t));break;case"Backspace":{if("mix"==this.settings.mode||this.state.editing.scope)return;const t=this.input.raw.call(this);""!=t&&8203!=t.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);e&&this.dropdown.highlightOption(e)},onMouseLeave(t){this.dropdown.highlightOption()},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode(e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption(e):this.dropdown.hide()})).catch((t=>t))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t?+t.getAttribute("tagifySuggestionIdx"):-1;return this.suggestedListItems[e]||null},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.suggestedListItems[this.getNodeIndex(t)],this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position())},selectOption(t){var e,i=this.settings.dropdown,s=i.clearOnSelect,a=i.closeOnSelect;if(!t)return e=this.addTags(this.state.inputText,!0),void(a&&this.dropdown.hide());var n=t.getAttribute("tagifySuggestionIdx"),o=this.suggestedListItems[+n];this.trigger("dropdown:select",{data:o,elm:t}),n&&o?(this.state.editing?this.onEditTagDone(null,d({__isValid:!0},o)):e=this["mix"==this.settings.mode?"addMixTags":"addTags"]([o],s),this.DOM.input.parentNode&&(setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0),this.placeCaretAfterNode(e)})),a?setTimeout(this.dropdown.hide.bind(this)):this.dropdown.refilter())):this.dropdown.hide()},selectAll(){return this.suggestedListItems.length=0,this.dropdown.hide(),this.addTags(this.dropdown.filterListItems(""),!0),this},filterListItems(t,e){var i,s,a,n,o,r=this.settings,d=r.dropdown,g=(e=e||{},t="select"==r.mode&&this.value.length&&this.value[0][r.tagTextProp]==t?"":t,[]),p=[],c=r.whitelist,u=d.maxItems||1/0,m=d.searchKeys,v=0;if(!t||!m.length)return(r.duplicates?c:c.filter((t=>!this.isTagDuplicate(l(t)?t.value:t)))).slice(0,u);function f(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(o=d.caseSensitive?""+t:(""+t).toLowerCase();vm.includes(t)))?["value"]:m;d.fuzzySearch&&!e.exact?(a=T.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase().trim(),d.accentedSearch&&(a=h(a),o=h(o)),t=0==a.indexOf(o),u=a===o,s=f(a,o)):(t=!0,s=T.some((t=>{var s=""+(i[t]||"");return d.accentedSearch&&(s=h(s),o=h(o)),d.caseSensitive||(s=s.toLowerCase()),u=s===o,e.exact?s===o:0==s.indexOf(o)}))),n=!r.duplicates&&this.isTagDuplicate(l(i)?i.value:i),s&&!n&&(u&&t?p.push(i):"startsWith"==d.sortby&&t?g.unshift(i):g.push(i))}return"function"==typeof d.sortby?d.sortby(p.concat(g),o):p.concat(g).slice(0,u)},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return d([],t).map(((t,e)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var i=this.dropdown.getMappedValue(t);t.value=i&&"string"==typeof i?r(i):i;var s=this.settings.templates.dropdownItem.apply(this,[t,this]);return s=s.replace(/\s*tagifySuggestionIdx=(["'])(.*?)\1/gim,"").replace(">",` tagifySuggestionIdx="${e}">`)})).join("")}},f={empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},T={wrapper:(t,e)=>`\n \n `,tag(t,e){var i=this.settings;return`\n \n \n ${t[i.tagTextProp]||t.value} \n
\n `},dropdown(t){var e=t.dropdown,i="manual"==e.position,s=`${t.classNames.dropdown}`;return``},dropdownItem(t,e){return`${t.value}
`},dropdownItemNoMatch:null};var w={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(t=!0){var e,i=this.events.callbacks,s=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t){for(var a in this.state.mainEvents=t,t&&!this.listeners.main&&(this.events.bindGlobal.call(this),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)],dblclick:["scope",i.onDoubleClickScope.bind(this)],paste:["input",i.onPaste.bind(this)],drop:["input",i.onDrop.bind(this)]})this.DOM[e[a][0]][s](a,e[a][1]);clearInterval(this.listeners.main.originalInputValueObserverInterval),this.listeners.main.originalInputValueObserverInterval=setInterval(i.observeOriginalInputValue.bind(this),500);var n=this.listeners.main.inputMutationObserver||new MutationObserver(i.onInputDOMChange.bind(this));n&&n.disconnect(),"mix"==this.settings.mode&&n.observe(this.DOM.input,{childList:!0})}},bindGlobal(t){var e,i=this.events.callbacks,s=t?"removeEventListener":"addEventListener";if(t||!this.listeners.global)for(e of(this.listeners.global=this.listeners&&this.listeners.global||[{type:this.isIE?"keydown":"input",target:this.DOM.input,cb:i[this.isIE?"onInputIE":"onInput"].bind(this)},{type:"keydown",target:window,cb:i.onWindowKeyDown.bind(this)},{type:"blur",target:this.DOM.input,cb:i.onFocusBlur.bind(this)}],this.listeners.global))e.target[s](e.type,e.cb)},unbindGlobal(){this.events.bindGlobal.call(this,!0)},callbacks:{onFocusBlur(t){var e=t.target?this.trim(t.target.textContent):"",i=this.settings,s=t.type,a=i.dropdown.enabled>=0,n={relatedTarget:t.relatedTarget},o=this.state.actions.selectOption&&(a||!i.dropdown.closeOnSelect),r=this.state.actions.addNew&&a,l=t.relatedTarget&&p.call(this,t.relatedTarget)&&this.DOM.scope.contains(t.relatedTarget);if("blur"==s){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide(),void this.DOM.input.focus();this.postUpdate(),this.triggerChangeEvent()}if(!o&&!r)if(this.state.hasFocus="focus"==s&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=i.mode){if("focus"==s)return this.trigger("focus",n),void(0!==i.dropdown.enabled&&i.userInput||this.dropdown.show(this.value.length?"":void 0));"blur"==s&&(this.trigger("blur",n),this.loading(!1),"select"==this.settings.mode&&l&&(e=""),("select"==this.settings.mode&&e?!this.value.length||this.value[0].value!=e:e&&!this.state.actions.selectOption&&i.addTagOnBlur)&&this.addTags(e,!0),"select"!=this.settings.mode||e||this.removeTags()),this.DOM.input.removeAttribute("style"),this.dropdown.hide()}else"focus"==s?this.trigger("focus",n):"blur"==t.type&&(this.trigger("blur",n),this.loading(!1),this.dropdown.hide(),this.state.dropdown.visible=void 0,this.setStateSelection())},onWindowKeyDown(t){var e,i=document.activeElement;if(p.call(this,i)&&this.DOM.scope.contains(document.activeElement))switch(e=i.nextElementSibling,t.key){case"Backspace":this.removeTags(i),(e||this.DOM.input).focus();break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.settings;"select"==e.mode&&e.enforceWhitelist&&this.value.length&&"Tab"!=t.key&&t.preventDefault();var i=this.trim(t.target.textContent);if(this.trigger("keydown",{originalEvent:this.cloneEvent(t)}),"mix"==e.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var s,a,r,l=document.getSelection(),d="Delete"==t.key&&l.anchorOffset==(l.anchorNode.length||0),h=1==l.anchorNode.nodeType||!l.anchorOffset&&1==l.anchorNode.previousSibling.nodeType&&l.anchorNode.previousSibling,c=n(this.DOM.input.innerHTML),u=this.getTagElms();if("edit"==e.backspace&&h)return s=1==l.anchorNode.nodeType?null:l.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,s),void t.preventDefault();if(g()&&h)return r=o(h),h.hasAttribute("readonly")||h.remove(),this.DOM.input.focus(),void setTimeout((()=>{this.placeCaretAfterNode(r),this.DOM.input.click()}));if("BR"==l.anchorNode.nodeName)return;if((d||h)&&1==l.anchorNode.nodeType?a=0==l.anchorOffset?d?u[0]:null:u[l.anchorOffset-1]:d?a=l.anchorNode.nextElementSibling:h&&(a=h),3==l.anchorNode.nodeType&&!l.anchorNode.nodeValue&&l.anchorNode.previousElementSibling&&t.preventDefault(),(h||d)&&!e.backspace)return void t.preventDefault();if("Range"!=l.type&&!l.anchorOffset&&l.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=l.type&&a&&a.hasAttribute("readonly"))return void this.placeCaretAfterNode(o(a));clearTimeout(m),m=setTimeout((()=>{var t=document.getSelection(),e=n(this.DOM.input.innerHTML),i=!d&&t.anchorNode.previousSibling;if(e.length>=c.length&&i)if(p.call(this,i)&&!i.hasAttribute("readonly")){if(this.removeTags(i),this.fixFirefoxLastTagNoCaret(),2==this.DOM.input.children.length&&"BR"==this.DOM.input.children[1].tagName)return this.DOM.input.innerHTML="",this.value.length=0,!0}else i.remove();this.value=[].map.call(u,((t,e)=>{var i=this.tagData(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}switch(t.key){case"Backspace":"select"==e.mode&&e.enforceWhitelist&&this.value.length?this.removeTags():this.state.dropdown.visible&&"manual"!=e.dropdown.position||""!=i&&8203!=i.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show();break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&e.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let s="select"==e.mode;if(!i||s)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible||229==t.keyCode)return;t.preventDefault(),setTimeout((()=>{this.state.actions.selectOption||this.addTags(i,!0)}))}},onInput(t){if("mix"==this.settings.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled,s={value:e,inputElm:this.DOM.input};s.isValid=this.validateTag({value:e}),this.state.inputText!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e)&&this.input.set.call(this):this.settings.dropdown.enabled>=0&&this.dropdown[i?"show":"hide"](e),this.trigger("input",s))},onMixTagsInput(t){var e,i,s,a,n,o,r,l,h=this.settings,p=this.value.length,c=this.getTagElms(),u=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(c,(t=>this.tagData(t).value));if("deleteContentBackward"==t.inputType&&g()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&u.appendChild(this.createTagElem(t))})),u.childNodes.length&&(m.insertNode(u),this.setRangeAtStartEnd(!1,u.lastChild)),c.length!=p)return this.value=[].map.call(this.getTagElms(),(t=>this.tagData(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(h.pattern).length-1,(i=e.match(h.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(h.pattern)[0],value:a.replace(h.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,l=this.state.tag.value.match(h.delimiters))return this.state.tag.value=this.state.tag.value.replace(h.delimiters,""),this.state.tag.delimiters=l[0],this.addTags(this.state.tag.value,h.dropdown.clearOnSelect),void this.dropdown.hide();n=this.state.tag.value.length>=h.dropdown.enabled;try{r=(r=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&r.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(r||s{this.update({withoutChangeEvent:!0}),this.trigger("input",d({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"](this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},observeOriginalInputValue(){this.DOM.originalInput.value!=this.DOM.originalInput.tagifyValue&&this.loadOriginalValues()},onClickScope(t){var e=this.settings,i=t.target.closest("."+e.classNames.tag),s=+new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains(e.classNames.tagX))return i?(this.trigger("click",{tag:i,index:this.getNodeIndex(i),data:this.tagData(i),originalEvent:this.cloneEvent(t)}),void(1!==e.editTags&&1!==e.editTags.clicks||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==e.mode&&this.fixFirefoxLastTagNoCaret(),s>500)?this.state.dropdown.visible?this.dropdown.hide():0===e.dropdown.enabled&&"mix"!=e.mode&&this.dropdown.show(this.value.length?"":void 0):"select"==e.mode&&!this.state.dropdown.visible&&this.dropdown.show());this.removeTags(t.target.parentNode)}else this.state.hasFocus||this.DOM.input.focus()},onPaste(t){t.preventDefault();var e,i,s=this.settings;if("select"==s.mode&&s.enforceWhitelist||!s.userInput)return!1;s.readonly||(e=t.clipboardData||window.clipboardData,i=e.getData("Text"),s.hooks.beforePaste(t,{tagify:this,pastedText:i,clipboardData:e}).then((e=>{void 0===e&&(e=i),e&&(this.injectAtCaret(e,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.settings.pasteAsTags?this.addTags(this.state.inputText+e,!0):this.state.inputText=e)})).catch((t=>t)))},onDrop(t){t.preventDefault()},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=this.tagData(i),n=this.input.normalize.call(this,t),o=i.innerHTML!=i.__tagifyTagData.__originalHTML,r=this.validateTag({[this.settings.tagTextProp]:n});o||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show(n)),this.trigger("edit:input",{tag:i,index:s,data:d({},this.value[s],{newValue:n}),originalEvent:this.cloneEvent(e)})},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e,i,s=this.settings,a=t.closest("."+s.classNames.tag),n=this.input.normalize.call(this,t),o=this.tagData(a).__originalData,r=a.innerHTML!=a.__tagifyTagData.__originalHTML,l=this.validateTag({[s.tagTextProp]:n});if(n)if(r){if(e=this.hasMaxTags(),i=this.getWhitelistItem(n)||d({},o,{[s.tagTextProp]:n,value:n,__isValid:l}),s.transformTag.call(this,i,o),!0!==(l=!e&&this.validateTag({[s.tagTextProp]:i[s.tagTextProp]}))){if(this.trigger("invalid",{data:i,tag:a,message:l}),s.editTags.keepInvalid)return;s.keepInvalidTags?i.__isValid=l:i=o}else s.keepInvalidTags&&(delete i.title,delete i["aria-invalid"],delete i.class);this.onEditTagDone(a,i)}else this.onEditTagDone(a,o);else this.onEditTagDone(a)}},onEditTagkeydown(t,e){switch(this.trigger("edit:keydown",{originalEvent:this.cloneEvent(t)}),t.key){case"Esc":case"Escape":e.innerHTML=e.__tagifyTagData.__originalHTML;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=this.settings;s&&a.userInput&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),"select"==a.mode||a.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:this.tagData(s)}))},onInputDOMChange(t){t.forEach((t=>{t.addedNodes.forEach((t=>{if(t)if("
"==t.outerHTML)t.replaceWith(document.createElement("br"));else if(1==t.nodeType&&t.querySelector(this.settings.classNames.tagSelector)){let e=document.createTextNode("");3==t.childNodes[0].nodeType&&"BR"!=t.previousSibling.nodeName&&(e=document.createTextNode("\n")),t.replaceWith(e,...[...t.childNodes].slice(0,-1)),this.placeCaretAfterNode(e.previousSibling)}else p.call(this,t)&&t.previousSibling&&"BR"==t.previousSibling.nodeName&&(t.previousSibling.replaceWith("\n"),this.placeCaretAfterNode(t.previousSibling.previousSibling))})),t.removedNodes.forEach((t=>{t&&"BR"==t.nodeName&&p.call(this,e)&&(this.removeTags(e),this.fixFirefoxLastTagNoCaret())}))}));var e=this.DOM.input.lastChild;e&&""==e.nodeValue&&e.remove(),e&&"BR"==e.nodeName||this.DOM.input.appendChild(document.createElement("br"))}}};function b(t,e){return t?t.previousElementSibling&&t.previousElementSibling.classList.contains("tagify")?(console.warn("Tagify: ","input element is already Tagified",t),this):(d(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?d({},o):o).tagify=this,s instanceof Object)for(var r in s)s[r]instanceof HTMLElement&&(o[r]=s[r]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox="undefined"!=typeof InstallTrigger,this.isIE=window.document.documentMode,this.applySettings(t,e||{}),this.state={inputText:"",editing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),u.call(this),this.getCSSVars(),setTimeout((()=>this.loadOriginalValues())),this.events.customBinding.call(this),this.events.binding.call(this),void(t.autofocus&&this.DOM.input.focus())):(console.warn("Tagify: ","input element not found",t),this)}return b.prototype={_dropdown:v,customEventsList:["change","add","remove","invalid","input","click","keydown","focus","blur","edit:input","edit:beforeUpdate","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch","dropdown:scroll"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},templates:T,parseTemplate(t,e){return t=this.settings.templates[t]||t,this.parseHTML(t.apply(this,e))},set whitelist(t){this.settings.whitelist=t&&Array.isArray(t)?t:[]},get whitelist(){return this.settings.whitelist},applySettings(t,i){c.templates=this.templates;var s=this.settings=d({},c,i);s.disabled=t.hasAttribute("disabled"),s.readonly=t.hasAttribute("readonly"),s.placeholder=t.getAttribute("placeholder")||s.placeholder||"",s.required=t.hasAttribute("required");for(let t in s.classNames)Object.defineProperty(s.classNames,t+"Selector",{get(){return"."+this[t].split(" ")[0]}});if(this.isIE&&(s.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var i=t.getAttribute("data-"+e);i&&(i=i.split(s.delimiters))instanceof Array&&(s[e]=i)})),"autoComplete"in i&&!l(i.autoComplete)&&(s.autoComplete=c.autoComplete,s.autoComplete.enabled=i.autoComplete),"mix"==s.mode&&(s.autoComplete.rightKey=!0,s.delimiters=i.delimiters||null,s.tagTextProp&&!s.dropdown.searchKeys.includes(s.tagTextProp)&&s.dropdown.searchKeys.push(s.tagTextProp)),t.pattern)try{s.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings.delimiters)try{s.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}this.TEXTS=e(e({},f),s.texts||{}),"select"!=s.mode&&s.userInput||(s.dropdown.enabled=0),s.dropdown.appendTarget=i.dropdown&&i.dropdown.appendTarget?i.dropdown.appendTarget:document.body},getAttributes(t){var e,i=this.getCustomAttributes(t),s="";for(e in i)s+=" "+e+(void 0!==t[e]?`="${i[e]}"`:"");return s},getCustomAttributes(t){if(!l(t))return"";var e,i={};for(e in t)"__"!=e.slice(0,2)&&"class"!=e&&t.hasOwnProperty(e)&&void 0!==t[e]&&(i[e]=r(t[e]));return i},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCaretGlobalPosition(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);var e;this.CSSVars={tagHideTransition:(({value:t,unit:e})=>"s"==e?1e3*t:t)(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}((e="tag-hide-transition",t.getPropertyValue("--"+e))))}},build(t){var e=this.DOM;this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t))},destroy(){this.events.unbindGlobal.call(this),this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.dropdown.hide(!0),clearTimeout(this.dropdownHide__bindEventsTimeout)},loadOriginalValues(t){var e,i=this.settings;if(void 0===t&&(t=i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value),this.removeAllTags({withoutChangeEvent:!0}),t)if("mix"==i.mode)this.parseMixTags(t.trim()),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend"," ");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value,this.state.loadedOriginalValues=!0},cloneEvent(t){var e={};for(var i in t)e[i]=t[i];return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:w,fixFirefoxLastTagNoCaret(){},placeCaretAfterNode(t){if(t&&t.parentNode){var e=t.nextSibling,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e||t),s.collapse(!0),i.removeAllRanges(),i.addRange(s))}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide();var i=this.settings;function s(){return t.querySelector(i.classNames.tagTextSelector)}var a=s(),n=this.getNodeIndex(t),o=this.tagData(t),r=this.events.callbacks,l=this,h=!0;if(a){if(!(o instanceof Object&&"editable"in o)||o.editable)return a.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),this.tagData(t,{__originalData:d({},o),__originalHTML:t.innerHTML}),a.addEventListener("focus",r.onEditTagFocus.bind(this,t)),a.addEventListener("blur",(function(){setTimeout((()=>r.onEditTagBlur.call(l,s())))})),a.addEventListener("input",r.onEditTagInput.bind(this,a)),a.addEventListener("keydown",(e=>r.onEditTagkeydown.call(this,e,t))),a.focus(),this.setRangeAtStartEnd(!1,a),e.skipValidation||(h=this.editTagToggleValidity(t)),a.originalIsValid=h,this.trigger("edit:start",{tag:t,index:n,data:o,isValid:h}),this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i;if(e=e||this.tagData(t))return(i=!("__isValid"in e)||!0===e.__isValid)||this.removeTagsFromValue(t),this.update(),t.classList.toggle(this.settings.classNames.tagNotAllowed,!i),e.__isValid;console.warn("tag has no data: ",t,e)},onEditTagDone(t,e){e=e||{};var i={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:this.tagData(t),data:e};this.trigger("edit:beforeUpdate",i,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&e[this.settings.tagTextProp]?(t=this.replaceTag(t,e),this.editTagToggleValidity(t,e),this.settings.a11y.focusableTags&&t.focus()):t&&this.removeTags(t),this.trigger("edit:updated",i),this.dropdown.hide(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&d(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(this.tagData(t))})),this.update()},setRangeAtStartEnd(t,e){t="number"==typeof t?t:!!t,e=(e=e||this.DOM.input).lastChild||e;var i=document.getSelection();try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){}},injectAtCaret(t,e){if(e=e||this.state.selection.range)return"string"==typeof t&&(t=document.createTextNode(t)),e.deleteContents(),e.insertNode(t),this.setRangeAtStartEnd(!1,t),this.updateValueByDOMTags(),this.update(),this},input:{set(t="",e=!0){var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=r(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},raw(){return this.DOM.input.textContent},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach((t=>3==t.nodeType&&i.push(t.nodeValue))),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," "),this.settings.trim&&(i=i.replace(/^\s+/,"")),i},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{})&&(t={value:t});var e=t.value?""+t.value:"",i=e.substr(0,this.state.inputText.length).toLowerCase(),s=e.substring(this.state.inputText.length);e&&this.state.inputText&&i==this.state.inputText.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd()),this.input.autocomplete.suggest.call(this),this.dropdown.hide(),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(...t){var e="."+[...this.settings.classNames.tag.split(" "),...t].join(".");return[].slice.call(this.DOM.scope.querySelectorAll(e))},getLastTag(){var t=this.DOM.scope.querySelectorAll(`${this.settings.classNames.tagSelector}:not(.${this.settings.classNames.tagHide}):not([readonly])`);return t[t.length-1]},tagData:(t,e,i)=>t?(e&&(t.__tagifyTagData=i?e:d({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag elment doesn't exist",t,e),e),isTagDuplicate(t,e){var i=this.settings;return"select"!=i.mode&&this.value.reduce(((a,n)=>s(this.trim(""+t),n.value,e||i.dropdown.caseSensitive)?a+1:a),0)},getTagIndexByValue(t){var e=[];return this.getTagElms().forEach(((i,a)=>{s(this.trim(i.textContent),t,this.settings.dropdown.caseSensitive)&&e.push(a)})),e},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(t,e,i){e=e||"value";var a,n=this.settings;return(i=i||n.whitelist).some((i=>{var o="string"==typeof i?i:i[e]||i.value;if(s(o,t,n.dropdown.caseSensitive,n.trim))return a="string"==typeof i?{value:i}:i,!0})),a||"value"!=e||"value"==n.tagTextProp||(a=this.getWhitelistItem(t,n.tagTextProp,i)),a},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,this.state.editing)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t,e){var i=this.settings;document.activeElement.blur(),i[e||"readonly"]=t,this.DOM.scope[(t?"set":"remove")+"Attribute"](e||"readonly",!0),"mix"==i.mode&&(this.DOM.input.contentEditable=!t)},setDisabled(t){this.setReadonly(t,"disabled")},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp;e.enforceWhitelist;var o=[],r=!!i&&i[0]instanceof Object,l=t instanceof Array,d=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=d(t)}else l&&(t=[].concat(...t.map((t=>t.value?t:d(t)))));return r&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0});this.settings.duplicates||(i=i.filter((t=>!e.includes(t.value))));var s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),o.length&&(t=o)),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];return t=t.split(i[0]).map(((t,e)=>{var d,h,g,p=t.split(i[1]),c=p[0],u=l.length==o;try{if(c==+c)throw Error;h=JSON.parse(c)}catch(t){h=this.normalizeTags(c)[0]||{value:c}}if(u||!(p.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else a.call(this,h),h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),p[0]=g.outerHTML,this.value.push(h);return p.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize(),this.getTagElms().forEach(((t,e)=>this.tagData(t,l[e]))),this.update({withoutChangeEvent:!0}),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),i=n.nodeValue.lastIndexOf(e),s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0}},selectTag(t,e){var i=this.settings;if(!i.enforceWhitelist||this.isTagWhitelisted(e.value)){this.input.set.call(this,e[i.tagTextProp]||e.value,!0),this.state.actions.selectOption&&setTimeout(this.setRangeAtStartEnd.bind(this));var s=this.getLastTag();return s?this.replaceTag(s,e):this.appendTag(t),i.enforceWhitelist&&this.DOM.input.removeAttribute("contenteditable"),this.value[0]=e,this.trigger("add",{tag:t,data:e}),this.update(),[t]}},addEmptyTag(t){var e=d({value:""},t||{}),i=this.createTagElem(e);this.tagData(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0})},addTags(t,e,i){var s=[],a=this.settings,n=document.createDocumentFragment();return i=i||a.skipInvalid,t&&0!=t.length?(t=this.normalizeTags(t),"mix"==a.mode?this.addMixTags(t):("select"==a.mode&&(e=!1),this.DOM.input.removeAttribute("style"),t.forEach((t=>{var e,o={},r=Object.assign({},t,{value:t.value+""});if((t=Object.assign({},r)).__isValid=this.hasMaxTags()||this.validateTag(t),a.transformTag.call(this,t),!0!==t.__isValid){if(i)return;d(o,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:r}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value))}if(t.readonly&&(o["aria-readonly"]=!0),e=this.createTagElem(t,o),s.push(e),"select"==a.mode)return this.selectTag(e,t);n.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.trigger("add",{tag:e,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position()})),this.appendTag(n),this.update(),t.length&&e&&this.input.set.call(this),this.dropdown.refilter(),s)):("select"==a.mode&&this.removeAllTags(),s)},addMixTags(t){if((t=this.normalizeTags(t))[0].prefix||this.state.tag)return this.prefixedTextToTag(t[0]);"string"==typeof t&&(t=[{value:t}]);var e=!!this.state.selection,i=document.createDocumentFragment();return t.forEach((t=>{var e=this.createTagElem(t);i.appendChild(e),this.insertAfterTag(e)})),e?this.injectAtCaret(i):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(i),this.updateValueByDOMTags(),this.update()),i},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;this.placeCaretAfterNode(a)}return this.state.tag=null,this.trigger("add",d({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.scope.lastElementChild;i===e.input?e.scope.insertBefore(t,i):e.scope.appendChild(t)},createTagElem(t,i){t.__tagId=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)));var s,a=d({},t,e({value:r(t.value+"")},i));return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(s=this.parseTemplate("tag",[a])),this.tagData(s,t),s},reCheckInvalidTags(){var t=this.settings;this.getTagElms(t.classNames.tagNotAllowed).forEach(((t,e)=>{var i=this.tagData(t),s=this.hasMaxTags(),a=this.validateTag(i);if(!0===a&&!s)return i=i.__preInvalidData?i.__preInvalidData:{value:i.value},this.replaceTag(t,i);t.title=s||a}))},removeTags(t,e,i){var s;t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>(e&&"string"==typeof e&&(e=this.getTagElmByValue(e)),e&&this.tagData(e)&&t.push({node:e,idx:this.getTagIdx(this.tagData(e)),data:this.tagData(e,{__removed:!0})}),t)),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==this.settings.mode&&(i=0,this.input.set.call(this)),1==s.length&&s[0].node.classList.contains(this.settings.classNames.tagNotAllowed)&&(e=!0),s.length&&this.settings.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?this.settings.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter(),this.dropdown.position(),this.DOM.input.normalize(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(this.settings.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(this.removeTagsFromValue(s.map((t=>t.node))),this.update(),"select"==this.settings.mode&&this.DOM.input.setAttribute("contenteditable",!0))})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeTagsFromValue(t){(t=Array.isArray(t)?t:[t]).forEach((t=>{var e=this.tagData(t),i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)}))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.position(),"select"==this.settings.mode&&this.input.set.call(this),this.update(t)},postUpdate(){var t=this.settings.classNames,e="mix"==this.settings.mode?this.settings.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value:this.value.length;this.toggleClass(t.hasMaxTags,this.value.length>=this.settings.maxTags),this.toggleClass(t.hasNoTags,!this.value.length),this.toggleClass(t.empty,!e)},update(t){var e=this.DOM.originalInput,i=this.getInputValue();this.settings.mixMode.integrated||(e.value=i,e.tagifyValue=e.value),this.postUpdate(),!(t||{}).withoutChangeEvent&&this.state.loadedOriginalValues&&this.triggerChangeEvent()},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return e=t||this.value,i=this.dataProps,e&&Array.isArray(e)&&e.map((t=>a(t,i)));var e,i},getMixedTagsAsString(){var t="",e=this,i=this.settings.mixTagsInterpolator;return function s(n){n.childNodes.forEach((n=>{if(1==n.nodeType){const o=e.tagData(n);if("BR"==n.tagName&&(t+="\r\n"),"DIV"==n.tagName||"P"==n.tagName)t+="\r\n",s(n);else if(p.call(e,n)&&o){if(o.__removed)return;t+=i[0]+JSON.stringify(a(o,e.dataProps))+i[1]}}else t+=n.textContent}))}(this.DOM.input),t}},b.prototype.removeTag=b.prototype.removeTags,b}));
+ })(jQuery);
\ No newline at end of file
diff --git a/node_modules/@yaireo/tagify/dist/react.tagify.js b/node_modules/@yaireo/tagify/dist/react.tagify.js
new file mode 100644
index 0000000..7c63ccc
--- /dev/null
+++ b/node_modules/@yaireo/tagify/dist/react.tagify.js
@@ -0,0 +1,8 @@
+/**
+ * Tagify (v 4.8.1) - tags input component
+ * By Yair Even-Or
+ * Don't sell this code. (c)
+ * https://github.com/yairEO/tagify
+ */
+
+!function(e,n){"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?module.exports=n():e.React.tagify=n()}(this,(function(){"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=exports.MixedTags=void 0;var e,n=function(e,n){if(!n&&e&&e.__esModule)return e;if(null===e||"object"!=typeof e&&"function"!=typeof e)return{default:e};var t=a(n);if(t&&t.has(e))return t.get(e);var o={},r=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var c=r?Object.getOwnPropertyDescriptor(e,u):null;c&&(c.get||c.set)?Object.defineProperty(o,u,c):o[u]=e[u]}o.default=e,t&&t.set(e,o);return o}(require("react")),t=require("react-dom/server"),o=require("prop-types"),r=(e=require("./tagify.min.js"))&&e.__esModule?e:{default:e};const u=["children"];function a(e){if("function"!=typeof WeakMap)return null;var n=new WeakMap,t=new WeakMap;return(a=function(e){return e?t:n})(e)}function c(){return(c=Object.assign||function(e){for(var n=1;n=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(e);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(r[t]=e[t])}return r}const i=e=>e;const l=({name:e,value:o,loading:u=!1,onInput:a=i,onAdd:c=i,onRemove:d=i,onEditInput:l=i,onEditBeforeUpdate:s=i,onEditUpdated:f=i,onEditStart:p=i,onEditKeydown:y=i,onInvalid:g=i,onClick:w=i,onKeydown:h=i,onFocus:O=i,onBlur:b=i,onChange:m=i,onDropdownShow:v=i,onDropdownHide:E=i,onDropdownSelect:j=i,onDropdownScroll:D=i,onDropdownNoMatch:M=i,onDropdownUpdated:x=i,readOnly:S,disabled:k,children:I,settings:R={},InputMode:N="input",autoFocus:P,className:T,whitelist:C,tagifyRef:U,placeholder:F="",defaultValue:_,showDropdown:V})=>{const q=(0,n.useRef)(),B=(0,n.useRef)(),K=(0,n.useRef)(),W=_||o,A=(0,n.useMemo)((()=>({ref:B,name:e,defaultValue:I||"string"==typeof W?W:JSON.stringify(W),className:T,readOnly:S,disabled:k,autoFocus:P,placeholder:F})),[]),H=(0,n.useCallback)((()=>{P&&K.current&&K.current.DOM.input.focus()}),[K]);return(0,n.useEffect)((()=>{!function(e){if(e)for(let o in e){let r=e[o];String(r).includes("jsxRuntime")&&(e[o]=(...e)=>(0,t.renderToStaticMarkup)(n.default.createElement(r,{props:e})))}}(R.templates),"textarea"==N&&(R.mode="mix"),C&&C.length&&(R.whitelist=C);const e=new r.default(B.current,R);return e.on("input",a).on("add",c).on("remove",d).on("invalid",g).on("keydown",h).on("focus",O).on("blur",b).on("click",w).on("change",m).on("edit:input",l).on("edit:beforeUpdate",s).on("edit:updated",f).on("edit:start",p).on("edit:keydown",y).on("dropdown:show",v).on("dropdown:hide",E).on("dropdown:select",j).on("dropdown:scroll",D).on("dropdown:noMatch",M).on("dropdown:updated",x),U&&(U.current=e),K.current=e,H(),()=>{e.destroy()}}),[]),(0,n.useEffect)((()=>{H()}),[P]),(0,n.useEffect)((()=>{q.current&&(K.current.settings.whitelist.length=0,C&&C.length&&K.current.settings.whitelist.push(...C))}),[C]),(0,n.useEffect)((()=>{const e=K.current.getInputValue();q.current&&!((e,n)=>{const t=e=>"string"==typeof e?e:JSON.stringify(e);return t(e)==t(n)})(o,e)&&K.current.loadOriginalValues(o)}),[o]),(0,n.useEffect)((()=>{q.current&&K.current.toggleClass(T)}),[T]),(0,n.useEffect)((()=>{q.current&&K.current.loading(u)}),[u]),(0,n.useEffect)((()=>{q.current&&K.current.setReadonly(S)}),[S]),(0,n.useEffect)((()=>{q.current&&K.current.setDisabled(k)}),[k]),(0,n.useEffect)((()=>{const e=K.current;q.current&&(V?(e.dropdown.show.call(e,V),e.toggleFocusClass(!0)):e.dropdown.hide.call(e))}),[V]),(0,n.useEffect)((()=>{q.current=!0}),[]),n.default.createElement("div",{className:"tags-input"},n.default.createElement(N,A))};l.propTypes={name:o.string,value:(0,o.oneOfType)([o.string,o.array]),loading:o.bool,children:(0,o.oneOfType)([o.string,o.array]),onChange:o.func,readOnly:o.bool,settings:o.object,InputMode:o.string,autoFocus:o.bool,className:o.string,tagifyRef:o.object,whitelist:o.array,placeholder:o.string,defaultValue:(0,o.oneOfType)([o.string,o.array]),showDropdown:(0,o.oneOfType)([o.string,o.bool]),onInput:o.func,onAdd:o.func,onRemove:o.func,onEditInput:o.func,onEditBeforeUpdate:o.func,onEditUpdated:o.func,onEditStart:o.func,onEditKeydown:o.func,onInvalid:o.func,onClick:o.func,onKeydown:o.func,onFocus:o.func,onBlur:o.func,onDropdownShow:o.func,onDropdownHide:o.func,onDropdownSelect:o.func,onDropdownScroll:o.func,onDropdownNoMatch:o.func,onDropdownUpdated:o.func};const s=n.default.memo(l);s.displayName="Tags";exports.MixedTags=e=>{let t=e.children,o=d(e,u);return n.default.createElement(s,c({InputMode:"textarea"},o),t)};var f=s;return exports.default=f,exports}));
\ No newline at end of file
diff --git a/node_modules/@yaireo/tagify/dist/tagify.css b/node_modules/@yaireo/tagify/dist/tagify.css
new file mode 100644
index 0000000..9fe784c
--- /dev/null
+++ b/node_modules/@yaireo/tagify/dist/tagify.css
@@ -0,0 +1 @@
+@charset "UTF-8";:root{--tagify-dd-color-primary:rgb(53,149,246);--tagify-dd-bg-color:white}.tagify{--tags-disabled-bg:#F1F1F1;--tags-border-color:#DDD;--tags-hover-border-color:#CCC;--tags-focus-border-color:#3595f6;--tag-bg:#E5E5E5;--tag-hover:#D3E2E2;--tag-text-color:black;--tag-text-color--edit:black;--tag-pad:0.3em 0.5em;--tag-inset-shadow-size:1.1em;--tag-invalid-color:#D39494;--tag-invalid-bg:rgba(211, 148, 148, 0.5);--tag-remove-bg:rgba(211, 148, 148, 0.3);--tag-remove-btn-color:black;--tag-remove-btn-bg:none;--tag-remove-btn-bg--hover:#c77777;--input-color:inherit;--tag--min-width:1ch;--tag--max-width:auto;--tag-hide-transition:0.3s;--placeholder-color:rgba(0, 0, 0, 0.4);--placeholder-color-focus:rgba(0, 0, 0, 0.25);--loader-size:.8em;display:flex;align-items:flex-start;flex-wrap:wrap;border:1px solid #ddd;border:1px solid var(--tags-border-color);padding:0;line-height:normal;cursor:text;outline:0;position:relative;box-sizing:border-box;transition:.1s}@keyframes tags--bump{30%{transform:scale(1.2)}}@keyframes rotateLoader{to{transform:rotate(1turn)}}.tagify:hover{border-color:#ccc;border-color:var(--tags-hover-border-color)}.tagify.tagify--focus{transition:0s;border-color:#3595f6;border-color:var(--tags-focus-border-color)}.tagify[disabled]{background:var(--tags-disabled-bg);filter:saturate(0);opacity:.5;pointer-events:none}.tagify[readonly].tagify--select{pointer-events:none}.tagify[readonly]:not(.tagify--mix):not(.tagify--select){cursor:default}.tagify[readonly]:not(.tagify--mix):not(.tagify--select)>.tagify__input{visibility:hidden;width:0;margin:5px 0}.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div{padding:.3em .5em;padding:var(--tag-pad)}.tagify[readonly]:not(.tagify--mix):not(.tagify--select) .tagify__tag>div::before{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)}.tagify[readonly] .tagify__tag__removeBtn{display:none}.tagify--loading .tagify__input>br:last-child{display:none}.tagify--loading .tagify__input::before{content:none}.tagify--loading .tagify__input::after{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;content:""!important;margin:-2px 0 -2px .5em}.tagify--loading .tagify__input:empty::after{margin-left:0}.tagify+input,.tagify+textarea{position:absolute!important;left:-9999em!important;transform:scale(0)!important}.tagify__tag{display:inline-flex;align-items:center;margin:5px 0 5px 5px;position:relative;z-index:1;outline:0;cursor:default;transition:.13s ease-out}.tagify__tag>div{vertical-align:top;box-sizing:border-box;max-width:100%;padding:.3em .5em;padding:var(--tag-pad,.3em .5em);color:#000;color:var(--tag-text-color,#000);line-height:inherit;border-radius:3px;white-space:nowrap;transition:.13s ease-out}.tagify__tag>div>*{white-space:pre-wrap;overflow:hidden;text-overflow:ellipsis;display:inline-block;vertical-align:top;min-width:1ch;max-width:auto;min-width:var(--tag--min-width,1ch);max-width:var(--tag--max-width,auto);transition:.8s ease,.1s color}.tagify__tag>div>[contenteditable]{outline:0;-webkit-user-select:text;user-select:text;cursor:text;margin:-2px;padding:2px;max-width:350px}.tagify__tag>div::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 1.1em #e5e5e5 inset;box-shadow:0 0 0 var(--tag-inset-shadow-size,1.1em) var(--tag-bg,#e5e5e5) inset}.tagify__tag:focus div::before,.tagify__tag:hover:not([readonly]) div::before{top:-2px;right:-2px;bottom:-2px;left:-2px;box-shadow:0 0 0 1.1em #d3e2e2 inset;box-shadow:0 0 0 var(--tag-inset-shadow-size,1.1em) var(--tag-hover,#d3e2e2) inset}.tagify__tag--loading{pointer-events:none}.tagify__tag--loading .tagify__tag__removeBtn{display:none}.tagify__tag--loading::after{--loader-size:.4em;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;margin:0 .5em 0 -.1em}.tagify__tag--flash div::before{animation:none}.tagify__tag--hide{width:0!important;padding-left:0;padding-right:0;margin-left:0;margin-right:0;opacity:0;transform:scale(0);transition:.3s;transition:var(--tag-hide-transition,.3s);pointer-events:none}.tagify__tag--hide>div>*{white-space:nowrap}.tagify__tag.tagify--noAnim>div::before{animation:none}.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div>span{opacity:.5}.tagify__tag.tagify--notAllowed:not(.tagify__tag--editable) div::before{box-shadow:0 0 0 1.1em rgba(211,148,148,.5) inset!important;box-shadow:0 0 0 var(--tag-inset-shadow-size,1.1em) var(--tag-invalid-bg,rgba(211,148,148,.5)) inset!important;transition:.2s}.tagify__tag[readonly] .tagify__tag__removeBtn{display:none}.tagify__tag[readonly]>div::before{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)}.tagify__tag--editable>div{color:#000;color:var(--tag-text-color--edit,#000)}.tagify__tag--editable>div::before{box-shadow:0 0 0 2px #d3e2e2 inset!important;box-shadow:0 0 0 2px var(--tag-hover,#d3e2e2) inset!important}.tagify__tag--editable>.tagify__tag__removeBtn{pointer-events:none}.tagify__tag--editable>.tagify__tag__removeBtn::after{opacity:0;transform:translateX(100%) translateX(5px)}.tagify__tag--editable.tagify--invalid>div::before{box-shadow:0 0 0 2px #d39494 inset!important;box-shadow:0 0 0 2px var(--tag-invalid-color,#d39494) inset!important}.tagify__tag__removeBtn{order:5;display:inline-flex;align-items:center;justify-content:center;border-radius:50px;cursor:pointer;font:14px/1 Arial;background:0 0;background:var(--tag-remove-btn-bg,none);color:#000;color:var(--tag-remove-btn-color,#000);width:14px;height:14px;margin-right:4.6666666667px;margin-left:auto;overflow:hidden;transition:.2s ease-out}.tagify__tag__removeBtn::after{content:"×";transition:.3s,color 0s}.tagify__tag__removeBtn:hover{color:#fff;background:#c77777;background:var(--tag-remove-btn-bg--hover,#c77777)}.tagify__tag__removeBtn:hover+div>span{opacity:.5}.tagify__tag__removeBtn:hover+div::before{box-shadow:0 0 0 1.1em rgba(211,148,148,.3) inset!important;box-shadow:0 0 0 var(--tag-inset-shadow-size,1.1em) var(--tag-remove-bg,rgba(211,148,148,.3)) inset!important;transition:box-shadow .2s}.tagify:not(.tagify--mix) .tagify__input br{display:none}.tagify:not(.tagify--mix) .tagify__input *{display:inline;white-space:nowrap}.tagify__input{flex-grow:1;display:inline-block;min-width:110px;margin:5px;padding:.3em .5em;padding:var(--tag-pad,.3em .5em);line-height:inherit;position:relative;white-space:pre-wrap;color:inherit;color:var(--input-color,inherit);box-sizing:inherit}.tagify__input:empty::before{transition:.2s ease-out;opacity:1;transform:none;display:inline-block;width:auto}.tagify--mix .tagify__input:empty::before{display:inline-block}.tagify__input:focus{outline:0}.tagify__input:focus::before{transition:.2s ease-out;opacity:0;transform:translatex(6px)}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){.tagify__input:focus::before{display:none}}@supports (-ms-ime-align:auto){.tagify__input:focus::before{display:none}}.tagify__input:focus:empty::before{transition:.2s ease-out;opacity:1;transform:none;color:rgba(0,0,0,.25);color:var(--placeholder-color-focus)}@-moz-document url-prefix(){.tagify__input:focus:empty::after{display:none}}.tagify__input::before{content:attr(data-placeholder);height:1em;line-height:1em;margin:auto 0;z-index:1;color:rgba(0,0,0,.4);color:var(--placeholder-color);white-space:nowrap;pointer-events:none;opacity:0;position:absolute}.tagify--mix .tagify__input::before{display:none;position:static;line-height:inherit}.tagify__input::after{content:attr(data-suggest);display:inline-block;white-space:pre;color:#000;opacity:.3;pointer-events:none;max-width:100px}.tagify__input .tagify__tag{margin:0 1px}.tagify__input .tagify__tag>div{padding-top:0;padding-bottom:0}.tagify--mix{display:block}.tagify--mix .tagify__input{padding:5px;margin:0;width:100%;height:100%;line-height:1.5;display:block}.tagify--mix .tagify__input::before{height:auto}.tagify--mix .tagify__input::after{content:none}.tagify--select::after{content:">";opacity:.5;position:absolute;top:50%;right:0;bottom:0;font:16px monospace;line-height:8px;height:8px;pointer-events:none;transform:translate(-150%,-50%) scaleX(1.2) rotate(90deg);transition:.2s ease-in-out}.tagify--select[aria-expanded=true]::after{transform:translate(-150%,-50%) rotate(270deg) scaleY(1.2)}.tagify--select .tagify__tag{position:absolute;top:0;right:1.8em;bottom:0}.tagify--select .tagify__tag div{display:none}.tagify--select .tagify__input{width:100%}.tagify--invalid{--tags-border-color:#D39494}.tagify__dropdown{position:absolute;z-index:9999;transform:translateY(1px);overflow:hidden}.tagify__dropdown[placement=top]{margin-top:0;transform:translateY(-100%)}.tagify__dropdown[placement=top] .tagify__dropdown__wrapper{border-top-width:1.1px;border-bottom-width:0}.tagify__dropdown[position=text]{box-shadow:0 0 0 3px rgba(var(--tagify-dd-color-primary),.1);font-size:.9em}.tagify__dropdown[position=text] .tagify__dropdown__wrapper{border-width:1px}.tagify__dropdown__wrapper{max-height:300px;overflow:auto;background:#fff;background:var(--tagify-dd-bg-color);border:1px solid #3595f6;border-color:var(--tagify-dd-color-primary);border-bottom-width:1.33px;border-top-width:0;box-shadow:0 2px 4px -2px rgba(0,0,0,.2);transition:.25s cubic-bezier(0,1,.5,1)}.tagify__dropdown--initial .tagify__dropdown__wrapper{max-height:20px;transform:translateY(-1em)}.tagify__dropdown--initial[placement=top] .tagify__dropdown__wrapper{transform:translateY(2em)}.tagify__dropdown__item{box-sizing:inherit;padding:.3em .5em;margin:1px;cursor:pointer;border-radius:2px;position:relative;outline:0}.tagify__dropdown__item--active{background:#3595f6;background:var(--tagify-dd-color-primary);color:#fff}.tagify__dropdown__item:active{filter:brightness(105%)}
\ No newline at end of file
diff --git a/node_modules/@yaireo/tagify/dist/tagify.min.js b/node_modules/@yaireo/tagify/dist/tagify.min.js
new file mode 100644
index 0000000..7ecb804
--- /dev/null
+++ b/node_modules/@yaireo/tagify/dist/tagify.min.js
@@ -0,0 +1,8 @@
+/**
+ * Tagify (v 4.8.1) - tags input component
+ * By Yair Even-Or
+ * Don't sell this code. (c)
+ * https://github.com/yairEO/tagify
+ */
+
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Tagify=e()}(this,(function(){"use strict";function t(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function e(e){for(var s=1;s(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase());function a(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function n(t){var e=document.createElement("div");return t.replace(/\?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function o(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function r(t){return"string"==typeof t?t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/`|'/g,"'"):t}function l(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function d(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(l(e[i])){l(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(Array.isArray(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function h(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var g=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);function p(t){return t&&t.classList&&t.classList.contains(this.settings.classNames.tag)}var c={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,userInput:!0,keepInvalidTags:!1,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,pasteAsTags:!0,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tag:"tagify__tag",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",inputInvalid:"tagify__input--invalid",tagX:"tagify__tag__removeBtn",tagText:"tagify__tag-text",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownInital:"tagify__dropdown--initial",scopeLoading:"tagify--loading",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve()}};function u(){this.dropdown={};for(let t in this._dropdown)this.dropdown[t]="function"==typeof this._dropdown[t]?this._dropdown[t].bind(this):this._dropdown[t];this.settings.dropdown.enabled>=0&&this.dropdown.init()}var m,v={init(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector(this.settings.classNames.dropdownWrapperSelector)},show(t){var e,i,a,n=this.settings,o="mix"==n.mode&&!n.enforceWhitelist,r=!n.whitelist||!n.whitelist.length,d="manual"==n.dropdown.position;if(t=void 0===t?this.state.inputText:t,(!r||o||n.templates.dropdownItemNoMatch)&&!1!==n.dropdown.enable&&!this.state.isLoading){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems(t),t&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",t),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:t}))),!a){if(this.suggestedListItems.length)t&&o&&!this.state.editing.scope&&!s(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!o||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide();this.suggestedListItems=[{value:t}]}i=""+(l(e=this.suggestedListItems[0])?e.value:e),n.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill(a),n.dropdown.highlightFirst&&this.dropdown.highlightOption(this.DOM.dropdown.content.children[0]),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.setStateSelection(),d||setTimeout((()=>{this.dropdown.position(),this.dropdown.render()})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},toggle(t){this.dropdown[this.state.dropdown.visible&&!t?"hide":"show"]()},render(){var t,e,i,s=(t=this.DOM.dropdown,(i=t.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(i),e=i.clientHeight,i.parentNode.removeChild(i),e),a=this.settings;return this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(a.classNames.dropdownInital),this.dropdown.position(s),a.dropdown.appendTarget.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(a.classNames.dropdownInital)))),this},fill(t){var e;t="string"==typeof t?t:this.dropdown.createListHTML(t||this.suggestedListItems),this.DOM.dropdown.content.innerHTML=(e=t)?e.replace(/\>[\r\n ]+\<").replace(/(<.*?>)|\s+/g,((t,e)=>e||" ")):""},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems(t),this.dropdown.fill(),this.suggestedListItems.length||this.dropdown.hide(),this.trigger("dropdown:updated",this.DOM.dropdown)},position(t){var e=this.settings.dropdown;if("manual"!=e.position){var i,s,a,n,o,r,l=this.DOM.dropdown,d=e.placeAbove,h=document.documentElement.clientHeight,g=Math.max(document.documentElement.clientWidth||0,window.innerWidth||0)>480?e.position:"all",p=this.DOM["input"==g?"input":"scope"];t=t||l.clientHeight,this.state.dropdown.visible&&("text"==g?(a=(i=this.getCaretGlobalPosition()).bottom,s=i.top,n=i.left,o="auto"):(r=function(t){for(var e=0,i=0;t;)e+=t.offsetLeft||0,i+=t.offsetTop||0,t=t.parentNode;return{left:e,top:i}}(this.settings.dropdown.appendTarget),s=(i=p.getBoundingClientRect()).top-r.top,a=i.bottom-1-r.top,n=i.left-r.left,o=i.width+"px"),s=Math.floor(s),a=Math.ceil(a),d=void 0===d?h-i.bottom{if(e)return this.dropdown.selectOption(e);this.dropdown.hide(),"mix"!=this.settings.mode&&this.addTags(this.state.inputText.trim(),!0)})).catch((t=>t));break;case"Backspace":{if("mix"==this.settings.mode||this.state.editing.scope)return;const t=this.input.raw.call(this);""!=t&&8203!=t.charCodeAt(0)||(!0===this.settings.backspace?this.removeTags():"edit"==this.settings.backspace&&setTimeout(this.editTag.bind(this),0))}}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);e&&this.dropdown.highlightOption(e)},onMouseLeave(t){this.dropdown.highlightOption()},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode(e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption(e):this.dropdown.hide()})).catch((t=>t))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t?+t.getAttribute("tagifySuggestionIdx"):-1;return this.suggestedListItems[e]||null},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.suggestedListItems[this.getNodeIndex(t)],this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position())},selectOption(t){var e,i=this.settings.dropdown,s=i.clearOnSelect,a=i.closeOnSelect;if(!t)return e=this.addTags(this.state.inputText,!0),void(a&&this.dropdown.hide());var n=t.getAttribute("tagifySuggestionIdx"),o=this.suggestedListItems[+n];this.trigger("dropdown:select",{data:o,elm:t}),n&&o?(this.state.editing?this.onEditTagDone(null,d({__isValid:!0},o)):e=this["mix"==this.settings.mode?"addMixTags":"addTags"]([o],s),this.DOM.input.parentNode&&(setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0),this.placeCaretAfterNode(e)})),a?setTimeout(this.dropdown.hide.bind(this)):this.dropdown.refilter())):this.dropdown.hide()},selectAll(){return this.suggestedListItems.length=0,this.dropdown.hide(),this.addTags(this.dropdown.filterListItems(""),!0),this},filterListItems(t,e){var i,s,a,n,o,r=this.settings,d=r.dropdown,g=(e=e||{},t="select"==r.mode&&this.value.length&&this.value[0][r.tagTextProp]==t?"":t,[]),p=[],c=r.whitelist,u=d.maxItems||1/0,m=d.searchKeys,v=0;if(!t||!m.length)return(r.duplicates?c:c.filter((t=>!this.isTagDuplicate(l(t)?t.value:t)))).slice(0,u);function f(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(o=d.caseSensitive?""+t:(""+t).toLowerCase();vm.includes(t)))?["value"]:m;d.fuzzySearch&&!e.exact?(a=T.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase().trim(),d.accentedSearch&&(a=h(a),o=h(o)),t=0==a.indexOf(o),u=a===o,s=f(a,o)):(t=!0,s=T.some((t=>{var s=""+(i[t]||"");return d.accentedSearch&&(s=h(s),o=h(o)),d.caseSensitive||(s=s.toLowerCase()),u=s===o,e.exact?s===o:0==s.indexOf(o)}))),n=!r.duplicates&&this.isTagDuplicate(l(i)?i.value:i),s&&!n&&(u&&t?p.push(i):"startsWith"==d.sortby&&t?g.unshift(i):g.push(i))}return"function"==typeof d.sortby?d.sortby(p.concat(g),o):p.concat(g).slice(0,u)},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return d([],t).map(((t,e)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var i=this.dropdown.getMappedValue(t);t.value=i&&"string"==typeof i?r(i):i;var s=this.settings.templates.dropdownItem.apply(this,[t,this]);return s=s.replace(/\s*tagifySuggestionIdx=(["'])(.*?)\1/gim,"").replace(">",` tagifySuggestionIdx="${e}">`)})).join("")}},f={empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},T={wrapper:(t,e)=>`\n \n `,tag(t,e){var i=this.settings;return`\n \n \n ${t[i.tagTextProp]||t.value} \n
\n `},dropdown(t){var e=t.dropdown,i="manual"==e.position,s=`${t.classNames.dropdown}`;return``},dropdownItem(t,e){return`${t.value}
`},dropdownItemNoMatch:null};var w={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(t=!0){var e,i=this.events.callbacks,s=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t){for(var a in this.state.mainEvents=t,t&&!this.listeners.main&&(this.events.bindGlobal.call(this),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",i.onFocusBlur.bind(this)],keydown:["input",i.onKeydown.bind(this)],click:["scope",i.onClickScope.bind(this)],dblclick:["scope",i.onDoubleClickScope.bind(this)],paste:["input",i.onPaste.bind(this)],drop:["input",i.onDrop.bind(this)]})this.DOM[e[a][0]][s](a,e[a][1]);clearInterval(this.listeners.main.originalInputValueObserverInterval),this.listeners.main.originalInputValueObserverInterval=setInterval(i.observeOriginalInputValue.bind(this),500);var n=this.listeners.main.inputMutationObserver||new MutationObserver(i.onInputDOMChange.bind(this));n&&n.disconnect(),"mix"==this.settings.mode&&n.observe(this.DOM.input,{childList:!0})}},bindGlobal(t){var e,i=this.events.callbacks,s=t?"removeEventListener":"addEventListener";if(t||!this.listeners.global)for(e of(this.listeners.global=this.listeners&&this.listeners.global||[{type:this.isIE?"keydown":"input",target:this.DOM.input,cb:i[this.isIE?"onInputIE":"onInput"].bind(this)},{type:"keydown",target:window,cb:i.onWindowKeyDown.bind(this)},{type:"blur",target:this.DOM.input,cb:i.onFocusBlur.bind(this)}],this.listeners.global))e.target[s](e.type,e.cb)},unbindGlobal(){this.events.bindGlobal.call(this,!0)},callbacks:{onFocusBlur(t){var e=t.target?this.trim(t.target.textContent):"",i=this.settings,s=t.type,a=i.dropdown.enabled>=0,n={relatedTarget:t.relatedTarget},o=this.state.actions.selectOption&&(a||!i.dropdown.closeOnSelect),r=this.state.actions.addNew&&a,l=t.relatedTarget&&p.call(this,t.relatedTarget)&&this.DOM.scope.contains(t.relatedTarget);if("blur"==s){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide(),void this.DOM.input.focus();this.postUpdate(),this.triggerChangeEvent()}if(!o&&!r)if(this.state.hasFocus="focus"==s&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=i.mode){if("focus"==s)return this.trigger("focus",n),void(0!==i.dropdown.enabled&&i.userInput||this.dropdown.show(this.value.length?"":void 0));"blur"==s&&(this.trigger("blur",n),this.loading(!1),"select"==this.settings.mode&&l&&(e=""),("select"==this.settings.mode&&e?!this.value.length||this.value[0].value!=e:e&&!this.state.actions.selectOption&&i.addTagOnBlur)&&this.addTags(e,!0),"select"!=this.settings.mode||e||this.removeTags()),this.DOM.input.removeAttribute("style"),this.dropdown.hide()}else"focus"==s?this.trigger("focus",n):"blur"==t.type&&(this.trigger("blur",n),this.loading(!1),this.dropdown.hide(),this.state.dropdown.visible=void 0,this.setStateSelection())},onWindowKeyDown(t){var e,i=document.activeElement;if(p.call(this,i)&&this.DOM.scope.contains(document.activeElement))switch(e=i.nextElementSibling,t.key){case"Backspace":this.removeTags(i),(e||this.DOM.input).focus();break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.settings;"select"==e.mode&&e.enforceWhitelist&&this.value.length&&"Tab"!=t.key&&t.preventDefault();var i=this.trim(t.target.textContent);if(this.trigger("keydown",{originalEvent:this.cloneEvent(t)}),"mix"==e.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var s,a,r,l=document.getSelection(),d="Delete"==t.key&&l.anchorOffset==(l.anchorNode.length||0),h=1==l.anchorNode.nodeType||!l.anchorOffset&&1==l.anchorNode.previousSibling.nodeType&&l.anchorNode.previousSibling,c=n(this.DOM.input.innerHTML),u=this.getTagElms();if("edit"==e.backspace&&h)return s=1==l.anchorNode.nodeType?null:l.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,s),void t.preventDefault();if(g()&&h)return r=o(h),h.hasAttribute("readonly")||h.remove(),this.DOM.input.focus(),void setTimeout((()=>{this.placeCaretAfterNode(r),this.DOM.input.click()}));if("BR"==l.anchorNode.nodeName)return;if((d||h)&&1==l.anchorNode.nodeType?a=0==l.anchorOffset?d?u[0]:null:u[l.anchorOffset-1]:d?a=l.anchorNode.nextElementSibling:h&&(a=h),3==l.anchorNode.nodeType&&!l.anchorNode.nodeValue&&l.anchorNode.previousElementSibling&&t.preventDefault(),(h||d)&&!e.backspace)return void t.preventDefault();if("Range"!=l.type&&!l.anchorOffset&&l.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=l.type&&a&&a.hasAttribute("readonly"))return void this.placeCaretAfterNode(o(a));clearTimeout(m),m=setTimeout((()=>{var t=document.getSelection(),e=n(this.DOM.input.innerHTML),i=!d&&t.anchorNode.previousSibling;if(e.length>=c.length&&i)if(p.call(this,i)&&!i.hasAttribute("readonly")){if(this.removeTags(i),this.fixFirefoxLastTagNoCaret(),2==this.DOM.input.children.length&&"BR"==this.DOM.input.children[1].tagName)return this.DOM.input.innerHTML="",this.value.length=0,!0}else i.remove();this.value=[].map.call(u,((t,e)=>{var i=this.tagData(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}switch(t.key){case"Backspace":"select"==e.mode&&e.enforceWhitelist&&this.value.length?this.removeTags():this.state.dropdown.visible&&"manual"!=e.dropdown.position||""!=i&&8203!=i.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show();break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&e.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let s="select"==e.mode;if(!i||s)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible||229==t.keyCode)return;t.preventDefault(),setTimeout((()=>{this.state.actions.selectOption||this.addTags(i,!0)}))}},onInput(t){if("mix"==this.settings.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var e=this.input.normalize.call(this),i=e.length>=this.settings.dropdown.enabled,s={value:e,inputElm:this.DOM.input};s.isValid=this.validateTag({value:e}),this.state.inputText!=e&&(this.input.set.call(this,e,!1),-1!=e.search(this.settings.delimiters)?this.addTags(e)&&this.input.set.call(this):this.settings.dropdown.enabled>=0&&this.dropdown[i?"show":"hide"](e),this.trigger("input",s))},onMixTagsInput(t){var e,i,s,a,n,o,r,l,h=this.settings,p=this.value.length,c=this.getTagElms(),u=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(c,(t=>this.tagData(t).value));if("deleteContentBackward"==t.inputType&&g()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&u.appendChild(this.createTagElem(t))})),u.childNodes.length&&(m.insertNode(u),this.setRangeAtStartEnd(!1,u.lastChild)),c.length!=p)return this.value=[].map.call(this.getTagElms(),(t=>this.tagData(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(h.pattern).length-1,(i=e.match(h.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(h.pattern)[0],value:a.replace(h.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,l=this.state.tag.value.match(h.delimiters))return this.state.tag.value=this.state.tag.value.replace(h.delimiters,""),this.state.tag.delimiters=l[0],this.addTags(this.state.tag.value,h.dropdown.clearOnSelect),void this.dropdown.hide();n=this.state.tag.value.length>=h.dropdown.enabled;try{r=(r=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&r.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(r||s{this.update({withoutChangeEvent:!0}),this.trigger("input",d({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"](this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},observeOriginalInputValue(){this.DOM.originalInput.value!=this.DOM.originalInput.tagifyValue&&this.loadOriginalValues()},onClickScope(t){var e=this.settings,i=t.target.closest("."+e.classNames.tag),s=+new Date-this.state.hasFocus;if(t.target!=this.DOM.scope){if(!t.target.classList.contains(e.classNames.tagX))return i?(this.trigger("click",{tag:i,index:this.getNodeIndex(i),data:this.tagData(i),originalEvent:this.cloneEvent(t)}),void(1!==e.editTags&&1!==e.editTags.clicks||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==e.mode&&this.fixFirefoxLastTagNoCaret(),s>500)?this.state.dropdown.visible?this.dropdown.hide():0===e.dropdown.enabled&&"mix"!=e.mode&&this.dropdown.show(this.value.length?"":void 0):"select"==e.mode&&!this.state.dropdown.visible&&this.dropdown.show());this.removeTags(t.target.parentNode)}else this.state.hasFocus||this.DOM.input.focus()},onPaste(t){t.preventDefault();var e,i,s=this.settings;if("select"==s.mode&&s.enforceWhitelist||!s.userInput)return!1;s.readonly||(e=t.clipboardData||window.clipboardData,i=e.getData("Text"),s.hooks.beforePaste(t,{tagify:this,pastedText:i,clipboardData:e}).then((e=>{void 0===e&&(e=i),e&&(this.injectAtCaret(e,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.settings.pasteAsTags?this.addTags(this.state.inputText+e,!0):this.state.inputText=e)})).catch((t=>t)))},onDrop(t){t.preventDefault()},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=this.tagData(i),n=this.input.normalize.call(this,t),o=i.innerHTML!=i.__tagifyTagData.__originalHTML,r=this.validateTag({[this.settings.tagTextProp]:n});o||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show(n)),this.trigger("edit:input",{tag:i,index:s,data:d({},this.value[s],{newValue:n}),originalEvent:this.cloneEvent(e)})},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t){if(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t)){var e,i,s=this.settings,a=t.closest("."+s.classNames.tag),n=this.input.normalize.call(this,t),o=this.tagData(a).__originalData,r=a.innerHTML!=a.__tagifyTagData.__originalHTML,l=this.validateTag({[s.tagTextProp]:n});if(n)if(r){if(e=this.hasMaxTags(),i=this.getWhitelistItem(n)||d({},o,{[s.tagTextProp]:n,value:n,__isValid:l}),s.transformTag.call(this,i,o),!0!==(l=!e&&this.validateTag({[s.tagTextProp]:i[s.tagTextProp]}))){if(this.trigger("invalid",{data:i,tag:a,message:l}),s.editTags.keepInvalid)return;s.keepInvalidTags?i.__isValid=l:i=o}else s.keepInvalidTags&&(delete i.title,delete i["aria-invalid"],delete i.class);this.onEditTagDone(a,i)}else this.onEditTagDone(a,o);else this.onEditTagDone(a)}},onEditTagkeydown(t,e){switch(this.trigger("edit:keydown",{originalEvent:this.cloneEvent(t)}),t.key){case"Esc":case"Escape":e.innerHTML=e.__tagifyTagData.__originalHTML;case"Enter":case"Tab":t.preventDefault(),t.target.blur()}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=this.settings;s&&a.userInput&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),"select"==a.mode||a.readonly||e||i||!this.settings.editTags||this.editTag(s),this.toggleFocusClass(!0),this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:this.tagData(s)}))},onInputDOMChange(t){t.forEach((t=>{t.addedNodes.forEach((t=>{if(t)if("
"==t.outerHTML)t.replaceWith(document.createElement("br"));else if(1==t.nodeType&&t.querySelector(this.settings.classNames.tagSelector)){let e=document.createTextNode("");3==t.childNodes[0].nodeType&&"BR"!=t.previousSibling.nodeName&&(e=document.createTextNode("\n")),t.replaceWith(e,...[...t.childNodes].slice(0,-1)),this.placeCaretAfterNode(e.previousSibling)}else p.call(this,t)&&t.previousSibling&&"BR"==t.previousSibling.nodeName&&(t.previousSibling.replaceWith("\n"),this.placeCaretAfterNode(t.previousSibling.previousSibling))})),t.removedNodes.forEach((t=>{t&&"BR"==t.nodeName&&p.call(this,e)&&(this.removeTags(e),this.fixFirefoxLastTagNoCaret())}))}));var e=this.DOM.input.lastChild;e&&""==e.nodeValue&&e.remove(),e&&"BR"==e.nodeName||this.DOM.input.appendChild(document.createElement("br"))}}};function b(t,e){return t?t.previousElementSibling&&t.previousElementSibling.classList.contains("tagify")?(console.warn("Tagify: ","input element is already Tagified",t),this):(d(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?d({},o):o).tagify=this,s instanceof Object)for(var r in s)s[r]instanceof HTMLElement&&(o[r]=s[r]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox="undefined"!=typeof InstallTrigger,this.isIE=window.document.documentMode,this.applySettings(t,e||{}),this.state={inputText:"",editing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),u.call(this),this.getCSSVars(),setTimeout((()=>this.loadOriginalValues())),this.events.customBinding.call(this),this.events.binding.call(this),void(t.autofocus&&this.DOM.input.focus())):(console.warn("Tagify: ","input element not found",t),this)}return b.prototype={_dropdown:v,customEventsList:["change","add","remove","invalid","input","click","keydown","focus","blur","edit:input","edit:beforeUpdate","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch","dropdown:scroll"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:function(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild},templates:T,parseTemplate(t,e){return t=this.settings.templates[t]||t,this.parseHTML(t.apply(this,e))},set whitelist(t){this.settings.whitelist=t&&Array.isArray(t)?t:[]},get whitelist(){return this.settings.whitelist},applySettings(t,i){c.templates=this.templates;var s=this.settings=d({},c,i);s.disabled=t.hasAttribute("disabled"),s.readonly=t.hasAttribute("readonly"),s.placeholder=t.getAttribute("placeholder")||s.placeholder||"",s.required=t.hasAttribute("required");for(let t in s.classNames)Object.defineProperty(s.classNames,t+"Selector",{get(){return"."+this[t].split(" ")[0]}});if(this.isIE&&(s.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var i=t.getAttribute("data-"+e);i&&(i=i.split(s.delimiters))instanceof Array&&(s[e]=i)})),"autoComplete"in i&&!l(i.autoComplete)&&(s.autoComplete=c.autoComplete,s.autoComplete.enabled=i.autoComplete),"mix"==s.mode&&(s.autoComplete.rightKey=!0,s.delimiters=i.delimiters||null,s.tagTextProp&&!s.dropdown.searchKeys.includes(s.tagTextProp)&&s.dropdown.searchKeys.push(s.tagTextProp)),t.pattern)try{s.pattern=new RegExp(t.pattern)}catch(t){}if(this.settings.delimiters)try{s.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}this.TEXTS=e(e({},f),s.texts||{}),"select"!=s.mode&&s.userInput||(s.dropdown.enabled=0),s.dropdown.appendTarget=i.dropdown&&i.dropdown.appendTarget?i.dropdown.appendTarget:document.body},getAttributes(t){var e,i=this.getCustomAttributes(t),s="";for(e in i)s+=" "+e+(void 0!==t[e]?`="${i[e]}"`:"");return s},getCustomAttributes(t){if(!l(t))return"";var e,i={};for(e in t)"__"!=e.slice(0,2)&&"class"!=e&&t.hasOwnProperty(e)&&void 0!==t[e]&&(i[e]=r(t[e]));return i},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCaretGlobalPosition(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);var e;this.CSSVars={tagHideTransition:(({value:t,unit:e})=>"s"==e?1e3*t:t)(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}((e="tag-hide-transition",t.getPropertyValue("--"+e))))}},build(t){var e=this.DOM;this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t))},destroy(){this.events.unbindGlobal.call(this),this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.dropdown.hide(!0),clearTimeout(this.dropdownHide__bindEventsTimeout)},loadOriginalValues(t){var e,i=this.settings;if(void 0===t&&(t=i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value),this.removeAllTags({withoutChangeEvent:!0}),t)if("mix"==i.mode)this.parseMixTags(t.trim()),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend"," ");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value,this.state.loadedOriginalValues=!0},cloneEvent(t){var e={};for(var i in t)e[i]=t[i];return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:w,fixFirefoxLastTagNoCaret(){},placeCaretAfterNode(t){if(t&&t.parentNode){var e=t.nextSibling,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e||t),s.collapse(!0),i.removeAllRanges(),i.addRange(s))}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide();var i=this.settings;function s(){return t.querySelector(i.classNames.tagTextSelector)}var a=s(),n=this.getNodeIndex(t),o=this.tagData(t),r=this.events.callbacks,l=this,h=!0;if(a){if(!(o instanceof Object&&"editable"in o)||o.editable)return a.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),this.tagData(t,{__originalData:d({},o),__originalHTML:t.innerHTML}),a.addEventListener("focus",r.onEditTagFocus.bind(this,t)),a.addEventListener("blur",(function(){setTimeout((()=>r.onEditTagBlur.call(l,s())))})),a.addEventListener("input",r.onEditTagInput.bind(this,a)),a.addEventListener("keydown",(e=>r.onEditTagkeydown.call(this,e,t))),a.focus(),this.setRangeAtStartEnd(!1,a),e.skipValidation||(h=this.editTagToggleValidity(t)),a.originalIsValid=h,this.trigger("edit:start",{tag:t,index:n,data:o,isValid:h}),this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i;if(e=e||this.tagData(t))return(i=!("__isValid"in e)||!0===e.__isValid)||this.removeTagsFromValue(t),this.update(),t.classList.toggle(this.settings.classNames.tagNotAllowed,!i),e.__isValid;console.warn("tag has no data: ",t,e)},onEditTagDone(t,e){e=e||{};var i={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:this.tagData(t),data:e};this.trigger("edit:beforeUpdate",i,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&e[this.settings.tagTextProp]?(t=this.replaceTag(t,e),this.editTagToggleValidity(t,e),this.settings.a11y.focusableTags&&t.focus()):t&&this.removeTags(t),this.trigger("edit:updated",i),this.dropdown.hide(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&d(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(this.tagData(t))})),this.update()},setRangeAtStartEnd(t,e){t="number"==typeof t?t:!!t,e=(e=e||this.DOM.input).lastChild||e;var i=document.getSelection();try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){}},injectAtCaret(t,e){if(e=e||this.state.selection.range)return"string"==typeof t&&(t=document.createTextNode(t)),e.deleteContents(),e.insertNode(t),this.setRangeAtStartEnd(!1,t),this.updateValueByDOMTags(),this.update(),this},input:{set(t="",e=!0){var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=r(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},raw(){return this.DOM.input.textContent},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t){var e=t||this.DOM.input,i=[];e.childNodes.forEach((t=>3==t.nodeType&&i.push(t.nodeValue))),i=i.join("\n");try{i=i.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return i=i.replace(/\s/g," "),this.settings.trim&&(i=i.replace(/^\s+/,"")),i},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"string"==typeof(t=t||{})&&(t={value:t});var e=t.value?""+t.value:"",i=e.substr(0,this.state.inputText.length).toLowerCase(),s=e.substring(this.state.inputText.length);e&&this.state.inputText&&i==this.state.inputText.toLowerCase()?(this.DOM.input.setAttribute("data-suggest",s),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd()),this.input.autocomplete.suggest.call(this),this.dropdown.hide(),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(...t){var e="."+[...this.settings.classNames.tag.split(" "),...t].join(".");return[].slice.call(this.DOM.scope.querySelectorAll(e))},getLastTag(){var t=this.DOM.scope.querySelectorAll(`${this.settings.classNames.tagSelector}:not(.${this.settings.classNames.tagHide}):not([readonly])`);return t[t.length-1]},tagData:(t,e,i)=>t?(e&&(t.__tagifyTagData=i?e:d({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag elment doesn't exist",t,e),e),isTagDuplicate(t,e){var i=this.settings;return"select"!=i.mode&&this.value.reduce(((a,n)=>s(this.trim(""+t),n.value,e||i.dropdown.caseSensitive)?a+1:a),0)},getTagIndexByValue(t){var e=[];return this.getTagElms().forEach(((i,a)=>{s(this.trim(i.textContent),t,this.settings.dropdown.caseSensitive)&&e.push(a)})),e},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(t,e,i){e=e||"value";var a,n=this.settings;return(i=i||n.whitelist).some((i=>{var o="string"==typeof i?i:i[e]||i.value;if(s(o,t,n.dropdown.caseSensitive,n.trim))return a="string"==typeof i?{value:i}:i,!0})),a||"value"!=e||"value"==n.tagTextProp||(a=this.getWhitelistItem(t,n.tagTextProp,i)),a},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,this.state.editing)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t,e){var i=this.settings;document.activeElement.blur(),i[e||"readonly"]=t,this.DOM.scope[(t?"set":"remove")+"Attribute"](e||"readonly",!0),"mix"==i.mode&&(this.DOM.input.contentEditable=!t)},setDisabled(t){this.setReadonly(t,"disabled")},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp;e.enforceWhitelist;var o=[],r=!!i&&i[0]instanceof Object,l=t instanceof Array,d=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=d(t)}else l&&(t=[].concat(...t.map((t=>t.value?t:d(t)))));return r&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0});this.settings.duplicates||(i=i.filter((t=>!e.includes(t.value))));var s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),o.length&&(t=o)),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];return t=t.split(i[0]).map(((t,e)=>{var d,h,g,p=t.split(i[1]),c=p[0],u=l.length==o;try{if(c==+c)throw Error;h=JSON.parse(c)}catch(t){h=this.normalizeTags(c)[0]||{value:c}}if(u||!(p.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else a.call(this,h),h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),p[0]=g.outerHTML,this.value.push(h);return p.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize(),this.getTagElms().forEach(((t,e)=>this.tagData(t,l[e]))),this.update({withoutChangeEvent:!0}),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),i=n.nodeValue.lastIndexOf(e),s=n.splitText(i),t&&n.parentNode.replaceChild(t,s),!0}},selectTag(t,e){var i=this.settings;if(!i.enforceWhitelist||this.isTagWhitelisted(e.value)){this.input.set.call(this,e[i.tagTextProp]||e.value,!0),this.state.actions.selectOption&&setTimeout(this.setRangeAtStartEnd.bind(this));var s=this.getLastTag();return s?this.replaceTag(s,e):this.appendTag(t),i.enforceWhitelist&&this.DOM.input.removeAttribute("contenteditable"),this.value[0]=e,this.trigger("add",{tag:t,data:e}),this.update(),[t]}},addEmptyTag(t){var e=d({value:""},t||{}),i=this.createTagElem(e);this.tagData(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0})},addTags(t,e,i){var s=[],a=this.settings,n=document.createDocumentFragment();return i=i||a.skipInvalid,t&&0!=t.length?(t=this.normalizeTags(t),"mix"==a.mode?this.addMixTags(t):("select"==a.mode&&(e=!1),this.DOM.input.removeAttribute("style"),t.forEach((t=>{var e,o={},r=Object.assign({},t,{value:t.value+""});if((t=Object.assign({},r)).__isValid=this.hasMaxTags()||this.validateTag(t),a.transformTag.call(this,t),!0!==t.__isValid){if(i)return;d(o,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:r}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value))}if(t.readonly&&(o["aria-readonly"]=!0),e=this.createTagElem(t,o),s.push(e),"select"==a.mode)return this.selectTag(e,t);n.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.trigger("add",{tag:e,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position()})),this.appendTag(n),this.update(),t.length&&e&&this.input.set.call(this),this.dropdown.refilter(),s)):("select"==a.mode&&this.removeAllTags(),s)},addMixTags(t){if((t=this.normalizeTags(t))[0].prefix||this.state.tag)return this.prefixedTextToTag(t[0]);"string"==typeof t&&(t=[{value:t}]);var e=!!this.state.selection,i=document.createDocumentFragment();return t.forEach((t=>{var e=this.createTagElem(t);i.appendChild(e),this.insertAfterTag(e)})),e?this.injectAtCaret(i):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(i),this.updateValueByDOMTags(),this.update()),i},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;this.placeCaretAfterNode(a)}return this.state.tag=null,this.trigger("add",d({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.scope.lastElementChild;i===e.input?e.scope.insertBefore(t,i):e.scope.appendChild(t)},createTagElem(t,i){t.__tagId=([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)));var s,a=d({},t,e({value:r(t.value+"")},i));return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(s=this.parseTemplate("tag",[a])),this.tagData(s,t),s},reCheckInvalidTags(){var t=this.settings;this.getTagElms(t.classNames.tagNotAllowed).forEach(((t,e)=>{var i=this.tagData(t),s=this.hasMaxTags(),a=this.validateTag(i);if(!0===a&&!s)return i=i.__preInvalidData?i.__preInvalidData:{value:i.value},this.replaceTag(t,i);t.title=s||a}))},removeTags(t,e,i){var s;t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>(e&&"string"==typeof e&&(e=this.getTagElmByValue(e)),e&&this.tagData(e)&&t.push({node:e,idx:this.getTagIdx(this.tagData(e)),data:this.tagData(e,{__removed:!0})}),t)),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==this.settings.mode&&(i=0,this.input.set.call(this)),1==s.length&&s[0].node.classList.contains(this.settings.classNames.tagNotAllowed)&&(e=!0),s.length&&this.settings.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?this.settings.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter(),this.dropdown.position(),this.DOM.input.normalize(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(this.settings.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(this.removeTagsFromValue(s.map((t=>t.node))),this.update(),"select"==this.settings.mode&&this.DOM.input.setAttribute("contenteditable",!0))})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeTagsFromValue(t){(t=Array.isArray(t)?t:[t]).forEach((t=>{var e=this.tagData(t),i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)}))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.position(),"select"==this.settings.mode&&this.input.set.call(this),this.update(t)},postUpdate(){var t=this.settings.classNames,e="mix"==this.settings.mode?this.settings.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value:this.value.length;this.toggleClass(t.hasMaxTags,this.value.length>=this.settings.maxTags),this.toggleClass(t.hasNoTags,!this.value.length),this.toggleClass(t.empty,!e)},update(t){var e=this.DOM.originalInput,i=this.getInputValue();this.settings.mixMode.integrated||(e.value=i,e.tagifyValue=e.value),this.postUpdate(),!(t||{}).withoutChangeEvent&&this.state.loadedOriginalValues&&this.triggerChangeEvent()},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return e=t||this.value,i=this.dataProps,e&&Array.isArray(e)&&e.map((t=>a(t,i)));var e,i},getMixedTagsAsString(){var t="",e=this,i=this.settings.mixTagsInterpolator;return function s(n){n.childNodes.forEach((n=>{if(1==n.nodeType){const o=e.tagData(n);if("BR"==n.tagName&&(t+="\r\n"),"DIV"==n.tagName||"P"==n.tagName)t+="\r\n",s(n);else if(p.call(e,n)&&o){if(o.__removed)return;t+=i[0]+JSON.stringify(a(o,e.dataProps))+i[1]}}else t+=n.textContent}))}(this.DOM.input),t}},b.prototype.removeTag=b.prototype.removeTags,b}));
diff --git a/node_modules/@yaireo/tagify/dist/tagify.polyfills.min.js b/node_modules/@yaireo/tagify/dist/tagify.polyfills.min.js
new file mode 100644
index 0000000..908b58b
--- /dev/null
+++ b/node_modules/@yaireo/tagify/dist/tagify.polyfills.min.js
@@ -0,0 +1,10 @@
+/**
+ * Tagify (v 4.8.1) - tags input component
+ * By Yair Even-Or
+ * Don't sell this code. (c)
+ * https://github.com/yairEO/tagify
+ */
+
+!function(t){"function"==typeof define&&define.amd?define(t):t()}((function(){"use strict";function t(t,e){e=e||{bubbles:!1,cancelable:!1,detail:void 0};var n=document.createEvent("CustomEvent");return n.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),n}var e;"".trim||(String.prototype.trim=function(){return this.replace(/^[\s]+|[\s]+$/g,"")}),window.NodeList&&!NodeList.prototype.forEach&&(NodeList.prototype.forEach=Array.prototype.forEach),Array.prototype.findIndex||Object.defineProperty(Array.prototype,"findIndex",{value:function(t){if(null==this)throw new TypeError('"this" is null or not defined');var e=Object(this),n=e.length>>>0;if("function"!=typeof t)throw new TypeError("predicate must be a function");for(var o=arguments[1],r=0;r>>0,r=0;rthis.length)&&-1!==this.indexOf(t,e)}),"function"!=typeof Object.assign&&Object.defineProperty(Object,"assign",{value:function(t,e){if(null==t)throw new TypeError("Cannot convert undefined or null to object");for(var n=Object(t),o=1;o
+
+
+
+
+
\ No newline at end of file
diff --git a/node_modules/@yaireo/tagify/package.json b/node_modules/@yaireo/tagify/package.json
new file mode 100644
index 0000000..98ce612
--- /dev/null
+++ b/node_modules/@yaireo/tagify/package.json
@@ -0,0 +1,124 @@
+{
+ "_from": "@yaireo/tagify",
+ "_id": "@yaireo/tagify@4.8.1",
+ "_inBundle": false,
+ "_integrity": "sha512-SK3mN6HPzNzW3rgolvdsv/3m8OX5xdbh6H8oz/fjdLCa0Qt3JoaAQqjsMxaLG575+ywl5jpvsaIj+JB6yi9LUQ==",
+ "_location": "/@yaireo/tagify",
+ "_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-4.8.1.tgz",
+ "_shasum": "464708e3204b03e0991c06fb8fd12fac60f5abd2",
+ "_spec": "@yaireo/tagify",
+ "_where": "C:\\blog\\blog",
+ "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.15.5",
+ "@babel/plugin-proposal-object-rest-spread": "^7.14.7",
+ "@babel/plugin-transform-destructuring": "^7.14.7",
+ "@babel/preset-env": "^7.15.4",
+ "@babel/preset-react": "^7.14.5",
+ "@rollup/plugin-babel": "^5.3.0",
+ "@rollup/stream": "git+https://github.com/andremacola/stream.git",
+ "beepbeep": "^1.3.0",
+ "gulp": "^4.0.2",
+ "gulp-autoprefixer": "^8.0.0",
+ "gulp-babel": "^8.0.0",
+ "gulp-bump": "^3.2.0",
+ "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-header-comment": "^0.9.0",
+ "gulp-insert": "^0.5.0",
+ "gulp-load-plugins": "^2.0.7",
+ "gulp-rename": "^2.0.0",
+ "gulp-replace": "^1.1.3",
+ "gulp-sass": "^5.0.0",
+ "gulp-sourcemaps": "^3.0.0",
+ "gulp-streamify": "^1.0.2",
+ "gulp-tag-version": "^1.3.1",
+ "gulp-tap": "^2.0.0",
+ "gulp-terser": "^2.0.1",
+ "gulp-umd": "^2.0.0",
+ "gulp-util": "^3.0.8",
+ "gulp-watch": "^5.0.1",
+ "path": "^0.12.7",
+ "rollup": "^2.56.3",
+ "rollup-plugin-banner": "^0.2.1",
+ "rollup-plugin-terser": "^7.0.2",
+ "run-sequence": "^2.2.1",
+ "sass": "^1.39.0",
+ "semver": "^7.3.5",
+ "vinyl-source-stream": "^2.0.0"
+ },
+ "files": [
+ "/dist",
+ "/src/tagify.scss"
+ ],
+ "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",
+ "np": {
+ "yarn": false,
+ "yolo": true
+ },
+ "peerDependencies": {
+ "prop-types": "^15.7.2"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/yairEO/tagify.git"
+ },
+ "scripts": {
+ "build": "gulp",
+ "prepublishOnly": "pkg-ok",
+ "serve": "npx http-server -o index.html -c-1",
+ "start": "gulp --dev",
+ "test": "echo \"No test specified\"",
+ "version": "gulp build && git add ."
+ },
+ "version": "4.8.1"
+}
diff --git a/node_modules/@yaireo/tagify/src/tagify.scss b/node_modules/@yaireo/tagify/src/tagify.scss
new file mode 100644
index 0000000..f2c8c8a
--- /dev/null
+++ b/node_modules/@yaireo/tagify/src/tagify.scss
@@ -0,0 +1,693 @@
+@use "sass:math";
+
+: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-disabled-bg : #F1F1F1;
+ --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($size:5px){
+ background: linear-gradient(45deg, var(--tag-bg) 25%,
+ transparent 25%,
+ transparent 50%,
+ var(--tag-bg) 50%,
+ var(--tag-bg) 75%,
+ transparent 75%,
+ transparent) 0/#{$size} #{$size};
+ 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 : normal;
+ 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);
+ }
+
+ &[disabled]{
+ background: var(--tags-disabled-bg);
+ filter: saturate(0);
+ opacity: .5;
+ pointer-events: none;
+ }
+
+ // Global "read-only" mode (no input button)
+ &[readonly]{
+ {$self}--select{
+ pointer-events: none;
+ }
+
+ &:not(#{$self}--mix):not(#{$self}--select){
+ 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{
+ > br:last-child{ display:none; }
+ &::before{ content:none; }
+ &::after{
+ @include loader;
+ content: '' !important;
+ margin: -2px 0 -2px .5em;
+ }
+ &:empty{
+ &::after{
+ margin-left:0;
+ }
+ }
+ }
+ }
+
+ ///////////////////////////////////////////
+ // Hides originals
+ + input,
+ + textarea{
+ position: absolute !important;
+ left: -9999em !important;
+ transform: scale(0) !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]),
+ &:focus{
+ div{ // :not([contenteditable])
+ &::before{
+ $size: math.div(-$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 : math.div($size,3);
+ margin-left : auto;
+
+ 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 1px;
+ // line-height: 1.1;
+
+ > div{
+ padding-top:0; padding-bottom:0;
+ }
+ }
+ }
+
+ &--mix {
+ display: block; // display:flex makes Chrome generates
when pressing ENTER key
+
+ #{ $self }__input{
+ padding: $tagMargin;
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ line-height: 1.5;
+ display: block; // needed to resolve this bug: https://bugs.chromium.org/p/chromium/issues/detail?id=1182621
+
+ &::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: math.div($size,2);
+ height: math.div($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: 1.1px; // fixes - https://bugs.chromium.org/p/chromium/issues/detail?id=1147523
+ 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: auto;
+ 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-bottom-width: 1.33px; // fixes - https://bugs.chromium.org/p/chromium/issues/detail?id=1147523
+ border-top-width: 0;
+ box-shadow: 0 2px 4px -2px rgba(black,.2);
+ // box-sizing: border-box;
+ transition: $trans;
+ }
+
+ // 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%);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..46b9ada
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,11 @@
+{
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@yaireo/tagify": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-4.8.1.tgz",
+ "integrity": "sha512-SK3mN6HPzNzW3rgolvdsv/3m8OX5xdbh6H8oz/fjdLCa0Qt3JoaAQqjsMxaLG575+ywl5jpvsaIj+JB6yi9LUQ=="
+ }
+ }
+}
diff --git a/src/main/java/myblog/blog/article/controller/ArticleController.java b/src/main/java/myblog/blog/article/controller/ArticleController.java
index 9d5b168..b5c45ff 100644
--- a/src/main/java/myblog/blog/article/controller/ArticleController.java
+++ b/src/main/java/myblog/blog/article/controller/ArticleController.java
@@ -1,40 +1,62 @@
package myblog.blog.article.controller;
import lombok.RequiredArgsConstructor;
+import myblog.blog.article.dto.ArticleForMainView;
import myblog.blog.article.dto.NewArticleDto;
import myblog.blog.article.service.ArticleService;
+import myblog.blog.category.dto.CategoryForMainView;
+import myblog.blog.category.service.CategoryService;
import myblog.blog.member.auth.PrincipalDetails;
+import myblog.blog.tags.service.TagsService;
+import org.springframework.data.domain.Slice;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
@Controller
@RequiredArgsConstructor
public class ArticleController {
private final ArticleService articleService;
+ private final TagsService tagsService;
+ private final CategoryService categoryService;
@GetMapping("article/write")
public String writeArticleForm(NewArticleDto newArticleDto, Model model){
+
+ CategoryForMainView categoryForView = categoryService.getCategoryForView();
+
+ model.addAttribute("category",categoryForView);
model.addAttribute(newArticleDto);
- return "articleWriteForm";
+ return "article/articleWriteForm";
}
@PostMapping("article/write")
+ @Transactional
public String WriteArticle(@ModelAttribute NewArticleDto newArticleDto, Authentication authentication){
+
PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
newArticleDto.setMemberId(principal.getMemberId());
- Long articleId = articleService.writeArticle(newArticleDto);
+ articleService.writeArticle(newArticleDto);
return "redirect:/";
}
+ @GetMapping("/main/article/{pageNum}")
+ public @ResponseBody
+ List nextPage(@PathVariable int pageNum){
+
+ return articleService.getRecentArticles(pageNum).getContent();
+ }
+
+
}
diff --git a/src/main/java/myblog/blog/article/domain/Article.java b/src/main/java/myblog/blog/article/domain/Article.java
index dddc5ca..fb0f909 100644
--- a/src/main/java/myblog/blog/article/domain/Article.java
+++ b/src/main/java/myblog/blog/article/domain/Article.java
@@ -3,9 +3,13 @@ package myblog.blog.article.domain;
import lombok.Builder;
import lombok.Getter;
import myblog.blog.base.domain.BasicEntity;
+import myblog.blog.category.domain.Category;
import myblog.blog.member.doamin.Member;
+import myblog.blog.tags.domain.ArticleTagList;
import javax.persistence.*;
+import java.util.ArrayList;
+import java.util.List;
@Entity
@Getter
@@ -27,20 +31,31 @@ public class Article extends BasicEntity {
@Column(columnDefinition = "bigint default 0",nullable = false)
private Long hit;
private String toc;
+
+ private String thumbnailUrl;
+
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
- private String thumbnailUrl;
+
+ @OneToMany(mappedBy = "article")
+ private List articleTagLists = new ArrayList<>();
+
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "category_id")
+ private Category category;
protected Article() {
}
@Builder
- public Article(String title, String content, String toc, Member member) {
+ public Article(String title, String content, String toc, Member member, String thumbnailUrl, Category category) {
this.title = title;
this.content = content;
this.toc = toc;
this.member = member;
+ this.thumbnailUrl = thumbnailUrl;
this.hit = 0L;
+ this.category = category;
}
}
diff --git a/src/main/java/myblog/blog/article/dto/ArticleForMainView.java b/src/main/java/myblog/blog/article/dto/ArticleForMainView.java
new file mode 100644
index 0000000..1b9a542
--- /dev/null
+++ b/src/main/java/myblog/blog/article/dto/ArticleForMainView.java
@@ -0,0 +1,19 @@
+package myblog.blog.article.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+public class ArticleForMainView {
+
+ private Long id;
+ private String title;
+ private String content;
+ private String thumbnailUrl;
+ private LocalDateTime createdDate;
+
+}
diff --git a/src/main/java/myblog/blog/article/dto/NewArticleDto.java b/src/main/java/myblog/blog/article/dto/NewArticleDto.java
index 121e220..fd5b0c1 100644
--- a/src/main/java/myblog/blog/article/dto/NewArticleDto.java
+++ b/src/main/java/myblog/blog/article/dto/NewArticleDto.java
@@ -1,9 +1,16 @@
package myblog.blog.article.dto;
+import com.fasterxml.jackson.databind.JsonNode;
import lombok.Getter;
import lombok.Setter;
+import myblog.blog.tags.domain.Tags;
+import myblog.blog.tags.dto.TagsDto;
import javax.validation.constraints.NotBlank;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
@Setter
@Getter
@@ -19,5 +26,8 @@ public class NewArticleDto {
private String thumbnailUrl;
+ private String category;
+ private String tags;
+
}
diff --git a/src/main/java/myblog/blog/article/repository/ArticlePagingRepository.java b/src/main/java/myblog/blog/article/repository/ArticlePagingRepository.java
new file mode 100644
index 0000000..aa4ad33
--- /dev/null
+++ b/src/main/java/myblog/blog/article/repository/ArticlePagingRepository.java
@@ -0,0 +1,12 @@
+package myblog.blog.article.repository;
+
+import myblog.blog.article.domain.Article;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.repository.Repository;
+
+public interface ArticlePagingRepository extends Repository {
+
+ Slice findBy(Pageable pageable);
+
+}
diff --git a/src/main/java/myblog/blog/article/repository/ArticleRepository.java b/src/main/java/myblog/blog/article/repository/ArticleRepository.java
index e335067..829cbe4 100644
--- a/src/main/java/myblog/blog/article/repository/ArticleRepository.java
+++ b/src/main/java/myblog/blog/article/repository/ArticleRepository.java
@@ -1,11 +1,18 @@
package myblog.blog.article.repository;
import myblog.blog.article.domain.Article;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.List;
+
public interface ArticleRepository extends JpaRepository {
+ List findTop6ByOrderByHitDesc();
+
+ Slice findByOrderByCreatedDateDesc(Pageable pageable);
}
diff --git a/src/main/java/myblog/blog/article/service/ArticleService.java b/src/main/java/myblog/blog/article/service/ArticleService.java
index 9b304d2..6439158 100644
--- a/src/main/java/myblog/blog/article/service/ArticleService.java
+++ b/src/main/java/myblog/blog/article/service/ArticleService.java
@@ -2,27 +2,70 @@ package myblog.blog.article.service;
import lombok.RequiredArgsConstructor;
import myblog.blog.article.domain.Article;
+import myblog.blog.article.dto.ArticleForMainView;
import myblog.blog.article.dto.NewArticleDto;
import myblog.blog.article.repository.ArticleRepository;
+import myblog.blog.category.service.CategoryService;
import myblog.blog.member.doamin.Member;
import myblog.blog.member.repository.MemberRepository;
+import myblog.blog.member.service.Oauth2MemberService;
+import myblog.blog.tags.service.TagsService;
+import org.modelmapper.ModelMapper;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
@Service
+@Transactional
@RequiredArgsConstructor
public class ArticleService {
- private final ArticleRepository articleRepository;
+ private final ArticleRepository
+ articleRepository;
private final MemberRepository memberRepository;
+ private final TagsService tagsService;
+ private final CategoryService categoryService;
+ private final Oauth2MemberService memberService;
+ private final ModelMapper modelMapper;
public Long writeArticle(NewArticleDto articleDto) {
Article newArticle = createNewArticleFrom(articleDto);
+
articleRepository.save(newArticle);
+ tagsService.createNewTagsAndArticleTagList(articleDto.getTags(), newArticle);
+
return newArticle.getId();
}
+ public List getPopularArticles() {
+ List top6ByOrderByHitDesc = articleRepository.findTop6ByOrderByHitDesc();
+
+ List articles = new ArrayList<>();
+
+ for (Article article : top6ByOrderByHitDesc) {
+ articles.add(modelMapper.map(article, ArticleForMainView.class));
+ }
+
+
+ return articles;
+
+ }
+
+ public Slice getRecentArticles(int page) {
+
+ return articleRepository.findByOrderByCreatedDateDesc(PageRequest.of(page, 5))
+ .map(article -> modelMapper.map(article, ArticleForMainView.class));
+
+ }
+
private Article createNewArticleFrom(NewArticleDto articleDto) {
Member member =
memberRepository.findById(articleDto.getMemberId()).orElseThrow(() -> {
@@ -33,7 +76,13 @@ public class ArticleService {
.title(articleDto.getTitle())
.content(articleDto.getContent())
.toc(articleDto.getToc())
+ .thumbnailUrl(articleDto.getThumbnailUrl())
+ .category(categoryService.findCategory(articleDto.getCategory()))
.member(member)
.build();
}
+
+ /*--------------------------------------------------------------------------------------------*/
+
+
}
diff --git a/src/main/java/myblog/blog/base/config/AppConfig.java b/src/main/java/myblog/blog/base/config/AppConfig.java
new file mode 100644
index 0000000..95ef30e
--- /dev/null
+++ b/src/main/java/myblog/blog/base/config/AppConfig.java
@@ -0,0 +1,13 @@
+package myblog.blog.base.config;
+
+import org.modelmapper.ModelMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class AppConfig {
+
+ @Bean
+ public ModelMapper modelMapper(){ return new ModelMapper();}
+
+}
diff --git a/src/main/java/myblog/blog/base/config/SecurityConfig.java b/src/main/java/myblog/blog/base/config/SecurityConfig.java
index 38d2ef7..7040c47 100644
--- a/src/main/java/myblog/blog/base/config/SecurityConfig.java
+++ b/src/main/java/myblog/blog/base/config/SecurityConfig.java
@@ -10,6 +10,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
@EnableWebSecurity
@@ -43,6 +44,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID","remember-me")
+ .and().csrf()
+ .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
+
.and()
.oauth2Login()
@@ -51,6 +55,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
.userInfoEndpoint()
.userService(oauth2MemberService)
+
+
;
}
}
diff --git a/src/main/java/myblog/blog/category/domain/Category.java b/src/main/java/myblog/blog/category/domain/Category.java
new file mode 100644
index 0000000..0bb2423
--- /dev/null
+++ b/src/main/java/myblog/blog/category/domain/Category.java
@@ -0,0 +1,51 @@
+package myblog.blog.category.domain;
+
+
+import lombok.Builder;
+import lombok.Getter;
+import myblog.blog.article.domain.Article;
+import myblog.blog.base.domain.BasicEntity;
+
+import javax.persistence.*;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@Getter
+@SequenceGenerator(
+ name = "CATEGORY_SEQ_GENERATOR",
+ sequenceName = "CATEGORY_SEQ",
+ initialValue = 1, allocationSize = 50)
+public class Category extends BasicEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CATEGORY_SEQ_GENERATOR")
+ @Column(name = "category_id")
+ private Long id;
+
+ private String title;
+
+ @OneToMany(mappedBy = "category")
+ private List articles = new ArrayList<>();
+
+ @Column(nullable = false)
+ private int tier;
+
+ // 셀프조인
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "parents_id")
+ private Category parents;
+
+ @OneToMany(mappedBy = "parents")
+ private List child = new ArrayList<>();
+
+ @Builder
+ public Category(String title, Category parents, int tier) {
+ this.title = title;
+ this.parents = parents;
+ this.tier = tier;
+ }
+
+ protected Category() {
+ }
+}
diff --git a/src/main/java/myblog/blog/category/dto/CategoryCountForRepository.java b/src/main/java/myblog/blog/category/dto/CategoryCountForRepository.java
new file mode 100644
index 0000000..e7555ea
--- /dev/null
+++ b/src/main/java/myblog/blog/category/dto/CategoryCountForRepository.java
@@ -0,0 +1,14 @@
+package myblog.blog.category.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class CategoryCountForRepository {
+
+ private String title;
+ private int tier;
+ private int count;
+
+}
diff --git a/src/main/java/myblog/blog/category/dto/CategoryForMainView.java b/src/main/java/myblog/blog/category/dto/CategoryForMainView.java
new file mode 100644
index 0000000..1af011f
--- /dev/null
+++ b/src/main/java/myblog/blog/category/dto/CategoryForMainView.java
@@ -0,0 +1,53 @@
+package myblog.blog.category.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Getter
+@Setter
+public class CategoryForMainView {
+
+ private int count;
+ private String title;
+ private List categoryTCountList = new ArrayList<>();
+
+
+ public static CategoryForMainView createCategory(List crList) {
+
+ Collections.reverse(crList);
+ return recursBuilding(0, crList);
+
+ }
+
+ private static CategoryForMainView recursBuilding(int d, List crList) {
+
+ CategoryForMainView categoryForMainView = new CategoryForMainView();
+
+ while (!crList.isEmpty()) {
+ CategoryCountForRepository cSource = crList.get(0);
+
+ if (cSource.getTier() == d) {
+ if(categoryForMainView.getTitle() != null
+ && categoryForMainView.getTitle() != cSource.getTitle()){
+ return categoryForMainView;
+ }
+ categoryForMainView.setTitle(cSource.getTitle());
+ categoryForMainView.setCount(cSource.getCount());
+ crList.remove(0);
+ } else if (cSource.getTier() > d) {
+ CategoryForMainView sub = recursBuilding(d + 1, crList);
+ categoryForMainView.getCategoryTCountList().add(sub);
+ } else {
+ return categoryForMainView;
+ }
+
+ }
+ return categoryForMainView;
+ }
+
+
+}
diff --git a/src/main/java/myblog/blog/category/repository/CategoryRepository.java b/src/main/java/myblog/blog/category/repository/CategoryRepository.java
new file mode 100644
index 0000000..5a131d2
--- /dev/null
+++ b/src/main/java/myblog/blog/category/repository/CategoryRepository.java
@@ -0,0 +1,11 @@
+package myblog.blog.category.repository;
+
+import myblog.blog.category.domain.Category;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface CategoryRepository extends JpaRepository {
+
+ Category findByTitle(String title);
+
+
+}
diff --git a/src/main/java/myblog/blog/category/repository/NaCategoryRepository.java b/src/main/java/myblog/blog/category/repository/NaCategoryRepository.java
new file mode 100644
index 0000000..b0d5ce1
--- /dev/null
+++ b/src/main/java/myblog/blog/category/repository/NaCategoryRepository.java
@@ -0,0 +1,27 @@
+package myblog.blog.category.repository;
+
+import myblog.blog.category.dto.CategoryCountForRepository;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Mapper
+@Repository
+public interface NaCategoryRepository {
+
+ @Select("select ifnull(f.title,'total') as title,ifnull(tier,0) as tier, count\n" +
+ "from \n" +
+ "(select ifnull(child,parent) as title, count\n" +
+ "from\n" +
+ "(select c.title 'parent', b.title as 'child' , count(*) as 'count'\n" +
+ "from article a\n" +
+ "join category b on (a.category_id = b.category_id)\n" +
+ "left join category c on (b.parents_id = c.category_id)\n" +
+ "group by parent, child with rollup) d\n" +
+ ") e\n" +
+ "left join category f on (e.title = f.title)")
+ List getCategoryCount();
+
+}
diff --git a/src/main/java/myblog/blog/category/service/CategoryService.java b/src/main/java/myblog/blog/category/service/CategoryService.java
new file mode 100644
index 0000000..aa6d8f5
--- /dev/null
+++ b/src/main/java/myblog/blog/category/service/CategoryService.java
@@ -0,0 +1,47 @@
+package myblog.blog.category.service;
+
+import lombok.RequiredArgsConstructor;
+import myblog.blog.category.domain.Category;
+import myblog.blog.category.dto.CategoryCountForRepository;
+import myblog.blog.category.dto.CategoryForMainView;
+import myblog.blog.category.repository.CategoryRepository;
+import myblog.blog.category.repository.NaCategoryRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class CategoryService {
+
+ private final CategoryRepository categoryRepository;
+ private final NaCategoryRepository naCategoryRepository;
+
+ public Long createNewCategory(String title, String parent) {
+
+ Category parentCategory = null;
+ if (parent != null) {
+ parentCategory = categoryRepository.findByTitle(parent);
+ }
+
+ Category category = Category.builder()
+ .title(title)
+ .parents(parentCategory)
+ .build();
+
+ return category.getId();
+ }
+
+ public Category findCategory(String title){
+ return categoryRepository.findByTitle(title);
+ }
+
+ public CategoryForMainView getCategoryForView(){
+
+ List categoryCount = naCategoryRepository.getCategoryCount();
+
+ return CategoryForMainView.createCategory(categoryCount);
+
+ }
+
+}
diff --git a/src/main/java/myblog/blog/img/controller/UploadImgController.java b/src/main/java/myblog/blog/img/controller/UploadImgController.java
new file mode 100644
index 0000000..979d841
--- /dev/null
+++ b/src/main/java/myblog/blog/img/controller/UploadImgController.java
@@ -0,0 +1,28 @@
+package myblog.blog.img.controller;
+
+import lombok.RequiredArgsConstructor;
+import myblog.blog.img.domain.UploadedImg;
+import myblog.blog.img.dto.UploadImgDto;
+import myblog.blog.img.service.UploadImgService;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.IOException;
+
+@RestController
+@RequiredArgsConstructor
+public class UploadImgController {
+
+ private final UploadImgService uploadImgService;
+
+ @PostMapping("/article/uploadImg")
+ public @ResponseBody
+ String imgUpload(@ModelAttribute UploadImgDto uploadImgDto) throws IOException {
+
+ return uploadImgService.storeImg(uploadImgDto.getImg());
+
+ }
+
+}
diff --git a/src/main/java/myblog/blog/img/domain/UploadedImg.java b/src/main/java/myblog/blog/img/domain/UploadedImg.java
index 5c6f431..4e5cc31 100644
--- a/src/main/java/myblog/blog/img/domain/UploadedImg.java
+++ b/src/main/java/myblog/blog/img/domain/UploadedImg.java
@@ -4,7 +4,6 @@ import lombok.Getter;
import lombok.Setter;
@Getter
-@Setter
public class UploadedImg {
private String uploadFileName;
diff --git a/src/main/java/myblog/blog/img/dto/UploadImgDto.java b/src/main/java/myblog/blog/img/dto/UploadImgDto.java
new file mode 100644
index 0000000..d77e900
--- /dev/null
+++ b/src/main/java/myblog/blog/img/dto/UploadImgDto.java
@@ -0,0 +1,13 @@
+package myblog.blog.img.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.web.multipart.MultipartFile;
+
+@Getter
+@Setter
+public class UploadImgDto {
+
+ private MultipartFile img;
+
+}
diff --git a/src/main/java/myblog/blog/img/service/ImgService.java b/src/main/java/myblog/blog/img/service/UploadImgService.java
similarity index 85%
rename from src/main/java/myblog/blog/img/service/ImgService.java
rename to src/main/java/myblog/blog/img/service/UploadImgService.java
index d098cd1..69ed90d 100644
--- a/src/main/java/myblog/blog/img/service/ImgService.java
+++ b/src/main/java/myblog/blog/img/service/UploadImgService.java
@@ -15,19 +15,18 @@ import java.util.UUID;
@Service
@RequiredArgsConstructor
-public class ImgService {
+public class UploadImgService {
@Value("${git.gitToken}")
private String gitToken;
- @Value("${git.imgRepo}")
+ @Value("${git.imgRepo}")
private String gitRepo;
+
@Value("${git.imgUrl}")
private String imgUrl;
- private final ModelMapper modelMapper;
-
- public UploadedImg storeImg(MultipartFile multipartFile) throws IOException {
+ public String storeImg(MultipartFile multipartFile) throws IOException {
if (multipartFile.isEmpty()) {
throw new IllegalArgumentException("이미지가 존재하지 않습니다.");
}
@@ -41,7 +40,9 @@ public class ImgService {
repository.createContent().path("img/"+storeFileName)
.content(multipartFile.getBytes()).message("test").branch("main").commit();
- return new UploadedImg(originalFilename, storeFileName, imgUrl +storeFileName+"?raw=true");
+ UploadedImg uploadedImg = new UploadedImg(originalFilename, storeFileName, imgUrl + storeFileName + "?raw=true");
+
+ return uploadedImg.getUploadUrl();
}
diff --git a/src/main/java/myblog/blog/main/MainController.java b/src/main/java/myblog/blog/main/MainController.java
index 4b3a13f..51f49ba 100644
--- a/src/main/java/myblog/blog/main/MainController.java
+++ b/src/main/java/myblog/blog/main/MainController.java
@@ -1,18 +1,41 @@
package myblog.blog.main;
import lombok.RequiredArgsConstructor;
+import myblog.blog.article.domain.Article;
+import myblog.blog.article.dto.ArticleForMainView;
+import myblog.blog.article.service.ArticleService;
+import myblog.blog.category.dto.CategoryForMainView;
+import myblog.blog.category.service.CategoryService;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
+import java.util.List;
+
@Controller
@RequiredArgsConstructor
public class MainController {
+ private final ArticleService articleService;
+ private final CategoryService categoryService;
+
@GetMapping("/")
- public String main() {
+ public String main(Model model) {
+
+ List popularArticles = articleService.getPopularArticles();
+ Slice recentArticles = articleService.getRecentArticles(0);
+ CategoryForMainView categoryForView = categoryService.getCategoryForView();
+
+ model.addAttribute("category",categoryForView);
+ model.addAttribute("popularArticles", popularArticles);
+ model.addAttribute("recentArticles",recentArticles);
return "main";
}
+
+
}
diff --git a/src/main/java/myblog/blog/member/controller/MemberController.java b/src/main/java/myblog/blog/member/controller/MemberController.java
index 79eb4eb..8a667e9 100644
--- a/src/main/java/myblog/blog/member/controller/MemberController.java
+++ b/src/main/java/myblog/blog/member/controller/MemberController.java
@@ -1,13 +1,19 @@
package myblog.blog.member.controller;
+import lombok.RequiredArgsConstructor;
+import myblog.blog.category.dto.CategoryForMainView;
+import myblog.blog.category.service.CategoryService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
+@RequiredArgsConstructor
public class MemberController {
+ private final CategoryService categoryService;
+
@GetMapping("/login")
public String loginFrom(@RequestParam(value = "error",required = false) String error, Model model){
@@ -15,6 +21,11 @@ public class MemberController {
model.addAttribute("errMsg","이미 가입된 이메일입니다.");
}
+ CategoryForMainView categoryForView = categoryService.getCategoryForView();
+
+ model.addAttribute("category",categoryForView);
+
+
return "login";
}
diff --git a/src/main/java/myblog/blog/member/repository/MemberRepository.java b/src/main/java/myblog/blog/member/repository/MemberRepository.java
index 5cfc115..c6dfa4f 100644
--- a/src/main/java/myblog/blog/member/repository/MemberRepository.java
+++ b/src/main/java/myblog/blog/member/repository/MemberRepository.java
@@ -4,7 +4,6 @@ import myblog.blog.member.doamin.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
-@Repository
public interface MemberRepository extends JpaRepository {
Member findByUserId(String userId);
diff --git a/src/main/java/myblog/blog/member/service/Oauth2MemberService.java b/src/main/java/myblog/blog/member/service/Oauth2MemberService.java
index 4d84996..856e2ed 100644
--- a/src/main/java/myblog/blog/member/service/Oauth2MemberService.java
+++ b/src/main/java/myblog/blog/member/service/Oauth2MemberService.java
@@ -77,7 +77,6 @@ public class Oauth2MemberService extends DefaultOAuth2UserService {
return member;
}
- @PostConstruct
public void insertAdmin(){
Member admin = memberRepository.findByEmail(adminEmail);
diff --git a/src/main/java/myblog/blog/tags/domain/ArticleTagList.java b/src/main/java/myblog/blog/tags/domain/ArticleTagList.java
new file mode 100644
index 0000000..de21eac
--- /dev/null
+++ b/src/main/java/myblog/blog/tags/domain/ArticleTagList.java
@@ -0,0 +1,37 @@
+package myblog.blog.tags.domain;
+
+import lombok.Builder;
+import myblog.blog.article.domain.Article;
+import myblog.blog.base.domain.BasicEntity;
+
+import javax.persistence.*;
+
+@Entity
+@SequenceGenerator(
+ name = "ARTICLE_TAG_LIST_SEQ_GENERATOR",
+ sequenceName = "ARTICLE_TAG_LIST_SEQ",
+ initialValue = 1, allocationSize = 50)
+public class ArticleTagList extends BasicEntity {
+ @Id
+ @Column(name = "article_tag_list_id")
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ARTICLE_TAG_LIST_SEQ_GENERATOR")
+ private Long id;
+
+ @ManyToOne
+ @JoinColumn(name = "article_id")
+ private Article article;
+
+ @ManyToOne
+ @JoinColumn(name = "tags_id")
+ private Tags tags;
+
+ @Builder
+ public ArticleTagList(Article article, Tags tags) {
+ this.article = article;
+ this.tags = tags;
+ }
+
+ protected ArticleTagList() {
+
+ }
+}
diff --git a/src/main/java/myblog/blog/tags/domain/Tags.java b/src/main/java/myblog/blog/tags/domain/Tags.java
new file mode 100644
index 0000000..1e5a7a9
--- /dev/null
+++ b/src/main/java/myblog/blog/tags/domain/Tags.java
@@ -0,0 +1,36 @@
+package myblog.blog.tags.domain;
+
+import lombok.Builder;
+import lombok.Getter;
+import myblog.blog.base.domain.BasicEntity;
+
+import javax.persistence.*;
+import java.util.ArrayList;
+import java.util.List;
+
+@Entity
+@SequenceGenerator(
+ name = "TAGS_SEQ_GENERATOR",
+ sequenceName = "TAGS_SEQ",
+ initialValue = 1, allocationSize = 50)
+@Getter
+public class Tags extends BasicEntity {
+
+ @Id
+ @Column(name = "tags_id")
+ @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "TAGS_SEQ_GENERATOR")
+ private Long id;
+ @Column(unique = true, nullable = false)
+ private String name;
+ @OneToMany(mappedBy = "tags")
+ private List articleTagLists = new ArrayList<>();
+
+ @Builder
+ public Tags(String name) {
+ this.name = name;
+ }
+
+ protected Tags() {
+
+ }
+}
diff --git a/src/main/java/myblog/blog/tags/dto/TagsDto.java b/src/main/java/myblog/blog/tags/dto/TagsDto.java
new file mode 100644
index 0000000..cfd1677
--- /dev/null
+++ b/src/main/java/myblog/blog/tags/dto/TagsDto.java
@@ -0,0 +1,14 @@
+package myblog.blog.tags.dto;
+
+import lombok.Data;
+
+@Data
+public class TagsDto {
+
+ private String value;
+
+ @Override
+ public String toString() {
+ return "{ value : " + value + "}";
+ }
+}
diff --git a/src/main/java/myblog/blog/tags/repository/ArticleTagListsRepository.java b/src/main/java/myblog/blog/tags/repository/ArticleTagListsRepository.java
new file mode 100644
index 0000000..128b655
--- /dev/null
+++ b/src/main/java/myblog/blog/tags/repository/ArticleTagListsRepository.java
@@ -0,0 +1,7 @@
+package myblog.blog.tags.repository;
+
+import myblog.blog.tags.domain.ArticleTagList;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface ArticleTagListsRepository extends JpaRepository {
+}
diff --git a/src/main/java/myblog/blog/tags/repository/TagsRepository.java b/src/main/java/myblog/blog/tags/repository/TagsRepository.java
new file mode 100644
index 0000000..4939471
--- /dev/null
+++ b/src/main/java/myblog/blog/tags/repository/TagsRepository.java
@@ -0,0 +1,10 @@
+package myblog.blog.tags.repository;
+
+import myblog.blog.tags.domain.Tags;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface TagsRepository extends JpaRepository {
+
+ Tags findByName(String name);
+
+}
diff --git a/src/main/java/myblog/blog/tags/service/TagsService.java b/src/main/java/myblog/blog/tags/service/TagsService.java
new file mode 100644
index 0000000..ac073e7
--- /dev/null
+++ b/src/main/java/myblog/blog/tags/service/TagsService.java
@@ -0,0 +1,49 @@
+package myblog.blog.tags.service;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.RequiredArgsConstructor;
+import myblog.blog.article.domain.Article;
+import myblog.blog.article.repository.ArticleRepository;
+import myblog.blog.tags.domain.ArticleTagList;
+import myblog.blog.tags.domain.Tags;
+import myblog.blog.tags.dto.TagsDto;
+import myblog.blog.tags.repository.ArticleTagListsRepository;
+import myblog.blog.tags.repository.TagsRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class TagsService {
+
+ private final TagsRepository tagsRepository;
+ private final ArticleTagListsRepository articleTagListsRepository;
+
+ public void createNewTagsAndArticleTagList(String names, Article article) {
+
+ Gson gson = new Gson();
+ ArrayList tagsDtoArrayList = gson.fromJson(names, ArrayList.class);
+
+ for (Map tags : tagsDtoArrayList) {
+
+ Tags tag = tagsRepository.findByName(tags.get("value").toString());
+ if (tag == null) {
+ tag = tagsRepository.save(Tags.builder().name(tags.get("value").toString()).build());
+
+ }
+ articleTagListsRepository.save(ArticleTagList.builder()
+ .article(article)
+ .tags(tag)
+ .build());
+ }
+
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 7b491ac..e8f4602 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -15,7 +15,7 @@ server:
jpa:
hibernate:
- ddl-auto: create
+ ddl-auto:
properties:
hibernate:
# show_sql: true
diff --git a/src/main/resources/static/css/articleWrite.css b/src/main/resources/static/css/articleWrite.css
new file mode 100644
index 0000000..e7ec65e
--- /dev/null
+++ b/src/main/resources/static/css/articleWrite.css
@@ -0,0 +1,20 @@
+.thumbBox{
+ position: relative;
+ display: none;
+}
+
+.thumbDelBtn{
+ position: absolute;
+ font-size: 60vh;
+ background: transparent;
+ border: transparent;
+ visibility: hidden;
+}
+
+.thumbBox:hover .thumbDelBtn{
+ visibility: visible;
+}
+
+.thumbBox:hover .thumbImg{
+ opacity: 0.5;
+}
\ No newline at end of file
diff --git a/src/main/resources/static/css/mainCss.css b/src/main/resources/static/css/mainCss.css
index 3998ec6..fe0e7eb 100644
--- a/src/main/resources/static/css/mainCss.css
+++ b/src/main/resources/static/css/mainCss.css
@@ -205,6 +205,7 @@ body::-webkit-scrollbar-track {
}
.arrow-up {
+ visibility: hidden;
position: fixed;
bottom: 50px;
right: 50px;
diff --git a/src/main/resources/static/js/arrow.js b/src/main/resources/static/js/arrow.js
new file mode 100644
index 0000000..e524a0f
--- /dev/null
+++ b/src/main/resources/static/js/arrow.js
@@ -0,0 +1,14 @@
+const arrow = document.getElementById("arrow");
+
+arrow.addEventListener('click', () => {
+ window.scrollTo(0, 0);
+})
+
+window.addEventListener("scroll", () => {
+
+ if (scrollY != 0) {
+ arrow.style.visibility = 'visible';
+ } else {
+ arrow.style.visibility = 'hidden';
+ }
+}, {passive: true});
diff --git a/src/main/resources/static/js/editor.js b/src/main/resources/static/js/editor.js
index 05ceb15..7d8e67f 100644
--- a/src/main/resources/static/js/editor.js
+++ b/src/main/resources/static/js/editor.js
@@ -89,14 +89,13 @@ const editorMobile = new toastui.Editor({
editor.setMarkdown(contents.value);
editorMobile.setMarkdown(contents.value);
-
function uploadImage(blob) {
let token = getCsrfToken();
let formData = new FormData();
formData.append('img', blob);
const xhr = new XMLHttpRequest();
- xhr.open("POST", "/board/uploadImg", false);
+ xhr.open("POST", "/article/uploadImg", false);
xhr.setRequestHeader("contentType", "multipart/form-data");
xhr.setRequestHeader("X-XSRF-TOKEN", token);
xhr.send(formData);
diff --git a/src/main/resources/static/js/getCsrf.js b/src/main/resources/static/js/getCsrf.js
new file mode 100644
index 0000000..f0bd19f
--- /dev/null
+++ b/src/main/resources/static/js/getCsrf.js
@@ -0,0 +1,10 @@
+
+function getCookie(str) {
+ const value = document.cookie.match('(^|;) ?' + str + '=([^;]*)(;|$)');
+ return value ? value[2] : null;
+}
+
+function getCsrfToken() {
+ let token = getCookie("XSRF-TOKEN");
+ return token;
+}
diff --git a/src/main/resources/static/js/infinityScroll.js b/src/main/resources/static/js/infinityScroll.js
new file mode 100644
index 0000000..8b0a8e7
--- /dev/null
+++ b/src/main/resources/static/js/infinityScroll.js
@@ -0,0 +1,80 @@
+let pageNum = 1;
+let flag = false;
+const container = document.getElementById("infiniteScrollBox");
+
+function InfinityScroll() {
+ const next = document.getElementById("nextPagination");
+ const screenHeight = screen.height;
+
+ document.addEventListener('scroll', OnScroll, {passive: true})
+
+ function OnScroll() {
+ const fullHeight = container.clientHeight;
+ const scrollPosition = pageYOffset;
+ if (fullHeight - screenHeight / 2 <= scrollPosition && !flag) {
+ flag = true;
+
+ makeNextPage();
+
+ }
+ }
+
+}
+
+function makeNextPage() {
+ const xhr = new XMLHttpRequest();
+ // 페이지 요청보내기
+ xhr.open('GET', "/main/article/" + pageNum);
+ // xhr.setRequestHeader("X-XSRF-TOKEN", token);
+ xhr.send();
+
+ xhr.onload = () => {
+
+ if (xhr.readyState === xhr.DONE) {
+ if (xhr.status === 200 || xhr.status === 201 || xhr.status === 202) {
+
+
+ let list = JSON.parse(xhr.response);
+
+ console.log(list);
+
+ // 다음 페이지 작성
+ const nextPage = document.createElement('div');
+ nextPage.setAttribute("id", "articlePage-" + pageNum++);
+ // 아티클 개별추가
+ for (let listElement of list) {
+ let article = document.createElement('div');
+ let date = moment(listElement.createdDate).format('YYYY-MM-DD');
+
+ let articleHtmlSource = ' ';
+ articleHtmlSource +=
+ ""
+
+ article.innerHTML = articleHtmlSource;
+ nextPage.appendChild(article);
+ }
+
+ container.appendChild(nextPage);
+
+ flag = false;
+ } else {
+ console.error(xhr.response);
+ }
+ }
+ }
+}
+
+InfinityScroll()
diff --git a/src/main/resources/static/js/tags.js b/src/main/resources/static/js/tags.js
new file mode 100644
index 0000000..0031359
--- /dev/null
+++ b/src/main/resources/static/js/tags.js
@@ -0,0 +1,14 @@
+const input = document.querySelector('input[name="tags"]');
+
+var whitelist = ["Spring","Java","Spring Boot"];
+
+const tagify = new Tagify(input, {
+ whitelist:whitelist,
+ maxTags: 10,
+ dropdown: {
+ maxItems: 20,
+ classname: "tags-look",
+ enabled: 0,
+ closeOnSelect: false
+ }
+})
\ No newline at end of file
diff --git a/src/main/resources/static/js/thumbnail.js b/src/main/resources/static/js/thumbnail.js
new file mode 100644
index 0000000..a1a5cf0
--- /dev/null
+++ b/src/main/resources/static/js/thumbnail.js
@@ -0,0 +1,46 @@
+const thumbBox = document.getElementById("thumbBox");
+const uploadThumbBtn = document.getElementById("thumbnail");
+const thumbDel = document.getElementById("thumbDelBtn");
+const previewThumb = document.getElementById("thumbnailPreView");
+const thumbUrl = document.getElementById("thumbnailUrl")
+
+function uploadImg(input) {
+
+ if(input.files && input.files[0]) {
+
+ let token = getCsrfToken();
+ let formData = new FormData();
+ formData.append('img', input.files[0]);
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("POST", "/article/uploadImg", false);
+ xhr.setRequestHeader("contentType", "multipart/form-data");
+ xhr.setRequestHeader("X-XSRF-TOKEN", token);
+ xhr.send(formData);
+
+ if (xhr.readyState === 4 && xhr.status === 200) {
+
+ thumbUrl.value = xhr.response;
+
+ const reader = new FileReader();
+ reader.onload = e => {
+ previewThumb.src = e.target.result;
+ }
+ reader.readAsDataURL(input.files[0])
+ thumbBox.style.display = ''
+
+ } else {
+ alert("이미지가 정상적으로 업로드되지 못했습니다.")
+ }
+
+ }
+}
+
+uploadThumbBtn.addEventListener("change", e => {
+ uploadImg(e.target);
+})
+
+thumbDel.addEventListener("click", () =>{
+ thumbBox.style.display = 'none';
+ thumbUrl.value = "";
+})
\ No newline at end of file
diff --git a/src/main/resources/static/package-lock.json b/src/main/resources/static/package-lock.json
index 1cfc7fd..eb57603 100644
--- a/src/main/resources/static/package-lock.json
+++ b/src/main/resources/static/package-lock.json
@@ -19,6 +19,11 @@
"prosemirror-view": "^1.18.7"
}
},
+ "@yaireo/tagify": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/@yaireo/tagify/-/tagify-4.8.1.tgz",
+ "integrity": "sha512-SK3mN6HPzNzW3rgolvdsv/3m8OX5xdbh6H8oz/fjdLCa0Qt3JoaAQqjsMxaLG575+ywl5jpvsaIj+JB6yi9LUQ=="
+ },
"bootstrap": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz",
diff --git a/src/main/resources/static/package.json b/src/main/resources/static/package.json
index 1815dd1..0f98a43 100644
--- a/src/main/resources/static/package.json
+++ b/src/main/resources/static/package.json
@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@toast-ui/editor": "^3.1.1",
+ "@yaireo/tagify": "^4.8.1",
"bootstrap": "^5.1.3"
}
}
diff --git a/src/main/resources/templates/articleWriteForm.html b/src/main/resources/templates/article/articleWriteForm.html
similarity index 56%
rename from src/main/resources/templates/articleWriteForm.html
rename to src/main/resources/templates/article/articleWriteForm.html
index 0ebdbfd..2d9edc7 100644
--- a/src/main/resources/templates/articleWriteForm.html
+++ b/src/main/resources/templates/article/articleWriteForm.html
@@ -38,7 +38,9 @@
+
+
@@ -47,38 +49,69 @@
-
+
+
+
+
+
+
+
+