mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 18:27:25 +03:00
Define react-tag-autocomplete styles for v7
This commit is contained in:
parent
3436b52c06
commit
e4aec16ba5
4 changed files with 73 additions and 65 deletions
|
@ -4,13 +4,14 @@ import { faDesktop } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Checkbox, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
import { Checkbox, SimpleCard } from '@shlinkio/shlink-frontend-kit';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { isEmpty, pipe, replace, trim } from 'ramda';
|
import { isEmpty } from 'ramda';
|
||||||
import type { ChangeEvent, FC } from 'react';
|
import type { ChangeEvent, FC } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
import { Button, FormGroup, Input, Row } from 'reactstrap';
|
||||||
import type { InputType } from 'reactstrap/types/lib/Input';
|
import type { InputType } from 'reactstrap/types/lib/Input';
|
||||||
import type { ShlinkCreateShortUrlData, ShlinkDeviceLongUrls, ShlinkEditShortUrlData } from '../api-contract';
|
import type { ShlinkCreateShortUrlData, ShlinkDeviceLongUrls, ShlinkEditShortUrlData } from '../api-contract';
|
||||||
import type { DomainSelectorProps } from '../domains/DomainSelector';
|
import type { DomainSelectorProps } from '../domains/DomainSelector';
|
||||||
|
import { normalizeTag } from '../tags/helpers';
|
||||||
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
import type { TagsSelectorProps } from '../tags/helpers/TagsSelector';
|
||||||
import { IconInput } from '../utils/components/IconInput';
|
import { IconInput } from '../utils/components/IconInput';
|
||||||
import type { DateTimeInputProps } from '../utils/dates/DateTimeInput';
|
import type { DateTimeInputProps } from '../utils/dates/DateTimeInput';
|
||||||
|
@ -35,7 +36,6 @@ export interface ShortUrlFormProps<T extends ShlinkCreateShortUrlData | ShlinkEd
|
||||||
onSave: (shortUrlData: T) => Promise<unknown>;
|
onSave: (shortUrlData: T) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeTag = pipe(trim, replace(/ /g, '-'));
|
|
||||||
const toDate = (date?: string | Date): Date | undefined => (typeof date === 'string' ? parseISO(date) : date);
|
const toDate = (date?: string | Date): Date | undefined => (typeof date === 'string' ? parseISO(date) : date);
|
||||||
|
|
||||||
const isCreationData = (data: ShlinkCreateShortUrlData | ShlinkEditShortUrlData): data is ShlinkCreateShortUrlData =>
|
const isCreationData = (data: ShlinkCreateShortUrlData | ShlinkEditShortUrlData): data is ShlinkCreateShortUrlData =>
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { useEffect } from 'react';
|
import resolveClasses from 'classnames';
|
||||||
import type { OptionRendererProps, TagRendererProps, TagSuggestion } from 'react-tag-autocomplete';
|
import { useEffect, useRef } from 'react';
|
||||||
|
import type { OptionRendererProps, ReactTagsAPI, TagRendererProps, TagSuggestion } from 'react-tag-autocomplete';
|
||||||
import { ReactTags } from 'react-tag-autocomplete';
|
import { ReactTags } from 'react-tag-autocomplete';
|
||||||
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
|
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
|
||||||
import { useSetting } from '../../utils/settings';
|
import { useSetting } from '../../utils/settings';
|
||||||
import type { TagsList } from '../reducers/tagsList';
|
import type { TagsList } from '../reducers/tagsList';
|
||||||
|
import { normalizeTag } from './index';
|
||||||
import { Tag } from './Tag';
|
import { Tag } from './Tag';
|
||||||
import { TagBullet } from './TagBullet';
|
import { TagBullet } from './TagBullet';
|
||||||
|
|
||||||
|
@ -19,6 +21,10 @@ interface TagsSelectorConnectProps extends TagsSelectorProps {
|
||||||
tagsList: TagsList;
|
tagsList: TagsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NOT_FOUND_TAG = 'Tag not found';
|
||||||
|
const NEW_TAG = 'Add tag';
|
||||||
|
const isSelectableOption = (tag: string) => tag !== NOT_FOUND_TAG;
|
||||||
|
const isNewOption = (tag: string) => tag === NEW_TAG;
|
||||||
const toTagSuggestion = (tag: string): TagSuggestion => ({ label: tag, value: tag });
|
const toTagSuggestion = (tag: string): TagSuggestion => ({ label: tag, value: tag });
|
||||||
|
|
||||||
export const TagsSelector = (colorGenerator: ColorGenerator) => (
|
export const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||||
|
@ -30,33 +36,51 @@ export const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||||
|
|
||||||
const shortUrlCreation = useSetting('shortUrlCreation');
|
const shortUrlCreation = useSetting('shortUrlCreation');
|
||||||
const searchMode = shortUrlCreation?.tagFilteringMode ?? 'startsWith';
|
const searchMode = shortUrlCreation?.tagFilteringMode ?? 'startsWith';
|
||||||
|
const apiRef = useRef<ReactTagsAPI>(null);
|
||||||
|
|
||||||
const ReactTagsTag = ({ tag, onClick: deleteTag }: TagRendererProps) => (
|
const ReactTagsTag = ({ tag, onClick: deleteTag }: TagRendererProps) => (
|
||||||
<Tag colorGenerator={colorGenerator} text={tag.label} clearable className="react-tags__tag" onClose={deleteTag} />
|
<Tag colorGenerator={colorGenerator} text={tag.label} clearable className="react-tags__tag" onClose={deleteTag} />
|
||||||
);
|
);
|
||||||
const ReactTagsSuggestion = ({ option }: OptionRendererProps) => (
|
const ReactTagsSuggestion = ({ option, classNames }: OptionRendererProps) => {
|
||||||
|
const isSelectable = isSelectableOption(option.label);
|
||||||
|
const isNew = isNewOption(option.label);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={resolveClasses(classNames.option, {
|
||||||
|
[classNames.optionIsActive]: isSelectable && option.active,
|
||||||
|
'react-tags__listbox-option--not-selectable': !isSelectable,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{!isSelectable ? <i>{option.label}</i> : (
|
||||||
<>
|
<>
|
||||||
<TagBullet tag={`${option.label}`} colorGenerator={colorGenerator} />
|
{!isNew && <TagBullet tag={`${option.label}`} colorGenerator={colorGenerator} />}
|
||||||
{option.label}
|
{!isNew ? option.label : <i>Add "{normalizeTag(apiRef.current?.input.value ?? '')}"</i>}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactTags
|
<ReactTags
|
||||||
|
ref={apiRef}
|
||||||
selected={selectedTags.map(toTagSuggestion)}
|
selected={selectedTags.map(toTagSuggestion)}
|
||||||
suggestions={tagsList.tags.filter((tag) => !selectedTags.includes(tag)).map(toTagSuggestion)}
|
suggestions={tagsList.tags.filter((tag) => !selectedTags.includes(tag)).map(toTagSuggestion)}
|
||||||
renderTag={ReactTagsTag}
|
renderTag={ReactTagsTag}
|
||||||
renderOption={ReactTagsSuggestion}
|
renderOption={ReactTagsSuggestion}
|
||||||
|
activateFirstOption
|
||||||
allowNew={allowNew}
|
allowNew={allowNew}
|
||||||
// addOnBlur TODO Implement manually
|
newOptionText={NEW_TAG}
|
||||||
|
noOptionsText={NOT_FOUND_TAG}
|
||||||
placeholderText={placeholder ?? 'Add tags to the URL'}
|
placeholderText={placeholder ?? 'Add tags to the URL'}
|
||||||
delimiterKeys={['Enter', 'Tab', ',']}
|
delimiterKeys={['Enter', 'Tab', ',']}
|
||||||
suggestionsTransform={
|
suggestionsTransform={
|
||||||
(query, suggestions) => {
|
(query, suggestions) => {
|
||||||
const searchTerm = query.toLowerCase().trim();
|
const searchTerm = query.toLowerCase().trim();
|
||||||
return searchTerm.length < 1 ? [] : suggestions.filter(
|
return searchTerm.length < 1 ? [] : [...suggestions.filter(
|
||||||
({ label }) => (searchMode === 'includes' ? label.includes(searchTerm) : label.startsWith(searchTerm)),
|
({ label }) => (searchMode === 'includes' ? label.includes(searchTerm) : label.startsWith(searchTerm)),
|
||||||
);
|
)].slice(0, 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onDelete={(removedTagIndex) => {
|
onDelete={(removedTagIndex) => {
|
||||||
|
@ -67,7 +91,7 @@ export const TagsSelector = (colorGenerator: ColorGenerator) => (
|
||||||
onAdd={({ label: newTag }) => onChange(
|
onAdd={({ label: newTag }) => onChange(
|
||||||
// * Avoid duplicated tags (thanks to the Set),
|
// * Avoid duplicated tags (thanks to the Set),
|
||||||
// * Split any of the new tags by comma, allowing to paste multiple comma-separated tags at once.
|
// * Split any of the new tags by comma, allowing to paste multiple comma-separated tags at once.
|
||||||
[...new Set([...selectedTags, ...newTag.toLowerCase().split(',')])],
|
[...new Set([...selectedTags, ...newTag.split(',')])],
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
3
shlink-web-component/src/tags/helpers/index.ts
Normal file
3
shlink-web-component/src/tags/helpers/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const ONE_OR_MORE_SPACES_REGEX = /\s+/g;
|
||||||
|
|
||||||
|
export const normalizeTag = (tag: string) => tag.trim().toLowerCase().replace(ONE_OR_MORE_SPACES_REGEX, '-');
|
|
@ -1,5 +1,6 @@
|
||||||
@import '../../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
@import '../../../node_modules/@shlinkio/shlink-frontend-kit/dist/base';
|
||||||
|
|
||||||
|
// Main wrapper
|
||||||
.react-tags {
|
.react-tags {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px 0 0 6px;
|
padding: 5px 0 0 6px;
|
||||||
|
@ -26,45 +27,35 @@
|
||||||
background-color: var(--input-color);
|
background-color: var(--input-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags.is-focused {
|
// Mimic bootstrap input focus ring
|
||||||
|
.react-tags.is-active {
|
||||||
box-shadow: 0 0 0 .2rem rgb(70 150 229 / 25%);
|
box-shadow: 0 0 0 .2rem rgb(70 150 229 / 25%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-tags__label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.react-tags__tag {
|
.react-tags__tag {
|
||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__selected {
|
.react-tags__list {
|
||||||
display: inline;
|
display: inline;
|
||||||
vertical-align: 2px;
|
vertical-align: 2px;
|
||||||
|
padding: 0;
|
||||||
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__selected-tag {
|
.react-tags__list-item {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
}
|
||||||
margin: 0 6px 6px 0;
|
.react-tags__list-item:not(:last-child) {
|
||||||
padding: 6px 8px;
|
margin-right: 3px;
|
||||||
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 {
|
// The block to search
|
||||||
content: '\2715';
|
.react-tags__combobox {
|
||||||
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;
|
display: inline-block;
|
||||||
|
|
||||||
/* match tag layout */
|
/* match tag layout */
|
||||||
|
@ -76,13 +67,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: $smMin) {
|
@media screen and (min-width: $smMin) {
|
||||||
.react-tags__search {
|
.react-tags__combobox {
|
||||||
/* this will become the offsetParent for suggestions */
|
/* this will become the offsetParent for suggestions */
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__search-input {
|
.react-tags__combobox-input {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
color: var(--input-text-color);
|
color: var(--input-text-color);
|
||||||
|
@ -98,62 +89,52 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__search-input::placeholder {
|
.react-tags__combobox-input::placeholder {
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__search-input::-ms-clear {
|
.react-tags__combobox-input::-ms-clear {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__suggestions {
|
.react-tags__listbox {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: $smMin) {
|
|
||||||
.react-tags__suggestions {
|
|
||||||
width: 240px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-tags__suggestions ul {
|
|
||||||
margin: 4px -1px;
|
margin: 4px -1px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
box-shadow: 0 2px 6px rgb(0 0 0 / .2);
|
box-shadow: 0 2px 6px rgb(0 0 0 / .2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__suggestions li {
|
@media screen and (min-width: $smMin) {
|
||||||
|
.react-tags__listbox {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-tags__listbox .react-tags__listbox-option {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__suggestions li:not(:last-child) {
|
.react-tags__listbox .react-tags__listbox-option:not(:last-child) {
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__suggestions li mark {
|
.react-tags__listbox .react-tags__listbox-option:hover:not(.react-tags__listbox-option--not-selectable) {
|
||||||
text-decoration: underline;
|
|
||||||
background: none;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.react-tags__suggestions li:hover {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--active-color);
|
background-color: var(--active-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__suggestions li.is-active {
|
.react-tags__listbox .react-tags__listbox-option.is-active {
|
||||||
background-color: var(--active-color);
|
background-color: var(--active-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-tags__suggestions li.is-disabled {
|
.react-tags__listbox .react-tags__listbox-option.is-disabled {
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue