Skip to content

Commit

Permalink
feat: list component
Browse files Browse the repository at this point in the history
  • Loading branch information
chen_gh committed Sep 10, 2020
1 parent b5307cf commit a648464
Show file tree
Hide file tree
Showing 30 changed files with 2,317 additions and 3 deletions.
649 changes: 649 additions & 0 deletions packages/components/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"@types/classnames": "^2.2.9",
"@types/lodash": "^4.14.136",
"@types/react": "^16.8.23",
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "^16.8.2",
"@types/react-virtualized": "^9.21.10",
"@types/shallowequal": "^1.1.1",
"classnames": "^2.2.6",
"lodash": "^4.17.14",
Expand All @@ -39,7 +41,10 @@
"rc-tooltip": "^4.2.1",
"rc-upload": "^3.2.0",
"rc-util": "^5.0.5",
"react-dnd": "^9.3.4",
"react-dnd-html5-backend": "^9.3.4",
"react-use": "^15.3.3",
"react-virtualized": "^9.22.2",
"shallowequal": "^1.1.0"
},
"devDependencies": {
Expand Down
37 changes: 37 additions & 0 deletions packages/components/src/components/list/Group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react';
import Checkbox from 'antd/lib/checkbox';
import noop from 'lodash/noop';
import { GroupProps } from './interface';

export default class Group extends React.PureComponent<GroupProps> {
public static defaultProps = {
onSelect: noop,
style: {},
};

public render() {
const {
name,
icon,
style,
isMultiple,
showGroupCheckBox = false,
isSelected,
indeterminate,
option,
labelRenderer,
} = this.props;

return (
<div className="gio-select-option group" style={{ ...style, color: '#1248E9' }} onClick={this.handleSelect}>
{icon}
{isMultiple && showGroupCheckBox && <Checkbox checked={isSelected} indeterminate={indeterminate} />}
{labelRenderer ? labelRenderer(option, true) : name}
</div>
);
}

private handleSelect = () => {
this.props.onSelect && this.props.onSelect(this.props.option);
};
}
139 changes: 139 additions & 0 deletions packages/components/src/components/list/Sortable/SortableItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import React, { cloneElement } from 'react';
import classnames from 'classnames';
import { findDOMNode } from 'react-dom';
import reactDnd, { DragSource, DropTarget } from 'react-dnd';

export interface ItemWithId {
value: string | number;
canDrag: boolean;
}

export interface SortableItemProps {
template: React.ReactElement<any>;
index: number;
position: number;
sortData: ItemWithId;
className?: string;
canDrag: boolean;
onDrop: () => any;
onCancel: () => any;
onHover: (sourcePosition: number, targetPosition: number) => any;
}

export interface WrappedSortableItemProps extends SortableItemProps {
connectDragSource: reactDnd.ConnectDragSource;
connectDropTarget: reactDnd.ConnectDropTarget;
isDragging: boolean;
isOver: boolean;
}

const dragSource = {
beginDrag(props: SortableItemProps) {
return {
position: props.position,
};
},

endDrag(props: SortableItemProps, monitor: reactDnd.DragSourceMonitor) {
if (monitor.didDrop()) {
props.onDrop();
} else {
props.onCancel();
}
},

canDrag(props: SortableItemProps) {
return props.canDrag;
},
};

const dropTarget = {
hover(props: SortableItemProps, monitor: reactDnd.DropTargetMonitor) {
const sourcePosition = (monitor.getItem() as SortableItemProps).position;
const targetPosition = props.position;

if (sourcePosition !== targetPosition) {
props.onHover(sourcePosition, targetPosition);
}
},
};

function collectSource(connect: reactDnd.DragSourceConnector, monitor: reactDnd.DragSourceMonitor) {
return {
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging(),
};
}

function collectTarget(connect: reactDnd.DropTargetConnector, monitor: reactDnd.DropTargetMonitor) {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
};
}

class Item extends React.PureComponent<WrappedSortableItemProps, {}> {
public state = {
hovered: false,
};

public componentDidMount() {
const ref = this._createRef();
const { connectDragSource, connectDropTarget } = this.props;
/*eslint-disable */
const domNode = (findDOMNode(this.refs[ref]) as any) as React.ReactElement<{}>;
/* eslint-enable */
connectDragSource(domNode);
connectDropTarget(domNode);
}

public render() {
const isOver = this.props.isOver;
const isDragging = this.props.isDragging;
return cloneElement(this.props.template, {
sortData: this.props.sortData,
index: this.props.index,
isOver,
isDragging,
ref: this._createRef(),
onMouseEnter: this._onMouseEnter,
onMouseLeave: this._onMouseLeave,
onDragStart: this._onDragStart,
className: classnames(this.props.className, 'gio-sortable-item', {
'is-over': isOver,
'is-dragging': isDragging,
'is-hovered': this.state.hovered,
}),
});
}

private _onDragStart = () => {
this.setState({
hovered: false,
});
};

private _onMouseEnter = () => {
this.setState({
hovered: true,
});
};

private _onMouseLeave = () => {
this.setState({
hovered: false,
});
};

private _createRef() {
const position = this.props.position;
const value = this.props.sortData.value;
return `${value}${position}`;
}
}

