import React from 'react'; import { connect } from 'react-redux'; import { orderBy, get, isEmpty, map } from 'lodash'; import c from 'classnames'; import fuzzy from 'fuzzy'; import Waypoint from 'react-waypoint'; import Dialog from '../UI/Dialog'; import { resolvePath, fileExtension } from '../../lib/pathHelper'; import { changeDraftField } from '../../actions/entries'; import { loadMedia as loadMediaAction, persistMedia as persistMediaAction, deleteMedia as deleteMediaAction, insertMedia as insertMediaAction, closeMediaLibrary as closeMediaLibraryAction, } from '../../actions/mediaLibrary'; import MediaLibraryFooter from './MediaLibraryFooter'; /** * Extensions used to determine which files to show when the media library is * accessed from an image insertion field. */ const IMAGE_EXTENSIONS_VIEWABLE = [ 'jpg', 'jpeg', 'webp', 'gif', 'png', 'bmp', 'tiff' ]; const IMAGE_EXTENSIONS = [ ...IMAGE_EXTENSIONS_VIEWABLE, 'svg' ]; class MediaLibrary extends React.Component { /** * The currently selected file and query are tracked in component state as * they do not impact the rest of the application. */ state = { selectedFile: {}, query: '', }; componentDidMount() { this.props.loadMedia(); } componentWillReceiveProps(nextProps) { /** * We clear old state from the media library when it's being re-opened * because, when doing so on close, the state is cleared while the media * library is still fading away. */ const isOpening = !this.props.isVisible && nextProps.isVisible; if (isOpening) { this.setState({ selectedFile: {}, query: '' }); } } /** * Filter an array of file data to include only images. */ filterImages = (files = []) => { return files.filter(file => { const ext = fileExtension(file.name).toLowerCase(); return IMAGE_EXTENSIONS.includes(ext); }); }; /** * Transform file data for table display. */ toTableData = files => { const tableData = files && files.map(({ key, name, size, queryOrder, url, urlIsPublicPath }) => { const ext = fileExtension(name).toLowerCase(); return { key, name, type: ext.toUpperCase(), size, queryOrder, url, urlIsPublicPath, isImage: IMAGE_EXTENSIONS.includes(ext), isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), }; }); /** * Get the sort order for use with `lodash.orderBy`, and always add the * `queryOrder` sort as the lowest priority sort order. */ const { sortFields } = this.state; const fieldNames = map(sortFields, 'fieldName').concat('queryOrder'); const directions = map(sortFields, 'direction').concat('asc'); return orderBy(tableData, fieldNames, directions); }; handleClose = () => { this.props.closeMediaLibrary(); }; /** * Toggle asset selection on click. */ handleAssetClick = asset => { const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; this.setState({ selectedFile }); }; /** * Upload a file. */ handlePersist = async event => { /** * Stop the browser from automatically handling the file input click, and * get the file for upload. */ event.stopPropagation(); event.preventDefault(); const { loadMedia, persistMedia, privateUpload } = this.props; const { files: fileList } = event.dataTransfer || event.target; const files = [...fileList]; const file = files[0]; /** * Upload the selected file, then refresh the media library. This should be * improved in the future, but isn't currently resulting in noticeable * performance/load time issues. */ await persistMedia(file, privateUpload); this.scrollToTop(); }; /** * Stores the public path of the file in the application store, where the * editor field that launched the media library can retrieve it. */ handleInsert = () => { const { selectedFile } = this.state; const { name, url, urlIsPublicPath } = selectedFile; const { insertMedia, publicFolder } = this.props; const publicPath = urlIsPublicPath ? url : resolvePath(name, publicFolder); insertMedia(publicPath); this.handleClose(); }; /** * Removes the selected file from the backend. */ handleDelete = () => { const { selectedFile } = this.state; const { files, deleteMedia } = this.props; if (!window.confirm('Are you sure you want to delete selected media?')) { return; } const file = files.find(file => selectedFile.key === file.key); deleteMedia(file) .then(() => { this.setState({ selectedFile: {} }); }); }; handleLoadMore = () => { const { loadMedia, dynamicSearchQuery, page } = this.props; loadMedia({ query: dynamicSearchQuery, page: page + 1 }); }; /** * Executes media library search for implementations that support dynamic * search via request. For these implementations, the Enter key must be * pressed to execute search. If assets are being stored directly through * the GitHub backend, search is in-memory and occurs as the query is typed, * so this handler has no impact. */ handleSearchKeyDown = async (event) => { if (event.key === 'Enter' && this.props.dynamicSearch) { await this.props.loadMedia({ query: this.state.query }) this.scrollToTop(); } }; scrollToTop = () => { this.scrollContainerRef.scrollTop = 0; } /** * Updates query state as the user types in the search field. */ handleSearchChange = event => { this.setState({ query: event.target.value }); }; /** * Filters files that do not match the query. Not used for dynamic search. */ queryFilter = (query, files) => { /** * Because file names don't have spaces, typing a space eliminates all * potential matches, so we strip them all out internally before running the * query. */ const strippedQuery = query.replace(/ /g, ''); const matches = fuzzy.filter(strippedQuery, files, { extract: file => file.name }); const matchFiles = matches.map((match, queryIndex) => { const file = files[match.index]; return { ...file, queryIndex }; }); return matchFiles; }; render() { const { isVisible, canInsert, files, dynamicSearch, dynamicSearchActive, forImage, isLoading, isPersisting, isDeleting, hasNextPage, page, isPaginating, } = this.props; const { query, selectedFile } = this.state; const filteredFiles = forImage ? this.filterImages(files) : files; const queriedFiles = (!dynamicSearch && query) ? this.queryFilter(query, filteredFiles) : filteredFiles; const tableData = this.toTableData(queriedFiles); const hasFiles = files && !!files.length; const hasFilteredFiles = filteredFiles && !!filteredFiles.length; const hasSearchResults = queriedFiles && !!queriedFiles.length; const hasMedia = hasSearchResults; const shouldShowEmptyMessage = !hasMedia; const emptyMessage = (isLoading && !hasMedia && 'Loading...') || (dynamicSearchActive && 'No results.') || (!hasFiles && 'No assets found.') || (!hasFilteredFiles && 'No images found.') || (!hasSearchResults && 'No results.'); return ( } >
{file.name}