index.js 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import React from "react"
  2. import ajax from "misago/services/ajax"
  3. import snackbar from "misago/services/snackbar"
  4. import misago from "misago"
  5. import cleanResults from "./clean-results"
  6. import Dropdown from "./dropdown"
  7. export default class extends React.Component {
  8. constructor() {
  9. super()
  10. this.state = {
  11. isLoading: false,
  12. isOpen: false,
  13. query: "",
  14. results: [],
  15. }
  16. this.intervalId = null
  17. }
  18. componentDidMount() {
  19. document.addEventListener("mousedown", this.onDocumentMouseDown)
  20. document.addEventListener("keydown", this.onEscape)
  21. }
  22. componentWillUnmount() {
  23. document.removeEventListener("mousedown", this.onDocumentMouseDown)
  24. document.removeEventListener("keydown", this.onEscape)
  25. }
  26. onToggle = (ev) => {
  27. this.setState((prevState, props) => {
  28. if (!prevState.isOpen) {
  29. window.setTimeout(() => {
  30. this.container.querySelector("input").focus()
  31. }, 100)
  32. }
  33. return { isOpen: !prevState.isOpen }
  34. })
  35. }
  36. onDocumentMouseDown = (ev) => {
  37. let closeResults = true
  38. let node = ev.target
  39. while (node !== null && node !== document) {
  40. if (node === this.container) {
  41. closeResults = false
  42. return
  43. }
  44. node = node.parentNode
  45. }
  46. if (closeResults) {
  47. this.setState({ isOpen: false })
  48. }
  49. }
  50. onEscape = (ev) => {
  51. if (ev.key === "Escape") {
  52. this.setState({ isOpen: false })
  53. }
  54. }
  55. onChange = (ev) => {
  56. const query = ev.target.value
  57. this.setState({ query })
  58. this.loadResults(query.trim())
  59. }
  60. loadResults(query) {
  61. if (!query.length) return
  62. const delay = 300 + Math.random() * 300
  63. if (this.intervalId) {
  64. window.clearTimeout(this.intervalId)
  65. }
  66. this.setState({ isLoading: true })
  67. this.intervalId = window.setTimeout(() => {
  68. ajax.get(misago.get("SEARCH_API"), { q: query }).then(
  69. (data) => {
  70. this.setState({
  71. intervalId: null,
  72. isLoading: false,
  73. results: cleanResults(data),
  74. })
  75. },
  76. (rejection) => {
  77. snackbar.apiError(rejection)
  78. this.setState({
  79. intervalId: null,
  80. isLoading: false,
  81. results: [],
  82. })
  83. }
  84. )
  85. }, delay)
  86. }
  87. render() {
  88. let className = "navbar-search dropdown"
  89. if (this.state.isOpen) className += " open"
  90. return (
  91. <div
  92. className={className}
  93. ref={(container) => (this.container = container)}
  94. >
  95. <a
  96. aria-haspopup="true"
  97. aria-expanded="false"
  98. className="navbar-icon"
  99. data-toggle="dropdown"
  100. href={misago.get("SEARCH_URL")}
  101. onClick={this.onToggle}
  102. >
  103. <i className="material-icon">search</i>
  104. </a>
  105. <Dropdown
  106. isLoading={this.state.isLoading}
  107. onChange={this.onChange}
  108. results={this.state.results}
  109. query={this.state.query}
  110. />
  111. </div>
  112. )
  113. }
  114. }