MarkupEditor.jsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import React from "react"
  2. import classnames from "classnames"
  3. import misago from "../../"
  4. import ajax from "../../services/ajax"
  5. import snackbar from "../../services/snackbar"
  6. import MisagoMarkup from "../misago-markup"
  7. import MarkupEditorAttachments from "./MarkupEditorAttachments"
  8. import MarkupEditorFooter from "./MarkupEditorFooter"
  9. import MarkupEditorToolbar from "./MarkupEditorToolbar"
  10. import uploadFile from "./uploadFile"
  11. class MarkupEditor extends React.Component {
  12. constructor(props) {
  13. super(props)
  14. this.state = {
  15. element: null,
  16. focused: false,
  17. loading: false,
  18. preview: false,
  19. parsed: null,
  20. }
  21. }
  22. showPreview = () => {
  23. if (this.state.loading) return
  24. this.setState({ loading: true, preview: true, element: null })
  25. ajax.post(misago.get("PARSE_MARKUP_API"), { post: this.props.value }).then(
  26. (data) => {
  27. this.setState({ loading: false, parsed: data.parsed })
  28. },
  29. (rejection) => {
  30. if (rejection.status === 400) {
  31. snackbar.error(rejection.detail)
  32. } else {
  33. snackbar.apiError(rejection)
  34. }
  35. this.setState({ loading: false, preview: false })
  36. }
  37. )
  38. }
  39. closePreview = () => {
  40. this.setState({ loading: false, preview: false })
  41. }
  42. onDrop = (event) => {
  43. event.preventDefault()
  44. event.stopPropagation()
  45. if (!event.dataTransfer.files) return
  46. const { onAttachmentsChange: setState } = this.props
  47. if (misago.get("user").acl.max_attachment_size) {
  48. for (let i = 0; i < event.dataTransfer.files.length; i++) {
  49. const file = event.dataTransfer.files[i]
  50. uploadFile(file, setState)
  51. }
  52. }
  53. }
  54. onPaste = (event) => {
  55. const { onAttachmentsChange: setState } = this.props
  56. const files = []
  57. for (let i = 0; i < event.clipboardData.items.length; i++) {
  58. const item = event.clipboardData.items[i]
  59. if (item.kind === "file") {
  60. files.push(item.getAsFile())
  61. }
  62. }
  63. if (files.length) {
  64. event.preventDefault()
  65. event.stopPropagation()
  66. if (misago.get("user").acl.max_attachment_size) {
  67. for (let i = 0; i < files.length; i++) {
  68. uploadFile(files[i], setState)
  69. }
  70. }
  71. }
  72. }
  73. render = () => (
  74. <div
  75. className={classnames("markup-editor", {
  76. "markup-editor-focused": this.state.focused && !this.state.preview,
  77. })}
  78. >
  79. <MarkupEditorToolbar
  80. disabled={this.props.disabled || this.state.preview}
  81. element={this.state.element}
  82. update={(value) => this.props.onChange({ target: { value } })}
  83. updateAttachments={this.props.onAttachmentsChange}
  84. />
  85. {this.state.preview ? (
  86. <div className="markup-editor-preview">
  87. {this.state.loading ? (
  88. <div className="ui-preview">
  89. <span className="ui-preview-text" style={{ width: "240px" }} />
  90. </div>
  91. ) : (
  92. <MisagoMarkup markup={this.state.parsed} />
  93. )}
  94. </div>
  95. ) : (
  96. <textarea
  97. className="markup-editor-textarea form-control"
  98. placeholder={this.props.placeholder}
  99. value={this.props.value}
  100. disabled={this.props.disabled || this.state.loading}
  101. rows={6}
  102. ref={(element) => {
  103. if (element && !this.state.element) {
  104. this.setState({ element })
  105. setMentions(this.props, element)
  106. }
  107. }}
  108. onChange={this.props.onChange}
  109. onDrop={this.onDrop}
  110. onFocus={() => this.setState({ focused: true })}
  111. onPaste={this.onPaste}
  112. onBlur={() => this.setState({ focused: false })}
  113. />
  114. )}
  115. {this.props.attachments.length > 0 && (
  116. <MarkupEditorAttachments
  117. attachments={this.props.attachments}
  118. disabled={this.props.disabled || this.state.preview}
  119. element={this.state.element}
  120. setState={this.props.onAttachmentsChange}
  121. update={(value) => this.props.onChange({ target: { value } })}
  122. />
  123. )}
  124. <MarkupEditorFooter
  125. preview={this.state.preview}
  126. canProtect={this.props.canProtect}
  127. isProtected={this.props.isProtected}
  128. disabled={this.props.disabled}
  129. empty={
  130. this.props.value.trim().length <
  131. misago.get("SETTINGS").post_length_min || this.state.loading
  132. }
  133. enableProtection={this.props.enableProtection}
  134. disableProtection={this.props.disableProtection}
  135. showPreview={this.showPreview}
  136. closePreview={this.closePreview}
  137. submitText={this.props.submitText}
  138. />
  139. </div>
  140. )
  141. }
  142. function setMentions(props, element) {
  143. $(element).atwho({
  144. at: "@",
  145. displayTpl: '<li><img src="${avatar}" alt="">${username}</li>',
  146. insertTpl: "@${username}",
  147. searchKey: "username",
  148. callbacks: {
  149. remoteFilter: function (query, callback) {
  150. $.getJSON(misago.get("MENTION_API"), { q: query }, callback)
  151. },
  152. },
  153. })
  154. $(element).on("inserted.atwho", (event, flag, query) => {
  155. props.onChange(event)
  156. })
  157. }
  158. export default MarkupEditor