import classNames from 'classnames'; import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; import {defineMessages, injectIntl, intlShape} from 'react-intl'; import LibraryItem from '../../containers/library-item.jsx'; import Modal from '../../containers/windowed-modal.jsx'; import Divider from '../divider/divider.jsx'; import Filter from '../filter/filter.jsx'; import TagButton from '../../containers/tag-button.jsx'; import Spinner from '../spinner/spinner.jsx'; import Separator from '../tw-extension-separator/separator.jsx'; import RemovedTrademarks from '../tw-removed-trademarks/removed-trademarks.jsx'; import {APP_NAME} from '../../lib/brand.js'; import styles from './library.css'; const messages = defineMessages({ filterPlaceholder: { id: 'gui.library.filterPlaceholder', defaultMessage: 'Search', description: 'Placeholder text for library search field' }, allTag: { id: 'gui.library.allTag', defaultMessage: 'All', description: 'Label for library tag to revert to all items after filtering by tag.' } }); const ALL_TAG = {tag: 'all', intlLabel: messages.allTag}; const tagListPrefix = [ALL_TAG]; class LibraryComponent extends React.Component { constructor (props) { super(props); bindAll(this, [ 'handleClose', 'handleFilterChange', 'handleFilterClear', 'handleMouseEnter', 'handleMouseLeave', 'handlePlayingEnd', 'handleSelect', 'handleFavorite', 'handleTagClick', 'setFilteredDataRef' ]); const favorites = this.readFavoritesFromStorage(); this.state = { playingItem: null, filterQuery: '', selectedTag: ALL_TAG.tag, canDisplay: false, favorites, initialFavorites: favorites }; } componentDidMount () { // Rendering all the items in the library can take a bit, so we'll always // show one frame with a loading spinner. setTimeout(() => { this.setState({ canDisplay: true }); }); if (this.props.setStopHandler) this.props.setStopHandler(this.handlePlayingEnd); } componentDidUpdate (prevProps, prevState) { if (prevState.filterQuery !== this.state.filterQuery || prevState.selectedTag !== this.state.selectedTag) { this.scrollToTop(); } if (this.state.favorites !== prevState.favorites) { try { localStorage.setItem(this.getFavoriteStorageKey(), JSON.stringify(this.state.favorites)); } catch (error) { // ignore } } } handleSelect (id) { this.handleClose(); this.props.onItemSelected(this.getFilteredData()[id]); } readFavoritesFromStorage () { let data; try { data = JSON.parse(localStorage.getItem(this.getFavoriteStorageKey())); } catch (error) { // ignore } if (!Array.isArray(data)) { data = []; } return data; } getFavoriteStorageKey () { return `tw:library-favorites:${this.props.id}`; } handleFavorite (id) { const data = this.getFilteredData()[id]; const key = data[this.props.persistableKey]; this.setState(oldState => ({ favorites: oldState.favorites.includes(key) ? ( oldState.favorites.filter(i => i !== key) ) : ( [...oldState.favorites, key] ) })); } handleClose () { this.props.onRequestClose(); } handleTagClick (tag) { if (this.state.playingItem === null) { this.setState({ filterQuery: '', selectedTag: tag.toLowerCase() }); } else { this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]); this.setState({ filterQuery: '', playingItem: null, selectedTag: tag.toLowerCase() }); } } handleMouseEnter (id) { // don't restart if mouse over already playing item if (this.props.onItemMouseEnter && this.state.playingItem !== id) { this.props.onItemMouseEnter(this.getFilteredData()[id]); this.setState({ playingItem: id }); } } handleMouseLeave (id) { if (this.props.onItemMouseLeave) { this.props.onItemMouseLeave(this.getFilteredData()[id]); this.setState({ playingItem: null }); } } handlePlayingEnd () { if (this.state.playingItem !== null) { this.setState({ playingItem: null }); } } handleFilterChange (event) { if (this.state.playingItem === null) { this.setState({ filterQuery: event.target.value, selectedTag: ALL_TAG.tag }); } else { this.props.onItemMouseLeave(this.getFilteredData()[[this.state.playingItem]]); this.setState({ filterQuery: event.target.value, playingItem: null, selectedTag: ALL_TAG.tag }); } } handleFilterClear () { this.setState({filterQuery: ''}); } getFilteredData () { // When no filtering, favorites get their own section if (this.state.selectedTag === 'all' && !this.state.filterQuery) { const favoriteItems = this.props.data .filter(dataItem => ( this.state.initialFavorites.includes(dataItem[this.props.persistableKey]) )) .map(dataItem => ({ ...dataItem, key: `favorite-${dataItem[this.props.persistableKey]}` })); if (favoriteItems.length) { favoriteItems.push('---'); } return [ ...favoriteItems, ...this.props.data ]; } // When filtering, favorites are just listed first, not in a separate section. const favoriteItems = []; const nonFavoriteItems = []; for (const dataItem of this.props.data) { if (dataItem === '---') { // ignore } else if (this.state.initialFavorites.includes(dataItem[this.props.persistableKey])) { favoriteItems.push(dataItem); } else { nonFavoriteItems.push(dataItem); } } let filteredItems = favoriteItems.concat(nonFavoriteItems); if (this.state.selectedTag !== 'all') { filteredItems = filteredItems.filter(dataItem => ( dataItem.tags && dataItem.tags.map(i => i.toLowerCase()).includes(this.state.selectedTag) )); } if (this.state.filterQuery) { filteredItems = filteredItems.filter(dataItem => { const search = [...dataItem.tags]; if (dataItem.name) { // Use the name if it is a string, else use formatMessage to get the translated name if (typeof dataItem.name === 'string') { search.push(dataItem.name); } else { search.push(this.props.intl.formatMessage(dataItem.name.props, { APP_NAME })); } } if (dataItem.description) { search.push(dataItem.description); } return search .join('\n') .toLowerCase() .includes(this.state.filterQuery.toLowerCase()); }); } return filteredItems; } scrollToTop () { this.filteredDataRef.scrollTop = 0; } setFilteredDataRef (ref) { this.filteredDataRef = ref; } render () { const filteredData = this.state.canDisplay && this.props.data && this.getFilteredData(); return (
{(this.props.filterable || this.props.tags) && (
{this.props.filterable && ( )} {this.props.filterable && this.props.tags && ( )} {this.props.tags &&
{tagListPrefix.concat(this.props.tags).map((tagProps, id) => ( ))}
}
)}
{filteredData && this.getFilteredData().map((dataItem, index) => ( dataItem === '---' ? ( ) : (
); } } LibraryComponent.propTypes = { data: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.oneOfType([ /* eslint-disable react/no-unused-prop-types, lines-around-comment */ // An item in the library PropTypes.shape({ // @todo remove md5/rawURL prop from library, refactor to use storage md5: PropTypes.string, name: PropTypes.oneOfType([ PropTypes.string, PropTypes.node ]), rawURL: PropTypes.string }), PropTypes.string /* eslint-enable react/no-unused-prop-types, lines-around-comment */ ])), PropTypes.instanceOf(Promise) ]), filterable: PropTypes.bool, id: PropTypes.string.isRequired, persistableKey: PropTypes.string, intl: intlShape.isRequired, onItemMouseEnter: PropTypes.func, onItemMouseLeave: PropTypes.func, onItemSelected: PropTypes.func, onRequestClose: PropTypes.func, setStopHandler: PropTypes.func, showPlayButton: PropTypes.bool, tags: PropTypes.arrayOf(PropTypes.shape(TagButton.propTypes)), title: PropTypes.string.isRequired, visible: PropTypes.bool, removedTrademarks: PropTypes.bool }; LibraryComponent.defaultProps = { filterable: true, persistableKey: 'name', showPlayButton: false }; export default injectIntl(LibraryComponent);