ui: autocomplete: dnd support

This commit is contained in:
undefined committed 2022-04-06 03:37:54 +08:00
1 parent 217facc710
commit 422c078b0c
2 files changed
+56 -11

No files matched your search

@@ -3,8 +3,10 @@
import React, { import React, {
forwardRef, useState, useRef, useImperativeHandle, useEffect, forwardRef, useState, useRef, useImperativeHandle, useEffect,
} from 'react'; } from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { debounce } from 'lodash'; import { debounce, uniqueId } from 'lodash';
import Icon from 'vj/components/react/IconComponent'; import Icon from 'vj/components/react/IconComponent';
export interface AutoCompleteProps<Item> { export interface AutoCompleteProps<Item> {
@@ -25,6 +27,7 @@ export interface AutoCompleteProps<Item> {
itemKey?: (item: Item) => string; itemKey?: (item: Item) => string;
onChange?: (value: string) => any; onChange?: (value: string) => any;
multi?: boolean; multi?: boolean;
draggable?: boolean;
selectedKeys?: string[]; selectedKeys?: string[];
allowEmptyQuery?: boolean; allowEmptyQuery?: boolean;
freeSolo?: boolean; freeSolo?: boolean;
@@ -51,6 +54,30 @@ export interface AutoCompleteHandle<Item> {
const superCache = {}; const superCache = {};
function DraggableSelection({
type, id, move, children, ...props
}) {
const ref = useRef<HTMLDivElement>(null);
const [isDragging, drag] = useDrag<string, any, boolean>(() => ({
type,
item: id,
collect: (m) => m.isDragging(),
}));
const [, drop] = useDrop({
accept: type,
hover: (item: string) => {
if (!ref.current) return;
move(item, id);
},
});
drag(drop(ref));
return (
<div ref={ref} {...props} style={{ opacity: isDragging ? 0.2 : 1 }}>
{children}
</div>
);
}
// eslint-disable-next-line prefer-arrow-callback // eslint-disable-next-line prefer-arrow-callback
const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, ref: React.Ref<AutoCompleteHandle<T>>) { const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, ref: React.Ref<AutoCompleteHandle<T>>) {
const width = props.width ?? '100%'; const width = props.width ?? '100%';
@@ -64,6 +91,7 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
const itemKey = props.itemKey ?? itemText; const itemKey = props.itemKey ?? itemText;
const onChange = props.onChange ?? (() => { }); const onChange = props.onChange ?? (() => { });
const multi = props.multi ?? false; const multi = props.multi ?? false;
const draggable = props.draggable ?? props.multi;
const allowEmptyQuery = props.allowEmptyQuery ?? false; const allowEmptyQuery = props.allowEmptyQuery ?? false;
const freeSolo = props.freeSolo ?? false; const freeSolo = props.freeSolo ?? false;
const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : ((i) => i); const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : ((i) => i);
@@ -74,6 +102,7 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
const [itemList, setItemList] = useState([]); // items list const [itemList, setItemList] = useState([]); // items list
const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list) const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list)
const [rerender, setRerender] = useState(false); const [rerender, setRerender] = useState(false);
const [draggableId] = useState(uniqueId());
const inputRef = useRef<HTMLInputElement>(); const inputRef = useRef<HTMLInputElement>();
const listRef = useRef<HTMLUListElement>(); const listRef = useRef<HTMLUListElement>();
@@ -224,28 +253,34 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
}, },
}), [selected, selectedKeys, inputRef, multi]); }), [selected, selectedKeys, inputRef, multi]);
const move = (dragId: string, hoverId: string) => {
if (dragId === hoverId || !draggable) return;
const dragIndex = selectedKeys.indexOf(dragId);
const hoverIndex = selectedKeys.indexOf(hoverId);
setSelectedKeys((s) => {
const a = [...s];
a.splice(dragIndex, 1);
a.splice(hoverIndex, 0, s[dragIndex]);
return a;
});
};
return ( return (
<div style={{ display: 'inline-block', width: '100%' }}> <div style={{ display: 'inline-block', width: '100%' }}>
<div <div
className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'} className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'}
style={{ width, height }} style={{ width, height }}
> >
<DndProvider backend={HTML5Backend}>
{multi && selectedKeys.map((key) => { {multi && selectedKeys.map((key) => {
const item = valueCache[key]; const item = valueCache[key];
return ( return (
<div className="autocomplete-tag" key={item ? key : `draft-${key}`}> <DraggableSelection type={draggableId} id={key} move={move} className="autocomplete-tag" key={item ? key : `draft-${key}`}>
<div>{item ? itemText(item) : key}</div> <div>{item ? itemText(item) : key}</div>
<Icon name="close" onClick={() => toggleItem(item, key)} /> <Icon name="close" onClick={() => toggleItem(item, key)} />
</div> </DraggableSelection>
); );
})} })}
{disabled && (
<input
disabled
autoComplete="off"
value={disabledHint}
/>
)}
<input <input
ref={inputRef} ref={inputRef}
autoComplete="off" autoComplete="off"
@@ -262,7 +297,15 @@ const AutoComplete = forwardRef(function Impl<T>(props: AutoCompleteProps<T>, re
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
defaultValue={multi ? '' : selectedKeys.join(',')} defaultValue={multi ? '' : selectedKeys.join(',')}
/> />
</DndProvider>
</div> </div>
{disabled && (
<input
disabled
autoComplete="off"
value={disabledHint}
/>
)}
{focused && itemList.length > 0 && ( {focused && itemList.length > 0 && (
<ul ref={listRef} className="autocomplete-list" style={listStyle} onMouseDown={(e) => e.preventDefault()}> <ul ref={listRef} className="autocomplete-list" style={listStyle} onMouseDown={(e) => e.preventDefault()}>
{itemList.map((item, idx) => ( {itemList.map((item, idx) => (
+3 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.34.35", "version": "4.34.36",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "hydro.js", "main": "hydro.js",
@@ -77,6 +77,8 @@
"queue-microtask": "^1.2.3", "queue-microtask": "^1.2.3",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dnd": "^16.0.0",
"react-dnd-html5-backend": "^16.0.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-redux": "^7.2.8", "react-redux": "^7.2.8",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",