1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-24 20:36:40 +07:00

refactored tag component

This commit is contained in:
Fedor Katurov 2020-10-31 18:09:15 +07:00
parent 01f52bcb63
commit ede4f4662c
16 changed files with 344 additions and 207 deletions

View file

@ -3,4 +3,8 @@
align-items: flex-start;
justify-content: flex-start;
flex-wrap: wrap;
&> * {
margin: 0 $gap $gap 0;
}
}

View file

@ -1,6 +1,6 @@
import React, { FC, memo } from 'react';
import { Tags } from '../Tags';
import { ITag } from '~/redux/types';
import { Tags } from '~/components/tags/Tags';
interface IProps {
is_editable?: boolean;

View file

@ -1,17 +0,0 @@
import React, { FC, memo } from "react";
import { Tags } from "../Tags";
import { ITag } from "~/redux/types";
interface IProps {
is_editable?: boolean;
tags: ITag[];
onChange?: (tags: string[]) => void;
}
const NodeTagsPlaceholder: FC<IProps> = memo(
({ is_editable, tags, onChange }) => (
<Tags tags={tags} is_editable={is_editable} onTagsChange={onChange} />
)
);
export { NodeTagsPlaceholder };

View file

@ -0,0 +1,15 @@
import React, { FC, memo } from 'react';
import { ITag } from '~/redux/types';
import { Tags } from '~/components/tags/Tags';
interface IProps {
is_editable?: boolean;
tags: ITag[];
onChange?: (tags: string[]) => void;
}
const NodeTagsPlaceholder: FC<IProps> = memo(({ is_editable, tags, onChange }) => (
<Tags tags={tags} is_editable={is_editable} onTagsChange={onChange} />
));
export { NodeTagsPlaceholder };

View file

@ -1,69 +0,0 @@
import React, { ChangeEventHandler, FC, FocusEventHandler, KeyboardEventHandler, useCallback, } from 'react';
import * as styles from './styles.scss';
import { ITag } from '~/redux/types';
import classNames = require('classnames');
const getTagFeature = (tag: Partial<ITag>) => {
if (tag.title.substr(0, 1) === '/') return 'green';
return '';
};
interface IProps {
tag: Partial<ITag>;
size?: 'normal' | 'big';
is_hoverable?: boolean;
is_editing?: boolean;
onInput?: ChangeEventHandler<HTMLInputElement>;
onKeyUp?: KeyboardEventHandler;
onBlur?: FocusEventHandler<HTMLInputElement>;
onClick?: (tag: Partial<ITag>) => void;
}
const Tag: FC<IProps> = ({
tag,
is_hoverable,
is_editing,
size = 'normal',
onInput,
onKeyUp,
onBlur,
onClick,
}) => {
const onClickHandler = useCallback(() => {
if (!onClick) return;
onClick(tag);
}, [tag, onClick]);
return (
<div
className={classNames(styles.tag, getTagFeature(tag), size, {
is_hoverable,
is_editing,
input: !!onInput,
clickable: !!onClick,
})}
onClick={onClickHandler}
>
<div className={styles.hole} />
<div className={styles.title}>{tag.title}</div>
{onInput && (
<input
type="text"
value={tag.title}
size={1}
placeholder="Добавить"
maxLength={24}
onChange={onInput}
onKeyUp={onKeyUp}
onBlur={onBlur}
/>
)}
</div>
);
};
export { Tag };

View file

@ -1,118 +0,0 @@
import React, {
ChangeEvent,
FC,
HTMLAttributes,
KeyboardEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { TagField } from '~/components/containers/TagField';
import { ITag } from '~/redux/types';
import { Tag } from '~/components/node/Tag';
import uniq from 'ramda/es/uniq';
type IProps = HTMLAttributes<HTMLDivElement> & {
tags: Partial<ITag>[];
is_editable?: boolean;
onTagsChange?: (tags: string[]) => void;
onTagClick?: (tag: Partial<ITag>) => void;
};
export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick, ...props }) => {
const [input, setInput] = useState('');
const [data, setData] = useState([]);
const timer = useRef(null);
const [catTags, ordinaryTags] = useMemo(
() =>
(tags || []).reduce(
(obj, tag) =>
tag.title.substr(0, 1) === '/' ? [[...obj[0], tag], obj[1]] : [obj[0], [...obj[1], tag]],
[[], []]
),
[tags]
);
const onInput = useCallback(
({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
clearTimeout(timer.current);
setInput(value);
},
[setInput, timer]
);
const onKeyUp = useCallback(
({ key }: KeyboardEvent) => {
if (key === 'Backspace' && input === '' && data.length) {
setData(data.slice(0, data.length - 1));
setInput(data[data.length - 1].title);
}
if (key === 'Enter' || key === ',' || key === 'Comma') {
setData(
uniq([
...data,
...input
.split(',')
.map((title: string) =>
title
.trim()
.substr(0, 32)
.toLowerCase()
)
.filter(el => el.length > 0)
.filter(el => !tags.some(tag => tag.title.trim() === el.trim()))
.map(title => ({
title,
})),
])
);
setInput('');
}
},
[input, setInput, data, setData]
);
const onSubmit = useCallback(() => {
const title = input && input.trim();
const items = (title ? [...data, { title }] : data)
.filter(tag => tag.title.length > 0)
.map(tag => ({
...tag,
title: tag.title.toLowerCase(),
}));
if (!items.length) return;
setData(items);
setInput('');
onTagsChange(uniq([...tags, ...items]).map(tag => tag.title));
}, [tags, data, onTagsChange, input, setInput]);
useEffect(() => {
setData(data.filter(({ title }) => !tags.some(tag => tag.title.trim() === title.trim())));
}, [tags]);
return (
<TagField {...props}>
{catTags.map(tag => (
<Tag key={tag.title} tag={tag} onClick={onTagClick} />
))}
{ordinaryTags.map(tag => (
<Tag key={tag.title} tag={tag} onClick={onTagClick} />
))}
{data.map(tag => (
<Tag key={tag.title} tag={tag} is_editing />
))}
{is_editable && (
<Tag tag={{ title: input }} onInput={onInput} onKeyUp={onKeyUp} onBlur={onSubmit} />
)}
</TagField>
);
};

View file

@ -0,0 +1,43 @@
import React, { FC, FocusEventHandler, useCallback, } from 'react';
import * as styles from './styles.scss';
import { ITag } from '~/redux/types';
import { TagWrapper } from '~/components/tags/TagWrapper';
const getTagFeature = (tag: Partial<ITag>) => {
if (tag.title.substr(0, 1) === '/') return 'green';
return '';
};
interface IProps {
tag: Partial<ITag>;
size?: 'normal' | 'big';
is_hoverable?: boolean;
is_editing?: boolean;
onBlur?: FocusEventHandler<HTMLInputElement>;
onClick?: (tag: Partial<ITag>) => void;
}
const Tag: FC<IProps> = ({ tag, is_hoverable, is_editing, size = 'normal', onBlur, onClick }) => {
const onClickHandler = useCallback(() => {
if (!onClick) return;
onClick(tag);
}, [tag, onClick]);
return (
<div className={styles.wrap}>
<TagWrapper
feature={getTagFeature(tag)}
size={size}
is_hoverable={is_hoverable}
is_editing={is_editing}
onClick={onClick && onClickHandler}
title={tag.title}
/>
</div>
);
};
export { Tag };

View file

@ -0,0 +1,4 @@
.wrap {
background-color: blue;
position: relative;
}

View file

@ -0,0 +1,9 @@
import React, { FC } from 'react';
import styles from './styles.module.scss';
import classNames from 'classnames';
interface IProps {}
const TagAutocomplete: FC<IProps> = () => <div className={classNames(styles.window)}>auto</div>;
export { TagAutocomplete };

View file

@ -0,0 +1,11 @@
.window {
display: none;
position: absolute;
top: 0;
right: 0;
width: calc(90vw - 20px);
max-width: 300px;
background: red;
height: 100px;
z-index: -1;
}

View file

@ -0,0 +1,121 @@
import React, {
ChangeEvent,
FC,
FocusEventHandler,
KeyboardEvent,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import { TagAutocomplete } from '~/components/tags/TagAutocomplete';
import { TagWrapper } from '~/components/tags/TagWrapper';
const placeholder = 'Добавить';
const prepareInput = (input: string): string[] => {
return input
.split(',')
.map((title: string) =>
title
.trim()
.substr(0, 32)
.toLowerCase()
)
.filter(el => el.length > 0);
};
interface IProps {
onAppend: (tags: string[]) => void;
onClearTag: () => string | undefined;
onSubmit: (last: string[]) => void;
}
const TagInput: FC<IProps> = ({ onAppend, onClearTag, onSubmit }) => {
const [focused, setFocused] = useState(false);
const [input, setInput] = useState('');
const ref = useRef<HTMLInputElement>(null);
const onInput = useCallback(
({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
if (!value.trim()) {
setInput(value);
return;
}
const items = prepareInput(value);
if (items.length > 1) {
onAppend(items.slice(0, items.length - 1));
}
setInput(items[items.length - 1]);
},
[setInput]
);
const onKeyUp = useCallback(
({ key }: KeyboardEvent) => {
if (key === 'Escape' && ref.current) {
setInput('');
ref.current.blur();
return;
}
if (key === 'Backspace' && input === '') {
setInput(onClearTag() || '');
return;
}
if (key === 'Enter' || key === ',' || key === 'Comma') {
const created = prepareInput(input);
if (created.length) {
onAppend(created);
}
setInput('');
}
if (key === 'Enter' && ref.current) {
ref.current.blur();
}
},
[input, setInput, onClearTag, onAppend, onSubmit, ref.current]
);
const onFocus = useCallback(() => setFocused(true), []);
const onBlur = useCallback<FocusEventHandler<HTMLInputElement>>(() => {
setFocused(false);
if (input.trim()) {
const created = prepareInput(input);
onAppend(created);
setInput('');
onSubmit(created);
}
}, [input, onAppend, setInput, onSubmit]);
const feature = useMemo(() => (input.substr(0, 1) === '/' ? 'green' : ''), [input]);
return (
<TagWrapper title={input || placeholder} has_input={true} feature={feature}>
{onInput && <TagAutocomplete />}
<input
type="text"
value={input}
size={1}
placeholder={placeholder}
maxLength={24}
onChange={onInput}
onKeyUp={onKeyUp}
onBlur={onBlur}
onFocus={onFocus}
ref={ref}
/>
</TagWrapper>
);
};
export { TagInput };

View file

@ -0,0 +1,40 @@
import React, { FC } from 'react';
import classNames from 'classnames';
import styles from './styles.module.scss';
interface IProps {
feature?: string;
size?: string;
is_hoverable?: boolean;
is_editing?: boolean;
has_input?: boolean;
onClick?: () => void;
title?: string;
}
const TagWrapper: FC<IProps> = ({
children,
feature,
size,
is_hoverable,
is_editing,
has_input,
onClick,
title = '',
}) => (
<div
className={classNames(styles.tag, feature, size, {
is_hoverable,
is_editing,
input: has_input,
clickable: onClick,
})}
onClick={onClick}
>
<div className={styles.hole} />
<div className={styles.title}>{title}</div>
{children}
</div>
);
export { TagWrapper };

View file

@ -14,8 +14,9 @@ $big: 1.2;
font: $font_14_semibold;
align-self: flex-start;
padding: 0 8px 0 0;
margin: 0 $gap $gap 0;
//margin: 0 $gap $gap 0;
position: relative;
z-index: 4;
&:global(.big) {
height: $tag_height * $big;
@ -88,6 +89,7 @@ $big: 1.2;
top: 0;
bottom: 0;
width: 100%;
min-width: 100px;
padding-left: $tag_height;
padding-right: 5px;
box-sizing: border-box;
@ -118,3 +120,4 @@ $big: 1.2;
overflow: hidden;
text-overflow: ellipsis;
}

View file

@ -0,0 +1,90 @@
import React, { FC, HTMLAttributes, useCallback, useEffect, useMemo, useState, } from 'react';
import { TagField } from '~/components/containers/TagField';
import { ITag } from '~/redux/types';
import uniq from 'ramda/es/uniq';
import { Tag } from '~/components/tags/Tag';
import { TagInput } from '~/components/tags/TagInput';
type IProps = HTMLAttributes<HTMLDivElement> & {
tags: Partial<ITag>[];
is_editable?: boolean;
onTagsChange?: (tags: string[]) => void;
onTagClick?: (tag: Partial<ITag>) => void;
};
export const Tags: FC<IProps> = ({ tags, is_editable, onTagsChange, onTagClick, ...props }) => {
const [data, setData] = useState<string[]>([]);
const [catTags, ordinaryTags] = useMemo(
() =>
(tags || []).reduce(
(obj, tag) =>
tag.title.substr(0, 1) === '/' ? [[...obj[0], tag], obj[1]] : [obj[0], [...obj[1], tag]],
[[], []]
),
[tags]
);
const onSubmit = useCallback(
(last: string[]) => {
const exist = tags.map(tag => tag.title);
onTagsChange(uniq([...exist, ...data, ...last]));
},
[data]
);
//
// const onSubmit = useCallback(() => {
// const title = input && input.trim();
// const items = (title ? [...data, { title }] : data)
// .filter(tag => tag.title.length > 0)
// .map(tag => ({
// ...tag,
// title: tag.title.toLowerCase(),
// }));
//
// if (!items.length) return;
//
// setData(items);
// setInput('');
// onTagsChange(uniq([...tags, ...items]).map(tag => tag.title));
// }, [tags, data, onTagsChange, input, setInput]);
useEffect(() => {
setData(data.filter(title => !tags.some(tag => tag.title.trim() === title.trim())));
}, [tags]);
const onAppendTag = useCallback(
(created: string[]) => {
setData(uniq([...data, ...created]).filter(title => !tags.some(it => it.title === title)));
},
[data, setData, tags]
);
const onClearTag = useCallback((): string | undefined => {
if (!data.length) return;
const last = data[data.length - 1];
setData(data.slice(0, data.length - 1));
return last;
}, [data, setData]);
return (
<TagField {...props}>
{catTags.map(tag => (
<Tag key={tag.title} tag={tag} onClick={onTagClick} />
))}
{ordinaryTags.map(tag => (
<Tag key={tag.title} tag={tag} onClick={onTagClick} />
))}
{data.map(title => (
<Tag key={title} tag={{ title }} is_editing />
))}
{is_editable && (
<TagInput onAppend={onAppendTag} onClearTag={onClearTag} onSubmit={onSubmit} />
)}
</TagField>
);
};

View file

@ -2,7 +2,6 @@ import React, { FC, useCallback, useEffect, useMemo } from 'react';
import { SidebarWrapper } from '~/containers/sidebars/SidebarWrapper';
import styles from './styles.module.scss';
import { useHistory, useRouteMatch } from 'react-router';
import { Tag } from '~/components/node/Tag';
import { Icon } from '~/components/input/Icon';
import { Link } from 'react-router-dom';
import { TagSidebarList } from '~/components/sidebar/TagSidebarList';
@ -11,6 +10,7 @@ import { selectTagNodes } from '~/redux/tag/selectors';
import * as ACTIONS from '~/redux/tag/actions';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import { InfiniteScroll } from '~/components/containers/InfiniteScroll';
import { Tag } from '~/components/tags/Tag';
const mapStateToProps = state => ({
nodes: selectTagNodes(state),

View file

@ -176,6 +176,7 @@ module.exports = () => {
contentBase: 'dist',
publicPath: '/',
hot: true,
open: false,
},
};
};