(core) Refactor forms implementation

Summary: WIP

Test Plan: Existing tests.

Reviewers: jarek

Reviewed By: jarek

Subscribers: jarek

Differential Revision: https://phab.getgrist.com/D4196
This commit is contained in:
George Gevoian
2024-02-21 14:22:01 -05:00
parent 6800ebfbad
commit c6fd79ac1f
53 changed files with 1746 additions and 1811 deletions

14
static/form.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
<!-- INSERT BASE -->
<link rel="icon" type="image/x-icon" href="icons/favicon.png" />
<link rel="stylesheet" href="icons/icons.css">
<title>Loading...<!-- INSERT TITLE SUFFIX --></title>
</head>
<body>
<!-- INSERT CONFIG -->
<script crossorigin="anonymous" src="form.bundle.js"></script>
</body>
</html>

View File

@@ -1,19 +0,0 @@
## grist-form-submit.js
File is taken from https://github.com/gristlabs/grist-form-submit. But it is modified to work with
forms, especially for:
- Ref and RefList columns, as by default it sends numbers as strings (FormData issue), and Grist
doesn't know how to convert them back to numbers.
- Empty strings are not sent at all - otherwise Grist won't be able to fire trigger formulas
correctly and provide default values for columns.
- By default it requires a redirect URL, now it is optional.
## purify.min.js
File taken from https://www.npmjs.com/package/dompurify. It is used to sanitize HTML. It wasn't
modified at all.
## form.html
This is handlebars template filled by DocApi.ts

View File

