From ede4f4662c1aa625b82ca73b7e11758915cbb460 Mon Sep 17 00:00:00 2001 From: Fedor Katurov Date: Sat, 31 Oct 2020 18:09:15 +0700 Subject: [PATCH] refactored tag component --- .../containers/TagField/styles.scss | 4 + src/components/node/NodeTags/index.tsx | 2 +- src/components/node/NodeTags/placeholder.tsx | 17 --- .../node/NodeTagsPlaceholder/index.tsx | 15 +++ src/components/node/Tag/index.tsx | 69 ---------- src/components/node/Tags/index.tsx | 118 ----------------- src/components/tags/Tag/index.tsx | 43 +++++++ src/components/tags/Tag/styles.scss | 4 + src/components/tags/TagAutocomplete/index.tsx | 9 ++ .../tags/TagAutocomplete/styles.module.scss | 11 ++ src/components/tags/TagInput/index.tsx | 121 ++++++++++++++++++ src/components/tags/TagWrapper/index.tsx | 40 ++++++ .../TagWrapper/styles.module.scss} | 5 +- src/components/tags/Tags/index.tsx | 90 +++++++++++++ src/containers/sidebars/TagSidebar/index.tsx | 2 +- webpack.config.js | 1 + 16 files changed, 344 insertions(+), 207 deletions(-) delete mode 100644 src/components/node/NodeTags/placeholder.tsx create mode 100644 src/components/node/NodeTagsPlaceholder/index.tsx delete mode 100644 src/components/node/Tag/index.tsx delete mode 100644 src/components/node/Tags/index.tsx create mode 100644 src/components/tags/Tag/index.tsx create mode 100644 src/components/tags/Tag/styles.scss create mode 100644 src/components/tags/TagAutocomplete/index.tsx create mode 100644 src/components/tags/TagAutocomplete/styles.module.scss create mode 100644 src/components/tags/TagInput/index.tsx create mode 100644 src/components/tags/TagWrapper/index.tsx rename src/components/{node/Tag/styles.scss => tags/TagWrapper/styles.module.scss} (97%) create mode 100644 src/components/tags/Tags/index.tsx diff --git a/src/components/containers/TagField/styles.scss b/src/components/containers/TagField/styles.scss index f65e12d0..18edac3e 100644 --- a/src/components/containers/TagField/styles.scss +++ b/src/components/containers/TagField/styles.scss @@ -3,4 +3,8 @@ align-items: flex-start; justify-content: flex-start; flex-wrap: wrap; + + &> * { + margin: 0 $gap $gap 0; + } } diff --git a/src/components/node/NodeTags/index.tsx b/src/components/node/NodeTags/index.tsx index 910d2590..d5c48923 100644 --- a/src/components/node/NodeTags/index.tsx +++ b/src/components/node/NodeTags/index.tsx @@ -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; diff --git a/src/components/node/NodeTags/placeholder.tsx b/src/components/node/NodeTags/placeholder.tsx deleted file mode 100644 index f5b015b4..00000000 --- a/src/components/node/NodeTags/placeholder.tsx +++ /dev/null @@ -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 = memo( - ({ is_editable, tags, onChange }) => ( - - ) -); - -export { NodeTagsPlaceholder }; diff --git a/src/components/node/NodeTagsPlaceholder/index.tsx b/src/components/node/NodeTagsPlaceholder/index.tsx new file mode 100644 index 00000000..4fb8e75d --- /dev/null +++ b/src/components/node/NodeTagsPlaceholder/index.tsx @@ -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 = memo(({ is_editable, tags, onChange }) => ( + +)); + +export { NodeTagsPlaceholder }; diff --git a/src/components/node/Tag/index.tsx b/src/components/node/Tag/index.tsx deleted file mode 100644 index f9bb1c8a..00000000 --- a/src/components/node/Tag/index.tsx +++ /dev/null @@ -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) => { - if (tag.title.substr(0, 1) === '/') return 'green'; - - return ''; -}; - -interface IProps { - tag: Partial; - size?: 'normal' | 'big'; - - is_hoverable?: boolean; - is_editing?: boolean; - - onInput?: ChangeEventHandler; - onKeyUp?: KeyboardEventHandler; - onBlur?: FocusEventHandler; - onClick?: (tag: Partial) => void; -} - -const Tag: FC = ({ - tag, - is_hoverable, - is_editing, - size = 'normal', - onInput, - onKeyUp, - onBlur, - onClick, -}) => { - const onClickHandler = useCallback(() => { - if (!onClick) return; - onClick(tag); - }, [tag, onClick]); - - return ( -
-
-
{tag.title}
- - {onInput && ( - - )} -
- ); -}; - -export { Tag }; diff --git a/src/components/node/Tags/index.tsx b/src/components/node/Tags/index.tsx deleted file mode 100644 index 894dde37..00000000 --- a/src/components/node/Tags/index.tsx +++ /dev/null @@ -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 & { - tags: Partial[]; - is_editable?: boolean; - onTagsChange?: (tags: string[]) => void; - onTagClick?: (tag: Partial) => void; -}; - -export const Tags: FC = ({ 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) => { - 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 ( - - {catTags.map(tag => ( - - ))} - - {ordinaryTags.map(tag => ( - - ))} - - {data.map(tag => ( - - ))} - - {is_editable && ( - - )} - - ); -}; diff --git a/src/components/tags/Tag/index.tsx b/src/components/tags/Tag/index.tsx new file mode 100644 index 00000000..2dcbc077 --- /dev/null +++ b/src/components/tags/Tag/index.tsx @@ -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) => { + if (tag.title.substr(0, 1) === '/') return 'green'; + + return ''; +}; + +interface IProps { + tag: Partial; + size?: 'normal' | 'big'; + + is_hoverable?: boolean; + is_editing?: boolean; + + onBlur?: FocusEventHandler; + onClick?: (tag: Partial) => void; +} + +const Tag: FC = ({ tag, is_hoverable, is_editing, size = 'normal', onBlur, onClick }) => { + const onClickHandler = useCallback(() => { + if (!onClick) return; + onClick(tag); + }, [tag, onClick]); + + return ( +
+ +
+ ); +}; + +export { Tag }; diff --git a/src/components/tags/Tag/styles.scss b/src/components/tags/Tag/styles.scss new file mode 100644 index 00000000..a834b87e --- /dev/null +++ b/src/components/tags/Tag/styles.scss @@ -0,0 +1,4 @@ +.wrap { + background-color: blue; + position: relative; +} diff --git a/src/components/tags/TagAutocomplete/index.tsx b/src/components/tags/TagAutocomplete/index.tsx new file mode 100644 index 00000000..69ce26fa --- /dev/null +++ b/src/components/tags/TagAutocomplete/index.tsx @@ -0,0 +1,9 @@ +import React, { FC } from 'react'; +import styles from './styles.module.scss'; +import classNames from 'classnames'; + +interface IProps {} + +const TagAutocomplete: FC = () =>
auto
; + +export { TagAutocomplete }; diff --git a/src/components/tags/TagAutocomplete/styles.module.scss b/src/components/tags/TagAutocomplete/styles.module.scss new file mode 100644 index 00000000..740c4a96 --- /dev/null +++ b/src/components/tags/TagAutocomplete/styles.module.scss @@ -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; +} diff --git a/src/components/tags/TagInput/index.tsx b/src/components/tags/TagInput/index.tsx new file mode 100644 index 00000000..cae76eb1 --- /dev/null +++ b/src/components/tags/TagInput/index.tsx @@ -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 = ({ onAppend, onClearTag, onSubmit }) => { + const [focused, setFocused] = useState(false); + const [input, setInput] = useState(''); + const ref = useRef(null); + + const onInput = useCallback( + ({ target: { value } }: ChangeEvent) => { + 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>(() => { + 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 ( + + {onInput && } + + + + ); +}; + +export { TagInput }; diff --git a/src/components/tags/TagWrapper/index.tsx b/src/components/tags/TagWrapper/index.tsx new file mode 100644 index 00000000..2eb0f28e --- /dev/null +++ b/src/components/tags/TagWrapper/index.tsx @@ -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 = ({ + children, + feature, + size, + is_hoverable, + is_editing, + has_input, + onClick, + title = '', +}) => ( +
+
+
{title}
+ {children} +
+); + +export { TagWrapper }; diff --git a/src/components/node/Tag/styles.scss b/src/components/tags/TagWrapper/styles.module.scss similarity index 97% rename from src/components/node/Tag/styles.scss rename to src/components/tags/TagWrapper/styles.module.scss index 1fb4d6df..87e71e1b 100644 --- a/src/components/node/Tag/styles.scss +++ b/src/components/tags/TagWrapper/styles.module.scss @@ -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; } + diff --git a/src/components/tags/Tags/index.tsx b/src/components/tags/Tags/index.tsx new file mode 100644 index 00000000..2cf064fb --- /dev/null +++ b/src/components/tags/Tags/index.tsx @@ -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 & { + tags: Partial[]; + is_editable?: boolean; + onTagsChange?: (tags: string[]) => void; + onTagClick?: (tag: Partial) => void; +}; + +export const Tags: FC = ({ tags, is_editable, onTagsChange, onTagClick, ...props }) => { + const [data, setData] = useState([]); + + 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 ( + + {catTags.map(tag => ( + + ))} + + {ordinaryTags.map(tag => ( + + ))} + + {data.map(title => ( + + ))} + + {is_editable && ( + + )} + + ); +}; diff --git a/src/containers/sidebars/TagSidebar/index.tsx b/src/containers/sidebars/TagSidebar/index.tsx index bcef7151..edeca2d0 100644 --- a/src/containers/sidebars/TagSidebar/index.tsx +++ b/src/containers/sidebars/TagSidebar/index.tsx @@ -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), diff --git a/webpack.config.js b/webpack.config.js index 6cb6b064..f6bc4dd1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -176,6 +176,7 @@ module.exports = () => { contentBase: 'dist', publicPath: '/', hot: true, + open: false, }, }; };