export default () => {
const type = 'item';
return DragSource(type, dragSource, collectSource)(DropTarget(type, dropTarget, collectTarget)(Item));
};
94 changes: 94 additions & 0 deletions packages/components/src/components/list/Sortable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import createSortableItem, { ItemWithId } from './SortableItem';
import _ from 'lodash';
import { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

export interface SortableCollectionProps {
container: string;
collection: ItemWithId[];
onSorted: (collection: ItemWithId[]) => any;
getId?: (item: ItemWithId, index: number) => string;
template: React.ReactElement<any>;
}

export interface SortableCollectionState {
collection: ItemWithId[];
SortableItemClass: React.Component;
}

export default class SortableCollection extends React.PureComponent<SortableCollectionProps, {}> {
public static defaultProps = { container: 'div' };

public static getDerivedStateFromProps(nextProps: SortableCollectionProps, state: any) {
if (state.stateChanged) {
return { ...state, stateChanged: false };
}
return { collection: nextProps.collection };
}

public state: any = {
collection: this.props.collection,
SortableItemClass: createSortableItem(),
stateChanged: false,
};

public render() {
const SortableItem = this.state.SortableItemClass;

const children = this.state.collection.map((props: ItemWithId, i: number) => {
const originalPosition = this.props.collection.indexOf(props);
const key = this.props.getId ? this.props.getId(props, i) : props.value;
const canDrag = props.canDrag === false ? props.canDrag : true;
return (
<SortableItem
sortData={props}
canDrag={canDrag}
index={i}
key={key}
position={originalPosition}
onHover={this._handleHover}
onDrop={this._handleDrop}
onCancel={this._handleCancel}
template={this.props.template}
/>
);
});

return (
<DndProvider backend={HTML5Backend as any}>
{React.createElement(
this.props.container,
_.omit(this.props, ['collection', 'container', 'onSorted', 'template']),
children
)}
</DndProvider>
);
}

private _handleHover = (originalSourcePosition: number, originalTargetPosition: number) => {
const source = this.props.collection[originalSourcePosition];
const currentSourcePosition = this.state.collection.indexOf(source);
const target = this.props.collection[originalTargetPosition];
const currentTargetPosition = this.state.collection.indexOf(target);
console.log('====================================');
console.log(this.state.collection);
console.log('====================================');
if (source) {
const newCollection = [...this.state.collection];
newCollection.splice(currentSourcePosition, 1);
newCollection.splice(currentTargetPosition, 0, source);
this.setState({ collection: newCollection, stateChanged: true });
}
};

private _handleDrop = () => {
if (this.props.collection !== this.state.collection) {
this.props.onSorted(this.state.collection);
}
};

private _handleCancel = () => {
this.setState({ collection: this.props.collection, stateChanged: true });
};
}
88 changes: 88 additions & 0 deletions packages/components/src/components/list/Sortable/template.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React from 'react';
import classnames from 'classnames';
import { noop, pick, get } from 'lodash';
import { DragMove, CloseCircleFilled } from '@gio-design/icons';

const bemClsFactor = (blockName: string) => (elemName?: string, modifierName?: string) => {
const elemName_ = elemName ? `__${elemName}` : '';
const modifierName_ = modifierName ? `--${modifierName}` : '';
return `${blockName}${elemName_}${modifierName_}`;
};

const cls = bemClsFactor('pa_sider-selected-item');

interface SiderSelectedItemProps {
onRemove: (sortData: any) => void;
selected?: string;
collapsed?: boolean;
onSelect?: (item: any) => void;
[key: string]: any;
}

export default class SiderSelectedItem extends React.PureComponent<SiderSelectedItemProps, any> {
public static defaultProps = {
onRemove: noop,
};

public constructor(props: SiderSelectedItemProps) {
super(props);
this.state = {
visible: false,
};
}

public render() {
const { sortData: item = {} } = this.props;
const props = this.props;
const className = classnames('gio-select-option', cls(), {
indented: props.indented,
selected: item.value === props.selected,
collapsed: props.collapsed,
[this.props.className]: !!this.props.className,
[`v-${item.value}`]: true,
});

return (
<div
{...pick(props, ['onDragStart', 'onMouseEnter', 'onMouseLeave'])}
className={className}
onClick={() => {
if (props.onSelect) {
props.onSelect(item);
}
}}
>
{get(item, 'canDrag') !== false && (
<IconCircle className={classnames({ selected: item.value === props.selected, collapsed: props.collapsed })}>
<DragMove className="icon-drag" />
</IconCircle>
)}

{props.collapsed ? null : (
<span className={classnames(cls('name-wrap'))}>
<span className={classnames(cls('name'), { selected: item.value === props.selected })} title={item.name}>
{item.label}
</span>
</span>
)}

<span
onClick={(e) => e.stopPropagation()}
className={classnames('dh-can-remove', {
collapsed: props.collapsed,
})}
>
<CloseCircleFilled size="small" color="#5C4E61" />
</span>
</div>
);
}
}

const IconCircle = (props: any) => {
const className = classnames('pa-sider-icon-circle', props.className, {
scaled: props.scaled, // 利用 transform scale 解决 font-size < 12 的问题
});

return <span className={className}>{props.children}</span>;
};
Loading

0 comments on commit a648464

Please sign in to comment.