2016-08-10 21:36:11 +02:00
|
|
|
// NPM imports
|
2016-07-07 23:23:18 +02:00
|
|
|
import React, { Component, PropTypes } from "react";
|
|
|
|
import { Link} from "react-router";
|
2016-07-29 23:57:21 +02:00
|
|
|
import CSSModules from "react-css-modules";
|
2016-08-01 00:26:52 +02:00
|
|
|
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
2016-08-04 15:28:07 +02:00
|
|
|
import FontAwesome from "react-fontawesome";
|
2016-08-01 00:26:52 +02:00
|
|
|
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
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Local imports
|
|
|
|
import { immutableDiff, messagesMap } from "../../utils/";
|
|
|
|
|
|
|
|
// Other components
|
2016-07-07 23:23:18 +02:00
|
|
|
import FilterBar from "./FilterBar";
|
|
|
|
import Pagination from "./Pagination";
|
2016-08-01 00:26:52 +02:00
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Translations
|
2016-08-01 00:26:52 +02:00
|
|
|
import commonMessages from "../../locales/messagesDescriptors/common";
|
|
|
|
import messages from "../../locales/messagesDescriptors/grid";
|
2016-07-07 23:23:18 +02:00
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Styles
|
2016-07-29 23:57:21 +02:00
|
|
|
import css from "../../styles/elements/Grid.scss";
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Define translations
|
2016-08-01 00:26:52 +02:00
|
|
|
const gridMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Constants
|
|
|
|
const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
|
|
|
|
getSortData: {
|
|
|
|
name: ".name",
|
2016-08-10 23:50:23 +02:00
|
|
|
nSubitems: ".sub-items .n-sub-items",
|
2016-08-10 21:36:11 +02:00
|
|
|
},
|
|
|
|
transitionDuration: 0,
|
|
|
|
sortBy: "name",
|
|
|
|
itemSelector: ".grid-item",
|
|
|
|
percentPosition: true,
|
|
|
|
layoutMode: "fitRows",
|
|
|
|
filter: "*",
|
|
|
|
fitRows: {
|
2016-08-10 23:50:23 +02:00
|
|
|
gutter: 0,
|
|
|
|
},
|
2016-08-10 21:36:11 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A single item in the grid, art + text under the art.
|
|
|
|
*/
|
2016-08-01 00:26:52 +02:00
|
|
|
class GridItemCSSIntl extends Component {
|
2016-08-10 23:50:23 +02:00
|
|
|
render() {
|
2016-08-01 00:26:52 +02:00
|
|
|
const {formatMessage} = this.props.intl;
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Get number of sub-items
|
2016-08-05 00:00:25 +02:00
|
|
|
let nSubItems = this.props.item.get(this.props.subItemsType);
|
2016-08-06 15:30:03 +02:00
|
|
|
if (Immutable.List.isList(nSubItems)) {
|
|
|
|
nSubItems = nSubItems.size;
|
2016-07-07 23:23:18 +02:00
|
|
|
}
|
2016-07-28 23:14:52 +02:00
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Define correct sub-items label (plural)
|
|
|
|
let subItemsLabel = formatMessage(
|
|
|
|
gridMessages[this.props.subItemsLabel],
|
|
|
|
{ itemCount: nSubItems }
|
|
|
|
);
|
2016-07-28 23:14:52 +02:00
|
|
|
|
2016-08-12 13:11:14 +02:00
|
|
|
let to = "/" + this.props.itemsType + "/" + this.props.item.get("id");
|
|
|
|
if (this.props.buildLinkTo) {
|
|
|
|
to = this.props.buildLinkTo(this.props.itemsType, this.props.item);
|
|
|
|
}
|
2016-08-05 00:00:25 +02:00
|
|
|
const id = "grid-item-" + this.props.itemsType + "/" + this.props.item.get("id");
|
2016-08-02 13:07:12 +02:00
|
|
|
const title = formatMessage(gridMessages["app.grid.goTo" + this.props.itemsType.capitalize() + "Page"]);
|
2016-08-10 21:36:11 +02:00
|
|
|
|
2016-07-07 23:23:18 +02:00
|
|
|
return (
|
2016-07-29 23:57:21 +02:00
|
|
|
<div className="grid-item col-xs-6 col-sm-3" styleName="placeholders" id={id}>
|
|
|
|
<div className="grid-item-content text-center">
|
2016-08-05 00:00:25 +02:00
|
|
|
<Link title={title} to={to}><img src={this.props.item.get("art")} width="200" height="200" className="img-responsive img-circle art" styleName="art" alt={this.props.item.get("name")}/></Link>
|
|
|
|
<h4 className="name" styleName="name">{this.props.item.get("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>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2016-08-01 00:26:52 +02:00
|
|
|
GridItemCSSIntl.propTypes = {
|
2016-08-06 15:30:03 +02:00
|
|
|
item: PropTypes.instanceOf(Immutable.Map).isRequired,
|
2016-08-02 13:07:12 +02:00
|
|
|
itemsType: PropTypes.string.isRequired,
|
2016-08-01 00:26:52 +02:00
|
|
|
itemsLabel: PropTypes.string.isRequired,
|
|
|
|
subItemsType: PropTypes.string.isRequired,
|
|
|
|
subItemsLabel: PropTypes.string.isRequired,
|
2016-08-12 13:11:14 +02:00
|
|
|
buildLinkTo: PropTypes.func,
|
2016-08-10 23:50:23 +02:00
|
|
|
intl: intlShape.isRequired,
|
2016-07-07 23:23:18 +02:00
|
|
|
};
|
2016-08-01 00:26:52 +02:00
|
|
|
export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
|
2016-07-29 23:57:21 +02:00
|
|
|
|
2016-07-07 23:23:18 +02:00
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
/**
|
|
|
|
* A grid, formatted using Isotope.JS
|
|
|
|
*/
|
2016-07-07 23:23:18 +02:00
|
|
|
export class Grid extends Component {
|
2016-08-10 23:50:23 +02:00
|
|
|
constructor(props) {
|
2016-07-07 23:23:18 +02:00
|
|
|
super(props);
|
|
|
|
|
|
|
|
// Init grid data member
|
|
|
|
this.iso = null;
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Bind this
|
|
|
|
this.createIsotopeContainer = this.createIsotopeContainer.bind(this);
|
2016-07-07 23:23:18 +02:00
|
|
|
this.handleFiltering = this.handleFiltering.bind(this);
|
|
|
|
}
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
/**
|
|
|
|
* Create an isotope container if none already exist.
|
|
|
|
*/
|
2016-08-10 23:50:23 +02:00
|
|
|
createIsotopeContainer() {
|
2016-07-07 23:23:18 +02:00
|
|
|
if (this.iso == null) {
|
|
|
|
this.iso = new Isotope(this.refs.grid, ISOTOPE_OPTIONS);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
/**
|
|
|
|
* Handle filtering on the grid.
|
|
|
|
*/
|
2016-08-10 23:50:23 +02:00
|
|
|
handleFiltering(props) {
|
2016-07-07 23:23:18 +02:00
|
|
|
// If no query provided, drop any filter in use
|
|
|
|
if (props.filterText == "") {
|
|
|
|
return this.iso.arrange(ISOTOPE_OPTIONS);
|
|
|
|
}
|
2016-08-10 21:36:11 +02:00
|
|
|
|
2016-07-07 23:23:18 +02:00
|
|
|
// Use Fuse for the filter
|
2016-08-05 00:00:25 +02:00
|
|
|
let result = new Fuse(
|
2016-08-08 12:35:21 +02:00
|
|
|
props.items.toJS(),
|
2016-07-07 23:23:18 +02:00
|
|
|
{
|
|
|
|
"keys": ["name"],
|
|
|
|
"threshold": 0.4,
|
2016-08-10 23:50:23 +02:00
|
|
|
"include": ["score"],
|
2016-08-10 21:36:11 +02:00
|
|
|
}
|
|
|
|
).search(props.filterText);
|
2016-07-07 23:23:18 +02:00
|
|
|
|
|
|
|
// Apply filter on grid
|
|
|
|
this.iso.arrange({
|
2016-07-29 23:57:21 +02:00
|
|
|
filter: function (item) {
|
2016-08-05 00:00:25 +02:00
|
|
|
let name = $(item).find(".name").text();
|
2016-07-29 23:57:21 +02:00
|
|
|
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) {
|
2016-08-05 00:00:25 +02:00
|
|
|
let name = $(item).find(".name").text();
|
2016-07-07 23:23:18 +02:00
|
|
|
return result.reduce(function (p, c) {
|
|
|
|
if (c.item.name == name) {
|
|
|
|
return c.score + p;
|
|
|
|
}
|
|
|
|
return p;
|
|
|
|
}, 0);
|
2016-08-10 23:50:23 +02:00
|
|
|
},
|
2016-07-07 23:23:18 +02:00
|
|
|
},
|
2016-08-10 23:50:23 +02:00
|
|
|
sortBy: "relevance",
|
2016-07-07 23:23:18 +02:00
|
|
|
});
|
|
|
|
this.iso.updateSortData();
|
|
|
|
this.iso.arrange();
|
|
|
|
}
|
|
|
|
|
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
2016-08-10 21:36:11 +02:00
|
|
|
// Shallow comparison, render is pure
|
2016-07-30 22:54:19 +02:00
|
|
|
return shallowCompare(this, nextProps, nextState);
|
2016-07-07 23:23:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
componentWillReceiveProps(nextProps) {
|
2016-08-10 21:36:11 +02:00
|
|
|
// Handle filtering if filterText is changed
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-10 23:50:23 +02:00
|
|
|
componentDidMount() {
|
2016-07-07 23:23:18 +02:00
|
|
|
// Setup grid
|
|
|
|
this.createIsotopeContainer();
|
|
|
|
// Only arrange if there are elements to arrange
|
2016-08-10 21:36:11 +02:00
|
|
|
if (this.props.items.size > 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-10 21:36:11 +02:00
|
|
|
(n) => "grid-item-" + prevProps.itemsType + "/" + n.get("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-10 21:36:11 +02:00
|
|
|
(n) => "grid-item-" + itemsType + "/" + n.get("id")
|
|
|
|
);
|
2016-07-07 23:23:18 +02:00
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Find which keys are new between the current set of keys and any new
|
|
|
|
// children passed to this component
|
2016-08-01 00:26:52 +02:00
|
|
|
let addKeys = immutableDiff(newKeys, currentKeys);
|
2016-07-07 23:23:18 +02:00
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Find which keys have been removed between the current set of keys
|
|
|
|
// and any new children passed to this component
|
2016-08-01 00:26:52 +02:00
|
|
|
let removeKeys = immutableDiff(currentKeys, newKeys);
|
2016-07-07 23:23:18 +02:00
|
|
|
|
2016-08-05 00:00:25 +02:00
|
|
|
let iso = this.iso;
|
2016-08-10 21:36:11 +02:00
|
|
|
// Remove removed items
|
|
|
|
if (removeKeys.size > 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
|
|
|
}
|
2016-08-10 21:36:11 +02:00
|
|
|
// Add new items
|
|
|
|
if (addKeys.size > 0) {
|
2016-08-01 00:26:52 +02:00
|
|
|
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
|
2016-08-10 23:50:23 +02:00
|
|
|
imagesLoaded(this.refs.grid).on("progress", function () {
|
2016-07-07 23:23:18 +02:00
|
|
|
// Layout after each image load, fix for responsive grid
|
|
|
|
if (!iso) { // Grid could have been destroyed in the meantime
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
iso.layout();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-08-10 23:50:23 +02:00
|
|
|
render() {
|
2016-08-10 21:36:11 +02:00
|
|
|
// Handle loading
|
2016-08-05 00:00:25 +02:00
|
|
|
let loading = null;
|
2016-08-10 21:36:11 +02:00
|
|
|
if (this.props.isFetching) {
|
2016-08-01 00:26:52 +02:00
|
|
|
loading = (
|
|
|
|
<div className="row text-center">
|
|
|
|
<p>
|
2016-08-04 15:28:07 +02:00
|
|
|
<FontAwesome name="spinner" className="fa-pulse fa-3x fa-fw" aria-hidden="true" />
|
|
|
|
<span className="sr-only"><FormattedMessage {...gridMessages["app.common.loading"]} /></span>
|
2016-08-01 00:26:52 +02:00
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2016-08-10 21:36:11 +02:00
|
|
|
|
|
|
|
// Build grid items
|
|
|
|
let gridItems = [];
|
2016-08-12 13:11:14 +02:00
|
|
|
const { itemsType, itemsLabel, subItemsType, subItemsLabel, buildLinkTo } = this.props;
|
2016-08-10 21:36:11 +02:00
|
|
|
this.props.items.forEach(function (item) {
|
2016-08-12 13:11:14 +02:00
|
|
|
gridItems.push(<GridItem item={item} itemsType={itemsType} itemsLabel={itemsLabel} subItemsType={subItemsType} subItemsLabel={subItemsLabel} buildLinkTo={buildLinkTo} key={item.get("id")} />);
|
2016-08-10 21:36:11 +02:00
|
|
|
});
|
|
|
|
|
2016-07-07 23:23:18 +02:00
|
|
|
return (
|
2016-08-01 00:26:52 +02:00
|
|
|
<div>
|
|
|
|
<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>
|
2016-08-10 21:36:11 +02:00
|
|
|
{ loading }
|
2016-07-07 23:23:18 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Grid.propTypes = {
|
2016-08-01 00:26:52 +02:00
|
|
|
isFetching: PropTypes.bool.isRequired,
|
|
|
|
items: PropTypes.instanceOf(Immutable.List).isRequired,
|
2016-08-02 13:07:12 +02:00
|
|
|
itemsType: PropTypes.string.isRequired,
|
2016-08-01 00:26:52 +02:00
|
|
|
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-08-12 13:11:14 +02:00
|
|
|
buildLinkTo: PropTypes.func,
|
2016-08-10 23:50:23 +02:00
|
|
|
filterText: PropTypes.string,
|
2016-07-07 23:23:18 +02:00
|
|
|
};
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Full grid with pagination and filtering input.
|
|
|
|
*/
|
2016-07-07 23:23:18 +02:00
|
|
|
export default class FilterablePaginatedGrid extends Component {
|
2016-08-10 23:50:23 +02:00
|
|
|
constructor(props) {
|
2016-07-07 23:23:18 +02:00
|
|
|
super(props);
|
2016-08-10 21:36:11 +02:00
|
|
|
|
2016-07-07 23:23:18 +02:00
|
|
|
this.state = {
|
2016-08-10 23:50:23 +02:00
|
|
|
filterText: "", // No filterText at init
|
2016-07-07 23:23:18 +02:00
|
|
|
};
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
// Bind this
|
2016-07-07 23:23:18 +02:00
|
|
|
this.handleUserInput = this.handleUserInput.bind(this);
|
|
|
|
}
|
|
|
|
|
2016-08-10 21:36:11 +02:00
|
|
|
/**
|
|
|
|
* Method called whenever the filter input is changed.
|
|
|
|
*
|
|
|
|
* Update the state accordingly.
|
|
|
|
*
|
|
|
|
* @param filterText Content of the filter input.
|
|
|
|
*/
|
2016-08-10 23:50:23 +02:00
|
|
|
handleUserInput(filterText) {
|
2016-07-07 23:23:18 +02:00
|
|
|
this.setState({
|
2016-08-10 23:50:23 +02:00
|
|
|
filterText: filterText,
|
2016-07-07 23:23:18 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-08-10 23:50:23 +02:00
|
|
|
render() {
|
2016-07-07 23:23:18 +02:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
|
2016-08-01 00:26:52 +02:00
|
|
|
<Grid filterText={this.state.filterText} {...this.props.grid} />
|
|
|
|
<Pagination {...this.props.pagination} />
|
2016-07-07 23:23:18 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
FilterablePaginatedGrid.propTypes = {
|
2016-08-01 00:26:52 +02:00
|
|
|
grid: PropTypes.object.isRequired,
|
2016-08-10 23:50:23 +02:00
|
|
|
pagination: PropTypes.object.isRequired,
|
2016-07-07 23:23:18 +02:00
|
|
|
};
|