1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-28 08:06:56 +00:00

refactor(metadata): remove ui-component (#7492)

1. ModalPortal
2. Icon
3. IconBtn
4. Loading
5. CenteredLoading
6. ClickOutside
7. SearchInput
8. Switch
9. CustomizeAddTool
10. SfCalendar
11. SfFilterCalendar
12. CustomizeSelect
13. CustomizePopover
14. FieldDisplaySettings
15. Formatters
16. remove duplicate codes
This commit is contained in:
Jerry Ren
2025-03-01 10:12:48 +08:00
committed by GitHub
parent 67083238c2
commit 890880a5c8
281 changed files with 3523 additions and 1271 deletions

View File

@@ -0,0 +1,100 @@
.seafile-customize-select {
position: relative;
display: flex;
padding: 0 10px;
border-radius: 3px;
align-items: center;
justify-content: space-between;
max-width: 900px;
user-select: none;
text-align: left;
line-height: 1.5;
background-image: none;
font-size: 14px;
color: #212529;
}
.seafile-customize-select:focus,
.seafile-customize-select.focus {
border-color: #1991eb !important;
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
}
.seafile-customize-select.disabled:focus,
.seafile-customize-select.focus.disabled,
.seafile-customize-select.disabled:hover {
border-color: rgba(0, 40, 100, 0.12) !important;
box-shadow: unset;
cursor: default;
}
.seafile-customize-select:hover {
cursor: pointer;
border-color: rgb(179, 179, 179);
}
.seafile-customize-select .sf3-font-down {
color: #999;
}
.seafile-customize-select .selected-option {
display: flex;
flex: 1;
overflow: hidden;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
background: #fff;
}
.seafile-customize-select .selected-option .custom-select-dropdown-icon {
height: 12px;
width: 12px;
color: #999;
display: flex;
align-items: center;
justify-content: center;
margin-left: 0.5rem;
}
.seafile-customize-select.selector-collaborator .seafile-option-group .seafile-option-group-content,
.seafile-customize-select.selector-group .seafile-option-group .seafile-option-group-content {
padding: 10px;
}
.seafile-customize-select.selector-collaborator .seafile-option-group .seafile-option-group-content {
padding: 10px 0;
}
.seafile-customize-select.selector-collaborator .option {
padding: 5px 0 5px 10px !important;
line-height: 20px;
}
.seafile-customize-select.selector-group .option {
height: 30px;
display: flex;
align-items: center;
}
.seafile-customize-select.selector-group .select-group-option {
justify-content: space-between;
}
.seafile-customize-select.selector-group .selected-option .selected-group {
padding: 0 2px;
background: #eceff4;
border-radius: 3px;
}
.seafile-customize-select .selected-option-show {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.seafile-customize-select .select-placeholder {
line-height: 1;
font-size: 14px;
white-space: nowrap;
}

View File

@@ -0,0 +1,173 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ModalPortal from '../modal-portal';
import SelectOptionGroup from './select-option-group';
import { getEventClassName } from '../../utils/dom';
import './index.css';
class CustomizeSelect extends Component {
constructor(props) {
super(props);
this.state = {
isShowSelectOptions: false
};
}
onSelectToggle = (event) => {
event.preventDefault();
/*
if select is showing, click events do not need to be monitored by other click events,
so it can be closed when other select is clicked.
*/
if (this.state.isShowSelectOptions) event.stopPropagation();
const eventClassName = getEventClassName(event);
if (this.props.readOnly || eventClassName.indexOf('option-search-control') > -1 || eventClassName === 'seafile-option-group-search') return;
// Prevent closing by pressing the space bar in the search input
if (event.target.value === '') return;
this.setState({
isShowSelectOptions: !this.state.isShowSelectOptions
});
};
onClick = (event) => {
if (this.props.isShowSelected && event.target.className.includes('icon-fork-number')) {
return;
}
if (!this.selector.contains(event.target)) {
this.closeSelect();
}
};
closeSelect = () => {
this.setState({ isShowSelectOptions: false });
};
getSelectedOptionTop = () => {
if (!this.selector) return 38;
const { height } = this.selector.getBoundingClientRect();
return height;
};
getFilterOptions = (searchValue) => {
const { options, searchable } = this.props;
if (!searchable) return options || [];
const validSearchVal = searchValue.trim().toLowerCase();
if (!validSearchVal) return options || [];
return options.filter(option => {
const { value, name } = option;
if (typeof name === 'string') {
return name.toLowerCase().indexOf(validSearchVal) > -1;
}
if (typeof value === 'object') {
if (value.column) {
return value.column.name.toLowerCase().indexOf(validSearchVal) > -1;
}
if (value.name) {
return value.name.toLowerCase().indexOf(validSearchVal) > -1;
}
return value.columnOption && value.columnOption.name.toLowerCase().indexOf(validSearchVal) > -1;
}
return false;
});
};
renderDropDownIcon = () => {
const { readOnly, component } = this.props;
if (readOnly) return;
const { DropDownIcon } = component || {};
if (DropDownIcon) {
return (
<div className="custom-select-dropdown-icon">{DropDownIcon}</div>
);
}
return (<i className="sf3-font sf3-font-down" aria-hidden="true"></i>);
};
render() {
const { className, value, options, placeholder, searchable, searchPlaceholder, noOptionsPlaceholder,
readOnly, isInModal, addOptionAble, component } = this.props;
return (
<div
ref={(node) => this.selector = node}
className={classnames('seafile-customize-select custom-select',
{ 'focus': this.state.isShowSelectOptions },
{ 'disabled': readOnly },
className
)}
onClick={this.onSelectToggle}>
<div className="selected-option">
{value && value.label ?
<span className="selected-option-show">{value.label}</span>
:
<span className="select-placeholder">{placeholder}</span>
}
{this.renderDropDownIcon()}
</div>
{this.state.isShowSelectOptions && !isInModal && (
<SelectOptionGroup
value={value}
addOptionAble={addOptionAble}
component={component}
isShowSelected={this.props.isShowSelected}
top={this.getSelectedOptionTop()}
options={options}
onSelectOption={this.props.onSelectOption}
searchable={searchable}
searchPlaceholder={searchPlaceholder}
noOptionsPlaceholder={noOptionsPlaceholder}
onClickOutside={this.onClick}
closeSelect={this.closeSelect}
getFilterOptions={this.getFilterOptions}
supportMultipleSelect={this.props.supportMultipleSelect}
/>
)}
{this.state.isShowSelectOptions && isInModal && (
<ModalPortal>
<SelectOptionGroup
className={className}
value={value}
addOptionAble={addOptionAble}
component={component}
isShowSelected={this.props.isShowSelected}
position={this.selector.getBoundingClientRect()}
isInModal={isInModal}
top={this.getSelectedOptionTop()}
options={options}
onSelectOption={this.props.onSelectOption}
searchable={searchable}
searchPlaceholder={searchPlaceholder}
noOptionsPlaceholder={noOptionsPlaceholder}
onClickOutside={this.onClick}
closeSelect={this.closeSelect}
getFilterOptions={this.getFilterOptions}
supportMultipleSelect={this.props.supportMultipleSelect}
/>
</ModalPortal>
)}
</div>
);
}
}
CustomizeSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.object,
options: PropTypes.array,
placeholder: PropTypes.string,
onSelectOption: PropTypes.func,
readOnly: PropTypes.bool,
searchable: PropTypes.bool,
addOptionAble: PropTypes.bool,
searchPlaceholder: PropTypes.string,
noOptionsPlaceholder: PropTypes.string,
component: PropTypes.object,
supportMultipleSelect: PropTypes.bool,
isShowSelected: PropTypes.bool,
isInModal: PropTypes.bool, // if select component in a modal (option group need ModalPortal to show)
};
export default CustomizeSelect;

View File

@@ -0,0 +1,103 @@
.seafile-option-group {
position: absolute;
left: 0;
min-height: 60px;
max-height: 300px;
min-width: 100%;
max-width: 15rem;
padding: 0.5rem 0;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
background: #fff;
border: 1px solid rgba(0, 40, 100, 0.12);
border-radius: 3px;
z-index: 10001;
}
.seafile-option-group .seafile-option-group-search {
width: 100%;
padding: 0 10px 6px 10px;
min-width: 170px;
}
.seafile-option-group-search .form-control {
height: 31px;
}
.seafile-option-group .none-search-result {
height: 100px;
width: 100%;
padding: 10px;
color: #666666;
}
.seafile-option-group .seafile-option-group-content {
max-height: 252px;
overflow-y: auto;
}
.seafile-select-option {
display: block;
width: 100%;
line-height: 24px;
padding: 0.25rem 10px;
clear: both;
font-weight: 400;
text-align: inherit;
background-color: transparent;
border: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.seafile-select-option.seafile-select-option-active {
background-color: #20a0ff;
color: #fff;
cursor: pointer;
}
.seafile-select-option.seafile-select-option-active .select-option-name {
color: #fff;
}
.seafile-select-option:hover .header-icon .seafile-multicolor-icon,
.seafile-select-option.seafile-select-option-active .header-icon .seafile-multicolor-icon {
fill: #fff;
}
.seafile-select-option:not(.seafile-select-option-active):hover .header-icon .seafile-multicolor-icon {
fill: #aaa;
}
.seafile-select-option .select-option-name .single-select-option {
margin: 0 0 0 12px;
}
.seafile-select-option .select-option-name .multiple-select-option {
margin: 0;
}
.seafile-option-group-selector-single-select .select-option-name,
.seafile-option-group-selector-multiple-select .multiple-option-name {
display: flex;
align-items: center;
justify-content: space-between;
}
.seafile-option-group-selector-multiple-select .multiple-check-icon {
display: inline-flex;
width: 20px;
text-align: center;
}
.seafile-option-group-selector-multiple-select .multiple-check-icon .seafile-multicolor-icon-check-mark {
font-size: 12px;
color: #798d99;
}
.seafile-option-group-selector-single-select .seafile-select-option:hover,
.seafile-option-group-selector-single-select .seafile-select-option.seafile-select-option-active,
.seafile-option-group-selector-multiple-select .seafile-select-option:hover,
.seafile-option-group-selector-multiple-select .seafile-select-option.seafile-select-option-active {
background-color: #f5f5f5;
}

View File

@@ -0,0 +1,233 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import ClickOutside from '../../click-outside';
import SearchInput from '../../search-input';
import Option from './option';
import { KeyCodes } from '../../../constants';
import './index.css';
const OPTION_HEIGHT = 32;
class SelectOptionGroup extends Component {
constructor(props) {
super(props);
this.state = {
searchVal: '',
activeIndex: -1,
disableHover: false,
};
this.filterOptions = null;
this.timer = null;
}
componentDidMount() {
window.addEventListener('keydown', this.onHotKey);
setTimeout(() => {
this.resetMenuStyle();
}, 1);
}
componentWillUnmount() {
this.filterOptions = null;
this.timer && clearTimeout(this.timer);
window.removeEventListener('keydown', this.onHotKey);
}
resetMenuStyle = () => {
const { isInModal, position } = this.props;
const { top, height } = this.optionGroupRef.getBoundingClientRect();
if (isInModal) {
if (position.y + position.height + height > window.innerHeight) {
this.optionGroupRef.style.top = (position.y - height) + 'px';
}
this.optionGroupRef.style.opacity = 1;
}
else {
if (height + top > window.innerHeight) {
const borderWidth = 2;
this.optionGroupRef.style.top = -1 * (height + borderWidth) + 'px';
}
}
};
onHotKey = (event) => {
const keyCode = event.keyCode;
if (keyCode === KeyCodes.UpArrow) {
this.onPressUp();
} else if (keyCode === KeyCodes.DownArrow) {
this.onPressDown();
} else if (keyCode === KeyCodes.Enter) {
let option = this.filterOptions && this.filterOptions[this.state.activeIndex];
if (option) {
this.props.onSelectOption(option.value);
if (!this.props.supportMultipleSelect) {
this.props.closeSelect();
}
}
} else if (keyCode === KeyCodes.Tab || keyCode === KeyCodes.Escape) {
this.props.closeSelect();
}
};
onPressUp = () => {
if (this.state.activeIndex > 0) {
this.setState({ activeIndex: this.state.activeIndex - 1 }, () => {
this.scrollContent();
});
}
};
onPressDown = () => {
if (this.filterOptions && this.state.activeIndex < this.filterOptions.length - 1) {
this.setState({ activeIndex: this.state.activeIndex + 1 }, () => {
this.scrollContent();
});
}
};
onMouseDown = (e) => {
const { isInModal } = this.props;
// prevent event propagation when click option or search input
if (isInModal) {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}
};
scrollContent = () => {
const { offsetHeight, scrollTop } = this.optionGroupContentRef;
this.setState({ disableHover: true });
this.timer = setTimeout(() => {
this.setState({ disableHover: false });
}, 500);
if (this.state.activeIndex * OPTION_HEIGHT === 0) {
this.optionGroupContentRef.scrollTop = 0;
return;
}
if (this.state.activeIndex * OPTION_HEIGHT < scrollTop) {
this.optionGroupContentRef.scrollTop = scrollTop - OPTION_HEIGHT;
}
else if (this.state.activeIndex * OPTION_HEIGHT > offsetHeight + scrollTop) {
this.optionGroupContentRef.scrollTop = scrollTop + OPTION_HEIGHT;
}
};
changeIndex = (index) => {
this.setState({ activeIndex: index });
};
onChangeSearch = (searchVal) => {
let value = searchVal || '';
if (value !== this.state.searchVal) {
this.setState({ searchVal: value, activeIndex: -1, });
}
};
renderOptGroup = (searchVal) => {
let { noOptionsPlaceholder, onSelectOption } = this.props;
this.filterOptions = this.props.getFilterOptions(searchVal);
if (this.filterOptions.length === 0) {
return (
<div className="none-search-result">{noOptionsPlaceholder}</div>
);
}
return this.filterOptions.map((opt, i) => {
let key = opt.value.column ? opt.value.column.key : i;
let isActive = this.state.activeIndex === i;
return (
<Option
key={key}
index={i}
isActive={isActive}
value={opt.value}
onSelectOption={onSelectOption}
changeIndex={this.changeIndex}
supportMultipleSelect={this.props.supportMultipleSelect}
disableHover={this.state.disableHover}
>
{opt.label}
</Option>
);
});
};
render() {
const { searchable, searchPlaceholder, top, left, minWidth, value, isShowSelected, isInModal, position,
className, addOptionAble, component } = this.props;
const { AddOption } = component || {};
let { searchVal } = this.state;
let style = { top: top || 0, left: left || 0 };
if (minWidth) {
style = { top: top || 0, left: left || 0, minWidth };
}
if (isInModal) {
style = {
position: 'fixed',
left: position.x,
top: position.y + position.height,
minWidth: position.width,
opacity: 0,
};
}
return (
<ClickOutside onClickOutside={this.props.onClickOutside}>
<div
className={classnames('seafile-option-group', className ? 'seafile-option-group-' + className : '', {
'pt-0': isShowSelected,
'create-new-seafile-option-group': addOptionAble,
})}
ref={(ref) => this.optionGroupRef = ref}
style={style}
onMouseDown={this.onMouseDown}
>
{isShowSelected &&
<div className="editor-list-delete mb-2" onClick={(e) => e.stopPropagation()}>{value.label || ''}</div>
}
{searchable && (
<div className="seafile-option-group-search">
<SearchInput
className="option-search-control"
placeholder={searchPlaceholder}
onChange={this.onChangeSearch}
autoFocus={true}
/>
</div>
)}
<div className="seafile-option-group-content" ref={(ref) => this.optionGroupContentRef = ref}>
{this.renderOptGroup(searchVal)}
</div>
{addOptionAble && AddOption}
</div>
</ClickOutside>
);
}
}
SelectOptionGroup.propTypes = {
top: PropTypes.number,
left: PropTypes.number,
minWidth: PropTypes.number,
options: PropTypes.array,
onSelectOption: PropTypes.func,
searchable: PropTypes.bool,
addOptionAble: PropTypes.bool,
component: PropTypes.object,
searchPlaceholder: PropTypes.string,
noOptionsPlaceholder: PropTypes.string,
onClickOutside: PropTypes.func.isRequired,
closeSelect: PropTypes.func.isRequired,
getFilterOptions: PropTypes.func.isRequired,
supportMultipleSelect: PropTypes.bool,
value: PropTypes.object,
isShowSelected: PropTypes.bool,
stopClickEvent: PropTypes.bool,
isInModal: PropTypes.bool,
position: PropTypes.object,
className: PropTypes.string,
};
export default SelectOptionGroup;

View File

@@ -0,0 +1,50 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
class Option extends Component {
onSelectOption = (value, event) => {
if (this.props.supportMultipleSelect) {
event.stopPropagation();
}
this.props.onSelectOption(value, event);
};
onMouseEnter = () => {
if (!this.props.disableHover) {
this.props.changeIndex(this.props.index);
}
};
onMouseLeave = () => {
if (!this.props.disableHover) {
this.props.changeIndex(-1);
}
};
render() {
return (
<div
className={classnames('seafile-select-option', { 'seafile-select-option-active': this.props.isActive })}
onClick={this.onSelectOption.bind(this, this.props.value)}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>{this.props.children}
</div>
);
}
}
Option.propTypes = {
index: PropTypes.number,
isActive: PropTypes.bool,
changeIndex: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
onSelectOption: PropTypes.func,
supportMultipleSelect: PropTypes.bool,
disableHover: PropTypes.bool,
};
export default Option;