UNPKG

5.78 kBJavaScriptView Raw
1const preact = require('preact')
2const findDOMElement = require('@uppy/utils/lib/findDOMElement')
3
4/**
5 * Defer a frequent call to the microtask queue.
6 */
7function debounce (fn) {
8 let calling = null
9 let latestArgs = null
10 return (...args) => {
11 latestArgs = args
12 if (!calling) {
13 calling = Promise.resolve().then(() => {
14 calling = null
15 // At this point `args` may be different from the most
16 // recent state, if multiple calls happened since this task
17 // was queued. So we use the `latestArgs`, which definitely
18 // is the most recent call.
19 return fn(...latestArgs)
20 })
21 }
22 return calling
23 }
24}
25
26/**
27 * Boilerplate that all Plugins share - and should not be used
28 * directly. It also shows which methods final plugins should implement/override,
29 * this deciding on structure.
30 *
31 * @param {object} main Uppy core object
32 * @param {object} object with plugin options
33 * @returns {Array|string} files or success/fail message
34 */
35module.exports = class Plugin {
36 constructor (uppy, opts) {
37 this.uppy = uppy
38 this.opts = opts || {}
39
40 this.update = this.update.bind(this)
41 this.mount = this.mount.bind(this)
42 this.install = this.install.bind(this)
43 this.uninstall = this.uninstall.bind(this)
44 }
45
46 getPluginState () {
47 const { plugins } = this.uppy.getState()
48 return plugins[this.id] || {}
49 }
50
51 setPluginState (update) {
52 const { plugins } = this.uppy.getState()
53
54 this.uppy.setState({
55 plugins: {
56 ...plugins,
57 [this.id]: {
58 ...plugins[this.id],
59 ...update
60 }
61 }
62 })
63 }
64
65 setOptions (newOpts) {
66 this.opts = { ...this.opts, ...newOpts }
67 this.setPluginState() // so that UI re-renders with new options
68 }
69
70 update (state) {
71 if (typeof this.el === 'undefined') {
72 return
73 }
74
75 if (this._updateUI) {
76 this._updateUI(state)
77 }
78 }
79
80 // Called after every state update, after everything's mounted. Debounced.
81 afterUpdate () {
82
83 }
84
85 /**
86 * Called when plugin is mounted, whether in DOM or into another plugin.
87 * Needed because sometimes plugins are mounted separately/after `install`,
88 * so this.el and this.parent might not be available in `install`.
89 * This is the case with @uppy/react plugins, for example.
90 */
91 onMount () {
92
93 }
94
95 /**
96 * Check if supplied `target` is a DOM element or an `object`.
97 * If it’s an object — target is a plugin, and we search `plugins`
98 * for a plugin with same name and return its target.
99 *
100 * @param {string|object} target
101 *
102 */
103 mount (target, plugin) {
104 const callerPluginName = plugin.id
105
106 const targetElement = findDOMElement(target)
107
108 if (targetElement) {
109 this.isTargetDOMEl = true
110
111 // API for plugins that require a synchronous rerender.
112 this.rerender = (state) => {
113 // plugin could be removed, but this.rerender is debounced below,
114 // so it could still be called even after uppy.removePlugin or uppy.close
115 // hence the check
116 if (!this.uppy.getPlugin(this.id)) return
117 this.el = preact.render(this.render(state), targetElement, this.el)
118 this.afterUpdate()
119 }
120 this._updateUI = debounce(this.rerender)
121
122 this.uppy.log(`Installing ${callerPluginName} to a DOM element '${target}'`)
123
124 // clear everything inside the target container
125 if (this.opts.replaceTargetContent) {
126 targetElement.innerHTML = ''
127 }
128
129 this.el = preact.render(this.render(this.uppy.getState()), targetElement)
130
131 this.onMount()
132 return this.el
133 }
134
135 let targetPlugin
136 if (typeof target === 'object' && target instanceof Plugin) {
137 // Targeting a plugin *instance*
138 targetPlugin = target
139 } else if (typeof target === 'function') {
140 // Targeting a plugin type
141 const Target = target
142 // Find the target plugin instance.
143 this.uppy.iteratePlugins((plugin) => {
144 if (plugin instanceof Target) {
145 targetPlugin = plugin
146 return false
147 }
148 })
149 }
150
151 if (targetPlugin) {
152 this.uppy.log(`Installing ${callerPluginName} to ${targetPlugin.id}`)
153 this.parent = targetPlugin
154 this.el = targetPlugin.addTarget(plugin)
155
156 this.onMount()
157 return this.el
158 }
159
160 this.uppy.log(`Not installing ${callerPluginName}`)
161
162 let message = `Invalid target option given to ${callerPluginName}.`
163 if (typeof target === 'function') {
164 message += ' The given target is not a Plugin class. ' +
165 'Please check that you\'re not specifying a React Component instead of a plugin. ' +
166 'If you are using @uppy/* packages directly, make sure you have only 1 version of @uppy/core installed: ' +
167 'run `npm ls @uppy/core` on the command line and verify that all the versions match and are deduped correctly.'
168 } else {
169 message += 'If you meant to target an HTML element, please make sure that the element exists. ' +
170 'Check that the <script> tag initializing Uppy is right before the closing </body> tag at the end of the page. ' +
171 '(see https://github.com/transloadit/uppy/issues/1042)\n\n' +
172 'If you meant to target a plugin, please confirm that your `import` statements or `require` calls are correct.'
173 }
174 throw new Error(message)
175 }
176
177 render (state) {
178 throw (new Error('Extend the render method to add your plugin to a DOM element'))
179 }
180
181 addTarget (plugin) {
182 throw (new Error('Extend the addTarget method to add your plugin to another plugin\'s target'))
183 }
184
185 unmount () {
186 if (this.isTargetDOMEl && this.el && this.el.parentNode) {
187 this.el.parentNode.removeChild(this.el)
188 }
189 }
190
191 install () {
192
193 }
194
195 uninstall () {
196 this.unmount()
197 }
198}