All checks were successful
Build and Deploy Gateway / build-and-deploy (push) Successful in 25s
- Создан компонент RuleEditorDrawer для редактирования правил с поддержкой JSON. - Добавлен компонент ServersPage для отображения и управления серверами. - Реализован компонент SettingsPage для управления подписками и конфигурациями. - Создан компонент Sidebar для навигации по приложению. - Добавлен компонент StatusPane для отображения статуса сервера. - Реализован компонент Toasts для отображения уведомлений. - Создан компонент Topbar для отображения информации о текущем состоянии. - Добавлен модуль country.js для определения страны по тегу сервера. Refs: None
62 lines
1.8 KiB
JavaScript
62 lines
1.8 KiB
JavaScript
import React, { useState } from 'react';
|
||
|
||
/**
|
||
* Chip input. Items separated by Enter, comma, или space (для CIDR/портов).
|
||
* Невалидные элементы помечаются красным.
|
||
*/
|
||
export function ChipsInput({ value = [], onChange, placeholder = '', validate, splitter = /[\s,]/ }) {
|
||
const [draft, setDraft] = useState('');
|
||
|
||
function commit(text) {
|
||
const parts = String(text).split(splitter).map((p) => p.trim()).filter(Boolean);
|
||
if (!parts.length) return;
|
||
const next = Array.from(new Set([...value, ...parts]));
|
||
onChange(next);
|
||
setDraft('');
|
||
}
|
||
|
||
function remove(item) {
|
||
onChange(value.filter((v) => v !== item));
|
||
}
|
||
|
||
function onKeyDown(e) {
|
||
if (e.key === 'Enter' || e.key === ',') {
|
||
e.preventDefault();
|
||
if (draft.trim()) commit(draft);
|
||
} else if (e.key === 'Backspace' && !draft && value.length) {
|
||
onChange(value.slice(0, -1));
|
||
}
|
||
}
|
||
|
||
function onPaste(e) {
|
||
const text = e.clipboardData.getData('text');
|
||
if (text && splitter.test(text)) {
|
||
e.preventDefault();
|
||
commit(text);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="chips" onClick={(e) => e.currentTarget.querySelector('input')?.focus()}>
|
||
{value.map((item) => {
|
||
const invalid = validate ? !validate(item) : false;
|
||
return (
|
||
<span key={item} className={`chip ${invalid ? 'error' : ''}`}>
|
||
{item}
|
||
<button type="button" onClick={() => remove(item)} title="Убрать">×</button>
|
||
</span>
|
||
);
|
||
})}
|
||
<input
|
||
className="chip-input"
|
||
value={draft}
|
||
onChange={(e) => setDraft(e.target.value)}
|
||
onKeyDown={onKeyDown}
|
||
onPaste={onPaste}
|
||
onBlur={() => draft.trim() && commit(draft)}
|
||
placeholder={value.length ? '' : placeholder}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|