From c3304154e2c9f9e8377289573f1f21f710c34275 Mon Sep 17 00:00:00 2001 From: wwsun Date: Sun, 8 Sep 2024 13:05:10 +0800 Subject: [PATCH] feat: add basic classNameInput --- .../src/ui/classname-input.stories.tsx | 10 + .../designer/src/setters/classname-setter.tsx | 27 +++ packages/ui/src/classname-input.tsx | 181 ++++++++++++++++++ packages/ui/src/index.ts | 1 + 4 files changed, 219 insertions(+) create mode 100644 apps/storybook/src/ui/classname-input.stories.tsx create mode 100644 packages/designer/src/setters/classname-setter.tsx create mode 100644 packages/ui/src/classname-input.tsx diff --git a/apps/storybook/src/ui/classname-input.stories.tsx b/apps/storybook/src/ui/classname-input.stories.tsx new file mode 100644 index 0000000..250964c --- /dev/null +++ b/apps/storybook/src/ui/classname-input.stories.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { ClassNameInput } from '@music163/tango-ui'; + +export default { + title: 'UI/ClassNameInput', +}; + +export function Basic() { + return ; +} diff --git a/packages/designer/src/setters/classname-setter.tsx b/packages/designer/src/setters/classname-setter.tsx new file mode 100644 index 0000000..bf05b27 --- /dev/null +++ b/packages/designer/src/setters/classname-setter.tsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; + +function ClassNameInput() { + const [classNames, setClassNames] = useState([]); + + const handleInputChange = (event: React.ChangeEvent) => { + const input = event.target.value; + const classes = input.split(' ').filter((className) => className.trim() !== ''); + setClassNames(classes); + }; + + return ( +
+ +
+ 当前 class 列表: + {classNames.map((className, index) => ( + + {className} + + ))} +
+
+ ); +} + +export default ClassNameInput; diff --git a/packages/ui/src/classname-input.tsx b/packages/ui/src/classname-input.tsx new file mode 100644 index 0000000..f2ede2b --- /dev/null +++ b/packages/ui/src/classname-input.tsx @@ -0,0 +1,181 @@ +import React, { useState, useRef, KeyboardEvent, ChangeEvent, useEffect } from 'react'; +import styled from 'styled-components'; + +const InputWrapper = styled.div` + display: flex; + flex-wrap: wrap; + align-items: center; + padding: 4px; + border: 1px solid #d9d9d9; + border-radius: 4px; + min-height: 40px; + position: relative; +`; + +const ClassBadge = styled.span` + background-color: #ff4d4f; + color: white; + padding: 2px 8px; + margin: 2px; + border-radius: 16px; + font-size: 14px; +`; + +const Input = styled.input` + flex: 1; + border: none; + outline: none; + padding: 4px; + font-size: 16px; + min-width: 50px; +`; + +const SuggestionList = styled.ul` + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: white; + border: 1px solid #d9d9d9; + border-top: none; + list-style-type: none; + padding: 0; + margin: 0; + max-height: 200px; + overflow-y: auto; + z-index: 1; +`; + +const SuggestionItem = styled.li<{ isHighlighted: boolean }>` + padding: 8px; + cursor: pointer; + background-color: ${(props) => (props.isHighlighted ? '#e6f7ff' : 'transparent')}; + &:hover { + background-color: #f0f0f0; + } +`; + +// 这里只是一个简化的 Tailwind CSS 类名列表,实际使用时应该包含更多类名 +const tailwindClasses = [ + 'text-red-500', + 'bg-blue-300', + 'p-4', + 'm-2', + 'flex', + 'items-center', + 'justify-between', + 'rounded-lg', + 'shadow-md', + 'hover:bg-gray-100', + 'focus:outline-none', + 'transition', +]; + +export function ClassNameInput() { + const [classNames, setClassNames] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [suggestions, setSuggestions] = useState([]); + const [highlightedIndex, setHighlightedIndex] = useState(-1); + const inputRef = useRef(null); + + const isValidClassName = (className: string) => { + return /^[a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)*$/.test(className); + }; + + const handleInputKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) { + addClassName(suggestions[highlightedIndex]); + } else { + addClassName(inputValue.trim()); + } + } else if (event.key === 'ArrowDown') { + event.preventDefault(); + setHighlightedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0)); + } else if (event.key === 'ArrowUp') { + event.preventDefault(); + setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1)); + } else if (event.key === 'Backspace' && inputValue === '' && classNames.length > 0) { + event.preventDefault(); + const newClassNames = [...classNames]; + newClassNames.pop(); + setClassNames(newClassNames); + } + }; + + const handleInputChange = (event: ChangeEvent) => { + const input = event.target.value; + setInputValue(input); + if (input) { + const matchedSuggestions = tailwindClasses.filter( + (className) => className.startsWith(input) && !classNames.includes(className), + ); + setSuggestions(matchedSuggestions); + setHighlightedIndex(-1); + } else { + setSuggestions([]); + } + }; + + const addClassName = (className: string) => { + if (className && !classNames.includes(className) && isValidClassName(className)) { + setClassNames([...classNames, className]); + setInputValue(''); + setSuggestions([]); + setHighlightedIndex(-1); + } + }; + + const removeClassName = (index: number) => { + setClassNames(classNames.filter((_, i) => i !== index)); + }; + + const handleSuggestionClick = (suggestion: string) => { + addClassName(suggestion); + }; + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (inputRef.current && !inputRef.current.contains(event.target as Node)) { + setSuggestions([]); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + return ( + inputRef.current?.focus()}> + {classNames.map((className, index) => ( + removeClassName(index)}> + {className} × + + ))} + + {suggestions.length > 0 && ( + + {suggestions.map((suggestion, index) => ( + handleSuggestionClick(suggestion)} + isHighlighted={index === highlightedIndex} + > + {suggestion} + + ))} + + )} + + ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 7e7833a..dd08a3e 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -26,3 +26,4 @@ export * from './tag-select'; export * from './popover'; export * from './drag-panel'; export * from './context-action'; +export * from './classname-input';