UNPKG

keystone

Version:

Web Application Framework and Admin GUI / Content Management System built on Express.js and Mongoose

263 lines (246 loc) 6.5 kB
/** TODO: - Format size of stored file (if present) using bytes package? - Display file type icon? (see LocalFileField) */ import Field from '../Field'; import React, { PropTypes } from 'react'; import { Button, FormField, FormInput, FormNote, } from '../../../admin/client/App/elemental'; import FileChangeMessage from '../../components/FileChangeMessage'; import HiddenFileInput from '../../components/HiddenFileInput'; import ImageThumbnail from '../../components/ImageThumbnail'; let uploadInc = 1000; const buildInitialState = (props) => ({ action: null, removeExisting: false, uploadFieldPath: `File-${props.path}-${++uploadInc}`, userSelectedFile: null, }); module.exports = Field.create({ propTypes: { autoCleanup: PropTypes.bool, collapse: PropTypes.bool, label: PropTypes.string, note: PropTypes.string, path: PropTypes.string.isRequired, thumb: PropTypes.bool, value: PropTypes.shape({ filename: PropTypes.string, // TODO: these are present but not used in the UI, // should we start using them? // filetype: PropTypes.string, // originalname: PropTypes.string, // path: PropTypes.string, // size: PropTypes.number, }), }, statics: { type: 'File', getDefaultValue: () => ({}), }, getInitialState () { return buildInitialState(this.props); }, shouldCollapse () { return this.props.collapse && !this.hasExisting(); }, componentWillUpdate (nextProps) { // Show the new filename when it's finished uploading if (this.props.value.filename !== nextProps.value.filename) { this.setState(buildInitialState(nextProps)); } }, // ============================== // HELPERS // ============================== hasFile () { return this.hasExisting() || !!this.state.userSelectedFile; }, hasExisting () { return this.props.value && !!this.props.value.filename; }, getFilename () { return this.state.userSelectedFile ? this.state.userSelectedFile.name : this.props.value.filename; }, getFileUrl () { return this.props.value && this.props.value.url; }, isImage () { const href = this.props.value ? this.props.value.url : undefined; return href && href.match(/\.(jpeg|jpg|gif|png|svg)$/i) != null; }, // ============================== // METHODS // ============================== triggerFileBrowser () { this.refs.fileInput.clickDomNode(); }, handleFileChange (event) { const userSelectedFile = event.target.files[0]; this.setState({ userSelectedFile: userSelectedFile, }); }, handleRemove (e) { var state = {}; if (this.state.userSelectedFile) { state = buildInitialState(this.props); } else if (this.hasExisting()) { state.removeExisting = true; if (this.props.autoCleanup) { if (e.altKey) { state.action = 'reset'; } else { state.action = 'delete'; } } else { if (e.altKey) { state.action = 'delete'; } else { state.action = 'reset'; } } } this.setState(state); }, undoRemove () { this.setState(buildInitialState(this.props)); }, // ============================== // RENDERERS // ============================== renderFileNameAndChangeMessage () { const href = this.props.value ? this.props.value.url : undefined; return ( <div> {(this.hasFile() && !this.state.removeExisting) ? ( <FileChangeMessage component={href ? 'a' : 'span'} href={href} target="_blank"> {this.getFilename()} </FileChangeMessage> ) : null} {this.renderChangeMessage()} </div> ); }, renderChangeMessage () { if (this.state.userSelectedFile) { return ( <FileChangeMessage color="success"> Save to Upload </FileChangeMessage> ); } else if (this.state.removeExisting) { return ( <FileChangeMessage color="danger"> File {this.props.autoCleanup ? 'deleted' : 'removed'} - save to confirm </FileChangeMessage> ); } else { return null; } }, renderClearButton () { if (this.state.removeExisting) { return ( <Button variant="link" onClick={this.undoRemove}> Undo Remove </Button> ); } else { var clearText; if (this.state.userSelectedFile) { clearText = 'Cancel Upload'; } else { clearText = (this.props.autoCleanup ? 'Delete File' : 'Remove File'); } return ( <Button variant="link" color="cancel" onClick={this.handleRemove}> {clearText} </Button> ); } }, renderActionInput () { // If the user has selected a file for uploading, we need to point at // the upload field. If the file is being deleted, we submit that. if (this.state.userSelectedFile || this.state.action) { const value = this.state.userSelectedFile ? `upload:${this.state.uploadFieldPath}` : (this.state.action === 'delete' ? 'remove' : ''); return ( <input name={this.getInputName(this.props.path)} type="hidden" value={value} /> ); } else { return null; } }, renderImagePreview () { const imageSource = this.getFileUrl(); return ( <ImageThumbnail component="a" href={imageSource} target="__blank" style={{ float: 'left', marginRight: '1em', maxWidth: '50%' }} > <img src={imageSource} style={{ 'max-height': 100, 'max-width': '100%' }} /> </ImageThumbnail> ); }, renderUI () { const { label, note, path, thumb } = this.props; const isImage = this.isImage(); const hasFile = this.hasFile(); const previews = ( <div style={(isImage && thumb) ? { marginBottom: '1em' } : null}> {isImage && thumb && this.renderImagePreview()} {hasFile && this.renderFileNameAndChangeMessage()} </div> ); const buttons = ( <div style={hasFile ? { marginTop: '1em' } : null}> <Button onClick={this.triggerFileBrowser}> {hasFile ? 'Change' : 'Upload'} File </Button> {hasFile && this.renderClearButton()} </div> ); return ( <div data-field-name={path} data-field-type="file"> <FormField label={label} htmlFor={path}> {this.shouldRenderField() ? ( <div> {previews} {buttons} <HiddenFileInput key={this.state.uploadFieldPath} name={this.state.uploadFieldPath} onChange={this.handleFileChange} ref="fileInput" /> {this.renderActionInput()} </div> ) : ( <div> {hasFile ? this.renderFileNameAndChangeMessage() : <FormInput noedit>no file</FormInput>} </div> )} {!!note && <FormNote html={note} />} </FormField> </div> ); }, });