diff --git a/packages/@uppy/form/.npmignore b/packages/@uppy/form/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/form/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/form/src/index.js b/packages/@uppy/form/src/index.js deleted file mode 100644 index c3b73f43c2..0000000000 --- a/packages/@uppy/form/src/index.js +++ /dev/null @@ -1,144 +0,0 @@ -import BasePlugin from '@uppy/core/lib/BasePlugin.js' -import findDOMElement from '@uppy/utils/lib/findDOMElement' -import toArray from '@uppy/utils/lib/toArray' - -import getFormData from 'get-form-data' - -import packageJson from '../package.json' - -/** - * Form - */ -export default class Form extends BasePlugin { - static VERSION = packageJson.version - - /** @type {HTMLFormElement} */ - form // TODO: make this private (or at least, mark it as readonly) - - constructor (uppy, opts) { - super(uppy, opts) - this.type = 'acquirer' - this.id = this.opts.id || 'Form' - this.title = 'Form' - - // set default options - const defaultOptions = { - target: null, - resultName: 'uppyResult', - getMetaFromForm: true, - addResultToForm: true, - submitOnSuccess: false, - triggerUploadOnSubmit: false, - } - - // merge default options with the ones set by user - this.opts = { ...defaultOptions, ...opts } - - this.handleFormSubmit = this.handleFormSubmit.bind(this) - this.handleUploadStart = this.handleUploadStart.bind(this) - this.handleSuccess = this.handleSuccess.bind(this) - this.addResultToForm = this.addResultToForm.bind(this) - this.getMetaFromForm = this.getMetaFromForm.bind(this) - } - - handleUploadStart () { - if (this.opts.getMetaFromForm) { - this.getMetaFromForm() - } - } - - handleSuccess (result) { - if (this.opts.addResultToForm) { - this.addResultToForm(result) - } - - if (this.opts.submitOnSuccess) { - this.form.requestSubmit() - } - } - - handleFormSubmit (ev) { - if (this.opts.triggerUploadOnSubmit) { - ev.preventDefault() - const elements = toArray(ev.target.elements) - const disabledByUppy = [] - elements.forEach((el) => { - const isButton = el.tagName === 'BUTTON' || (el.tagName === 'INPUT' && el.type === 'submit') - if (isButton && !el.disabled) { - el.disabled = true // eslint-disable-line no-param-reassign - disabledByUppy.push(el) - } - }) - this.uppy.upload().then(() => { - disabledByUppy.forEach((button) => { - button.disabled = false // eslint-disable-line no-param-reassign - }) - }, (err) => { - disabledByUppy.forEach((button) => { - button.disabled = false // eslint-disable-line no-param-reassign - }) - return Promise.reject(err) - }).catch((err) => { - this.uppy.log(err.stack || err.message || err) - }) - } - } - - addResultToForm (result) { - this.uppy.log('[Form] Adding result to the original form:') - this.uppy.log(result) - - let resultInput = this.form.querySelector(`[name="${this.opts.resultName}"]`) - if (resultInput) { - // Append new result to the previous result array. - // If the previous result is empty, or not an array, - // set it to an empty array. - let updatedResult - try { - updatedResult = JSON.parse(resultInput.value) - } catch (err) { - // Nothing, since we check for array below anyway - } - - if (!Array.isArray(updatedResult)) { - updatedResult = [] - } - updatedResult.push(result) - resultInput.value = JSON.stringify(updatedResult) - return - } - - resultInput = document.createElement('input') - resultInput.name = this.opts.resultName - resultInput.type = 'hidden' - resultInput.value = JSON.stringify([result]) - - this.form.appendChild(resultInput) - } - - getMetaFromForm () { - const formMeta = getFormData(this.form) - // We want to exclude meta the the Form plugin itself has added - // See https://github.com/transloadit/uppy/issues/1637 - delete formMeta[this.opts.resultName] - this.uppy.setMeta(formMeta) - } - - install () { - this.form = findDOMElement(this.opts.target) - if (!this.form || this.form.nodeName !== 'FORM') { - this.uppy.log('Form plugin requires a
target element passed in options to operate, none was found', 'error') - return - } - - this.form.addEventListener('submit', this.handleFormSubmit) - this.uppy.on('upload', this.handleUploadStart) - this.uppy.on('complete', this.handleSuccess) - } - - uninstall () { - this.form.removeEventListener('submit', this.handleFormSubmit) - this.uppy.off('upload', this.handleUploadStart) - this.uppy.off('complete', this.handleSuccess) - } -} diff --git a/packages/@uppy/form/src/index.ts b/packages/@uppy/form/src/index.ts new file mode 100644 index 0000000000..93a28c05de --- /dev/null +++ b/packages/@uppy/form/src/index.ts @@ -0,0 +1,174 @@ +import BasePlugin, { type DefinePluginOpts } from '@uppy/core/lib/BasePlugin.js' +import findDOMElement from '@uppy/utils/lib/findDOMElement' +import toArray from '@uppy/utils/lib/toArray' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore untyped +import getFormData from 'get-form-data' + +import type { UIPluginOptions, Uppy, UppyEventMap } from '@uppy/core' +import type { Body, Meta } from '@uppy/utils/lib/UppyFile' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore We don't want TS to generate types for the package.json +import packageJson from '../package.json' + +type Result = Parameters< + UppyEventMap['complete'] +>[0] + +export interface FormOptions extends UIPluginOptions { + resultName?: string + getMetaFromForm?: boolean + addResultToForm?: boolean + submitOnSuccess?: boolean + triggerUploadOnSubmit?: boolean +} + +const defaultOptions = { + resultName: 'uppyResult', + getMetaFromForm: true, + addResultToForm: true, + submitOnSuccess: false, + triggerUploadOnSubmit: false, +} + +type Opts = DefinePluginOpts + +function assertHTMLFormElement(input: Node | null): HTMLFormElement { + if (input == null || input.nodeName !== 'FORM') { + throw new Error('ASSERTION FAILED: the target is not a element', { + cause: input, + }) + } + return input as any +} + +export default class Form extends BasePlugin< + Opts, + M, + B +> { + static VERSION = packageJson.version + + form: HTMLFormElement // TODO: make this private (or at least, mark it as readonly) + + constructor(uppy: Uppy, opts?: FormOptions) { + super(uppy, { ...defaultOptions, ...opts }) + this.type = 'acquirer' + this.id = this.opts.id || 'Form' + + this.handleFormSubmit = this.handleFormSubmit.bind(this) + this.handleUploadStart = this.handleUploadStart.bind(this) + this.handleSuccess = this.handleSuccess.bind(this) + this.addResultToForm = this.addResultToForm.bind(this) + this.getMetaFromForm = this.getMetaFromForm.bind(this) + } + + handleUploadStart(): void { + if (this.opts.getMetaFromForm) { + this.getMetaFromForm() + } + } + + handleSuccess(result: Result): void { + if (this.opts.addResultToForm) { + this.addResultToForm(result) + } + + if (this.opts.submitOnSuccess) { + this.form.requestSubmit() + } + } + + handleFormSubmit(ev: Event): void { + if (this.opts.triggerUploadOnSubmit) { + ev.preventDefault() + const elements = toArray((ev.target as HTMLFormElement).elements) + const disabledByUppy: HTMLButtonElement[] = [] + elements.forEach((el) => { + const isButton = + el.tagName === 'BUTTON' || + (el.tagName === 'INPUT' && + (el as HTMLButtonElement).type === 'submit') + if (isButton && !(el as HTMLButtonElement).disabled) { + ;(el as HTMLButtonElement).disabled = true // eslint-disable-line no-param-reassign + disabledByUppy.push(el as HTMLButtonElement) + } + }) + this.uppy + .upload() + .then( + () => { + disabledByUppy.forEach((button) => { + button.disabled = false // eslint-disable-line no-param-reassign + }) + }, + (err) => { + disabledByUppy.forEach((button) => { + button.disabled = false // eslint-disable-line no-param-reassign + }) + return Promise.reject(err) + }, + ) + .catch((err) => { + this.uppy.log(err.stack || err.message || err) + }) + } + } + + addResultToForm(result: Result): void { + this.uppy.log('[Form] Adding result to the original form:') + this.uppy.log(result) + + let resultInput: HTMLInputElement | null = this.form.querySelector( + `[name="${this.opts.resultName}"]`, + ) + if (resultInput) { + // Append new result to the previous result array. + // If the previous result is empty, or not an array, + // set it to an empty array. + let updatedResult + try { + updatedResult = JSON.parse(resultInput.value) + } catch (err) { + // Nothing, since we check for array below anyway + } + + if (!Array.isArray(updatedResult)) { + updatedResult = [] + } + updatedResult.push(result) + resultInput.value = JSON.stringify(updatedResult) + return + } + + resultInput = document.createElement('input') + resultInput.name = this.opts.resultName + resultInput.type = 'hidden' + resultInput.value = JSON.stringify([result]) + + this.form.appendChild(resultInput) + } + + getMetaFromForm(): void { + const formMeta = getFormData(this.form) + // We want to exclude meta the the Form plugin itself has added + // See https://github.com/transloadit/uppy/issues/1637 + delete formMeta[this.opts.resultName] + this.uppy.setMeta(formMeta) + } + + install(): void { + this.form = assertHTMLFormElement(findDOMElement(this.opts.target)) + + this.form.addEventListener('submit', this.handleFormSubmit) + this.uppy.on('upload', this.handleUploadStart) + this.uppy.on('complete', this.handleSuccess) + } + + uninstall(): void { + this.form.removeEventListener('submit', this.handleFormSubmit) + this.uppy.off('upload', this.handleUploadStart) + this.uppy.off('complete', this.handleSuccess) + } +} diff --git a/packages/@uppy/form/tsconfig.build.json b/packages/@uppy/form/tsconfig.build.json new file mode 100644 index 0000000000..1b0ca41093 --- /dev/null +++ b/packages/@uppy/form/tsconfig.build.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "noImplicitAny": false, + "outDir": "./lib", + "paths": { + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"] + }, + "resolveJsonModule": false, + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/form/tsconfig.json b/packages/@uppy/form/tsconfig.json new file mode 100644 index 0000000000..a76c3b714a --- /dev/null +++ b/packages/@uppy/form/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, + "paths": { + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + }, + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../utils/tsconfig.build.json", + }, + { + "path": "../core/tsconfig.build.json", + }, + ], +}