ampache_react/app/components/elements/Grid.jsx

261 lines
8.7 KiB
React
Raw Normal View History

2016-07-07 23:23:18 +02:00
import React, { Component, PropTypes } from "react";
import { Link} from "react-router";
import CSSModules from "react-css-modules";
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
import Immutable from "immutable";
2016-07-07 23:23:18 +02:00
import imagesLoaded from "imagesloaded";
import Isotope from "isotope-layout";
import Fuse from "fuse.js";
2016-07-30 22:54:19 +02:00
import shallowCompare from "react-addons-shallow-compare";
2016-07-07 23:23:18 +02:00
import FilterBar from "./FilterBar";
import Pagination from "./Pagination";
import { immutableDiff, messagesMap } from "../../utils/";
import commonMessages from "../../locales/messagesDescriptors/common";
import messages from "../../locales/messagesDescriptors/grid";
2016-07-07 23:23:18 +02:00
import css from "../../styles/elements/Grid.scss";
const gridMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
class GridItemCSSIntl extends Component {
2016-07-07 23:23:18 +02:00
render () {
const {formatMessage} = this.props.intl;
2016-07-07 23:23:18 +02:00
var nSubItems = this.props.item[this.props.subItemsType];
if (Array.isArray(nSubItems)) {
nSubItems = nSubItems.length;
}
var subItemsLabel = formatMessage(gridMessages[this.props.subItemsLabel], { itemCount: nSubItems });
2016-08-02 13:07:12 +02:00
const to = "/" + this.props.itemsType + "/" + this.props.item.id;
const id = "grid-item-" + this.props.itemsType + "/" + this.props.item.id;
2016-08-02 13:07:12 +02:00
const title = formatMessage(gridMessages["app.grid.goTo" + this.props.itemsType.capitalize() + "Page"]);
2016-07-07 23:23:18 +02:00
return (
<div className="grid-item col-xs-6 col-sm-3" styleName="placeholders" id={id}>
<div className="grid-item-content text-center">
<Link title={title} to={to}><img src={this.props.item.art} width="200" height="200" className="img-responsive img-circle art" styleName="art" alt={this.props.item.name}/></Link>
<h4 className="name" styleName="name">{this.props.item.name}</h4>
2016-07-07 23:23:18 +02:00
<span className="sub-items text-muted"><span className="n-sub-items">{nSubItems}</span> <span className="sub-items-type">{subItemsLabel}</span></span>
</div>
</div>
);
}
}
GridItemCSSIntl.propTypes = {
2016-07-07 23:23:18 +02:00
item: PropTypes.object.isRequired,
2016-08-02 13:07:12 +02:00
itemsType: PropTypes.string.isRequired,
itemsLabel: PropTypes.string.isRequired,
subItemsType: PropTypes.string.isRequired,
subItemsLabel: PropTypes.string.isRequired,
intl: intlShape.isRequired
2016-07-07 23:23:18 +02:00
};
export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
2016-07-07 23:23:18 +02:00
const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
getSortData: {
name: ".name",
nSubitems: ".sub-items .n-sub-items"
},
transitionDuration: 0,
sortBy: "name",
itemSelector: ".grid-item",
percentPosition: true,
layoutMode: "fitRows",
filter: "*",
fitRows: {
gutter: 0
}
};
export class Grid extends Component {
constructor (props) {
super(props);
// Init grid data member
this.iso = null;
this.handleFiltering = this.handleFiltering.bind(this);
}
createIsotopeContainer () {
if (this.iso == null) {
this.iso = new Isotope(this.refs.grid, ISOTOPE_OPTIONS);
}
}
handleFiltering (props) {
// If no query provided, drop any filter in use
if (props.filterText == "") {
return this.iso.arrange(ISOTOPE_OPTIONS);
}
// Use Fuse for the filter
var result = new Fuse(
2016-08-02 13:07:12 +02:00
props.items.toArray(),
2016-07-07 23:23:18 +02:00
{
"keys": ["name"],
"threshold": 0.4,
"include": ["score"]
}).search(props.filterText);
// Apply filter on grid
this.iso.arrange({
filter: function (item) {
var name = $(item).find(".name").text();
return result.find(function (i) { return i.item.name == name; });
2016-07-07 23:23:18 +02:00
},
transitionDuration: "0.4s",
getSortData: {
relevance: function (item) {
var name = $(item).find(".name").text();
return result.reduce(function (p, c) {
if (c.item.name == name) {
return c.score + p;
}
return p;
}, 0);
}
},
sortBy: "relevance"
});
this.iso.updateSortData();
this.iso.arrange();
}
shouldComponentUpdate(nextProps, nextState) {
2016-07-30 22:54:19 +02:00
return shallowCompare(this, nextProps, nextState);
2016-07-07 23:23:18 +02:00
}
componentWillReceiveProps(nextProps) {
2016-07-30 22:54:19 +02:00
if (nextProps.filterText !== this.props.filterText) {
2016-07-07 23:23:18 +02:00
this.handleFiltering(nextProps);
}
}
componentDidMount () {
// Setup grid
this.createIsotopeContainer();
// Only arrange if there are elements to arrange
2016-07-30 22:54:19 +02:00
const length = this.props.items.length || 0;
if (length > 0) {
2016-07-07 23:23:18 +02:00
this.iso.arrange();
}
}
componentDidUpdate(prevProps) {
// The list of keys seen in the previous render
2016-07-30 22:54:19 +02:00
let currentKeys = prevProps.items.map(
2016-08-02 13:07:12 +02:00
(n) => "grid-item-" + prevProps.itemsType + "/" + n.id);
2016-07-07 23:23:18 +02:00
// The latest list of keys that have been rendered
2016-08-02 13:07:12 +02:00
const {itemsType} = this.props;
2016-07-30 22:54:19 +02:00
let newKeys = this.props.items.map(
2016-08-02 13:07:12 +02:00
(n) => "grid-item-" + itemsType + "/" + n.id);
2016-07-07 23:23:18 +02:00
// Find which keys are new between the current set of keys and any new children passed to this component
let addKeys = immutableDiff(newKeys, currentKeys);
2016-07-07 23:23:18 +02:00
// Find which keys have been removed between the current set of keys and any new children passed to this component
let removeKeys = immutableDiff(currentKeys, newKeys);
2016-07-07 23:23:18 +02:00
2016-08-02 13:07:12 +02:00
var iso = this.iso;
if (removeKeys.count() > 0) {
2016-08-02 13:07:12 +02:00
removeKeys.forEach(removeKey => iso.remove(document.getElementById(removeKey)));
iso.arrange();
2016-07-07 23:23:18 +02:00
}
if (addKeys.count() > 0) {
const itemsToAdd = addKeys.map((addKey) => document.getElementById(addKey)).toArray();
2016-08-02 13:07:12 +02:00
iso.addItems(itemsToAdd);
iso.arrange();
2016-07-07 23:23:18 +02:00
}
// Layout again after images are loaded
imagesLoaded(this.refs.grid).on("progress", function() {
// Layout after each image load, fix for responsive grid
if (!iso) { // Grid could have been destroyed in the meantime
return;
}
iso.layout();
});
}
render () {
var gridItems = [];
2016-08-02 13:07:12 +02:00
const { itemsType, itemsLabel, subItemsType, subItemsLabel } = this.props;
2016-07-07 23:23:18 +02:00
this.props.items.forEach(function (item) {
2016-08-02 13:07:12 +02:00
gridItems.push(<GridItem item={item} itemsType={itemsType} itemsLabel={itemsLabel} subItemsType={subItemsType} subItemsLabel={subItemsLabel} key={item.id} />);
2016-07-07 23:23:18 +02:00
});
var loading = null;
if (gridItems.length == 0 && this.props.isFetching) {
loading = (
<div className="row text-center">
<p>
<FormattedMessage {...gridMessages["app.common.loading"]} />
</p>
</div>
);
}
2016-07-07 23:23:18 +02:00
return (
<div>
{ loading }
<div className="row">
<div className="grid" ref="grid">
{/* Sizing element */}
<div className="grid-sizer col-xs-6 col-sm-3"></div>
{/* Other items */}
{ gridItems }
</div>
2016-07-07 23:23:18 +02:00
</div>
</div>
);
}
}
Grid.propTypes = {
isFetching: PropTypes.bool.isRequired,
items: PropTypes.instanceOf(Immutable.List).isRequired,
2016-08-02 13:07:12 +02:00
itemsType: PropTypes.string.isRequired,
itemsLabel: PropTypes.string.isRequired,
2016-07-07 23:23:18 +02:00
subItemsType: PropTypes.string.isRequired,
2016-08-02 13:07:12 +02:00
subItemsLabel: PropTypes.string.isRequired,
2016-07-07 23:23:18 +02:00
filterText: PropTypes.string
};
export default class FilterablePaginatedGrid extends Component {
constructor (props) {
super(props);
this.state = {
filterText: ""
};
this.handleUserInput = this.handleUserInput.bind(this);
}
handleUserInput (filterText) {
this.setState({
filterText: filterText.trim()
});
}
render () {
return (
<div>
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
<Grid filterText={this.state.filterText} {...this.props.grid} />
<Pagination {...this.props.pagination} />
2016-07-07 23:23:18 +02:00
</div>
);
}
}
FilterablePaginatedGrid.propTypes = {
grid: PropTypes.object.isRequired,
pagination: PropTypes.object.isRequired
2016-07-07 23:23:18 +02:00
};