mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 14:57:22 +03:00
Replaced tags component by one which is better maintained
This commit is contained in:
parent
741bc21a55
commit
66c5c7ebf1
9 changed files with 195 additions and 194 deletions
65
package-lock.json
generated
65
package-lock.json
generated
|
@ -6578,15 +6578,6 @@
|
|||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"@types/react-autosuggest": {
|
||||
"version": "10.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-autosuggest/-/react-autosuggest-10.1.2.tgz",
|
||||
"integrity": "sha512-K23lmXhC3Bbd8y/jm5+wYrw/NAeN4U/wlHTgAEBIwLOyQKFCFYA3ONKte9P21L+RGIXRP8UlzHOSRtmIZw5Nqw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-color": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz",
|
||||
|
@ -6669,10 +6660,10 @@
|
|||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-tagsinput": {
|
||||
"version": "3.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-tagsinput/-/react-tagsinput-3.19.7.tgz",
|
||||
"integrity": "sha512-yj/3iFBLoan/0vzXMxC9zGhO1uJ89qjQldekf0o3fX4mYdaAPW/VbP921fsyYt6PdHmJ9UMo+kERSMzUAml1xQ==",
|
||||
"@types/react-tag-autocomplete": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-tag-autocomplete/-/react-tag-autocomplete-6.1.0.tgz",
|
||||
"integrity": "sha512-6qJQS81ZMaqV/ZSADwiU91TXnR6ZJINPqoV3z2SMMSlUcO6CV8Vc5QnqcqcVTj2CHnU3UQ2Q5QfSj3NyXomcDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
|
@ -12919,7 +12910,8 @@
|
|||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
|
||||
"dev": true
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
|
@ -24575,18 +24567,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"react-autosuggest": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-10.1.0.tgz",
|
||||
"integrity": "sha512-/azBHmc6z/31s/lBf6irxPf/7eejQdR0IqnZUzjdSibtlS8+Rw/R79pgDAo6Ft5QqCUTyEQ+f0FhL+1olDQ8OA==",
|
||||
"requires": {
|
||||
"es6-promise": "^4.2.8",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-themeable": "^1.1.0",
|
||||
"section-iterator": "^2.0.0",
|
||||
"shallow-equal": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"react-chartjs-2": {
|
||||
"version": "2.11.1",
|
||||
"resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz",
|
||||
|
@ -24998,25 +24978,10 @@
|
|||
"resolved": "https://registry.npmjs.org/react-swipeable/-/react-swipeable-6.0.1.tgz",
|
||||
"integrity": "sha512-69nonicgjT4ofeHxZSpjuz37BoIiWMEbUYkX0mdTCY2mX1U53XDzDUIOVKRg6vVBNGL+pxYjbRzmylXWORh1xQ=="
|
||||
},
|
||||
"react-tagsinput": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.yarnpkg.com/react-tagsinput/-/react-tagsinput-3.19.0.tgz",
|
||||
"integrity": "sha1-bjtFWV8tKV1GV78ZRJGYj5SMqr8="
|
||||
},
|
||||
"react-themeable": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz",
|
||||
"integrity": "sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4=",
|
||||
"requires": {
|
||||
"object-assign": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"object-assign": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
|
||||
"integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I="
|
||||
}
|
||||
}
|
||||
"react-tag-autocomplete": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-tag-autocomplete/-/react-tag-autocomplete-6.1.0.tgz",
|
||||
"integrity": "sha512-AMhVqxEEIrOfzH0A9XrpsTaLZCVYgjjxp3DSTuSvx91LBSFI6uYcKe38ltR/H/TQw4aytofVghQ1hR9sKpXRQA=="
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "2.9.0",
|
||||
|
@ -26121,11 +26086,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"section-iterator": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz",
|
||||
"integrity": "sha1-v0RNev7rlK1Dw5rS+yYVFifMuio="
|
||||
},
|
||||
"select-hose": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||
|
@ -26374,11 +26334,6 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"shallow-equal": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
|
||||
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
"qs": "^6.9.6",
|
||||
"ramda": "^0.27.1",
|
||||
"react": "^17.0.1",
|
||||
"react-autosuggest": "^10.1.0",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-copy-to-clipboard": "^5.0.2",
|
||||
|
@ -51,7 +50,7 @@
|
|||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-swipeable": "^6.0.1",
|
||||
"react-tagsinput": "^3.19.0",
|
||||
"react-tag-autocomplete": "^6.1.0",
|
||||
"reactstrap": "^8.9.0",
|
||||
"redux": "^4.0.5",
|
||||
"redux-localstorage-simple": "^2.4.0",
|
||||
|
@ -80,7 +79,6 @@
|
|||
"@types/qs": "^6.9.5",
|
||||
"@types/ramda": "^0.27.38",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-autosuggest": "^10.1.2",
|
||||
"@types/react-color": "^3.0.4",
|
||||
"@types/react-copy-to-clipboard": "^5.0.0",
|
||||
"@types/react-datepicker": "^3.1.5",
|
||||
|
@ -88,7 +86,7 @@
|
|||
"@types/react-leaflet": "^2.5.2",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-tagsinput": "^3.19.7",
|
||||
"@types/react-tag-autocomplete": "^6.1.0",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.3.1",
|
||||
"adm-zip": "^0.4.16",
|
||||
|
|
145
src/common/react-tag-autocomplete.scss
Normal file
145
src/common/react-tag-autocomplete.scss
Normal file
|
@ -0,0 +1,145 @@
|
|||
@import '../utils/base';
|
||||
|
||||
.react-tags {
|
||||
position: relative;
|
||||
padding: 5px 0 0 6px;
|
||||
border-radius: .3rem;
|
||||
background-color: var(--input-color);
|
||||
border: 1px solid var(--input-border-color);
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
|
||||
/* shared font styles */
|
||||
font-size: 1em;
|
||||
line-height: 1.2;
|
||||
|
||||
/* clicking anywhere will focus the input */
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.react-tags.is-focused {
|
||||
box-shadow: 0 0 0 .2rem rgb(70 150 229 / 25%);
|
||||
}
|
||||
|
||||
.react-tags__tag {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.react-tags__selected {
|
||||
display: inline;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
|
||||
.react-tags__selected-tag {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
margin: 0 6px 6px 0;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--input-border-color);
|
||||
border-radius: .25rem;
|
||||
background: #f1f1f1;
|
||||
|
||||
/* match the font styles */
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.react-tags__selected-tag:after {
|
||||
content: '\2715';
|
||||
color: #aaaaaa;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.react-tags__selected-tag:hover,
|
||||
.react-tags__selected-tag:focus {
|
||||
border-color: var(--input-border-color);
|
||||
}
|
||||
|
||||
.react-tags__search {
|
||||
display: inline-block;
|
||||
|
||||
/* match tag layout */
|
||||
padding: 6px 2px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
/* prevent autoresize overflowing the container */
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $smMin) {
|
||||
.react-tags__search {
|
||||
/* this will become the offsetParent for suggestions */
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tags__search-input {
|
||||
font-size: 1.25rem;
|
||||
line-height: inherit;
|
||||
color: var(--input-text-color);
|
||||
background-color: var(--input-color);
|
||||
|
||||
/* prevent autoresize overflowing the container */
|
||||
max-width: 100%;
|
||||
|
||||
/* remove styles and layout from this element */
|
||||
margin: 0 0 0 7px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-tags__search-input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-tags__suggestions {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $smMin) {
|
||||
.react-tags__suggestions {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tags__suggestions ul {
|
||||
margin: 4px -1px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: var(--primary-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: .25rem;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.react-tags__suggestions li {
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.react-tags__suggestions li:not(:last-child) {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.react-tags__suggestions li mark {
|
||||
text-decoration: underline;
|
||||
background: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.react-tags__suggestions li:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--active-color);
|
||||
}
|
||||
|
||||
.react-tags__suggestions li.is-active {
|
||||
background-color: var(--active-color);
|
||||
}
|
||||
|
||||
.react-tags__suggestions li.is-disabled {
|
||||
opacity: .5;
|
||||
cursor: auto;
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
@import '../utils/base';
|
||||
|
||||
.react-tagsinput {
|
||||
background-color: var(--input-color);
|
||||
border: 1px solid var(--input-border-color);
|
||||
border-radius: .25rem;
|
||||
overflow: hidden;
|
||||
min-height: 2.6rem;
|
||||
padding: .5rem 0 0 1rem;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
|
||||
}
|
||||
|
||||
.react-tagsinput--focused {
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 .2rem rgb(70 150 229 / 25%);
|
||||
}
|
||||
|
||||
.react-tagsinput-tag {
|
||||
font-size: 1rem;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
margin: 0 5px 6px 0;
|
||||
padding: 6px 8px;
|
||||
line-height: 1;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.react-tagsinput-remove {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.react-tagsinput-tag span:before {
|
||||
content: '\2715';
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.react-tagsinput-input {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: none;
|
||||
padding: 1px 0;
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
font-size: 1.25rem;
|
||||
color: var(--input-text-color);
|
||||
}
|
||||
|
||||
.react-tagsinput-input::placeholder {
|
||||
color: $textPlaceholder;
|
||||
}
|
||||
|
||||
.react-autosuggest__suggestion--highlighted {
|
||||
background-color: var(--active-color);
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
@import './utils/base';
|
||||
@import 'node_modules/bootstrap/scss/bootstrap.scss';
|
||||
@import './common/react-tagsinput.scss';
|
||||
@import './common/react-tag-autocomplete.scss';
|
||||
@import './theme/theme';
|
||||
|
||||
* {
|
||||
|
|
|
@ -97,7 +97,7 @@ export const ShortUrlForm = (
|
|||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<TagsSelector tags={shortUrlData.tags ?? []} onChange={changeTags} />
|
||||
<TagsSelector selectedTags={shortUrlData.tags ?? []} onChange={changeTags} />
|
||||
</FormGroup>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import { FC } from 'react';
|
||||
import { FC, MouseEventHandler } from 'react';
|
||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
||||
import './Tag.scss';
|
||||
|
||||
interface TagProps {
|
||||
colorGenerator: ColorGenerator;
|
||||
text: string;
|
||||
className?: string;
|
||||
clearable?: boolean;
|
||||
onClick?: () => void;
|
||||
onClose?: () => void;
|
||||
onClick?: MouseEventHandler;
|
||||
onClose?: MouseEventHandler;
|
||||
}
|
||||
|
||||
const Tag: FC<TagProps> = ({ text, children, clearable, colorGenerator, onClick, onClose }) => (
|
||||
const Tag: FC<TagProps> = ({ text, children, clearable, className = '', colorGenerator, onClick, onClose }) => (
|
||||
<span
|
||||
className="badge tag"
|
||||
className={`badge tag ${className}`}
|
||||
style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable || !onClick ? 'auto' : 'pointer' }}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
@import '../../utils/base';
|
||||
|
||||
.react-autosuggest__suggestions-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.react-autosuggest__suggestion {
|
||||
margin-left: -6px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.react-autosuggest__suggestion--highlighted {
|
||||
background-color: $lightGrey;
|
||||
}
|
|
@ -1,13 +1,12 @@
|
|||
import { ChangeEvent, useEffect } from 'react';
|
||||
import TagsInput, { RenderInputProps, RenderTagProps } from 'react-tagsinput';
|
||||
import Autosuggest, { ChangeEvent as AutoChangeEvent, SuggestionSelectedEventData } from 'react-autosuggest';
|
||||
import { useEffect } from 'react';
|
||||
import ReactTags, { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete';
|
||||
import ColorGenerator from '../../utils/services/ColorGenerator';
|
||||
import { TagsList } from '../reducers/tagsList';
|
||||
import TagBullet from './TagBullet';
|
||||
import './TagsSelector.scss';
|
||||
import Tag from './Tag';
|
||||
|
||||
export interface TagsSelectorProps {
|
||||
tags: string[];
|
||||
selectedTags: string[];
|
||||
onChange: (tags: string[]) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
@ -17,67 +16,44 @@ interface TagsSelectorConnectProps extends TagsSelectorProps {
|
|||
tagsList: TagsList;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
const toComponentTag = (tag: string) => ({ id: tag, name: tag });
|
||||
|
||||
const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||
{ tags, onChange, listTags, tagsList, placeholder = 'Add tags to the URL' }: TagsSelectorConnectProps,
|
||||
{ selectedTags, onChange, listTags, tagsList, placeholder = 'Add tags to the URL' }: TagsSelectorConnectProps,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
listTags();
|
||||
}, []);
|
||||
|
||||
const renderTag = (
|
||||
{ tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other }: RenderTagProps<string>,
|
||||
) => (
|
||||
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
|
||||
{getTagDisplayValue(tag)}
|
||||
{!disabled && <span className={classNameRemove} onClick={() => onRemove(key)} />}
|
||||
</span>
|
||||
const renderTag = ({ tag, onDelete }: TagComponentProps) =>
|
||||
<Tag colorGenerator={colorGenerator} text={tag.name} clearable className="react-tags__tag" onClose={onDelete} />;
|
||||
const renderSuggestion = ({ item }: SuggestionComponentProps) => (
|
||||
<>
|
||||
<TagBullet tag={`${item.name}`} colorGenerator={colorGenerator} />
|
||||
{item.name}
|
||||
</>
|
||||
);
|
||||
const renderAutocompleteInput = (data: RenderInputProps<string>) => {
|
||||
const { addTag, ...otherProps } = data;
|
||||
const handleOnChange = (e: ChangeEvent<HTMLInputElement>, { method }: AutoChangeEvent) => {
|
||||
method === 'enter' ? e.preventDefault() : otherProps.onChange(e);
|
||||
};
|
||||
|
||||
const inputValue = otherProps.value?.trim().toLowerCase() ?? '';
|
||||
const suggestions = tagsList.tags.filter((tag) => tag.startsWith(inputValue));
|
||||
|
||||
return (
|
||||
<Autosuggest
|
||||
ref={otherProps.ref}
|
||||
suggestions={suggestions}
|
||||
inputProps={{ ...otherProps, onChange: handleOnChange }}
|
||||
highlightFirstSuggestion
|
||||
shouldRenderSuggestions={(value: string) => value.trim().length > 0}
|
||||
getSuggestionValue={(suggestion) => suggestion}
|
||||
renderSuggestion={(suggestion) => (
|
||||
<>
|
||||
<TagBullet tag={suggestion} colorGenerator={colorGenerator} />
|
||||
{suggestion}
|
||||
</>
|
||||
)}
|
||||
onSuggestionsFetchRequested={noop}
|
||||
onSuggestionsClearRequested={noop}
|
||||
onSuggestionSelected={(_, { suggestion }: SuggestionSelectedEventData<string>) => {
|
||||
addTag(suggestion);
|
||||
<ReactTags
|
||||
tags={selectedTags.map(toComponentTag)}
|
||||
tagComponent={renderTag}
|
||||
suggestions={tagsList.tags.filter((tag) => !selectedTags.includes(tag)).map(toComponentTag)}
|
||||
suggestionComponent={renderSuggestion}
|
||||
allowNew
|
||||
placeholderText={placeholder}
|
||||
onDelete={(removedTagIndex) => {
|
||||
selectedTags.splice(removedTagIndex, 1);
|
||||
|
||||
onChange(selectedTags);
|
||||
}}
|
||||
onAddition={({ name: newTag }) => {
|
||||
const tags = [ ...selectedTags, newTag.toLowerCase() ]; // eslint-disable-line @typescript-eslint/no-unsafe-call
|
||||
|
||||
onChange(tags);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TagsInput
|
||||
value={tags}
|
||||
inputProps={{ placeholder }}
|
||||
onlyUnique
|
||||
renderTag={renderTag}
|
||||
renderInput={renderAutocompleteInput}
|
||||
// FIXME Workaround to be able to add tags on Android
|
||||
addOnBlur
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagsSelector;
|
||||
|
|
Loading…
Reference in a new issue