@@ -1,533 +0,0 @@
html,
body {
background-color: #f7f7f7;
padding: 0px;
margin: 0px;
line-height: 1.42857143;
}
* {
box-sizing: border-box;
}
.grist-form-container {
--icon-Tick: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExLjYxODMwNjksNC42NzcwMjg0NyBDMTEuNzk2Njc4OSw0LjQ2NjIyNTE3IDEyLjExMjE2NzgsNC40Mzk5MzQ0MyAxMi4zMjI5NzExLDQuNjE4MzA2NDUgQzEyLjUzMzc3NDQsNC43OTY2Nzg0OCAxMi41NjAwNjUyLDUuMTEyMTY3NDEgMTIuMzgxNjkzMSw1LjMyMjk3MDcxIEw2LjUzMDY4ODI3LDEyLjIzNzc5NDYgTDMuNjQ2NDQ2NjEsOS4zNTM1NTI5OCBDMy40NTExODQ0Niw5LjE1ODI5MDg0IDMuNDUxMTg0NDYsOC44NDE3MDgzNSAzLjY0NjQ0NjYxLDguNjQ2NDQ2MiBDMy44NDE3MDg3Niw4LjQ1MTE4NDA2IDQuMTU4MjkxMjQsOC40NTExODQwNiA0LjM1MzU1MzM5LDguNjQ2NDQ2MiBMNi40NjkzMTE3MywxMC43NjIyMDQ1IEwxMS42MTgzMDY5LDQuNjc3MDI4NDcgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+);
--icon-Minus: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3QgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiB4PSIyIiB5PSI3LjUiIHdpZHRoPSIxMiIgaGVpZ2h0PSIxIiByeD0iLjUiLz48L3N2Zz4=);
--icon-Expand: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTgsOS4xNzQ2MzA1MiBMMTAuOTIxODI3Myw2LjE4OTAyMzIgQzExLjE2ODQ3NDIsNS45MzY5OTIyNyAxMS41NjgzNjc5LDUuOTM2OTkyMjcgMTEuODE1MDE0OCw2LjE4OTAyMzIgQzEyLjA2MTY2MTcsNi40NDEwNTQxMyAxMi4wNjE2NjE3LDYuODQ5Njc3MDEgMTEuODE1MDE0OCw3LjEwMTcwNzk0IEw4LDExIEw0LjE4NDk4NTE5LDcuMTAxNzA3OTQgQzMuOTM4MzM4MjcsNi44NDk2NzcwMSAzLjkzODMzODI3LDYuNDQxMDU0MTMgNC4xODQ5ODUxOSw2LjE4OTAyMzIgQzQuNDMxNjMyMTEsNS45MzY5OTIyNyA0LjgzMTUyNTc4LDUuOTM2OTkyMjcgNS4wNzgxNzI3LDYuMTg5MDIzMiBMOCw5LjE3NDYzMDUyIFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDggOC41KSIvPjwvc3ZnPg==');
--primary: #16b378;
--primary-dark: #009058;
--dark-gray: #D9D9D9;
--light-gray: #bfbfbf;
--light: white;
color: #262633;
background-color: #f7f7f7;
min-height: 100%;
width: 100%;
padding: 52px 0px 52px 0px;
font-size: 15px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Liberation Sans", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.grist-form-container .grist-form-confirm {
background-color: white;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
border: 1px solid var(--dark-gray);
border-radius: 3px;
max-width: 600px;
margin: 0px auto;
}
.grist-form {
margin: 0px auto;
background-color: white;
border: 1px solid var(--dark-gray);
width: 600px;
border-radius: 8px;
display: flex;
flex-direction: column;
max-width: calc(100% - 32px);
margin-bottom: 16px;
padding-top: 20px;
--grist-form-padding: 48px;
padding-left: var(--grist-form-padding);
padding-right: var(--grist-form-padding);
}
@media screen and (max-width: 600px) {
.grist-form-container {
padding: 20px 0px 20px 0px;
}
.grist-form {
--grist-form-padding: 20px;
}
}
.grist-form > div + div {
margin-top: 16px;
}
.grist-form .grist-section {
border-radius: 3px;
border: 1px solid var(--dark-gray);
padding: 16px 24px;
padding: 24px;
margin-top: 24px;
}
.grist-form .grist-section > div + div {
margin-top: 16px;
}
.grist-form input[type="text"],
.grist-form input[type="date"],
.grist-form input[type="datetime-local"],
.grist-form input[type="number"] {
height: 27px;
padding: 4px 8px;
border: 1px solid var(--dark-gray);
border-radius: 3px;
outline: none;
}
.grist-form .grist-field {
display: flex;
flex-direction: column;
}
.grist-form .grist-field .grist-field-description {
color: #222;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: pre-wrap;
font-style: italic;
font-weight: 400;
line-height: 1.6;
}
.grist-form .grist-field input[type="text"] {
padding: 4px 8px;
border-radius: 3px;
border: 1px solid var(--dark-gray);
font-size: 13px;
outline-color: var(--primary);
outline-width: 1px;
line-height: inherit;
width: 100%;
}
.grist-form .grist-submit, .grist-form-container button {
display: flex;
justify-content: center;
align-items: center;
}
.grist-form input[type="submit"], .grist-form-container button {
background-color: var(--primary);
border: 1px solid var(--primary);
color: white;
padding: 10px 24px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
line-height: inherit;
}
.grist-form input[type="datetime-local"] {
width: 100%;
line-height: inherit;
}
.grist-form input[type="date"] {
width: 100%;
line-height: inherit;
}
.grist-form .grist-columns {
display: grid;
grid-template-columns: repeat(var(--grist-columns-count), 1fr);
gap: 4px;
}
.grist-form select {
padding: 4px 8px;
border-radius: 3px;
border: 1px solid var(--dark-gray);
font-size: 13px;
outline-color: var(--primary);
outline-width: 1px;
background: white;
line-height: inherit;
height: 27px;
flex: auto;
width: 100%;
}
.grist-form .grist-checkbox-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.grist-form .grist-checkbox {
display: flex;
}
.grist-form .grist-checkbox:hover {
--color: var(--light-gray);
}
.grist-form input[type="checkbox"] {
-webkit-appearance: none;
-moz-appearance: none;
padding: 0;
flex-shrink: 0;
display: inline-block;
width: 16px;
height: 16px;
--radius: 3px;
position: relative;
margin-right: 8px;
vertical-align: baseline;
}
.grist-form input[type="checkbox"]:checked:enabled, .grist-form input[type="checkbox"]:indeterminate:enabled {
--color: var(--primary);
}
.grist-form input[type="checkbox"]:disabled {
--color: var(--dark-gray);
cursor: not-allowed;
}
.grist-form input[type="checkbox"]::before, .grist-form input[type="checkbox"]::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
box-sizing: border-box;
border: 1px solid var(--color, var(--dark-gray));
border-radius: var(--radius);
}
.grist-form input[type="checkbox"]:checked::before, .grist-form input[type="checkbox"]:disabled::before, .grist-form input[type="checkbox"]:indeterminate::before {
background-color: var(--color);
}
.grist-form input[type="checkbox"]:not(:checked):indeterminate::after {
-webkit-mask-image: var(--icon-Minus);
}
.grist-form input[type="checkbox"]:not(:disabled)::after {
background-color: var(--light);
}
.grist-form input[type="checkbox"]:checked::after, .grist-form input[type="checkbox"]:indeterminate::after {
content: '';
position: absolute;
height: 16px;
width: 16px;
-webkit-mask-image: var(--icon-Tick);
-webkit-mask-size: contain;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
background-color: var(--light);
}
.grist-form .grist-submit input[type="submit"]:hover, .grist-form-container button:hover {
border-color: var(--primary-dark);
background-color: var(--primary-dark);
}
.grist-power-by {
color: #494949;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 16px;
display: flex;
align-items: center;
justify-content: center;
padding-left: 10px;
padding-right: 10px;
}
.grist-power-by a {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #494949;
text-decoration: none;
}
.grist-logo {
width: 58px;
height: 20.416px;
flex-shrink: 0;
background: url(logo.png);
background-position: 0 0;
background-size: contain;
background-color: transparent;
background-repeat: no-repeat;
margin-top: 3px;
}
.grist-question > .grist-label {
color: var(--dark, #262633);
font-size: 13px;
font-style: normal;
font-weight: 700;
line-height: 16px; /* 145.455% */
margin-top: 8px;
margin-bottom: 8px;
display: block;
}
.grist-label-required::after {
content: "*";
color: var(--primary, #16b378);
margin-left: 4px;
}
/* Markdown reset */
.grist-form h1,
.grist-form h2,
.grist-form h3,
.grist-form h4,
.grist-form h5,
.grist-form h6 {
margin: 4px 0px;
font-weight: normal;
}
.grist-form h1 {
font-size: 24px;
}
.grist-form h2 {
font-size: 22px;
}
.grist-form h3 {
font-size: 16px;
}
.grist-form h4 {
font-size: 13px;
}
.grist-form h5 {
font-size: 11px;
}
.grist-form h6 {
font-size: 10px;
}
.grist-form p {
margin: 0px;
}
.grist-form strong {
font-weight: 600;
}
.grist-form hr {
border: 0px;
border-top: 1px solid var(--dark-gray);
margin: 4px 0px;
}
.grist-text-left {
text-align: left;
}
.grist-text-right {
text-align: right;
}
.grist-text-center {
text-align: center;
}
.grist-switch {
cursor: pointer;
display: inline-flex;
align-items: center;
}
.grist-switch input[type='checkbox']::after {
content: none;
}
.grist-switch input[type='checkbox']::before {
content: none;
}
.grist-switch input[type='checkbox'] {
position: absolute;
}
.grist-switch > span {
margin-left: 8px;
}
/* Slider component */
.grist-widget_switch {
position: relative;
width: 30px;
height: 17px;
display: inline-block;
flex: none;
}
.grist-switch_slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--grist-theme-switch-slider-fg, #ccc);
border-radius: 17px;
}
.grist-switch_slider:hover {
box-shadow: 0 0 1px #2196F3;
}
.grist-switch_circle {
position: absolute;
cursor: pointer;
content: "";
height: 13px;
width: 13px;
left: 2px;
bottom: 2px;
background-color: var(--grist-theme-switch-circle-fg, white);
border-radius: 17px;
}
input:checked + .grist-switch_transition > .grist-switch_slider {
background-color: var(--primary, #16b378);
}
input:checked + .grist-switch_transition > .grist-switch_circle {
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
.grist-switch_on > .grist-switch_slider {
background-color: var(--grist-actual-cell-color, #2CB0AF);
}
.grist-switch_on > .grist-switch_circle {
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
.grist-switch_transition > .grist-switch_slider, .grist-switch_transition > .grist-switch_circle {
-webkit-transition: .4s;
transition: .4s;
}
.grist-form-confirm-container {
padding-left: 16px;
padding-right: 16px;
}
.grist-form-confirm-body {
padding: 48px 16px 16px 16px;
}
.grist-form-confirm-image {
width: 100%;
height: 100%;
max-width: 250px;
max-height: 215px;
}
.grist-form-confirm-text {
font-weight: 600;
font-size: 16px;
line-height: 24px;
margin-top: 32px;
white-space: prewrap;
}
.grist-form-confirm-buttons {
display: flex;
justify-content: center;
align-items: center;
margin-top: 24px;
}
.grist-form-confirm-new-response-button {
position: relative;
outline: none;
border-style: none;
line-height: normal;
user-select: none;
display: flex;
justify-content: center;
align-items: center;
padding: 12px 24px;
min-height: 40px;
background: var(--primary, #16B378);
border-radius: 3px;
color: #FFFFFF;
}
.grist-form-confirm-new-response-button:hover {
background: var(--primary-dark);
cursor: pointer;
}
.grist-form-footer,
.grist-form-confirm-footer {
border-top: 1px solid var(--dark-gray);
padding: 8px 16px;
}
.grist-form-footer {
margin-left: calc(-1 * var(--grist-form-padding));
margin-right: calc(-1 * var(--grist-form-padding));
}
.grist-form-confirm-footer {
width: 100%;
}
.grist-form-build-form-link-container {
display: flex;
align-items: center;
justify-content: center;
margin-top: 8px;
}
.grist-form-build-form-link {
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
line-height: 16px;
text-decoration-line: underline;
color: var(--primary-dark);
}
.grist-form-icon {
position: relative;
display: inline-block;
vertical-align: middle;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
width: 16px;
height: 16px;
background-color: black;
}
.grist-form-icon-expand {
-webkit-mask-image: var(--icon-Expand);
background-color: var(--primary-dark);
}

View File

@@ -1,108 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf8">
{{#if BASE}}
<base href="{{ BASE }}">
{{/if}}
<title>{{ TITLE }}</title>
<link rel="icon" type="image/x-icon" href="icons/favicon.png" />
<script src="forms/grist-form-submit.js"></script>
<script src="forms/purify.min.js"></script>
<script>
// Make all links open in a new tab.
DOMPurify.addHook('uponSanitizeAttribute', (node) => {
if (!('target' in node)) { return; }
node.setAttribute('target', '_blank');
// Make sure that this is set explicitly, as it's often set by the browser.
node.setAttribute('rel', 'noopener');
});
</script>
<link rel="stylesheet" href="forms/form.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<main class='grist-form-container'>
<form class='grist-form'
onsubmit="event.target.parentElement.querySelector('.grist-form-confirm').style.display = 'flex', event.target.style.display = 'none'"
data-grist-doc="{{ DOC_URL }}"
data-grist-table="{{ TABLE_ID }}"
data-grist-success-url="{{ SUCCESS_URL }}"
>
{{ dompurify CONTENT }}
<div class='grist-form-footer'>
<div class="grist-power-by">
<a href="{{ FORMS_LANDING_PAGE_URL }}" target="_blank">
<div>Powered by</div>
<div class="grist-logo"></div>
</a>
</div>
<div class='grist-form-build-form-link-container'>
<a class='grist-form-build-form-link' href="{{ FORMS_LANDING_PAGE_URL }}" target="_blank">
Build your own form
<div class="grist-form-icon grist-form-icon-expand"></div>
</a>
</div>
</div>
</form>
<div class="grist-form-confirm-container">
<div class='grist-form-confirm' style='display: none'>
<div class="grist-form-confirm-body">
<img class='grist-form-confirm-image' src="forms/form-submitted.svg">
<div class='grist-form-confirm-text'>
{{ SUCCESS_TEXT }}
</div>
{{#if ANOTHER_RESPONSE }}
<div class='grist-form-confirm-buttons'>
<button
class='grist-form-confirm-new-response-button'
onclick='window.location.reload()'
>
Submit new response
</button>
</div>
{{/if}}
</div>
<div class='grist-form-confirm-footer'>
<div class="grist-power-by">
<a href="https://www.getgrist.com/forms/?utm_source=grist-forms&utm_medium=grist-forms&utm_campaign=forms-footer" target="_blank">
<div>Powered by</div>
<div class="grist-logo"></div>
</a>
</div>
<div class='grist-form-build-form-link-container'>
<a class='grist-form-build-form-link' href="https://www.getgrist.com/forms/?utm_source=grist-forms&utm_medium=grist-forms&utm_campaign=forms-footer" target="_blank">
Build your own form
<div class="grist-form-icon grist-form-icon-expand"></div>
</a>
</div>
</div>
</div>
</div>
</main>
<script>
// Validate choice list on submit
document.querySelector('.grist-form input[type="submit"]').addEventListener('click', function(event) {
// When submit is pressed make sure that all choice lists that are required
// have at least one option selected
const choiceLists = document.querySelectorAll('.grist-checkbox-list.required:not(:has(input:checked))');
Array.from(choiceLists).forEach(function(choiceList) {
// If the form has at least one checkbox make it required
const firstCheckbox = choiceList.querySelector('input[type="checkbox"]');
firstCheckbox?.setAttribute('required', 'required');
});
// All other required choice lists with at least one option selected are no longer required
const choiceListsRequired = document.querySelectorAll('.grist-checkbox-list.required:has(input:checked)');
Array.from(choiceListsRequired).forEach(function(choiceList) {
// If the form has at least one checkbox make it required
const firstCheckbox = choiceList.querySelector('input[type="checkbox"]');
firstCheckbox?.removeAttribute('required');
});
});
</script>
</body>
</html>

View File

@@ -1,211 +0,0 @@
// If the script is loaded multiple times, only register the handlers once.
if (!window.gristFormSubmit) {
(function() {
/**
* gristFormSubmit(gristDocUrl, gristTableId, formData)
* - `gristDocUrl` should be the URL of the Grist document, from step 1 of the setup instructions.
* - `gristTableId` should be the table ID from step 2.
* - `formData` should be a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
* object, typically obtained as `new FormData(formElement)`. Inside the `submit` event handler, it
* can be convenient to use `new FormData(event.target)`.
* - formElement is the form element that was submitted.
*
* This function sends values from `formData` to add a new record in the specified Grist table. It
* returns a promise for the result of the add-record API call. In case of an error, the promise
* will be rejected with an error message.
*/
async function gristFormSubmit(docUrl, tableId, formData, formElement) {
// Pick out the server and docId from the docUrl.
const match = /^(https?:\/\/[^\/]+(?:\/o\/[^\/]+)?)\/(?:doc\/([^\/?#]+)|([^\/?#]{12,})\/)/.exec(docUrl);
if (!match) { throw new Error("Invalid Grist doc URL " + docUrl); }
const server = match[1];
const docId = match[2] || match[3];
// Construct the URL to use for the add-record API endpoint.
const destUrl = server + "/api/docs/" + docId + "/tables/" + tableId + "/records";
const payload = {records: [{fields: formDataToJson(formData, formElement)}]};
const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
};
const resp = await window.fetch(destUrl, options);
if (resp.status !== 200) {
// Try to report a helpful error.
let body = '', error, match;
try { body = await resp.json(); } catch (e) {}
if (typeof body.error === 'string' && (match = /KeyError '(.*)'/.exec(body.error))) {
error = 'No column "' + match[1] + '" in table "' + tableId + '". ' +
'Be sure to use column ID rather than column label';
} else {
error = body.error || String(body);
}
throw new Error('Failed to add record: ' + error);
}
return await resp.json();
}
// Convert FormData into a mapping of Grist fields. Skips any keys starting with underscore.
// For fields with multiple values (such as to populate ChoiceList), use field names like `foo[]`
// (with the name ending in a pair of empty square brackets).
function formDataToJson(f) {
const keys = Array.from(f.keys()).filter(k => !k.startsWith("_"));
return Object.fromEntries(keys.map(k =>
k.endsWith('[]') ? [k.slice(0, -2), ['L', ...f.getAll(k)]] : [k, f.get(k)]));
}
/**
* TypedFormData is a wrapper around FormData that provides type information for the fields.
*/
class TypedFormData {
constructor(formElement, formData) {
if (!(formElement instanceof HTMLFormElement)) throw new Error("formElement must be a form");
if (formData && !(formData instanceof FormData)) throw new Error("formData must be a FormData");
this._formData = formData ?? new FormData(formElement);
this._formElement = formElement;
}
keys() {
const keys = Array.from(this._formData.keys());
// Don't return keys for scalar values which just return empty string.
// Otherwise Grist won't fire trigger formulas.
return keys.filter(key => {
// If there are multiple values, return this key as it is.
if (this._formData.getAll(key).length !== 1) { return true; }
// If the value is empty string or null, don't return the key.
const value = this._formData.get(key);
return value !== '' && value !== null;
});
}
type(key) {
return this._formElement?.querySelector(`[name="${key}"]`)?.getAttribute('data-grist-type');
}
get(key) {
const value = this._formData.get(key);
if (value === null) { return null; }
const type = this.type(key);
return type === 'Ref' || type === 'RefList' ? Number(value) : value;
}
getAll(key) {
const values = Array.from(this._formData.getAll(key));
if (['Ref', 'RefList'].includes(this.type(key))) {
return values.map(v => Number(v));
}
return values;
}
}
// Handle submissions for plain forms that include special data-grist-* attributes.
async function handleSubmitPlainForm(ev) {
if (!['data-grist-doc', 'data-grist-table']
.some(attr => ev.target.hasAttribute(attr))) {
// This form isn't configured for Grist at all; don't interfere with it.
return;
}
ev.preventDefault();
try {
const docUrl = ev.target.getAttribute('data-grist-doc');
const tableId = ev.target.getAttribute('data-grist-table');
if (!docUrl) { throw new Error("Missing attribute data-grist-doc='GRIST_DOC_URL'"); }
if (!tableId) { throw new Error("Missing attribute data-grist-table='GRIST_TABLE_ID'"); }
const successUrl = ev.target.getAttribute('data-grist-success-url');
await gristFormSubmit(docUrl, tableId, new TypedFormData(ev.target));
// On success, redirect to the requested URL.
if (successUrl) {
window.location.href = successUrl;
}
} catch (err) {
reportSubmitError(ev, err);
}
}
function reportSubmitError(ev, err) {
console.warn("grist-form-submit error:", err.message);
// Find an element to use for the validation message to alert the user.
let scapegoat = null;
(
(scapegoat = ev.submitter)?.setCustomValidity ||
(scapegoat = ev.target.querySelector('input[type=submit]'))?.setCustomValidity ||
(scapegoat = ev.target.querySelector('button'))?.setCustomValidity ||
(scapegoat = [...ev.target.querySelectorAll('input')].pop())?.setCustomValidity
)
scapegoat?.setCustomValidity("Form misconfigured: " + err.message);
ev.target.reportValidity();
}
// Handle submissions for Contact Form 7 forms.
async function handleSubmitWPCF7(ev) {
try {
const formId = ev.detail.contactFormId;
const docUrl = ev.target.querySelector('[data-grist-doc]')?.getAttribute('data-grist-doc');
const tableId = ev.target.querySelector('[data-grist-table]')?.getAttribute('data-grist-table');
if (!docUrl) { throw new Error("Missing attribute data-grist-doc='GRIST_DOC_URL'"); }
if (!tableId) { throw new Error("Missing attribute data-grist-table='GRIST_TABLE_ID'"); }
await gristFormSubmit(docUrl, tableId, new TypedFormData(ev.target));
console.log("grist-form-submit WPCF7 Form %s: Added record", formId);
} catch (err) {
console.warn("grist-form-submit WPCF7 Form %s misconfigured:", formId, err.message);
}
}
function setUpGravityForms(options) {
// Use capture to get the event before GravityForms processes it.
document.addEventListener('submit', ev => handleSubmitGravityForm(ev, options), true);
}
gristFormSubmit.setUpGravityForms = setUpGravityForms;
async function handleSubmitGravityForm(ev, options) {
try {
ev.preventDefault();
ev.stopPropagation();
const docUrl = options.docUrl;
const tableId = options.tableId;
if (!docUrl) { throw new Error("setUpGravityForm: missing docUrl option"); }
if (!tableId) { throw new Error("setUpGravityForm: missing tableId option"); }
const f = new TypedFormData(ev.target);
for (const key of Array.from(f.keys())) {
// Skip fields other than input fields.
if (!key.startsWith("input_")) {
f.delete(key);
continue;
}
// Rename multiple fields to use "[]" convention rather than ".N" convention.
const multi = key.split(".");
if (multi.length > 1) {
f.append(multi[0] + "[]", f.get(key));
f.delete(key);
}
}
console.warn("Processed FormData", f);
await gristFormSubmit(docUrl, tableId, f);
// Follow through by doing the form submission normally.
ev.target.submit();
} catch (err) {
reportSubmitError(ev, err);
return;
}
}
window.gristFormSubmit = gristFormSubmit;
document.addEventListener('submit', handleSubmitPlainForm);
document.addEventListener('wpcf7mailsent', handleSubmitWPCF7);
})();
}

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB