Skip to content

Commit

Permalink
feat(EditableList): Add editable list widget
Browse files Browse the repository at this point in the history
  • Loading branch information
floryst committed May 24, 2018
1 parent 3a0a0d4 commit 3eb894b
Show file tree
Hide file tree
Showing 3 changed files with 451 additions and 0 deletions.
109 changes: 109 additions & 0 deletions src/React/Widgets/EditableListWidget/example/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Load CSS
import 'normalize.css';
import 'font-awesome/css/font-awesome.css';

import React from 'react';
import ReactDOM from 'react-dom';

import EditableListWidget from '..';

const container = document.querySelector('.content');

const columns = [
{
key: 'name',
dataKey: 'name',
label: 'Name',
// className: 'fancyNameClass',
render: (name) => <span style={{ fontStyle: 'italic' }}>{name}</span>,
},
{
key: 'age',
dataKey: 'age',
label: 'Age',
},
{
key: 'gender',
dataKey: 'gender',
label: 'Gender',
},
];

const data = [
{
name: 'Wayne',
age: 28,
gender: 'male',
},
{
name: 'Deborah',
age: 45,
gender: 'female',
},
{
name: 'Nicole',
age: 22,
gender: 'female',
},
{
name: 'Carlos',
age: 31,
gender: 'male',
},
];

class Example extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
data,
};

this.onAdd = this.onAdd.bind(this);
this.onDelete = this.onDelete.bind(this);
this.onSortChange = this.onSortChange.bind(this);
}

onAdd(idx) {
const newData = this.state.data.slice();
newData.splice(idx, 0, {
name: 'New Name',
age: '0',
gender: 'something',
});
this.setState({ data: newData });
}

onDelete(key) {
const newData = this.state.data.slice();
newData.splice(key, 1);
this.setState({ data: newData });
}

onSortChange(src, dst) {
const newData = this.state.data.slice();
newData.splice(dst, 0, ...newData.splice(src, 1));
this.setState({ data: newData });
}

render() {
const keyedData = this.state.data.map((item, idx) =>
Object.assign({ key: idx }, item)
);

return (
<EditableListWidget
sortable
columns={columns}
data={keyedData}
onAdd={this.onAdd}
onDelete={this.onDelete}
onSortChange={this.onSortChange}
/>
);
}
}

ReactDOM.render(<Example />, container);

document.body.style.margin = '10px';
252 changes: 252 additions & 0 deletions src/React/Widgets/EditableListWidget/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import React from 'react';
import PropTypes from 'prop-types';

import style from 'PVWStyle/ReactWidgets/EditableListWidget.mcss';

function clamp(min, max, value) {
return Math.min(max, Math.max(min, value));
}

class EditableList extends React.PureComponent {
constructor(props) {
super(props);

this.state = {
dragTargetKey: null,
dragOffset: 0,
initialMouseY: 0,
initialTargetY: 0,
initialIndex: 0,
sortIndex: 0,
};

this.container = null;
this.dragTargetEl = null;

this.getWindow = this.getWindow.bind(this);
this.onDragStart = this.onDragStart.bind(this);
this.onDragMove = this.onDragMove.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
}

onDragStart(ev, itemKey) {
// gets the drag handle's containing row div
let target = ev.target;
while (!target.getAttribute('draggrip')) {
target = target.parentNode;
}
target = target.parentNode;

const itemIndex = this.props.data.findIndex((item) => item.key === itemKey);

const targetRect = target.getBoundingClientRect();
const containerRect = this.container.getBoundingClientRect();

const window = this.getWindow();
window.addEventListener('mousemove', this.onDragMove);
window.addEventListener('mouseup', this.onDragEnd);

this.dragTargetEl = target;

this.setState({
dragTargetKey: itemKey,
dragOffset: 0,
// only lock to Y axis
initialMouseY: ev.pageY,
initialTargetY: targetRect.top - containerRect.top,
initialIndex: itemIndex,
sortIndex: itemIndex,
});

ev.stopPropagation();
ev.preventDefault();
}

onDragMove(ev) {
const containerRect = this.container.getBoundingClientRect();
const clampedMouseY = clamp(
containerRect.top,
containerRect.bottom,
ev.pageY
);

// ignore currently dragging node
const siblings = Array.from(this.dragTargetEl.parentNode.childNodes).filter(
(node) => node !== this.dragTargetEl
);

let newIndex = -1;
for (let i = 0; i < siblings.length; ++i) {
const { top: siblingTop, bottom: siblingBottom } = siblings[
i
].getBoundingClientRect();

if (clampedMouseY >= siblingTop && clampedMouseY <= siblingBottom) {
newIndex = i;
}
}

const targetHeight = this.dragTargetEl.offsetHeight;
const newDragOffset = clamp(
-this.state.initialTargetY,
containerRect.height - this.state.initialTargetY - targetHeight,
ev.pageY - this.state.initialMouseY
);

this.setState({
dragOffset: newDragOffset,
sortIndex: newIndex,
});

ev.stopPropagation();
ev.preventDefault();
}

onDragEnd(ev) {
window.removeEventListener('mousemove', this.onDragMove);
window.removeEventListener('mouseup', this.onDragEnd);

this.setState({ dragTargetKey: null });

ev.stopPropagation();
ev.preventDefault();

this.props.onSortChange(this.state.initialIndex, this.state.sortIndex);
}

getWindow() {
const doc = (this.container || {}).ownerDocument || document;
return doc.defaultView || window;
}

render() {
const rows = this.props.data.map((item) => {
const cells = this.props.columns.map((column) => {
const value = item[column.dataKey];
const content = column.render ? column.render(value, item) : value;
const cellKey = `${column.key}::${item.key}`;
const classes = [style.column];
Array.prototype.push.apply(classes, column.classNames);

return (
<div key={cellKey} className={classes.join(' ')}>
<div className={style.columnVerticalWrapper}>
{column.label ? (
<span className={style.colname}>{column.label}:</span>
) : null}
<span className={style.colcontent}>{content}</span>
</div>
</div>
);
});

const rowClasses = [style.row];
const rowStyles = {};

if (this.state.dragTargetKey === item.key) {
rowClasses.push(style.dragging);
Object.assign(rowStyles, {
top: `${this.state.initialTargetY + this.state.dragOffset}px`,
});
}

return (
<div key={item.key} className={rowClasses.join(' ')} style={rowStyles}>
{this.props.sortable ? (
<div
draggrip="true"
className={style.dragGrip}
onMouseDown={(ev) => this.onDragStart(ev, item.key)}
>
<svg
className={style.icon}
width="14"
height="13"
viewBox="0 0 14 13"
>
<path d="M0 1L14 1L14 4L0 4L0 1Z" />
<path d="M14 5.45L0 5.45L0 8.45L14 8.45L14 5.45Z" />
<path d="M14 10L0 10L0 13L14 13L14 10Z" />
</svg>
</div>
) : null}
<div className={style.rowContent}>{cells}</div>
<div className={style.remove}>
<button
className={style.icon}
onClick={() => this.props.onDelete(item.key)}
>
<svg width="16" height="16" viewBox="0 0 16 16">
<path d="M13.3 0L16 2.71L2.71 16L0 13.3L13.3 0Z" />
<path d="M16 13.3L13.29 16L0 2.71L2.71 0L16 13.3Z" />
</svg>
</button>
</div>
</div>
);
});

if (Number.isInteger(this.state.dragTargetKey)) {
let insertIndex = this.state.sortIndex;
if (insertIndex > this.state.initialIndex) {
// +1 so the placeholder index skips the drag target.
insertIndex += 1;
}
rows.splice(
insertIndex,
0,
<div
key="item-placeholder"
className={style.placeholder}
style={{
width: `${this.dragTargetEl.offsetWidth}px`,
height: `${this.dragTargetEl.offsetHeight}px`,
}}
/>
);
}

return (
<div>
<div
className={style.list}
ref={(r) => {
this.container = r;
}}
>
{rows}
</div>
<div className={style.row}>
<button
className={style.addButton}
onClick={() => this.props.onAdd(rows.length)}
>
{this.props.addLabel}
</button>
</div>
</div>
);
}
}

EditableList.propTypes = {
columns: PropTypes.array,
data: PropTypes.array,
sortable: PropTypes.bool,
onAdd: PropTypes.func,
onDelete: PropTypes.func,
onSortChange: PropTypes.func,
addLabel: PropTypes.string,
};

EditableList.defaultProps = {
columns: [],
data: [],
sortable: false,
onAdd: () => {},
onDelete: () => {},
onSortChange: () => {},
addLabel: 'Add new item',
};

export default EditableList;
Loading

0 comments on commit 3eb894b

Please sign in to comment.