UIPlugin.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { render } from 'preact'
  2. import findDOMElement from '@uppy/utils/lib/findDOMElement'
  3. import getTextDirection from '@uppy/utils/lib/getTextDirection'
  4. import BasePlugin from './BasePlugin.js'
  5. /**
  6. * Defer a frequent call to the microtask queue.
  7. *
  8. * @param {() => T} fn
  9. * @returns {Promise<T>}
  10. */
  11. function debounce (fn) {
  12. let calling = null
  13. let latestArgs = null
  14. return (...args) => {
  15. latestArgs = args
  16. if (!calling) {
  17. calling = Promise.resolve().then(() => {
  18. calling = null
  19. // At this point `args` may be different from the most
  20. // recent state, if multiple calls happened since this task
  21. // was queued. So we use the `latestArgs`, which definitely
  22. // is the most recent call.
  23. return fn(...latestArgs)
  24. })
  25. }
  26. return calling
  27. }
  28. }
  29. /**
  30. * UIPlugin is the extended version of BasePlugin to incorporate rendering with Preact.
  31. * Use this for plugins that need a user interface.
  32. *
  33. * For plugins without an user interface, see BasePlugin.
  34. */
  35. class UIPlugin extends BasePlugin {
  36. #updateUI
  37. /**
  38. * Check if supplied `target` is a DOM element or an `object`.
  39. * If it’s an object — target is a plugin, and we search `plugins`
  40. * for a plugin with same name and return its target.
  41. */
  42. mount (target, plugin) {
  43. const callerPluginName = plugin.id
  44. const targetElement = findDOMElement(target)
  45. if (targetElement) {
  46. this.isTargetDOMEl = true
  47. // When target is <body> with a single <div> element,
  48. // Preact thinks it’s the Uppy root element in there when doing a diff,
  49. // and destroys it. So we are creating a fragment (could be empty div)
  50. const uppyRootElement = document.createElement('div')
  51. uppyRootElement.classList.add('uppy-Root')
  52. // API for plugins that require a synchronous rerender.
  53. this.#updateUI = debounce((state) => {
  54. // plugin could be removed, but this.rerender is debounced below,
  55. // so it could still be called even after uppy.removePlugin or uppy.close
  56. // hence the check
  57. if (!this.uppy.getPlugin(this.id)) return
  58. render(this.render(state), uppyRootElement)
  59. this.afterUpdate()
  60. })
  61. this.uppy.log(`Installing ${callerPluginName} to a DOM element '${target}'`)
  62. if (this.opts.replaceTargetContent) {
  63. // Doing render(h(null), targetElement), which should have been
  64. // a better way, since because the component might need to do additional cleanup when it is removed,
  65. // stopped working — Preact just adds null into target, not replacing
  66. targetElement.innerHTML = ''
  67. }
  68. render(this.render(this.uppy.getState()), uppyRootElement)
  69. this.el = uppyRootElement
  70. targetElement.appendChild(uppyRootElement)
  71. // Set the text direction if the page has not defined one.
  72. uppyRootElement.dir = this.opts.direction || getTextDirection(uppyRootElement) || 'ltr'
  73. this.onMount()
  74. return this.el
  75. }
  76. let targetPlugin
  77. if (typeof target === 'object' && target instanceof UIPlugin) {
  78. // Targeting a plugin *instance*
  79. targetPlugin = target
  80. } else if (typeof target === 'function') {
  81. // Targeting a plugin type
  82. const Target = target
  83. // Find the target plugin instance.
  84. this.uppy.iteratePlugins(p => {
  85. if (p instanceof Target) {
  86. targetPlugin = p
  87. }
  88. })
  89. }
  90. if (targetPlugin) {
  91. this.uppy.log(`Installing ${callerPluginName} to ${targetPlugin.id}`)
  92. this.parent = targetPlugin
  93. this.el = targetPlugin.addTarget(plugin)
  94. this.onMount()
  95. return this.el
  96. }
  97. this.uppy.log(`Not installing ${callerPluginName}`)
  98. let message = `Invalid target option given to ${callerPluginName}.`
  99. if (typeof target === 'function') {
  100. message += ' The given target is not a Plugin class. '
  101. + 'Please check that you\'re not specifying a React Component instead of a plugin. '
  102. + 'If you are using @uppy/* packages directly, make sure you have only 1 version of @uppy/core installed: '
  103. + 'run `npm ls @uppy/core` on the command line and verify that all the versions match and are deduped correctly.'
  104. } else {
  105. message += 'If you meant to target an HTML element, please make sure that the element exists. '
  106. + 'Check that the <script> tag initializing Uppy is right before the closing </body> tag at the end of the page. '
  107. + '(see https://github.com/transloadit/uppy/issues/1042)\n\n'
  108. + 'If you meant to target a plugin, please confirm that your `import` statements or `require` calls are correct.'
  109. }
  110. throw new Error(message)
  111. }
  112. update (state) {
  113. if (this.el != null) {
  114. this.#updateUI?.(state)
  115. }
  116. }
  117. unmount () {
  118. if (this.isTargetDOMEl) {
  119. this.el?.remove()
  120. }
  121. this.onUnmount()
  122. }
  123. // eslint-disable-next-line class-methods-use-this
  124. onMount () {}
  125. // eslint-disable-next-line class-methods-use-this
  126. onUnmount () {}
  127. }
  128. export default UIPlugin