211110 메인화면 개발중
1. 메인화면에 필요한 게시물 리스트 조회 로직과 화면 렌더링 구현 2. 조회수순으로 메인화면 노출과 최신 업로드 순 게시물 노출 로직 구분 3. 무한스크롤 구현 4. 스크롤 화살표 구현 5. 계층형 카테고리 개발과 화면 렌더링 완료 - 롤업함수와 백트래킹으로 구현
This commit is contained in:
@@ -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 {
|
||||
|
||||
19
node_modules/@yaireo/tagify/LICENSE
generated
vendored
Normal file
19
node_modules/@yaireo/tagify/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright 2019 Yair Even-Or
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
995
node_modules/@yaireo/tagify/README.md
generated
vendored
Normal file
995
node_modules/@yaireo/tagify/README.md
generated
vendored
Normal file
@@ -0,0 +1,995 @@
|
||||
<h1 align="center">
|
||||
<a href='https://yaireo.github.io/tagify'><img src="/docs/readme-header.svg" width="320" height="160"><a/>
|
||||
<br><br>
|
||||
<a href='https://yaireo.github.io/tagify'>Tagify</a> - <em>tags</em> input component
|
||||
</h1>
|
||||
|
||||
<p align="center">
|
||||
Transforms an input field or a textarea into a <em>Tags component</em>, in an easy, customizable way,
|
||||
with great performance and small code footprint, exploded with features.
|
||||
<br>
|
||||
<strong>Vanilla</strong> ⚡ <strong>React</strong> ⚡ <strong>Vue</strong> ⚡ <strong>Angular</strong>
|
||||
<p>
|
||||
|
||||
<h3 align="center">
|
||||
👉 <a href="https://yaireo.github.io/tagify">See Demos</a> 👈
|
||||
</h3>
|
||||
|
||||
<p align="center">
|
||||
<a href='https://www.npmjs.com/package/@yaireo/tagify'>
|
||||
<img src="https://img.shields.io/npm/v/@yaireo/tagify.svg" />
|
||||
</a>
|
||||
<a href='https://simple.wikipedia.org/wiki/MIT_License'>
|
||||
<img src="https://img.shields.io/badge/license-MIT-lightgrey" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/@yaireo/tagify" />
|
||||
<img src="https://img.shields.io/npm/dw/@yaireo/tagify" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="/docs/mix3.gif?sanitize=true" />
|
||||
<img src="/docs/demo.gif?sanitize=true" />
|
||||
</p>
|
||||
|
||||
## Table of Contents
|
||||
|
||||
<!--ts-->
|
||||
- [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)
|
||||
<!--te-->
|
||||
|
||||
## 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
|
||||
<script src="https://unpkg.com/@yaireo/tagify"></script>
|
||||
<script src="https://unpkg.com/@yaireo/tagify/dist/tagify.polyfills.min.js"></script>
|
||||
<link href="https://unpkg.com/@yaireo/tagify/dist/tagify.css" rel="stylesheet" type="text/css" />
|
||||
```
|
||||
|
||||
`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 `<select>`)
|
||||
* Supports whitelist/blacklist
|
||||
* Supports Templates for: <em>component wrapper</em>, <em>tag items</em>, <em>suggestion list</em> & <em>suggestion items</em>
|
||||
* Shows suggestions 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)
|
||||
* <del>ARIA accessibility support</del>(Component too generic for any meaningful ARIA)
|
||||
* Supports read-only mode to the whole componenet or per-tag
|
||||
* Each tag can have any properties desired (class, data-whatever, readonly...)
|
||||
* Automatically disallow duplicate tags (vis "settings" object)
|
||||
* Has built-in CSS loader, if needed (Ex. <em>AJAX</em> whitelist pulling)
|
||||
* Tags can be trimmed via `hellip` by giving `max-width` to the `tag` element in your `CSS`
|
||||
* Easily change direction to RTL (via the SCSS file)
|
||||
* <del>Internet Explorer - A polyfill script should be used: `tagify.polyfills.min.js` (in `/dist`)</del> ***(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:<br>
|
||||
`'[{"value":"cat"}, {"value":"dog"}]'`
|
||||
|
||||
I **recommend** keeping this because some situations might have values such as addresses (tags contain commas):<br>
|
||||
`'[{"value":"Apt. 2A, Jacksonville, FL 39404"}, {"value":"Forrest Ray, 191-103 Integer Rd., Corona New Mexico"}]'`
|
||||
|
||||
Another example for complex tags state might be disabled tags, or ones with custom identifier *class*:<br>
|
||||
*(tags can be clicked, so delevopers can choose to use this to disable/enable tags)*<br>
|
||||
`'[{"value":"cat", "disabled":true}, {"value":"dog"}, {"value":"bird", "class":"color-green"}]'`
|
||||
|
||||
To change the format, assuming your tags have no commas and are fairly simple:
|
||||
|
||||
```js
|
||||
var tagify = new Tagify(inputElm, {
|
||||
originalInputValueFormat: valuesArr => valuesArr.map(item => item.value).join(',')
|
||||
})
|
||||
```
|
||||
|
||||
**Output:**<br>
|
||||
`"cat,dog"`
|
||||
|
||||
|
||||
## Ajax whitelist
|
||||
Dynamically-loaded suggestions list (*whitelist*) from the server (as the user types) is a frequent need to many.
|
||||
|
||||
Tagify comes with its own loading animation, which is a very lightweight CSS-only code, and the <em>loading</em>
|
||||
state is controlled by the method `tagify.loading` which accepts `true` or `false` as arguments.
|
||||
|
||||
Below is a basic example using the `fetch` API. I advise to abort the last request on any input before starting a new request.
|
||||
|
||||
<details>
|
||||
<summary>Example:</summary>
|
||||
|
||||
```javascript
|
||||
var input = document.querySelector('input'),
|
||||
tagify = new Tagify(input, {whitelist:[]}),
|
||||
controller; // for aborting the call
|
||||
|
||||
// listen to any keystrokes which modify tagify's input
|
||||
tagify.on('input', onInput)
|
||||
|
||||
function onInput( e ){
|
||||
var value = e.detail.value
|
||||
tagify.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
|
||||
})
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Edit tags
|
||||
Tags which aren't `read-only` can be edited by double-clicking them (by default)
|
||||
or by changing the `editTags` *setting* to `1`, making tags editable by single-clicking them.
|
||||
|
||||
The value is saved on `blur` or by pressing `enter` key. Pressing `Escape` will revert the change trigger `blur`.
|
||||
<kbd>ctrl</kbd><kbd>z</kbd> will revert the change if an edited tag was marked as not valid (perhaps duplicate or blacklisted)
|
||||
|
||||
To prevent *all* tags from being allowed to be editable, set the `editTags` setting to `false` (or `null`).<br>
|
||||
To do the same but for specific tag(s), set those tags' data with `editable` property set to `false`:
|
||||
|
||||
```html
|
||||
<input value='[{"value":"foo", "editable":false}, {"value":"bar"}]'>
|
||||
```
|
||||
|
||||
|
||||
## 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 `<tag title="${(tagData.title || tagData.value)}"
|
||||
contenteditable='false'
|
||||
spellcheck='false'
|
||||
tabIndex="${this.settings.a11y.focusableTags ? 0 : -1}"
|
||||
class="${this.settings.classNames.tag} ${tagData.class ? tagData.class : ""}"
|
||||
${this.getAttributes(tagData)}>
|
||||
<x title='' class="${this.settings.classNames.tagX}" role='button' aria-label='remove tag'></x>
|
||||
<div>
|
||||
<span class="${this.settings.classNames.tagText}">${tagData[this.settings.tagTextProp] || tagData.value}</span>
|
||||
</div>
|
||||
</tag>`
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Suggestions list
|
||||
|
||||
<p align="center">
|
||||
<img src="/docs/suggestions-list.apng" alt='suggestions list dropdown'/>
|
||||
</p>
|
||||
|
||||
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 `<body>` element and will be rendered by default in a position below (bottom of) the Tagify element.
|
||||
Using the keyboard arrows up/down will highlight an option from the list, and hitting the Enter key to select.
|
||||
|
||||
It is possible to tweak the 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
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
<p align="center"><b>Will render</b></p>
|
||||
|
||||
```html
|
||||
<div class="tagify__dropdown tagify__dropdown--text" style="left:993.5px; top:106.375px; width:616px;">
|
||||
<div class="tagify__dropdown__wrapper">
|
||||
<div class="tagify__dropdown__item tagify__dropdown__item--active" value="aaab">aaab</div>
|
||||
<div class="tagify__dropdown__item" value="aaabb">aaabb</div>
|
||||
<div class="tagify__dropdown__item" value="aaabc">aaabc</div>
|
||||
<div class="tagify__dropdown__item" value="aaabd">aaabd</div>
|
||||
<div class="tagify__dropdown__item" value="aaabe">aaabe</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
By default searching the suggestions is using [fuzzy-search](https://en.wikipedia.org/wiki/Approximate_string_matching) (see [settings](#settings)).
|
||||
|
||||
If you wish to assign *alias* to items (in your suggestion list), add the `searchBy` property to *whitelist* items you wish
|
||||
to have an *alias* for.
|
||||
|
||||
In the below example, typing a part of a string which is included in the `searchBy` property, for example *`land midd"`* -
|
||||
the suggested item which match the value "Israel" will be rendered in the suggestions (dropdown) list.
|
||||
|
||||
### [Example](https://yaireo.github.io/tagify/#section-extra-properties) for a suggestion item alias
|
||||
|
||||
```javascript
|
||||
whitelist = [
|
||||
...
|
||||
{ value:'Israel', code:'IL', searchBy:'holy land, desert, middle east' },
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
Another handy setting is `dropdown.searchKeys` which, like the above `dropdown.searchBy` setting, allows
|
||||
expanding the search of any typed terms to more than the `value` property of the whitelist items (if items are a *Collection*).
|
||||
|
||||
### Example whitelist:
|
||||
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
value : 123456,
|
||||
nickname : "foo",
|
||||
email : "foo@mail.com"
|
||||
},
|
||||
{
|
||||
value : 987654,
|
||||
nickname : "bar",
|
||||
email : "bar@mail.com"
|
||||
},
|
||||
...more..
|
||||
]
|
||||
```
|
||||
|
||||
// setting to search in other keys:
|
||||
```javascript
|
||||
{
|
||||
dropdown: {
|
||||
searchKeys: ["nickname", "email"] // fuzzy-search matching for those whitelist items' properties
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Mixed-Content
|
||||
|
||||
[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 `<Select>` 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 (
|
||||
<Tags
|
||||
tagifyRef={tagifyRef} // optional Ref object for the Tagify instance itself, to get access to inner-methods
|
||||
settings={settings} // tagify settings object
|
||||
defaultValue="a,b,c"
|
||||
{...tagifyProps} // dynamic props such as "loading", "showDropdown:'abc'", "value"
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
})
|
||||
```
|
||||
|
||||
To gain full access to Tagify's (instance) inner methods, A custom `ref` can be used:
|
||||
|
||||
```jsx
|
||||
...
|
||||
const tagifyRef = useRef()
|
||||
...
|
||||
<Tags tagifyRef={tagifyRef} ... />
|
||||
|
||||
// or mix-mode
|
||||
<MixedTags
|
||||
settings={...}
|
||||
onChange={...}
|
||||
defaultValue={`This is a textarea which mixes text with [[{"value":"tags"}]].`}
|
||||
/>
|
||||
```
|
||||
|
||||
`<MixedTags>` component is a shorthand for `<Tags InputMode="textarea">`
|
||||
|
||||
#### Updating the component's state
|
||||
|
||||
The `settings` prop is **only used once** in the initialization process, please do not update it afterwards.
|
||||
|
||||
---
|
||||
<details>
|
||||
<summary>📖 List of (React) props for the <code><Tags/></code> component</summary>
|
||||
|
||||
|
||||
Prop | Type | Updatable | Info
|
||||
----------------------- | ------------------------- |:---------:| -----------------------------------------------------------
|
||||
settings | <sub>Object</sub> | | See [*settings* section](#settings)
|
||||
name | <sub>String</sub> | ✔ | `<input>`'s element `name` attribute
|
||||
value | <sub>String/Array</sub> | ✔ | Initial value.
|
||||
defaultValue | <sub>String/Array</sub> | | Same as `value prop
|
||||
placeholder | <sub>String</sub> | ✔ | placeholder text for the component
|
||||
readOnly | <sub>Boolean</sub> | ✔ | Toggles `readonly` state. With capital `O`.
|
||||
tagifyRef | <sub>Object</sub> | | `useRef` hook refference for the component inner instance of vailla *Tagify* (for methods access)
|
||||
showFilteredDropdown | <sub>Boolean/String</sub> | ✔ | if `true` shows the suggestions dropdown. if assigned a String, show the dropdown pre-filtered.
|
||||
loading | <sub>Boolean</sub> | ✔ | Toggles `loading` state for the whole component
|
||||
whitelist | <sub>Array</sub> | ✔ | Sets the `whitelist` which is the basis for the suggestions dropdown & autocomplete
|
||||
className | <sub>String</sub> | | Component's optional class name to be added
|
||||
InputMode | <sub>String</sub> | | `"textarea"` will create a `<textarea>` (hidden) element instead of the default `<input>` and automatically make Tagify act as [*"mix mode"*](#mixed-content)
|
||||
autoFocus | <sub>Boolean</sub> | | Should the component have focus on mount. Must be unique, per-page.
|
||||
children | <sub>String/Array</sub> | | `value`/`defaultValue` props are prefered
|
||||
onChange | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onInput | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onAdd | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onRemove | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onInvalid | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onClick | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onKeydown | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onFocus | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onBlur | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onEditInput | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onEditBeforeUpdate | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onEditUpdated | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onEditStart | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onEditKeydown | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onDropdownShow | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onDropdownHide | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onDropdownSelect | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onDropdownScroll | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onDropdownNoMatch | <sub>Function</sub> | | See [*events* section](#events)
|
||||
onDropdownUpdated | <sub>Function</sub> | | See [*events* section](#events)
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
|
||||
## jQuery version
|
||||
|
||||
`jQuery.tagify.js`
|
||||
|
||||
A jQuery wrapper verison is also available, but I advise not using it because it's basically the exact same as the "normal"
|
||||
script (non-jqueryfied) and all the jQuery's wrapper does is allowing to chain the event listeners for ('add', 'remove', 'invalid')
|
||||
|
||||
```javascript
|
||||
$('[name=tags]')
|
||||
.tagify()
|
||||
.on('add', function(e, tagData){
|
||||
console.log('added', ...tagData) // data, index, and DOM node
|
||||
});
|
||||
```
|
||||
|
||||
Accessing methods can be done via the [`.data('tagify')`](https://api.jquery.com/data):
|
||||
|
||||
```javascript
|
||||
$('[name=tags]').tagify();
|
||||
// get tags from the server (ajax) and add them:
|
||||
$('[name=tags]').data('tagify').addTags('aaa, bbb, ccc')
|
||||
````
|
||||
|
||||
## HTML input & textarea attributes
|
||||
|
||||
The below list of *attributes* affect *Tagify*.<br>
|
||||
These can also be set by Tagify [settings](#settings) Object manually, and not *declerativly* (via attributes).
|
||||
|
||||
Attribute | Example | Info
|
||||
----------------- | ----------------------------------------------------- | --------------------
|
||||
[pattern](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern) | <pre lang=html>`<input pattern='^[A-Za-z_✲ ]{1,15}$'>`</pre> | Tag Regex pattern which tag input is validated by.
|
||||
[placeholder](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#htmlattrdefplaceholder) | <pre lang=html>`<input placeholder='please type your tags'>`</pre> | This attribute's value will be used as a constant placeholder, which is visible unless something is being typed.
|
||||
readOnly | <pre lang=html>`<input readOnly>`</pre> | No user-interaction (add/remove/edit) allowed.
|
||||
autofocus | <pre lang=html>`<input autofocus>`</pre> | Automatically focus the the Tagify component when the component is loaded
|
||||
required | <pre lang=html>`<input required>`</pre> | Adds a `required` attribute to the Tagify wrapper element. Does nothing more.
|
||||
|
||||
|
||||
|
||||
## FAQ
|
||||
List of questions & scenarios which might come up during development with Tagify:
|
||||
|
||||
<details>
|
||||
<summary><strong>Dynamic whitelist</strong></summary>
|
||||
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"]
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><strong>tags/whitelist data structure</strong></summary>
|
||||
|
||||
Tagify does not accept just *any* kind of data structure.<br>
|
||||
If a tag data is represented as an `Object`, it **must** contain a **unique** property `value`
|
||||
which Tagify uses to check if a tag already exists, among other things, so make sure it is present.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```javascript
|
||||
[{ "id":1, "name":"foo bar" }]
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```javascript
|
||||
[{ "id":1, "value": 1, "name":"foo bar" }]
|
||||
```
|
||||
|
||||
```javascript
|
||||
[{ "value":1, "name":"foo bar" }]
|
||||
```
|
||||
|
||||
```javascript
|
||||
[{ "value":"foo bar" }]
|
||||
```
|
||||
|
||||
```javascript
|
||||
// ad a simple array of Strings
|
||||
["foo bar"]
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary><strong>Save changes (Ex. to a server)</strong></summary>
|
||||
|
||||
In framework-less projects, the developer should save the state of the Tagify component (somewhere), and
|
||||
the question is:<br/>
|
||||
**when should the state be saved?**
|
||||
On every change made to *Tagify's* internal state (`tagify.value` via the `update()` method).<br>
|
||||
|
||||
|
||||
```javascript
|
||||
var tagify = new Tagify(...)
|
||||
|
||||
// listen to "change" events on the "original" input/textarea element
|
||||
tagify.DOM.originalInput.addEventListener('change', onTagsChange)
|
||||
|
||||
// This example uses async/await but you can use Promises, of course, if you prefer.
|
||||
async function onTagsChange(e){
|
||||
const {name, value} = e.target
|
||||
// "imaginary" async function "saveToServer" should get the field's name & value
|
||||
await saveToServer(name, value)
|
||||
}
|
||||
```
|
||||
|
||||
If you are using *React/Vue/Angular* or any "modern" framework, then you already know how to
|
||||
attach "onChange" event listeners to your `<input>`/`<textarea>` elements, so the above is irrelevant.
|
||||
</details>
|
||||
|
||||
----
|
||||
|
||||
<details>
|
||||
<summary><strong>Render tags in one single line</strong></summary>
|
||||
|
||||
Stopping tags from wrapping to new lines, add this to your `.tagify` *selector CSS Rule*:
|
||||
|
||||
```css
|
||||
flex-wrap: nowrap;
|
||||
````
|
||||
</details>
|
||||
|
||||
----
|
||||
|
||||
<details>
|
||||
<summary><strong>Submit on `Enter` key</strong></summary>
|
||||
|
||||
Tagify internally has `state` property, per `Tagify` instance
|
||||
and this may be useful for a variety of things when implementing a specific scenario.
|
||||
|
||||
```js
|
||||
var tagify = new Tagify(...)
|
||||
var formElm = document.forms[0]; // just an example
|
||||
|
||||
tagify.on('keydown', onTagifyKeyDown)
|
||||
|
||||
function onTagifyKeyDown(e){
|
||||
if( e.key == 'Enter' && // "enter" key pressed
|
||||
!tagify.state.inputText && // assuming user is not in the middle oy adding a tag
|
||||
!tagify.state.editing // user not editing a tag
|
||||
){
|
||||
setTimeout(() => formElm.submit()) // put some buffer to make sure tagify has done with whatever, to be on the safe-side
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
* [Double-click tag fires both "edit" & "click" custom events](https://github.com/yairEO/tagify/issues/247)
|
||||
* [Manualy open the suggestions dropdown](https://github.com/yairEO/tagify/issues/254)
|
||||
* [Render your own suggestions dropdown](https://github.com/yairEO/tagify/issues/244)
|
||||
* [Allow max length on mix mode](https://github.com/yairEO/tagify/issues/252)
|
||||
* [Always show dropdown](https://github.com/yairEO/tagify/issues/253)
|
||||
* [Limit the length of a tag value (minimum & maximum)](https://github.com/yairEO/tagify/issues/245)
|
||||
* [*Mixed mode* initial value](https://github.com/yairEO/tagify/issues/237)
|
||||
* [Random colors for each tag](https://github.com/yairEO/tagify/issues/223)
|
||||
* [Format input value for server side](https://github.com/yairEO/tagify/issues/220)
|
||||
* [Writing to tagify textarea](https://github.com/yairEO/tagify/issues/294)
|
||||
* [Scroll all tags within one line, instead of growing vertically](https://github.com/yairEO/tagify/issues/145)
|
||||
* [Insert emoji at caret location when editing a tag](https://github.com/yairEO/tagify/issues/365)
|
||||
* [propagate `change` event](https://github.com/yairEO/tagify/issues/413)
|
||||
* [Manually update tag data after it was added](https://github.com/yairEO/tagify/issues/433)
|
||||
* [Ajax Whitelist with "enforceWhitelist" setting enabled](https://github.com/yairEO/tagify/issues/465)
|
||||
* [Custom (multiple) tag 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.<br>It's important the size fits *exactly* to the tag.<br>Change this if you change the `--tag-pad` or fontsize.
|
||||
--tag-invalid-color | For border color of edited tags with invalid value being typed into them
|
||||
--tag-invalid-bg | Background color for invalid Tags.
|
||||
--tag-remove-bg | Tag background color when hovering the `×` button.
|
||||
--tag-remove-btn-color | Remove (`×`) button text color
|
||||
--tag-remove-btn-bg | Remove (`×`) button background color
|
||||
--tag-remove-btn-bg--hover | Remove (`×`) button hover background color
|
||||
--loader-size | Loading animation size. `1em` is pretty big, default is a bit less.
|
||||
--tag-hide-transition | Controls the transition property when a tag is removed. default is '.3s'
|
||||
--placeholder-color | Placeholder text color
|
||||
--placeholder-color-focus | Placeholder text color when Tagify has focus and no input was typed
|
||||
--input-color | Input text color
|
||||
|
||||
### Full list of Tagify's [SCSS variables](https://github.com/yairEO/tagify/blob/master/src/tagify.scss#L9-L24)
|
||||
|
||||
|
||||
## Methods
|
||||
|
||||
`Tagify` is [prototype](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes) based and There are many methods, but I've chosen to list the most relevant ones:
|
||||
|
||||
Name | Parameters | Info
|
||||
-------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------
|
||||
`destroy` | | Reverts the input element back as it was before Tagify was applied
|
||||
`removeAllTags` | | Removes all tags and resets the original input tag's value property
|
||||
`addTags` | <ol><li>`Array`/`String`/`Object` tag(s) to add</li><li>`Boolean` clear input after adding</li><li>`Boolean` - skip adding invalids</li><ol> | Accepts a String (word, single or multiple with a delimiter), an Array of Objects (see above) or Strings.
|
||||
`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` | <ol><li>`Array`/`HTMLElement`/`String` tag(s) to remove</li><li>`silent` does not update the component's value</li><li>`tranDuration` Transition duration (in `ms`)</li></ul> | (#502) Remove single/multiple Tags. When nothing passed, removes last tag. <ul><li>`silent` - A flag, which when turned on, does not remove any value and does not update the original input value but simply removes the tag from tagify</li><li>`tranDuration` - delay for animation, after which the tag will be removed from the DOM</li></ul>
|
||||
`addEmptyTag` | `Object` <sub>(`tagData`)</sub> | Create an empty tag (optionally with pre-defined data) and enters "edit" mode directly. [See demo](https://yaireo.github.io/tagify#section-different-look)
|
||||
`loadOriginalValues` | `String`/`Array` | Converts the input's value into tags. This method gets called automatically when instansiating Tagify. Also works for mixed-tags
|
||||
`getWhitelistItemsByValue` | `Object` | `{value}` - return an Array of found matching items (case-insensitive)
|
||||
`getTagIndexByValue` | `String` | Returns the index of a specific tag, by value
|
||||
`getTagElmByValue` | `String` | Returns the first matched tag node, if found
|
||||
`isTagDuplicate` | `String` | Returns how many tags already exists with that value
|
||||
`parseMixTags` | `String` | Converts a String argument (`[[foo]] and [[bar]] are..`) into HTML with mixed tags & texts
|
||||
`getTagElms` | | Returns a DOM nodes list of all the tags
|
||||
`getTagElmByValue` | `String` | Returns a specific tag DOM node by value
|
||||
`tagData` | `HTMLElement`, `Object` | set/get tag data on a tag element (has`.tagify__tag` class by default)
|
||||
`editTag` | `HTMLElement` | Goes to edit-mode in a specific tag
|
||||
`replaceTag` | `tagElm`, `Object` <sub>(`tagData`)</sub> | Exit a tag's edit-mode. if "tagData" exists, replace the tag element with new data and update Tagify value
|
||||
`loading` | `Boolean` | toggle loading state on/off (Ex. AJAX whitelist pulling)
|
||||
`tagLoading` | `HTMLElement`, Boolean | same as above but for a specific tag element
|
||||
`createTagElem` | `Object` <sub>(`tagData`)</sub> | Returns a tag element from the supplied tag data
|
||||
`injectAtCaret` | `HTMLElement` <sub>(`injectedNode`)</sub>, `Object` <sub>(`range`)</sub> | Injects text or HTML node at last caret position. `range` parameter is *optional*
|
||||
`placeCaretAfterNode` | `HTMLElement` | Places the caret after a given node
|
||||
`insertAfterTag` | `HTMLElement` <sub>(tag element)</sub>, `HTMLElement`/`String` <sub>(whatever to insert after)</sub> |
|
||||
`toggleClass` | `Boolean` | Toggles `class` on the main *tagify* container (`scope`)
|
||||
`dropdown.selectAll` | | Add **all** whitelist items as tags and close the suggestion dropdown
|
||||
`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` <sub>(template name or function)</sub>, `Array` <sub>(data)</sub> | converts a template string (by selecting one from the `settings.templates` by name or supplying a template function which returns a String) into a DOM node
|
||||
`setReadonly` | `Boolean` | Toggles "readonly" mode on/off
|
||||
`setDisabled` | `Boolean` | Toggles "disabled" mode on/off
|
||||
|
||||
|
||||
## Events
|
||||
|
||||
All triggered events return the instance's scope (tagify).<br>
|
||||
See `e.detail` for custom event additional data.
|
||||
|
||||
<details>
|
||||
<summary>Example 1</summary>
|
||||
|
||||
```javascript
|
||||
var tagify = new Tagify(...)
|
||||
|
||||
// events can be chainable, and multiple events may be binded for the same callback
|
||||
tagify
|
||||
.on('input', e => console.log(e.detail))
|
||||
.on('edit:input edit:updated edit:start edit:keydown', e => console.log(e.type, e.detail))
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Example 2</summary>
|
||||
|
||||
```javascript
|
||||
var tagify = new Tagify(inputNode, {
|
||||
callbacks: {
|
||||
"change": (e) => console.log(e.detail))
|
||||
"dropdown:show": (e) => console.log(e.detail))
|
||||
}
|
||||
})
|
||||
```
|
||||
</details>
|
||||
|
||||
Name | Info
|
||||
------------------ | --------------------------------------------------------------------------
|
||||
change | Any change to the value has occured. `e.details.value` callback listener argument is a *String*
|
||||
add | A tag has been added
|
||||
remove | A tag has been removed ([use `removeTag`](https://github.com/yairEO/tagify/issues/222) instead with *jQuery*)
|
||||
invalid | A tag has been added but did not pass vaildation. See [event detail](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events)
|
||||
input | [Input](https://developer.mozilla.org/en-US/docs/Web/Events/input) event, when a tag is being typed/edited. `e.detail` exposes `value`, `inputElm` & `isValid`
|
||||
click | Clicking a tag. Exposes the tag element, its index & data
|
||||
dblclick | Double-clicking a tag
|
||||
keydown | When tagify input has focus and a key was pressed
|
||||
focus | The component currently has focus
|
||||
blur | The component lost focus
|
||||
edit:input | Typing inside an edited tag
|
||||
edit:beforeUpdate | Just before a tag has been updated, while still in "edit" mode
|
||||
edit:updated | A tag as been updated (changed view editing or by directly calling the `replaceTag()` method)
|
||||
edit:start | A tag is now in "edit mode"
|
||||
edit:keydown | keydown event while an edited tag is in focus
|
||||
dropdown:show | Suggestions dropdown is to be rendered. The dropdown DOM node is passed in the callback, [see demo](https://yaireo.github.io/tagify/#section-basic).
|
||||
dropdown:hide | Suggestions dropdown has been removed from the DOM
|
||||
dropdown:select | Suggestions dropdown item selected (by mouse/keyboard/touch)
|
||||
dropdown:scroll | Tells the percentage scrolled. (`event.detail.percentage`)
|
||||
dropdown:noMatch | No whitelist suggestion item matched for the the typed input. At this point it is possible to manually set `tagify.suggestedListItems` to any possible custom value, for example: `[{ value:"default" }]`
|
||||
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.
|
||||
|
||||
<details>
|
||||
<summary>For example, if a developer wishes to add a (native) confirmation popup before a tag is removed (by a user action):
|
||||
</summary>
|
||||
|
||||
```javascript
|
||||
var input = document.querySelector('input')
|
||||
var tagify = new Tagify(input,{
|
||||
hooks: {
|
||||
/**
|
||||
* Removes a tag
|
||||
* @param {Array} tags [Array of Objects [{node:..., data:...}, {...}, ...]]
|
||||
*/
|
||||
beforeRemoveTag : function( tags ){
|
||||
return new Promise((resolve, reject) => {
|
||||
confirm("Remove " + tags[0].data.value + "?")
|
||||
? resolve()
|
||||
: reject()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
Name | Parameters | Info
|
||||
---------------------- | ------------------------------------------- | --------------------------------------------------------------------------
|
||||
beforeRemoveTag | Array <sub>(of Objects)</sub> | [Example](https://jsbin.com/xoseyux/edit?html,js,output)
|
||||
suggestionClick | Object <sub>(click event data)</sub> | [Example](https://jsbin.com/tuwihuf/edit?html,js,output)
|
||||
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 | <sub>String</sub> | `"value"` | Tag data Object property which will be displayed as the tag's text. Remember to keep "value" property <em>unique</em>. See Also: `dropdown.mapValueTo`, `dropdown.searchKeys`
|
||||
placeholder | <sub>String</sub> | | Placeholder text. If this attribute is set on an input/textarea element it will override this setting
|
||||
delimiters | <sub>String</sub> | `","` | [RegEx **string**] split tags by any of these delimiters. Example delimeters: ",|.| " (*comma*, *dot* or *whitespace*)
|
||||
pattern | <sub>String/RegEx</sub> | null | Validate input by RegEx pattern (can also be applied on the input itself as an attribute) Ex: `/[1-9]/`
|
||||
mode | <sub>String</sub> | null | Use `select` for single-value dropdown-like select box. See `mix` as value to allow mixed-content. The 'pattern' setting must be set to some character.
|
||||
mixTagsInterpolator | <sub>Array</sub> | <sub>`['[[', ']]']`</sub> | Interpolation for mix mode. Everything between these will become a tag
|
||||
mixTagsAllowedAfter | <sub>RegEx</sub> | <sub>`/,\|\.\|\:\|\s/`</sub> | Define conditions in which typed mix-tags content is allowing a tag to be created after.
|
||||
duplicates | <sub>Boolean</sub> | false | Should duplicate tags be allowed or not
|
||||
trim | <sub>Boolean</sub> | true | If `true` trim the tag's value (remove before/after whitespaces)
|
||||
enforceWhitelist | <sub>Boolean</sub> | false | Should ONLY use tags allowed in whitelist.<br>In `mix-mode`, setting it to `false` will not allow creating new tags.
|
||||
userInput | <sub>Boolean</sub> | true | Disable manually typing/pasting/editing tags (tags may only be added from the whitelist)
|
||||
autoComplete.enabled | <sub>Boolean</sub> | true | Tries to suggest the input's value while typing (match from whitelist) by adding the rest of term as grayed-out text
|
||||
autoComplete.rightKey | <sub>Boolean</sub> | false | If `true`, when `→` is pressed, use the suggested value to create a tag, else just auto-completes the input. In mixed-mode this is ignored and treated as "true"
|
||||
whitelist | <sub>Array</sub> | [] | An array of allowed tags (*Strings* or *Objects*). When using *Objects* in the *whitelist* array a `value` property is a must & should be unique. <br/>Also, the *whitelist used for auto-completion when `autoCompletion.enabled` is `true`
|
||||
blacklist | <sub>Array</sub> | [] | An array of tags which aren't allowed
|
||||
addTagOnBlur | <sub>Boolean</sub> | true | Automatically adds the text which was inputed as a tag when blur event happens
|
||||
pasteAsTags | <sub>Boolean</sub> | true | Automatically converts pasted text into tags
|
||||
callbacks | <sub>Object</sub> | {} | Exposed callbacks object to be triggered on events: `'add'` / `'remove'` tags
|
||||
maxTags | <sub>Number</sub> | Infinity | Maximum number of allowed tags. when reached, adds a class "tagify--hasMaxTags" to `<Tags>`
|
||||
editTags | <sub>Object/Number</sub> | {} | `false` or `null` will disallow editing
|
||||
editTags.*clicks* | <sub>Number</sub> | 2 | Number of clicks to enter "edit-mode": 1 for single click. Any other value is considered as double-click
|
||||
editTags.*keepInvalid* | <sub>Boolean</sub> | true | keeps invalid edits as-is until `esc` is pressed while in focus
|
||||
templates | <sub>Object</sub> | <sub>`wrapper`, `tag`, `dropdownItem`</sub> | Object consisting of functions which return template strings
|
||||
validate | <sub>Function</sub> | | 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 | <sub>Function</sub> | | Takes a tag data as argument and allows mutating it before a tag is created or edited.<br>Should not `return` anything, only **mutate**.
|
||||
keepInvalidTags | <sub>Boolean</sub> | false | If `true`, do not remove tags which did not pass validation
|
||||
skipInvalid | <sub>Boolean</sub> | false | If `true`, do not add invalid, temporary, tags before automatically removing them
|
||||
backspace | <sub>*</sub> | true | On pressing backspace key:<br> `true` - remove last tag <br>`edit` - edit last tag<br>`false` - do nothing (useful for outside style)
|
||||
originalInputValueFormat | <sub>Function</sub> | | If you wish your original input/textarea `value` property format to other than the default (which I recommend keeping) you may use this and make sure it returns a *string*.
|
||||
mixMode.*insertAfterTag* | <sub>Node/String</sub> | `\u00A0` | `node` or `string` to add after a tag added |
|
||||
a11y.*focusableTags* | <sub>Boolean</sub> | false | allows tags to get focus, and also to be deleted via <kbd>Backspace</kbd>
|
||||
dropdown.*enabled* | <sub>Number</sub> | 2 | Minimum characters input for showing a suggestions list. `false` will not render a suggestions list.
|
||||
dropdown.*caseSensitive* | <sub>Boolean</sub> | false | if `true`, match **exact** item when a suggestion is selected (from the dropdown) and also more strict matching for dulpicate items. **Ensure** `fuzzySearch` is `false` for this to work.
|
||||
dropdown.*maxItems* | <sub>Number</sub> | 10 | Maximum items to show in the suggestions list
|
||||
dropdown.*classname* | <sub>String</sub> | `""` | Custom *classname* for the dropdown suggestions list
|
||||
dropdown.*fuzzySearch* | <sub>Boolean</sub> | true | Enables filtering dropdown items values' by string *containing* and not only *beginning*
|
||||
dropdown.*sortby* | <sub>String/Function</sub> | | 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.<br><br> 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.<br><br>(*default sorting order is same as the whitelist's*)
|
||||
dropdown.*accentedSearch* | <sub>Boolean</sub> | true | Enable searching for <em>accented</em> items in the whitelist without typing exact match (#491)
|
||||
dropdown.*position* | <sub>String</sub> | `"all"` | <ul><li>`manual` - will not render the dropdown, and you would need to do it yourself. [See demo](https://yaireo.github.io/tagify/#section-manual-suggestions)</li><li>`text` - places the dropdown next to the caret</li><li>`input` - places the dropdown next to the input (useful in rare situations)</li><li>`all` - normal, full-width design</li></ul>
|
||||
dropdown.*highlightFirst* | <sub>Boolean</sub> | false | When a suggestions list is shown, highlight the first item, and also suggest it in the input (The suggestion can be accepted with <kbd>→</kbd> key)
|
||||
dropdown.*closeOnSelect* | <sub>Boolean</sub> | true | close the dropdown after selecting an item, if `enabled:0` is set (which means always show dropdown on focus)
|
||||
dropdown.*clearOnSelect* | <sub>Boolean</sub> | true | Keep typed text after selecting a suggestion
|
||||
dropdown.*mapValueTo* | <sub>Function/String</sub> | | If whitelist is an Array of Objects:<br>Ex. `[{value:'foo', email:'foo@a.com'},...]`)<br> this setting controlls which data <em>key</em> will be printed in the dropdown.<br> Ex.1: `mapValueTo: data => "To:" + data.email`<br>Ex.2: `mapValueTo: "email"`
|
||||
dropdown.*searchKeys* | <sub>Array</sub> | <sub>`["value", "searchBy"]`</sub> | When a user types something and trying to match the whitelist items for suggestions, this setting allows matching other keys of a whitelist objects
|
||||
dropdown.*appendTarget* | <sub>HTMLNode</sub> | `document.body` | Target-Node which the *suggestions dropdown* is appended to (*only when rendered*)
|
||||
dropdown.*placeAbove* | <sub>Boolean</sub> | | 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.
|
||||
32
node_modules/@yaireo/tagify/dist/jQuery.tagify.min.js
generated
vendored
Normal file
32
node_modules/@yaireo/tagify/dist/jQuery.tagify.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
8
node_modules/@yaireo/tagify/dist/react.tagify.js
generated
vendored
Normal file
8
node_modules/@yaireo/tagify/dist/react.tagify.js
generated
vendored
Normal file
@@ -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<arguments.length;n++){var t=arguments[n];for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o])}return e}).apply(this,arguments)}function d(e,n){if(null==e)return{};var t,o,r=function(e,n){if(null==e)return{};var t,o,r={},u=Object.keys(e);for(o=0;o<u.length;o++)t=u[o],n.indexOf(t)>=0||(r[t]=e[t]);return r}(e,n);if(Object.getOwnPropertySymbols){var u=Object.getOwnPropertySymbols(e);for(o=0;o<u.length;o++)t=u[o],n.indexOf(t)>=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}));
|
||||
1
node_modules/@yaireo/tagify/dist/tagify.css
generated
vendored
Normal file
1
node_modules/@yaireo/tagify/dist/tagify.css
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
8
node_modules/@yaireo/tagify/dist/tagify.min.js
generated
vendored
Normal file
8
node_modules/@yaireo/tagify/dist/tagify.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
10
node_modules/@yaireo/tagify/dist/tagify.polyfills.min.js
generated
vendored
Normal file
10
node_modules/@yaireo/tagify/dist/tagify.polyfills.min.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
27
node_modules/@yaireo/tagify/dist/tagify.vue
generated
vendored
Normal file
27
node_modules/@yaireo/tagify/dist/tagify.vue
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
<template v-once>
|
||||
<textarea v-if="mode === 'textarea'" v-model="value"/>
|
||||
<input v-else :value="value" v-on:change="onChange">
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tagify from "./tagify.min.js"
|
||||
import "./tagify.css"
|
||||
|
||||
export default {
|
||||
name: "Tags",
|
||||
props: {
|
||||
mode: String,
|
||||
settings: Object,
|
||||
value: [String, Array],
|
||||
onChange: Function
|
||||
},
|
||||
watch: {
|
||||
value(newVal, oldVal) {
|
||||
this.tagify.loadOriginalValues(newVal)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.tagify = new Tagify(this.$el, this.settings)
|
||||
}
|
||||
};
|
||||
</script>
|
||||
124
node_modules/@yaireo/tagify/package.json
generated
vendored
Normal file
124
node_modules/@yaireo/tagify/package.json
generated
vendored
Normal file
@@ -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"
|
||||
}
|
||||
693
node_modules/@yaireo/tagify/src/tagify.scss
generated
vendored
Normal file
693
node_modules/@yaireo/tagify/src/tagify.scss
generated
vendored
Normal file
@@ -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 <div><br></div> 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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
package-lock.json
generated
Normal file
11
package-lock.json
generated
Normal file
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ArticleForMainView> nextPage(@PathVariable int pageNum){
|
||||
|
||||
return articleService.getRecentArticles(pageNum).getContent();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ArticleTagList> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<Article,Long> {
|
||||
|
||||
Slice<Article> findBy(Pageable pageable);
|
||||
|
||||
}
|
||||
@@ -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<Article, Long> {
|
||||
|
||||
List<Article> findTop6ByOrderByHitDesc();
|
||||
|
||||
Slice<Article> findByOrderByCreatedDateDesc(Pageable pageable);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ArticleForMainView> getPopularArticles() {
|
||||
List<Article> top6ByOrderByHitDesc = articleRepository.findTop6ByOrderByHitDesc();
|
||||
|
||||
List<ArticleForMainView> articles = new ArrayList<>();
|
||||
|
||||
for (Article article : top6ByOrderByHitDesc) {
|
||||
articles.add(modelMapper.map(article, ArticleForMainView.class));
|
||||
}
|
||||
|
||||
|
||||
return articles;
|
||||
|
||||
}
|
||||
|
||||
public Slice<ArticleForMainView> 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();
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
}
|
||||
|
||||
13
src/main/java/myblog/blog/base/config/AppConfig.java
Normal file
13
src/main/java/myblog/blog/base/config/AppConfig.java
Normal file
@@ -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();}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
51
src/main/java/myblog/blog/category/domain/Category.java
Normal file
51
src/main/java/myblog/blog/category/domain/Category.java
Normal file
@@ -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<Article> 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<Category> child = new ArrayList<>();
|
||||
|
||||
@Builder
|
||||
public Category(String title, Category parents, int tier) {
|
||||
this.title = title;
|
||||
this.parents = parents;
|
||||
this.tier = tier;
|
||||
}
|
||||
|
||||
protected Category() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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<CategoryForMainView> categoryTCountList = new ArrayList<>();
|
||||
|
||||
|
||||
public static CategoryForMainView createCategory(List<CategoryCountForRepository> crList) {
|
||||
|
||||
Collections.reverse(crList);
|
||||
return recursBuilding(0, crList);
|
||||
|
||||
}
|
||||
|
||||
private static CategoryForMainView recursBuilding(int d, List<CategoryCountForRepository> 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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, Long> {
|
||||
|
||||
Category findByTitle(String title);
|
||||
|
||||
|
||||
}
|
||||
@@ -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<CategoryCountForRepository> getCategoryCount();
|
||||
|
||||
}
|
||||
@@ -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<CategoryCountForRepository> categoryCount = naCategoryRepository.getCategoryCount();
|
||||
|
||||
return CategoryForMainView.createCategory(categoryCount);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class UploadedImg {
|
||||
|
||||
private String uploadFileName;
|
||||
|
||||
13
src/main/java/myblog/blog/img/dto/UploadImgDto.java
Normal file
13
src/main/java/myblog/blog/img/dto/UploadImgDto.java
Normal file
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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<ArticleForMainView> popularArticles = articleService.getPopularArticles();
|
||||
Slice<ArticleForMainView> recentArticles = articleService.getRecentArticles(0);
|
||||
CategoryForMainView categoryForView = categoryService.getCategoryForView();
|
||||
|
||||
model.addAttribute("category",categoryForView);
|
||||
model.addAttribute("popularArticles", popularArticles);
|
||||
model.addAttribute("recentArticles",recentArticles);
|
||||
|
||||
return "main";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
@@ -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, Long> {
|
||||
|
||||
Member findByUserId(String userId);
|
||||
|
||||
@@ -77,7 +77,6 @@ public class Oauth2MemberService extends DefaultOAuth2UserService {
|
||||
return member;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void insertAdmin(){
|
||||
|
||||
Member admin = memberRepository.findByEmail(adminEmail);
|
||||
|
||||
37
src/main/java/myblog/blog/tags/domain/ArticleTagList.java
Normal file
37
src/main/java/myblog/blog/tags/domain/ArticleTagList.java
Normal file
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
36
src/main/java/myblog/blog/tags/domain/Tags.java
Normal file
36
src/main/java/myblog/blog/tags/domain/Tags.java
Normal file
@@ -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<ArticleTagList> articleTagLists = new ArrayList<>();
|
||||
|
||||
@Builder
|
||||
public Tags(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
protected Tags() {
|
||||
|
||||
}
|
||||
}
|
||||
14
src/main/java/myblog/blog/tags/dto/TagsDto.java
Normal file
14
src/main/java/myblog/blog/tags/dto/TagsDto.java
Normal file
@@ -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 + "}";
|
||||
}
|
||||
}
|
||||
@@ -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<ArticleTagList, Long> {
|
||||
}
|
||||
@@ -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, Long> {
|
||||
|
||||
Tags findByName(String name);
|
||||
|
||||
}
|
||||
49
src/main/java/myblog/blog/tags/service/TagsService.java
Normal file
49
src/main/java/myblog/blog/tags/service/TagsService.java
Normal file
@@ -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<Map> 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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ server:
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create
|
||||
ddl-auto:
|
||||
properties:
|
||||
hibernate:
|
||||
# show_sql: true
|
||||
|
||||
20
src/main/resources/static/css/articleWrite.css
Normal file
20
src/main/resources/static/css/articleWrite.css
Normal file
@@ -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;
|
||||
}
|
||||
@@ -205,6 +205,7 @@ body::-webkit-scrollbar-track {
|
||||
}
|
||||
|
||||
.arrow-up {
|
||||
visibility: hidden;
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 50px;
|
||||
|
||||
14
src/main/resources/static/js/arrow.js
Normal file
14
src/main/resources/static/js/arrow.js
Normal file
@@ -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});
|
||||
@@ -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);
|
||||
|
||||
10
src/main/resources/static/js/getCsrf.js
Normal file
10
src/main/resources/static/js/getCsrf.js
Normal file
@@ -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;
|
||||
}
|
||||
80
src/main/resources/static/js/infinityScroll.js
Normal file
80
src/main/resources/static/js/infinityScroll.js
Normal file
@@ -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 +=
|
||||
"<div class=\"card mb-3 recent-card\">\n" +
|
||||
" <a href=\"#\">\n" +
|
||||
" <div class=\"row g-0\">\n" +
|
||||
" <div class=\"col-3\">\n" +
|
||||
" <div class=\"ratio ratio-1x1\" style=\"background-image: url("+listElement.thumbnailUrl+"); background-size: cover;\"></div>\n" +
|
||||
" </div>\n" +
|
||||
" <div class=\"col-9 row row-cols-1 align-self-center\">\n" +
|
||||
" <h3 class=\"card-title col mb-3 text-truncate\">"+listElement.title+"</h3>\n" +
|
||||
" <p class=\"d-none d-md-block col recent-card-text\" >"+listElement.content+"</p>\n" +
|
||||
" <p class=\"col mb-0\"><small class=\"text-muted\">"+date+"</small></p>\n" +
|
||||
" </div>\n" +
|
||||
" </div>\n" +
|
||||
" </a>\n" +
|
||||
"</div>"
|
||||
|
||||
article.innerHTML = articleHtmlSource;
|
||||
nextPage.appendChild(article);
|
||||
}
|
||||
|
||||
container.appendChild(nextPage);
|
||||
|
||||
flag = false;
|
||||
} else {
|
||||
console.error(xhr.response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InfinityScroll()
|
||||
14
src/main/resources/static/js/tags.js
Normal file
14
src/main/resources/static/js/tags.js
Normal file
@@ -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
|
||||
}
|
||||
})
|
||||
46
src/main/resources/static/js/thumbnail.js
Normal file
46
src/main/resources/static/js/thumbnail.js
Normal file
@@ -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 = "";
|
||||
})
|
||||
5
src/main/resources/static/package-lock.json
generated
5
src/main/resources/static/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@toast-ui/editor": "^3.1.1",
|
||||
"@yaireo/tagify": "^4.8.1",
|
||||
"bootstrap": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"/>
|
||||
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="/node_modules/@yaireo/tagify/dist/tagify.css"/>
|
||||
<link rel="stylesheet" href="/css/mainCss.css"/>
|
||||
<link rel="stylesheet" href="/css/articleWrite.css"/>
|
||||
|
||||
<script src="https://kit.fontawesome.com/233840a552.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
@@ -47,38 +49,69 @@
|
||||
|
||||
<section>
|
||||
|
||||
<div style="margin-bottom: 150px"></div>
|
||||
<div id="thumbBox" style="display: none">
|
||||
<div class="card d-flex align-items-center justify-content-center thumbBox">
|
||||
<img src="" id="thumbnailPreView" class="card-img-top w-100 vh-100 cover thumbImg" alt="">
|
||||
<button id="thumbDelBtn" class="thumbDelBtn"><i class="far fa-trash-alt "></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-xxl-none p-0" style="margin-bottom: 100px"></div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center mt-5 mb-3 g-0">
|
||||
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<label class="btn btn-secondary" for="thumbnail">썸네일 등록</label>
|
||||
<input type="file" id="thumbnail" name="thumbnail" accept="image/*" class="d-none">
|
||||
</div>
|
||||
|
||||
|
||||
<form class="" method="post" enctype="multipart/form-data" th:object="${newArticleDto}"
|
||||
th:action="@{/article/write}" id="writeArticleForm">
|
||||
|
||||
<div class="form-group">
|
||||
<input id="title" name="title" type="text" class="form-control"
|
||||
placeholder="제목을 입력해주세요" required max="30">
|
||||
<textarea type="text" name="thumbnailUrl" id="thumbnailUrl" hidden></textarea>
|
||||
|
||||
<div class="mt-3 mb-3">
|
||||
<select id="category" name="category" class="form-select ps-3" aria-label="Select category">
|
||||
<option selected>카테고리를 선택해주세요</option>
|
||||
<option value="Java">Java</option>
|
||||
<option value="Spring">Spring</option>
|
||||
<option value="Jpa">Jpa</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group ">
|
||||
<input id="title" name="title" type="text" class="form-control ps-3"
|
||||
placeholder="제목을 입력해주세요" required max="30">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<textarea type="text" name="content" id="content" hidden></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center mt-4 mb-3 g-0">
|
||||
<label for="editor"></label>
|
||||
<div class=" d-none d-sm-block">
|
||||
<div style="background: white" id="editor"></div>
|
||||
</div>
|
||||
<div class=" d-block d-sm-none">
|
||||
<div style="background: white" id="editorMobile"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- tools -->
|
||||
|
||||
<div class="mt-3">
|
||||
<input id="tags" type="text" name="tags" placeholder="태그"
|
||||
class="form-control tagify-outside ps-3" aria-describedby="tagHelp"/>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<!-- -->
|
||||
|
||||
|
||||
<div class="row justify-content-center mb-3 g-0">
|
||||
<label for="editor"></label>
|
||||
<div class=" d-none d-sm-block">
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
<div class=" d-block d-sm-none">
|
||||
<div id="editorMobile"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-evenly mb-5 g-0">
|
||||
<div class="row justify-content-evenly mt-4 mb-5 g-0">
|
||||
<button class="btn btn-secondary col-4 d-none d-sm-block" onclick="post()">등 록</button>
|
||||
<button class="btn btn-secondary col-4 d-block d-sm-none" onclick="postMobile()">등 록</button>
|
||||
<button class="btn btn-secondary col-4" onclick="javascript:history.back()">취 소</button>
|
||||
@@ -86,8 +119,12 @@
|
||||
</div>
|
||||
|
||||
<!--scripts-->
|
||||
<script src="/node_modules/@yaireo/tagify/dist/tagify.min.js"></script>
|
||||
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
|
||||
<script src="/js/getCsrf.js"></script>
|
||||
<script src="/js/editor.js"></script>
|
||||
<script src="/js/tags.js"></script>
|
||||
<script src="/js/thumbnail.js"></script>
|
||||
|
||||
</section>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<header class="fixed-top d-xxl-none p-0">
|
||||
<nav class="navbar navbar-light bg-light">
|
||||
<div class="container-fluid p-0">
|
||||
@@ -20,8 +21,8 @@
|
||||
</a>
|
||||
</div>
|
||||
<div id="nav-login" sec:authorize="!isAuthenticated()"><a th:href="@{/login}"><i
|
||||
class="far fa-address-card"></i></a></div>
|
||||
<div sec:authorize="isAuthenticated()">
|
||||
class="far fa-address-card pe-2"></i></a></div>
|
||||
<div sec:authorize="isAuthenticated()" class="pe-2">
|
||||
<span sec:authorize="isAuthenticated()" th:text="${#authentication.name} + '님'"></span>
|
||||
<a th:href="@{/article/write}"><span sec:authorize="hasRole('ADMIN')" style="font-size: 21px"><i
|
||||
class="fas fa-edit"></i></span></a>
|
||||
@@ -34,91 +35,12 @@
|
||||
id="offcanvasMenu"
|
||||
aria-labelledby="offcanvasLabel">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title">BLOG</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<div class="offcanvas-body bg-white">
|
||||
<div href="#" class="d-flex align-items-center pb-3 mb-3 link-dark border-bottom">
|
||||
<form class="d-flex">
|
||||
<input class="form-control me-2" type="search" placeholder="검색해" aria-label="Search">
|
||||
<button class="btn btn-outline-success" type="submit"><i class="fas fa-search"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled ps-0">
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#home-collapse" aria-expanded="true">
|
||||
Link
|
||||
</button>
|
||||
<div class="collapse show" id="home-collapse">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">
|
||||
<i class="fab fa-github" style="font-size: 20px;"></i>  Github
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#dashboard-collapse1" aria-expanded="true">
|
||||
카테고리
|
||||
</button>
|
||||
<div class="collapse show" id="dashboard-collapse1">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#dashboard-collapse2" aria-expanded="true">
|
||||
카테고리
|
||||
</button>
|
||||
<div class="collapse show" id="dashboard-collapse2">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#dashboard-collapse3" aria-expanded="true">
|
||||
카테고리
|
||||
</button>
|
||||
<div class="collapse show" id="dashboard-collapse3">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="border-top my-3"></li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#account-collapse" aria-expanded="true">
|
||||
Account
|
||||
</button>
|
||||
<div class="collapse show" id="account-collapse">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li sec:authorize="!isAuthenticated()"><a href="#" th:href="@{/login}"
|
||||
class="link-dark rounded">로그인</a></li>
|
||||
<li sec:authorize="isAuthenticated()" th:text="${#authentication.name} + '님 환영합니다'"></li>
|
||||
<li sec:authorize="isAuthenticated()">
|
||||
<form method="post" id="logoutOC" name="logoutOC" th:action="@{/logout}">
|
||||
<a href="#" class="nav-link active" onclick="document.logout.submit()">로그아웃</a>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div th:replace="layout/sideBar.html :: sideBar"></div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
@@ -133,104 +55,11 @@
|
||||
|
||||
<div class="p-4 sidebar-inner">
|
||||
|
||||
<div class="m-4 sidebar-header">
|
||||
<a href="#">
|
||||
<h5 class="text-black-50">BLOG</h5>
|
||||
</a>
|
||||
<a href="#">
|
||||
<h5 class="text-black-50">ABOUT ME</h5>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div href="#" class="d-flex align-items-center pb-3 mb-3 link-dark border-bottom">
|
||||
<form class="d-flex">
|
||||
<input class="form-control me-2" type="search" placeholder="검색해" aria-label="Search">
|
||||
<button class="btn btn-outline-success" type="submit"><i class="fas fa-search"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled ps-0">
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#home-collapse-xl" aria-expanded="true">
|
||||
Link
|
||||
</button>
|
||||
<div class="collapse show" id="home-collapse-xl">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">
|
||||
<i class="fab fa-github" style="font-size: 20px;"></i>  Github
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#dashboard-collapse1-xl" aria-expanded="true">
|
||||
카테고리
|
||||
</button>
|
||||
<div class="collapse show" id="dashboard-collapse1-xl">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#dashboard-collapse2-xl" aria-expanded="true">
|
||||
카테고리
|
||||
</button>
|
||||
<div class="collapse show" id="dashboard-collapse2-xl">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#dashboard-collapse3-xl" aria-expanded="true">
|
||||
카테고리
|
||||
</button>
|
||||
<div class="collapse show" id="dashboard-collapse3-xl">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="#" class="link-dark rounded">카테고리1</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리2</a></li>
|
||||
<li><a href="#" class="link-dark rounded">카테고리3</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="border-top my-3"></li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#account-collapse-xl" aria-expanded="true">
|
||||
Account
|
||||
</button>
|
||||
<div class="collapse show" id="account-collapse-xl">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li sec:authorize="!isAuthenticated()"><a th:href="@{/login}"
|
||||
class="link-dark rounded">로그인</a></li>
|
||||
<li sec:authorize="isAuthenticated()" th:text="${#authentication.name} + '님 환영합니다'"></li>
|
||||
|
||||
<li sec:authorize="isAuthenticated()">
|
||||
<div class="link-dark rounded">
|
||||
<form method="post" id="logout" name="logout" th:action="@{/logout}">
|
||||
<a href="#" class="nav-link active" onclick="document.logout.submit()">로그아웃</a>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div th:replace="layout/sideBar.html :: sideBar"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- sidebar e -->
|
||||
|
||||
|
||||
<!-- xxl dummy -->
|
||||
<div class="bg-white col-xxl-2 d-none d-xxl-block p-0">
|
||||
</div>
|
||||
@@ -241,25 +70,22 @@
|
||||
|
||||
<section th:replace="${content}"></section>
|
||||
|
||||
<button class="arrow-up">
|
||||
<button id="arrow" class="arrow-up">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
</button>
|
||||
|
||||
<script src="/node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/arrow.js"></script>
|
||||
<!-- scripts e -->
|
||||
|
||||
<!-- sections e -->
|
||||
<!-- sections e -->
|
||||
|
||||
<footer class="footer bg-light">
|
||||
<div class="container text-center p-2">
|
||||
<h5><span class="text-muted">Copyright ©Jinia</span></h5>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<!-- body e -->
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
83
src/main/resources/templates/layout/sideBar.html
Normal file
83
src/main/resources/templates/layout/sideBar.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
|
||||
<div th:fragment="sideBar">
|
||||
|
||||
<div class="m-4 sidebar-header">
|
||||
<a th:href="@{/}">
|
||||
<h5 class="text-black-50">BLOG</h5>
|
||||
</a>
|
||||
<a href="#">
|
||||
<h5 class="text-black-50">ABOUT ME</h5>
|
||||
</a>
|
||||
<a sec:authorize="hasRole('ADMIN')" th:href="@{/article/write}" href="#">
|
||||
<h5 class="text-black-50">WRITE ARTICLE</h5>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div href="#" class="d-flex align-items-center pb-3 mb-3 link-dark border-bottom">
|
||||
<form class="d-flex">
|
||||
<input class="form-control me-2" type="search" placeholder="검색해" aria-label="Search">
|
||||
<button class="btn btn-outline-success" type="submit"><i class="fas fa-search"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled ps-0">
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse-link" aria-expanded="true">
|
||||
Link
|
||||
</button>
|
||||
<div class="collapse show" id="collapse-link">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li><a href="https://github.com/jinia91" class="link-dark rounded">
|
||||
<i class="fab fa-github" style="font-size: 20px;"></i>  Github
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="mb-1" th:text="|전체글(${category.getCount()})|"></li>
|
||||
|
||||
<li class="mb-1" th:each="superCategory : ${category.getCategoryTCountList()}">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
th:data-bs-target="|#collapse-${superCategory.getTitle()}-sub|"
|
||||
aria-expanded="true"
|
||||
th:text="|${superCategory.getTitle()}(${superCategory.getCount()})|">
|
||||
</button>
|
||||
|
||||
<div class="collapse show" th:id="|collapse-${superCategory.getTitle()}-sub|">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small" th:each="subCategory : ${superCategory.getCategoryTCountList()}">
|
||||
<li><a href="#" class="link-dark rounded" th:text="|${subCategory.getTitle()}(${subCategory.getCount()})|">카테고리1</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="border-top my-3"></li>
|
||||
<li class="mb-1">
|
||||
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
|
||||
data-bs-target="#account-collapse-xl" aria-expanded="true">
|
||||
Account
|
||||
</button>
|
||||
<div class="collapse show" id="account-collapse-xl">
|
||||
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
|
||||
<li sec:authorize="!isAuthenticated()"><a th:href="@{/login}"
|
||||
class="link-dark rounded">로그인</a></li>
|
||||
<li sec:authorize="isAuthenticated()" th:text="${#authentication.name} + '님 환영합니다'"></li>
|
||||
|
||||
<li sec:authorize="isAuthenticated()">
|
||||
<div class="link-dark rounded">
|
||||
<form method="post" id="logout" name="logout" th:action="@{/logout}">
|
||||
<a href="#" class="nav-link active" onclick="document.logout.submit()">로그아웃</a>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
</html>
|
||||
368
src/main/resources/templates/main-origin.html
Normal file
368
src/main/resources/templates/main-origin.html
Normal file
@@ -0,0 +1,368 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:replace="~{layout/layout.html :: layout(~{::head}, ~{::section})}"
|
||||
lang="ko" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Jinia's Log</title>
|
||||
<!-- SEO -->
|
||||
<meta name="description" content=""/>
|
||||
<meta name="keyword" content=""/>
|
||||
<meta name="author" content="jinia"/>
|
||||
<meta name="viewport" content="width=device-width, user-scalable = no, initial-scale=1.0"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
|
||||
<!-- OPEN GRAPH(FACEBOOK, LINKEDIN) -->
|
||||
<meta property="og:type" content=""/>
|
||||
<meta property="og:description" content=""/>
|
||||
<meta property="og:title" content=""/>
|
||||
<meta property="og:image" content=""/>
|
||||
<meta property="og:url" content=""/>
|
||||
<meta property="og:site_name" content=""/>
|
||||
|
||||
<!-- twitter -->
|
||||
<meta property="twitter:card" content=""/>
|
||||
<meta property="twitter:title" content=""/>
|
||||
<meta property="twitter:description" content=""/>
|
||||
<meta property="twitter:image" content=""/>
|
||||
<meta property="twitter:url" content=""/>
|
||||
<meta property="twitter:creator" content=""/>
|
||||
|
||||
<link rel="icon" href=""/>
|
||||
<link rel="apple-touch-icon" href=""/>
|
||||
<link rel="short icon" type="image/x-icon" href=""/>
|
||||
|
||||
<!-- CSS RESET -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"/>
|
||||
<link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" href="/css/mainCss.css"/>
|
||||
<link rel="stylesheet" href="/css/login.css"/>
|
||||
|
||||
<script src="https://kit.fontawesome.com/233840a552.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<section>
|
||||
<div class="main">
|
||||
|
||||
<div id="carouselExampleCaptions" class="carousel slide" data-bs-ride="carousel">
|
||||
|
||||
<div class="carousel-indicators">
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="0" class="active"
|
||||
aria-current="true" aria-label="Slide 0"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="1"
|
||||
aria-label="Slide 1"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="2"
|
||||
aria-label="Slide 2"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="3"
|
||||
aria-label="Slide 3"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="4"
|
||||
aria-label="Slide 4"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="5"
|
||||
aria-label="Slide 5"></button>
|
||||
</div>
|
||||
|
||||
<div class="carousel-inner">
|
||||
|
||||
<div class="carousel-item active">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2020/11/08/13/28/tree-5723734_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3 class="">첫번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클 헤으응</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2021/09/27/14/39/paris-6661136_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>두번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클 헤으응2</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2021/10/23/16/31/italy-6735340_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>세번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클 헤으응3</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2021/10/13/15/09/water-6706894_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>네번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클
|
||||
헤으응31!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!헤으응31!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!헤으응31!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img
|
||||
src="https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>다섯번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클 헤으응3</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2021/01/01/21/31/halloween-5880068_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>여섯번째 이미지</h3>
|
||||
<p class="text-truncate">블로그 아티클 헤으응3</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleCaptions"
|
||||
data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#carouselExampleCaptions"
|
||||
data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-5 pt-2 popular-card-section">
|
||||
|
||||
<div class="cards-container container p-0">
|
||||
<div class=" row g-0 row-cols-2 row-cols-md-3">
|
||||
|
||||
<div class="col g-1 g-md-4 ">
|
||||
<a href="#">
|
||||
<div class=" card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2020/11/08/13/28/tree-5723734_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2021/09/27/14/39/paris-6661136_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2021/10/23/16/31/italy-6735340_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2021/10/13/15/09/water-6706894_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2021/01/01/21/31/halloween-5880068_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr class="py-1 mt-5" style="color: rgb(212, 200, 184);">
|
||||
|
||||
<div class="recent-cards mt-5">
|
||||
|
||||
<div class="cards-container container p-0">
|
||||
<h1 class="text-center">최신 포스팅</h1>
|
||||
<hr>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://cdn.pixabay.com/photo/2021/09/27/14/39/paris-6661136_1280.jpg);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴대충 긴대충 긴대충 긴대충 긴대충 긴대충 긴대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용</p>
|
||||
<p class="col mb-0"><small class="text-muted">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충
|
||||
존나 긴 내용대충 존나 긴
|
||||
내용대충
|
||||
존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용
|
||||
</p>
|
||||
<p class="col mb-0"><small class="text-muted">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충
|
||||
존나 긴 내용대충 존나 긴
|
||||
내용대충
|
||||
존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용
|
||||
</p>
|
||||
<p class="col mb-0"><small class="text-muted">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충
|
||||
존나 긴 내용대충 존나 긴
|
||||
내용대충
|
||||
존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용
|
||||
</p>
|
||||
<p class="col mb-0"><small class="text-muted">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://cdn.pixabay.com/photo/2021/01/01/21/31/halloween-5880068_1280.jpg);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충
|
||||
존나 긴 내용대충 존나 긴
|
||||
내용대충
|
||||
존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용
|
||||
</p>
|
||||
<p class="col mb-0"><small class="text-muted update">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -51,86 +51,31 @@
|
||||
<div id="carouselExampleCaptions" class="carousel slide" data-bs-ride="carousel">
|
||||
|
||||
<div class="carousel-indicators">
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="0" class="active"
|
||||
aria-current="true" aria-label="Slide 0"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="1"
|
||||
aria-label="Slide 1"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="2"
|
||||
aria-label="Slide 2"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="3"
|
||||
aria-label="Slide 3"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="4"
|
||||
aria-label="Slide 4"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="5"
|
||||
aria-label="Slide 5"></button>
|
||||
<div th:each="article,num : ${popularArticles}">
|
||||
<button th:if="${num.index}==0"
|
||||
th:data-bs-slide-to="${num.index}"
|
||||
th:aria-label="|Slide ${num.index}|"
|
||||
type="button" data-bs-target="#carouselExampleCaptions" class="active"
|
||||
aria-current="true"></button>
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions"
|
||||
th:if="${num.index}!=0"
|
||||
th:aria-label="|Slide ${num.index}|"
|
||||
th:data-bs-slide-to="${num.index}"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="carousel-inner">
|
||||
|
||||
<div class="carousel-item active">
|
||||
<div class="carousel-item" th:each="article,num : ${popularArticles}" th:classappend="${num.index}==0? active">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2020/11/08/13/28/tree-5723734_1280.jpg"
|
||||
<img th:src="${article.getThumbnailUrl()}"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3 class="">첫번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클 헤으응</p>
|
||||
<h3 class="" th:text="${article.getTitle()}">첫번째 이미지</h3>
|
||||
<p class="text-truncate w-50" th:text="${article.getContent()}">블로그 아티클 헤으응</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2021/09/27/14/39/paris-6661136_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>두번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클 헤으응2</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2021/10/23/16/31/italy-6735340_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>세번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클 헤으응3</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2021/10/13/15/09/water-6706894_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>네번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클
|
||||
헤으응31!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!헤으응31!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!헤으응31!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img
|
||||
src="https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>다섯번째 이미지</h3>
|
||||
<p class="text-truncate w-50">블로그 아티클 헤으응3</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="carousel-item">
|
||||
<a href="#">
|
||||
<img src="https://cdn.pixabay.com/photo/2021/01/01/21/31/halloween-5880068_1280.jpg"
|
||||
class="w-100 vh-100 cover" alt="...">
|
||||
<div class="card-img-overlay text-white text-center row justify-content-center align-content-center">
|
||||
<h3>여섯번째 이미지</h3>
|
||||
<p class="text-truncate">블로그 아티클 헤으응3</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -150,218 +95,58 @@
|
||||
|
||||
|
||||
<div class="mt-5 pt-2 popular-card-section">
|
||||
|
||||
<div class="cards-container container p-0">
|
||||
<div class=" row g-0 row-cols-2 row-cols-md-3">
|
||||
|
||||
<div class="col g-1 g-md-4 ">
|
||||
<div class="col g-1 g-md-4" th:each="article,num : ${popularArticles}">
|
||||
<a href="#">
|
||||
<div class=" card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2020/11/08/13/28/tree-5723734_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class=" card ratio ratio-1x1 popular-card "
|
||||
th:style="|background-image: url(${article.getThumbnailUrl()}); background-size: cover;|">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
<p th:text="${article.getTitle()}">타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2021/09/27/14/39/paris-6661136_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2021/10/23/16/31/italy-6735340_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2021/10/13/15/09/water-6706894_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col g-1 g-md-4">
|
||||
<a href="#">
|
||||
<div class="card ratio ratio-1x1 popular-card " style="background-image: url(https://cdn.pixabay.com/photo/2021/01/01/21/31/halloween-5880068_1280.jpg);
|
||||
background-size: cover;">
|
||||
<div class="card-description">
|
||||
<p>타이틀</p>
|
||||
</div>
|
||||
<div class="card-des-overlay">설명</div>
|
||||
<div class="card-des-overlay" th:text="${article.getContent()}">설명</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr class="py-1 mt-5" style="color: rgb(212, 200, 184);">
|
||||
|
||||
<div class="recent-cards mt-5">
|
||||
|
||||
<div class="cards-container container p-0">
|
||||
<div class="cards-container container p-0" id="infiniteScrollBox">
|
||||
<h1 class="text-center">최신 포스팅</h1>
|
||||
<hr>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://cdn.pixabay.com/photo/2021/09/27/14/39/paris-6661136_1280.jpg);
|
||||
background-size: cover;"></div>
|
||||
|
||||
<div id="articlePage-0">
|
||||
<div class="card mb-3 recent-card" th:each="article,num :${recentArticles.getContent()}">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" th:style="|background-image: url(${article.getThumbnailUrl()}); background-size: cover;|"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate" th:text="${article.getTitle()}">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text" th:text="${article.getContent()}">내용</p>
|
||||
<p class="col mb-0"><small class="text-muted" th:text="|작성일 : ${#temporals.format(article.getCreatedDate(), 'yyyy-MM-dd HH')}|"></small></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴대충 긴대충 긴대충 긴대충 긴대충 긴대충 긴대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용</p>
|
||||
<p class="col mb-0"><small class="text-muted">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충
|
||||
존나 긴 내용대충 존나 긴
|
||||
내용대충
|
||||
존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용
|
||||
</p>
|
||||
<p class="col mb-0"><small class="text-muted">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충
|
||||
존나 긴 내용대충 존나 긴
|
||||
내용대충
|
||||
존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용
|
||||
</p>
|
||||
<p class="col mb-0"><small class="text-muted">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://media.istockphoto.com/photos/matrix-background-with-the-green-numbers-picture-id539244598);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충
|
||||
존나 긴 내용대충 존나 긴
|
||||
내용대충
|
||||
존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용
|
||||
</p>
|
||||
<p class="col mb-0"><small class="text-muted">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3 recent-card">
|
||||
<a href="#">
|
||||
<div class="row g-0">
|
||||
<div class="col-3">
|
||||
<div class="ratio ratio-1x1" style="background-image: url(https://cdn.pixabay.com/photo/2021/01/01/21/31/halloween-5880068_1280.jpg);
|
||||
background-size: cover;"></div>
|
||||
</div>
|
||||
<div class="col-9 row row-cols-1 align-self-center">
|
||||
<h3 class="card-title col mb-3 text-truncate">글 제목</h3>
|
||||
<p class="d-none d-md-block col recent-card-text">대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충
|
||||
존나
|
||||
긴
|
||||
내용대충 존나 긴
|
||||
내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충
|
||||
존나 긴 내용대충 존나 긴
|
||||
내용대충
|
||||
존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴 내용대충 존나 긴
|
||||
내용대충 존나 긴 내용
|
||||
</p>
|
||||
<p class="col mb-0"><small class="text-muted update">Last updated 3 mins ago</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="nextPagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- js -->
|
||||
<script src="/js/infinityScroll.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.0/moment.min.js"></script>
|
||||
<!-- -->
|
||||
</section>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
package myblog.blog.article.service;
|
||||
|
||||
import myblog.blog.article.dto.NewArticleDto;
|
||||
import myblog.blog.article.domain.Article;
|
||||
import myblog.blog.article.repository.ArticlePagingRepository;
|
||||
import myblog.blog.article.repository.ArticleRepository;
|
||||
import org.assertj.core.api.Assertions;
|
||||
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 myblog.blog.category.service.CategoryService;
|
||||
import myblog.blog.member.repository.MemberRepository;
|
||||
import myblog.blog.member.service.Oauth2MemberService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@@ -10,6 +18,8 @@ import org.springframework.test.annotation.Rollback;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
@SpringBootTest
|
||||
@@ -23,24 +33,162 @@ class ArticleServiceTest {
|
||||
ArticleRepository articleRepository;
|
||||
@Autowired
|
||||
EntityManager entityManager;
|
||||
@Autowired
|
||||
ArticlePagingRepository articlePagingRepository;
|
||||
@Autowired
|
||||
Oauth2MemberService memberService;
|
||||
@Autowired
|
||||
CategoryService categoryService;
|
||||
@Autowired
|
||||
MemberRepository memberRepository;
|
||||
@Autowired
|
||||
CategoryRepository categoryRepository;
|
||||
|
||||
@Autowired
|
||||
NaCategoryRepository naCategoryRepository;
|
||||
|
||||
// @BeforeEach
|
||||
void 더미게시글() {
|
||||
memberService.insertAdmin();
|
||||
|
||||
Category ca1 = Category.builder()
|
||||
.title("CA1")
|
||||
.tier(1)
|
||||
.build();
|
||||
Category category1 = ca1;
|
||||
Category ca2 = Category.builder()
|
||||
.title("CA2")
|
||||
.tier(1)
|
||||
.build();
|
||||
Category category2 = ca2;
|
||||
Category category3 = Category.builder()
|
||||
.title("Java")
|
||||
.tier(2)
|
||||
.parents(ca1)
|
||||
.build();
|
||||
Category category4 = Category.builder()
|
||||
.title("Spring")
|
||||
.parents(ca1)
|
||||
.tier(2)
|
||||
.build();
|
||||
Category category5 = Category.builder()
|
||||
.title("Jpa")
|
||||
.parents(ca2)
|
||||
.tier(2)
|
||||
.build();
|
||||
|
||||
categoryRepository.save(category1);
|
||||
categoryRepository.save(category2);
|
||||
categoryRepository.save(category3);
|
||||
categoryRepository.save(category4);
|
||||
categoryRepository.save(category5);
|
||||
|
||||
String[] arr = {"Java", "Spring", "Jpa"};
|
||||
|
||||
int n = 100;
|
||||
while (n-- > 0) {
|
||||
|
||||
articleRepository.save(Article.builder()
|
||||
.title(UUID.randomUUID().toString())
|
||||
.content(String.valueOf(100 - n))
|
||||
.thumbnailUrl("https://picsum.photos/600/59" + (int) (Math.random() * 10))
|
||||
.category(categoryService.findCategory(arr[(int) (Math.random() * 3)]))
|
||||
.member(memberRepository.findById(1L).get())
|
||||
.build());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void 게시글테스트() throws Exception {
|
||||
|
||||
// List<Article> popularArticle = articleRepository.findTop6ByOrderByHitDesc();
|
||||
//
|
||||
// for (Article article : popularArticle) {
|
||||
// System.out.println("article.getId() = " + article.getId());
|
||||
// }
|
||||
//
|
||||
// List<Article> top5ByOOrderByCreatedDateDesc = articleRepository.findTop5ByOrderByCreatedDateDesc();
|
||||
//
|
||||
// for (Article article : top5ByOOrderByCreatedDateDesc) {
|
||||
// System.out.println("article.getCreatedDate() = " + article.getCreatedDate());
|
||||
//
|
||||
//
|
||||
// }
|
||||
|
||||
// entityManager.clear();
|
||||
|
||||
// PageRequest createdDate = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "createdDate"));
|
||||
//
|
||||
// Slice<Article> articles = articleRepository.findByOrderByCreatedDateDesc(createdDate);
|
||||
//
|
||||
// List<Article> content = articles.getContent();
|
||||
//
|
||||
// for (Article article : content) {
|
||||
// System.out.println("article.getCreatedDate() = " + article.getCreatedDate());
|
||||
// System.out.println("article.getContent() = " + article.getContent());
|
||||
// }
|
||||
//
|
||||
//
|
||||
// System.out.println(articles.getNumber());
|
||||
// System.out.println(articles.getNumberOfElements());
|
||||
// System.out.println(articles.hasNext());
|
||||
// }
|
||||
//
|
||||
|
||||
@Test
|
||||
public void 게시글테스트() throws Exception {
|
||||
// given
|
||||
NewArticleDto newArticleDto = new NewArticleDto();
|
||||
newArticleDto.setTitle("abs");
|
||||
newArticleDto.setMemberId(1L);
|
||||
newArticleDto.setToc("df");
|
||||
newArticleDto.setContent("sdfsf");
|
||||
public void 더미데이터테스트() throws Exception {
|
||||
|
||||
|
||||
|
||||
List<CategoryCountForRepository> categoryCountForRepository = naCategoryRepository.getCategoryCount();
|
||||
|
||||
// for (CategoryCountForRepository count : categoryCountForRepository) {
|
||||
//
|
||||
// System.out.println("tier "+ count.getTier() + count.getTitle() +"(" + count.getCount()+")");
|
||||
//
|
||||
// }
|
||||
//
|
||||
|
||||
CategoryForMainView category = CategoryForMainView.createCategory(categoryCountForRepository);
|
||||
|
||||
System.out.println("t1. " + category.getTitle() + "(" + category.getCount()+")");
|
||||
|
||||
List<CategoryForMainView> categoryTCountList = category.getCategoryTCountList();
|
||||
for (CategoryForMainView categoryForMainView : categoryTCountList) {
|
||||
System.out.println("ㄴt2. " + categoryForMainView.getTitle() + "(" + categoryForMainView.getCount()+")");
|
||||
List<CategoryForMainView> categoryTCountList1 = categoryForMainView.getCategoryTCountList();
|
||||
for (CategoryForMainView countForView : categoryTCountList1) {
|
||||
System.out.println(" ㄴt3. " + countForView.getTitle() + "(" + countForView.getCount() +")");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void 카테고리테스트() throws Exception {
|
||||
// given
|
||||
|
||||
articleRepository.save(Article.builder()
|
||||
.title(UUID.randomUUID().toString())
|
||||
.content(String.valueOf(166))
|
||||
.thumbnailUrl("https://picsum.photos/600/59" + (int) (Math.random() * 10))
|
||||
.category(categoryService.findCategory("child"))
|
||||
.member(memberRepository.findById(1L).get())
|
||||
.build());
|
||||
|
||||
articleRepository.save(Article.builder()
|
||||
.title(UUID.randomUUID().toString())
|
||||
.content(String.valueOf(133))
|
||||
.thumbnailUrl("https://picsum.photos/600/59" + (int) (Math.random() * 10))
|
||||
.category(categoryService.findCategory("child"))
|
||||
.member(memberRepository.findById(1L).get())
|
||||
.build());
|
||||
|
||||
// when
|
||||
Long articleId = articleService.writeArticle(newArticleDto);
|
||||
|
||||
// then
|
||||
|
||||
|
||||
System.out.println(articleRepository.findById(articleId).get().getContent());
|
||||
|
||||
// then
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user