mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 09:46:28 +08:00
ui: new autocomplete components (#190)
This commit is contained in:
1 parent
9eece289e0
commit
89d100c548
16 files changed
+839
-419
No files matched your search
@@ -55,6 +55,10 @@ module.exports = {
|
|||||||
// FIXME A bug with eslint-parser
|
// FIXME A bug with eslint-parser
|
||||||
'template-curly-spacing': 'off',
|
'template-curly-spacing': 'off',
|
||||||
|
|
||||||
|
// Note: must disable the base rule as it can report incorrect errors
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/no-use-before-define': ['error'],
|
||||||
|
|
||||||
'comma-dangle': [
|
'comma-dangle': [
|
||||||
'error',
|
'error',
|
||||||
'always-multiline',
|
'always-multiline',
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import { assign } from 'lodash';
|
|
||||||
import tpl from 'vj/utils/tpl';
|
|
||||||
import request from 'vj/utils/request';
|
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
|
||||||
import AutoComplete from '.';
|
|
||||||
|
|
||||||
function getText(domain) {
|
|
||||||
return domain._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItems(val) {
|
|
||||||
return request.get('/domain/search', { q: val });
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItem(domain) {
|
|
||||||
return tpl`
|
|
||||||
<div class="media">
|
|
||||||
<div class="media__left medium">
|
|
||||||
<img class="small domain-profile-avatar" src="${domain.avatarUrl}" width="30" height="30">
|
|
||||||
</div>
|
|
||||||
<div class="media__body medium">
|
|
||||||
<div class="domain-select__name">${domain.name}</div>
|
|
||||||
<div class="domain-select__id">ID = ${domain._id}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class DomainSelectAutoComplete extends AutoComplete {
|
|
||||||
static DOMAttachKey = 'vjDomainSelectAutoCompleteInstance';
|
|
||||||
|
|
||||||
constructor($dom, options) {
|
|
||||||
super($dom, {
|
|
||||||
classes: 'domain-select',
|
|
||||||
items: getItems,
|
|
||||||
render: renderItem,
|
|
||||||
text: getText,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assign(DomainSelectAutoComplete, DOMAttachedObject);
|
|
||||||
window.Hydro.components.DomainSelectAutoComplete = DomainSelectAutoComplete;
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { assign } from 'lodash';
|
||||||
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
|
import AutoComplete from '.';
|
||||||
|
import DomainSelectAutoCompleteFC from './components/DomainSelectAutoComplete';
|
||||||
|
|
||||||
|
export default class DomainSelectAutoComplete extends AutoComplete {
|
||||||
|
static DOMAttachKey = 'ucwDomainSelectAutoCompleteInstance';
|
||||||
|
|
||||||
|
constructor($dom, options) {
|
||||||
|
super($dom, {
|
||||||
|
classes: 'domain-select',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attach() {
|
||||||
|
const value = this.$dom.val();
|
||||||
|
ReactDOM.render(
|
||||||
|
<DomainSelectAutoCompleteFC
|
||||||
|
ref={(ref) => { this.ref = ref; }}
|
||||||
|
height="34px"
|
||||||
|
defaultItems={value}
|
||||||
|
onChange={this.onChange}
|
||||||
|
multi={this.options.multi}
|
||||||
|
freeSolo={this.options.multi}
|
||||||
|
/>,
|
||||||
|
this.container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(DomainSelectAutoComplete, DOMAttachedObject);
|
||||||
|
window.Hydro.components.DomainSelectAutoComplete = DomainSelectAutoComplete;
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { assign } from 'lodash';
|
|
||||||
import tpl from 'vj/utils/tpl';
|
|
||||||
import request from 'vj/utils/request';
|
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
|
||||||
import AutoComplete from './index';
|
|
||||||
|
|
||||||
function getText(pdoc) {
|
|
||||||
return pdoc.docId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItems(prefix) {
|
|
||||||
return request.get(`/d/${UiContext.domainId}/problem/list`, { prefix });
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItem(pdoc) {
|
|
||||||
return tpl`
|
|
||||||
<div class="media">
|
|
||||||
<div class="media__body medium">
|
|
||||||
<div class="problem-select__name">${pdoc.pid ? `${pdoc.pid} ` : ''}${pdoc.title}</div>
|
|
||||||
<div class="problem-select__id">ID = ${pdoc.docId}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ProblemSelectAutoComplete extends AutoComplete {
|
|
||||||
static DOMAttachKey = 'vjProblemSelectAutoCompleteInstance';
|
|
||||||
|
|
||||||
constructor($dom, options) {
|
|
||||||
super($dom, {
|
|
||||||
classes: 'problem-select',
|
|
||||||
items: getItems,
|
|
||||||
render: renderItem,
|
|
||||||
text: getText,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assign(ProblemSelectAutoComplete, DOMAttachedObject);
|
|
||||||
window.Hydro.components.ProblemSelectAutoComplete = ProblemSelectAutoComplete;
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { assign } from 'lodash';
|
||||||
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
|
import AutoComplete from '.';
|
||||||
|
import ProblemSelectAutoCompleteFC from './components/ProblemSelectAutoComplete';
|
||||||
|
|
||||||
|
export default class ProblemSelectAutoComplete extends AutoComplete {
|
||||||
|
static DOMAttachKey = 'ucwProblemSelectAutoCompleteInstance';
|
||||||
|
|
||||||
|
constructor($dom, options) {
|
||||||
|
super($dom, {
|
||||||
|
classes: 'problem-select',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attach() {
|
||||||
|
const value = this.$dom.val();
|
||||||
|
ReactDOM.render(
|
||||||
|
<ProblemSelectAutoCompleteFC
|
||||||
|
ref={(ref) => { this.ref = ref; }}
|
||||||
|
height="34px"
|
||||||
|
defaultItems={value}
|
||||||
|
onChange={this.onChange}
|
||||||
|
multi={this.options.multi}
|
||||||
|
freeSolo={this.options.multi}
|
||||||
|
/>,
|
||||||
|
this.container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(ProblemSelectAutoComplete, DOMAttachedObject);
|
||||||
|
window.Hydro.components.ProblemSelectAutoComplete = ProblemSelectAutoComplete;
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import tpl from 'vj/utils/tpl';
|
|
||||||
import request from 'vj/utils/request';
|
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
|
||||||
import AutoComplete from './index';
|
|
||||||
|
|
||||||
function getText(user) {
|
|
||||||
return user.uname;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItems(val) {
|
|
||||||
return request.get('/user/search', { q: val });
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderItem(user) {
|
|
||||||
return tpl`
|
|
||||||
<div class="media">
|
|
||||||
<div class="media__left medium">
|
|
||||||
<img class="small user-profile-avatar" src="${user.avatarUrl}" width="30" height="30">
|
|
||||||
</div>
|
|
||||||
<div class="media__body medium">
|
|
||||||
<div class="user-select__uname">${user.uname}</div>
|
|
||||||
<div class="user-select__uid">UID = ${user._id}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UserSelectAutoComplete extends AutoComplete {
|
|
||||||
static DOMAttachKey = 'vjUserSelectAutoCompleteInstance';
|
|
||||||
|
|
||||||
constructor($dom, options) {
|
|
||||||
super($dom, {
|
|
||||||
classes: 'user-select',
|
|
||||||
items: getItems,
|
|
||||||
render: renderItem,
|
|
||||||
text: getText,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_.assign(UserSelectAutoComplete, DOMAttachedObject);
|
|
||||||
window.Hydro.components.UserSelectAutoComplete = UserSelectAutoComplete;
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { assign } from 'lodash';
|
||||||
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
|
import AutoComplete from '.';
|
||||||
|
import UserSelectAutoCompleteFC from './components/UserSelectAutoComplete';
|
||||||
|
|
||||||
|
export default class UserSelectAutoComplete extends AutoComplete {
|
||||||
|
static DOMAttachKey = 'ucwUserSelectAutoCompleteInstance';
|
||||||
|
|
||||||
|
constructor($dom, options) {
|
||||||
|
super($dom, {
|
||||||
|
classes: 'user-select',
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
attach() {
|
||||||
|
const value = this.$dom.val();
|
||||||
|
ReactDOM.render(
|
||||||
|
<UserSelectAutoCompleteFC
|
||||||
|
ref={(ref) => { this.ref = ref; }}
|
||||||
|
height="34px"
|
||||||
|
defaultItems={value}
|
||||||
|
onChange={this.onChange}
|
||||||
|
multi={this.options.multi}
|
||||||
|
freeSolo={this.options.multi}
|
||||||
|
/>,
|
||||||
|
this.container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(UserSelectAutoComplete, DOMAttachedObject);
|
||||||
|
window.Hydro.components.UserSelectAutoComplete = UserSelectAutoComplete;
|
||||||
@@ -1,11 +1,112 @@
|
|||||||
.drop-element.autocomplete
|
.autocomplete-dummy
|
||||||
max-width: rem(250px)
|
display: none !important
|
||||||
|
|
||||||
.empty-row
|
.autocomplete-wrapper
|
||||||
color: $autocomplete-empty-row-color
|
width: 300px
|
||||||
font-size: $(font-size-small)
|
border: 1px solid #d9d9d9
|
||||||
padding: rem(10px)
|
background-color: #fff
|
||||||
|
padding: 1px
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
overflow-y: auto
|
||||||
|
|
||||||
.menu
|
&:hover
|
||||||
max-height: rem(250px)
|
border-color: #40a9ff
|
||||||
overflow: auto
|
|
||||||
|
&.focused
|
||||||
|
border-color: #40a9ff
|
||||||
|
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2)
|
||||||
|
|
||||||
|
input
|
||||||
|
background-color: #fff
|
||||||
|
color: rgba(0,0,0,.85)
|
||||||
|
height: 30px
|
||||||
|
box-sizing: border-box
|
||||||
|
padding: 4px 6px
|
||||||
|
width: 0
|
||||||
|
min-width: 30px
|
||||||
|
flex-grow: 1
|
||||||
|
border: 0
|
||||||
|
margin: 0
|
||||||
|
outline: 0
|
||||||
|
|
||||||
|
.autocomplete-tag
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
height: 24px
|
||||||
|
margin: 2px
|
||||||
|
line-height: 22px
|
||||||
|
background-color: #fafafa
|
||||||
|
border: 1px solid #e8e8e8
|
||||||
|
border-radius: 2px
|
||||||
|
box-sizing: content-box
|
||||||
|
padding: 0 4px 0 10px
|
||||||
|
outline: 0
|
||||||
|
overflow: hidden
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
border-color: #40a9ff
|
||||||
|
background-color: #e6f7ff
|
||||||
|
|
||||||
|
&>div
|
||||||
|
overflow: hidden
|
||||||
|
white-space: nowrap
|
||||||
|
text-overflow: ellipsis
|
||||||
|
|
||||||
|
&>svg
|
||||||
|
font-size: 12px
|
||||||
|
cursor: pointer
|
||||||
|
padding: 4px
|
||||||
|
|
||||||
|
.autocomplete-list
|
||||||
|
min-width: 300px
|
||||||
|
margin: 2px 0 0
|
||||||
|
padding: 0
|
||||||
|
position: absolute
|
||||||
|
list-style: none
|
||||||
|
background-color: #fff
|
||||||
|
overflow: auto
|
||||||
|
max-height: 500px
|
||||||
|
border-radius: 4px
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15)
|
||||||
|
z-index: 1
|
||||||
|
|
||||||
|
li
|
||||||
|
padding: 5px 12px
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
&>div
|
||||||
|
flex-grow: 1
|
||||||
|
|
||||||
|
&>svg
|
||||||
|
color: transparent
|
||||||
|
|
||||||
|
&[aria-selected='true']
|
||||||
|
background-color: #fafafa
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
svg
|
||||||
|
color: #1890ff
|
||||||
|
|
||||||
|
&[data-focus='true']
|
||||||
|
background-color: #e6f7ff
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
svg
|
||||||
|
color: currentColor
|
||||||
|
|
||||||
|
svg.autocomplete-icon
|
||||||
|
-webkit-user-select: none
|
||||||
|
-moz-user-select: none
|
||||||
|
-ms-user-select: none
|
||||||
|
user-select: none
|
||||||
|
width: 1em
|
||||||
|
height: 1em
|
||||||
|
display: inline-block
|
||||||
|
fill: currentColor
|
||||||
|
-webkit-flex-shrink: 0
|
||||||
|
-ms-flex-negative: 0
|
||||||
|
flex-shrink: 0
|
||||||
|
-webkit-transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms
|
||||||
|
transition: fill 200ms cubic-bezier(0.4, 0, 0.2, 1) 0ms
|
||||||
|
font-size: 1.25rem
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
/* eslint-disable jsx-a11y/role-supports-aria-props */
|
||||||
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||||
|
import React, {
|
||||||
|
forwardRef, useState, useRef, useImperativeHandle, createRef, useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { Icon } from '@blueprintjs/core';
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-arrow-callback
|
||||||
|
const AutoComplete = forwardRef(function AutoComplete(props, ref) {
|
||||||
|
const width = props.width ?? '100%';
|
||||||
|
// if you need fix height, set to at least "30px"
|
||||||
|
// for Hydro, no less then "34px" can be better
|
||||||
|
const height = props.height ?? 'auto';
|
||||||
|
const listStyle = props.listStyle ?? {};
|
||||||
|
const itemsFn = props.itemsFn ?? (async () => []);
|
||||||
|
const renderItem = props.renderItem ?? ((item) => item);
|
||||||
|
const itemText = props.itemText ?? ((item) => item);
|
||||||
|
const itemKey = props.itemKey ?? itemText;
|
||||||
|
const onChange = props.onChange ?? (() => { });
|
||||||
|
const multi = props.multi ?? false;
|
||||||
|
const rawDefaultItems = props.defaultItems ?? [];
|
||||||
|
const defaultItems = typeof rawDefaultItems === 'string'
|
||||||
|
? props.defaultItems.split(',').map((i) => i.trim()).filter((i) => i.length > 0) : rawDefaultItems;
|
||||||
|
const allowEmptyQuery = props.allowEmptyQuery ?? false;
|
||||||
|
const freeSolo = props.freeSolo ?? false;
|
||||||
|
const freeSoloConverter = freeSolo ? props.freeSoloConverter ?? ((i) => i) : ((i) => i);
|
||||||
|
|
||||||
|
const [focused, setFocused] = useState(false); // is focused
|
||||||
|
const [selected, setSelected] = useState(defaultItems); // selected items
|
||||||
|
const [selectedKeys, setSelectedKeys] = useState(defaultItems.map((i) => itemKey(i))); // keys of selected items
|
||||||
|
const [itemList, setItemList] = useState([]); // items list
|
||||||
|
const [currentItem, setCurrentItem] = useState(null); // index of current item (in item list)
|
||||||
|
|
||||||
|
const inputRef = createRef();
|
||||||
|
const listRef = createRef();
|
||||||
|
|
||||||
|
const cache = useRef({});
|
||||||
|
|
||||||
|
const queryList = async (query) => {
|
||||||
|
if (!query && !allowEmptyQuery) {
|
||||||
|
setItemList([]);
|
||||||
|
setCurrentItem(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let items;
|
||||||
|
if (cache.current[query]) {
|
||||||
|
items = cache.current[query];
|
||||||
|
} else {
|
||||||
|
items = await itemsFn(query);
|
||||||
|
cache.current[query] = items;
|
||||||
|
}
|
||||||
|
setItemList(items);
|
||||||
|
setCurrentItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateValue = () => {
|
||||||
|
const query = inputRef.current?.value;
|
||||||
|
if (!query) return multi ? selectedKeys.join(',') : '';
|
||||||
|
return (multi && selectedKeys.length > 0) ? `${selectedKeys.join(',')},${query}` : query;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatchChange = () => {
|
||||||
|
const value = calculateValue();
|
||||||
|
onChange(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatchChange();
|
||||||
|
}, [selectedKeys, multi]);
|
||||||
|
|
||||||
|
const handleInputChange = debounce(async (e) => {
|
||||||
|
const { target } = e;
|
||||||
|
const { value } = target;
|
||||||
|
queryList(value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const toggleItem = (item) => {
|
||||||
|
if (multi) {
|
||||||
|
const key = itemKey(item);
|
||||||
|
const idx = selectedKeys.indexOf(key);
|
||||||
|
if (idx !== -1) {
|
||||||
|
setSelected((s) => {
|
||||||
|
const newSelected = [...s];
|
||||||
|
newSelected.splice(idx, 1);
|
||||||
|
return newSelected;
|
||||||
|
});
|
||||||
|
setSelectedKeys((s) => {
|
||||||
|
const newSelectedKeys = [...s];
|
||||||
|
newSelectedKeys.splice(idx, 1);
|
||||||
|
return newSelectedKeys;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelected((s) => [...s, item]);
|
||||||
|
setSelectedKeys((s) => [...s, key]);
|
||||||
|
}
|
||||||
|
inputRef.current.value = '';
|
||||||
|
inputRef.current.focus();
|
||||||
|
} else {
|
||||||
|
inputRef.current.value = itemKey(item);
|
||||||
|
}
|
||||||
|
setItemList([]);
|
||||||
|
setCurrentItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputKeyDown = (e) => {
|
||||||
|
const { key, target } = e;
|
||||||
|
if (key === 'Escape') {
|
||||||
|
setItemList([]);
|
||||||
|
setCurrentItem(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'Enter' || key === ',') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentItem !== null) {
|
||||||
|
toggleItem(itemList[currentItem]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (freeSolo && target.value !== '') {
|
||||||
|
toggleItem(freeSoloConverter(target.value));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'Backspace') {
|
||||||
|
if (target.value.length > 0) return;
|
||||||
|
if (selected.length > 0) {
|
||||||
|
setSelected((s) => s.slice(0, -1));
|
||||||
|
setSelectedKeys((s) => s.slice(0, -1));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (itemList.length === 0) return;
|
||||||
|
const idx = (currentItem ?? 0) - 1;
|
||||||
|
const newIdx = idx < 0 ? itemList.length - 1 : idx;
|
||||||
|
setCurrentItem(newIdx);
|
||||||
|
listRef.current.children[newIdx].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (itemList.length === 0) return;
|
||||||
|
const idx = (currentItem ?? itemList.length - 1) + 1;
|
||||||
|
const newIdx = idx >= itemList.length ? 0 : idx;
|
||||||
|
setCurrentItem(newIdx);
|
||||||
|
listRef.current.children[newIdx].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
// eslint-disable-next-line no-useless-return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: handle other keys
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getSelectedItems: () => selected,
|
||||||
|
getSelectedItemsAsString: () => selectedKeys.join(','),
|
||||||
|
setSelectedItems: (items) => {
|
||||||
|
setSelected(items);
|
||||||
|
setSelectedKeys(items.map((i) => itemKey(i)));
|
||||||
|
},
|
||||||
|
getQuery: () => inputRef.current?.value,
|
||||||
|
setQuery: (query) => {
|
||||||
|
if (inputRef.current) inputRef.current.value = query;
|
||||||
|
},
|
||||||
|
triggerQuery: () => queryList(inputRef.current?.value),
|
||||||
|
closeList: () => {
|
||||||
|
setItemList([]);
|
||||||
|
setCurrentItem(null);
|
||||||
|
},
|
||||||
|
getValue: () => (multi ? selectedKeys.join(',') : (inputRef.current.value ?? '')),
|
||||||
|
getValueArray: () => (multi ? selected : [inputRef.current?.value].filter((i) => !!i)),
|
||||||
|
getValueWithQuery: () => calculateValue(),
|
||||||
|
getValueArrayWithQuery: () => {
|
||||||
|
const query = inputRef.current?.value;
|
||||||
|
if (!query) return multi ? selected : [''];
|
||||||
|
return multi ? [...selected, query] : [query];
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
setSelected([]);
|
||||||
|
setSelectedKeys([]);
|
||||||
|
if (inputRef.current) inputRef.current.value = '';
|
||||||
|
},
|
||||||
|
focus: () => {
|
||||||
|
setFocused(true);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'inline-block', width: '100%' }}>
|
||||||
|
<div
|
||||||
|
className={focused ? 'autocomplete-wrapper focused' : 'autocomplete-wrapper'}
|
||||||
|
style={{ width, height }}
|
||||||
|
>
|
||||||
|
{multi && selected.map((item, idx) => (
|
||||||
|
<div className="autocomplete-tag" key={selectedKeys[idx]}>
|
||||||
|
<div>{itemText(item)}</div>
|
||||||
|
<Icon icon="cross" onClick={() => toggleItem(item)} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(e) => {
|
||||||
|
dispatchChange();
|
||||||
|
handleInputChange(e);
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
setFocused(true);
|
||||||
|
if (allowEmptyQuery) {
|
||||||
|
handleInputChange();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => setFocused(false)}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{focused && itemList.length > 0 && (
|
||||||
|
<ul ref={listRef} className="autocomplete-list" style={listStyle} onMouseDown={(e) => e.preventDefault()}>
|
||||||
|
{itemList.map((item, idx) => (
|
||||||
|
<li
|
||||||
|
key={itemKey(item)}
|
||||||
|
onClick={() => toggleItem(item)}
|
||||||
|
onMouseMove={() => setCurrentItem(idx)}
|
||||||
|
aria-selected={selectedKeys.includes(itemKey(item))}
|
||||||
|
data-focus={idx === currentItem}
|
||||||
|
>
|
||||||
|
<div>{renderItem(item)}</div>
|
||||||
|
<Icon icon="tick" />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AutoComplete.propTypes = {
|
||||||
|
width: PropTypes.string,
|
||||||
|
height: PropTypes.string,
|
||||||
|
listStyle: PropTypes.object,
|
||||||
|
itemsFn: PropTypes.func.isRequired,
|
||||||
|
itemKey: PropTypes.func,
|
||||||
|
renderItem: PropTypes.func,
|
||||||
|
itemText: PropTypes.func,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
multi: PropTypes.bool,
|
||||||
|
defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]),
|
||||||
|
allowEmptyQuery: PropTypes.bool,
|
||||||
|
freeSolo: PropTypes.bool,
|
||||||
|
freeSoloConverter: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
AutoComplete.defaultProps = {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
listStyle: {},
|
||||||
|
renderItem: (item) => item,
|
||||||
|
itemText: (item) => item,
|
||||||
|
multi: false,
|
||||||
|
defaultItems: [],
|
||||||
|
allowEmptyQuery: false,
|
||||||
|
freeSolo: false,
|
||||||
|
freeSoloConverter: (input) => input,
|
||||||
|
};
|
||||||
|
|
||||||
|
AutoComplete.displayName = 'AutoComplete';
|
||||||
|
|
||||||
|
export default AutoComplete;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { forwardRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import request from 'vj/utils/request';
|
||||||
|
import AutoComplete from './AutoComplete';
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-arrow-callback
|
||||||
|
const DomainSelectAutoComplete = forwardRef(function DomainSelectAutoComplete(props, ref) {
|
||||||
|
const itemsFn = (query) => request.get('/domain/search', { q: query });
|
||||||
|
|
||||||
|
/* const itemText = (domain) => {
|
||||||
|
return domain.name;
|
||||||
|
}; */
|
||||||
|
|
||||||
|
const itemKey = (domain) => domain._id || domain;
|
||||||
|
|
||||||
|
const renderItem = (domain) => (
|
||||||
|
<div className="media">
|
||||||
|
<div className="media__left medium">
|
||||||
|
<img className="small domain-profile-avatar" src={domain.avatarUrl} alt="" width="30" height="30" />
|
||||||
|
</div>
|
||||||
|
<div className="media__body medium">
|
||||||
|
<div className="domain-select__name">{domain.name}</div>
|
||||||
|
<div className="domain-select__id">ID = {domain._id}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoComplete
|
||||||
|
ref={ref}
|
||||||
|
itemsFn={itemsFn}
|
||||||
|
itemText={itemKey}
|
||||||
|
itemKey={itemKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DomainSelectAutoComplete.propTypes = {
|
||||||
|
width: PropTypes.string,
|
||||||
|
height: PropTypes.string,
|
||||||
|
listStyle: PropTypes.object,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
multi: PropTypes.bool,
|
||||||
|
defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]),
|
||||||
|
allowEmptyQuery: PropTypes.bool,
|
||||||
|
freeSolo: PropTypes.bool,
|
||||||
|
freeSoloConverter: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
DomainSelectAutoComplete.defaultProps = {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
listStyle: {},
|
||||||
|
multi: false,
|
||||||
|
defaultItems: [],
|
||||||
|
allowEmptyQuery: false,
|
||||||
|
freeSolo: false,
|
||||||
|
freeSoloConverter: (input) => input,
|
||||||
|
};
|
||||||
|
|
||||||
|
DomainSelectAutoComplete.displayName = 'DomainSelectAutoComplete';
|
||||||
|
|
||||||
|
export default DomainSelectAutoComplete;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React, { forwardRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import request from 'vj/utils/request';
|
||||||
|
import AutoComplete from './AutoComplete';
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-arrow-callback
|
||||||
|
const ProblemSelectAutoComplete = forwardRef(function ProblemSelectAutoComplete(props, ref) {
|
||||||
|
const itemsFn = (query) => request.get(`/d/${UiContext.domainId}/problem/list`, { prefix: query });
|
||||||
|
|
||||||
|
/* const itemText = (pdoc) => {
|
||||||
|
return (pdoc.pid ? `${pdoc.pid} ` : '') + pdoc.title;
|
||||||
|
}; */
|
||||||
|
|
||||||
|
const itemKey = (pdoc) => pdoc.docId || pdoc;
|
||||||
|
|
||||||
|
const renderItem = (pdoc) => (
|
||||||
|
<div className="media">
|
||||||
|
<div className="media__body medium">
|
||||||
|
<div className="problem-select__name">{pdoc.pid ? `${pdoc.pid} ` : ''}{pdoc.title}</div>
|
||||||
|
<div className="problem-select__id">ID = {pdoc.docId}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoComplete
|
||||||
|
ref={ref}
|
||||||
|
itemsFn={itemsFn}
|
||||||
|
itemText={itemKey}
|
||||||
|
itemKey={itemKey}
|
||||||
|
renderItem={renderItem}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
ProblemSelectAutoComplete.propTypes = {
|
||||||
|
width: PropTypes.string,
|
||||||
|
height: PropTypes.string,
|
||||||
|
listStyle: PropTypes.object,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
multi: PropTypes.bool,
|
||||||
|
defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]),
|
||||||
|
allowEmptyQuery: PropTypes.bool,
|
||||||
|
freeSolo: PropTypes.bool,
|
||||||
|
freeSoloConverter: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
ProblemSelectAutoComplete.defaultProps = {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
listStyle: {},
|
||||||
|
multi: false,
|
||||||
|
defaultItems: [],
|
||||||
|
allowEmptyQuery: false,
|
||||||
|
freeSolo: false,
|
||||||
|
freeSoloConverter: (input) => input,
|
||||||
|
};
|
||||||
|
|
||||||
|
ProblemSelectAutoComplete.displayName = 'ProblemSelectAutoComplete';
|
||||||
|
|
||||||
|
export default ProblemSelectAutoComplete;
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { forwardRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import request from 'vj/utils/request';
|
||||||
|
import AutoComplete from './AutoComplete';
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-arrow-callback
|
||||||
|
const UserSelectAutoComplete = forwardRef(function UserSelectAutoComplete(props, ref) {
|
||||||
|
const itemsFn = (query) => request.get('/user/search', { q: query });
|
||||||
|
|
||||||
|
const itemText = (user) => user.uname || user;
|
||||||
|
|
||||||
|
const renderItem = (user) => (
|
||||||
|
<div className="media">
|
||||||
|
<div className="media__left medium">
|
||||||
|
<img className="small user-profile-avatar" alt="" src={user.avatarUrl} width="30" height="30" />
|
||||||
|
</div>
|
||||||
|
<div className="media__body medium">
|
||||||
|
<div className="user-select__uname">{user.uname}</div>
|
||||||
|
<div className="user-select__uid">UID = {user._id}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoComplete
|
||||||
|
ref={ref}
|
||||||
|
itemsFn={itemsFn}
|
||||||
|
itemText={itemText}
|
||||||
|
renderItem={renderItem}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
UserSelectAutoComplete.propTypes = {
|
||||||
|
width: PropTypes.string,
|
||||||
|
height: PropTypes.string,
|
||||||
|
listStyle: PropTypes.object,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
multi: PropTypes.bool,
|
||||||
|
defaultItems: PropTypes.oneOfType([(PropTypes.arrayOf(PropTypes.any)), PropTypes.string]),
|
||||||
|
allowEmptyQuery: PropTypes.bool,
|
||||||
|
freeSolo: PropTypes.bool,
|
||||||
|
freeSoloConverter: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
UserSelectAutoComplete.defaultProps = {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
listStyle: {},
|
||||||
|
multi: false,
|
||||||
|
defaultItems: [],
|
||||||
|
allowEmptyQuery: false,
|
||||||
|
freeSolo: false,
|
||||||
|
freeSoloConverter: (input) => input,
|
||||||
|
};
|
||||||
|
|
||||||
|
UserSelectAutoComplete.displayName = 'UserSelectAutoComplete';
|
||||||
|
|
||||||
|
export default UserSelectAutoComplete;
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import Drop from 'tether-drop';
|
|
||||||
import { assign } from 'lodash';
|
|
||||||
import 'jquery-scroll-lock';
|
|
||||||
|
|
||||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
|
||||||
|
|
||||||
import i18n from 'vj/utils/i18n';
|
|
||||||
import tpl from 'vj/utils/tpl';
|
|
||||||
import zIndexManager from 'vj/utils/zIndexManager';
|
|
||||||
|
|
||||||
export default class AutoComplete extends DOMAttachedObject {
|
|
||||||
static DOMAttachKey = 'vjAutoCompleteInstance';
|
|
||||||
|
|
||||||
constructor($dom, options = {}) {
|
|
||||||
super($dom);
|
|
||||||
this.options = {
|
|
||||||
items: async () => [],
|
|
||||||
render: () => '',
|
|
||||||
text: () => null,
|
|
||||||
minChar: 1,
|
|
||||||
cache: true,
|
|
||||||
clearDefaultValue: true,
|
|
||||||
position: 'bottom left',
|
|
||||||
classes: '',
|
|
||||||
multi: false,
|
|
||||||
...options,
|
|
||||||
};
|
|
||||||
this.clear(this.options.clearDefaultValue);
|
|
||||||
this.menuShown = false;
|
|
||||||
this.cache = {};
|
|
||||||
this.currentItems = [];
|
|
||||||
this.$menu = $(`<ol class="menu ${this.options.classes}"></ol>`);
|
|
||||||
this.$menu.scrollLock({ strict: false });
|
|
||||||
this.$menu.on('mousedown', this.onMenuClick.bind(this));
|
|
||||||
this.$menu.on('mousedown', '.menu__item', this.onItemClick.bind(this));
|
|
||||||
this.dropInstance = new Drop({
|
|
||||||
classes: 'autocomplete dropdown',
|
|
||||||
target: this.$dom[0],
|
|
||||||
content: this.$menu[0],
|
|
||||||
position: this.options.position,
|
|
||||||
constrainToWindow: false,
|
|
||||||
constrainToScrollParent: false,
|
|
||||||
openOn: false,
|
|
||||||
});
|
|
||||||
this.dropInstance.on('open', this.onDropOpen.bind(this));
|
|
||||||
this.attach();
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(clearValue = true) {
|
|
||||||
if (clearValue) this.$dom.val('');
|
|
||||||
this._value = null;
|
|
||||||
this.lastText = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
attach() {
|
|
||||||
this.$dom.on(`click.${this.eventNS}`, this.onClick.bind(this));
|
|
||||||
this.$dom.on(`focus.${this.eventNS}`, this.onFocus.bind(this));
|
|
||||||
this.$dom.on(`blur.${this.eventNS}`, this.onBlur.bind(this));
|
|
||||||
this.$dom.on(`keydown.${this.eventNS}`, this.onKeyDown.bind(this));
|
|
||||||
this.$dom.on(`keyup.${this.eventNS}`, this.onKeyUp.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
onClick() {
|
|
||||||
this.updateOpenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
onFocus() {
|
|
||||||
this.isFocus = true;
|
|
||||||
this.updateOpenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur() {
|
|
||||||
this.isFocus = false;
|
|
||||||
this.updateOpenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyDown() {
|
|
||||||
// TODO: Implement keyboard navigation
|
|
||||||
}
|
|
||||||
|
|
||||||
onKeyUp(ev) {
|
|
||||||
if (ev.which === 27 /* ESC */) {
|
|
||||||
this.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.$dom.val() === this.lastText) return;
|
|
||||||
this.lastText = this.$dom.val();
|
|
||||||
this.updateOpenState();
|
|
||||||
if (this.isOpen) this.renderList();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMenuClick(ev) {
|
|
||||||
ev.preventDefault(); // prevent from losing focus
|
|
||||||
}
|
|
||||||
|
|
||||||
onItemClick(ev) {
|
|
||||||
const idx = $(ev.currentTarget).attr('data-idx');
|
|
||||||
if (idx === undefined) return;
|
|
||||||
const item = this.currentItems[idx];
|
|
||||||
const text = this.options.text(item);
|
|
||||||
this._value = item;
|
|
||||||
this.$dom.trigger('vjAutoCompleteSelect', item);
|
|
||||||
if (text != null) {
|
|
||||||
if (!this.options.multi) this.$dom.val(text);
|
|
||||||
else {
|
|
||||||
const current = this.$dom.val().replace(/,/g, ',');
|
|
||||||
if (current.indexOf(',') !== -1) {
|
|
||||||
const items = current.split(',');
|
|
||||||
items[items.length - 1] = text;
|
|
||||||
this.$dom.val(items.join(', '));
|
|
||||||
} else this.$dom.val(`${text}, `);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDropOpen() {
|
|
||||||
$(this.dropInstance.drop).css('z-index', zIndexManager.getNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
async getItems(val) {
|
|
||||||
if (this.cache[val] !== undefined) return this.cache[val];
|
|
||||||
const data = await this.options.items(val);
|
|
||||||
if (this.options.cache) this.cache[val] = data;
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHtml(items) {
|
|
||||||
if (items.length === 0) {
|
|
||||||
return tpl`<div class="empty-row">${i18n('Oops, there are no results.')}</div>`;
|
|
||||||
}
|
|
||||||
return items.map((item, idx) => `
|
|
||||||
<li class="menu__item" data-idx="${idx}"><a href="javascript:;" class="menu__link">
|
|
||||||
${this.options.render(item)}
|
|
||||||
</a></li>
|
|
||||||
`).join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
async renderList() {
|
|
||||||
let val = this.$dom.val();
|
|
||||||
if (this.options.multi) {
|
|
||||||
const elems = val.split(',').map((i) => i.trim());
|
|
||||||
val = elems[elems.length - 1];
|
|
||||||
}
|
|
||||||
const items = await this.getItems(val);
|
|
||||||
const html = this.getHtml(items);
|
|
||||||
this.currentItems = items;
|
|
||||||
this.$menu.html(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
open() {
|
|
||||||
if (this.isOpen) return;
|
|
||||||
this.dropInstance.open();
|
|
||||||
this.isOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
if (!this.isOpen) return;
|
|
||||||
this.dropInstance.close();
|
|
||||||
this.isOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateOpenState() {
|
|
||||||
let content = this.$dom.val();
|
|
||||||
if (this.options.multi) {
|
|
||||||
content = content.split(',');
|
|
||||||
content = content[content.length - 1].trim();
|
|
||||||
}
|
|
||||||
if (this.isFocus && content.length >= this.options.minChar) {
|
|
||||||
if (!this.isOpen) {
|
|
||||||
this.open();
|
|
||||||
this.renderList();
|
|
||||||
}
|
|
||||||
} else if (this.isOpen) this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
detach() {
|
|
||||||
if (this.detached) return;
|
|
||||||
super.detach();
|
|
||||||
this.$dom.off(`focus.${this.eventNS}`);
|
|
||||||
this.$dom.off(`blur.${this.eventNS}`);
|
|
||||||
this.$dom.off(`keydown.${this.eventNS}`);
|
|
||||||
this.$dom.off(`keyup.${this.eventNS}`);
|
|
||||||
this.dropInstance.destroy();
|
|
||||||
this.$menu.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
value() {
|
|
||||||
return this._value;
|
|
||||||
}
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.$dom.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assign(AutoComplete, DOMAttachedObject);
|
|
||||||
window.Hydro.components.autocomplete = AutoComplete;
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { assign } from 'lodash';
|
||||||
|
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||||
|
import AutoCompleteFC from './components/AutoComplete';
|
||||||
|
|
||||||
|
export default class AutoComplete extends DOMAttachedObject {
|
||||||
|
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
||||||
|
|
||||||
|
constructor($dom, options = {}) {
|
||||||
|
super($dom);
|
||||||
|
this.options = {
|
||||||
|
items: async () => [],
|
||||||
|
render: () => '',
|
||||||
|
text: () => null,
|
||||||
|
multi: false,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
this.ref = null;
|
||||||
|
this.onChange = this.onChange.bind(this);
|
||||||
|
this.container = document.createElement('div');
|
||||||
|
this.$dom.addClass('autocomplete-dummy').after(this.container);
|
||||||
|
setTimeout(() => this.attach(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(clearValue = true) {
|
||||||
|
if (!this.ref) return;
|
||||||
|
if (clearValue) this.ref.clear();
|
||||||
|
else this.ref.closeList();
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(val) {
|
||||||
|
this.$dom.val(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
attach() {
|
||||||
|
const value = this.$dom.val();
|
||||||
|
ReactDOM.render(
|
||||||
|
<AutoCompleteFC
|
||||||
|
ref={(ref) => { this.ref = ref; }}
|
||||||
|
height="34px"
|
||||||
|
itemsFn={this.options.items}
|
||||||
|
renderItem={this.options.render}
|
||||||
|
itemText={this.options.text}
|
||||||
|
defaultItems={value}
|
||||||
|
onChange={this.onChange}
|
||||||
|
multi={this.options.multi}
|
||||||
|
freeSolo={this.options.multi}
|
||||||
|
/>,
|
||||||
|
this.container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
if (!this.ref) return;
|
||||||
|
this.ref.triggerQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.ref) return;
|
||||||
|
this.ref.closeList();
|
||||||
|
}
|
||||||
|
|
||||||
|
detach() {
|
||||||
|
if (this.detached) return;
|
||||||
|
super.detach();
|
||||||
|
ReactDOM.unmountComponentAtNode(this.container);
|
||||||
|
this.$dom.removeClass('autocomplete-dummy');
|
||||||
|
this.container.parentNode.removeChild(this.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.ref.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assign(AutoComplete, DOMAttachedObject);
|
||||||
|
window.Hydro.components.autocomplete = AutoComplete;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
.menu.problem-select
|
.menu.problem-select
|
||||||
width: rem(200px)
|
width: rem(200px)
|
||||||
|
|
||||||
.problem-select__uid
|
.problem-select__id
|
||||||
font-size: rem($font-size-small)
|
font-size: rem($font-size-small)
|
||||||
color: $userselect-uid-color
|
color: $userselect-uid-color
|
||||||
@@ -7,100 +7,102 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{{ form.form_select({
|
{{ form.form_select({
|
||||||
columns:3,
|
columns:3,
|
||||||
label:'Rule',
|
label:'Rule',
|
||||||
name:'rule',
|
name:'rule',
|
||||||
options:rules,
|
options:rules,
|
||||||
value:tdoc['rule']|default(''),
|
value:tdoc['rule']|default(''),
|
||||||
row:false
|
row:false
|
||||||
}) }}
|
}) }}
|
||||||
{{ form.form_text({
|
{{ form.form_text({
|
||||||
columns:4,
|
columns:4,
|
||||||
label:'Title',
|
label:'Title',
|
||||||
name:'title',
|
name:'title',
|
||||||
placeholder:_('title'),
|
placeholder:_('title'),
|
||||||
value:tdoc.title|default(''),
|
value:tdoc.title|default(''),
|
||||||
autofocus:true,
|
autofocus:true,
|
||||||
row:false
|
row:false
|
||||||
}) }}
|
}) }}
|
||||||
{{ form.form_text({
|
{{ form.form_text({
|
||||||
columns:3,
|
columns:3,
|
||||||
label:'Invitation Code',
|
label:'Invitation Code',
|
||||||
name:'code',
|
name:'code',
|
||||||
placeholder:_('(leave blank if none)'),
|
placeholder:_('(leave blank if none)'),
|
||||||
value:tdoc._code|default(''),
|
value:tdoc._code|default(''),
|
||||||
row:false
|
row:false
|
||||||
}) }}
|
}) }}
|
||||||
{{ form.form_checkbox({
|
{{ form.form_checkbox({
|
||||||
columns:2,
|
columns:2,
|
||||||
label:'Rated',
|
label:'Rated',
|
||||||
name:'rated',
|
name:'rated',
|
||||||
placeholder:_('Is rated'),
|
placeholder:_('Is rated'),
|
||||||
value:tdoc.rated|default(true),
|
value:tdoc.rated|default(true),
|
||||||
row:false
|
row:false
|
||||||
}) }}
|
}) }}
|
||||||
</div><div class="row">
|
</div>
|
||||||
|
<div class="row">
|
||||||
{{ form.form_text({
|
{{ form.form_text({
|
||||||
columns:4,
|
columns:4,
|
||||||
label:'Begin Date',
|
label:'Begin Date',
|
||||||
name:'beginAtDate',
|
name:'beginAtDate',
|
||||||
placeholder:'YYYY-mm-dd',
|
placeholder:'YYYY-mm-dd',
|
||||||
value:date_text,
|
value:date_text,
|
||||||
date:true,
|
date:true,
|
||||||
row:false
|
row:false
|
||||||
}) }}
|
}) }}
|
||||||
{{ form.form_text({
|
{{ form.form_text({
|
||||||
columns:4,
|
columns:4,
|
||||||
label:'Begin Time',
|
label:'Begin Time',
|
||||||
name:'beginAtTime',
|
name:'beginAtTime',
|
||||||
placeholder:'HH:MM',
|
placeholder:'HH:MM',
|
||||||
value:time_text,
|
value:time_text,
|
||||||
time:true,
|
time:true,
|
||||||
row:false
|
row:false
|
||||||
}) }}
|
}) }}
|
||||||
{{ form.form_text({
|
{{ form.form_text({
|
||||||
columns:4,
|
columns:4,
|
||||||
label:'Duration (hours)',
|
label:'Duration (hours)',
|
||||||
name:'duration',
|
name:'duration',
|
||||||
value:duration|default('3'),
|
value:duration|default('3'),
|
||||||
row:false
|
row:false
|
||||||
}) }}
|
}) }}
|
||||||
</div>
|
</div>
|
||||||
{{ form.form_text({
|
{{ form.form_text({
|
||||||
columns:null,
|
columns:null,
|
||||||
label:'Problems',
|
label:'Problems',
|
||||||
name:'pids',
|
name:'pids',
|
||||||
value:pids,
|
value:pids,
|
||||||
noAutocomplete:true,
|
placeholder:_("Seperated with ','")
|
||||||
placeholder:_("Seperated with ','")
|
}) }}
|
||||||
}) }}
|
|
||||||
{{ form.form_textarea({
|
{{ form.form_textarea({
|
||||||
columns:null,
|
columns:null,
|
||||||
label:'Content',
|
label:'Content',
|
||||||
name:'content',
|
name:'content',
|
||||||
value:tdoc['content']|default(''),
|
value:tdoc['content']|default(''),
|
||||||
markdown:true
|
markdown:true
|
||||||
}) }}
|
}) }}
|
||||||
<div class="row"><div class="columns">
|
<div class="row">
|
||||||
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
<div class="columns">
|
||||||
{% if page_name == 'contest_edit' %}
|
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
|
||||||
<button type="submit" class="rounded primary button">
|
{% if page_name == 'contest_edit' %}
|
||||||
{{ _('Update') }}
|
<button type="submit" class="rounded primary button">
|
||||||
</button>
|
{{ _('Update') }}
|
||||||
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}
|
</button>
|
||||||
|
{% if handler.user.own(tdoc) or handler.user.hasPerm(perm.PERM_EDIT_CONTEST) %}
|
||||||
<button name="operation" value="delete" type="submit" class="rounded button">
|
<button name="operation" value="delete" type="submit" class="rounded button">
|
||||||
{{ _('Delete') }}
|
{{ _('Delete') }}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="submit" class="rounded primary button">
|
<button type="submit" class="rounded primary button">
|
||||||
{{ _('Create') }}
|
{{ _('Create') }}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="button" class="rounded button" onclick="window.history.go(-1)">
|
<button type="button" class="rounded button" onclick="window.history.go(-1)">
|
||||||
{{ _('Cancel') }}
|
{{ _('Cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</div></div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user