src/Embed.js
// eslint-disable-next-line max-statements
export function embed(config = {}) {
const scripts = document.getElementsByTagName('script');
config = Object.assign(config, window.FormioConfig);
let thisScript = null;
let i = scripts.length;
const scriptName = config.scriptName || 'formio.embed.';
while (i--) {
if (
scripts[i].src && (scripts[i].src.indexOf(scriptName) !== -1)
) {
thisScript = scripts[i];
break;
}
}
if (thisScript) {
const query = {};
const queryString = thisScript.src.replace(/^[^?]+\??/, '');
queryString.replace(/\?/g, '&').split('&').forEach((item) => {
query[item.split('=')[0]] = item.split('=')[1] && decodeURIComponent(item.split('=')[1]);
});
let scriptSrc = thisScript.src.replace(/^([^?]+).*/, '$1').split('/');
scriptSrc.pop();
if (config.formioPath) {
config.formioPath(scriptSrc);
}
scriptSrc = scriptSrc.join('/');
query.script = query.script || (`${scriptSrc}/formio.form.min.js`);
query.styles = query.styles || (`${scriptSrc}/formio.form.min.css`);
const cdn = query.cdn || 'https://cdn.jsdelivr.net/npm';
config = Object.assign({
script: query.script,
style: query.styles,
libs: {
uswds: {
fa: true,
js: `${cdn}/uswds@2.10.0/dist/js/uswds.min.js`,
css: `${cdn}/uswds@2.10.0/dist/css/uswds.min.css`
},
fontawesome: {
css: `${cdn}/font-awesome@4.7.0/css/font-awesome.min.css`
},
bootstrap: {
css: `${cdn}/bootstrap@4.6.0/dist/css/bootstrap.min.css`
}
},
class: (query.class || 'formio-form-wrapper'),
src: query.src,
form: null,
submission: null,
project: query.project,
base: query.base,
submit: query.submit,
includeLibs: (query.libs === 'true' || query.libs === '1'),
template: query.template,
debug: (query.debug === 'true' || query.debug === '1'),
config: {},
redirect: (query.return || query.redirect),
before: () => {},
after: () => {}
}, config);
/**
* Print debug statements.
*
* @param {...any} args Arguments to pass to the console.log method.
*/
const debug = (...args) => {
if (config.debug) {
console.log(...args);
}
};
/**
* Creates a new DOM element.
*
* @param {string} tag The HTMLElement to add to the wrapper or shadow dom.
* @param {Object} attrs The attributes to add to this element.
* @param {Array<Object>} children The children attached to this element.
*/
const createElement = (tag, attrs, children) => {
const element = document.createElement(tag);
for (const attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
element.setAttribute(attr, attrs[attr]);
}
}
(children || []).forEach(child => {
element.appendChild(createElement(child.tag, child.attrs, child.children));
});
return element;
};
debug('Embedding Configuration', config);
// The id for this embedded form.
const id = `formio-${Math.random().toString(36).substring(7)}`;
config.id = id;
debug('Creating form wrapper');
let wrapper = createElement('div', {
'id': `"${id}-wrapper"`
});
// insertAfter doesn't exist, but effect is identical.
thisScript.parentNode.insertBefore(wrapper, thisScript.parentNode.firstElementChild.nextSibling);
// If we include the libraries, then we will attempt to run this in shadow dom.
if (config.includeLibs && (typeof wrapper.attachShadow === 'function')) {
wrapper = wrapper.attachShadow({
mode: 'open'
});
config.config.shadowRoot = wrapper;
}
const global = (name) => {
const globalValue = window[name];
debug(`Getting global ${name}`, globalValue);
return globalValue;
};
const addStyles = (href, global) => {
if (!href) {
return;
}
if (typeof href !== 'string' && href.length) {
href.forEach(ref => addStyles(ref));
return;
}
debug('Adding Styles', href);
const link = createElement('link', {
rel: 'stylesheet',
href
});
if (global) {
// Add globally as well.
document.head.appendChild(link);
}
wrapper.appendChild(createElement('link', {
rel: 'stylesheet',
href
}));
};
const addScript = (src, globalProp, onReady) => {
if (!src) {
return;
}
if (typeof src !== 'string' && src.length) {
src.forEach(ref => addScript(ref));
return;
}
if (globalProp && global(globalProp)) {
debug(`${globalProp} already loaded.`);
return global(globalProp);
}
debug('Adding Script', src);
wrapper.appendChild(createElement('script', {
src
}));
if (globalProp && onReady) {
debug(`Waiting to load ${globalProp}`);
const wait = setInterval(() => {
if (global(globalProp)) {
clearInterval(wait);
debug(`${globalProp} loaded.`);
onReady(global(globalProp));
}
}, 100);
}
};
// Create a loader
addStyles(`${scriptSrc}/formio.embed.min.css`);
debug('Creating loader');
const loader = createElement('div', {
'class': 'formio-loader'
}, [{
tag: 'div',
attrs: {
class: 'loader-wrapper'
},
children: [{
tag: 'div',
attrs: {
class: 'loader text-center'
}
}]
}]);
wrapper.appendChild(loader);
// Add the wrapper for the rendered form.
debug('Creating form element');
const formElement = createElement('div', {
class: config.class
});
wrapper.appendChild(formElement);
// Add the main formio script.
addScript(config.script, 'Formio', (Formio) => {
const renderForm = () => {
addStyles(config.style);
const isReady = config.before(Formio, formElement, config) || Formio.Promise.resolve();
const form = (config.form || config.src);
debug('Creating form', form, config.config);
isReady.then(() => {
Formio.license = true;
Formio.createForm(formElement, form, config.config).then((instance) => {
const submitDone = (submission) => {
debug('Submision Complete', submission);
let returnUrl = config.redirect;
// Allow form based configuration for return url.
if (
!returnUrl &&
(
instance._form &&
instance._form.settings &&
(
instance._form.settings.returnUrl ||
instance._form.settings.redirect
)
)
) {
debug('Return url found in form configuration');
returnUrl = instance._form.settings.returnUrl || instance._form.settings.redirect;
}
if (returnUrl) {
const formSrc = instance.formio ? instance.formio.formUrl : '';
const hasQuery = !!returnUrl.match(/\?/);
const isOrigin = returnUrl.indexOf(location.origin) === 0;
returnUrl += hasQuery ? '&' : '?';
returnUrl += `sub=${submission._id}`;
if (!isOrigin && formSrc) {
returnUrl += `&form=${encodeURIComponent(formSrc)}`;
}
debug('Return URL', returnUrl);
window.location.href = returnUrl;
if (isOrigin) {
window.location.reload();
}
}
};
if (config.submit) {
instance.nosubmit = true;
}
debug('Form created', instance);
// Remove the loader.
debug('Removing loader');
wrapper.removeChild(loader);
// Set the default submission data.
if (config.submission) {
debug('Setting submission', config.submission);
instance.submission = config.submission;
}
// Allow them to provide additional configs.
debug('Triggering embed event');
Formio.events.emit('formEmbedded', instance);
debug('Calling ready callback');
config.after(instance, config);
// Configure a redirect.
instance.on('submit', (submission) => {
debug("on('submit')", submission);
if (config.submit) {
debug(`Sending submission to ${config.submit}`);
const headers = {
'content-type': 'application/json'
};
const token = Formio.getToken();
if (token) {
headers['x-jwt-token'] = token;
}
Formio.fetch(config.submit, {
body: JSON.stringify(submission),
headers: headers,
method: 'POST',
mode: 'cors',
})
.then(resp => resp.json())
.then(submission => submitDone(submission));
}
else {
submitDone(submission);
}
});
});
});
};
if (config.base) {
Formio.setBaseUrl(config.base);
}
if (config.project) {
Formio.setProjectUrl(config.project);
}
// Add premium modules
if (global('premium')) {
debug('Using premium module.');
Formio.use(global('premium'));
}
if (global('vpat')) {
debug('Using vpat module.');
Formio.use(global('vpat'));
}
if (config.template) {
if (config.includeLibs) {
addStyles(config.libs[config.template].css);
addScript(config.libs[config.template].js);
if (config.libs[config.template].fa) {
addStyles(config.libs.fontawesome.css, true);
}
}
const templateSrc = `${cdn}/@formio/${config.template}@latest/dist/${config.template}.min`;
addStyles(`${templateSrc}.css`);
addScript(`${templateSrc}.js`, config.template, (template) => {
debug(`Using ${config.template}`);
Formio.use(template);
renderForm();
});
}
else if (global('uswds')) {
debug('Using uswds module.');
Formio.use(global('uswds'));
}
// Default bootstrap + fontawesome.
else if (config.includeLibs) {
addStyles(config.libs.fontawesome.css, true);
addStyles(config.libs.bootstrap.css);
}
// Render the form if no template is provided.
if (!config.template) {
renderForm();
}
});
}
else {
// Show an error if the script cannot be found.
document.write('<span>Could not locate the Embedded form.</span>');
}
}