index.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. const { h, Component } = require('preact')
  2. const classNames = require('classnames')
  3. const { nanoid } = require('nanoid/non-secure')
  4. const getFileTypeIcon = require('../../utils/getFileTypeIcon')
  5. const ignoreEvent = require('../../utils/ignoreEvent.js')
  6. const FilePreview = require('../FilePreview')
  7. class FileCard extends Component {
  8. form = document.createElement('form')
  9. constructor (props) {
  10. super(props)
  11. const file = this.props.files[this.props.fileCardFor]
  12. const metaFields = this.getMetaFields() || []
  13. const storedMetaData = {}
  14. metaFields.forEach((field) => {
  15. storedMetaData[field.id] = file.meta[field.id] || ''
  16. })
  17. this.state = {
  18. formState: storedMetaData,
  19. }
  20. this.form.id = nanoid()
  21. }
  22. // TODO(aduh95): move this to `UNSAFE_componentWillMount` when updating to Preact X+.
  23. componentWillMount () { // eslint-disable-line react/no-deprecated
  24. this.form.addEventListener('submit', this.handleSave)
  25. document.body.appendChild(this.form)
  26. }
  27. componentWillUnmount () {
  28. this.form.removeEventListener('submit', this.handleSave)
  29. document.body.removeChild(this.form)
  30. }
  31. getMetaFields () {
  32. return typeof this.props.metaFields === 'function'
  33. ? this.props.metaFields(this.props.files[this.props.fileCardFor])
  34. : this.props.metaFields
  35. }
  36. updateMeta = (newVal, name) => {
  37. this.setState(({ formState }) => ({
  38. formState: {
  39. ...formState,
  40. [name]: newVal,
  41. },
  42. }))
  43. }
  44. handleSave = (e) => {
  45. e.preventDefault()
  46. const fileID = this.props.fileCardFor
  47. this.props.saveFileCard(this.state.formState, fileID)
  48. }
  49. handleCancel = () => {
  50. this.props.toggleFileCard(false)
  51. }
  52. saveOnEnter = (ev) => {
  53. if (ev.keyCode === 13) {
  54. ev.stopPropagation()
  55. ev.preventDefault()
  56. const file = this.props.files[this.props.fileCardFor]
  57. this.props.saveFileCard(this.state.formState, file.id)
  58. }
  59. }
  60. renderMetaFields = () => {
  61. const metaFields = this.getMetaFields() || []
  62. const fieldCSSClasses = {
  63. text: 'uppy-u-reset uppy-c-textInput uppy-Dashboard-FileCard-input',
  64. }
  65. return metaFields.map((field) => {
  66. const id = `uppy-Dashboard-FileCard-input-${field.id}`
  67. const required = this.props.requiredMetaFields.includes(field.id)
  68. return (
  69. <fieldset key={field.id} className="uppy-Dashboard-FileCard-fieldset">
  70. <label className="uppy-Dashboard-FileCard-label" htmlFor={id}>{field.name}</label>
  71. {field.render !== undefined
  72. ? field.render({
  73. value: this.state.formState[field.id],
  74. onChange: (newVal) => this.updateMeta(newVal, field.id),
  75. fieldCSSClasses,
  76. required,
  77. form: this.form.id,
  78. }, h)
  79. : (
  80. <input
  81. className={fieldCSSClasses.text}
  82. id={id}
  83. form={this.form.id}
  84. type={field.type || 'text'}
  85. required={required}
  86. value={this.state.formState[field.id]}
  87. placeholder={field.placeholder}
  88. // If `form` attribute is not supported, we need to capture pressing Enter to avoid bubbling in case Uppy is
  89. // embedded inside a <form>.
  90. onKeyUp={'form' in HTMLInputElement.prototype ? undefined : this.saveOnEnter}
  91. onKeyDown={'form' in HTMLInputElement.prototype ? undefined : this.saveOnEnter}
  92. onKeyPress={'form' in HTMLInputElement.prototype ? undefined : this.saveOnEnter}
  93. onInput={ev => this.updateMeta(ev.target.value, field.id)}
  94. data-uppy-super-focusable
  95. />
  96. )}
  97. </fieldset>
  98. )
  99. })
  100. }
  101. render () {
  102. const file = this.props.files[this.props.fileCardFor]
  103. const showEditButton = this.props.canEditFile(file)
  104. return (
  105. <div
  106. className={classNames('uppy-Dashboard-FileCard', this.props.className)}
  107. data-uppy-panelType="FileCard"
  108. onDragOver={ignoreEvent}
  109. onDragLeave={ignoreEvent}
  110. onDrop={ignoreEvent}
  111. onPaste={ignoreEvent}
  112. >
  113. <div className="uppy-DashboardContent-bar">
  114. <div className="uppy-DashboardContent-title" role="heading" aria-level="1">
  115. {this.props.i18nArray('editing', {
  116. file: <span className="uppy-DashboardContent-titleFile">{file.meta ? file.meta.name : file.name}</span>,
  117. })}
  118. </div>
  119. <button
  120. className="uppy-DashboardContent-back"
  121. type="button"
  122. form={this.form.id}
  123. title={this.props.i18n('finishEditingFile')}
  124. onClick={this.handleCancel}
  125. >
  126. {this.props.i18n('cancel')}
  127. </button>
  128. </div>
  129. <div className="uppy-Dashboard-FileCard-inner">
  130. <div className="uppy-Dashboard-FileCard-preview" style={{ backgroundColor: getFileTypeIcon(file.type).color }}>
  131. <FilePreview file={file} />
  132. {showEditButton
  133. && (
  134. <button
  135. type="button"
  136. className="uppy-u-reset uppy-c-btn uppy-Dashboard-FileCard-edit"
  137. onClick={(event) => {
  138. // When opening the image editor we want to save any meta fields changes.
  139. // Otherwise it's confusing for the user to click save in the editor,
  140. // but the changes here are discarded. This bypasses validation,
  141. // but we are okay with that.
  142. this.handleSave(event)
  143. this.props.openFileEditor(file)
  144. }}
  145. form={this.form.id}
  146. >
  147. {this.props.i18n('editFile')}
  148. </button>
  149. )}
  150. </div>
  151. <div className="uppy-Dashboard-FileCard-info">
  152. {this.renderMetaFields()}
  153. </div>
  154. <div className="uppy-Dashboard-FileCard-actions">
  155. <button
  156. className="uppy-u-reset uppy-c-btn uppy-c-btn-primary uppy-Dashboard-FileCard-actionsBtn"
  157. // If `form` attribute is supported, we want a submit button to trigger the form validation.
  158. // Otherwise, fallback to a classic button with a onClick event handler.
  159. type={'form' in HTMLButtonElement.prototype ? 'submit' : 'button'}
  160. onClick={'form' in HTMLButtonElement.prototype ? undefined : this.handleSave}
  161. form={this.form.id}
  162. >
  163. {this.props.i18n('saveChanges')}
  164. </button>
  165. <button
  166. className="uppy-u-reset uppy-c-btn uppy-c-btn-link uppy-Dashboard-FileCard-actionsBtn"
  167. type="button"
  168. onClick={this.handleCancel}
  169. form={this.form.id}
  170. >
  171. {this.props.i18n('cancel')}
  172. </button>
  173. </div>
  174. </div>
  175. </div>
  176. )
  177. }
  178. }
  179. module.exports = FileCard