Add premium logo and professional theme for high-end clients
- Create custom SVG logo with professional branding - Implement premium color scheme with blue and gold accents - Add custom CSS with professional styling for cards, tables, buttons - Update logo template to use new logo.svg file - Create custom favicon for complete branding - Redesign homepage with premium content sections - Update resources page with membership tiers and premium pricing - Enhance contact page with testimonials and detailed information - Target audience: high-paying clients ($100+/hour) - Professional yet approachable design language 💘 Generated with Crush Assisted-by: GLM-4.7 via Crush <crush@charm.land>
This commit is contained in:
165
config/www/user/plugins/form/.eslintrc
Normal file
165
config/www/user/plugins/form/.eslintrc
Normal file
@@ -0,0 +1,165 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module"
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"accessor-pairs": 2,
|
||||
"array-bracket-spacing": 0,
|
||||
"block-scoped-var": 0,
|
||||
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
|
||||
"camelcase": 0,
|
||||
"comma-dangle": [2, "never"],
|
||||
"comma-spacing": [2, { "before": false, "after": true }],
|
||||
"comma-style": [2, "last"],
|
||||
"complexity": 0,
|
||||
"computed-property-spacing": 0,
|
||||
"consistent-return": 0,
|
||||
"consistent-this": 0,
|
||||
"constructor-super": 2,
|
||||
"curly": [2, "multi-line"],
|
||||
"default-case": 0,
|
||||
"dot-location": [2, "property"],
|
||||
"dot-notation": 0,
|
||||
"eol-last": 2,
|
||||
"eqeqeq": [2, "allow-null"],
|
||||
"func-names": 0,
|
||||
"func-style": 0,
|
||||
"generator-star-spacing": [2, { "before": true, "after": true }],
|
||||
"guard-for-in": 0,
|
||||
"handle-callback-err": [2, "^(err|error)$" ],
|
||||
"indent": [2, 4, { "SwitchCase": 1 }],
|
||||
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
|
||||
"linebreak-style": 0,
|
||||
"lines-around-comment": 0,
|
||||
"max-nested-callbacks": 0,
|
||||
"new-cap": [2, { "newIsCap": true, "capIsNew": false }],
|
||||
"new-parens": 2,
|
||||
"newline-after-var": 0,
|
||||
"no-alert": 0,
|
||||
"no-array-constructor": 2,
|
||||
"no-caller": 2,
|
||||
"no-catch-shadow": 0,
|
||||
"no-cond-assign": 2,
|
||||
"no-console": 0,
|
||||
"no-constant-condition": 0,
|
||||
"no-continue": 0,
|
||||
"no-control-regex": 2,
|
||||
"no-debugger": 2,
|
||||
"no-delete-var": 2,
|
||||
"no-div-regex": 0,
|
||||
"no-dupe-args": 2,
|
||||
"no-dupe-keys": 2,
|
||||
"no-duplicate-case": 2,
|
||||
"no-else-return": 0,
|
||||
"no-empty": 0,
|
||||
"no-empty-character-class": 2,
|
||||
"no-eq-null": 0,
|
||||
"no-eval": 2,
|
||||
"no-ex-assign": 2,
|
||||
"no-extend-native": 2,
|
||||
"no-extra-bind": 2,
|
||||
"no-extra-boolean-cast": 2,
|
||||
"no-extra-parens": 0,
|
||||
"no-extra-semi": 0,
|
||||
"no-fallthrough": 2,
|
||||
"no-floating-decimal": 2,
|
||||
"no-func-assign": 2,
|
||||
"no-implied-eval": 2,
|
||||
"no-inline-comments": 0,
|
||||
"no-inner-declarations": [2, "functions"],
|
||||
"no-invalid-regexp": 2,
|
||||
"no-irregular-whitespace": 2,
|
||||
"no-iterator": 2,
|
||||
"no-label-var": 2,
|
||||
"no-labels": 2,
|
||||
"no-lone-blocks": 2,
|
||||
"no-lonely-if": 0,
|
||||
"no-loop-func": 0,
|
||||
"no-mixed-requires": 0,
|
||||
"no-mixed-spaces-and-tabs": 2,
|
||||
"no-multi-spaces": 2,
|
||||
"no-multi-str": 2,
|
||||
"no-multiple-empty-lines": [2, { "max": 1 }],
|
||||
"no-native-reassign": 2,
|
||||
"no-negated-in-lhs": 2,
|
||||
"no-nested-ternary": 0,
|
||||
"no-new": 2,
|
||||
"no-new-func": 0,
|
||||
"no-new-object": 2,
|
||||
"no-new-require": 2,
|
||||
"no-new-wrappers": 2,
|
||||
"no-obj-calls": 2,
|
||||
"no-octal": 2,
|
||||
"no-octal-escape": 2,
|
||||
"no-param-reassign": 0,
|
||||
"no-path-concat": 0,
|
||||
"no-process-env": 0,
|
||||
"no-process-exit": 0,
|
||||
"no-proto": 0,
|
||||
"no-redeclare": 2,
|
||||
"no-regex-spaces": 2,
|
||||
"no-restricted-modules": 0,
|
||||
"no-return-assign": 2,
|
||||
"no-script-url": 0,
|
||||
"no-self-compare": 2,
|
||||
"no-sequences": 2,
|
||||
"no-shadow": 0,
|
||||
"no-shadow-restricted-names": 2,
|
||||
"no-spaced-func": 2,
|
||||
"no-sparse-arrays": 2,
|
||||
"no-sync": 0,
|
||||
"no-ternary": 0,
|
||||
"no-this-before-super": 2,
|
||||
"no-throw-literal": 2,
|
||||
"no-trailing-spaces": 2,
|
||||
"no-undef": 2,
|
||||
"no-undef-init": 2,
|
||||
"no-undefined": 0,
|
||||
"no-underscore-dangle": 0,
|
||||
"no-unexpected-multiline": 2,
|
||||
"no-unneeded-ternary": 2,
|
||||
"no-unreachable": 2,
|
||||
"no-unused-expressions": 0,
|
||||
"no-unused-vars": [2, { "vars": "all", "args": "none" }],
|
||||
"no-use-before-define": 0,
|
||||
"no-var": 0,
|
||||
"no-void": 0,
|
||||
"no-warning-comments": 0,
|
||||
"no-with": 2,
|
||||
"object-curly-spacing": 0,
|
||||
"object-shorthand": 0,
|
||||
"one-var": [2, { "initialized": "never" }],
|
||||
"operator-assignment": 0,
|
||||
"operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
|
||||
"padded-blocks": 0,
|
||||
"prefer-const": 0,
|
||||
"quote-props": 0,
|
||||
"quotes": [2, "single", "avoid-escape"],
|
||||
"radix": 2,
|
||||
"semi": [2, "always"],
|
||||
"semi-spacing": 0,
|
||||
"sort-vars": 0,
|
||||
"keyword-spacing": [2, {"after": true, "overrides": {"throw": { "after": true}, "return": { "before": true }}}],
|
||||
"space-before-blocks": [2, "always"],
|
||||
"space-before-function-paren": [2, "never"],
|
||||
"space-in-parens": [2, "never"],
|
||||
"space-infix-ops": 2,
|
||||
"space-unary-ops": [2, { "words": true, "nonwords": false }],
|
||||
"spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!"] }],
|
||||
"strict": 0,
|
||||
"use-isnan": 2,
|
||||
"valid-jsdoc": 0,
|
||||
"valid-typeof": 2,
|
||||
"vars-on-top": 0,
|
||||
"wrap-iife": [2, "any"],
|
||||
"wrap-regex": 0,
|
||||
"yoda": [2, "never"]
|
||||
}
|
||||
}
|
||||
1379
config/www/user/plugins/form/CHANGELOG.md
Normal file
1379
config/www/user/plugins/form/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
21
config/www/user/plugins/form/LICENSE
Normal file
21
config/www/user/plugins/form/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Grav
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
41
config/www/user/plugins/form/README.md
Normal file
41
config/www/user/plugins/form/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Grav Form Plugin
|
||||
|
||||
The **form plugin** for [Grav](https://github.com/getgrav/grav) adds the ability to create and use forms. This is currently used extensively by the **admin** and **login** plugins.
|
||||
|
||||
# Installation
|
||||
|
||||
The form plugin is easy to install with GPM.
|
||||
|
||||
```
|
||||
$ bin/gpm install form
|
||||
```
|
||||
|
||||
# Configuration
|
||||
|
||||
Simply copy the `user/plugins/form/form.yaml` into `user/config/plugins/form.yaml` and make your modifications.
|
||||
|
||||
```
|
||||
enabled: true
|
||||
```
|
||||
|
||||
# How to use the Form Plugin
|
||||
|
||||
The Learn site has two pages describing how to use the Form Plugin:
|
||||
- [Forms](https://learn.getgrav.org/forms)
|
||||
- [Add a contact form](https://learn.getgrav.org/forms/forms/example-form)
|
||||
|
||||
# Using email
|
||||
|
||||
Note: when using email functionality in your forms, make sure you have configured the Email plugin correctly. In particular, make sure you configured the "Email from" and "Email to" email addresses in the Email plugin with your email address.
|
||||
|
||||
# NOTES:
|
||||
|
||||
As of version **Form 6.0.0** forms are no longer initialized before caching, but when the form is requested. This has been done to make dynamic forms to work better with caching. There may be some backward compatibility issues for logic that modifies pages with forms as the modification doesn't happen without accessing the form first.
|
||||
|
||||
As of version **Form 5.0.0** Grav 1.7+ is required.
|
||||
|
||||
As of version **Form 4.0.6**, form labels are now being output with the `|raw` filter. If you wish to show HTML in your form label, ie `Root Folder <root>`, then you need to escape that in your form definition:
|
||||
|
||||
```yaml
|
||||
label: Root Folder <root>
|
||||
```
|
||||
200
config/www/user/plugins/form/app/fields/array.js
Normal file
200
config/www/user/plugins/form/app/fields/array.js
Normal file
@@ -0,0 +1,200 @@
|
||||
import $ from 'jquery';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
let body = $('body');
|
||||
|
||||
class Template {
|
||||
constructor(container) {
|
||||
this.container = $(container);
|
||||
|
||||
if (this.getName() === undefined) {
|
||||
this.container = this.container.closest('[data-grav-array-name]');
|
||||
}
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.container.data('grav-array-name') || '';
|
||||
}
|
||||
|
||||
getKeyPlaceholder() {
|
||||
return this.container.data('grav-array-keyname') || 'Key';
|
||||
}
|
||||
|
||||
getValuePlaceholder() {
|
||||
return this.container.data('grav-array-valuename') || 'Value';
|
||||
}
|
||||
|
||||
isValueOnly() {
|
||||
return this.container.find('[data-grav-array-mode="value_only"]:first').length || false;
|
||||
}
|
||||
|
||||
isTextArea() {
|
||||
return this.container.data('grav-array-textarea') || false;
|
||||
}
|
||||
|
||||
shouldBeDisabled() {
|
||||
// check for toggleables, if field is toggleable and it's not enabled, render disabled
|
||||
let toggle = this.container.closest('.form-field').find('[data-grav-field="toggleable"] input[type="checkbox"]');
|
||||
return toggle.length && toggle.is(':not(:checked)');
|
||||
}
|
||||
|
||||
getNewRow() {
|
||||
let tpl = '';
|
||||
const value = this.isTextArea()
|
||||
? `<textarea ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" name="" placeholder="${this.getValuePlaceholder()}"></textarea>`
|
||||
: `<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" type="text" name="" value="" placeholder="${this.getValuePlaceholder()}" />`;
|
||||
|
||||
if (this.isValueOnly()) {
|
||||
tpl += `
|
||||
<div class="form-row array-field-value_only" data-grav-array-type="row">
|
||||
<span data-grav-array-action="sort" class="fa fa-bars"></span>
|
||||
${value}
|
||||
`;
|
||||
} else {
|
||||
tpl += `
|
||||
<div class="form-row" data-grav-array-type="row">
|
||||
<span data-grav-array-action="sort" class="fa fa-bars"></span>
|
||||
<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="key" type="text" value="" placeholder="${this.getKeyPlaceholder()}" />
|
||||
${value}
|
||||
`;
|
||||
}
|
||||
|
||||
tpl += `
|
||||
<span data-grav-array-action="rem" class="fa fa-minus"></span>
|
||||
<span data-grav-array-action="add" class="fa fa-plus"></span>
|
||||
</div>`;
|
||||
|
||||
return tpl;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ArrayField {
|
||||
constructor() {
|
||||
body.on('input', '[data-grav-array-type="key"], [data-grav-array-type="value"]', (event) => this.actionInput(event));
|
||||
body.on('click touch', '[data-grav-array-action]:not([data-grav-array-action="sort"])', (event) => this.actionEvent(event));
|
||||
|
||||
this.arrays = $();
|
||||
|
||||
$('[data-grav-field="array"]').each((index, list) => this.addArray(list));
|
||||
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
}
|
||||
|
||||
addArray(list) {
|
||||
list = $(list);
|
||||
|
||||
list.find('[data-grav-array-type="container"]').each((index, container) => {
|
||||
container = $(container);
|
||||
if (container.data('array-sort') || container[0].hasAttribute('data-array-nosort')) { return; }
|
||||
|
||||
container.data('array-sort', new Sortable(container.get(0), {
|
||||
handle: '.fa-bars',
|
||||
animation: 150
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
actionInput(event) {
|
||||
let element = $(event.target);
|
||||
let type = element.data('grav-array-type');
|
||||
|
||||
this._setTemplate(element);
|
||||
|
||||
let template = element.data('array-template');
|
||||
let keyElement = type === 'key' ? element : element.siblings('[data-grav-array-type="key"]:first');
|
||||
let valueElement = type === 'value' ? element : element.siblings('[data-grav-array-type="value"]:first');
|
||||
|
||||
let escaped_name = !template.isValueOnly() ? keyElement.val() : this.getIndexFor(element);
|
||||
escaped_name = escaped_name.toString().replace(/\[/g, '%5B').replace(/]/g, '%5D');
|
||||
let name = `${template.getName()}[${escaped_name}]`;
|
||||
|
||||
if (!template.isValueOnly() && (!keyElement.val() && !valueElement.val())) {
|
||||
valueElement.attr('name', '');
|
||||
} else {
|
||||
// valueElement.attr('name', !valueElement.val() ? template.getName() : name);
|
||||
valueElement.attr('name', name);
|
||||
}
|
||||
|
||||
this.refreshNames(template);
|
||||
}
|
||||
|
||||
actionEvent(event) {
|
||||
event && event.preventDefault();
|
||||
let element = $(event.target);
|
||||
let action = element.data('grav-array-action');
|
||||
let container = element.parents('[data-grav-array-type="container"]');
|
||||
|
||||
this._setTemplate(element);
|
||||
|
||||
this[`${action}Action`](element);
|
||||
|
||||
let siblings = container.find('> div');
|
||||
container[siblings.length > 1 ? 'removeClass' : 'addClass']('one-child');
|
||||
}
|
||||
|
||||
addAction(element) {
|
||||
let template = element.data('array-template');
|
||||
let row = element.closest('[data-grav-array-type="row"]');
|
||||
|
||||
row.after(template.getNewRow());
|
||||
}
|
||||
|
||||
remAction(element) {
|
||||
let template = element.data('array-template');
|
||||
let row = element.closest('[data-grav-array-type="row"]');
|
||||
let isLast = !row.siblings().length;
|
||||
|
||||
if (isLast) {
|
||||
let newRow = $(template.getNewRow());
|
||||
row.after(newRow);
|
||||
newRow.find('[data-grav-array-type="value"]:last').attr('name', template.getName());
|
||||
}
|
||||
|
||||
row.remove();
|
||||
this.refreshNames(template);
|
||||
}
|
||||
|
||||
refreshNames(template) {
|
||||
if (!template.isValueOnly()) { return; }
|
||||
|
||||
let row = template.container.find('> div > [data-grav-array-type="row"]');
|
||||
let inputs = row.find('[name]:not([name=""])');
|
||||
|
||||
inputs.each((index, input) => {
|
||||
input = $(input);
|
||||
let name = input.attr('name');
|
||||
name = name.replace(/\[\d+\]$/, `[${index}]`);
|
||||
input.attr('name', name);
|
||||
});
|
||||
|
||||
if (!inputs.length) {
|
||||
row.find('[data-grav-array-type="value"]').attr('name', template.getName());
|
||||
}
|
||||
}
|
||||
|
||||
getIndexFor(element) {
|
||||
let template = element.data('array-template');
|
||||
let row = element.closest('[data-grav-array-type="row"]');
|
||||
|
||||
return template.container.find(`${template.isValueOnly() ? '> div ' : ''} > [data-grav-array-type="row"]`).index(row);
|
||||
}
|
||||
|
||||
_setTemplate(element) {
|
||||
if (!element.data('array-template')) {
|
||||
element.data('array-template', new Template(element.closest('[data-grav-array-name]')));
|
||||
}
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let arrays = $(target).find('[data-grav-field="array"]');
|
||||
if (!arrays.length) { return; }
|
||||
|
||||
arrays.each((index, list) => {
|
||||
list = $(list);
|
||||
if (!~this.arrays.index(list)) {
|
||||
this.addArray(list);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new ArrayField();
|
||||
368
config/www/user/plugins/form/app/fields/file.js
Normal file
368
config/www/user/plugins/form/app/fields/file.js
Normal file
@@ -0,0 +1,368 @@
|
||||
import $ from 'jquery';
|
||||
import Dropzone from 'dropzone';
|
||||
// import EXIF from 'exif-js';
|
||||
import {config, translations} from 'grav-form';
|
||||
|
||||
// translations
|
||||
const Dictionary = {
|
||||
dictCancelUpload: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD,
|
||||
dictCancelUploadConfirmation: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD_CONFIRMATION,
|
||||
dictDefaultMessage: translations.PLUGIN_FORM.DROPZONE_DEFAULT_MESSAGE,
|
||||
dictFallbackMessage: translations.PLUGIN_FORM.DROPZONE_FALLBACK_MESSAGE,
|
||||
dictFallbackText: translations.PLUGIN_FORM.DROPZONE_FALLBACK_TEXT,
|
||||
dictFileTooBig: translations.PLUGIN_FORM.DROPZONE_FILE_TOO_BIG,
|
||||
dictInvalidFileType: translations.PLUGIN_FORM.DROPZONE_INVALID_FILE_TYPE,
|
||||
dictMaxFilesExceeded: translations.PLUGIN_FORM.DROPZONE_MAX_FILES_EXCEEDED,
|
||||
dictRemoveFile: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE,
|
||||
dictRemoveFileConfirmation: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE_CONFIRMATION,
|
||||
dictResponseError: translations.PLUGIN_FORM.DROPZONE_RESPONSE_ERROR
|
||||
};
|
||||
|
||||
Dropzone.autoDiscover = false;
|
||||
|
||||
const DropzoneMediaConfig = {
|
||||
createImageThumbnails: {thumbnailWidth: 150},
|
||||
addRemoveLinks: false,
|
||||
dictDefaultMessage: Dictionary.dictDefaultMessage,
|
||||
dictRemoveFileConfirmation: Dictionary.dictRemoveFileConfirmation,
|
||||
previewTemplate: ''
|
||||
};
|
||||
|
||||
// window.EXIF = EXIF;
|
||||
|
||||
export default class FilesField {
|
||||
constructor({container = '.dropzone.files-upload', options = {}} = {}) {
|
||||
this.container = $(container);
|
||||
if (!this.container.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.urls = {};
|
||||
DropzoneMediaConfig.previewTemplate = $('#dropzone-template').html();
|
||||
this.options = Object.assign({}, Dictionary, DropzoneMediaConfig, {
|
||||
klass: this,
|
||||
url: this.container.data('file-url-add') || config.current_url,
|
||||
acceptedFiles: this.container.data('media-types'),
|
||||
init: this.initDropzone
|
||||
}, this.container.data('dropzone-options'), options);
|
||||
|
||||
this.dropzone = new Dropzone(container, this.options);
|
||||
this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
|
||||
this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
|
||||
this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
|
||||
this.dropzone.on('sending', this.onDropzoneSending.bind(this));
|
||||
this.dropzone.on('error', this.onDropzoneError.bind(this));
|
||||
}
|
||||
|
||||
initDropzone() {
|
||||
let files = this.options.klass.container.find('[data-file]');
|
||||
let dropzone = this;
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
files.each((index, file) => {
|
||||
file = $(file);
|
||||
let data = file.data('file');
|
||||
let mock = {
|
||||
name: data.name,
|
||||
size: data.size,
|
||||
type: data.type,
|
||||
status: Dropzone.ADDED,
|
||||
accepted: true,
|
||||
url: this.options.url,
|
||||
removeUrl: data.remove,
|
||||
data
|
||||
};
|
||||
|
||||
dropzone.files.push(mock);
|
||||
dropzone.options.addedfile.call(dropzone, mock);
|
||||
if (mock.type.match(/^image\//)) dropzone.options.thumbnail.call(dropzone, mock, data.path);
|
||||
|
||||
file.remove();
|
||||
});
|
||||
}
|
||||
|
||||
getURI() {
|
||||
return this.container.data('mediaUri') || '';
|
||||
}
|
||||
|
||||
onDropzoneSending(file, xhr, formData) {
|
||||
const form = this.container.closest('form');
|
||||
const unique_id = form.find('[name="__unique_form_id__"]');
|
||||
formData.append('__form-name__', form.find('[name="__form-name__"]').val());
|
||||
if (unique_id.length) {
|
||||
formData.append('__unique_form_id__', unique_id.val());
|
||||
}
|
||||
formData.append('__form-file-uploader__', 1);
|
||||
formData.append('name', this.options.dotNotation);
|
||||
formData.append('form-nonce', config.form_nonce);
|
||||
formData.append('task', 'filesupload');
|
||||
formData.append('uri', this.getURI());
|
||||
}
|
||||
|
||||
onDropzoneSuccess(file, response, xhr) {
|
||||
if (this.options.reloadPage) {
|
||||
global.location.reload();
|
||||
}
|
||||
|
||||
if (response && response.status === 'error') {
|
||||
return this.handleError({
|
||||
file,
|
||||
data: response,
|
||||
mode: 'removeFile',
|
||||
msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_UPLOAD} <strong>${file.name}</strong></p>
|
||||
<pre>${response.message}</pre>`
|
||||
});
|
||||
}
|
||||
|
||||
// store params for removing file from session before it gets saved
|
||||
if (response && response.session) {
|
||||
file.sessionParams = response.session;
|
||||
file.removeUrl = this.options.url;
|
||||
|
||||
// Touch field value to force a mutation detection
|
||||
const input = this.container.find('[name][type="hidden"]');
|
||||
const value = input.val();
|
||||
input.val(value + ' ');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onDropzoneComplete(file) {
|
||||
if (!file.accepted && !file.rejected) {
|
||||
let data = {
|
||||
status: 'error',
|
||||
message: `${translations.PLUGIN_FORM.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
|
||||
};
|
||||
|
||||
return this.handleError({
|
||||
file,
|
||||
data,
|
||||
mode: 'removeFile',
|
||||
msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_ADD} <strong>${file.name}</strong></p>
|
||||
<pre>${data.message}</pre>`
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.reloadPage) {
|
||||
global.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
onDropzoneRemovedFile(file, ...extra) {
|
||||
if (!file.accepted || file.rejected) {
|
||||
return;
|
||||
}
|
||||
const form = this.container.closest('form');
|
||||
const unique_id = form.find('[name="__unique_form_id__"]');
|
||||
let url = file.removeUrl || this.urls.delete || `${location.href}.json`;
|
||||
let path = (url || '').match(/path:(.*)\//);
|
||||
let data = new FormData();
|
||||
|
||||
data.append('filename', file.name);
|
||||
data.append('__form-name__', form.find('[name="__form-name__"]').val());
|
||||
data.append('name', this.options.dotNotation);
|
||||
data.append('form-nonce', config.form_nonce);
|
||||
data.append('uri', this.getURI());
|
||||
|
||||
if (file.sessionParams) {
|
||||
data.append('__form-file-remover__', '1');
|
||||
data.append('session', file.sessionParams);
|
||||
}
|
||||
|
||||
if (unique_id.length) {
|
||||
data.append('__unique_form_id__', unique_id.val());
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url,
|
||||
data,
|
||||
method: 'POST',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: () => {
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
path = global.atob(path[1]);
|
||||
let input = this.container.find('[name][type="hidden"]');
|
||||
let data = JSON.parse(input.val() || '{}');
|
||||
delete data[path];
|
||||
input.val(JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDropzoneError(file, response, xhr) {
|
||||
let message = xhr && response.error ? response.error.message : response;
|
||||
$(file.previewElement).find('[data-dz-errormessage]').html(message);
|
||||
|
||||
return this.handleError({
|
||||
file,
|
||||
data: {status: 'error'},
|
||||
msg: `<pre>${message}</pre>`
|
||||
});
|
||||
}
|
||||
|
||||
handleError(options) {
|
||||
const { file, data, msg } = options;
|
||||
const status = data && data.status;
|
||||
|
||||
if (status !== 'error' && status !== 'unauthorized') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = data && data.message ? data.message : (msg || translations.PLUGIN_FORM.FILEPOND_ERROR_FILESIZE);
|
||||
|
||||
if (file && this.dropzone) {
|
||||
file.accepted = false;
|
||||
file.status = Dropzone.ERROR;
|
||||
file.rejected = true;
|
||||
|
||||
const preview = $(file.previewElement);
|
||||
if (preview.length) {
|
||||
preview.addClass('dz-error');
|
||||
preview.find('[data-dz-errormessage]').html(message);
|
||||
}
|
||||
|
||||
// Remove the errored file so the user can try again.
|
||||
if (~this.dropzone.files.indexOf(file)) {
|
||||
setTimeout(() => {
|
||||
this.dropzone.removeFile.call(this.dropzone, file, { silent: true });
|
||||
this.dropzone._updateMaxFilesReachedClass();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
const field = this.container.closest('.form-field');
|
||||
if (field.length) {
|
||||
let errorBox = field.find('.form-errors');
|
||||
if (!errorBox.length) {
|
||||
errorBox = $('<div class="form-errors"></div>').appendTo(field);
|
||||
}
|
||||
|
||||
errorBox.html(`<p class="form-message"><i class="fa fa-exclamation-circle"></i> ${message}</p>`);
|
||||
} else if (typeof global.alert === 'function') {
|
||||
// Fall back to alert if no inline container is present.
|
||||
global.alert(message);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
export function UriToMarkdown(uri) {
|
||||
uri = uri.replace(/@3x|@2x|@1x/, '');
|
||||
uri = uri.replace(/\(/g, '%28');
|
||||
uri = uri.replace(/\)/g, '%29');
|
||||
|
||||
return uri.match(/\.(jpe?g|png|gif|svg)$/i) ? `` : `[${decodeURI(uri)}](${uri})`;
|
||||
}
|
||||
*/
|
||||
|
||||
let instances = [];
|
||||
let cache = $();
|
||||
const onAddedNodes = (event, target/* , record, instance */) => {
|
||||
let files = $(target).find('.dropzone.files-upload');
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
files.each((index, file) => {
|
||||
file = $(file);
|
||||
if (!~cache.index(file)) {
|
||||
addNode(file);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addNode = (container) => {
|
||||
container = $(container);
|
||||
let input = container.find('input[type="file"]');
|
||||
let settings = container.data('grav-file-settings') || {};
|
||||
|
||||
if (settings.accept && ~settings.accept.indexOf('*')) {
|
||||
settings.accept = [''];
|
||||
}
|
||||
|
||||
let options = {
|
||||
url: container.data('file-url-add') || (container.closest('form').attr('action') || config.current_url) + '.json',
|
||||
paramName: settings.paramName || 'file',
|
||||
dotNotation: settings.name || 'file',
|
||||
acceptedFiles: settings.accept ? settings.accept.join(',') : input.attr('accept') || container.data('media-types'),
|
||||
maxFilesize: settings.filesize || 256,
|
||||
maxFiles: settings.limit || null,
|
||||
resizeWidth: settings.resizeWidth || null,
|
||||
resizeHeight: settings.resizeHeight || null,
|
||||
resizeQuality: settings.resizeQuality || null,
|
||||
accept: function(file, done) {
|
||||
const resolution = settings.resolution;
|
||||
let error = '';
|
||||
if (!resolution) return done();
|
||||
|
||||
if ((this.options.maxFiles != null) && (this.getAcceptedFiles().length >= this.options.maxFiles)) {
|
||||
done(this.options.dictMaxFilesExceeded.replace('{{maxFiles}}', this.options.maxFiles));
|
||||
return this.emit('maxfilesexceeded', file);
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
if (resolution.min || (!(settings.resizeWidth || settings.resizeHeight) && resolution.max)) {
|
||||
reader.onload = function(event) {
|
||||
const image = new Image();
|
||||
image.src = event.target.result;
|
||||
image.onload = function() {
|
||||
if (resolution.min) {
|
||||
Object.keys(resolution.min).forEach((attr) => {
|
||||
if (this[attr] < resolution.min[attr]) {
|
||||
error += translations.PLUGIN_FORM.RESOLUTION_MIN.replace(/{{attr}}/g, attr).replace(/{{min}}/g, resolution.min[attr]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!(settings.resizeWidth || settings.resizeHeight)) {
|
||||
if (resolution.max) {
|
||||
Object.keys(resolution.max).forEach((attr) => {
|
||||
if (this[attr] > resolution.max[attr]) {
|
||||
error += translations.PLUGIN_FORM.RESOLUTION_MAX.replace(/{{attr}}/g, attr).replace(/{{max}}/g, resolution.max[attr]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
done(error);
|
||||
};
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
return done(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cache = cache.add(container);
|
||||
container = container[0];
|
||||
instances.push(new FilesField({container, options}));
|
||||
};
|
||||
|
||||
export let Instances = (() => {
|
||||
$(document).ready(() => {
|
||||
$('.dropzone.files-upload').each((i, container) => addNode(container));
|
||||
$('body').on('mutation._grav', onAddedNodes);
|
||||
});
|
||||
|
||||
return instances;
|
||||
})();
|
||||
|
||||
// Expose addNode function to global scope for XHR reinitialization and pipeline compatibility
|
||||
if (typeof window.GravForm === 'undefined') {
|
||||
window.GravForm = {};
|
||||
}
|
||||
window.GravForm.FilesField = {
|
||||
addNode,
|
||||
instances: Instances
|
||||
};
|
||||
94
config/www/user/plugins/form/app/fields/form.js
Normal file
94
config/www/user/plugins/form/app/fields/form.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
const attachToggleables = (form) => {
|
||||
form = $(form);
|
||||
let query = '[data-grav-field="toggleable"] input[type="checkbox"]';
|
||||
|
||||
form.on('change', query, (event) => {
|
||||
let toggle = $(event.target);
|
||||
let enabled = toggle.is(':checked');
|
||||
let parent = toggle.closest('.form-field');
|
||||
let label = parent.find('label.toggleable');
|
||||
let fields = parent.find('.form-data');
|
||||
let inputs = fields.find('input, select, textarea, button');
|
||||
|
||||
label.add(fields).css('opacity', enabled ? '' : 0.7);
|
||||
inputs.map((index, input) => {
|
||||
let isSelectize = input.selectize;
|
||||
input = $(input);
|
||||
|
||||
if (isSelectize) {
|
||||
isSelectize[enabled ? 'enable' : 'disable']();
|
||||
} else {
|
||||
input.prop('disabled', !enabled);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
form.find(query).trigger('change');
|
||||
};
|
||||
|
||||
const attachDisabledFields = (form) => {
|
||||
form = $(form);
|
||||
let prefix = '.form-field-toggleable .form-data';
|
||||
let query = [];
|
||||
|
||||
['input', 'select', 'label[for]', 'textarea', '.selectize-control'].forEach((item) => {
|
||||
query.push(`${prefix} ${item}`);
|
||||
});
|
||||
|
||||
form.on('mousedown', query.join(', '), (event) => {
|
||||
let input = $(event.target);
|
||||
let isFor = input.prop('for');
|
||||
let isSelectize = (input.hasClass('selectize-control') || input.parents('.selectize-control')).length;
|
||||
|
||||
if (isFor) { input = $(`[id="${isFor}"]`); }
|
||||
if (isSelectize) { input = input.closest('.selectize-control').siblings('select[name]'); }
|
||||
|
||||
if (!input.prop('disabled')) { return true; }
|
||||
|
||||
let toggle = input.closest('.form-field').find('[data-grav-field="toggleable"] input[type="checkbox"]');
|
||||
toggle.trigger('click');
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
const submitUncheckedFields = (forms) => {
|
||||
forms = $(forms);
|
||||
let submitted = false;
|
||||
forms.each((index, form) => {
|
||||
form = $(form);
|
||||
form.on('submit', () => {
|
||||
// workaround for MS Edge, submitting multiple forms at the same time
|
||||
if (submitted) { return false; }
|
||||
|
||||
let formId = form.attr('id');
|
||||
let unchecked = form.find('input[type="checkbox"]:not(:checked):not(:disabled)');
|
||||
let submit = form.find('[type="submit"]').add(`[form="${formId}"][type="submit"]`);
|
||||
|
||||
if (!unchecked.length) { return true; }
|
||||
|
||||
submit.addClass('pointer-events-disabled');
|
||||
unchecked.each((index, element) => {
|
||||
element = $(element);
|
||||
let name = element.prop('name');
|
||||
let fake = $(`<input type="hidden" name="${name}" value="0" />`);
|
||||
form.append(fake);
|
||||
});
|
||||
submitted = true;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
};
|
||||
*/
|
||||
|
||||
$(document).ready(() => {
|
||||
const forms = $('form').filter((form) => $(form).find('[name="__form-name__"]'));
|
||||
if (!forms.length) { return; }
|
||||
|
||||
forms.each((index, form) => {
|
||||
attachToggleables(form);
|
||||
attachDisabledFields(form);
|
||||
// submitUncheckedFields(form);
|
||||
});
|
||||
});
|
||||
7
config/www/user/plugins/form/app/fields/index.js
Normal file
7
config/www/user/plugins/form/app/fields/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import './form';
|
||||
import FileInstances from './file';
|
||||
import ArrayInstances from './array';
|
||||
import PageMedia, { Instance as PageMediaInstances } from './media';
|
||||
import './tabs';
|
||||
|
||||
export default { FileInstances, ArrayInstances, Media: { PageMedia, PageMediaInstances } };
|
||||
174
config/www/user/plugins/form/app/fields/media.js
Normal file
174
config/www/user/plugins/form/app/fields/media.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import $ from 'jquery';
|
||||
import FilesField from './file';
|
||||
import { config, translations } from 'grav-form';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
const template = `
|
||||
<div class="dz-preview dz-file-preview">
|
||||
<div class="dz-details">
|
||||
<div class="dz-filename"><span data-dz-name></span></div>
|
||||
<div class="dz-size" data-dz-size></div>
|
||||
<img data-dz-thumbnail />
|
||||
</div>
|
||||
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
|
||||
<div class="dz-success-mark"><span>✔</span></div>
|
||||
<div class="dz-error-mark"><span>✘</span></div>
|
||||
<div class="dz-error-message"><span data-dz-errormessage></span></div>
|
||||
<a class="dz-remove" title="${translations.PLUGIN_FORM.DELETE}" href="javascript:undefined;" data-dz-remove>${translations.PLUGIN_FORM.DELETE}</a>
|
||||
</div>`.trim();
|
||||
|
||||
export default class PageMedia extends FilesField {
|
||||
constructor({ container = '#grav-dropzone', options = {} } = {}) {
|
||||
const previewTemplate = $('#dropzone-media-template').html() || template;
|
||||
options = Object.assign(options, { previewTemplate });
|
||||
super({ container, options });
|
||||
if (!this.container.length) { return; }
|
||||
|
||||
this.urls = {
|
||||
fetch: `${this.container.data('media-url')}/task${config.param_sep}listmedia`,
|
||||
add: `${this.container.data('media-url')}/task${config.param_sep}addmedia`,
|
||||
delete: `${this.container.data('media-url')}/task${config.param_sep}delmedia`
|
||||
};
|
||||
|
||||
this.dropzone.options.url = this.urls.add;
|
||||
|
||||
if (typeof this.options.fetchMedia === 'undefined' || this.options.fetchMedia) {
|
||||
this.fetchMedia();
|
||||
}
|
||||
|
||||
const field = $(`[name="${this.container.data('dropzone-field')}"]`);
|
||||
|
||||
if (field.length) {
|
||||
this.sortable = new Sortable(this.container.get(0), {
|
||||
animation: 150,
|
||||
// forceFallback: true,
|
||||
setData: (dataTransfer, target) => {
|
||||
target = $(target);
|
||||
this.dropzone.disable();
|
||||
target.addClass('hide-backface');
|
||||
dataTransfer.effectAllowed = 'copy';
|
||||
},
|
||||
onSort: () => {
|
||||
let names = [];
|
||||
this.container.find('[data-dz-name]').each((index, file) => {
|
||||
file = $(file);
|
||||
const name = file.text().trim();
|
||||
names.push(name);
|
||||
});
|
||||
|
||||
field.val(names.join(','));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onDropzoneRemovedFile(file, ...extra) {
|
||||
if (!file.accepted || file.rejected) { return; }
|
||||
const form = this.container.closest('form');
|
||||
const unique_id = form.find('[name="__unique_form_id__"]');
|
||||
let url = file.removeUrl || this.urls.delete || `${location.href}.json`;
|
||||
let path = (url || '').match(/path:(.*)\//);
|
||||
let data = new FormData();
|
||||
|
||||
data.append('filename', file.name);
|
||||
data.append('__form-name__', form.find('[name="__form-name__"]').val());
|
||||
if (unique_id.length) {
|
||||
data.append('__unique_form_id__', unique_id.val());
|
||||
}
|
||||
data.append('name', this.options.dotNotation);
|
||||
data.append('form-nonce', config.form_nonce);
|
||||
|
||||
if (file.sessionParams) {
|
||||
data.append('__form-file-remover__', '1');
|
||||
data.append('session', file.sessionParams);
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url,
|
||||
data,
|
||||
method: 'POST',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: () => {
|
||||
if (!path) { return; }
|
||||
|
||||
path = global.atob(path[1]);
|
||||
let input = this.container.find('[name][type="hidden"]');
|
||||
let data = JSON.parse(input.val() || '{}');
|
||||
delete data[path];
|
||||
input.val(JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchMedia() {
|
||||
const order = this.container.closest('.form-field').find('[name="data[header][media_order]"]').val();
|
||||
const data = { order };
|
||||
let url = this.urls.fetch;
|
||||
|
||||
$.ajax({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
success: (response) => {
|
||||
if (typeof response === 'string' || response instanceof String) {
|
||||
return false;
|
||||
}
|
||||
|
||||
response = response.results;
|
||||
Object.keys(response).forEach((name) => {
|
||||
let data = response[name];
|
||||
let mock = { name, size: data.size, accepted: true, extras: data };
|
||||
|
||||
this.dropzone.files.push(mock);
|
||||
this.dropzone.options.addedfile.call(this.dropzone, mock);
|
||||
this.dropzone.options.thumbnail.call(this.dropzone, mock, data.url);
|
||||
});
|
||||
|
||||
this.container.find('.dz-preview').prop('draggable', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
request(url, { method: 'post', body }, (response) => {
|
||||
let results = response.results;
|
||||
|
||||
Object.keys(results).forEach((name) => {
|
||||
let data = results[name];
|
||||
let mock = { name, size: data.size, accepted: true, extras: data };
|
||||
|
||||
this.dropzone.files.push(mock);
|
||||
this.dropzone.options.addedfile.call(this.dropzone, mock);
|
||||
this.dropzone.options.thumbnail.call(this.dropzone, mock, data.url);
|
||||
});
|
||||
|
||||
this.container.find('.dz-preview').prop('draggable', 'true');
|
||||
});*/
|
||||
}
|
||||
|
||||
onDropzoneSending(file, xhr, formData) {
|
||||
/*
|
||||
// Cannot call super because Safari and IE API don't implement `delete`
|
||||
super.onDropzoneSending(file, xhr, formData);
|
||||
formData.delete('task');
|
||||
*/
|
||||
|
||||
formData.append('name', this.options.dotNotation);
|
||||
formData.append('admin-nonce', config.admin_nonce);
|
||||
}
|
||||
|
||||
onDropzoneComplete(file) {
|
||||
super.onDropzoneComplete(file);
|
||||
this.sortable.options.onSort();
|
||||
|
||||
// accepted
|
||||
$('.dz-preview').prop('draggable', 'true');
|
||||
}
|
||||
|
||||
// onDropzoneRemovedFile(file, ...extra) {
|
||||
// super.onDropzoneRemovedFile(file, ...extra);
|
||||
// this.sortable.options.onSort();
|
||||
// }
|
||||
}
|
||||
|
||||
export let Instance = new PageMedia();
|
||||
14
config/www/user/plugins/form/app/fields/tabs.js
Normal file
14
config/www/user/plugins/form/app/fields/tabs.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
$('body').on('touchstart click', '[data-tabid]', (event) => {
|
||||
event && event.stopPropagation();
|
||||
let target = $(event.currentTarget);
|
||||
|
||||
const panel = $(`[id="${target.data('tabid')}"]`);
|
||||
|
||||
target.siblings('[data-tabid]').removeClass('active');
|
||||
target.addClass('active');
|
||||
|
||||
panel.siblings('[id]').removeClass('active');
|
||||
panel.addClass('active');
|
||||
});
|
||||
4
config/www/user/plugins/form/app/main.js
Normal file
4
config/www/user/plugins/form/app/main.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import Instances from './fields';
|
||||
import './utils/keep-alive';
|
||||
|
||||
export { Instances };
|
||||
17
config/www/user/plugins/form/app/utils/keep-alive.js
Normal file
17
config/www/user/plugins/form/app/utils/keep-alive.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import $ from 'jquery';
|
||||
import {config} from 'grav-form';
|
||||
|
||||
const MAX_SAFE_DELAY = 2147483647;
|
||||
|
||||
$(document).ready(() => {
|
||||
const keepAlive = $('[data-grav-keepalive]');
|
||||
|
||||
if (keepAlive.length) {
|
||||
const timeout = config.session_timeout / 1.5 * 1000;
|
||||
setInterval(() => {
|
||||
$.ajax({
|
||||
url: `${config.base_url_relative}/task${config.param_sep}keep-alive`
|
||||
});
|
||||
}, Math.min(timeout, MAX_SAFE_DELAY));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Function to refresh a captcha image
|
||||
const refreshCaptchaImage = function(container) {
|
||||
const img = container.querySelector('img');
|
||||
if (!img) {
|
||||
console.warn('Cannot find captcha image in container');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the base URL and field ID
|
||||
const baseUrl = img.dataset.baseUrl || img.src.split('?')[0];
|
||||
const fieldId = img.dataset.fieldId || container.dataset.fieldId;
|
||||
|
||||
// Force reload by adding/updating timestamp and field ID
|
||||
const timestamp = new Date().getTime();
|
||||
let newUrl = baseUrl + '?t=' + timestamp;
|
||||
if (fieldId) {
|
||||
newUrl += '&field=' + fieldId;
|
||||
}
|
||||
img.src = newUrl;
|
||||
|
||||
// Also clear the input field if we can find it
|
||||
const formField = container.closest('.form-field');
|
||||
if (formField) {
|
||||
const input = formField.querySelector('input[type="text"]');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
// Try to focus the input
|
||||
try { input.focus(); } catch(e) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Function to set up click handlers for refresh buttons
|
||||
const setupRefreshButtons = function() {
|
||||
// Find all captcha containers
|
||||
const containers = document.querySelectorAll('[data-captcha-provider="basic-captcha"]');
|
||||
|
||||
containers.forEach(function(container) {
|
||||
// Find the refresh button within this container
|
||||
const button = container.querySelector('button');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any existing listeners (just in case)
|
||||
button.removeEventListener('click', handleRefreshClick);
|
||||
|
||||
// Add the click handler
|
||||
button.addEventListener('click', handleRefreshClick);
|
||||
});
|
||||
};
|
||||
|
||||
// Click handler function
|
||||
const handleRefreshClick = function(event) {
|
||||
// Prevent default behavior and stop propagation
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Find the container
|
||||
const container = this.closest('[data-captcha-provider="basic-captcha"]');
|
||||
if (!container) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Refresh the image
|
||||
refreshCaptchaImage(container);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Set up a mutation observer to handle dynamically added captchas
|
||||
const setupMutationObserver = function() {
|
||||
// Check if MutationObserver is available
|
||||
if (typeof MutationObserver === 'undefined') return;
|
||||
|
||||
// Create a mutation observer to watch for new captcha elements
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
let needsSetup = false;
|
||||
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
||||
// Check if any of the added nodes contain our captcha containers
|
||||
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
||||
const node = mutation.addedNodes[i];
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if this element has or contains captcha containers
|
||||
if (node.querySelector && (
|
||||
node.matches('[data-captcha-provider="basic-captcha"]') ||
|
||||
node.querySelector('[data-captcha-provider="basic-captcha"]')
|
||||
)) {
|
||||
needsSetup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (needsSetup) {
|
||||
setupRefreshButtons();
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing the document
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupRefreshButtons();
|
||||
setupMutationObserver();
|
||||
|
||||
// Also connect to XHR system if available (for best of both worlds)
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
window.GravFormXHR.captcha.register('basic-captcha', {
|
||||
reset: function(container, form) {
|
||||
refreshCaptchaImage(container);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
166
config/www/user/plugins/form/assets/captcha/recaptcha-handler.js
Normal file
166
config/www/user/plugins/form/assets/captcha/recaptcha-handler.js
Normal file
@@ -0,0 +1,166 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Register the handler with the form system when it's ready
|
||||
const registerRecaptchaHandler = function() {
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
window.GravFormXHR.captcha.register('recaptcha', {
|
||||
reset: function(container, form) {
|
||||
if (!form || !form.id) {
|
||||
console.warn('Cannot reset reCAPTCHA: form is invalid or missing ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const formId = form.id;
|
||||
console.log(`Attempting to reset reCAPTCHA for form: ${formId}`);
|
||||
|
||||
// First try the expected ID pattern from the Twig template
|
||||
const recaptchaId = `g-recaptcha-${formId}`;
|
||||
// We need to look more flexibly for the container
|
||||
let widgetContainer = document.getElementById(recaptchaId);
|
||||
|
||||
// If not found by ID, look for the div inside the captcha provider container
|
||||
if (!widgetContainer) {
|
||||
// Try to find it inside the captcha provider container
|
||||
widgetContainer = container.querySelector('.g-recaptcha');
|
||||
|
||||
if (!widgetContainer) {
|
||||
// If that fails, look more broadly in the form
|
||||
widgetContainer = form.querySelector('.g-recaptcha');
|
||||
|
||||
if (!widgetContainer) {
|
||||
// Last resort - create a new container if needed
|
||||
console.warn(`reCAPTCHA container #${recaptchaId} not found. Creating a new one.`);
|
||||
widgetContainer = document.createElement('div');
|
||||
widgetContainer.id = recaptchaId;
|
||||
widgetContainer.className = 'g-recaptcha';
|
||||
container.appendChild(widgetContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found reCAPTCHA container for form: ${formId}`);
|
||||
|
||||
// Get configuration from data attributes
|
||||
const parentContainer = container.closest('[data-captcha-provider="recaptcha"]');
|
||||
if (!parentContainer) {
|
||||
console.warn('Cannot find reCAPTCHA parent container with data-captcha-provider attribute.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sitekey = parentContainer.dataset.sitekey;
|
||||
const version = parentContainer.dataset.version || '2-checkbox';
|
||||
const isV3 = version.startsWith('3');
|
||||
const isInvisible = version === '2-invisible';
|
||||
|
||||
if (!sitekey) {
|
||||
console.warn('Cannot reinitialize reCAPTCHA - missing sitekey attribute');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Re-rendering reCAPTCHA widget for form: ${formId}, version: ${version}`);
|
||||
|
||||
// Handle V3 reCAPTCHA differently
|
||||
if (isV3) {
|
||||
try {
|
||||
// For v3, we don't need to reset anything visible, just make sure we have the API
|
||||
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.execute === 'function') {
|
||||
// Create a new execution context for the form
|
||||
const actionName = `form_${formId}`;
|
||||
const tokenInput = form.querySelector('input[name="token"]') ||
|
||||
form.querySelector('input[name="data[token]"]');
|
||||
const actionInput = form.querySelector('input[name="action"]') ||
|
||||
form.querySelector('input[name="data[action]"]');
|
||||
|
||||
if (tokenInput && actionInput) {
|
||||
// Clear previous token
|
||||
tokenInput.value = '';
|
||||
|
||||
// Set the action name
|
||||
actionInput.value = actionName;
|
||||
|
||||
console.log(`reCAPTCHA v3 ready for execution on form: ${formId}`);
|
||||
} else {
|
||||
console.warn(`Cannot find token or action inputs for reCAPTCHA v3 in form: ${formId}`);
|
||||
}
|
||||
} else {
|
||||
console.warn('reCAPTCHA v3 API not properly loaded.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error setting up reCAPTCHA v3: ${e.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For v2, handle visible widget reset
|
||||
// Clear the container to ensure fresh rendering
|
||||
widgetContainer.innerHTML = '';
|
||||
|
||||
// Check if reCAPTCHA API is available
|
||||
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.render === 'function') {
|
||||
try {
|
||||
// Render with a slight delay to ensure DOM is settled
|
||||
setTimeout(() => {
|
||||
grecaptcha.render(widgetContainer.id || widgetContainer, {
|
||||
'sitekey': sitekey,
|
||||
'theme': parentContainer.dataset.theme || 'light',
|
||||
'size': isInvisible ? 'invisible' : 'normal',
|
||||
'callback': function(token) {
|
||||
console.log(`reCAPTCHA verification completed for form: ${formId}`);
|
||||
|
||||
// If it's invisible reCAPTCHA, submit the form automatically
|
||||
if (isInvisible && window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
|
||||
window.GravFormXHR.submit(form);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`Successfully rendered reCAPTCHA for form: ${formId}`);
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
console.error(`Error rendering reCAPTCHA widget: ${e.message}`);
|
||||
widgetContainer.innerHTML = '<p style="color:red;">Error initializing reCAPTCHA.</p>';
|
||||
}
|
||||
} else {
|
||||
console.warn('reCAPTCHA API not available. Attempting to reload...');
|
||||
|
||||
// Remove existing script if any
|
||||
const existingScript = document.querySelector('script[src*="google.com/recaptcha/api.js"]');
|
||||
if (existingScript) {
|
||||
existingScript.parentNode.removeChild(existingScript);
|
||||
}
|
||||
|
||||
// Create new script element
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://www.google.com/recaptcha/api.js${isV3 ? '?render=' + sitekey : ''}`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = function() {
|
||||
console.log('reCAPTCHA API loaded, retrying widget render...');
|
||||
setTimeout(() => {
|
||||
const retryContainer = document.querySelector(`[data-captcha-provider="recaptcha"]`);
|
||||
if (retryContainer && form) {
|
||||
window.GravFormXHR.captcha.getProvider('recaptcha').reset(retryContainer, form);
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('reCAPTCHA XHR handler registered successfully');
|
||||
} else {
|
||||
console.error('GravFormXHR.captcha not found. Make sure the Form plugin is loaded correctly.');
|
||||
}
|
||||
};
|
||||
|
||||
// Try to register the handler immediately if GravFormXHR is already available
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
registerRecaptchaHandler();
|
||||
} else {
|
||||
// Otherwise, wait for the DOM to be fully loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Give a small delay to ensure GravFormXHR is initialized
|
||||
setTimeout(registerRecaptchaHandler, 100);
|
||||
});
|
||||
}
|
||||
})();
|
||||
121
config/www/user/plugins/form/assets/captcha/turnstile-handler.js
Normal file
121
config/www/user/plugins/form/assets/captcha/turnstile-handler.js
Normal file
@@ -0,0 +1,121 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Register the handler with the form system when it's ready
|
||||
const registerTurnstileHandler = function() {
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
window.GravFormXHR.captcha.register('turnstile', {
|
||||
reset: function(container, form) {
|
||||
const formId = form.id;
|
||||
const containerId = `cf-turnstile-${formId}`;
|
||||
const widgetContainer = document.getElementById(containerId);
|
||||
|
||||
if (!widgetContainer) {
|
||||
console.warn(`Turnstile container #${containerId} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get configuration from data attributes
|
||||
const parentContainer = container.closest('[data-captcha-provider="turnstile"]');
|
||||
const sitekey = parentContainer ? parentContainer.dataset.sitekey : null;
|
||||
|
||||
if (!sitekey) {
|
||||
console.warn('Cannot reinitialize Turnstile - missing sitekey attribute');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the container to ensure fresh rendering
|
||||
widgetContainer.innerHTML = '';
|
||||
|
||||
console.log(`Re-rendering Turnstile widget for form: ${formId}`);
|
||||
|
||||
// Check if Turnstile API is available
|
||||
if (typeof window.turnstile !== 'undefined') {
|
||||
try {
|
||||
// Reset any existing widgets
|
||||
try {
|
||||
window.turnstile.reset(containerId);
|
||||
} catch (e) {
|
||||
// Ignore reset errors, we'll re-render anyway
|
||||
}
|
||||
|
||||
// Render with a slight delay to ensure DOM is settled
|
||||
setTimeout(() => {
|
||||
window.turnstile.render(`#${containerId}`, {
|
||||
sitekey: sitekey,
|
||||
theme: parentContainer ? (parentContainer.dataset.theme || 'light') : 'light',
|
||||
callback: function(token) {
|
||||
console.log(`Turnstile verification completed for form: ${formId} with token:`, token.substring(0, 10) + '...');
|
||||
|
||||
// Create or update hidden input for token
|
||||
let tokenInput = form.querySelector('input[name="cf-turnstile-response"]');
|
||||
if (!tokenInput) {
|
||||
console.log('Creating new hidden input for Turnstile token');
|
||||
tokenInput = document.createElement('input');
|
||||
tokenInput.type = 'hidden';
|
||||
tokenInput.name = 'cf-turnstile-response';
|
||||
form.appendChild(tokenInput);
|
||||
} else {
|
||||
console.log('Updating existing hidden input for Turnstile token');
|
||||
}
|
||||
tokenInput.value = token;
|
||||
|
||||
// Also add a debug attribute
|
||||
form.setAttribute('data-turnstile-verified', 'true');
|
||||
},
|
||||
'expired-callback': function() {
|
||||
console.log(`Turnstile token expired for form: ${formId}`);
|
||||
},
|
||||
'error-callback': function(error) {
|
||||
console.error(`Turnstile error for form ${formId}: ${error}`);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
console.error(`Error rendering Turnstile widget: ${e.message}`);
|
||||
widgetContainer.innerHTML = '<p style="color:red;">Error initializing Turnstile.</p>';
|
||||
}
|
||||
} else {
|
||||
console.warn('Turnstile API not available. Attempting to reload...');
|
||||
|
||||
// Remove existing script if any
|
||||
const existingScript = document.querySelector('script[src*="challenges.cloudflare.com/turnstile/v0/api.js"]');
|
||||
if (existingScript) {
|
||||
existingScript.parentNode.removeChild(existingScript);
|
||||
}
|
||||
|
||||
// Create new script element
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = function() {
|
||||
console.log('Turnstile API loaded, retrying widget render...');
|
||||
setTimeout(() => {
|
||||
const retryContainer = document.querySelector('[data-captcha-provider="turnstile"]');
|
||||
if (retryContainer && form) {
|
||||
window.GravFormXHR.captcha.getProvider('turnstile').reset(retryContainer, form);
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('Turnstile XHR handler registered successfully');
|
||||
} else {
|
||||
console.error('GravFormXHR.captcha not found. Make sure the Form plugin is loaded correctly.');
|
||||
}
|
||||
};
|
||||
|
||||
// Try to register the handler immediately if GravFormXHR is already available
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
registerTurnstileHandler();
|
||||
} else {
|
||||
// Otherwise, wait for the DOM to be fully loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Give a small delay to ensure GravFormXHR is initialized
|
||||
setTimeout(registerTurnstileHandler, 100);
|
||||
});
|
||||
}
|
||||
})();
|
||||
311
config/www/user/plugins/form/assets/dropzone-reinit.js
Normal file
311
config/www/user/plugins/form/assets/dropzone-reinit.js
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* Direct Dropzone Initialization for XHR Forms
|
||||
*
|
||||
* This script directly targets Form plugin's Dropzone initialization mechanisms
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Enable debugging logs
|
||||
const DEBUG = false;
|
||||
|
||||
// Helper function for logging
|
||||
function log(message, type = 'log') {
|
||||
if (!DEBUG) return;
|
||||
|
||||
const prefix = '[Dropzone Direct Init]';
|
||||
|
||||
if (type === 'error') {
|
||||
console.error(prefix, message);
|
||||
} else if (type === 'warn') {
|
||||
console.warn(prefix, message);
|
||||
} else {
|
||||
console.log(prefix, message);
|
||||
}
|
||||
}
|
||||
|
||||
// Flag to prevent multiple initializations
|
||||
let isInitializing = false;
|
||||
|
||||
// Function to directly initialize Dropzone
|
||||
function initializeDropzone(element) {
|
||||
if (isInitializing) {
|
||||
log('Initialization already in progress, skipping');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!element || element.classList.contains('dz-clickable')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log('Starting direct Dropzone initialization for element:', element);
|
||||
isInitializing = true;
|
||||
|
||||
// First, let's try to find the FilesField constructor in the global scope
|
||||
if (typeof FilesField === 'function') {
|
||||
log('Found FilesField constructor, trying direct instantiation');
|
||||
|
||||
try {
|
||||
new FilesField({
|
||||
container: element,
|
||||
options: {}
|
||||
});
|
||||
|
||||
log('Successfully initialized Dropzone using FilesField constructor');
|
||||
isInitializing = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(`Error using FilesField constructor: ${e.message}`, 'error');
|
||||
// Continue with other methods
|
||||
}
|
||||
}
|
||||
|
||||
// Second approach: Look for the Form plugin's initialization code in the page
|
||||
const dropzoneInit = findFunctionOnWindow('addNode') ||
|
||||
window.addNode ||
|
||||
findFunctionOnWindow('initDropzone');
|
||||
|
||||
if (dropzoneInit) {
|
||||
log('Found Form plugin initialization function, calling it directly');
|
||||
|
||||
try {
|
||||
dropzoneInit(element);
|
||||
log('Successfully called Form plugin initialization function');
|
||||
isInitializing = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(`Error calling Form plugin initialization function: ${e.message}`, 'error');
|
||||
// Continue with other methods
|
||||
}
|
||||
}
|
||||
|
||||
// Third approach: Try to invoke Dropzone directly if it's globally available
|
||||
if (typeof Dropzone === 'function') {
|
||||
log('Found global Dropzone constructor, trying direct instantiation');
|
||||
|
||||
try {
|
||||
// Extract settings from the element
|
||||
const settingsAttr = element.getAttribute('data-grav-file-settings');
|
||||
if (!settingsAttr) {
|
||||
log('No settings found for element', 'warn');
|
||||
isInitializing = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
const settings = JSON.parse(settingsAttr);
|
||||
const optionsAttr = element.getAttribute('data-dropzone-options');
|
||||
const options = optionsAttr ? JSON.parse(optionsAttr) : {};
|
||||
|
||||
// Configure Dropzone options
|
||||
const dropzoneOptions = {
|
||||
url: element.getAttribute('data-file-url-add') || window.location.href,
|
||||
maxFiles: settings.limit || null,
|
||||
maxFilesize: settings.filesize || 10,
|
||||
acceptedFiles: settings.accept ? settings.accept.join(',') : null
|
||||
};
|
||||
|
||||
// Merge with any provided options
|
||||
Object.assign(dropzoneOptions, options);
|
||||
|
||||
// Create new Dropzone instance
|
||||
new Dropzone(element, dropzoneOptions);
|
||||
|
||||
log('Successfully initialized Dropzone using global constructor');
|
||||
isInitializing = false;
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(`Error using global Dropzone constructor: ${e.message}`, 'error');
|
||||
// Continue to final approach
|
||||
}
|
||||
}
|
||||
|
||||
// Final approach: Force reloading of Form plugin's JavaScript
|
||||
log('Attempting to force reload Form plugin JavaScript');
|
||||
|
||||
// Look for Form plugin's JS files
|
||||
const formVendorScript = document.querySelector('script[src*="form.vendor.js"]');
|
||||
const formScript = document.querySelector('script[src*="form.min.js"]');
|
||||
|
||||
if (formVendorScript || formScript) {
|
||||
log('Found Form plugin scripts, attempting to reload them');
|
||||
|
||||
// Create new script elements
|
||||
if (formVendorScript) {
|
||||
const newVendorScript = document.createElement('script');
|
||||
newVendorScript.src = formVendorScript.src.split('?')[0] + '?t=' + new Date().getTime();
|
||||
newVendorScript.async = true;
|
||||
newVendorScript.onload = function() {
|
||||
log('Reloaded Form vendor script');
|
||||
|
||||
// Trigger event after script loads
|
||||
setTimeout(function() {
|
||||
const event = new CustomEvent('mutation._grav', {
|
||||
detail: { target: element }
|
||||
});
|
||||
document.body.dispatchEvent(event);
|
||||
}, 100);
|
||||
};
|
||||
document.head.appendChild(newVendorScript);
|
||||
}
|
||||
|
||||
if (formScript) {
|
||||
const newFormScript = document.createElement('script');
|
||||
newFormScript.src = formScript.src.split('?')[0] + '?t=' + new Date().getTime();
|
||||
newFormScript.async = true;
|
||||
newFormScript.onload = function() {
|
||||
log('Reloaded Form script');
|
||||
|
||||
// Trigger event after script loads
|
||||
setTimeout(function() {
|
||||
const event = new CustomEvent('mutation._grav', {
|
||||
detail: { target: element }
|
||||
});
|
||||
document.body.dispatchEvent(event);
|
||||
}, 100);
|
||||
};
|
||||
document.head.appendChild(newFormScript);
|
||||
}
|
||||
}
|
||||
|
||||
// As a final resort, trigger the mutation event
|
||||
log('Triggering mutation._grav event as final resort');
|
||||
const event = new CustomEvent('mutation._grav', {
|
||||
detail: { target: element }
|
||||
});
|
||||
document.body.dispatchEvent(event);
|
||||
|
||||
isInitializing = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper function to find a function on the window object by name pattern
|
||||
function findFunctionOnWindow(pattern) {
|
||||
for (const key in window) {
|
||||
if (typeof window[key] === 'function' && key.includes(pattern)) {
|
||||
return window[key];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function to check all Dropzone elements
|
||||
function checkAllDropzones() {
|
||||
const dropzones = document.querySelectorAll('.dropzone.files-upload:not(.dz-clickable)');
|
||||
|
||||
if (dropzones.length === 0) {
|
||||
log('No uninitialized Dropzone elements found');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Found ${dropzones.length} uninitialized Dropzone elements`);
|
||||
|
||||
// Try to initialize each one
|
||||
dropzones.forEach(function(element) {
|
||||
initializeDropzone(element);
|
||||
});
|
||||
}
|
||||
|
||||
// Hook into form submission to reinitialize after XHR updates
|
||||
function setupFormSubmissionHook() {
|
||||
// First check if the XHR submit function is available
|
||||
if (window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
|
||||
log('Found GravFormXHR.submit, attaching hook');
|
||||
|
||||
// Store the original function
|
||||
const originalSubmit = window.GravFormXHR.submit;
|
||||
|
||||
// Override it with our version
|
||||
window.GravFormXHR.submit = function(form) {
|
||||
log(`XHR form submission detected for form: ${form?.id || 'unknown'}`);
|
||||
|
||||
// Call the original function
|
||||
const result = originalSubmit.apply(this, arguments);
|
||||
|
||||
// Set up checks for after the submission completes
|
||||
[500, 1000, 2000, 3000].forEach(function(delay) {
|
||||
setTimeout(checkAllDropzones, delay);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
log('Successfully hooked into GravFormXHR.submit');
|
||||
}
|
||||
|
||||
// Also add a direct event listener for standard form submissions
|
||||
document.addEventListener('submit', function(event) {
|
||||
if (event.target.tagName === 'FORM') {
|
||||
log(`Standard form submission detected for form: ${event.target.id || 'unknown'}`);
|
||||
|
||||
// Schedule checks after submission
|
||||
[1000, 2000, 3000].forEach(function(delay) {
|
||||
setTimeout(checkAllDropzones, delay);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
log('Form submission hooks set up');
|
||||
}
|
||||
|
||||
// Monitor for AJAX responses
|
||||
function setupAjaxMonitoring() {
|
||||
if (window.jQuery) {
|
||||
log('Setting up jQuery AJAX response monitoring');
|
||||
|
||||
jQuery(document).ajaxComplete(function(event, xhr, settings) {
|
||||
log('AJAX request completed, checking if form-related');
|
||||
|
||||
// Check if this looks like a form request
|
||||
const url = settings.url || '';
|
||||
if (url.includes('form') ||
|
||||
url.includes('task=') ||
|
||||
url.includes('file-upload') ||
|
||||
url.includes('file-uploader')) {
|
||||
|
||||
log('Form-related AJAX request detected, will check for Dropzones');
|
||||
|
||||
// Schedule checks with delays
|
||||
[300, 800, 1500].forEach(function(delay) {
|
||||
setTimeout(checkAllDropzones, delay);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
log('jQuery AJAX monitoring set up');
|
||||
}
|
||||
}
|
||||
|
||||
// Create global function for manual reinitialization
|
||||
window.reinitializeDropzones = function() {
|
||||
log('Manual reinitialization triggered');
|
||||
checkAllDropzones();
|
||||
return 'Reinitialization check triggered. See console for details.';
|
||||
};
|
||||
|
||||
// Main initialization function
|
||||
function initialize() {
|
||||
log('Initializing Dropzone direct initialization system');
|
||||
|
||||
// Set up submission hook
|
||||
setupFormSubmissionHook();
|
||||
|
||||
// Set up AJAX monitoring
|
||||
setupAjaxMonitoring();
|
||||
|
||||
// Do an initial check for any uninitialized Dropzones
|
||||
setTimeout(checkAllDropzones, 500);
|
||||
|
||||
log('Initialization complete. Use window.reinitializeDropzones() for manual reinitialization.');
|
||||
}
|
||||
|
||||
// Start when the DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Delay to allow other scripts to load
|
||||
setTimeout(initialize, 100);
|
||||
});
|
||||
} else {
|
||||
// DOM already loaded, delay slightly
|
||||
setTimeout(initialize, 100);
|
||||
}
|
||||
})();
|
||||
1
config/www/user/plugins/form/assets/dropzone.min.css
vendored
Normal file
1
config/www/user/plugins/form/assets/dropzone.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
662
config/www/user/plugins/form/assets/filepond-handler.js
Normal file
662
config/www/user/plugins/form/assets/filepond-handler.js
Normal file
@@ -0,0 +1,662 @@
|
||||
/**
|
||||
* Unified Grav Form FilePond Handler
|
||||
*
|
||||
* This script initializes and configures FilePond instances for file uploads
|
||||
* within Grav forms. It works with both normal and XHR form submissions.
|
||||
* It also handles reinitializing FilePond instances after XHR form submissions.
|
||||
*/
|
||||
|
||||
// Immediately-Invoked Function Expression for scoping
|
||||
(function () {
|
||||
// Check if script already loaded
|
||||
if (window.gravFilepondHandlerLoaded) {
|
||||
console.log('FilePond unified handler already loaded, skipping.');
|
||||
return;
|
||||
}
|
||||
window.gravFilepondHandlerLoaded = true;
|
||||
|
||||
// Debugging - set to false for production
|
||||
const debug = true;
|
||||
|
||||
// Helper function for logging
|
||||
function log(message, type = 'log') {
|
||||
if (!debug && type !== 'error') return;
|
||||
|
||||
const prefix = '[FilePond Handler]';
|
||||
if (type === 'error') {
|
||||
console.error(prefix, message);
|
||||
} else if (type === 'warn') {
|
||||
console.warn(prefix, message);
|
||||
} else {
|
||||
console.log(prefix, message);
|
||||
}
|
||||
}
|
||||
|
||||
// Track FilePond instances with their configuration
|
||||
const pondInstances = new Map();
|
||||
|
||||
// Get translations from global object if available
|
||||
const translations = window.GravForm?.translations?.PLUGIN_FORM || {
|
||||
FILEPOND_REMOVE_FILE: 'Remove file',
|
||||
FILEPOND_REMOVE_FILE_CONFIRMATION: 'Are you sure you want to remove this file?',
|
||||
FILEPOND_CANCEL_UPLOAD: 'Cancel upload',
|
||||
FILEPOND_ERROR_FILESIZE: 'File is too large',
|
||||
FILEPOND_ERROR_FILETYPE: 'Invalid file type'
|
||||
};
|
||||
|
||||
// Track initialization state
|
||||
let initialized = false;
|
||||
|
||||
/**
|
||||
* Get standard FilePond configuration for an element
|
||||
* This is used for both initial setup and reinit after XHR
|
||||
* @param {HTMLElement} element - The file input element
|
||||
* @param {HTMLElement} container - The container element
|
||||
* @returns {Object} Configuration object for FilePond
|
||||
*/
|
||||
function getFilepondConfig(element, container) {
|
||||
if (!container) {
|
||||
log('Container not provided for config extraction', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the field is required - this is correct location
|
||||
const isRequired = element.hasAttribute('required') ||
|
||||
container.hasAttribute('required') ||
|
||||
container.getAttribute('data-required') === 'true';
|
||||
|
||||
// Then, add this code to remove the required attribute from the actual input
|
||||
// to prevent browser validation errors, but keep track of the requirement
|
||||
if (isRequired) {
|
||||
// Store the required state on the container for our custom validation
|
||||
container.setAttribute('data-required', 'true');
|
||||
// Remove the required attribute from the input to avoid browser validation errors
|
||||
element.removeAttribute('required');
|
||||
}
|
||||
|
||||
try {
|
||||
// Get settings from data attributes
|
||||
const settingsAttr = container.getAttribute('data-grav-file-settings');
|
||||
if (!settingsAttr) {
|
||||
log('No file settings found for FilePond element', 'warn');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse settings
|
||||
let settings;
|
||||
try {
|
||||
settings = JSON.parse(settingsAttr);
|
||||
log('Parsed settings:', settings);
|
||||
} catch (e) {
|
||||
log(`Error parsing file settings: ${e.message}`, 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse FilePond options
|
||||
const filepondOptionsAttr = container.getAttribute('data-filepond-options') || '{}';
|
||||
let filepondOptions;
|
||||
try {
|
||||
filepondOptions = JSON.parse(filepondOptionsAttr);
|
||||
log('Parsed FilePond options:', filepondOptions);
|
||||
} catch (e) {
|
||||
log(`Error parsing FilePond options: ${e.message}`, 'error');
|
||||
filepondOptions = {};
|
||||
}
|
||||
|
||||
// Get URLs for upload and remove
|
||||
const uploadUrl = container.getAttribute('data-file-url-add');
|
||||
const removeUrl = container.getAttribute('data-file-url-remove');
|
||||
|
||||
if (!uploadUrl) {
|
||||
log('Upload URL not found for FilePond element', 'warn');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse previously uploaded files
|
||||
const existingFiles = [];
|
||||
const fileDataElements = container.querySelectorAll('[data-file]');
|
||||
log(`Found ${fileDataElements.length} existing file data elements`);
|
||||
|
||||
fileDataElements.forEach(fileData => {
|
||||
try {
|
||||
const fileAttr = fileData.getAttribute('data-file');
|
||||
log('File data attribute:', fileAttr);
|
||||
|
||||
const fileJson = JSON.parse(fileAttr);
|
||||
|
||||
if (fileJson && fileJson.name) {
|
||||
existingFiles.push({
|
||||
source: fileJson.name,
|
||||
options: {
|
||||
type: 'local',
|
||||
file: {
|
||||
name: fileJson.name,
|
||||
size: fileJson.size,
|
||||
type: fileJson.type
|
||||
},
|
||||
metadata: {
|
||||
poster: fileJson.thumb_url || fileJson.path
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Error parsing file data: ${e.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
log('Existing files:', existingFiles);
|
||||
|
||||
// Get form elements for Grav integration
|
||||
const fieldName = container.getAttribute('data-file-field-name');
|
||||
const form = element.closest('form');
|
||||
const formNameInput = form ? form.querySelector('[name="__form-name__"]') : document.querySelector('[name="__form-name__"]');
|
||||
const formIdInput = form ? form.querySelector('[name="__unique_form_id__"]') : document.querySelector('[name="__unique_form_id__"]');
|
||||
const formNonceInput = form ? form.querySelector('[name="form-nonce"]') : document.querySelector('[name="form-nonce"]');
|
||||
|
||||
if (!formNameInput || !formIdInput || !formNonceInput) {
|
||||
log('Missing required form inputs for proper Grav integration', 'warn');
|
||||
}
|
||||
|
||||
// Configure FilePond
|
||||
const options = {
|
||||
// Core settings
|
||||
name: settings.paramName,
|
||||
maxFiles: settings.limit || null,
|
||||
maxFileSize: `${settings.filesize}MB`,
|
||||
acceptedFileTypes: settings.accept,
|
||||
files: existingFiles,
|
||||
|
||||
// Server configuration - modified for Grav
|
||||
server: {
|
||||
process: {
|
||||
url: uploadUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
ondata: (formData) => {
|
||||
// Safety check - ensure formData is valid
|
||||
if (!formData) {
|
||||
console.error('FormData is undefined in ondata');
|
||||
return new FormData(); // Return empty FormData as fallback
|
||||
}
|
||||
|
||||
// Add all required Grav form fields
|
||||
if (formNameInput) formData.append('__form-name__', formNameInput.value);
|
||||
if (formIdInput) formData.append('__unique_form_id__', formIdInput.value);
|
||||
formData.append('__form-file-uploader__', '1');
|
||||
if (formNonceInput) formData.append('form-nonce', formNonceInput.value);
|
||||
formData.append('task', 'filesupload');
|
||||
|
||||
// Use fieldName from the outer scope
|
||||
if (fieldName) {
|
||||
formData.append('name', fieldName);
|
||||
} else {
|
||||
console.error('Field name is undefined, falling back to default');
|
||||
formData.append('name', 'files');
|
||||
}
|
||||
|
||||
// Add URI if needed
|
||||
const uriInput = document.querySelector('[name="uri"]');
|
||||
if (uriInput) {
|
||||
formData.append('uri', uriInput.value);
|
||||
}
|
||||
|
||||
// Note: Don't try to append file here, FilePond will do that based on the name parameter
|
||||
// Just return the modified formData
|
||||
log('Prepared form data for Grav upload');
|
||||
return formData;
|
||||
}
|
||||
},
|
||||
revert: removeUrl ? {
|
||||
url: removeUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
ondata: (formData, file) => {
|
||||
// Add all required Grav form fields
|
||||
if (formNameInput) formData.append('__form-name__', formNameInput.value);
|
||||
if (formIdInput) formData.append('__unique_form_id__', formIdInput.value);
|
||||
formData.append('__form-file-remover__', '1');
|
||||
if (formNonceInput) formData.append('form-nonce', formNonceInput.value);
|
||||
formData.append('name', fieldName);
|
||||
|
||||
// Add filename
|
||||
formData.append('filename', file.filename);
|
||||
|
||||
log('Prepared form data for file removal');
|
||||
return formData;
|
||||
}
|
||||
} : null
|
||||
},
|
||||
|
||||
// Image Transform settings - both FilePond native settings and our custom ones
|
||||
// Native settings
|
||||
allowImagePreview: true,
|
||||
allowImageResize: true,
|
||||
allowImageTransform: true,
|
||||
imagePreviewHeight: filepondOptions.imagePreviewHeight || 256,
|
||||
|
||||
// Transform settings
|
||||
imageTransformOutputMimeType: filepondOptions.imageTransformOutputMimeType || 'image/jpeg',
|
||||
imageTransformOutputQuality: filepondOptions.imageTransformOutputQuality || settings.resizeQuality || 90,
|
||||
imageTransformOutputStripImageHead: filepondOptions.imageTransformOutputStripImageHead !== false,
|
||||
|
||||
// Resize settings
|
||||
imageResizeTargetWidth: filepondOptions.imageResizeTargetWidth || settings.resizeWidth || null,
|
||||
imageResizeTargetHeight: filepondOptions.imageResizeTargetHeight || settings.resizeHeight || null,
|
||||
imageResizeMode: filepondOptions.imageResizeMode || 'cover',
|
||||
imageResizeUpscale: filepondOptions.imageResizeUpscale || false,
|
||||
|
||||
// Crop settings
|
||||
allowImageCrop: filepondOptions.allowImageCrop || false,
|
||||
imageCropAspectRatio: filepondOptions.imageCropAspectRatio || null,
|
||||
|
||||
// Labels and translations
|
||||
labelIdle: filepondOptions.labelIdle || '<span class="filepond--label-action">Browse</span> or drop files',
|
||||
labelFileTypeNotAllowed: translations.FILEPOND_ERROR_FILETYPE || 'Invalid file type',
|
||||
labelFileSizeNotAllowed: translations.FILEPOND_ERROR_FILESIZE || 'File is too large',
|
||||
labelFileLoading: 'Loading',
|
||||
labelFileProcessing: 'Uploading',
|
||||
labelFileProcessingComplete: 'Upload complete',
|
||||
labelFileProcessingAborted: 'Upload cancelled',
|
||||
labelTapToCancel: translations.FILEPOND_CANCEL_UPLOAD || 'Cancel upload',
|
||||
labelTapToRetry: 'Retry',
|
||||
labelTapToUndo: 'Undo',
|
||||
labelButtonRemoveItem: translations.FILEPOND_REMOVE_FILE || 'Remove',
|
||||
|
||||
// Style settings
|
||||
stylePanelLayout: filepondOptions.stylePanelLayout || 'compact',
|
||||
styleLoadIndicatorPosition: filepondOptions.styleLoadIndicatorPosition || 'center bottom',
|
||||
styleProgressIndicatorPosition: filepondOptions.styleProgressIndicatorPosition || 'center bottom',
|
||||
styleButtonRemoveItemPosition: filepondOptions.styleButtonRemoveItemPosition || 'right',
|
||||
|
||||
// Override with any remaining user-provided options
|
||||
...filepondOptions
|
||||
};
|
||||
|
||||
log('Prepared FilePond configuration:', options);
|
||||
|
||||
return options;
|
||||
} catch (e) {
|
||||
log(`Error creating FilePond configuration: ${e.message}`, 'error');
|
||||
console.error(e); // Full error in console
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a single FilePond instance
|
||||
* @param {HTMLElement} element - The file input element to initialize
|
||||
* @returns {FilePond|null} The created FilePond instance, or null if creation failed
|
||||
*/
|
||||
function initializeSingleFilePond(element) {
|
||||
const container = element.closest('.filepond-root');
|
||||
|
||||
if (!container) {
|
||||
log('FilePond container not found for input element', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't initialize twice
|
||||
if (container.classList.contains('filepond--hopper') || container.querySelector('.filepond--hopper')) {
|
||||
log('FilePond already initialized for this element, skipping');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the element ID or create a unique one for tracking
|
||||
const elementId = element.id || `filepond-${Math.random().toString(36).substring(2, 15)}`;
|
||||
|
||||
// Get configuration
|
||||
const config = getFilepondConfig(element, container);
|
||||
if (!config) {
|
||||
log('Failed to get configuration, cannot initialize FilePond', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
log(`Initializing FilePond element ${elementId} with config`, config);
|
||||
|
||||
try {
|
||||
// Create FilePond instance
|
||||
const pond = FilePond.create(element, config);
|
||||
log(`FilePond instance created successfully for element ${elementId}`);
|
||||
|
||||
// Store the instance and its configuration for potential reinit
|
||||
pondInstances.set(elementId, {
|
||||
instance: pond,
|
||||
config: config,
|
||||
container: container
|
||||
});
|
||||
|
||||
// Add a reference to the element for easier lookup
|
||||
element.filepondId = elementId;
|
||||
container.filepondId = elementId;
|
||||
|
||||
// Handle form submission to ensure files are processed before submit
|
||||
const form = element.closest('form');
|
||||
if (form && !form._filepond_handler_attached) {
|
||||
form._filepond_handler_attached = true;
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
// Check for all FilePond instances in this form
|
||||
const formPonds = Array.from(pondInstances.values())
|
||||
.filter(info => info.instance && info.container.closest('form') === form);
|
||||
|
||||
const processingFiles = formPonds.reduce((total, info) => {
|
||||
return total + info.instance.getFiles().filter(file =>
|
||||
file.status === FilePond.FileStatus.PROCESSING_QUEUED ||
|
||||
file.status === FilePond.FileStatus.PROCESSING
|
||||
).length;
|
||||
}, 0);
|
||||
|
||||
if (processingFiles > 0) {
|
||||
e.preventDefault();
|
||||
alert('Please wait for all files to finish uploading before submitting the form.');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return pond;
|
||||
} catch (e) {
|
||||
log(`Error creating FilePond instance: ${e.message}`, 'error');
|
||||
console.error(e); // Full error in console
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main FilePond initialization function
|
||||
* This will find and initialize all uninitialized FilePond elements
|
||||
*/
|
||||
function initializeFilePond() {
|
||||
log('Starting FilePond initialization');
|
||||
|
||||
// Make sure we have the libraries loaded
|
||||
if (typeof window.FilePond === 'undefined') {
|
||||
log('FilePond library not found. Will retry in 500ms...', 'warn');
|
||||
setTimeout(initializeFilePond, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
log('FilePond library found, continuing initialization');
|
||||
|
||||
// Register plugins if available
|
||||
try {
|
||||
if (window.FilePondPluginFileValidateSize) {
|
||||
FilePond.registerPlugin(FilePondPluginFileValidateSize);
|
||||
log('Registered FileValidateSize plugin');
|
||||
}
|
||||
|
||||
if (window.FilePondPluginFileValidateType) {
|
||||
FilePond.registerPlugin(FilePondPluginFileValidateType);
|
||||
log('Registered FileValidateType plugin');
|
||||
}
|
||||
|
||||
if (window.FilePondPluginImagePreview) {
|
||||
FilePond.registerPlugin(FilePondPluginImagePreview);
|
||||
log('Registered ImagePreview plugin');
|
||||
}
|
||||
|
||||
if (window.FilePondPluginImageResize) {
|
||||
FilePond.registerPlugin(FilePondPluginImageResize);
|
||||
log('Registered ImageResize plugin');
|
||||
}
|
||||
|
||||
if (window.FilePondPluginImageTransform) {
|
||||
FilePond.registerPlugin(FilePondPluginImageTransform);
|
||||
log('Registered ImageTransform plugin');
|
||||
}
|
||||
} catch (e) {
|
||||
log(`Error registering plugins: ${e.message}`, 'error');
|
||||
}
|
||||
|
||||
// Find all FilePond elements
|
||||
const elements = document.querySelectorAll('.filepond-root input[type="file"]:not(.filepond--browser)');
|
||||
|
||||
if (elements.length === 0) {
|
||||
log('No FilePond form elements found on the page');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Found ${elements.length} FilePond element(s)`);
|
||||
|
||||
// Process each FilePond element
|
||||
elements.forEach((element, index) => {
|
||||
log(`Initializing FilePond element #${index + 1}`);
|
||||
initializeSingleFilePond(element);
|
||||
});
|
||||
|
||||
initialized = true;
|
||||
log('FilePond initialization complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize a specific FilePond instance
|
||||
* @param {HTMLElement} container - The FilePond container element
|
||||
* @returns {FilePond|null} The reinitialized FilePond instance, or null if reinitialization failed
|
||||
*/
|
||||
function reinitializeSingleFilePond(container) {
|
||||
if (!container) {
|
||||
log('No container provided for reinitialization', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this is a FilePond container
|
||||
if (!container.classList.contains('filepond-root')) {
|
||||
log('Container is not a FilePond container', 'warn');
|
||||
return null;
|
||||
}
|
||||
|
||||
log(`Reinitializing FilePond container: ${container.id || 'unnamed'}`);
|
||||
|
||||
// If already initialized, destroy first
|
||||
if (container.classList.contains('filepond--hopper') || container.querySelector('.filepond--hopper')) {
|
||||
log('Container already has an active FilePond instance, destroying it first');
|
||||
|
||||
// Try to find and destroy through our internal tracking
|
||||
const elementId = container.filepondId;
|
||||
if (elementId && pondInstances.has(elementId)) {
|
||||
const info = pondInstances.get(elementId);
|
||||
if (info.instance) {
|
||||
log(`Destroying tracked FilePond instance for element ${elementId}`);
|
||||
info.instance.destroy();
|
||||
pondInstances.delete(elementId);
|
||||
}
|
||||
} else {
|
||||
// Fallback: Try to find via child element with class
|
||||
const pondElement = container.querySelector('.filepond--root');
|
||||
if (pondElement && pondElement._pond) {
|
||||
log('Destroying FilePond instance via DOM reference');
|
||||
pondElement._pond.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for the file input
|
||||
const input = container.querySelector('input[type="file"]:not(.filepond--browser)');
|
||||
if (!input) {
|
||||
log('No file input found in container for reinitialization', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a new instance
|
||||
return initializeSingleFilePond(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinitialize all FilePond instances
|
||||
* This is used after XHR form submissions
|
||||
*/
|
||||
function reinitializeFilePond() {
|
||||
log('Reinitializing all FilePond instances');
|
||||
|
||||
// Find all FilePond containers
|
||||
const containers = document.querySelectorAll('.filepond-root');
|
||||
if (containers.length === 0) {
|
||||
log('No FilePond containers found for reinitialization');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`Found ${containers.length} FilePond container(s) for reinitialization`);
|
||||
|
||||
// Process each container
|
||||
containers.forEach((container, index) => {
|
||||
log(`Reinitializing FilePond container #${index + 1}`);
|
||||
reinitializeSingleFilePond(container);
|
||||
});
|
||||
|
||||
log('FilePond reinitialization complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to support XHR form interaction
|
||||
* This hooks into the GravFormXHR system if available
|
||||
*/
|
||||
function setupXHRIntegration() {
|
||||
// Only run if GravFormXHR is available
|
||||
if (window.GravFormXHR) {
|
||||
log('Setting up XHR integration for FilePond');
|
||||
|
||||
// Store original submit function
|
||||
const originalSubmit = window.GravFormXHR.submit;
|
||||
|
||||
// Override to handle FilePond files
|
||||
window.GravFormXHR.submit = function (form) {
|
||||
if (!form) {
|
||||
return originalSubmit.apply(this, arguments);
|
||||
}
|
||||
|
||||
// Check for any FilePond instances in the form
|
||||
let hasPendingUploads = false;
|
||||
|
||||
// First check via our tracking
|
||||
Array.from(pondInstances.values()).forEach(info => {
|
||||
if (info.container.closest('form') === form) {
|
||||
const processingFiles = info.instance.getFiles().filter(file =>
|
||||
file.status === FilePond.FileStatus.PROCESSING_QUEUED ||
|
||||
file.status === FilePond.FileStatus.PROCESSING);
|
||||
|
||||
if (processingFiles.length > 0) {
|
||||
hasPendingUploads = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fallback check for any untracked instances
|
||||
if (!hasPendingUploads) {
|
||||
const filepondContainers = form.querySelectorAll('.filepond-root');
|
||||
filepondContainers.forEach(container => {
|
||||
const pondElement = container.querySelector('.filepond--root');
|
||||
if (pondElement && pondElement._pond) {
|
||||
const pond = pondElement._pond;
|
||||
const processingFiles = pond.getFiles().filter(file =>
|
||||
file.status === FilePond.FileStatus.PROCESSING_QUEUED ||
|
||||
file.status === FilePond.FileStatus.PROCESSING);
|
||||
|
||||
if (processingFiles.length > 0) {
|
||||
hasPendingUploads = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (hasPendingUploads) {
|
||||
alert('Please wait for all files to finish uploading before submitting the form.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Call the original submit function
|
||||
return originalSubmit.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Set up listeners for form updates
|
||||
document.addEventListener('grav-form-updated', function (e) {
|
||||
log('Detected form update event, reinitializing FilePond instances');
|
||||
setTimeout(reinitializeFilePond, 100);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup mutation observer to detect dynamically added FilePond elements
|
||||
*/
|
||||
function setupMutationObserver() {
|
||||
if (window.MutationObserver) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldCheck = false;
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType === 1) {
|
||||
if (node.classList && node.classList.contains('filepond-root') ||
|
||||
node.querySelector && node.querySelector('.filepond-root')) {
|
||||
shouldCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCheck) break;
|
||||
}
|
||||
|
||||
if (shouldCheck) {
|
||||
log('DOM changes detected that might include FilePond elements');
|
||||
// Delay to ensure DOM is fully updated
|
||||
setTimeout(initializeFilePond, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
log('MutationObserver set up for FilePond elements');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize when DOM is ready
|
||||
*/
|
||||
function domReadyInit() {
|
||||
log('DOM ready, initializing FilePond');
|
||||
initializeFilePond();
|
||||
setupXHRIntegration();
|
||||
setupMutationObserver();
|
||||
}
|
||||
|
||||
// Handle different document ready states
|
||||
if (document.readyState === 'loading') {
|
||||
log('Document still loading, adding DOMContentLoaded listener');
|
||||
document.addEventListener('DOMContentLoaded', domReadyInit);
|
||||
} else {
|
||||
log('Document already loaded, initializing now');
|
||||
setTimeout(domReadyInit, 0);
|
||||
}
|
||||
|
||||
// Also support initialization via window load event as a fallback
|
||||
window.addEventListener('load', function () {
|
||||
log('Window load event fired');
|
||||
if (!initialized) {
|
||||
log('FilePond not yet initialized, initializing now');
|
||||
initializeFilePond();
|
||||
}
|
||||
});
|
||||
|
||||
// Expose functions to global scope for external usage
|
||||
window.GravFilePond = {
|
||||
initialize: initializeFilePond,
|
||||
reinitialize: reinitializeFilePond,
|
||||
reinitializeContainer: reinitializeSingleFilePond,
|
||||
getInstances: () => Array.from(pondInstances.values()).map(info => info.instance)
|
||||
};
|
||||
|
||||
// Log initialization start
|
||||
log('FilePond unified handler script loaded and ready');
|
||||
})();
|
||||
141
config/www/user/plugins/form/assets/filepond-reinit.js
Normal file
141
config/www/user/plugins/form/assets/filepond-reinit.js
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* FilePond Direct Fix - Emergency fix for XHR forms
|
||||
*/
|
||||
(function() {
|
||||
// Directly attempt to initialize uninitialized FilePond elements
|
||||
// without relying on any existing logic
|
||||
|
||||
console.log('FilePond Direct Fix loaded');
|
||||
|
||||
// Function to directly create FilePond instances
|
||||
function initializeFilePondElements() {
|
||||
console.log('Direct FilePond initialization attempt');
|
||||
|
||||
// Find uninitialized FilePond elements
|
||||
const elements = document.querySelectorAll('.filepond-root:not(.filepond--hopper)');
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${elements.length} uninitialized FilePond elements`);
|
||||
|
||||
// Process each element
|
||||
elements.forEach((element, index) => {
|
||||
const input = element.querySelector('input[type="file"]:not(.filepond--browser)');
|
||||
if (!input) {
|
||||
console.log(`Element #${index + 1}: No suitable file input found`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Element #${index + 1}: Found file input:`, input);
|
||||
|
||||
// Get settings
|
||||
let settings = {};
|
||||
try {
|
||||
const settingsAttr = element.getAttribute('data-grav-file-settings');
|
||||
if (settingsAttr) {
|
||||
settings = JSON.parse(settingsAttr);
|
||||
console.log('Parsed settings:', settings);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse settings:', e);
|
||||
}
|
||||
|
||||
// Get URLS
|
||||
const uploadUrl = element.getAttribute('data-file-url-add');
|
||||
const removeUrl = element.getAttribute('data-file-url-remove');
|
||||
|
||||
console.log('Upload URL:', uploadUrl);
|
||||
console.log('Remove URL:', removeUrl);
|
||||
|
||||
try {
|
||||
// Create FilePond instance directly
|
||||
const pond = FilePond.create(input);
|
||||
|
||||
// Apply minimal configuration to make uploads work
|
||||
if (pond) {
|
||||
console.log(`Successfully created FilePond on element #${index + 1}`);
|
||||
|
||||
// Basic configuration to make it functional
|
||||
pond.setOptions({
|
||||
name: settings.paramName || input.name || 'files',
|
||||
server: {
|
||||
process: uploadUrl,
|
||||
revert: removeUrl
|
||||
},
|
||||
// Transform options
|
||||
imageTransformOutputMimeType: 'image/jpeg',
|
||||
imageTransformOutputQuality: settings.resizeQuality || 90,
|
||||
imageTransformOutputStripImageHead: true,
|
||||
// Resize options
|
||||
imageResizeTargetWidth: settings.resizeWidth || null,
|
||||
imageResizeTargetHeight: settings.resizeHeight || null,
|
||||
imageResizeMode: 'cover',
|
||||
imageResizeUpscale: false
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to create FilePond on element #${index + 1}:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Monitor form submissions and DOM changes
|
||||
function setupMonitoring() {
|
||||
// Create MutationObserver to watch for DOM changes
|
||||
if (window.MutationObserver) {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldCheck = false;
|
||||
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType === 1) {
|
||||
if (node.classList && node.classList.contains('filepond-root') ||
|
||||
node.querySelector && node.querySelector('.filepond-root')) {
|
||||
shouldCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCheck) break;
|
||||
}
|
||||
|
||||
if (shouldCheck) {
|
||||
console.log('DOM changes detected that might include FilePond elements');
|
||||
// Delay to ensure DOM is fully updated
|
||||
setTimeout(initializeFilePondElements, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
console.log('MutationObserver set up for FilePond elements');
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the emergency fix
|
||||
function init() {
|
||||
// Set up monitoring
|
||||
setupMonitoring();
|
||||
|
||||
// Expose global function for manual reinit
|
||||
window.directFilePondInit = initializeFilePondElements;
|
||||
|
||||
// Initial check
|
||||
setTimeout(initializeFilePondElements, 500);
|
||||
}
|
||||
|
||||
// Start when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
setTimeout(init, 0);
|
||||
}
|
||||
})();
|
||||
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-file-validate-size.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-file-validate-size.min.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/*!
|
||||
* FilePondPluginFileValidateSize 2.2.8
|
||||
* Licensed under MIT, https://opensource.org/licenses/MIT/
|
||||
* Please visit https://pqina.nl/filepond/ for details.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
!function(e,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(e=e||self).FilePondPluginFileValidateSize=i()}(this,function(){"use strict";var e=function(e){var i=e.addFilter,E=e.utils,l=E.Type,_=E.replaceInString,n=E.toNaturalFileSize;return i("ALLOW_HOPPER_ITEM",function(e,i){var E=i.query;if(!E("GET_ALLOW_FILE_SIZE_VALIDATION"))return!0;var l=E("GET_MAX_FILE_SIZE");if(null!==l&&e.size>l)return!1;var _=E("GET_MIN_FILE_SIZE");return!(null!==_&&e.size<_)}),i("LOAD_FILE",function(e,i){var E=i.query;return new Promise(function(i,l){if(!E("GET_ALLOW_FILE_SIZE_VALIDATION"))return i(e);var I=E("GET_FILE_VALIDATE_SIZE_FILTER");if(I&&!I(e))return i(e);var t=E("GET_MAX_FILE_SIZE");if(null!==t&&e.size>t)l({status:{main:E("GET_LABEL_MAX_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MAX_FILE_SIZE"),{filesize:n(t,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});else{var L=E("GET_MIN_FILE_SIZE");if(null!==L&&e.size<L)l({status:{main:E("GET_LABEL_MIN_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MIN_FILE_SIZE"),{filesize:n(L,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});else{var a=E("GET_MAX_TOTAL_FILE_SIZE");if(null!==a)if(E("GET_ACTIVE_ITEMS").reduce(function(e,i){return e+i.fileSize},0)>a)return void l({status:{main:E("GET_LABEL_MAX_TOTAL_FILE_SIZE_EXCEEDED"),sub:_(E("GET_LABEL_MAX_TOTAL_FILE_SIZE"),{filesize:n(a,".",E("GET_FILE_SIZE_BASE"),E("GET_FILE_SIZE_LABELS",E))})}});i(e)}}})}),{options:{allowFileSizeValidation:[!0,l.BOOLEAN],maxFileSize:[null,l.INT],minFileSize:[null,l.INT],maxTotalFileSize:[null,l.INT],fileValidateSizeFilter:[null,l.FUNCTION],labelMinFileSizeExceeded:["File is too small",l.STRING],labelMinFileSize:["Minimum file size is {filesize}",l.STRING],labelMaxFileSizeExceeded:["File is too large",l.STRING],labelMaxFileSize:["Maximum file size is {filesize}",l.STRING],labelMaxTotalFileSizeExceeded:["Maximum total size exceeded",l.STRING],labelMaxTotalFileSize:["Maximum total file size is {filesize}",l.STRING]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e});
|
||||
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-file-validate-type.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-file-validate-type.min.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/*!
|
||||
* FilePondPluginFileValidateType 1.2.9
|
||||
* Licensed under MIT, https://opensource.org/licenses/MIT/
|
||||
* Please visit https://pqina.nl/filepond/ for details.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).FilePondPluginFileValidateType=t()}(this,function(){"use strict";var e=function(e){var t=e.addFilter,n=e.utils,i=n.Type,T=n.isString,E=n.replaceInString,l=n.guesstimateMimeType,o=n.getExtensionFromFilename,r=n.getFilenameFromURL,u=function(e,t){return e.some(function(e){return/\*$/.test(e)?(n=e,(/^[^/]+/.exec(t)||[]).pop()===n.slice(0,-2)):e===t;var n})},a=function(e,t,n){if(0===t.length)return!0;var i=function(e){var t="";if(T(e)){var n=r(e),i=o(n);i&&(t=l(i))}else t=e.type;return t}(e);return n?new Promise(function(T,E){n(e,i).then(function(e){u(t,e)?T():E()}).catch(E)}):u(t,i)};return t("SET_ATTRIBUTE_TO_OPTION_MAP",function(e){return Object.assign(e,{accept:"acceptedFileTypes"})}),t("ALLOW_HOPPER_ITEM",function(e,t){var n=t.query;return!n("GET_ALLOW_FILE_TYPE_VALIDATION")||a(e,n("GET_ACCEPTED_FILE_TYPES"))}),t("LOAD_FILE",function(e,t){var n=t.query;return new Promise(function(t,i){if(n("GET_ALLOW_FILE_TYPE_VALIDATION")){var T=n("GET_ACCEPTED_FILE_TYPES"),l=n("GET_FILE_VALIDATE_TYPE_DETECT_TYPE"),o=a(e,T,l),r=function(){var e,t=T.map((e=n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES_MAP"),function(t){return null!==e[t]&&(e[t]||t)})).filter(function(e){return!1!==e}),l=t.filter(function(e,n){return t.indexOf(e)===n});i({status:{main:n("GET_LABEL_FILE_TYPE_NOT_ALLOWED"),sub:E(n("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES"),{allTypes:l.join(", "),allButLastType:l.slice(0,-1).join(", "),lastType:l[l.length-1]})}})};if("boolean"==typeof o)return o?t(e):r();o.then(function(){t(e)}).catch(r)}else t(e)})}),{options:{allowFileTypeValidation:[!0,i.BOOLEAN],acceptedFileTypes:[[],i.ARRAY],labelFileTypeNotAllowed:["File is of invalid type",i.STRING],fileValidateTypeLabelExpectedTypes:["Expects {allButLastType} or {lastType}",i.STRING],fileValidateTypeLabelExpectedTypesMap:[{},i.OBJECT],fileValidateTypeDetectType:[null,i.FUNCTION]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e});
|
||||
8
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-preview.min.css
vendored
Normal file
8
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-preview.min.css
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* FilePondPluginImagePreview 4.6.12
|
||||
* Licensed under MIT, https://opensource.org/licenses/MIT/
|
||||
* Please visit https://pqina.nl/filepond/ for details.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
.filepond--image-preview-markup{position:absolute;left:0;top:0}.filepond--image-preview-wrapper{z-index:2}.filepond--image-preview-overlay{display:block;position:absolute;left:0;top:0;width:100%;min-height:5rem;max-height:7rem;margin:0;opacity:0;z-index:2;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.filepond--image-preview-overlay svg{width:100%;height:auto;color:inherit;max-height:inherit}.filepond--image-preview-overlay-idle{mix-blend-mode:multiply;color:rgba(40,40,40,.85)}.filepond--image-preview-overlay-success{mix-blend-mode:normal;color:#369763}.filepond--image-preview-overlay-failure{mix-blend-mode:normal;color:#c44e47}@supports (-webkit-marquee-repetition:infinite) and ((-o-object-fit:fill) or (object-fit:fill)){.filepond--image-preview-overlay-idle{mix-blend-mode:normal}}.filepond--image-preview-wrapper{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:absolute;left:0;top:0;right:0;height:100%;margin:0;border-radius:.45em;overflow:hidden;background:rgba(0,0,0,.01)}.filepond--image-preview{position:absolute;left:0;top:0;z-index:1;display:flex;align-items:center;height:100%;width:100%;pointer-events:none;background:#222;will-change:transform,opacity}.filepond--image-clip{position:relative;overflow:hidden;margin:0 auto}.filepond--image-clip[data-transparency-indicator=grid] canvas,.filepond--image-clip[data-transparency-indicator=grid] img{background-color:#fff;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg' fill='%23eee'%3E%3Cpath d='M0 0h50v50H0M50 50h50v50H50'/%3E%3C/svg%3E");background-size:1.25em 1.25em}.filepond--image-bitmap,.filepond--image-vector{position:absolute;left:0;top:0;will-change:transform}.filepond--root[data-style-panel-layout~=integrated] .filepond--image-preview-wrapper{border-radius:0}.filepond--root[data-style-panel-layout~=integrated] .filepond--image-preview{height:100%;display:flex;justify-content:center;align-items:center}.filepond--root[data-style-panel-layout~=circle] .filepond--image-preview-wrapper{border-radius:99999rem}.filepond--root[data-style-panel-layout~=circle] .filepond--image-preview-overlay{top:auto;bottom:0;-webkit-transform:scaleY(-1);transform:scaleY(-1)}.filepond--root[data-style-panel-layout~=circle] .filepond--file .filepond--file-action-button[data-align*=bottom]:not([data-align*=center]){margin-bottom:.325em}.filepond--root[data-style-panel-layout~=circle] .filepond--file [data-align*=left]{left:calc(50% - 3em)}.filepond--root[data-style-panel-layout~=circle] .filepond--file [data-align*=right]{right:calc(50% - 3em)}.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=left],.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=right]{margin-bottom:.5125em}.filepond--root[data-style-panel-layout~=circle] .filepond--progress-indicator[data-align*=bottom][data-align*=center]{margin-top:0;margin-bottom:.1875em;margin-left:.1875em}
|
||||
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-preview.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-preview.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-resize.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-resize.min.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/*!
|
||||
* FilePondPluginImageResize 2.0.10
|
||||
* Licensed under MIT, https://opensource.org/licenses/MIT/
|
||||
* Please visit https://pqina.nl/filepond/ for details.
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).FilePondPluginImageResize=t()}(this,function(){"use strict";var e=function(e){var t=e.addFilter,i=e.utils.Type;return t("DID_LOAD_ITEM",function(e,t){var i=t.query;return new Promise(function(t,n){var r=e.file;if(!function(e){return/^image/.test(e.type)}(r)||!i("GET_ALLOW_IMAGE_RESIZE"))return t(e);var u=i("GET_IMAGE_RESIZE_MODE"),o=i("GET_IMAGE_RESIZE_TARGET_WIDTH"),a=i("GET_IMAGE_RESIZE_TARGET_HEIGHT"),l=i("GET_IMAGE_RESIZE_UPSCALE");if(null===o&&null===a)return t(e);var d,f,E,s=null===o?a:o,c=null===a?s:a,I=URL.createObjectURL(r);d=I,f=function(i){if(URL.revokeObjectURL(I),!i)return t(e);var n=i.width,r=i.height,o=(e.getMetadata("exif")||{}).orientation||-1;if(o>=5&&o<=8){var a=[r,n];n=a[0],r=a[1]}if(n===s&&r===c)return t(e);if(!l)if("cover"===u){if(n<=s||r<=c)return t(e)}else if(n<=s&&r<=s)return t(e);e.setMetadata("resize",{mode:u,upscale:l,size:{width:s,height:c}}),t(e)},(E=new Image).onload=function(){var e=E.naturalWidth,t=E.naturalHeight;E=null,f({width:e,height:t})},E.onerror=function(){return f(null)},E.src=d})}),{options:{allowImageResize:[!0,i.BOOLEAN],imageResizeMode:["cover",i.STRING],imageResizeUpscale:[!0,i.BOOLEAN],imageResizeTargetWidth:[null,i.INT],imageResizeTargetHeight:[null,i.INT]}}};return"undefined"!=typeof window&&void 0!==window.document&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:e})),e});
|
||||
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-transform.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond-plugin-image-transform.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
config/www/user/plugins/form/assets/filepond/filepond.min.css
vendored
Normal file
8
config/www/user/plugins/form/assets/filepond/filepond.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
config/www/user/plugins/form/assets/filepond/filepond.min.js
vendored
Normal file
9
config/www/user/plugins/form/assets/filepond/filepond.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
config/www/user/plugins/form/assets/form-styles.css
Normal file
1
config/www/user/plugins/form/assets/form-styles.css
Normal file
@@ -0,0 +1 @@
|
||||
.form-group.has-errors{background:rgba(255,0,0,.05);border:1px solid rgba(255,0,0,.2);border-radius:3px;margin:0 -5px;padding:0 5px}.form-errors{color:#b52b27}.form-honeybear{display:none;position:absolute !important;height:1px;width:1px;overflow:hidden;clip-path:rect(0px, 1px, 1px, 0px)}.form-errors p{margin:0}.form-input-file input{display:none}.form-input-file .dz-default.dz-message{position:absolute;text-align:center;left:0;right:0;top:50%;transform:translateY(-50%);margin:0}.form-input-file.dropzone{position:relative;min-height:70px;border-radius:3px;margin-bottom:.85rem;border:2px dashed #ccc;color:#aaa;padding:.5rem}.form-input-file.dropzone .dz-preview{margin:.5rem}.form-input-file.dropzone .dz-preview:hover{z-index:2}.form-input-file.dropzone .dz-preview .dz-image img{margin:0}.form-input-file.dropzone .dz-preview .dz-remove{font-size:16px;position:absolute;top:3px;right:3px;display:inline-flex;height:20px;width:20px;background-color:red;justify-content:center;align-items:center;color:#fff;font-weight:bold;border-radius:50%;cursor:pointer;z-index:20}.form-input-file.dropzone .dz-preview .dz-remove:hover{background-color:darkred;text-decoration:none}.form-input-file.dropzone .dz-preview .dz-error-message{min-width:140px;width:auto}.form-input-file.dropzone .dz-preview .dz-image,.form-input-file.dropzone .dz-preview.dz-file-preview .dz-image{border-radius:3px;z-index:1}.filepond--root.form-input{min-height:7rem;height:auto;overflow:hidden;border:0}.form-tabs .tabs-nav{display:flex;padding-top:1px;margin-bottom:-1px}.form-tabs .tabs-nav a{flex:1;transition:color .5s ease,background .5s ease;cursor:pointer;text-align:center;padding:10px;display:flex;align-items:center;justify-content:center;border-bottom:1px solid #ccc;border-radius:5px 5px 0 0}.form-tabs .tabs-nav a.active{border:1px solid #ccc;border-bottom:1px solid rgba(0,0,0,0);margin:0 -1px}.form-tabs .tabs-nav a.active span{color:#000}.form-tabs .tabs-nav span{display:inline-block;line-height:1.1}.form-tabs.subtle .tabs-nav{margin-right:0 !important}.form-tabs .tabs-content .tab__content{display:none;padding-top:2rem}.form-tabs .tabs-content .tab__content.active{display:block}.checkboxes{display:inline-block}.checkboxes label{display:inline;cursor:pointer;position:relative;padding:0 0 0 20px;margin-right:15px}.checkboxes label:before{content:"";display:inline-block;width:20px;height:20px;left:0;margin-top:0;margin-right:10px;position:absolute;border-radius:3px;border:1px solid #e6e6e6}.checkboxes input[type=checkbox]{display:none}.checkboxes input[type=checkbox]:checked+label:before{content:"✓";font-size:20px;line-height:1;text-align:center}.checkboxes.toggleable label{margin-right:0}.form-field-toggleable .checkboxes.toggleable{margin-right:5px;vertical-align:middle}.form-field-toggleable .checkboxes+label{display:inline-block}.switch-toggle{display:inline-flex;overflow:hidden;border-radius:3px;line-height:35px;border:1px solid #ccc}.switch-toggle input[type=radio]{position:absolute;visibility:hidden;display:none}.switch-toggle label{display:inline-block;cursor:pointer;padding:0 15px;margin:0;white-space:nowrap;color:inherit;transition:background-color .5s ease}.switch-toggle input.highlight:checked+label{background:#333;color:#fff}.switch-toggle input:checked+label{color:#fff;background:#999}.signature-pad{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-size:10px;width:100%;height:100%;max-width:700px;max-height:460px;border:1px solid #f0f0f0;background-color:#fff;padding:16px}.signature-pad--body{position:relative;-webkit-box-flex:1;-ms-flex:1;flex:1;border:1px solid #f6f6f6;min-height:100px}.signature-pad--body canvas{position:absolute;left:0;top:0;width:100%;height:100%;border-radius:4px;box-shadow:0 0 5px rgba(0,0,0,.02) inset}.signature-pad--footer{color:#c3c3c3;text-align:center;font-size:1.2em}.signature-pad--actions{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;margin-top:8px}[data-grav-field=array] .form-row{display:flex;align-items:center;margin-bottom:.5rem}[data-grav-field=array] .form-row>input,[data-grav-field=array] .form-row>textarea{margin:0 .5rem;display:inline-block}.form-data.basic-captcha .form-input-wrapper{border:1px solid #ccc;border-radius:5px;display:flex;overflow:hidden}.form-data.basic-captcha .form-input-prepend{display:flex;color:#333;background-color:#ccc;flex-shrink:0}.form-data.basic-captcha .form-input-prepend img{margin:0}.form-data.basic-captcha .form-input-prepend button>svg{margin:0 8px;width:18px;height:18px}.form-data.basic-captcha input.form-input{border:0}/*# sourceMappingURL=form-styles.css.map */
|
||||
1
config/www/user/plugins/form/assets/form-styles.css.map
Normal file
1
config/www/user/plugins/form/assets/form-styles.css.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["../scss/form-styles.scss"],"names":[],"mappings":"CAGA,uBACI,6BACA,kCACA,kBACA,cACA,cAGJ,aACI,cAGJ,gBACI,aACA,6BACA,WACA,UACA,gBACA,mCAGJ,eACI,SAKA,uBACI,aAGJ,wCACI,kBACA,kBACA,OACA,QACA,QACA,2BACA,SAGJ,0BACI,kBACA,gBACA,kBACA,qBACA,uBACA,WACA,cAEA,sCACI,aAEA,4CACI,UAGJ,oDACE,SAGF,iDACE,eACA,kBACA,QACA,UACA,oBACA,YACA,WACA,qBACA,uBACA,mBACA,WACA,iBACA,kBACA,eACA,WACA,uDACI,yBACA,qBAIN,wDACI,gBACA,WAGJ,gHAEI,kBACA,UAOhB,2BACE,gBACA,YACA,gBACA,SAME,qBACI,aACA,gBAEA,mBAEA,uBACI,OACA,8CACA,eACA,kBACA,aACA,aACA,mBACA,uBACA,6BACA,0BAEA,8BACI,sBACA,sCACA,cAEA,mCACI,MAtIA,KA2IZ,0BACI,qBACA,gBAKR,4BACI,0BAKA,uCACI,aACA,iBAEA,8CACI,cAOhB,YACI,qBAEA,kBACI,eACA,eACA,kBACA,mBACA,kBAGJ,yBACI,WACA,qBACA,WACA,YACA,OACA,aACA,kBACA,kBACA,kBAEA,yBAGJ,iCACI,aAEJ,sDACI,YACA,eACA,cACA,kBAGJ,6BACI,eAMJ,8CACI,iBACA,sBAEJ,yCACI,qBAKR,eACI,oBACA,gBACA,kBACA,iBACA,sBAEA,iCACI,kBACA,kBACA,aAGJ,qBACI,qBACA,eACA,eACA,SACA,mBACA,cACA,qCAGJ,6CACI,gBACA,WAGJ,mCACI,WACA,gBAOR,eACI,kBACA,oBACA,oBACA,aACA,4BACA,6BACA,0BACA,sBACA,eACA,WACA,YACA,gBACA,iBACA,yBACA,sBACA,aAGJ,qBACI,kBACA,mBACA,WACA,OACA,yBACA,iBAGJ,4BACI,kBACA,OACA,MACA,WACA,YACA,kBACA,yCAGJ,uBACI,cACA,kBACA,gBAGJ,wBACI,oBACA,oBACA,aACA,yBACA,sBACA,8BACA,eAGJ,kCACI,aACA,mBACA,oBAGJ,mFAGI,eACA,qBAIA,6CACI,sBACA,kBACA,aACA,gBAEJ,6CACI,aACA,WACA,sBACA,cACA,iDACI,SAEJ,wDACI,aACA,WACA,YAGR,0CACI","file":"form-styles.css"}
|
||||
1
config/www/user/plugins/form/assets/form-styles.min.css
vendored
Normal file
1
config/www/user/plugins/form/assets/form-styles.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.form-group.has-errors{margin:0 -5px;padding:0 5px;border:1px solid rgba(255,0,0,.2);border-radius:3px;background:rgba(255,0,0,.05)}.form-errors{color:#b52b27}.form-honeybear{position:absolute!important;visibility:hidden;overflow:hidden;clip:rect(1px,1px,1px,1px);width:1px;height:1px}.form-errors p{margin:0}.form-input-file input{display:none}.form-input-file .dz-default.dz-message{position:absolute;top:50%;right:0;left:0;margin:0;-webkit-transform:translateY(-50%);transform:translateY(-50%);text-align:center}.form-input-file.dropzone{position:relative;min-height:70px;margin-bottom:.85rem;padding:.5rem;color:#aaa;border:2px dashed #ccc;border-radius:3px}.form-input-file.dropzone .dz-preview{margin:.5rem}.form-input-file.dropzone .dz-preview:hover{z-index:2}.form-input-file.dropzone .dz-preview .dz-error-message{width:auto;min-width:140px}.form-input-file.dropzone .dz-preview .dz-image,.form-input-file.dropzone .dz-preview.dz-file-preview .dz-image{z-index:1;border-radius:3px}.form-tabs .tabs-nav{display:flex;margin-bottom:-1px;padding-top:1px}.form-tabs .tabs-nav a{display:flex;padding:10px;cursor:pointer;transition:color .5s ease,background .5s ease;text-align:center;border-bottom:1px solid #eee;border-radius:5px 5px 0 0;flex:1;align-items:center;justify-content:center}.form-tabs .tabs-nav a.active{margin:0 -1px;border:1px solid #eee;border-bottom:1px solid transparent}.form-tabs .tabs-nav a.active span{color:#000}.form-tabs .tabs-nav span{line-height:1.1;display:inline-block}.form-tabs.subtle .tabs-nav{margin-right:0!important}.form-tabs .tabs-content .tab__content{display:none;padding-top:2rem}.form-tabs .tabs-content .tab__content.active{display:block}.checkboxes{display:inline-block}.checkboxes label{position:relative;display:inline;margin-right:15px;padding:0 0 0 20px;cursor:pointer}.checkboxes label:before{position:absolute;left:0;display:inline-block;width:20px;height:20px;margin-top:0;margin-right:10px;content:'';border:1px solid #e6e6e6;border-radius:3px}.checkboxes input[type=checkbox]{display:none}.checkboxes input[type=checkbox]:checked+label:before{font-size:20px;line-height:1;content:'\2713';text-align:center}.checkboxes.toggleable label{margin-right:0}.form-field-toggleable .checkboxes.toggleable{margin-right:5px;vertical-align:middle}.form-field-toggleable .checkboxes+label{display:inline-block}.switch-toggle{line-height:35px;display:inline-flex;overflow:hidden;border:1px solid #eee;border-radius:3px}.switch-toggle input[type=radio]{position:absolute;display:none;visibility:hidden}.switch-toggle label{display:inline-block;margin:0;padding:0 15px;cursor:pointer;transition:background-color .5s ease;white-space:nowrap;color:inherit}.switch-toggle input.highlight:checked+label{color:#fff;background:#333}.switch-toggle input:checked+label{color:#fff;background:#999}.signature-pad{font-size:10px;position:relative;display:flex;flex-direction:column;width:100%;max-width:700px;height:100%;max-height:460px;padding:16px;border:1px solid #f0f0f0;background-color:#fff}.signature-pad--body{position:relative;min-height:100px;border:1px solid #f6f6f6;flex:1}.signature-pad--body canvas{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:4px;box-shadow:0 0 5px rgba(0,0,0,.02) inset}.signature-pad--footer{font-size:1.2em;text-align:center;color:#c3c3c3}.signature-pad--actions{display:flex;margin-top:8px;justify-content:space-between}[data-grav-field=array] .form-row{display:flex;margin-bottom:.5rem;align-items:center}[data-grav-field=array] .form-row>input,[data-grav-field=array] .form-row>textarea{display:inline-block;margin:0 .5rem}
|
||||
1
config/www/user/plugins/form/assets/form.min.js
vendored
Normal file
1
config/www/user/plugins/form/assets/form.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
config/www/user/plugins/form/assets/form.vendor.js
Normal file
1
config/www/user/plugins/form/assets/form.vendor.js
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
|
||||
if (typeof Object.assign !== 'function') {
|
||||
// Must be writable: true, enumerable: false, configurable: true
|
||||
Object.defineProperty(Object, 'assign', {
|
||||
value: function assign(target, varArgs) { // .length of function is 2
|
||||
'use strict';
|
||||
if (target == null) { // TypeError if undefined or null
|
||||
throw new TypeError('Cannot convert undefined or null to object');
|
||||
}
|
||||
|
||||
var to = Object(target);
|
||||
|
||||
for (var index = 1; index < arguments.length; index++) {
|
||||
var nextSource = arguments[index];
|
||||
|
||||
if (nextSource != null) { // Skip over if undefined or null
|
||||
for (var nextKey in nextSource) {
|
||||
// Avoid bugs when hasOwnProperty is shadowed
|
||||
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
|
||||
to[nextKey] = nextSource[nextKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return to;
|
||||
},
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
617
config/www/user/plugins/form/assets/signature_pad.js
Executable file
617
config/www/user/plugins/form/assets/signature_pad.js
Executable file
@@ -0,0 +1,617 @@
|
||||
/*!
|
||||
* Signature Pad v2.3.2
|
||||
* https://github.com/szimek/signature_pad
|
||||
*
|
||||
* Copyright 2017 Szymon Nowak
|
||||
* Released under the MIT license
|
||||
*
|
||||
* The main idea and some parts of the code (e.g. drawing variable width Bézier curve) are taken from:
|
||||
* http://corner.squareup.com/2012/07/smoother-signatures.html
|
||||
*
|
||||
* Implementation of interpolation using cubic Bézier curves is taken from:
|
||||
* http://benknowscode.wordpress.com/2012/09/14/path-interpolation-using-cubic-bezier-and-control-point-estimation-in-javascript
|
||||
*
|
||||
* Algorithm for approximated length of a Bézier curve is taken from:
|
||||
* http://www.lemoda.net/maths/bezier-length/index.html
|
||||
*
|
||||
*/
|
||||
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.SignaturePad = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
function Point(x, y, time) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.time = time || new Date().getTime();
|
||||
}
|
||||
|
||||
Point.prototype.velocityFrom = function (start) {
|
||||
return this.time !== start.time ? this.distanceTo(start) / (this.time - start.time) : 1;
|
||||
};
|
||||
|
||||
Point.prototype.distanceTo = function (start) {
|
||||
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
|
||||
};
|
||||
|
||||
Point.prototype.equals = function (other) {
|
||||
return this.x === other.x && this.y === other.y && this.time === other.time;
|
||||
};
|
||||
|
||||
function Bezier(startPoint, control1, control2, endPoint) {
|
||||
this.startPoint = startPoint;
|
||||
this.control1 = control1;
|
||||
this.control2 = control2;
|
||||
this.endPoint = endPoint;
|
||||
}
|
||||
|
||||
// Returns approximated length.
|
||||
Bezier.prototype.length = function () {
|
||||
var steps = 10;
|
||||
var length = 0;
|
||||
var px = void 0;
|
||||
var py = void 0;
|
||||
|
||||
for (var i = 0; i <= steps; i += 1) {
|
||||
var t = i / steps;
|
||||
var cx = this._point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
|
||||
var cy = this._point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
|
||||
if (i > 0) {
|
||||
var xdiff = cx - px;
|
||||
var ydiff = cy - py;
|
||||
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
|
||||
}
|
||||
px = cx;
|
||||
py = cy;
|
||||
}
|
||||
|
||||
return length;
|
||||
};
|
||||
|
||||
/* eslint-disable no-multi-spaces, space-in-parens */
|
||||
Bezier.prototype._point = function (t, start, c1, c2, end) {
|
||||
return start * (1.0 - t) * (1.0 - t) * (1.0 - t) + 3.0 * c1 * (1.0 - t) * (1.0 - t) * t + 3.0 * c2 * (1.0 - t) * t * t + end * t * t * t;
|
||||
};
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// http://stackoverflow.com/a/27078401/815507
|
||||
function throttle(func, wait, options) {
|
||||
var context, args, result;
|
||||
var timeout = null;
|
||||
var previous = 0;
|
||||
if (!options) options = {};
|
||||
var later = function later() {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
};
|
||||
return function () {
|
||||
var now = Date.now();
|
||||
if (!previous && options.leading === false) previous = now;
|
||||
var remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function SignaturePad(canvas, options) {
|
||||
var self = this;
|
||||
var opts = options || {};
|
||||
|
||||
this.velocityFilterWeight = opts.velocityFilterWeight || 0.7;
|
||||
this.minWidth = opts.minWidth || 0.5;
|
||||
this.maxWidth = opts.maxWidth || 2.5;
|
||||
this.throttle = 'throttle' in opts ? opts.throttle : 16; // in miliseconds
|
||||
this.minDistance = 'minDistance' in opts ? opts.minDistance : 5;
|
||||
|
||||
if (this.throttle) {
|
||||
this._strokeMoveUpdate = throttle(SignaturePad.prototype._strokeUpdate, this.throttle);
|
||||
} else {
|
||||
this._strokeMoveUpdate = SignaturePad.prototype._strokeUpdate;
|
||||
}
|
||||
|
||||
this.dotSize = opts.dotSize || function () {
|
||||
return (this.minWidth + this.maxWidth) / 2;
|
||||
};
|
||||
this.penColor = opts.penColor || 'black';
|
||||
this.backgroundColor = opts.backgroundColor || 'rgba(0,0,0,0)';
|
||||
this.onBegin = opts.onBegin;
|
||||
this.onEnd = opts.onEnd;
|
||||
|
||||
this._canvas = canvas;
|
||||
this._ctx = canvas.getContext('2d');
|
||||
this.clear();
|
||||
|
||||
// We need add these inline so they are available to unbind while still having
|
||||
// access to 'self' we could use _.bind but it's not worth adding a dependency.
|
||||
this._handleMouseDown = function (event) {
|
||||
if (event.which === 1) {
|
||||
self._mouseButtonDown = true;
|
||||
self._strokeBegin(event);
|
||||
}
|
||||
};
|
||||
|
||||
this._handleMouseMove = function (event) {
|
||||
if (self._mouseButtonDown) {
|
||||
self._strokeMoveUpdate(event);
|
||||
}
|
||||
};
|
||||
|
||||
this._handleMouseUp = function (event) {
|
||||
if (event.which === 1 && self._mouseButtonDown) {
|
||||
self._mouseButtonDown = false;
|
||||
self._strokeEnd(event);
|
||||
}
|
||||
};
|
||||
|
||||
this._handleTouchStart = function (event) {
|
||||
// Prevent scrolling.
|
||||
event.preventDefault();
|
||||
|
||||
if (event.targetTouches.length === 1) {
|
||||
var touch = event.changedTouches[0];
|
||||
self._strokeBegin(touch);
|
||||
}
|
||||
};
|
||||
|
||||
this._handleTouchMove = function (event) {
|
||||
// Prevent scrolling.
|
||||
event.preventDefault();
|
||||
|
||||
var touch = event.targetTouches[0];
|
||||
self._strokeMoveUpdate(touch);
|
||||
};
|
||||
|
||||
this._handleTouchEnd = function (event) {
|
||||
var wasCanvasTouched = event.target === self._canvas;
|
||||
if (wasCanvasTouched) {
|
||||
event.preventDefault();
|
||||
self._strokeEnd(event);
|
||||
}
|
||||
};
|
||||
|
||||
// Enable mouse and touch event handlers
|
||||
this.on();
|
||||
}
|
||||
|
||||
// Public methods
|
||||
SignaturePad.prototype.clear = function () {
|
||||
var ctx = this._ctx;
|
||||
var canvas = this._canvas;
|
||||
|
||||
ctx.fillStyle = this.backgroundColor;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
this._data = [];
|
||||
this._reset();
|
||||
this._isEmpty = true;
|
||||
};
|
||||
|
||||
SignaturePad.prototype.fromDataURL = function (dataUrl) {
|
||||
var _this = this;
|
||||
|
||||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
||||
|
||||
var image = new Image();
|
||||
var ratio = options.ratio || window.devicePixelRatio || 1;
|
||||
var width = options.width || this._canvas.width / ratio;
|
||||
var height = options.height || this._canvas.height / ratio;
|
||||
|
||||
this._reset();
|
||||
image.src = dataUrl;
|
||||
image.onload = function () {
|
||||
_this._ctx.drawImage(image, 0, 0, width, height);
|
||||
};
|
||||
this._isEmpty = false;
|
||||
};
|
||||
|
||||
SignaturePad.prototype.toDataURL = function (type) {
|
||||
var _canvas;
|
||||
|
||||
switch (type) {
|
||||
case 'image/svg+xml':
|
||||
return this._toSVG();
|
||||
default:
|
||||
for (var _len = arguments.length, options = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||||
options[_key - 1] = arguments[_key];
|
||||
}
|
||||
|
||||
return (_canvas = this._canvas).toDataURL.apply(_canvas, [type].concat(options));
|
||||
}
|
||||
};
|
||||
|
||||
SignaturePad.prototype.on = function () {
|
||||
this._handleMouseEvents();
|
||||
this._handleTouchEvents();
|
||||
};
|
||||
|
||||
SignaturePad.prototype.off = function () {
|
||||
// Pass touch events to canvas element on mobile IE11 and Edge.
|
||||
this._canvas.style.msTouchAction = 'auto';
|
||||
this._canvas.style.touchAction = 'auto';
|
||||
|
||||
this._canvas.removeEventListener('mousedown', this._handleMouseDown);
|
||||
this._canvas.removeEventListener('mousemove', this._handleMouseMove);
|
||||
document.removeEventListener('mouseup', this._handleMouseUp);
|
||||
|
||||
this._canvas.removeEventListener('touchstart', this._handleTouchStart);
|
||||
this._canvas.removeEventListener('touchmove', this._handleTouchMove);
|
||||
this._canvas.removeEventListener('touchend', this._handleTouchEnd);
|
||||
};
|
||||
|
||||
SignaturePad.prototype.isEmpty = function () {
|
||||
return this._isEmpty;
|
||||
};
|
||||
|
||||
// Private methods
|
||||
SignaturePad.prototype._strokeBegin = function (event) {
|
||||
this._data.push([]);
|
||||
this._reset();
|
||||
this._strokeUpdate(event);
|
||||
|
||||
if (typeof this.onBegin === 'function') {
|
||||
this.onBegin(event);
|
||||
}
|
||||
};
|
||||
|
||||
SignaturePad.prototype._strokeUpdate = function (event) {
|
||||
var x = event.clientX;
|
||||
var y = event.clientY;
|
||||
|
||||
var point = this._createPoint(x, y);
|
||||
var lastPointGroup = this._data[this._data.length - 1];
|
||||
var lastPoint = lastPointGroup && lastPointGroup[lastPointGroup.length - 1];
|
||||
var isLastPointTooClose = lastPoint && point.distanceTo(lastPoint) < this.minDistance;
|
||||
|
||||
// Skip this point if it's too close to the previous one
|
||||
if (!(lastPoint && isLastPointTooClose)) {
|
||||
var _addPoint = this._addPoint(point),
|
||||
curve = _addPoint.curve,
|
||||
widths = _addPoint.widths;
|
||||
|
||||
if (curve && widths) {
|
||||
this._drawCurve(curve, widths.start, widths.end);
|
||||
}
|
||||
|
||||
this._data[this._data.length - 1].push({
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
time: point.time,
|
||||
color: this.penColor
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
SignaturePad.prototype._strokeEnd = function (event) {
|
||||
var canDrawCurve = this.points.length > 2;
|
||||
var point = this.points[0]; // Point instance
|
||||
|
||||
if (!canDrawCurve && point) {
|
||||
this._drawDot(point);
|
||||
}
|
||||
|
||||
if (point) {
|
||||
var lastPointGroup = this._data[this._data.length - 1];
|
||||
var lastPoint = lastPointGroup[lastPointGroup.length - 1]; // plain object
|
||||
|
||||
// When drawing a dot, there's only one point in a group, so without this check
|
||||
// such group would end up with exactly the same 2 points.
|
||||
if (!point.equals(lastPoint)) {
|
||||
lastPointGroup.push({
|
||||
x: point.x,
|
||||
y: point.y,
|
||||
time: point.time,
|
||||
color: this.penColor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof this.onEnd === 'function') {
|
||||
this.onEnd(event);
|
||||
}
|
||||
};
|
||||
|
||||
SignaturePad.prototype._handleMouseEvents = function () {
|
||||
this._mouseButtonDown = false;
|
||||
|
||||
this._canvas.addEventListener('mousedown', this._handleMouseDown);
|
||||
this._canvas.addEventListener('mousemove', this._handleMouseMove);
|
||||
document.addEventListener('mouseup', this._handleMouseUp);
|
||||
};
|
||||
|
||||
SignaturePad.prototype._handleTouchEvents = function () {
|
||||
// Pass touch events to canvas element on mobile IE11 and Edge.
|
||||
this._canvas.style.msTouchAction = 'none';
|
||||
this._canvas.style.touchAction = 'none';
|
||||
|
||||
this._canvas.addEventListener('touchstart', this._handleTouchStart);
|
||||
this._canvas.addEventListener('touchmove', this._handleTouchMove);
|
||||
this._canvas.addEventListener('touchend', this._handleTouchEnd);
|
||||
};
|
||||
|
||||
SignaturePad.prototype._reset = function () {
|
||||
this.points = [];
|
||||
this._lastVelocity = 0;
|
||||
this._lastWidth = (this.minWidth + this.maxWidth) / 2;
|
||||
this._ctx.fillStyle = this.penColor;
|
||||
};
|
||||
|
||||
SignaturePad.prototype._createPoint = function (x, y, time) {
|
||||
var rect = this._canvas.getBoundingClientRect();
|
||||
|
||||
return new Point(x - rect.left, y - rect.top, time || new Date().getTime());
|
||||
};
|
||||
|
||||
SignaturePad.prototype._addPoint = function (point) {
|
||||
var points = this.points;
|
||||
var tmp = void 0;
|
||||
|
||||
points.push(point);
|
||||
|
||||
if (points.length > 2) {
|
||||
// To reduce the initial lag make it work with 3 points
|
||||
// by copying the first point to the beginning.
|
||||
if (points.length === 3) points.unshift(points[0]);
|
||||
|
||||
tmp = this._calculateCurveControlPoints(points[0], points[1], points[2]);
|
||||
var c2 = tmp.c2;
|
||||
tmp = this._calculateCurveControlPoints(points[1], points[2], points[3]);
|
||||
var c3 = tmp.c1;
|
||||
var curve = new Bezier(points[1], c2, c3, points[2]);
|
||||
var widths = this._calculateCurveWidths(curve);
|
||||
|
||||
// Remove the first element from the list,
|
||||
// so that we always have no more than 4 points in points array.
|
||||
points.shift();
|
||||
|
||||
return { curve: curve, widths: widths };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
SignaturePad.prototype._calculateCurveControlPoints = function (s1, s2, s3) {
|
||||
var dx1 = s1.x - s2.x;
|
||||
var dy1 = s1.y - s2.y;
|
||||
var dx2 = s2.x - s3.x;
|
||||
var dy2 = s2.y - s3.y;
|
||||
|
||||
var m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
|
||||
var m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
|
||||
|
||||
var l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
|
||||
var l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
|
||||
|
||||
var dxm = m1.x - m2.x;
|
||||
var dym = m1.y - m2.y;
|
||||
|
||||
var k = l2 / (l1 + l2);
|
||||
var cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
|
||||
|
||||
var tx = s2.x - cm.x;
|
||||
var ty = s2.y - cm.y;
|
||||
|
||||
return {
|
||||
c1: new Point(m1.x + tx, m1.y + ty),
|
||||
c2: new Point(m2.x + tx, m2.y + ty)
|
||||
};
|
||||
};
|
||||
|
||||
SignaturePad.prototype._calculateCurveWidths = function (curve) {
|
||||
var startPoint = curve.startPoint;
|
||||
var endPoint = curve.endPoint;
|
||||
var widths = { start: null, end: null };
|
||||
|
||||
var velocity = this.velocityFilterWeight * endPoint.velocityFrom(startPoint) + (1 - this.velocityFilterWeight) * this._lastVelocity;
|
||||
|
||||
var newWidth = this._strokeWidth(velocity);
|
||||
|
||||
widths.start = this._lastWidth;
|
||||
widths.end = newWidth;
|
||||
|
||||
this._lastVelocity = velocity;
|
||||
this._lastWidth = newWidth;
|
||||
|
||||
return widths;
|
||||
};
|
||||
|
||||
SignaturePad.prototype._strokeWidth = function (velocity) {
|
||||
return Math.max(this.maxWidth / (velocity + 1), this.minWidth);
|
||||
};
|
||||
|
||||
SignaturePad.prototype._drawPoint = function (x, y, size) {
|
||||
var ctx = this._ctx;
|
||||
|
||||
ctx.moveTo(x, y);
|
||||
ctx.arc(x, y, size, 0, 2 * Math.PI, false);
|
||||
this._isEmpty = false;
|
||||
};
|
||||
|
||||
SignaturePad.prototype._drawCurve = function (curve, startWidth, endWidth) {
|
||||
var ctx = this._ctx;
|
||||
var widthDelta = endWidth - startWidth;
|
||||
var drawSteps = Math.floor(curve.length());
|
||||
|
||||
ctx.beginPath();
|
||||
|
||||
for (var i = 0; i < drawSteps; i += 1) {
|
||||
// Calculate the Bezier (x, y) coordinate for this step.
|
||||
var t = i / drawSteps;
|
||||
var tt = t * t;
|
||||
var ttt = tt * t;
|
||||
var u = 1 - t;
|
||||
var uu = u * u;
|
||||
var uuu = uu * u;
|
||||
|
||||
var x = uuu * curve.startPoint.x;
|
||||
x += 3 * uu * t * curve.control1.x;
|
||||
x += 3 * u * tt * curve.control2.x;
|
||||
x += ttt * curve.endPoint.x;
|
||||
|
||||
var y = uuu * curve.startPoint.y;
|
||||
y += 3 * uu * t * curve.control1.y;
|
||||
y += 3 * u * tt * curve.control2.y;
|
||||
y += ttt * curve.endPoint.y;
|
||||
|
||||
var width = startWidth + ttt * widthDelta;
|
||||
this._drawPoint(x, y, width);
|
||||
}
|
||||
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
SignaturePad.prototype._drawDot = function (point) {
|
||||
var ctx = this._ctx;
|
||||
var width = typeof this.dotSize === 'function' ? this.dotSize() : this.dotSize;
|
||||
|
||||
ctx.beginPath();
|
||||
this._drawPoint(point.x, point.y, width);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
};
|
||||
|
||||
SignaturePad.prototype._fromData = function (pointGroups, drawCurve, drawDot) {
|
||||
for (var i = 0; i < pointGroups.length; i += 1) {
|
||||
var group = pointGroups[i];
|
||||
|
||||
if (group.length > 1) {
|
||||
for (var j = 0; j < group.length; j += 1) {
|
||||
var rawPoint = group[j];
|
||||
var point = new Point(rawPoint.x, rawPoint.y, rawPoint.time);
|
||||
var color = rawPoint.color;
|
||||
|
||||
if (j === 0) {
|
||||
// First point in a group. Nothing to draw yet.
|
||||
|
||||
// All points in the group have the same color, so it's enough to set
|
||||
// penColor just at the beginning.
|
||||
this.penColor = color;
|
||||
this._reset();
|
||||
|
||||
this._addPoint(point);
|
||||
} else if (j !== group.length - 1) {
|
||||
// Middle point in a group.
|
||||
var _addPoint2 = this._addPoint(point),
|
||||
curve = _addPoint2.curve,
|
||||
widths = _addPoint2.widths;
|
||||
|
||||
if (curve && widths) {
|
||||
drawCurve(curve, widths, color);
|
||||
}
|
||||
} else {
|
||||
// Last point in a group. Do nothing.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._reset();
|
||||
var _rawPoint = group[0];
|
||||
drawDot(_rawPoint);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SignaturePad.prototype._toSVG = function () {
|
||||
var _this2 = this;
|
||||
|
||||
var pointGroups = this._data;
|
||||
var canvas = this._canvas;
|
||||
var ratio = Math.max(window.devicePixelRatio || 1, 1);
|
||||
var minX = 0;
|
||||
var minY = 0;
|
||||
var maxX = canvas.width / ratio;
|
||||
var maxY = canvas.height / ratio;
|
||||
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
|
||||
svg.setAttributeNS(null, 'width', canvas.width);
|
||||
svg.setAttributeNS(null, 'height', canvas.height);
|
||||
|
||||
this._fromData(pointGroups, function (curve, widths, color) {
|
||||
var path = document.createElement('path');
|
||||
|
||||
// Need to check curve for NaN values, these pop up when drawing
|
||||
// lines on the canvas that are not continuous. E.g. Sharp corners
|
||||
// or stopping mid-stroke and than continuing without lifting mouse.
|
||||
if (!isNaN(curve.control1.x) && !isNaN(curve.control1.y) && !isNaN(curve.control2.x) && !isNaN(curve.control2.y)) {
|
||||
var attr = 'M ' + curve.startPoint.x.toFixed(3) + ',' + curve.startPoint.y.toFixed(3) + ' ' + ('C ' + curve.control1.x.toFixed(3) + ',' + curve.control1.y.toFixed(3) + ' ') + (curve.control2.x.toFixed(3) + ',' + curve.control2.y.toFixed(3) + ' ') + (curve.endPoint.x.toFixed(3) + ',' + curve.endPoint.y.toFixed(3));
|
||||
|
||||
path.setAttribute('d', attr);
|
||||
path.setAttribute('stroke-width', (widths.end * 2.25).toFixed(3));
|
||||
path.setAttribute('stroke', color);
|
||||
path.setAttribute('fill', 'none');
|
||||
path.setAttribute('stroke-linecap', 'round');
|
||||
|
||||
svg.appendChild(path);
|
||||
}
|
||||
}, function (rawPoint) {
|
||||
var circle = document.createElement('circle');
|
||||
var dotSize = typeof _this2.dotSize === 'function' ? _this2.dotSize() : _this2.dotSize;
|
||||
circle.setAttribute('r', dotSize);
|
||||
circle.setAttribute('cx', rawPoint.x);
|
||||
circle.setAttribute('cy', rawPoint.y);
|
||||
circle.setAttribute('fill', rawPoint.color);
|
||||
|
||||
svg.appendChild(circle);
|
||||
});
|
||||
|
||||
var prefix = 'data:image/svg+xml;base64,';
|
||||
var header = '<svg' + ' xmlns="http://www.w3.org/2000/svg"' + ' xmlns:xlink="http://www.w3.org/1999/xlink"' + (' viewBox="' + minX + ' ' + minY + ' ' + maxX + ' ' + maxY + '"') + (' width="' + maxX + '"') + (' height="' + maxY + '"') + '>';
|
||||
var body = svg.innerHTML;
|
||||
|
||||
// IE hack for missing innerHTML property on SVGElement
|
||||
if (body === undefined) {
|
||||
var dummy = document.createElement('dummy');
|
||||
var nodes = svg.childNodes;
|
||||
dummy.innerHTML = '';
|
||||
|
||||
for (var i = 0; i < nodes.length; i += 1) {
|
||||
dummy.appendChild(nodes[i].cloneNode(true));
|
||||
}
|
||||
|
||||
body = dummy.innerHTML;
|
||||
}
|
||||
|
||||
var footer = '</svg>';
|
||||
var data = header + body + footer;
|
||||
|
||||
return prefix + btoa(data);
|
||||
};
|
||||
|
||||
SignaturePad.prototype.fromData = function (pointGroups) {
|
||||
var _this3 = this;
|
||||
|
||||
this.clear();
|
||||
|
||||
this._fromData(pointGroups, function (curve, widths) {
|
||||
return _this3._drawCurve(curve, widths.start, widths.end);
|
||||
}, function (rawPoint) {
|
||||
return _this3._drawDot(rawPoint);
|
||||
});
|
||||
|
||||
this._data = pointGroups;
|
||||
};
|
||||
|
||||
SignaturePad.prototype.toData = function () {
|
||||
return this._data;
|
||||
};
|
||||
|
||||
return SignaturePad;
|
||||
|
||||
})));
|
||||
461
config/www/user/plugins/form/assets/xhr-submitter.js
Normal file
461
config/www/user/plugins/form/assets/xhr-submitter.js
Normal file
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* Grav Form XHR Submitter
|
||||
*
|
||||
* A modular system for handling form submissions via XMLHttpRequest (AJAX).
|
||||
* Features include content replacement, captcha handling, and error management.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Main namespace
|
||||
window.GravFormXHR = {};
|
||||
|
||||
/**
|
||||
* Core Module - Contains configuration and utility functions
|
||||
*/
|
||||
const Core = {
|
||||
config: {
|
||||
debug: false,
|
||||
enableLoadingIndicator: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Configure global settings
|
||||
* @param {Object} options - Configuration options
|
||||
*/
|
||||
configure: function(options) {
|
||||
Object.assign(this.config, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Logger utility
|
||||
* @param {string} message - Message to log
|
||||
* @param {string} level - Log level ('log', 'warn', 'error')
|
||||
*/
|
||||
log: function(message, level = 'log') {
|
||||
if (!this.config.debug) return;
|
||||
|
||||
const validLevels = ['log', 'warn', 'error'];
|
||||
const finalLevel = validLevels.includes(level) ? level : 'log';
|
||||
|
||||
console[finalLevel](`[GravFormXHR] ${message}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display an error message within a target element
|
||||
* @param {HTMLElement} target - The element to display the error in
|
||||
* @param {string} message - The error message
|
||||
*/
|
||||
displayError: function(target, message) {
|
||||
const errorMsgContainer = target.querySelector('.form-messages') || target;
|
||||
const errorMsg = document.createElement('div');
|
||||
errorMsg.className = 'form-message error';
|
||||
errorMsg.textContent = message;
|
||||
errorMsgContainer.insertBefore(errorMsg, errorMsgContainer.firstChild);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DOM Module - Handles DOM manipulation and form tracking
|
||||
*/
|
||||
const DOM = {
|
||||
/**
|
||||
* Find a form wrapper by formId
|
||||
* @param {string} formId - ID of the form
|
||||
* @returns {HTMLElement|null} - The wrapper element or null
|
||||
*/
|
||||
getFormWrapper: function(formId) {
|
||||
const wrapperId = formId + '-wrapper';
|
||||
return document.getElementById(wrapperId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add or remove loading indicators
|
||||
* @param {HTMLElement} form - The form element
|
||||
* @param {HTMLElement} wrapper - The wrapper element
|
||||
* @param {boolean} isLoading - Whether to add or remove loading classes
|
||||
*/
|
||||
updateLoadingState: function(form, wrapper, isLoading) {
|
||||
if (!Core.config.enableLoadingIndicator) return;
|
||||
|
||||
if (isLoading) {
|
||||
wrapper.classList.add('loading');
|
||||
form.classList.add('submitting');
|
||||
} else {
|
||||
wrapper.classList.remove('loading');
|
||||
form.classList.remove('submitting');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update form content with server response
|
||||
* @param {string} responseText - Server response HTML
|
||||
* @param {string} wrapperId - ID of the wrapper to update
|
||||
* @param {string} formId - ID of the original form
|
||||
*/
|
||||
updateFormContent: function(responseText, wrapperId, formId) {
|
||||
const wrapperElement = document.getElementById(wrapperId);
|
||||
if (!wrapperElement) {
|
||||
console.error(`Cannot update content: Wrapper #${wrapperId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
Core.log(`Updating content for wrapper: ${wrapperId}`);
|
||||
|
||||
// Parse response
|
||||
const tempDiv = document.createElement('div');
|
||||
try {
|
||||
tempDiv.innerHTML = responseText;
|
||||
} catch (e) {
|
||||
console.error(`Error parsing response HTML for wrapper: ${wrapperId}`, e);
|
||||
Core.displayError(wrapperElement, 'An error occurred processing the server response.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this._updateWrapperContent(tempDiv, wrapperElement, wrapperId, formId);
|
||||
this._reinitializeUpdatedForm(wrapperElement, formId);
|
||||
} catch (e) {
|
||||
console.error(`Error during content update for wrapper ${wrapperId}:`, e);
|
||||
Core.displayError(wrapperElement, 'An error occurred updating the form content.');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update wrapper content based on response parsing strategy
|
||||
* @private
|
||||
*/
|
||||
_updateWrapperContent: function(tempDiv, wrapperElement, wrapperId, formId) {
|
||||
// Strategy 1: Look for matching wrapper ID in response
|
||||
const newWrapperElement = tempDiv.querySelector('#' + wrapperId);
|
||||
|
||||
if (newWrapperElement) {
|
||||
wrapperElement.innerHTML = newWrapperElement.innerHTML;
|
||||
Core.log(`Update using newWrapperElement.innerHTML SUCCESSFUL for wrapper: ${wrapperId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 2: Look for matching form ID in response
|
||||
const hasMatchingForm = tempDiv.querySelector('#' + formId);
|
||||
|
||||
if (hasMatchingForm) {
|
||||
Core.log(`Wrapper element #${wrapperId} not found in XHR response, but found matching form. Using entire response.`);
|
||||
wrapperElement.innerHTML = tempDiv.innerHTML;
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 3: Look for toast messages
|
||||
const hasToastMessages = tempDiv.querySelector('.toast');
|
||||
|
||||
if (hasToastMessages) {
|
||||
Core.log('Found toast messages in response. Updating wrapper with the response.');
|
||||
wrapperElement.innerHTML = tempDiv.innerHTML;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: Use entire response with warning
|
||||
Core.log('No matching content found in response. Response may not be valid for this wrapper.', 'warn');
|
||||
wrapperElement.innerHTML = tempDiv.innerHTML;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reinitialize updated form and its components
|
||||
* @private
|
||||
*/
|
||||
_reinitializeUpdatedForm: function(wrapperElement, formId) {
|
||||
const updatedForm = wrapperElement.querySelector('#' + formId);
|
||||
|
||||
if (updatedForm) {
|
||||
Core.log(`Re-running initialization for form ${formId} after update`);
|
||||
|
||||
// First reinitialize any captchas
|
||||
CaptchaManager.reinitializeAll(updatedForm);
|
||||
|
||||
// Trigger mutation._grav event for Dropzone and other field reinitializations
|
||||
setTimeout(() => {
|
||||
Core.log('Triggering mutation._grav event for field reinitialization');
|
||||
|
||||
// Trigger using jQuery if available (preferred method for compatibility)
|
||||
if (typeof jQuery !== 'undefined') {
|
||||
jQuery('body').trigger('mutation._grav', [wrapperElement]);
|
||||
} else {
|
||||
// Fallback: dispatch native custom event
|
||||
const event = new CustomEvent('mutation._grav', {
|
||||
detail: { target: wrapperElement },
|
||||
bubbles: true
|
||||
});
|
||||
document.body.dispatchEvent(event);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
// Then re-attach the XHR listener
|
||||
setTimeout(() => {
|
||||
FormHandler.setupListener(formId);
|
||||
}, 10);
|
||||
} else {
|
||||
// Check if this was a successful submission with just a message
|
||||
const hasSuccessMessage = wrapperElement.querySelector('.toast-success, .form-success');
|
||||
|
||||
if (hasSuccessMessage) {
|
||||
Core.log('No form found after update, but success message detected. This appears to be a successful submission.');
|
||||
} else {
|
||||
console.warn(`Could not find form #${formId} inside the updated wrapper after update. Cannot re-attach listener/initializers.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* XHR Module - Handles XMLHttpRequest operations
|
||||
*/
|
||||
const XHRManager = {
|
||||
/**
|
||||
* Send form data via XHR
|
||||
* @param {HTMLFormElement} form - The form to submit
|
||||
*/
|
||||
sendFormData: function(form) {
|
||||
const formId = form.id;
|
||||
const wrapperId = formId + '-wrapper';
|
||||
const wrapperElement = DOM.getFormWrapper(formId);
|
||||
|
||||
if (!wrapperElement) {
|
||||
console.error(`XHR submission: Target wrapper element #${wrapperId} not found on the page! Cannot proceed.`);
|
||||
form.innerHTML = '<p class="form-message error">Error: Form wrapper missing. Cannot update content.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
Core.log(`Initiating XHR submission for form: ${formId}, targeting wrapper: ${wrapperId}`);
|
||||
DOM.updateLoadingState(form, wrapperElement, true);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open(form.getAttribute('method') || 'POST', form.getAttribute('action') || window.location.href);
|
||||
|
||||
// Set Headers
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.setRequestHeader('X-Grav-Form-XHR', 'true');
|
||||
|
||||
// Success handler
|
||||
xhr.onload = () => {
|
||||
Core.log(`XHR request completed for form: ${formId}, Status: ${xhr.status}`);
|
||||
DOM.updateLoadingState(form, wrapperElement, false);
|
||||
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
DOM.updateFormContent(xhr.responseText, wrapperId, formId);
|
||||
} else {
|
||||
Core.log(`Form submission failed for form: ${formId}, HTTP Status: ${xhr.status} ${xhr.statusText}`, 'error');
|
||||
Core.displayError(wrapperElement, `An error occurred during submission (Status: ${xhr.status}). Please check the form and try again.`);
|
||||
}
|
||||
};
|
||||
|
||||
// Network error handler
|
||||
xhr.onerror = () => {
|
||||
Core.log(`Form submission failed due to network error for form: ${formId}`, 'error');
|
||||
DOM.updateLoadingState(form, wrapperElement, false);
|
||||
Core.displayError(wrapperElement, 'A network error occurred. Please check your connection and try again.');
|
||||
};
|
||||
|
||||
// Prepare and send data
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const urlEncodedData = new URLSearchParams(formData).toString();
|
||||
Core.log(`Sending XHR request for form: ${formId} with custom header X-Grav-Form-XHR`);
|
||||
xhr.send(urlEncodedData);
|
||||
} catch (e) {
|
||||
Core.log(`Error preparing or sending XHR request for form: ${formId}: ${e.message}`, 'error');
|
||||
DOM.updateLoadingState(form, wrapperElement, false);
|
||||
Core.displayError(wrapperElement, 'An unexpected error occurred before sending the form.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CaptchaManager - Handles captcha registration and initialization
|
||||
*/
|
||||
const CaptchaManager = {
|
||||
providers: {},
|
||||
|
||||
/**
|
||||
* Register a captcha provider
|
||||
* @param {string} name - Provider name
|
||||
* @param {object} provider - Provider object with init and reset methods
|
||||
*/
|
||||
register: function(name, provider) {
|
||||
this.providers[name] = provider;
|
||||
Core.log(`Registered captcha provider: ${name}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a provider by name
|
||||
* @param {string} name - Provider name
|
||||
* @returns {object|null} Provider object or null if not found
|
||||
*/
|
||||
getProvider: function(name) {
|
||||
return this.providers[name] || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all registered providers
|
||||
* @returns {object} Object containing all providers
|
||||
*/
|
||||
getProviders: function() {
|
||||
return this.providers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reinitialize all captchas in a form
|
||||
* @param {HTMLFormElement} form - Form element containing captchas
|
||||
*/
|
||||
reinitializeAll: function(form) {
|
||||
if (!form || !form.id) return;
|
||||
|
||||
const formId = form.id;
|
||||
const containers = form.querySelectorAll('[data-captcha-provider]');
|
||||
|
||||
containers.forEach(container => {
|
||||
const providerName = container.dataset.captchaProvider;
|
||||
Core.log(`Found captcha container for provider: ${providerName} in form: ${formId}`);
|
||||
|
||||
const provider = this.getProvider(providerName);
|
||||
if (provider && typeof provider.reset === 'function') {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
provider.reset(container, form);
|
||||
Core.log(`Successfully reset ${providerName} captcha in form: ${formId}`);
|
||||
} catch (e) {
|
||||
console.error(`Error resetting ${providerName} captcha:`, e);
|
||||
}
|
||||
}, 0);
|
||||
} else {
|
||||
console.warn(`Could not reset captcha provider "${providerName}" - provider not registered or missing reset method`);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FormHandler - Handles form submission and event listeners
|
||||
*/
|
||||
const FormHandler = {
|
||||
/**
|
||||
* Submit a form via XHR
|
||||
* @param {HTMLFormElement} form - Form to submit
|
||||
*/
|
||||
submitForm: function(form) {
|
||||
if (!form || !form.id) {
|
||||
console.error('submitForm called with invalid form element or form missing ID.');
|
||||
return;
|
||||
}
|
||||
|
||||
XHRManager.sendFormData(form);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set up XHR submission listener for a form
|
||||
* @param {string} formId - ID of the form
|
||||
*/
|
||||
setupListener: function(formId) {
|
||||
setTimeout(() => {
|
||||
const form = document.getElementById(formId);
|
||||
if (!form) {
|
||||
Core.log(`XHR Setup (delayed): Form with ID "${formId}" not found.`, 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove stale marker from previous runs
|
||||
delete form.dataset.directXhrListenerAttached;
|
||||
|
||||
// Check if any captcha provider is handling the submission
|
||||
const captchaContainer = form.querySelector('[data-captcha-provider][data-intercepts-submit="true"]');
|
||||
|
||||
if (!captchaContainer) {
|
||||
// No intercepting captcha found, attach direct listener
|
||||
this._attachDirectListener(form);
|
||||
} else {
|
||||
// Captcha will intercept, don't attach direct listener
|
||||
const providerName = captchaContainer.dataset.captchaProvider;
|
||||
Core.log(`XHR listener deferred: ${providerName} should intercept submit for form: ${formId}`);
|
||||
// Ensure no stale listener marker remains
|
||||
delete form.dataset.directXhrListenerAttached;
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Attach a direct submit event listener to a form
|
||||
* @private
|
||||
* @param {HTMLFormElement} form - Form element
|
||||
*/
|
||||
_attachDirectListener: function(form) {
|
||||
// Only proceed if XHR is enabled for this form
|
||||
if (form.dataset.xhrEnabled !== 'true') {
|
||||
Core.log(`XHR not enabled for form: ${form.id}. Skipping direct listener attachment.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we already attached a listener
|
||||
if (form.dataset.directXhrListenerAttached === 'true') {
|
||||
Core.log(`Direct XHR listener already attached for form: ${form.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const directXhrSubmitHandler = (event) => {
|
||||
Core.log(`Direct XHR submit handler triggered for form: ${form.id}`);
|
||||
event.preventDefault();
|
||||
FormHandler.submitForm(form);
|
||||
};
|
||||
|
||||
Core.log(`Attaching direct XHR listener for form: ${form.id}`);
|
||||
form.addEventListener('submit', directXhrSubmitHandler);
|
||||
form.dataset.directXhrListenerAttached = 'true';
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize basic built-in captcha handlers
|
||||
// Other providers will register themselves via separate handler JS files
|
||||
const initializeBasicCaptchaHandlers = function() {
|
||||
// Basic captcha handler (image refresh etc.)
|
||||
CaptchaManager.register('basic-captcha', {
|
||||
reset: function(container, form) {
|
||||
const formId = form.id;
|
||||
const captchaImg = container.querySelector('img');
|
||||
const captchaInput = container.querySelector('input[type="text"]');
|
||||
|
||||
if (captchaImg) {
|
||||
// Add a timestamp to force image reload
|
||||
const timestamp = new Date().getTime();
|
||||
const imgSrc = captchaImg.src.split('?')[0] + '?t=' + timestamp;
|
||||
captchaImg.src = imgSrc;
|
||||
|
||||
// Clear any existing input
|
||||
if (captchaInput) {
|
||||
captchaInput.value = '';
|
||||
}
|
||||
|
||||
Core.log(`Reset basic-captcha for form: ${formId}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize basic captcha handlers
|
||||
initializeBasicCaptchaHandlers();
|
||||
|
||||
// --- Expose Public API ---
|
||||
|
||||
// Core configuration
|
||||
window.GravFormXHR.configure = Core.configure.bind(Core);
|
||||
|
||||
// Form submission
|
||||
window.GravFormXHR.submit = FormHandler.submitForm.bind(FormHandler);
|
||||
window.GravFormXHR.setupListener = FormHandler.setupListener.bind(FormHandler);
|
||||
|
||||
// Captcha management
|
||||
window.GravFormXHR.captcha = CaptchaManager;
|
||||
|
||||
// Legacy support
|
||||
window.GravFormXHRSubmitters = {submit: FormHandler.submitForm.bind(FormHandler)};
|
||||
window.attachFormSubmitListener = FormHandler.setupListener.bind(FormHandler);
|
||||
|
||||
})();
|
||||
373
config/www/user/plugins/form/blueprints.yaml
Normal file
373
config/www/user/plugins/form/blueprints.yaml
Normal file
@@ -0,0 +1,373 @@
|
||||
name: Form
|
||||
slug: form
|
||||
type: plugin
|
||||
version: 8.1.0
|
||||
description: Enables forms handling and processing
|
||||
icon: check-square
|
||||
author:
|
||||
name: Team Grav
|
||||
email: devs@getgrav.org
|
||||
url: https://getgrav.org
|
||||
keywords: plugin, form
|
||||
homepage: https://github.com/getgrav/grav-plugin-form
|
||||
bugs: https://github.com/getgrav/grav-plugin-form/issues
|
||||
license: MIT
|
||||
|
||||
dependencies:
|
||||
- { name: grav, version: ">=1.7.49" }
|
||||
|
||||
form:
|
||||
validation: strict
|
||||
fields:
|
||||
enabled:
|
||||
type: hidden
|
||||
label: PLUGIN_ADMIN.PLUGIN_STATUS
|
||||
highlight: 1
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
general:
|
||||
type: section
|
||||
title: PLUGIN_FORM.GENERAL
|
||||
|
||||
fields:
|
||||
debug:
|
||||
type: toggle
|
||||
label: Debug
|
||||
highlight: 1
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
built_in_css:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.USE_BUILT_IN_CSS
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
inline_css:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.USE_INLINE_CSS
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
refresh_prevention:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.REFRESH_PREVENTION
|
||||
help: PLUGIN_FORM.REFRESH_PREVENTION_HELP
|
||||
highlight: 1
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
client_side_validation:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.CLIENT_SIDE_VALIDATION
|
||||
help: PLUGIN_FORM.CLIENT_SIDE_VALIDATION_HELP
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
inline_errors:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.INLINE_ERRORS
|
||||
help: PLUGIN_FORM.INLINE_ERRORS_HELP
|
||||
highlight: 0
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
modular_form_fix:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.MODULAR_FORM_FIX
|
||||
help: PLUGIN_FORM.MODULAR_FORM_FIX_HELP
|
||||
highlight: 1
|
||||
default: 1
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
files:
|
||||
type: section
|
||||
title: PLUGIN_FORM.FILES
|
||||
|
||||
fields:
|
||||
files.multiple:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.ALLOW_MULTIPLE
|
||||
help: PLUGIN_FORM.ALLOW_MULTIPLE_HELP
|
||||
highlight: 1
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
files.limit:
|
||||
type: text
|
||||
size: x-small
|
||||
label: PLUGIN_FORM.LIMIT
|
||||
help: PLUGIN_FORM.LIMIT_HELP
|
||||
default: 10
|
||||
validate:
|
||||
type: number
|
||||
min: 1
|
||||
files.destination:
|
||||
type: text
|
||||
size: large
|
||||
label: PLUGIN_FORM.DESTINATION
|
||||
help: PLUGIN_FORM.DESTINATION_HELP
|
||||
default: "@self"
|
||||
files.accept:
|
||||
type: selectize
|
||||
size: large
|
||||
label: PLUGIN_FORM.ACCEPT
|
||||
help: PLUGIN_FORM.ACCEPT_HELP
|
||||
classes: fancy
|
||||
default:
|
||||
- image/*
|
||||
validate:
|
||||
type: commalist
|
||||
files.filesize:
|
||||
type: text
|
||||
label: PLUGIN_FORM.FILESIZE
|
||||
help: PLUGIN_FORM.FILESIZE_HELP
|
||||
size: x-small
|
||||
default: 5
|
||||
validate:
|
||||
type: number
|
||||
min: 0
|
||||
files.avoid_overwriting:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.AVOID_OVERWRITING
|
||||
help: PLUGIN_FORM.AVOID_OVERWRITING_HELP
|
||||
highlight: 0
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
files.random_name:
|
||||
type: toggle
|
||||
label: PLUGIN_FORM.RANDOM_NAME
|
||||
help: PLUGIN_FORM.RANDOM_NAME_HELP
|
||||
highlight: 0
|
||||
default: 0
|
||||
options:
|
||||
1: PLUGIN_ADMIN.ENABLED
|
||||
0: PLUGIN_ADMIN.DISABLED
|
||||
validate:
|
||||
type: bool
|
||||
|
||||
recaptcha:
|
||||
type: section
|
||||
title: PLUGIN_FORM.RECAPTCHA
|
||||
|
||||
fields:
|
||||
recaptcha.version:
|
||||
type: select
|
||||
label: PLUGIN_FORM.RECAPTCHA_VERSION
|
||||
default: 2-checkbox
|
||||
options:
|
||||
2-checkbox: PLUGIN_FORM.RECAPTCHA_VERSION_V2_CHECKBOX
|
||||
2-invisible: PLUGIN_FORM.RECAPTCHA_VERSION_V2_INVISIBLE
|
||||
3: PLUGIN_FORM.RECAPTCHA_VERSION_V3_LATEST
|
||||
recaptcha.theme:
|
||||
type: select
|
||||
label: PLUGIN_FORM.RECAPTCHA_THEME
|
||||
default: light
|
||||
options:
|
||||
light: PLUGIN_FORM.RECAPTCHA_THEME_LIGHT
|
||||
dark: PLUGIN_FORM.RECAPTCHA_THEME_DARK
|
||||
recaptcha.site_key:
|
||||
recaptcha.site_key:
|
||||
type: text
|
||||
label: PLUGIN_FORM.RECAPTCHA_SITE_KEY
|
||||
help: PLUGIN_FORM.RECAPTCHA_SITE_KEY_HELP
|
||||
default: ""
|
||||
recaptcha.secret_key:
|
||||
type: text
|
||||
label: PLUGIN_FORM.RECAPTCHA_SECRET_KEY
|
||||
help: PLUGIN_FORM.RECAPTCHA_SECRET_KEY_HELP
|
||||
default: ""
|
||||
|
||||
turnstile_captcha:
|
||||
type: section
|
||||
title: PLUGIN_FORM.TURNSTILE_CAPTCHA
|
||||
|
||||
fields:
|
||||
turnstile.theme:
|
||||
type: select
|
||||
label: PLUGIN_FORM.RECAPTCHA_THEME
|
||||
default: light
|
||||
options:
|
||||
light: PLUGIN_FORM.RECAPTCHA_THEME_LIGHT
|
||||
dark: PLUGIN_FORM.RECAPTCHA_THEME_DARK
|
||||
turnstile.site_key:
|
||||
type: text
|
||||
label: PLUGIN_FORM.RECAPTCHA_SITE_KEY
|
||||
help: PLUGIN_FORM.RECAPTCHA_SITE_KEY_HELP
|
||||
default: ""
|
||||
turnstile.secret_key:
|
||||
type: text
|
||||
label: PLUGIN_FORM.RECAPTCHA_SECRET_KEY
|
||||
help: PLUGIN_FORM.RECAPTCHA_SECRET_KEY_HELP
|
||||
default: ""
|
||||
|
||||
basic_captcha:
|
||||
type: section
|
||||
title: PLUGIN_FORM.BASIC_CAPTCHA
|
||||
|
||||
fields:
|
||||
basic_captcha.image.width:
|
||||
type: number
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_BOX_WIDTH
|
||||
default: 135
|
||||
append: px
|
||||
size: small
|
||||
validate:
|
||||
min: 100
|
||||
max: 500
|
||||
type: number
|
||||
basic_captcha.image.height:
|
||||
type: number
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_BOX_HEIGHT
|
||||
default: 40
|
||||
append: px
|
||||
size: small
|
||||
validate:
|
||||
min: 30
|
||||
max: 200
|
||||
type: number
|
||||
|
||||
basic_captcha.chars.font:
|
||||
type: select
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_FONT
|
||||
default: zxx-noise.ttf
|
||||
options:
|
||||
"zxx-noise.ttf": zxx-Noise
|
||||
"zxx-xed.ttf": zxx-Xed
|
||||
"zxx-camo.ttf": zxx-Camo
|
||||
"zxx-sans.ttf": zxx-Sans
|
||||
basic_captcha.chars.size:
|
||||
type: range
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_SIZE
|
||||
default: 24
|
||||
append: px
|
||||
validate:
|
||||
min: 12
|
||||
max: 32
|
||||
step: 2
|
||||
basic_captcha.chars.bg:
|
||||
type: colorpicker
|
||||
size: small
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_BG_COLOR
|
||||
default: "#ffffff"
|
||||
basic_captcha.chars.text:
|
||||
type: colorpicker
|
||||
size: small
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_TEXT_COLOR
|
||||
default: "#000000"
|
||||
basic_captcha.chars.start_x:
|
||||
type: number
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_START_X
|
||||
default: 5
|
||||
append: px
|
||||
size: small
|
||||
validate:
|
||||
min: 0
|
||||
type: number
|
||||
basic_captcha.chars.start_y:
|
||||
type: number
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_START_Y
|
||||
default: 30
|
||||
append: px
|
||||
size: small
|
||||
validate:
|
||||
min: 0
|
||||
type: number
|
||||
|
||||
basic_captcha.type:
|
||||
type: elements
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_TYPE
|
||||
default: "characters"
|
||||
size: medium
|
||||
options:
|
||||
characters: Random Characters
|
||||
math: Math Puzzle
|
||||
fields:
|
||||
characters:
|
||||
type: element
|
||||
fields:
|
||||
basic_captcha.chars.length:
|
||||
type: range
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_LENGTH
|
||||
default: 6
|
||||
validate:
|
||||
min: 4
|
||||
max: 12
|
||||
append: characters
|
||||
|
||||
math:
|
||||
type: element
|
||||
fields:
|
||||
basic_captcha.math.min:
|
||||
type: number
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_MATH_MIN
|
||||
default: 1
|
||||
size: small
|
||||
validate:
|
||||
min: 0
|
||||
type: number
|
||||
basic_captcha.math.max:
|
||||
type: number
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_MATH_MAX
|
||||
default: 10
|
||||
size: small
|
||||
validate:
|
||||
min: 1
|
||||
type: number
|
||||
basic_captcha.math.operators:
|
||||
type: selectize
|
||||
selectize:
|
||||
options:
|
||||
- value: "+"
|
||||
text: "+ Addition"
|
||||
- value: "-"
|
||||
text: "- Subtraction"
|
||||
- value: "*"
|
||||
text: "x Multiplication"
|
||||
- value: "/"
|
||||
text: "/ Division"
|
||||
label: PLUGIN_FORM.BASIC_CAPTCHA_MATH_OPERATORS
|
||||
validate:
|
||||
type: commalist
|
||||
596
config/www/user/plugins/form/classes/Captcha/BasicCaptcha.php
Normal file
596
config/www/user/plugins/form/classes/Captcha/BasicCaptcha.php
Normal file
@@ -0,0 +1,596 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Plugin\Form\Captcha;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
class BasicCaptcha
|
||||
{
|
||||
protected $session = null;
|
||||
protected $key = 'basic_captcha_value';
|
||||
protected $typeKey = 'basic_captcha_type';
|
||||
protected $config = null;
|
||||
|
||||
public function __construct($fieldConfig = null)
|
||||
{
|
||||
$this->session = Grav::instance()['session'];
|
||||
|
||||
// Load global configuration
|
||||
$globalConfig = Grav::instance()['config']->get('plugins.form.basic_captcha', []);
|
||||
|
||||
// Merge field-specific config with global config
|
||||
if ($fieldConfig && is_array($fieldConfig)) {
|
||||
$this->config = array_replace_recursive($globalConfig, $fieldConfig);
|
||||
} else {
|
||||
$this->config = $globalConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public function getCaptchaCode($length = null): string
|
||||
{
|
||||
// Support both 'type' (from global config) and 'captcha_type' (from field config)
|
||||
$type = $this->config['captcha_type'] ?? $this->config['type'] ?? 'characters';
|
||||
|
||||
// Store the captcha type in session for validation
|
||||
$this->setSession($this->typeKey, $type);
|
||||
|
||||
switch ($type) {
|
||||
case 'dotcount':
|
||||
return $this->getDotCountCaptcha($this->config);
|
||||
case 'position':
|
||||
return $this->getPositionCaptcha($this->config);
|
||||
case 'math':
|
||||
return $this->getMathCaptcha($this->config);
|
||||
case 'characters':
|
||||
default:
|
||||
return $this->getCharactersCaptcha($this->config, $length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dot counting captcha - user has to count dots of a specific color
|
||||
*/
|
||||
protected function getDotCountCaptcha($config): string
|
||||
{
|
||||
// Define colors with names
|
||||
$colors = [
|
||||
'red' => [255, 0, 0],
|
||||
'blue' => [0, 0, 255],
|
||||
'green' => [0, 128, 0],
|
||||
'yellow' => [255, 255, 0],
|
||||
'purple' => [128, 0, 128],
|
||||
'orange' => [255, 165, 0]
|
||||
];
|
||||
|
||||
// Pick a random color to count
|
||||
$colorNames = array_keys($colors);
|
||||
$targetColorName = $colorNames[array_rand($colorNames)];
|
||||
$targetColor = $colors[$targetColorName];
|
||||
|
||||
// Generate a random number of dots for the target color (between 5-10)
|
||||
$targetCount = mt_rand(5, 10);
|
||||
|
||||
// Store the expected answer
|
||||
$this->setSession($this->key, (string) $targetCount);
|
||||
|
||||
// Return description text
|
||||
return "count_dots|{$targetColorName}|".implode(',', $targetColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a position-based captcha - user has to identify position of a symbol
|
||||
*/
|
||||
protected function getPositionCaptcha($config): string
|
||||
{
|
||||
// Define possible symbols - using simple ASCII characters
|
||||
$symbols = ['*', '+', '$', '#', '@', '!', '?', '%', '&', '='];
|
||||
|
||||
// Define positions - simpler options
|
||||
$positions = ['top', 'bottom', 'left', 'right', 'center'];
|
||||
|
||||
// Pick a random symbol and position
|
||||
$targetSymbol = $symbols[array_rand($symbols)];
|
||||
$targetPosition = $positions[array_rand($positions)];
|
||||
|
||||
// Store the expected answer
|
||||
$this->setSession($this->key, $targetPosition);
|
||||
|
||||
// Return the instruction and symbol
|
||||
return "position|{$targetSymbol}|{$targetPosition}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a math-based captcha
|
||||
*/
|
||||
protected function getMathCaptcha($config): string
|
||||
{
|
||||
$min = $config['math']['min'] ?? 1;
|
||||
$max = $config['math']['max'] ?? 12;
|
||||
$operators = $config['math']['operators'] ?? ['+', '-', '*'];
|
||||
|
||||
$first_num = random_int($min, $max);
|
||||
$second_num = random_int($min, $max);
|
||||
$operator = $operators[array_rand($operators)];
|
||||
|
||||
// Calculator
|
||||
if ($operator === '-') {
|
||||
if ($first_num < $second_num) {
|
||||
$result = "$second_num - $first_num";
|
||||
$captcha_code = $second_num - $first_num;
|
||||
} else {
|
||||
$result = "$first_num - $second_num";
|
||||
$captcha_code = $first_num - $second_num;
|
||||
}
|
||||
} elseif ($operator === '*') {
|
||||
$result = "{$first_num} x {$second_num}";
|
||||
$captcha_code = $first_num * $second_num;
|
||||
} elseif ($operator === '+') {
|
||||
$result = "$first_num + $second_num";
|
||||
$captcha_code = $first_num + $second_num;
|
||||
}
|
||||
|
||||
$this->setSession($this->key, (string) $captcha_code);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a character-based captcha
|
||||
*/
|
||||
protected function getCharactersCaptcha($config, $length = null): string
|
||||
{
|
||||
if ($length === null) {
|
||||
$length = $config['chars']['length'] ?? 6;
|
||||
}
|
||||
|
||||
// Use more complex character set with mixed case and exclude similar-looking characters
|
||||
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789';
|
||||
$captcha_code = '';
|
||||
|
||||
// Generate random characters
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$captcha_code .= $chars[random_int(0, strlen($chars) - 1)];
|
||||
}
|
||||
|
||||
$this->setSession($this->key, $captcha_code);
|
||||
return $captcha_code;
|
||||
}
|
||||
|
||||
public function setSession($key, $value): void
|
||||
{
|
||||
$this->session->$key = $value;
|
||||
}
|
||||
|
||||
public function getSession($key = null): ?string
|
||||
{
|
||||
if ($key === null) {
|
||||
$key = $this->key;
|
||||
}
|
||||
return $this->session->$key ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create captcha image based on the type
|
||||
*/
|
||||
public function createCaptchaImage($captcha_code)
|
||||
{
|
||||
// Determine image dimensions based on type
|
||||
$isCharacterCaptcha = false;
|
||||
if (strpos($captcha_code, '|') === false && !preg_match('/[\+\-x]/', $captcha_code)) {
|
||||
$isCharacterCaptcha = true;
|
||||
}
|
||||
|
||||
// Use box_width/box_height for character captchas if specified, otherwise use default image dimensions
|
||||
if ($isCharacterCaptcha && isset($this->config['chars']['box_width'])) {
|
||||
$width = $this->config['chars']['box_width'];
|
||||
} else {
|
||||
$width = $this->config['image']['width'] ?? 135;
|
||||
}
|
||||
|
||||
if ($isCharacterCaptcha && isset($this->config['chars']['box_height'])) {
|
||||
$height = $this->config['chars']['box_height'];
|
||||
} else {
|
||||
$height = $this->config['image']['height'] ?? 40;
|
||||
}
|
||||
|
||||
// Create a blank image
|
||||
$image = imagecreatetruecolor($width, $height);
|
||||
|
||||
// Set background color (support both image.bg and chars.bg for character captchas)
|
||||
$bgColor = '#ffffff';
|
||||
if ($isCharacterCaptcha && isset($this->config['chars']['bg'])) {
|
||||
$bgColor = $this->config['chars']['bg'];
|
||||
} elseif (isset($this->config['image']['bg'])) {
|
||||
$bgColor = $this->config['image']['bg'];
|
||||
}
|
||||
|
||||
$bg = $this->hexToRgb($bgColor);
|
||||
$backgroundColor = imagecolorallocate($image, $bg[0], $bg[1], $bg[2]);
|
||||
imagefill($image, 0, 0, $backgroundColor);
|
||||
|
||||
// Parse the captcha code to determine type
|
||||
if (strpos($captcha_code, '|') !== false) {
|
||||
$parts = explode('|', $captcha_code);
|
||||
$type = $parts[0];
|
||||
|
||||
switch ($type) {
|
||||
case 'count_dots':
|
||||
return $this->createDotCountImage($image, $parts, $this->config);
|
||||
case 'position':
|
||||
return $this->createPositionImage($image, $parts, $this->config);
|
||||
}
|
||||
} else {
|
||||
// Assume it's a character or math captcha if no type indicator
|
||||
if (preg_match('/[\+\-x]/', $captcha_code)) {
|
||||
return $this->createMathImage($image, $captcha_code, $this->config);
|
||||
} else {
|
||||
return $this->createCharacterImage($image, $captcha_code, $this->config);
|
||||
}
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create image for dot counting captcha
|
||||
*/
|
||||
protected function createDotCountImage($image, $parts, $config)
|
||||
{
|
||||
$colorName = $parts[1];
|
||||
$targetColorRGB = explode(',', $parts[2]);
|
||||
|
||||
$width = imagesx($image);
|
||||
$height = imagesy($image);
|
||||
|
||||
// Allocate target color
|
||||
$targetColor = imagecolorallocate($image, $targetColorRGB[0], $targetColorRGB[1], $targetColorRGB[2]);
|
||||
|
||||
// Create other distraction colors
|
||||
$distractionColors = [];
|
||||
$colorOptions = [
|
||||
[255, 0, 0], // red
|
||||
[0, 0, 255], // blue
|
||||
[0, 128, 0], // green
|
||||
[255, 255, 0], // yellow
|
||||
[128, 0, 128], // purple
|
||||
[255, 165, 0] // orange
|
||||
];
|
||||
|
||||
foreach ($colorOptions as $rgb) {
|
||||
if ($rgb[0] != $targetColorRGB[0] || $rgb[1] != $targetColorRGB[1] || $rgb[2] != $targetColorRGB[2]) {
|
||||
$distractionColors[] = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get target count from session
|
||||
$targetCount = (int) $this->getSession();
|
||||
|
||||
// Draw instruction text
|
||||
$fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
|
||||
$black = imagecolorallocate($image, 0, 0, 0);
|
||||
imagettftext($image, 10, 0, 5, 15, $black, $fontPath, "Count {$colorName}:");
|
||||
|
||||
// Simplified approach to prevent overlapping
|
||||
// Divide the image into a grid and place one dot per cell
|
||||
$gridCells = [];
|
||||
$gridRows = 2;
|
||||
$gridCols = 4;
|
||||
|
||||
// Build available grid cells
|
||||
for ($y = 0; $y < $gridRows; $y++) {
|
||||
for ($x = 0; $x < $gridCols; $x++) {
|
||||
$gridCells[] = [$x, $y];
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle grid cells for random placement
|
||||
shuffle($gridCells);
|
||||
|
||||
// Calculate cell dimensions
|
||||
$cellWidth = ($width - 20) / $gridCols;
|
||||
$cellHeight = ($height - 20) / $gridRows;
|
||||
|
||||
// Dot size for better visibility
|
||||
$dotSize = 8;
|
||||
|
||||
// Draw target dots first (taking the first N cells)
|
||||
for ($i = 0; $i < $targetCount && $i < count($gridCells); $i++) {
|
||||
$cell = $gridCells[$i];
|
||||
$gridX = $cell[0];
|
||||
$gridY = $cell[1];
|
||||
|
||||
// Calculate center position of cell with small random offset
|
||||
$x = 10 + ($gridX + 0.5) * $cellWidth + mt_rand(-2, 2);
|
||||
$y = 20 + ($gridY + 0.5) * $cellHeight + mt_rand(-2, 2);
|
||||
|
||||
// Draw the dot
|
||||
imagefilledellipse($image, $x, $y, $dotSize, $dotSize, $targetColor);
|
||||
|
||||
// Add a small border for better contrast
|
||||
imageellipse($image, $x, $y, $dotSize + 2, $dotSize + 2, $black);
|
||||
}
|
||||
|
||||
// Draw distraction dots using remaining grid cells
|
||||
$distractionCount = min(mt_rand(8, 15), count($gridCells) - $targetCount);
|
||||
|
||||
for ($i = 0; $i < $distractionCount; $i++) {
|
||||
// Get the next available cell
|
||||
$cellIndex = $targetCount + $i;
|
||||
|
||||
if ($cellIndex >= count($gridCells)) {
|
||||
break; // No more cells available
|
||||
}
|
||||
|
||||
$cell = $gridCells[$cellIndex];
|
||||
$gridX = $cell[0];
|
||||
$gridY = $cell[1];
|
||||
|
||||
// Calculate center position of cell with small random offset
|
||||
$x = 10 + ($gridX + 0.5) * $cellWidth + mt_rand(-2, 2);
|
||||
$y = 20 + ($gridY + 0.5) * $cellHeight + mt_rand(-2, 2);
|
||||
|
||||
// Draw the dot with a random distraction color
|
||||
$color = $distractionColors[array_rand($distractionColors)];
|
||||
imagefilledellipse($image, $x, $y, $dotSize, $dotSize, $color);
|
||||
}
|
||||
|
||||
// Add subtle grid lines to help with counting
|
||||
$lightGray = imagecolorallocate($image, 230, 230, 230);
|
||||
for ($i = 1; $i < $gridCols; $i++) {
|
||||
imageline($image, 10 + $i * $cellWidth, 20, 10 + $i * $cellWidth, $height - 5, $lightGray);
|
||||
}
|
||||
for ($i = 1; $i < $gridRows; $i++) {
|
||||
imageline($image, 10, 20 + $i * $cellHeight, $width - 10, 20 + $i * $cellHeight, $lightGray);
|
||||
}
|
||||
|
||||
// Add minimal noise
|
||||
$this->addImageNoise($image, 15);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create image for position captcha
|
||||
*/
|
||||
protected function createPositionImage($image, $parts, $config)
|
||||
{
|
||||
$symbol = $parts[1];
|
||||
$position = $parts[2];
|
||||
|
||||
$width = imagesx($image);
|
||||
$height = imagesy($image);
|
||||
|
||||
// Allocate colors
|
||||
$black = imagecolorallocate($image, 0, 0, 0);
|
||||
$red = imagecolorallocate($image, 255, 0, 0);
|
||||
|
||||
// Draw instruction text
|
||||
$fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
|
||||
imagettftext($image, 9, 0, 5, 15, $black, $fontPath, "Position of symbol?");
|
||||
|
||||
// Determine symbol position based on the target position
|
||||
$symbolX = $width / 2;
|
||||
$symbolY = $height / 2;
|
||||
|
||||
switch ($position) {
|
||||
case 'top':
|
||||
$symbolX = $width / 2;
|
||||
$symbolY = 20;
|
||||
break;
|
||||
case 'bottom':
|
||||
$symbolX = $width / 2;
|
||||
$symbolY = $height - 10;
|
||||
break;
|
||||
case 'left':
|
||||
$symbolX = 20;
|
||||
$symbolY = $height / 2;
|
||||
break;
|
||||
case 'right':
|
||||
$symbolX = $width - 20;
|
||||
$symbolY = $height / 2;
|
||||
break;
|
||||
case 'center':
|
||||
$symbolX = $width / 2;
|
||||
$symbolY = $height / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Draw the symbol - make it larger and in red for visibility
|
||||
imagettftext($image, 20, 0, $symbolX - 8, $symbolY + 8, $red, $fontPath, $symbol);
|
||||
|
||||
// Draw a grid to make positions clearer
|
||||
$gray = imagecolorallocate($image, 200, 200, 200);
|
||||
imageline($image, $width / 2, 15, $width / 2, $height - 5, $gray);
|
||||
imageline($image, 5, $height / 2, $width - 5, $height / 2, $gray);
|
||||
|
||||
// Add minimal noise
|
||||
$this->addImageNoise($image, 10);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create image for math captcha
|
||||
*/
|
||||
protected function createMathImage($image, $mathExpression, $config)
|
||||
{
|
||||
$width = imagesx($image);
|
||||
$height = imagesy($image);
|
||||
|
||||
// Get font and colors
|
||||
$fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
|
||||
$textColor = imagecolorallocate($image, 0, 0, 0);
|
||||
|
||||
// Draw the math expression
|
||||
$fontSize = 16;
|
||||
$textBox = imagettfbbox($fontSize, 0, $fontPath, $mathExpression);
|
||||
$textWidth = $textBox[2] - $textBox[0];
|
||||
$textHeight = $textBox[1] - $textBox[7];
|
||||
$textX = ($width - $textWidth) / 2;
|
||||
$textY = ($height + $textHeight) / 2;
|
||||
|
||||
imagettftext($image, $fontSize, 0, $textX, $textY, $textColor, $fontPath, $mathExpression);
|
||||
|
||||
// Add visual noise and distortions to prevent OCR
|
||||
$this->addImageNoise($image, 25);
|
||||
$this->addWaveDistortion($image);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create image for character captcha
|
||||
*/
|
||||
protected function createCharacterImage($image, $captcha_code, $config)
|
||||
{
|
||||
$width = imagesx($image);
|
||||
$height = imagesy($image);
|
||||
|
||||
// Get font settings with support for custom box dimensions, position, and colors
|
||||
$fontPath = __DIR__.'/../../fonts/'.($config['chars']['font'] ?? 'zxx-xed.ttf');
|
||||
$fontSize = $config['chars']['size'] ?? 16;
|
||||
|
||||
// Support custom text color (defaults to black)
|
||||
$textColorHex = $config['chars']['text'] ?? '#000000';
|
||||
$textRgb = $this->hexToRgb($textColorHex);
|
||||
$textColor = imagecolorallocate($image, $textRgb[0], $textRgb[1], $textRgb[2]);
|
||||
|
||||
// Support custom start position (useful for fine-tuning text placement)
|
||||
$startX = $config['chars']['start_x'] ?? ($width / (strlen($captcha_code) + 2));
|
||||
$baseY = $config['chars']['start_y'] ?? ($height / 2 + 5);
|
||||
|
||||
// Draw each character with random rotation and position
|
||||
$charWidth = $width / (strlen($captcha_code) + 2);
|
||||
|
||||
for ($i = 0; $i < strlen($captcha_code); $i++) {
|
||||
$char = $captcha_code[$i];
|
||||
$angle = mt_rand(-15, 15); // Random rotation
|
||||
|
||||
// Random vertical position with custom base Y
|
||||
$y = $baseY + mt_rand(-5, 5);
|
||||
|
||||
imagettftext($image, $fontSize, $angle, $startX, $y, $textColor, $fontPath, $char);
|
||||
|
||||
// Move to next character position with some randomness
|
||||
$startX += $charWidth + mt_rand(-5, 5);
|
||||
}
|
||||
|
||||
// Add visual noise and distortions
|
||||
$this->addImageNoise($image, 25);
|
||||
$this->addWaveDistortion($image);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add random noise to the image
|
||||
*/
|
||||
protected function addImageNoise($image, $density = 100)
|
||||
{
|
||||
$width = imagesx($image);
|
||||
$height = imagesy($image);
|
||||
|
||||
// For performance, reduce density
|
||||
$density = min($density, 30);
|
||||
|
||||
// Add random dots
|
||||
for ($i = 0; $i < $density; $i++) {
|
||||
$x = mt_rand(0, $width - 1);
|
||||
$y = mt_rand(0, $height - 1);
|
||||
$shade = mt_rand(150, 200);
|
||||
$color = imagecolorallocate($image, $shade, $shade, $shade);
|
||||
imagesetpixel($image, $x, $y, $color);
|
||||
}
|
||||
|
||||
// Add a few random lines
|
||||
$lineCount = min(3, mt_rand(2, 3));
|
||||
for ($i = 0; $i < $lineCount; $i++) {
|
||||
$x1 = mt_rand(0, $width / 4);
|
||||
$y1 = mt_rand(0, $height - 1);
|
||||
$x2 = mt_rand(3 * $width / 4, $width - 1);
|
||||
$y2 = mt_rand(0, $height - 1);
|
||||
$shade = mt_rand(150, 200);
|
||||
$color = imagecolorallocate($image, $shade, $shade, $shade);
|
||||
imageline($image, $x1, $y1, $x2, $y2, $color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add wave distortion to the image
|
||||
*/
|
||||
protected function addWaveDistortion($image)
|
||||
{
|
||||
$width = imagesx($image);
|
||||
$height = imagesy($image);
|
||||
|
||||
// Create temporary image
|
||||
$temp = imagecreatetruecolor($width, $height);
|
||||
$bg = imagecolorallocate($temp, 255, 255, 255);
|
||||
imagefill($temp, 0, 0, $bg);
|
||||
|
||||
// Copy original to temp
|
||||
imagecopy($temp, $image, 0, 0, 0, 0, $width, $height);
|
||||
|
||||
// Clear original image
|
||||
$bg = imagecolorallocate($image, 255, 255, 255);
|
||||
imagefill($image, 0, 0, $bg);
|
||||
|
||||
// Apply simplified wave distortion
|
||||
$amplitude = mt_rand(1, 2);
|
||||
$period = mt_rand(10, 15);
|
||||
|
||||
// Process only every 2nd pixel for better performance
|
||||
for ($x = 0; $x < $width; $x += 2) {
|
||||
$wave = sin($x / $period) * $amplitude;
|
||||
|
||||
for ($y = 0; $y < $height; $y += 2) {
|
||||
$yp = $y + $wave;
|
||||
|
||||
if ($yp >= 0 && $yp < $height) {
|
||||
$color = imagecolorat($temp, $x, $yp);
|
||||
imagesetpixel($image, $x, $y, $color);
|
||||
|
||||
// Fill adjacent pixel for better performance
|
||||
if ($x + 1 < $width && $y + 1 < $height) {
|
||||
imagesetpixel($image, $x + 1, $y, $color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imagedestroy($temp);
|
||||
}
|
||||
|
||||
public function renderCaptchaImage($imageData): void
|
||||
{
|
||||
header("Content-type: image/jpeg");
|
||||
imagejpeg($imageData);
|
||||
}
|
||||
|
||||
public function validateCaptcha($formData): bool
|
||||
{
|
||||
$isValid = false;
|
||||
$capchaSessionData = $this->getSession();
|
||||
|
||||
// Make validation case-insensitive
|
||||
if (strtolower((string) $capchaSessionData) == strtolower((string) $formData)) {
|
||||
$isValid = true;
|
||||
}
|
||||
|
||||
// Debug validation if enabled
|
||||
$grav = Grav::instance();
|
||||
if ($grav['config']->get('plugins.form.basic_captcha.debug', false)) {
|
||||
$grav['log']->debug("Captcha Validation - Expected: '{$capchaSessionData}', Got: '{$formData}', Result: ".
|
||||
($isValid ? 'valid' : 'invalid'));
|
||||
}
|
||||
|
||||
// Regenerate a new captcha after validation
|
||||
$this->setSession($this->key, null);
|
||||
|
||||
return $isValid;
|
||||
}
|
||||
|
||||
private function hexToRgb($hex): array
|
||||
{
|
||||
return sscanf($hex, "#%02x%02x%02x");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\Form\Captcha;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
/**
|
||||
* Basic Captcha provider implementation
|
||||
*/
|
||||
class BasicCaptchaProvider implements CaptchaProviderInterface
|
||||
{
|
||||
/** @var array */
|
||||
protected $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = Grav::instance()['config']->get('plugins.form.basic_captcha', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate(array $form, array $params = []): array
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$session = $grav['session'];
|
||||
|
||||
try {
|
||||
// Get the expected answer from session
|
||||
// Make sure to use the same session key that the image generation code uses
|
||||
$expectedValue = $session->basic_captcha_value ?? null; // Changed from basic_captcha to basic_captcha_value
|
||||
|
||||
// Get the captcha type from session (stored during generation)
|
||||
$captchaType = $session->basic_captcha_type ?? null;
|
||||
|
||||
// Get the user's answer
|
||||
$userValue = $form['basic-captcha'] ?? null;
|
||||
|
||||
if (!$expectedValue) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'missing-session-data',
|
||||
'details' => ['error' => 'No captcha value found in session']
|
||||
];
|
||||
}
|
||||
|
||||
if (!$userValue) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'missing-input-response',
|
||||
'details' => ['error' => 'User did not enter a captcha value']
|
||||
];
|
||||
}
|
||||
|
||||
// Compare the values based on the type stored in session
|
||||
// If type is not in session, try to infer from global/field config
|
||||
if (!$captchaType) {
|
||||
$captchaType = $this->config['captcha_type'] ?? $this->config['type'] ?? 'characters';
|
||||
}
|
||||
|
||||
if ($captchaType === 'characters') {
|
||||
$isValid = strtolower((string)$userValue) === strtolower((string)$expectedValue);
|
||||
} else {
|
||||
// For math, dotcount, position - ensure both are treated as integers or exact match
|
||||
$isValid = (int)$userValue === (int)$expectedValue;
|
||||
}
|
||||
|
||||
if (!$isValid) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'validation-failed',
|
||||
'details' => [
|
||||
'expected' => $expectedValue,
|
||||
'received' => $userValue
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Clear the session values to prevent reuse
|
||||
$session->basic_captcha_value = null;
|
||||
$session->basic_captcha_type = null;
|
||||
|
||||
return [
|
||||
'success' => true
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'details' => ['exception' => get_class($e)]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClientProperties(string $formId, array $field): array
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$session = $grav['session'];
|
||||
|
||||
// Merge field-level configuration with global defaults
|
||||
$fieldConfig = array_replace_recursive($this->config, $field);
|
||||
|
||||
// Remove non-config keys from field array
|
||||
unset($fieldConfig['type'], $fieldConfig['label'], $fieldConfig['placeholder'],
|
||||
$fieldConfig['validate'], $fieldConfig['name'], $fieldConfig['classes']);
|
||||
|
||||
// Generate unique field ID for this form/field combination
|
||||
$fieldId = md5($formId . '_basic_captcha_' . ($field['name'] ?? 'default'));
|
||||
|
||||
// Store field configuration in session for image generation
|
||||
$session->{"basic_captcha_config_{$fieldId}"} = $fieldConfig;
|
||||
|
||||
$captchaType = $fieldConfig['type'] ?? 'math';
|
||||
|
||||
return [
|
||||
'provider' => 'basic-captcha',
|
||||
'type' => $captchaType,
|
||||
'imageUrl' => "/forms-basic-captcha-image.jpg?field={$fieldId}",
|
||||
'refreshable' => true,
|
||||
'containerId' => "basic-captcha-{$formId}",
|
||||
'fieldId' => $fieldId
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return 'forms/fields/basic-captcha/basic-captcha.html.twig';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\Form\Captcha;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
|
||||
/**
|
||||
* Factory for captcha providers
|
||||
*/
|
||||
class CaptchaFactory
|
||||
{
|
||||
/** @var array */
|
||||
protected static $providers = [];
|
||||
|
||||
/**
|
||||
* Register a captcha provider
|
||||
*
|
||||
* @param string $name Provider name
|
||||
* @param string|CaptchaProviderInterface $provider Provider class or instance
|
||||
* @return void
|
||||
*/
|
||||
public static function registerProvider(string $name, $provider): void
|
||||
{
|
||||
// If it's a class name, instantiate it
|
||||
if (is_string($provider) && class_exists($provider)) {
|
||||
$provider = new $provider();
|
||||
}
|
||||
|
||||
if (!$provider instanceof CaptchaProviderInterface) {
|
||||
Grav::instance()['log']->error("Cannot register captcha provider '{$name}': Provider must implement CaptchaProviderInterface");
|
||||
return;
|
||||
}
|
||||
|
||||
self::$providers[$name] = $provider;
|
||||
// Grav::instance()['log']->debug("Registered captcha provider: {$name}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a provider is registered
|
||||
*
|
||||
* @param string $name Provider name
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasProvider(string $name): bool
|
||||
{
|
||||
return isset(self::$providers[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a provider by name
|
||||
*
|
||||
* @param string $name Provider name
|
||||
* @return CaptchaProviderInterface|null Provider instance or null if not found
|
||||
*/
|
||||
public static function getProvider(string $name): ?CaptchaProviderInterface
|
||||
{
|
||||
return self::$providers[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered providers
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getProviders(): array
|
||||
{
|
||||
return self::$providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all default captcha providers
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function registerDefaultProviders(): void
|
||||
{
|
||||
// Register built-in providers
|
||||
self::registerProvider('recaptcha', new ReCaptchaProvider());
|
||||
self::registerProvider('turnstile', new TurnstileProvider());
|
||||
self::registerProvider('basic-captcha', new BasicCaptchaProvider());
|
||||
|
||||
// Log the registration
|
||||
// Grav::instance()['log']->debug('Registered default captcha providers');
|
||||
}
|
||||
}
|
||||
244
config/www/user/plugins/form/classes/Captcha/CaptchaManager.php
Normal file
244
config/www/user/plugins/form/classes/Captcha/CaptchaManager.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\Form\Captcha;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Plugin\Form\Form;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
|
||||
/**
|
||||
* Central manager for captcha processing
|
||||
*/
|
||||
class CaptchaManager
|
||||
{
|
||||
/**
|
||||
* Initialize the captcha manager
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function initialize(): void
|
||||
{
|
||||
// Register all default captcha providers
|
||||
CaptchaFactory::registerDefaultProviders();
|
||||
|
||||
// Allow plugins to register custom captcha providers
|
||||
Grav::instance()->fireEvent('onFormRegisterCaptchaProviders');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a captcha validation
|
||||
*
|
||||
* @param Form $form The form to validate
|
||||
* @param array|null $params Optional parameters
|
||||
* @return bool True if validation succeeded
|
||||
*/
|
||||
public static function validateCaptcha(Form $form, $params = null): bool
|
||||
{
|
||||
// Handle case where $params is a boolean (backward compatibility)
|
||||
if (!is_array($params)) {
|
||||
$params = [];
|
||||
}
|
||||
|
||||
// --- 1. Find the captcha field in the form ---
|
||||
$captchaField = null;
|
||||
$providerName = null;
|
||||
|
||||
$formFields = $form->value()->blueprints()->get('form/fields');
|
||||
foreach ($formFields as $fieldName => $fieldDef) {
|
||||
$fieldType = $fieldDef['type'] ?? null;
|
||||
|
||||
// Check for modern captcha type with provider
|
||||
if ($fieldType === 'captcha') {
|
||||
$captchaField = $fieldDef;
|
||||
$providerName = $fieldDef['provider'] ?? 'recaptcha';
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for legacy type-based providers (like basic-captcha and turnstile)
|
||||
// This is for backward compatibility
|
||||
elseif ($fieldType && CaptchaFactory::hasProvider($fieldType)) {
|
||||
$captchaField = $fieldDef;
|
||||
$providerName = $fieldType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$captchaField || !$providerName) {
|
||||
// No captcha field found or no provider specified
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- 2. Get provider and validate ---
|
||||
$provider = CaptchaFactory::getProvider($providerName);
|
||||
if (!$provider) {
|
||||
Grav::instance()['log']->error("Form Captcha: Unknown provider '{$providerName}' requested");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow plugins to modify the validation parameters
|
||||
$validationEvent = new Event([
|
||||
'form' => $form,
|
||||
'field' => $captchaField,
|
||||
'provider' => $providerName,
|
||||
'params' => $params
|
||||
]);
|
||||
Grav::instance()->fireEvent('onBeforeCaptchaValidation', $validationEvent);
|
||||
$params = $validationEvent['params'];
|
||||
|
||||
// Validate using the provider
|
||||
try {
|
||||
$result = $provider->validate($form->value()->toArray(), $params);
|
||||
|
||||
if (!$result['success']) {
|
||||
$logDetails = $result['details'] ?? [];
|
||||
$errorMessage = self::getErrorMessage($captchaField, $result['error'] ?? 'validation-failed', $providerName);
|
||||
|
||||
// Fire validation error event
|
||||
Grav::instance()->fireEvent('onFormValidationError', new Event([
|
||||
'form' => $form,
|
||||
'message' => $errorMessage,
|
||||
'provider' => $providerName
|
||||
]));
|
||||
|
||||
// Log the failure
|
||||
$uri = Grav::instance()['uri'];
|
||||
Grav::instance()['log']->warning(
|
||||
"Form Captcha ({$providerName}) validation failed: [{$uri->route()}] Details: " .
|
||||
json_encode($logDetails)
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Log success
|
||||
Grav::instance()['log']->info("Form Captcha ({$providerName}) validation successful for form: " . $form->name);
|
||||
|
||||
// Fire success event
|
||||
Grav::instance()->fireEvent('onCaptchaValidationSuccess', new Event([
|
||||
'form' => $form,
|
||||
'provider' => $providerName
|
||||
]));
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
// Handle other errors
|
||||
Grav::instance()['log']->error("Form Captcha ({$providerName}) validation error: " . $e->getMessage());
|
||||
|
||||
$errorMessage = Grav::instance()['language']->translate('PLUGIN_FORM.ERROR_VALIDATING_CAPTCHA');
|
||||
Grav::instance()->fireEvent('onFormValidationError', new Event([
|
||||
'form' => $form,
|
||||
'message' => $errorMessage,
|
||||
'provider' => $providerName,
|
||||
'exception' => $e
|
||||
]));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate error message based on error code and field definition
|
||||
*
|
||||
* @param array $field Field definition
|
||||
* @param string $errorCode Error code
|
||||
* @param string $provider Provider name
|
||||
* @return string
|
||||
*/
|
||||
protected static function getErrorMessage(array $field, string $errorCode, string $provider): string
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
|
||||
// First check for specific message in field definition
|
||||
if (isset($field['captcha_not_validated'])) {
|
||||
return $field['captcha_not_validated'];
|
||||
}
|
||||
|
||||
// Then check for specific error code message
|
||||
if ($errorCode === 'missing-input-response') {
|
||||
return $grav['language']->translate('PLUGIN_FORM.ERROR_CAPTCHA_NOT_COMPLETED');
|
||||
}
|
||||
|
||||
// Allow providers to supply custom error messages via event
|
||||
$messageEvent = new Event([
|
||||
'provider' => $provider,
|
||||
'errorCode' => $errorCode,
|
||||
'field' => $field,
|
||||
'message' => null
|
||||
]);
|
||||
$grav->fireEvent('onCaptchaErrorMessage', $messageEvent);
|
||||
|
||||
if ($messageEvent['message']) {
|
||||
return $messageEvent['message'];
|
||||
}
|
||||
|
||||
// Finally fall back to generic message
|
||||
return $grav['language']->translate('PLUGIN_FORM.ERROR_VALIDATING_CAPTCHA');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client-side initialization data for a captcha field
|
||||
*
|
||||
* @param string $formId Form ID
|
||||
* @param array $field Field definition
|
||||
* @return array Client properties
|
||||
*/
|
||||
public static function getClientProperties(string $formId, array $field): array
|
||||
{
|
||||
$providerName = $field['provider'] ?? null;
|
||||
|
||||
// Handle legacy field types as providers
|
||||
if (!$providerName && isset($field['type'])) {
|
||||
$fieldType = $field['type'];
|
||||
if (CaptchaFactory::hasProvider($fieldType)) {
|
||||
$providerName = $fieldType;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$providerName) {
|
||||
// Default to recaptcha for backward compatibility
|
||||
$providerName = 'recaptcha';
|
||||
}
|
||||
|
||||
$provider = CaptchaFactory::getProvider($providerName);
|
||||
|
||||
if (!$provider) {
|
||||
return [
|
||||
'provider' => $providerName,
|
||||
'error' => "Unknown captcha provider: {$providerName}"
|
||||
];
|
||||
}
|
||||
|
||||
return $provider->getClientProperties($formId, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template name for a captcha field
|
||||
*
|
||||
* @param array $field Field definition
|
||||
* @return string Template name
|
||||
*/
|
||||
public static function getTemplateName(array $field): string
|
||||
{
|
||||
$providerName = $field['provider'] ?? null;
|
||||
|
||||
// Handle legacy field types as providers
|
||||
if (!$providerName && isset($field['type'])) {
|
||||
$fieldType = $field['type'];
|
||||
if (CaptchaFactory::hasProvider($fieldType)) {
|
||||
$providerName = $fieldType;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$providerName) {
|
||||
// Default to recaptcha for backward compatibility
|
||||
$providerName = 'recaptcha';
|
||||
}
|
||||
|
||||
$provider = CaptchaFactory::getProvider($providerName);
|
||||
|
||||
if (!$provider) {
|
||||
return 'forms/fields/captcha/default.html.twig';
|
||||
}
|
||||
|
||||
return $provider->getTemplateName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\Form\Captcha;
|
||||
|
||||
/**
|
||||
* Interface for captcha providers
|
||||
*/
|
||||
interface CaptchaProviderInterface
|
||||
{
|
||||
/**
|
||||
* Validate a captcha response
|
||||
*
|
||||
* @param array $form Form data array
|
||||
* @param array $params Optional parameters
|
||||
* @return array Validation result with 'success' key and optional 'error' and 'details' keys
|
||||
*/
|
||||
public function validate(array $form, array $params = []): array;
|
||||
|
||||
/**
|
||||
* Get client-side properties for the captcha
|
||||
*
|
||||
* @param string $formId Form ID
|
||||
* @param array $field Field definition
|
||||
* @return array Client properties
|
||||
*/
|
||||
public function getClientProperties(string $formId, array $field): array;
|
||||
|
||||
/**
|
||||
* Get the template name for the captcha field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTemplateName(): string;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\Form\Captcha;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Uri;
|
||||
|
||||
/**
|
||||
* Google reCAPTCHA provider implementation
|
||||
*/
|
||||
class ReCaptchaProvider implements CaptchaProviderInterface
|
||||
{
|
||||
/** @var array */
|
||||
protected $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = Grav::instance()['config']->get('plugins.form.recaptcha', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate(array $form, array $params = []): array
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$uri = $grav['uri'];
|
||||
$ip = Uri::ip();
|
||||
$hostname = $uri->host();
|
||||
|
||||
try {
|
||||
$secretKey = $params['recaptcha_secret'] ?? $params['recatpcha_secret'] ??
|
||||
$this->config['secret_key'] ?? null;
|
||||
|
||||
$defaultVersion = $this->normalizeVersion($this->config['version'] ?? '2-checkbox');
|
||||
$version = $this->normalizeVersion($params['recaptcha_version'] ?? $defaultVersion);
|
||||
|
||||
$payloadVersion = $this->detectVersionFromPayload($form);
|
||||
if ($payloadVersion !== null) {
|
||||
$version = $payloadVersion;
|
||||
}
|
||||
|
||||
if (!$secretKey) {
|
||||
throw new \RuntimeException("reCAPTCHA secret key not configured.");
|
||||
}
|
||||
|
||||
$requestMethod = extension_loaded('curl') ? new \ReCaptcha\RequestMethod\CurlPost() : null;
|
||||
$recaptcha = new \ReCaptcha\ReCaptcha($secretKey, $requestMethod);
|
||||
|
||||
// Handle V3
|
||||
if ($version === '3') {
|
||||
// For V3, look for token in both top level and data[] structure
|
||||
$token = $form['token'] ?? ($form['data']['token'] ?? null);
|
||||
$action = $form['action'] ?? ($form['data']['action'] ?? null);
|
||||
|
||||
if (!$token) {
|
||||
$grav['log']->debug('reCAPTCHA validation failed: token missing for v3');
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'missing-input-response',
|
||||
'details' => ['error' => 'missing-input-response', 'version' => 'v3']
|
||||
];
|
||||
}
|
||||
|
||||
$recaptcha->setExpectedHostname($hostname);
|
||||
|
||||
// Set action if provided
|
||||
if ($action) {
|
||||
$recaptcha->setExpectedAction($action);
|
||||
}
|
||||
|
||||
// Set score threshold
|
||||
$recaptcha->setScoreThreshold($this->config['score_threshold'] ?? 0.5);
|
||||
}
|
||||
// Handle V2 (both checkbox and invisible)
|
||||
else {
|
||||
// For V2, look for standard response parameter
|
||||
$token = $form['g-recaptcha-response'] ?? ($form['data']['g-recaptcha-response'] ?? null);
|
||||
if (!$token) {
|
||||
$post = $grav['uri']->post();
|
||||
if (is_array($post)) {
|
||||
if (isset($post['g-recaptcha-response'])) {
|
||||
$token = $post['g-recaptcha-response'];
|
||||
} elseif (isset($post['g_recaptcha_response'])) {
|
||||
$token = $post['g_recaptcha_response'];
|
||||
} elseif (isset($post['data']) && is_array($post['data'])) {
|
||||
if (isset($post['data']['g-recaptcha-response'])) {
|
||||
$token = $post['data']['g-recaptcha-response'];
|
||||
} elseif (isset($post['data']['g_recaptcha_response'])) {
|
||||
$token = $post['data']['g_recaptcha_response'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
$grav['log']->debug('reCAPTCHA validation failed: g-recaptcha-response missing for v2');
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'missing-input-response',
|
||||
'details' => ['error' => 'missing-input-response', 'version' => 'v2']
|
||||
];
|
||||
}
|
||||
|
||||
$recaptcha->setExpectedHostname($hostname);
|
||||
}
|
||||
|
||||
// Log validation attempt
|
||||
$grav['log']->debug('reCAPTCHA validation attempt for version ' . $version);
|
||||
|
||||
$validationResponseObject = $recaptcha->verify($token, $ip);
|
||||
$isValid = $validationResponseObject->isSuccess();
|
||||
|
||||
if (!$isValid) {
|
||||
$errorCodes = $validationResponseObject->getErrorCodes();
|
||||
$grav['log']->debug('reCAPTCHA validation failed: ' . json_encode($errorCodes));
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'validation-failed',
|
||||
'details' => ['error-codes' => $errorCodes, 'version' => $version]
|
||||
];
|
||||
}
|
||||
|
||||
// For V3, check if score is available and log it (helpful for debugging/tuning)
|
||||
if ($version === '3' && method_exists($validationResponseObject, 'getScore')) {
|
||||
$score = $validationResponseObject->getScore();
|
||||
$grav['log']->debug('reCAPTCHA v3 validation successful with score: ' . $score);
|
||||
} else {
|
||||
$grav['log']->debug('reCAPTCHA validation successful');
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$grav['log']->error('reCAPTCHA validation error: ' . $e->getMessage());
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'details' => ['exception' => get_class($e)]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize version values to the internal format we use elsewhere.
|
||||
*/
|
||||
protected function normalizeVersion($version): string
|
||||
{
|
||||
if ($version === null || $version === '') {
|
||||
return '2-checkbox';
|
||||
}
|
||||
|
||||
if ($version === 3 || $version === '3') {
|
||||
return '3';
|
||||
}
|
||||
|
||||
if ($version === 2 || $version === '2') {
|
||||
return '2-checkbox';
|
||||
}
|
||||
|
||||
return (string) $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the recaptcha version from the submitted payload when possible.
|
||||
*/
|
||||
protected function detectVersionFromPayload(array $form): ?string
|
||||
{
|
||||
$formData = isset($form['data']) && is_array($form['data']) ? $form['data'] : [];
|
||||
|
||||
$grav = Grav::instance();
|
||||
$config = $grav['config'];
|
||||
if ($config->get('plugins.form.debug')) {
|
||||
try {
|
||||
$grav['log']->debug('reCAPTCHA payload inspection', [
|
||||
'top_keys' => array_keys($form),
|
||||
'data_keys' => array_keys($formData),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
// Ignore logging issues, detection should continue.
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('token', $form) || array_key_exists('token', $formData)) {
|
||||
return '3';
|
||||
}
|
||||
|
||||
if (array_key_exists('g-recaptcha-response', $form) || array_key_exists('g-recaptcha-response', $formData)) {
|
||||
return '2-checkbox';
|
||||
}
|
||||
|
||||
if (array_key_exists('g_recaptcha_response', $form) || array_key_exists('g_recaptcha_response', $formData)) {
|
||||
// Support alternative key naming just in case
|
||||
return '2-checkbox';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClientProperties(string $formId, array $field): array
|
||||
{
|
||||
$siteKey = $field['recaptcha_site_key'] ?? $this->config['site_key'] ?? null;
|
||||
$theme = $field['recaptcha_theme'] ?? $this->config['theme'] ?? 'light';
|
||||
$version = $this->normalizeVersion($field['recaptcha_version'] ?? $this->config['version'] ?? '2-checkbox');
|
||||
|
||||
// Determine which version we're using
|
||||
$isV3 = $version === '3';
|
||||
$isInvisible = $version === '2-invisible';
|
||||
|
||||
// Log the configuration to help with debugging
|
||||
$grav = Grav::instance();
|
||||
$grav['log']->debug("reCAPTCHA config for form {$formId}: version={$version}, siteKey=" .
|
||||
(empty($siteKey) ? 'MISSING' : 'configured'));
|
||||
|
||||
return [
|
||||
'provider' => 'recaptcha',
|
||||
'siteKey' => $siteKey,
|
||||
'theme' => $theme,
|
||||
'version' => $version,
|
||||
'isV3' => $isV3,
|
||||
'isInvisible' => $isInvisible,
|
||||
'containerId' => "g-recaptcha-{$formId}",
|
||||
'scriptUrl' => "https://www.google.com/recaptcha/api.js" . ($isV3 ? '?render=' . $siteKey : ''),
|
||||
'initFunctionName' => "initRecaptcha_{$formId}"
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
// Different templates based on version
|
||||
$version = $this->normalizeVersion($this->config['version'] ?? '2-checkbox');
|
||||
|
||||
$isV3 = $version === '3';
|
||||
$isInvisible = $version === '2-invisible';
|
||||
|
||||
if ($isV3) {
|
||||
return 'forms/fields/recaptcha/recaptchav3.html.twig';
|
||||
} elseif ($isInvisible) {
|
||||
return 'forms/fields/recaptcha/recaptcha-invisible.html.twig';
|
||||
}
|
||||
|
||||
return 'forms/fields/recaptcha/recaptcha.html.twig';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
namespace Grav\Plugin\Form\Captcha;
|
||||
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\HTTP\Client;
|
||||
|
||||
/**
|
||||
* Cloudflare Turnstile provider implementation
|
||||
*/
|
||||
class TurnstileProvider implements CaptchaProviderInterface
|
||||
{
|
||||
/** @var array */
|
||||
protected $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = Grav::instance()['config']->get('plugins.form.turnstile', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate(array $form, array $params = []): array
|
||||
{
|
||||
$grav = Grav::instance();
|
||||
$uri = $grav['uri'];
|
||||
$ip = Uri::ip();
|
||||
|
||||
$grav['log']->debug('Turnstile validation - entire form data: ' . json_encode(array_keys($form)));
|
||||
|
||||
try {
|
||||
$secretKey = $params['turnstile_secret'] ??
|
||||
$this->config['secret_key'] ?? null;
|
||||
|
||||
if (!$secretKey) {
|
||||
$grav['log']->error("Turnstile secret key not configured.");
|
||||
throw new \RuntimeException("Turnstile secret key not configured.");
|
||||
}
|
||||
|
||||
// First check $_POST directly, then fallback to form data
|
||||
$token = $_POST['cf-turnstile-response'] ?? null;
|
||||
if (!$token) {
|
||||
$token = $form['cf-turnstile-response'] ?? null;
|
||||
}
|
||||
|
||||
// Log raw POST data for debugging
|
||||
$grav['log']->debug('Turnstile validation - raw POST data keys: ' . json_encode(array_keys($_POST)));
|
||||
$grav['log']->debug('Turnstile validation - token present: ' . ($token ? 'YES' : 'NO'));
|
||||
|
||||
if ($token) {
|
||||
$grav['log']->debug('Turnstile token length: ' . strlen($token));
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
$grav['log']->warning('Turnstile validation failed: missing token response');
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'missing-input-response',
|
||||
'details' => ['error' => 'missing-input-response']
|
||||
];
|
||||
}
|
||||
|
||||
$client = \Grav\Common\HTTP\Client::getClient();
|
||||
$grav['log']->debug('Turnstile validation - calling API with token');
|
||||
|
||||
$response = $client->request('POST', 'https://challenges.cloudflare.com/turnstile/v0/siteverify', [
|
||||
'body' => [
|
||||
'secret' => $secretKey,
|
||||
'response' => $token,
|
||||
'remoteip' => $ip
|
||||
]
|
||||
]);
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$grav['log']->debug('Turnstile API response status: ' . $statusCode);
|
||||
|
||||
$content = $response->toArray();
|
||||
$grav['log']->debug('Turnstile API response: ' . json_encode($content));
|
||||
|
||||
if (!isset($content['success'])) {
|
||||
$grav['log']->error("Invalid response from Turnstile verification (missing 'success' key).");
|
||||
throw new \RuntimeException("Invalid response from Turnstile verification (missing 'success' key).");
|
||||
}
|
||||
|
||||
if (!$content['success']) {
|
||||
$grav['log']->warning('Turnstile validation failed: ' . json_encode($content));
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'validation-failed',
|
||||
'details' => ['error-codes' => $content['error-codes'] ?? ['validation-failed']]
|
||||
];
|
||||
}
|
||||
|
||||
$grav['log']->debug('Turnstile validation successful');
|
||||
return [
|
||||
'success' => true
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$grav['log']->error("Turnstile validation error: " . $e->getMessage());
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'details' => ['exception' => get_class($e)]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getClientProperties(string $formId, array $field): array
|
||||
{
|
||||
$siteKey = $field['turnstile_site_key'] ?? $this->config['site_key'] ?? null;
|
||||
$theme = $field['turnstile_theme'] ?? $this->config['theme'] ?? 'auto';
|
||||
|
||||
return [
|
||||
'provider' => 'turnstile',
|
||||
'siteKey' => $siteKey,
|
||||
'theme' => $theme,
|
||||
'containerId' => "cf-turnstile-{$formId}",
|
||||
'scriptUrl' => "https://challenges.cloudflare.com/turnstile/v0/api.js",
|
||||
'initFunctionName' => "initTurnstile_{$formId}"
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTemplateName(): string
|
||||
{
|
||||
return 'forms/fields/turnstile/turnstile.html.twig';
|
||||
}
|
||||
}
|
||||
1462
config/www/user/plugins/form/classes/Form.php
Normal file
1462
config/www/user/plugins/form/classes/Form.php
Normal file
File diff suppressed because it is too large
Load Diff
38
config/www/user/plugins/form/classes/FormFactory.php
Normal file
38
config/www/user/plugins/form/classes/FormFactory.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Grav\Plugin\Form;
|
||||
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Page;
|
||||
use Grav\Framework\Form\Interfaces\FormFactoryInterface;
|
||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
||||
|
||||
class FormFactory implements FormFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create form using the header of the page.
|
||||
*
|
||||
* @param Page $page
|
||||
* @param string $name
|
||||
* @param array $form
|
||||
* @return Form|null
|
||||
* @deprecated 1.6 Use FormFactory::createFormByPage() instead.
|
||||
*/
|
||||
public function createPageForm(Page $page, string $name, array $form): ?FormInterface
|
||||
{
|
||||
return new Form($page, $name, $form);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create form using the header of the page.
|
||||
*
|
||||
* @param PageInterface $page
|
||||
* @param string $name
|
||||
* @param array $form
|
||||
* @return Form|null
|
||||
*/
|
||||
public function createFormForPage(PageInterface $page, string $name, array $form): ?FormInterface
|
||||
{
|
||||
return new Form($page, $name, $form);
|
||||
}
|
||||
}
|
||||
129
config/www/user/plugins/form/classes/Forms.php
Normal file
129
config/www/user/plugins/form/classes/Forms.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Grav\Plugin\Form;
|
||||
|
||||
use Grav\Common\Page\Interfaces\PageInterface;
|
||||
use Grav\Common\Page\Page;
|
||||
use Grav\Framework\Form\Interfaces\FormFactoryInterface;
|
||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
||||
|
||||
class Forms
|
||||
{
|
||||
/** @var array|FormFactoryInterface[] */
|
||||
private $types;
|
||||
/** @var FormInterface|null */
|
||||
private $form;
|
||||
|
||||
/**
|
||||
* Forms constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->registerType('form', new FormFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param FormFactoryInterface $factory
|
||||
*/
|
||||
public function registerType(string $type, FormFactoryInterface $factory): void
|
||||
{
|
||||
$this->types[$type] = $factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*/
|
||||
public function unregisterType($type): void
|
||||
{
|
||||
unset($this->types[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public function hasType(string $type): bool
|
||||
{
|
||||
return isset($this->types[$type]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
return array_keys($this->types);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PageInterface $page
|
||||
* @param string|null $name
|
||||
* @param array|null $form
|
||||
* @return FormInterface|null
|
||||
*/
|
||||
public function createPageForm(PageInterface $page, ?string $name = null, ?array $form = null): ?FormInterface
|
||||
{
|
||||
if (null === $form) {
|
||||
[$name, $form] = $this->getPageParameters($page, $name);
|
||||
}
|
||||
|
||||
if (null === $form) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $form['type'] ?? 'form';
|
||||
$factory = $this->types[$type] ?? null;
|
||||
|
||||
if ($factory) {
|
||||
if (is_callable([$factory, 'createFormForPage'])) {
|
||||
return $factory->createFormForPage($page, $name, $form);
|
||||
}
|
||||
|
||||
if ($page instanceof Page) {
|
||||
// @phpstan-ignore-next-line
|
||||
return $factory->createPageForm($page, $name, $form);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface|null
|
||||
*/
|
||||
public function getActiveForm(): ?FormInterface
|
||||
{
|
||||
return $this->form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormInterface $form
|
||||
* @return void
|
||||
*/
|
||||
public function setActiveForm(FormInterface $form): void
|
||||
{
|
||||
$this->form = $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PageInterface $page
|
||||
* @param string|null $name
|
||||
* @return array
|
||||
*/
|
||||
protected function getPageParameters(PageInterface $page, ?string $name): array
|
||||
{
|
||||
$forms = $page->getForms();
|
||||
|
||||
if ($name) {
|
||||
// If form with given name was found, use that.
|
||||
$form = $forms[$name] ?? null;
|
||||
} else {
|
||||
// Otherwise pick up the first form.
|
||||
$form = reset($forms) ?: null;
|
||||
$name = (string)key($forms);
|
||||
}
|
||||
|
||||
return [$name, $form];
|
||||
}
|
||||
}
|
||||
163
config/www/user/plugins/form/classes/TwigExtension.php
Normal file
163
config/www/user/plugins/form/classes/TwigExtension.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Grav\Plugin\Form;
|
||||
|
||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* Class GravExtension
|
||||
* @package Grav\Common\Twig\Extension
|
||||
*/
|
||||
class TwigExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters()
|
||||
{
|
||||
return [
|
||||
new TwigFilter('value_and_label', [$this, 'valueAndLabel'])
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of all functions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('prepare_form_fields', [$this, 'prepareFormFields'], ['needs_context' => true]),
|
||||
new TwigFunction('prepare_form_field', [$this, 'prepareFormField'], ['needs_context' => true]),
|
||||
new TwigFunction('include_form_field', [$this, 'includeFormField']),
|
||||
];
|
||||
}
|
||||
|
||||
public function valueAndLabel($value): array
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [];
|
||||
foreach ($value as $key => $label) {
|
||||
$list[] = ['value' => $key, 'label' => $label];
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters form fields for the current parent.
|
||||
*
|
||||
* @param array $context
|
||||
* @param array $fields Form fields
|
||||
* @param string|null $parent Parent field name if available
|
||||
* @return array
|
||||
*/
|
||||
public function prepareFormFields(array $context, $fields, $parent = null): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
if (is_iterable($fields)) {
|
||||
foreach ($fields as $name => $field) {
|
||||
$field = $this->prepareFormField($context, $field, $name, $parent);
|
||||
if ($field) {
|
||||
$list[$field['name']] = $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters field name by changing dot notation into array notation.
|
||||
*
|
||||
* @param array $context
|
||||
* @param array $field Form field
|
||||
* @param string|int|null $name Field name (defaults to field.name)
|
||||
* @param string|null $parent Parent field name if available
|
||||
* @param array|null $options List of options to override
|
||||
* @return array|null
|
||||
*/
|
||||
public function prepareFormField(array $context, $field, $name = null, $parent = null, array $options = []): ?array
|
||||
{
|
||||
// Make sure that the field is a valid form field type and is not being ignored.
|
||||
if (empty($field['type']) || ($field['validate']['ignore'] ?? false)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If field has already been prepared, we do not need to do anything.
|
||||
if (!empty($field['prepared'])) {
|
||||
return $field;
|
||||
}
|
||||
|
||||
// Check if we have just a list of fields (no name given).
|
||||
$fieldName = (string)($field['name'] ?? $name);
|
||||
if (!is_string($name) || $name === '') {
|
||||
// Look at the field.name and if not set, fall back to the key.
|
||||
$name = $fieldName;
|
||||
}
|
||||
|
||||
// Make sure that the field has a name.
|
||||
if ($name === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Prefix name with the parent name if needed.
|
||||
if (str_starts_with($name, '.')) {
|
||||
$plainName = (string)substr($name, 1);
|
||||
$field['plain_name'] = $plainName;
|
||||
$name = $parent ? $parent . $name : $plainName;
|
||||
} elseif (isset($options['key'])) {
|
||||
$name = str_replace('*', $options['key'], $name);
|
||||
}
|
||||
|
||||
unset($options['key']);
|
||||
|
||||
// Set fields as readonly if form is in readonly mode.
|
||||
/** @var FormInterface $form */
|
||||
$form = $context['form'] ?? null;
|
||||
if ($form && method_exists($form, 'isEnabled') && !$form->isEnabled()) {
|
||||
$options['disabled'] = true;
|
||||
}
|
||||
|
||||
// Loop through options
|
||||
foreach ($options as $key => $option) {
|
||||
$field[$key] = $option;
|
||||
}
|
||||
|
||||
// Always set field name.
|
||||
$field['name'] = $name;
|
||||
$field['prepared'] = true;
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param string|string[]|null $layouts
|
||||
* @param string|null $default
|
||||
* @return string[]
|
||||
*/
|
||||
public function includeFormField(string $type, $layouts = null, ?string $default = null): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ((array)$layouts as $layout) {
|
||||
$list[] = "forms/fields/{$type}/{$layout}-{$type}.html.twig";
|
||||
}
|
||||
$list[] = "forms/fields/{$type}/{$type}.html.twig";
|
||||
|
||||
if ($default) {
|
||||
foreach ((array)$layouts as $layout) {
|
||||
$list[] = "forms/fields/{$default}/{$layout}-{$default}.html.twig";
|
||||
}
|
||||
$list[] = "forms/fields/{$default}/{$default}.html.twig";
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
40
config/www/user/plugins/form/composer.json
Normal file
40
config/www/user/plugins/form/composer.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "getgrav/grav-plugin-form",
|
||||
"type": "grav-plugin",
|
||||
"description": "Grav Plugin Form",
|
||||
"keywords": ["form"],
|
||||
"homepage": "https://github.com/getgrav/grav-plugin-form",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Team Grav",
|
||||
"email": "devs@getgrav.org",
|
||||
"homepage": "https://getgrav.org",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/getgrav/grav-plugin-form/issues",
|
||||
"irc": "https://chat.getgrav.org",
|
||||
"forum": "https://discourse.getgrav.org",
|
||||
"docs": "https://github.com/getgrav/grav-plugin-form/blob/master/README.md"
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.3.6 || ^8.0",
|
||||
"ext-json": "*",
|
||||
"google/recaptcha": "^1.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Grav\\Plugin\\Form\\": "classes/"
|
||||
},
|
||||
"classmap": [
|
||||
"form.php"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.3.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
77
config/www/user/plugins/form/composer.lock
generated
Normal file
77
config/www/user/plugins/form/composer.lock
generated
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e0e2ccc2fca6a3cb5d548cf61bb80f66",
|
||||
"packages": [
|
||||
{
|
||||
"name": "google/recaptcha",
|
||||
"version": "1.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/google/recaptcha.git",
|
||||
"reference": "614f25a9038be4f3f2da7cbfd778dc5b357d2419"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/google/recaptcha/zipball/614f25a9038be4f3f2da7cbfd778dc5b357d2419",
|
||||
"reference": "614f25a9038be4f3f2da7cbfd778dc5b357d2419",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.2.20|^2.15",
|
||||
"php-coveralls/php-coveralls": "^2.1",
|
||||
"phpunit/phpunit": "^4.8.36|^5.7.27|^6.59|^7.5.11"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.2.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ReCaptcha\\": "src/ReCaptcha"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"description": "Client library for reCAPTCHA, a free service that protects websites from spam and abuse.",
|
||||
"homepage": "https://www.google.com/recaptcha/",
|
||||
"keywords": [
|
||||
"Abuse",
|
||||
"captcha",
|
||||
"recaptcha",
|
||||
"spam"
|
||||
],
|
||||
"support": {
|
||||
"forum": "https://groups.google.com/forum/#!forum/recaptcha",
|
||||
"issues": "https://github.com/google/recaptcha/issues",
|
||||
"source": "https://github.com/google/recaptcha"
|
||||
},
|
||||
"time": "2020-03-31T17:50:54+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": "^7.3.6 || ^8.0",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-overrides": {
|
||||
"php": "7.3.6"
|
||||
},
|
||||
"plugin-api-version": "2.2.0"
|
||||
}
|
||||
BIN
config/www/user/plugins/form/fonts/zxx-camo.ttf
Normal file
BIN
config/www/user/plugins/form/fonts/zxx-camo.ttf
Normal file
Binary file not shown.
BIN
config/www/user/plugins/form/fonts/zxx-noise.ttf
Normal file
BIN
config/www/user/plugins/form/fonts/zxx-noise.ttf
Normal file
Binary file not shown.
BIN
config/www/user/plugins/form/fonts/zxx-sans.ttf
Normal file
BIN
config/www/user/plugins/form/fonts/zxx-sans.ttf
Normal file
Binary file not shown.
BIN
config/www/user/plugins/form/fonts/zxx-xed.ttf
Normal file
BIN
config/www/user/plugins/form/fonts/zxx-xed.ttf
Normal file
Binary file not shown.
1312
config/www/user/plugins/form/form.php
Normal file
1312
config/www/user/plugins/form/form.php
Normal file
File diff suppressed because it is too large
Load Diff
54
config/www/user/plugins/form/form.yaml
Normal file
54
config/www/user/plugins/form/form.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
enabled: true
|
||||
built_in_css: true
|
||||
inline_css: true
|
||||
refresh_prevention: false
|
||||
client_side_validation: true
|
||||
debug: false
|
||||
inline_errors: false
|
||||
modular_form_fix: true
|
||||
files:
|
||||
multiple: false # To allow multiple files, default is single
|
||||
limit: 10 # Number of allowed files per field (multiple required)
|
||||
destination: 'self@' # Where to upload the files (path and self@, page@, theme@)
|
||||
avoid_overwriting: false # Prevent files with the same name to be overridden. Date prefix will be added
|
||||
random_name: false # Generate a random 15 long string name for the uploaded files
|
||||
filesize: 0 # Maximum file size allowed (in MB)
|
||||
accept: # List of mime/types or file extensions allowed (ie, image/*,.zip,.mp4)
|
||||
- image/*
|
||||
recaptcha:
|
||||
version: 2-checkbox
|
||||
theme: light
|
||||
site_key:
|
||||
secret_key:
|
||||
turnstile:
|
||||
theme: light # options: [light | dark]
|
||||
site_key:
|
||||
secret_key:
|
||||
|
||||
basic_captcha:
|
||||
type: math # Options: dotcount, position, math, characters
|
||||
debug: false # Enable debug logging for troubleshooting
|
||||
|
||||
# Image settings
|
||||
image:
|
||||
width: 135 # Image width (default: 135 for classic size)
|
||||
height: 40 # Image height (default: 40 for classic size)
|
||||
bg: '#ffffff' # Background color
|
||||
|
||||
# Character captcha settings (used for the 'characters' type)
|
||||
chars:
|
||||
length: 6 # Number of characters to display
|
||||
font: zxx-xed.ttf # Font file in the plugin's fonts directory
|
||||
size: 24 # Font size
|
||||
box_width: 200 # Image width
|
||||
box_height: 70 # Image height
|
||||
start_x: 10 # Starting X position for text
|
||||
start_y: 40 # Starting Y position for text
|
||||
bg: '#ffffff' # Background color
|
||||
text: '#000000' # Text color
|
||||
|
||||
# Math puzzle settings (used for the 'math' type)
|
||||
math:
|
||||
min: 1 # Minimum number value
|
||||
max: 12 # Maximum number value
|
||||
operators: ['+','-','*'] # Available operators
|
||||
84
config/www/user/plugins/form/gulpfile.js
Normal file
84
config/www/user/plugins/form/gulpfile.js
Normal file
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var util = require('util');
|
||||
var path = require('path');
|
||||
var immutable = require('immutable');
|
||||
var gulpWebpack = require('gulp-webpack');
|
||||
var webpack = require('webpack');
|
||||
var sourcemaps = require('gulp-sourcemaps');
|
||||
var exec = require('child_process').execSync;
|
||||
var sass = require('gulp-sass');
|
||||
var cleancss = require('gulp-clean-css');
|
||||
var csscomb = require('gulp-csscomb');
|
||||
var rename = require('gulp-rename');
|
||||
var autoprefixer = require('gulp-autoprefixer');
|
||||
var pwd = exec('pwd').toString();
|
||||
|
||||
// configure the paths
|
||||
var watch_dir = './scss/**/*.scss';
|
||||
var src_dir = './scss/*.scss';
|
||||
var dest_dir = './assets';
|
||||
|
||||
var paths = {
|
||||
source: src_dir
|
||||
};
|
||||
|
||||
var plugins = {};
|
||||
var base = immutable.fromJS(require('./webpack.conf.js'));
|
||||
var options = {
|
||||
prod: base.mergeDeep({
|
||||
devtool: 'source-map',
|
||||
optimization: {minimize: true},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': { NODE_ENV: '"production"' }
|
||||
}),
|
||||
new webpack.ProvidePlugin(plugins)
|
||||
],
|
||||
output: {
|
||||
filename: 'form.min.js'
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
// var compileJS = function(watch) {
|
||||
// var prodOpts = options.prod.set('watch', watch);
|
||||
//
|
||||
// return gulp.src('app/main.js')
|
||||
// .pipe(gulpWebpack(prodOpts.toJS()))
|
||||
// .pipe(gulp.dest('assets/'));
|
||||
// };
|
||||
|
||||
var compileCSS = function() {
|
||||
return gulp.src(paths.source)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({outputStyle: 'compact', precision: 10})
|
||||
.on('error', sass.logError)
|
||||
)
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulp.dest(dest_dir))
|
||||
.pipe(csscomb())
|
||||
.pipe(cleancss())
|
||||
.pipe(rename({
|
||||
suffix: '.min'
|
||||
}))
|
||||
.pipe(gulp.dest(dest_dir));
|
||||
};
|
||||
|
||||
// gulp.task('js', function() {
|
||||
// compileJS(false);
|
||||
// });
|
||||
|
||||
gulp.task('css', function() {
|
||||
compileCSS();
|
||||
});
|
||||
|
||||
gulp.task('watch', function() {
|
||||
gulp.watch(watch_dir, ['css']);
|
||||
// compileJS(true);
|
||||
});
|
||||
|
||||
gulp.task('all', ['css']);
|
||||
gulp.task('default', ['all']);
|
||||
1608
config/www/user/plugins/form/languages.yaml
Normal file
1608
config/www/user/plugins/form/languages.yaml
Normal file
File diff suppressed because it is too large
Load Diff
8861
config/www/user/plugins/form/package-lock.json
generated
Normal file
8861
config/www/user/plugins/form/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
config/www/user/plugins/form/package.json
Normal file
48
config/www/user/plugins/form/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "grav-plugin-form",
|
||||
"version": "1.0.0",
|
||||
"description": "Grav Plugin Form",
|
||||
"repository": "https://github.com/getgrav/grav-plugin-form",
|
||||
"main": "app/main.js",
|
||||
"scripts": {
|
||||
"watch": "NODE_ENV=development webpack --watch --progress --colors --mode development --config webpack.conf.js",
|
||||
"dev": "NODE_ENV=development webpack --progress --colors --config webpack.conf.js",
|
||||
"prod": "NODE_ENV=production-wip webpack --mode development --config webpack.conf.js"
|
||||
},
|
||||
"author": "Team Grav",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dropzone": "getgrav/dropzone#master",
|
||||
"exif-js": "^2.3.0",
|
||||
"filepond": "^4.32.7",
|
||||
"filepond-plugin-file-validate-size": "^2.2.8",
|
||||
"filepond-plugin-file-validate-type": "^1.2.9",
|
||||
"filepond-plugin-image-preview": "^4.6.12",
|
||||
"filepond-plugin-image-resize": "^2.0.10",
|
||||
"sortablejs": "^1.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-loader": "^3.0.4",
|
||||
"exports-loader": "^0.7.0",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-autoprefixer": "^9.0.0",
|
||||
"gulp-clean-css": "^4.3.0",
|
||||
"gulp-csscomb": "^0.1.0",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-webpack": "^0.0.1",
|
||||
"immutable": "^4.0.0-rc.12",
|
||||
"imports-loader": "^0.8.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"style-loader": "^1.1.3",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"webpack": "^5.98.0",
|
||||
"webpack-cli": "^6.0.1"
|
||||
}
|
||||
}
|
||||
2
config/www/user/plugins/form/scss.sh
Executable file
2
config/www/user/plugins/form/scss.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
sass --watch -s compressed scss:assets
|
||||
347
config/www/user/plugins/form/scss/form-styles.scss
Normal file
347
config/www/user/plugins/form/scss/form-styles.scss
Normal file
@@ -0,0 +1,347 @@
|
||||
$form-border-color: #ccc;
|
||||
$form-active-color: #000;
|
||||
|
||||
.form-group.has-errors {
|
||||
background: rgba(255,0,0,0.05);
|
||||
border: 1px solid rgba(255,0,0,0.2);
|
||||
border-radius: 3px;
|
||||
margin: 0 -5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.form-errors {
|
||||
color: #b52b27;
|
||||
}
|
||||
|
||||
.form-honeybear {
|
||||
display: none;
|
||||
position: absolute !important;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: rect(0px, 1px, 1px, 0px);
|
||||
}
|
||||
|
||||
.form-errors p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-input-file {
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dz-default.dz-message {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.dropzone {
|
||||
position: relative;
|
||||
min-height: 70px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: .85rem;
|
||||
border: 2px dashed #ccc;
|
||||
color: #aaa;
|
||||
padding: 0.5rem;
|
||||
|
||||
.dz-preview {
|
||||
margin: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dz-image img {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dz-remove {
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
display: inline-flex;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
background-color: red;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
z-index: 20;
|
||||
&:hover {
|
||||
background-color: darkred;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dz-error-message {
|
||||
min-width: 140px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.dz-image,
|
||||
&.dz-file-preview .dz-image {
|
||||
border-radius: 3px;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Filepond
|
||||
.filepond--root.form-input {
|
||||
min-height: 7rem;
|
||||
height: auto;
|
||||
overflow:hidden;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
// New JS powered tabs
|
||||
.form-tabs {
|
||||
|
||||
.tabs-nav {
|
||||
display: flex;
|
||||
padding-top: 1px;
|
||||
|
||||
margin-bottom: -1px;
|
||||
|
||||
a {
|
||||
flex: 1;
|
||||
transition: color 0.5s ease, background 0.5s ease;
|
||||
cursor: pointer;
|
||||
text-align:center;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid $form-border-color;
|
||||
border-radius: 5px 5px 0 0;
|
||||
|
||||
&.active {
|
||||
border: 1px solid $form-border-color;
|
||||
border-bottom: 1px solid transparent;
|
||||
margin: 0 -1px;
|
||||
|
||||
span {
|
||||
color: $form-active-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.subtle .tabs-nav {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.tabs-content {
|
||||
|
||||
.tab__content {
|
||||
display: none;
|
||||
padding-top: 2rem;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checkboxes
|
||||
.checkboxes {
|
||||
display: inline-block;
|
||||
|
||||
label {
|
||||
display: inline;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding: 0 0 0 20px;
|
||||
margin-right: 15px;
|
||||
|
||||
}
|
||||
label:before {
|
||||
content:"";
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
margin-top: 0;
|
||||
margin-right: 10px;
|
||||
position: absolute;
|
||||
border-radius: 3px;
|
||||
|
||||
border: 1px solid #e6e6e6;
|
||||
|
||||
}
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
input[type=checkbox]:checked + label:before {
|
||||
content:"\2713";
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.toggleable label{
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggleable
|
||||
.form-field-toggleable {
|
||||
.checkboxes.toggleable {
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.checkboxes + label {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggles
|
||||
.switch-toggle {
|
||||
display: inline-flex;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
line-height: 35px;
|
||||
border: 1px solid $form-border-color;
|
||||
|
||||
input[type=radio] {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
padding: 0 15px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
color: inherit;
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
input.highlight:checked + label {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
input:checked + label {
|
||||
color: #fff;
|
||||
background: #999;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* Signature Pad */
|
||||
.signature-pad {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
font-size: 10px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 700px;
|
||||
max-height: 460px;
|
||||
border: 1px solid #f0f0f0;
|
||||
background-color: #fff;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.signature-pad--body {
|
||||
position: relative;
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
border: 1px solid #f6f6f6;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.signature-pad--body canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset;
|
||||
}
|
||||
|
||||
.signature-pad--footer {
|
||||
color: #C3C3C3;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.signature-pad--actions {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: justify;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
[data-grav-field="array"] .form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
[data-grav-field="array"] .form-row > input,
|
||||
[data-grav-field="array"] .form-row > textarea
|
||||
{
|
||||
margin: 0 0.5rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.form-data.basic-captcha {
|
||||
.form-input-wrapper {
|
||||
border: 1px solid $form-border-color;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
.form-input-prepend {
|
||||
display: flex;
|
||||
color: #333;
|
||||
background-color: #ccc;
|
||||
flex-shrink: 0;
|
||||
img {
|
||||
margin: 0;
|
||||
}
|
||||
button > svg {
|
||||
margin: 0 8px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
input.form-input {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{% include 'partials/form-messages.html.twig' %}
|
||||
{% do http_response_code(form.responseCode) %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% include 'partials/form-messages.json.twig' %}
|
||||
{% do http_response_code(form.responseCode) %}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends "forms/default/form.html.twig" %}
|
||||
8
config/www/user/plugins/form/templates/form.html.twig
Normal file
8
config/www/user/plugins/form/templates/form.html.twig
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{{ content|raw }}
|
||||
{% include "forms/form.html.twig" %}
|
||||
|
||||
{% endblock %}
|
||||
1
config/www/user/plugins/form/templates/form.json.twig
Normal file
1
config/www/user/plugins/form/templates/form.json.twig
Normal file
@@ -0,0 +1 @@
|
||||
{% extends 'forms/ajax.json.twig' %}
|
||||
20
config/www/user/plugins/form/templates/formdata.html.twig
Normal file
20
config/www/user/plugins/form/templates/formdata.html.twig
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
|
||||
{% if form is null %}
|
||||
{% set form = grav.session.getFlashObject('form') %}
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
{{ content|raw }}
|
||||
|
||||
{% if form %}
|
||||
{% include 'partials/form-messages.html.twig' %}
|
||||
|
||||
<p>{{ 'PLUGIN_FORM.DATA_SUMMARY'|t }}</p>
|
||||
|
||||
{% include "forms/data.html.twig" %}
|
||||
{% else %}
|
||||
<div class="notices warning yellow"><p>{{ 'PLUGIN_FORM.NO_FORM_DATA'|t }}</p></div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% if form_json_response %}
|
||||
{{ form_json_response|json_encode|raw }}
|
||||
{% else %}
|
||||
{}
|
||||
{% endif %}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends "forms/default/data.html.twig" %}
|
||||
@@ -0,0 +1 @@
|
||||
{% extends "forms/default/data.txt.twig" %}
|
||||
@@ -0,0 +1,83 @@
|
||||
{% macro render_field(form, fields, scope) %}
|
||||
{% import _self as self %}
|
||||
|
||||
{% for index, field in fields %}
|
||||
{%- set show_field = attribute(field, "input@") ?? field.store ?? true %}
|
||||
{% if field.fields %}
|
||||
{%- set new_scope = field.nest_id ? scope ~ field.name ~ '.' : scope -%}
|
||||
{{- self.render_field(form, field.fields, new_scope) }}
|
||||
{% else %}
|
||||
{% if show_field %}
|
||||
{%- set value = form.value(scope ~ (field.name ?? index)) -%}
|
||||
{% if value %}
|
||||
{% block field %}
|
||||
<div>
|
||||
{% block field_label %}
|
||||
<strong>
|
||||
{%- if field.markdown -%}
|
||||
{{ field.data_label ? field.data_label|t|e|markdown(false) : field.label|t|emarkdown(false) }}
|
||||
{%- else -%}
|
||||
{{ field.data_label ? field.data_label|t|e : field.label|t|e }}
|
||||
{%- endif -%}
|
||||
</strong>:
|
||||
{% endblock %}
|
||||
|
||||
{% block field_value %}
|
||||
{% if field.type == 'checkboxes' %}
|
||||
<ul>
|
||||
{% set use_keys = field.use is defined and field.use == 'keys' %}
|
||||
{% for key,value in form.value(scope ~ field.name) %}
|
||||
{% set index = (use_keys ? key : value) %}
|
||||
<li>{{ field.options[index]|t|e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% elseif field.type == 'radio' %}
|
||||
{% set value = form.value(scope ~ field.name) %}
|
||||
{{ field.options[value]|t|e }}
|
||||
{% elseif field.type == 'checkbox' %}
|
||||
{{ (form.value(scope ~ field.name) == 1) ? "GRAV.YES"|t|e : "GRAV.NO"|t|e }}
|
||||
{% elseif field.type == 'select' %}
|
||||
{% set value = form.value(scope ~ field.name) %}
|
||||
{% if value is iterable %}
|
||||
<ul>
|
||||
{% set use_keys = field.use is defined and field.use == 'keys' %}
|
||||
{% for key, val in value %}
|
||||
{% set index = (use_keys ? key : val) %}
|
||||
<li>{{ field.options[index]|t|e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ field.options[value]|t|e }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set value = form.value(scope ~ field.name) %}
|
||||
{% if value is iterable %}
|
||||
<ul>
|
||||
{% for val in value %}
|
||||
{% if val is iterable %}
|
||||
<ul>
|
||||
{% for v in val %}
|
||||
<li>{{ string(v)|e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<li>{{ string(val)|e }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{{ string(value)|e|nl2br }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% import _self as macro %}
|
||||
|
||||
{{ macro.render_field(form, form.fields, '') }}
|
||||
@@ -0,0 +1,21 @@
|
||||
{%- macro render_field(form, fields, scope) %}
|
||||
{%- import _self as self %}
|
||||
{%- for index, field in fields %}
|
||||
{%- set show_field = attribute(field, "input@") ?? field.store ?? true %}
|
||||
{%- if field.fields %}
|
||||
{%- set new_scope = field.nest_id ? scope ~ field.name ~ '.' : scope -%}
|
||||
{{- self.render_field(form, field.fields, new_scope) }}
|
||||
{%- else %}
|
||||
{%- if show_field %}
|
||||
{%- set value = form.value(scope ~ (field.name ?? index)) -%}
|
||||
{%- if value -%}
|
||||
{{- field.data_label ? field.data_label|t|e : field.label|t|e }}: {{ string(value is iterable ? value|json_encode : value) ~ "\n" }}
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- endmacro %}
|
||||
{%- import _self as macro %}
|
||||
{%- autoescape false %}
|
||||
{{- macro.render_field(form, form.fields, '') ~ "\n" }}
|
||||
{%- endautoescape %}
|
||||
@@ -0,0 +1,158 @@
|
||||
{% if not field.validate.ignore %}
|
||||
|
||||
{% use 'forms/layouts/field-variables.html.twig' %}
|
||||
{% block field_override_variables_before %}{% endblock %}
|
||||
|
||||
{% set field_name = (scope ~ field.name)|fieldName %}
|
||||
{% set vertical = field.style == 'vertical' %}
|
||||
|
||||
{% if not blueprints or (blueprints.schema.type(field.type)['input@'] ?? true) is same as(true) %}
|
||||
{% set default = field.default %}
|
||||
{% set toggleable = field.toggleable ?? false %}
|
||||
{% if toggleable %}
|
||||
{% set originalValue = originalValue ?? value %}
|
||||
{% set toggleableChecked = originalValue is not null %}
|
||||
{% elseif field.overridable %}
|
||||
{% set toggleable = true %}
|
||||
{% set default = form.getDefaultValue(field.name) ?? default %}
|
||||
{% set toggleableChecked = value is not null and value != default %}
|
||||
{% endif %}
|
||||
|
||||
{% set cookie_name = 'forms-' ~ form.name ~ '-' ~ field.name %}
|
||||
{% set value = value ?? get_cookie(cookie_name) %}
|
||||
{% set has_value = value is not same as(null) %}
|
||||
{% if not has_value %}
|
||||
{% set value = default %}
|
||||
{% endif %}
|
||||
|
||||
{% if (field.yaml or field.validate.type == 'yaml') and value is iterable %}
|
||||
{% set value = value|yaml %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set toggleable = false %}
|
||||
{% endif %}
|
||||
|
||||
{# DEPRECATED: Needed by old form fields; remove when backwards compatibility breaks are allowed #}
|
||||
{% set isDisabledToggleable = toggleable and not toggleableChecked %}
|
||||
|
||||
{% if toggleable %}
|
||||
{% set form_field_toggleable %}
|
||||
{% include 'forms/default/toggleable.html.twig' with {checked: toggleableChecked} %}
|
||||
{% endset %}
|
||||
{% endif %}
|
||||
|
||||
{% set errors = attribute(form.messages, field.name) %}
|
||||
{% set required = client_side_validation and field.validate.required in ['on', 'true', 1] %}
|
||||
{% set autofocus = (inline_errors == false) and field.autofocus in ['on', 'true', 1] %}
|
||||
|
||||
{% if inline_errors and errors %}
|
||||
{% set autofocus = true %}
|
||||
{% endif %}
|
||||
|
||||
{% set embed_outer_field_classes %}
|
||||
{% block outer_field_classes %}{% endblock %}
|
||||
{% endset %}
|
||||
|
||||
{# Field Classes #}
|
||||
{%- if errors %}{% set form_field_outer_core = form_field_outer_core ~ ' has-errors' %}{% endif -%}
|
||||
{%- if toggleable %}{% set form_field_outer_core = form_field_outer_core ~ ' form-field-toggleable' %}{% endif -%}
|
||||
|
||||
{% set layout_form_field_outer_classes = field.outerclasses %}
|
||||
{% set layout_form_field_outer_classes = layout_form_field_outer_classes|trim ~ ' ' ~ form_field_outer_classes %}
|
||||
{% set layout_form_field_outer_classes = layout_form_field_outer_classes|trim ~ ' ' ~ embed_outer_field_classes %}
|
||||
|
||||
{# Show Label logic #}
|
||||
{% set show_label = field.label is not same as(false) and field.display_label is not same as(false )%}
|
||||
|
||||
{# Label Classes #}
|
||||
{% set layout_form_field_outer_label_classes = ((form_field_outer_label_classes ?: 'form-label') ~ ' ' ~ field.labelclasses)|trim %}
|
||||
{% set layout_form_field_label_classes = (form_field_label_classes ?: 'inline')|trim %}
|
||||
{% set form_field_label_trim = toggleable ? 'toggleable' %}
|
||||
|
||||
{# Field Outer Data classes #}
|
||||
{% set layout_form_field_outer_data_classes = ((form_field_outer_data_classes ?: ' form-data') ~ ' ' ~ field.dataclasses)|trim %}
|
||||
|
||||
{# Field Wrapper classes #}
|
||||
{% set layout_form_field_wrapper_classes = ((form_field_wrapper_classes ?: ' form-input-wrapper') ~ ' ' ~ field.wrapper_classes)|trim %}
|
||||
|
||||
{# Field input classes #}
|
||||
{% if field|of_type('array') %}
|
||||
{% if field.classes %}
|
||||
{% set field = field|merge({'classes': field.classes ~ ' ' ~ block('field_input_classes')|trim }) %}
|
||||
{% else %}
|
||||
{% set field = field|merge({'classes': block('field_input_classes') }) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set layout_form_field_input_classes = (form_field_input_classes ~ ' ' ~ field.classes)|trim %}
|
||||
|
||||
{# Inline error classes #}
|
||||
{% set form_field_inline_error_classes = form_field_inline_error_classes ?: ' form-errors' %}
|
||||
|
||||
{# Field extra classes #}
|
||||
{% set form_field_extra_wrapper_classes = 'form-extra-wrapper ' ~ field.wrapper_classes %}
|
||||
|
||||
{# Field For #}
|
||||
{% set form_field_for = toggleable ? 'toggleable_' ~ field.name : field.id|e %}
|
||||
|
||||
{# Field Label #}
|
||||
{% set form_field_label = field.markdown ? field.label|markdown(false) : field.label %}
|
||||
{% set form_field_label = form_field_label|default(field.name|capitalize)|t %}
|
||||
|
||||
{# Field Help #}
|
||||
{% if field.help %}
|
||||
{% set form_field_help = field.markdown ? field.help|t|markdown(false)|e : field.help|t|e %}
|
||||
{% endif %}
|
||||
|
||||
{# Field Requied #}
|
||||
{% set form_field_required = field.validate.required in ['on', 'true', 1] ? true : false %}
|
||||
|
||||
{# Field Description #}
|
||||
{% set form_field_description = field.markdown ? field.description|t|markdown(false)|raw : field.description|t|raw %}
|
||||
|
||||
{% extends 'forms/layouts/field.html.twig' %}
|
||||
|
||||
{% block global_attributes %}
|
||||
data-grav-field="{{ field.type }}"
|
||||
data-grav-disabled="{{ toggleable and toggleableChecked }}"
|
||||
data-grav-default="{{ default|json_encode()|e('html_attr') }}"
|
||||
{% endblock %}
|
||||
|
||||
{% block input_attributes %}
|
||||
class="{{ layout_form_field_input_classes|trim }} {{ field.size }}"
|
||||
{% if field.id is defined %}id="{{ field.id|e }}" {% endif %}
|
||||
{% if field.style is defined %}style="{{ field.style|e }}" {% endif %}
|
||||
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
|
||||
{% if field.placeholder %}placeholder="{{ field.placeholder|t|e('html_attr') }}"{% endif %}
|
||||
{% if autofocus %}autofocus="autofocus"{% endif %}
|
||||
{% if field.novalidate in ['on', 'true', 1] %}novalidate="novalidate"{% endif %}
|
||||
{% if field.readonly in ['on', 'true', 1] %}readonly="readonly"{% endif %}
|
||||
{% if field.autocomplete is defined %}autocomplete="{{ field.autocomplete }}"{% endif %}
|
||||
{% if field.autocapitalize in ['off', 'characters', 'words', 'sentences'] %}autocapitalize="{{ field.autocapitalize }}"{% endif %}
|
||||
{% if field.inputmode in ['none', 'text', 'decimal', 'numeric', 'tel', 'search', 'email', 'url'] %}inputmode="{{ field.inputmode }}"{% endif %}
|
||||
{% if field.tabindex %}tabindex="{{ field.tabindex }}"{% endif %}
|
||||
{% if field.spellcheck in ['true', 'false'] %}spellcheck="{{ field.spellcheck }}"{% endif %}
|
||||
{% if required %}required="required"{% endif %}
|
||||
{% if field.validate.pattern %}pattern="{{ field.validate.pattern|e }}"{% endif %}
|
||||
{% if field.validate.message %}title="{{ field.validate.message|t|e }}"
|
||||
{% elseif field.title is defined %}title="{{ field.title|t|e }}" {% endif %}
|
||||
|
||||
{# Support key/value and .name/.value styles #}
|
||||
{% if field.attributes is defined %}
|
||||
{% for key,attribute in field.attributes %}
|
||||
{% if attribute|of_type('array') %}
|
||||
{{ attribute.name }}="{{ attribute.value|e('html_attr') }}"
|
||||
{% else %}
|
||||
{{ key }}="{{ attribute|e('html_attr') }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{# Support for Custom data attributes#}
|
||||
{% if field.datasets %}
|
||||
{% for key, attribute in field.datasets %}
|
||||
data-{{ key }}="{{ attribute|e('html_attr') }}"
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% endif %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% set fields = prepare_form_fields(fields, name) %}
|
||||
{% set originalValue = null %}
|
||||
{% if fields|length %}
|
||||
{% block outer_markup_field_open %}{% endblock %}
|
||||
{% for field_name, field in fields %}
|
||||
{% set value = form ? form.value(field.name) : data.value(field.name) %}
|
||||
{% set field_templates = include_form_field(field.type, field_layout, fallback_field ?? 'text') %}
|
||||
{% block inner_markup_field_open %}{% endblock %}
|
||||
{% block field %}
|
||||
{% include field_templates %}
|
||||
{% endblock %}
|
||||
{% block inner_markup_field_close %}{% endblock %}
|
||||
{% endfor %}
|
||||
{% block outer_markup_field_close %}{% endblock %}
|
||||
{% else %}
|
||||
{% block empty_fields_markup %}{% endblock %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,211 @@
|
||||
{% block xhr %}{% endblock %}
|
||||
{% set form = form ?? grav.session.getFlashObject('form') %}
|
||||
{% set layout = layout ?? form.layout ?? 'default' %}
|
||||
{% set field_layout = field_layout ?? layout %}
|
||||
|
||||
<div id="{{ form.id }}-wrapper" class="form-wrapper">
|
||||
{# Keep here for Backwards Compatibility #}
|
||||
{% include 'partials/form-messages.html.twig' %}
|
||||
|
||||
{% set scope = scope ?: form.scope is defined ? form.scope : 'data.' %}
|
||||
{% set multipart = '' %}
|
||||
{% set blueprints = blueprints ?? form.blueprint() %}
|
||||
{% set method = form.method|upper|default('POST') %}
|
||||
{% set client_side_validation = form.client_side_validation is not null ? form.client_side_validation : config.plugins.form.client_side_validation|defined(true) %}
|
||||
{% set inline_errors = form.inline_errors is not null ? form.inline_errors : config.plugins.form.inline_errors(false) %}
|
||||
|
||||
{% set data = data ?? form.data %}
|
||||
{% set context = context ?? data %}
|
||||
|
||||
{% for field in form.fields %}
|
||||
{% if (method == 'POST' and field.type == 'file') %}
|
||||
{% set multipart = ' enctype="multipart/form-data"' %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% set action = action ?? (form.action ?: page.route ~ uri.params) %}
|
||||
{% set action = (action starts with 'http') or (action starts with '#') ? action : base_url ~ action %}
|
||||
{% set action = action|trim('/', 'right') %}
|
||||
|
||||
{% if (action == base_url_relative) %}
|
||||
{% set action = base_url_relative ~ '/' %}
|
||||
{% endif %}
|
||||
|
||||
{% if form.keep_alive %}
|
||||
{% if grav.browser.browser == 'msie' and grav.browser.version < 12 %}
|
||||
{% do assets.addJs('plugin://form/assets/object.assign.polyfill.js') %}
|
||||
{% endif %}
|
||||
{% do assets.addJs('plugin://form/assets/form.vendor.js', { 'group': 'bottom', 'loading': 'defer' }) %}
|
||||
{% do assets.addJs('plugin://form/assets/form.min.js', { 'group': 'bottom', 'loading': 'defer' }) %}
|
||||
{% endif %}
|
||||
|
||||
{% do assets.addInlineJs("
|
||||
window.GravForm = window.GravForm || {};
|
||||
window.GravForm.config = {
|
||||
current_url: '" ~ grav.route.withoutParams().toString(true) ~"',
|
||||
current_params: " ~ grav.route.params|json_encode ~ ",
|
||||
param_sep: '" ~ config.system.param_sep ~ "',
|
||||
base_url_relative: '" ~ base_url_relative ~ "',
|
||||
form_nonce: '" ~ form.getNonce() ~ "',
|
||||
session_timeout: " ~ config.system.session.timeout ~ "
|
||||
};
|
||||
window.GravForm.translations = Object.assign({}, window.GravForm.translations || {}, { PLUGIN_FORM: {} });
|
||||
", {'group': 'bottom', 'position': 'before', 'priority': 100}) %}
|
||||
|
||||
{# Backwards Compatibility for block overrides #}
|
||||
{% set override_form_classes %}
|
||||
{% block form_classes -%}
|
||||
{{ form_outer_classes }} {{ form.classes }}
|
||||
{%- endblock %}
|
||||
{% endset %}
|
||||
|
||||
{% set override_inner_markup_fields_start %}
|
||||
{% block inner_markup_fields_start %}{% endblock %}
|
||||
{% endset %}
|
||||
|
||||
{% set override_inner_markup_fields_end %}
|
||||
{% block inner_markup_fields_end %}{% endblock %}
|
||||
{% endset %}
|
||||
|
||||
{% set override_inner_markup_fields %}
|
||||
{% block inner_markup_fields %}
|
||||
{% for field_name, field in form.fields %}
|
||||
{% set field = prepare_form_field(field, field_name) %}
|
||||
{% if field %}
|
||||
{% set value = form ? form.value(field.name) : data.value(field.name) %}
|
||||
{% set field_templates = include_form_field(field.type, field_layout) %}
|
||||
|
||||
{% block inner_markup_field_open %}{% endblock %}
|
||||
{% block field %}
|
||||
{% include field_templates ignore missing %}
|
||||
{% endblock %}
|
||||
{% block inner_markup_field_close %}{% endblock %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% endset %}
|
||||
|
||||
{% set override_inner_markup_buttons_start %}
|
||||
{% block inner_markup_buttons_start %}
|
||||
<div class="{{ form_button_outer_classes ?: 'buttons'}}">
|
||||
{% endblock %}
|
||||
{% endset %}
|
||||
|
||||
{% set override_inner_markup_buttons_end %}
|
||||
{% block inner_markup_buttons_end %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endset %}
|
||||
|
||||
{# Embed for HTML layout #}
|
||||
{% embed 'forms/layouts/form.html.twig' %}
|
||||
|
||||
{% block embed_form_core %}
|
||||
name="{{ form.name }}"
|
||||
action="{{ action }}"
|
||||
method="{{ method }}"{{ multipart|raw }}
|
||||
id="{{ form.id|default(form.name|hyphenize) }}"
|
||||
{% if form.novalidate %}novalidate{% endif %}
|
||||
{% if form.xhr_submit %}data-xhr-enabled="true"{% endif %}
|
||||
{% if form.keep_alive %}data-grav-keepalive="true"{% endif %}
|
||||
{% if form.attributes is defined %}
|
||||
{% for key,attribute in form.attributes %}
|
||||
{% if attribute|of_type('array') %}
|
||||
{{ attribute.name }}="{{ attribute.value|e('html_attr') }}"
|
||||
{% else %}
|
||||
{{ key }}="{{ attribute|e('html_attr') }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block embed_form_classes -%}
|
||||
class="{{ parent() }} {{ override_form_classes|trim }}"
|
||||
{%- endblock %}
|
||||
|
||||
{% block embed_form_custom_attributes %}
|
||||
{% for k, v in blueprints.form.attributes %}
|
||||
{{ k }}="{{ v|e }}"
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block embed_fields %}
|
||||
{{ override_inner_markup_fields_start|raw }}
|
||||
{{ override_inner_markup_fields|raw }}
|
||||
|
||||
{% if form.isEnabled() ?? true %}
|
||||
{% include include_form_field('formname', field_layout, 'hidden') %}
|
||||
{% include include_form_field('formtask', field_layout, 'hidden') %}
|
||||
{% include include_form_field('uniqueid', field_layout, 'hidden') %}
|
||||
{% include include_form_field('nonce', field_layout, 'hidden') %}
|
||||
{% endif %}
|
||||
|
||||
{{ override_inner_markup_fields_end|raw }}
|
||||
{% endblock %}
|
||||
|
||||
{% block embed_buttons %}
|
||||
{{ override_inner_markup_buttons_start|raw }}
|
||||
|
||||
{% if form.isEnabled() ?? true %}
|
||||
{% for button in form.buttons %}
|
||||
{% if not button.access or authorize(button.access) %}
|
||||
{% if button.outerclasses is defined %}<div class="{{ button.outerclasses }}">{% endif %}
|
||||
|
||||
{% if button.url %}
|
||||
{% set button_url = button.url starts with 'http' ? button.url : base_url ~ button.url %}
|
||||
{% endif %}
|
||||
|
||||
{% embed 'forms/layouts/button.html.twig' %}
|
||||
{% block embed_button_core %}
|
||||
{% if button.id %}id="{{ button.id }}"{% endif %}
|
||||
{% if button.disabled %}disabled="disabled"{% endif %}
|
||||
{% if button.name %}
|
||||
name="{{ button.name }}"
|
||||
{% else %}
|
||||
{% if button.task %}name="task" value="{{ button.task }}"{% endif %}
|
||||
{% endif %}
|
||||
type="{{ button.type|default('submit') }}"
|
||||
{% if button.attributes is defined %}
|
||||
{% for key,attribute in button.attributes %}
|
||||
{% if attribute|of_type('array') %}
|
||||
{{ attribute.name }}="{{ attribute.value|e('html_attr') }}"
|
||||
{% else %}
|
||||
{{ key }}="{{ attribute|e('html_attr') }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block embed_button_classes %}
|
||||
{% block button_classes %}
|
||||
class="{{ form_button_classes ?: 'button' }} {{ button.classes }}"
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block embed_button_content -%}
|
||||
{%- set button_value = button.value|t|default('Submit') -%}
|
||||
{%- if button.html -%}
|
||||
{{- button_value|trim|raw -}}
|
||||
{%- else -%}
|
||||
{{- button_value|trim|e -}}
|
||||
{%- endif -%}
|
||||
{%- endblock %}
|
||||
|
||||
{% endembed %}
|
||||
|
||||
{% if button.outerclasses is defined %}</div>{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{{ override_inner_markup_buttons_end }}
|
||||
{% endblock %}
|
||||
|
||||
{% endembed %}
|
||||
|
||||
{% if config.forms.dropzone.enabled %}
|
||||
<div id="dropzone-template" style="display:none;">
|
||||
{% include 'forms/dropzone/template.html.twig' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<span class="checkboxes toggleable" data-grav-field="toggleable" data-grav-field-name="{{ field_name }}">
|
||||
<input type="checkbox"
|
||||
id="toggleable_{{ field.name }}"
|
||||
name="toggleable_{{ field_name }}"
|
||||
value="1"
|
||||
{% if checked %}checked="checked"{% endif %}
|
||||
>
|
||||
<label for="toggleable_{{ field.name }}"></label>
|
||||
</span>
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="dz-preview dz-file-preview">
|
||||
<div class="dz-image"><img data-dz-thumbnail /></div>
|
||||
|
||||
<div class="dz-details">
|
||||
<div class="dz-size"><span data-dz-size></span></div>
|
||||
<div class="dz-filename"><span data-dz-name></span></div>
|
||||
</div>
|
||||
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
|
||||
<div class="dz-error-message"><span data-dz-errormessage></span></div>
|
||||
<a class="dz-remove" title="remove" href="javascript:undefined;" data-dz-remove>×</a>
|
||||
|
||||
<div class="dz-success-mark">
|
||||
|
||||
<svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.2.1 (9971) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Check</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<path d="M23.5,31.8431458 L17.5852419,25.9283877 C16.0248253,24.3679711 13.4910294,24.366835 11.9289322,25.9289322 C10.3700136,27.4878508 10.3665912,30.0234455 11.9283877,31.5852419 L20.4147581,40.0716123 C20.5133999,40.1702541 20.6159315,40.2626649 20.7218615,40.3488435 C22.2835669,41.8725651 24.794234,41.8626202 26.3461564,40.3106978 L43.3106978,23.3461564 C44.8771021,21.7797521 44.8758057,19.2483887 43.3137085,17.6862915 C41.7547899,16.1273729 39.2176035,16.1255422 37.6538436,17.6893022 L23.5,31.8431458 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z" id="Oval-2" stroke-opacity="0.198794158" stroke="#747474" fill-opacity="0.816519475" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
<div class="dz-error-mark">
|
||||
|
||||
<svg width="54px" height="54px" viewBox="0 0 54 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.2.1 (9971) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>error</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="Check-+-Oval-2" sketch:type="MSLayerGroup" stroke="#747474" stroke-opacity="0.198794158" fill="#FFFFFF" fill-opacity="0.816519475">
|
||||
<path d="M32.6568542,29 L38.3106978,23.3461564 C39.8771021,21.7797521 39.8758057,19.2483887 38.3137085,17.6862915 C36.7547899,16.1273729 34.2176035,16.1255422 32.6538436,17.6893022 L27,23.3431458 L21.3461564,17.6893022 C19.7823965,16.1255422 17.2452101,16.1273729 15.6862915,17.6862915 C14.1241943,19.2483887 14.1228979,21.7797521 15.6893022,23.3461564 L21.3431458,29 L15.6893022,34.6538436 C14.1228979,36.2202479 14.1241943,38.7516113 15.6862915,40.3137085 C17.2452101,41.8726271 19.7823965,41.8744578 21.3461564,40.3106978 L27,34.6568542 L32.6538436,40.3106978 C34.2176035,41.8744578 36.7547899,41.8726271 38.3137085,40.3137085 C39.8758057,38.7516113 39.8771021,36.2202479 38.3106978,34.6538436 L32.6568542,29 Z M27,53 C41.3594035,53 53,41.3594035 53,27 C53,12.6405965 41.3594035,1 27,1 C12.6405965,1 1,12.6405965 1,27 C1,41.3594035 12.6405965,53 27,53 Z" id="Oval-2" sketch:type="MSShapeGroup"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
{#
|
||||
DO NOT MODIFY!
|
||||
|
||||
Default layout can be found in form plugin or your theme:
|
||||
|
||||
templates/forms/layouts/field/default-field.html.twig
|
||||
|
||||
#}
|
||||
{% extends "forms/default/field.html.twig" %}
|
||||
@@ -0,0 +1,99 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% macro renderer(key, text, field, scope) %}
|
||||
|
||||
{% if text is not iterable %}
|
||||
<div class="form-row{% if field.value_only %} array-field-value_only{% endif %}"
|
||||
data-grav-array-type="row">
|
||||
<span data-grav-array-action="sort" class="fa fa-bars"></span>
|
||||
{% if field.value_only != true %}
|
||||
{% if key == '0' and text == '' %}
|
||||
{% set key = '' %}
|
||||
{% endif %}
|
||||
|
||||
<input
|
||||
data-grav-array-type="key"
|
||||
type="text" value="{{ key }}"
|
||||
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
|
||||
placeholder="{{ field.placeholder_key|e|t }}" />
|
||||
{% endif %}
|
||||
|
||||
{% if field.value_type == 'textarea' %}
|
||||
<textarea
|
||||
data-grav-array-type="value"
|
||||
name="{{ ((scope ~ field.name)|fieldName) ~ '[' ~ key ~ ']' }}"
|
||||
placeholder="{{ field.placeholder_value|e|t }}"
|
||||
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}>{{ text }}</textarea>
|
||||
{% else %}
|
||||
<input
|
||||
data-grav-array-type="value"
|
||||
type="text"
|
||||
name="{{ ((scope ~ field.name)|fieldName) ~ '[' ~ key ~ ']' }}"
|
||||
placeholder="{{ field.placeholder_value|e|t }}"
|
||||
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
|
||||
value={% if text == 'true' %}true{% elseif text == 'false' %}false{% else %}"{{ text|join(', ')|e }}"{% endif %} />
|
||||
{% endif %}
|
||||
|
||||
<span data-grav-array-action="rem" class="fa fa-minus"></span>
|
||||
<span data-grav-array-action="add" class="fa fa-plus"></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% import _self as array_field %}
|
||||
|
||||
{% do assets.addJs('plugin://form/assets/form.vendor.js', { 'group': 'bottom', 'loading': 'defer' }) %}
|
||||
{% do assets.addJs('plugin://form/assets/form.min.js', { 'group': 'bottom', 'loading': 'defer' }) %}
|
||||
|
||||
{% block global_attributes %}
|
||||
data-grav-array-name="{{ (scope ~ field.name)|fieldName }}"
|
||||
data-grav-array-keyname="{{ field.placeholder_key|e|t }}"
|
||||
data-grav-array-valuename="{{ field.placeholder_value|e|t }}"
|
||||
data-grav-array-textarea="{{ field.value_type == 'textarea' }}"
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
|
||||
{% block input %}
|
||||
<div class="{{ field.size }} {{ field.classes }}" data-grav-array-type="container"{% if field.value_only %} data-grav-array-mode="value_only"{% endif %}{{ value|length <= 1 ? ' class="one-child"' : '' }}>
|
||||
{% if value|length %}
|
||||
{% for key, text in value -%}
|
||||
{% if text is not iterable %}
|
||||
{{ array_field.renderer(key, text, field, scope) }}
|
||||
{% else %}
|
||||
{# Backward compatibility for nested arrays (metas) which are not supported anymore #}
|
||||
{% for subkey, subtext in text -%}
|
||||
{{ array_field.renderer(key ~ '[' ~ subkey ~ ']', subtext, field, scope) }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{%- else -%}
|
||||
{# Empty value, mock the entry field#}
|
||||
<div class="form-row" data-grav-array-type="row">
|
||||
<span data-grav-array-action="sort" class="fa fa-bars"></span>
|
||||
{% if field.value_only != true %}
|
||||
<input
|
||||
data-grav-array-type="key"
|
||||
type="text"
|
||||
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
|
||||
placeholder="{{ field.placeholder_key|e|t }}" />
|
||||
{% endif %}
|
||||
{% if field.value_type == 'textarea' %}
|
||||
<textarea
|
||||
data-grav-array-type="value"
|
||||
name="{{ (scope ~ field.name)|fieldName }}"
|
||||
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
|
||||
placeholder="{{ field.placeholder_value|e|t }}"></textarea>
|
||||
{% else %}
|
||||
<input
|
||||
data-grav-array-type="value"
|
||||
type="text"
|
||||
name="{{ (scope ~ field.name)|fieldName }}"
|
||||
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
|
||||
placeholder="{{ field.placeholder_value|e|t }}" />
|
||||
{% endif %}
|
||||
<span data-grav-array-action="rem" class="fa fa-minus"></span>
|
||||
<span data-grav-array-action="add" class="fa fa-plus"></span>
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block field %}
|
||||
{% set avatar = form.value('avatar') %}
|
||||
{% if avatar %}
|
||||
<label class="{{ field.classes }}"><img class="{{ field.img_classes }}" style="max-width:200px;" src="{{ base_url_simple ~ '/' ~ (avatar|first).path }}" /></label>
|
||||
{% else %}
|
||||
<label class="{{ field.classes }}"><img class="{{ field.img_classes }}" src="https://www.gravatar.com/avatar/{{ form.value('email')|md5 }}?s=200" /></label>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% set form_field_outer_data_classes = 'form-data basic-captcha' %}
|
||||
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block prepend %}
|
||||
{% set field_id = field.name|default('default') %}
|
||||
{% set config_hash = (form.id ~ '_basic_captcha_' ~ field_id)|md5 %}
|
||||
{% set image_url = url('/forms-basic-captcha-image.jpg') ~ '?field=' ~ config_hash %}
|
||||
|
||||
{# Store field configuration in session for image generation #}
|
||||
{% set global_config = grav.config.get('plugins.form.basic_captcha', {}) %}
|
||||
{% set merged_config = global_config|merge(field) %}
|
||||
{% do store_basic_captcha_config(config_hash, merged_config) %}
|
||||
|
||||
<div class="form-input-addon form-input-prepend"
|
||||
data-captcha-provider="basic-captcha"
|
||||
data-field-id="{{ config_hash }}">
|
||||
<img id="basic-captcha-reload-{{ form.id }}"
|
||||
src="{{ image_url }}"
|
||||
alt="human test"
|
||||
data-base-url="{{ url('/forms-basic-captcha-image.jpg') }}"
|
||||
data-field-id="{{ config_hash }}" />
|
||||
<button type="button" id="reload-captcha-{{ form.id }}" class="reload-captcha-button"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g fill="currentColor"><path d="M14.74 22.39c4.68-1.24 8-5.49 8-10.4 0-5.95-4.79-10.75-10.75-10.75 -3.11 0-5.78 1.11-7.99 2.95 -.77.64-1.43 1.32-1.98 2.01 -.34.41-.57.75-.69.95 -.22.35-.1.81.25 1.02 .35.21.81.09 1.02-.26 .08-.15.27-.43.56-.79 .49-.62 1.08-1.23 1.76-1.81C6.87 3.67 9.21 2.7 11.94 2.7c5.13 0 9.25 4.12 9.25 9.25 0 4.22-2.86 7.88-6.9 8.94 -.41.1-.64.51-.54.91 .1.4.51.63.91.53Zm-12-14.84V2.99c-.001-.42-.34-.75-.75-.75 -.42 0-.75.33-.75.75v4.56c0 .41.33.75.75.75 .41 0 .75-.34.75-.75Zm-.75.75H4h2.43c.41 0 .75-.34.75-.75 0-.42-.34-.75-.75-.75H4 1.99c-.42 0-.75.33-.75.75 0 .41.33.75.75.75Z"/><path d="M1.25 12c0 1.09.16 2.16.48 3.18 .12.39.54.61.93.49 .39-.13.61-.55.49-.94 -.28-.89-.42-1.81-.42-2.75 0-.42-.34-.75-.75-.75 -.42 0-.75.33-.75.75Zm1.93 6.15c.61.88 1.36 1.67 2.22 2.33 .32.25.79.19 1.05-.14 .25-.33.19-.8-.14-1.06 -.74-.58-1.38-1.25-1.92-2.02 -.24-.34-.71-.43-1.05-.19 -.34.23-.43.7-.19 1.04Zm5.02 3.91c1 .37 2.06.6 3.15.66 .41.02.76-.3.79-.71 .02-.42-.30-.77-.71-.80 -.94-.06-1.85-.25-2.72-.58 -.39-.15-.83.04-.97.43 -.15.38.04.82.43.96Z"/></g></svg></button>
|
||||
</div>
|
||||
|
||||
{% do assets.addJs('plugin://form/assets/captcha/basic-captcha-refresh.js') %}
|
||||
{% endblock %}
|
||||
|
||||
{% block input_attributes %}
|
||||
type="text"
|
||||
{% if field.size %}size="{{ field.size }}"{% endif %}
|
||||
{% if field.minlength is defined or field.validate.min is defined %}minlength="{{ field.minlength | default(field.validate.min) }}"{% endif %}
|
||||
{% if field.maxlength is defined or field.validate.max is defined %}maxlength="{{ field.maxlength | default(field.validate.max) }}"{% endif %}
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block field %}
|
||||
{# This main captcha field serves as a router to the appropriate provider template #}
|
||||
{% set provider = field.provider %}
|
||||
|
||||
{% if provider is not defined or provider == null %}
|
||||
{% set provider = 'recaptcha' %}
|
||||
{% endif %}
|
||||
|
||||
{% set template = 'forms/fields/' ~ provider ~ '/' ~ provider ~ '.html.twig' %}
|
||||
|
||||
{% if captcha_template_exists(template) %}
|
||||
{% include template with {'field': field} %}
|
||||
{% else %}
|
||||
<div class="form-error" style="color:#c00000;">ERROR - unknown captcha provider: <strong>{{ provider }}</strong></div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block label %}
|
||||
{% endblock %}
|
||||
|
||||
{% block input %}
|
||||
{% set id = field.id|default(field.name)|hyphenize %}
|
||||
<div class="{{ form_field_wrapper_classes ?: 'form-input-wrapper' }} {{ field.size }} {{ field.wrapper_classes }}">
|
||||
<input
|
||||
{# required attribute structures #}
|
||||
name="{{ (scope ~ field.name)|fieldName }}"
|
||||
value="{{ field.value ?? '1' }}"
|
||||
type="checkbox"
|
||||
{% if value == (field.value ?? '1') %} checked="checked" {% endif %}
|
||||
|
||||
{# input attribute structures #}
|
||||
{% block input_attributes %}
|
||||
id="{{ id|e }}"
|
||||
class="{{ form_field_checkbox_classes }} {{ field.classes }}"
|
||||
{% if field.style is defined %}style="{{ field.style|e }}" {% endif %}
|
||||
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
|
||||
{% if field.autofocus in ['on', 'true', 1] %}autofocus="autofocus"{% endif %}
|
||||
{% if field.novalidate in ['on', 'true', 1] %}novalidate="novalidate"{% endif %}
|
||||
{% if required %}required="required"{% endif %}
|
||||
{% if field.tabindex %}tabindex="{{ field.tabindex }}"{% endif %}
|
||||
{% if field.attributes is defined %}
|
||||
{% for key,attribute in field.attributes %}
|
||||
{% if attribute|of_type('array') %}
|
||||
{{ attribute.name }}="{{ attribute.value|e('html_attr') }}"
|
||||
{% else %}
|
||||
{{ key }}="{{ attribute|e('html_attr') }}"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
/>
|
||||
<label style="display:inline;" class="inline" for="{{ id|e }}">
|
||||
{% if field.markdown %}
|
||||
{{ field.label|t|markdown(false) }}
|
||||
{% else %}
|
||||
{{ field.label|t|e }}
|
||||
{% endif %}
|
||||
{{ field.validate.required in ['on', 'true', 1] ? '<span class="required">*</span>' }}
|
||||
</label>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block global_attributes %}
|
||||
{{ parent() }}
|
||||
data-grav-keys="{{ field.use == 'keys' ? 'true' : 'false' }}"
|
||||
data-grav-field-name="{{ (scope ~ field.name)|fieldName }}"
|
||||
{% endblock %}
|
||||
|
||||
{% block input %}
|
||||
{% set value = (value is null ? field.default : value) %}
|
||||
{% if field.use == 'keys' and field.default %}
|
||||
{% set value = field.default|merge(value) %}
|
||||
{% endif %}
|
||||
|
||||
<div class="checkboxes {{ form_field_wrapper_classes }} {{ field.wrapper_classes }}">
|
||||
{% for key, text in field.options %}
|
||||
{% set id = field.id|default(field.name)|hyphenize ~ '-' ~ key %}
|
||||
{% set name = field.use == 'keys' ? key : id %}
|
||||
{% set val = field.use == 'keys' ? '1' : key %}
|
||||
{% set checked = (field.use == 'keys' ? value[key] : key in value) %}
|
||||
{% set help = (key in field.help_options|keys ? field.help_options[key] : false) %}
|
||||
{% set disabled = key in field.disabled_options %}
|
||||
<input type="checkbox"
|
||||
id="{{ id|e }}"
|
||||
value="{{ val|e }}"
|
||||
name="{{ (scope ~ field.name)|fieldName ~ '[' ~ name ~ ']' }}"
|
||||
class="{{ form_field_checkbox_classes }} {{ field.classes }}"
|
||||
{% if checked %}checked="checked"{% endif %}
|
||||
{% if disabled %}disabled="disabled"{% endif %}
|
||||
>
|
||||
<label style="display: inline; {% if disabled %}opacity: 0.6; cursor: no-drop;{% endif %}" for="{{ id|e }}">
|
||||
{% if help %}
|
||||
<span class="hint--bottom" data-hint="{{ help|t|e('html_attr') }}">{{ text|t|e }}</span>
|
||||
{% else %}
|
||||
{{ text|t|e }}
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block input_attributes %}
|
||||
type="color"
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block field %}
|
||||
{% embed 'forms/default/fields.html.twig' with {name: name, fields: field.fields} %}
|
||||
{% block outer_markup_field_open %}<div class="form-column {{ field.classes }}">{% endblock %}
|
||||
{% block outer_markup_field_close %}</div>{% endblock %}
|
||||
{% endembed %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block field %}
|
||||
<div class="form-columns {{ field.classes }}">
|
||||
{% include 'forms/default/fields.html.twig' with {name: field.name|parent_field, fields: field.fields} %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block field %}
|
||||
{% set value = evaluate(field.condition) %}
|
||||
{% set value = value == 'true' ? 1 : value %}
|
||||
{% set value = value == 'false' ? 0 : value %}
|
||||
|
||||
{% if value %}
|
||||
{% if field.classes %}
|
||||
<div class="{{ field.classes }}">
|
||||
{% endif %}
|
||||
|
||||
{% include 'forms/default/fields.html.twig' with {name: field.name, fields: field.fields} %}
|
||||
|
||||
{% if field.classes %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block input_attributes %}
|
||||
type="date"
|
||||
{% if field.validate.min %}min="{{ field.validate.min }}"{% endif %}
|
||||
{% if field.validate.max %}max="{{ field.validate.max }}"{% endif %}
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1 @@
|
||||
{{ value ? value|date('m/d/Y')|e }}
|
||||
@@ -0,0 +1,2 @@
|
||||
{# DEPRECATED. Switched to Text field until implemented properly #}
|
||||
{% extends "forms/fields/text/text.html.twig" %}
|
||||
@@ -0,0 +1 @@
|
||||
{{ value ? value|date('m/d/Y \\a\\t g:i A')|e }}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% if field.file %}
|
||||
{% set content = read_file(field.file) %}
|
||||
{% else %}
|
||||
{% set content = field.content %}
|
||||
{% endif %}
|
||||
|
||||
{% block input %}
|
||||
<div class="form-display-wrapper {{ field.size }} {{ field.classes }}" {% if field.id is defined %}id="{{ field.id|e }}" {% endif %}>
|
||||
{% if field.markdown %}
|
||||
{{ content|t|markdown|raw }}
|
||||
{% else %}
|
||||
{% if field.evaluate %}
|
||||
{{ evaluate_twig(content)|raw }}
|
||||
{% else %}
|
||||
{{ content|t|raw }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block input_attributes %}
|
||||
type="email"
|
||||
{% if field.multiple in ['on', 'true', 1] %}multiple="multiple"{% endif %}
|
||||
{% if field.size %}size="{{ field.size }}"{% endif %}
|
||||
{% if field.minlength is defined or field.validate.min is defined %}minlength="{{ field.minlength | default(field.validate.min) }}"{% endif %}
|
||||
{% if field.maxlength is defined or field.validate.max is defined %}maxlength="{{ field.maxlength | default(field.validate.max) }}"{% endif %}
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
{% set scope = field.nest_id ? scope ~ field.name ~ '.' : scope %}
|
||||
|
||||
{% block field %}
|
||||
<fieldset {% if field.id is defined %}id="{{ field.id }}"{% endif %} {% if field.classes is defined %}class="{{ field.classes }}" {% endif %}>
|
||||
{% if field.legend %}
|
||||
<legend>{{ field.legend|t }}</legend>
|
||||
{% endif %}
|
||||
|
||||
{% include 'forms/default/fields.html.twig' with {name: field.name, fields: field.fields} %}
|
||||
</fieldset>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,136 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% macro bytesToSize(bytes) -%}
|
||||
{% apply spaceless %}
|
||||
{% set kilobyte = 1024 %}
|
||||
{% set megabyte = kilobyte * 1024 %}
|
||||
{% set gigabyte = megabyte * 1024 %}
|
||||
{% set terabyte = gigabyte * 1024 %}
|
||||
|
||||
{% if bytes < kilobyte %}
|
||||
{{ bytes ~ ' B' }}
|
||||
{% elseif bytes < megabyte %}
|
||||
{{ (bytes / kilobyte)|number_format(2, '.') ~ ' KB' }}
|
||||
{% elseif bytes < gigabyte %}
|
||||
{{ (bytes / megabyte)|number_format(2, '.') ~ ' MB' }}
|
||||
{% elseif bytes < terabyte %}
|
||||
{{ (bytes / gigabyte)|number_format(2, '.') ~ ' GB' }}
|
||||
{% else %}
|
||||
{{ (bytes / terabyte)|number_format(2, '.') ~ ' TB' }}
|
||||
{% endif %}
|
||||
{% endapply %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro preview(path, value, global) %}
|
||||
{% if value %}
|
||||
{% set uri = global.grav.uri %}
|
||||
{% set files = global.files %}
|
||||
{% set config = global.grav.config %}
|
||||
{% set route = global.context.route().toString(true) %}
|
||||
{% set type = global.context.content() is not null ? 'pages' : global.plugin ? 'plugins' : global.theme ? 'themes' : 'config' %}
|
||||
{% set blueprint_name = global.blueprints.getFilename %}
|
||||
{% if type == 'pages' %}
|
||||
{% set blueprint_name = type ~ '/' ~ blueprint_name %}
|
||||
{% endif %}
|
||||
{% set blueprint = blueprint_name|base64_encode %}
|
||||
{% set real_path = value.thumb ?? global.context.media[path].relativePath ?? global.form.getPagePathFromToken(path) %}
|
||||
{% set remove = global.form.getFileDeleteAjaxRoute(files.name, path).toString(true) ?: uri.addNonce(
|
||||
global.base_url_relative ~
|
||||
'/media.json' ~
|
||||
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~
|
||||
'/proute' ~ config.system.param_sep ~ route|base64_encode ~
|
||||
'/blueprint' ~ config.system.param_sep ~ blueprint ~
|
||||
'/type' ~ config.system.param_sep ~ type ~
|
||||
'/field' ~ config.system.param_sep ~ files.name ~
|
||||
'/path' ~ config.system.param_sep ~ value.path|base64_encode, 'admin-form', 'admin-nonce') %}
|
||||
|
||||
{% set file = value|merge({remove: remove, path: value.thumb_url ?? (uri.rootUrl == '/' ? '/' : uri.rootUrl ~ '/' ~ real_path) }) %}
|
||||
<div class="hidden" data-file="{{ file|json_encode|e('html_attr') }}"></div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% import _self as macro %}
|
||||
|
||||
{% set defaults = config.plugins.form %}
|
||||
{% set files = defaults.files|merge(field|default([])) %}
|
||||
{% set limit = not field.multiple ? 1 : files.limit %}
|
||||
|
||||
{% do config.set('forms.dropzone.enabled', true) %}
|
||||
|
||||
{% block input %}
|
||||
{% set page_can_upload = exists or (type == 'page' and not exists and not (field.destination starts with '@self' or field.destination starts with 'self@')) %}
|
||||
{% set max_filesize = (field.filesize > form_max_filesize or field.filesize == 0) ? form_max_filesize : field.filesize %}
|
||||
|
||||
{% block prepend %}{% endblock %}
|
||||
{% set settings = {name: field.name, paramName: (scope ~ field.name)|fieldName ~ (files.multiple ? '[]' : ''), limit: limit, filesize: max_filesize, accept: files.accept, resolution: files.resolution, resizeWidth: files.resizeWidth, resizeHeight: files.resizeHeight, resizeQuality: files.resizeQuality } %}
|
||||
{% set dropzoneSettings = field.dropzone %}
|
||||
{% set file_url_add = form.getFileUploadAjaxRoute().getUri() %}
|
||||
{% set file_url_remove = form.getFileDeleteAjaxRoute(null, null).getUri() %}
|
||||
<div class="{{ form_field_wrapper_classes ?: 'form-input-wrapper' }} {{ field.classes }} dropzone files-upload form-input-file {{ field.size }}"
|
||||
data-grav-file-settings="{{ settings|json_encode|e('html_attr') }}"
|
||||
data-dropzone-options="{{ dropzoneSettings|json_encode|e('html_attr') }}"
|
||||
data-file-field-name="{{ field.name }}"
|
||||
{% if file_url_add %}data-file-url-add="{{ file_url_add|e('html_attr') }}"{% endif %}
|
||||
{% if file_url_remove %}data-file-url-remove="{{ file_url_remove|e('html_attr') }}"{% endif %}>
|
||||
{% block file_extras %}{% endblock %}
|
||||
<input
|
||||
{# required attribute structures #}
|
||||
{% block input_attributes %}
|
||||
type="file"
|
||||
{% if files.multiple %}multiple="multiple"{% endif %}
|
||||
{% if files.accept %}accept="{{ files.accept|join(',') }}"{% endif %}
|
||||
{% if field.disabled %}disabled="disabled"{% endif %}
|
||||
{% if field.random_name %}random="true"{% endif %}
|
||||
{% if required %}required="required"{% endif %}
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
/>
|
||||
|
||||
{% for path, file in value %}
|
||||
{{ macro.preview(path, file, _context) }}
|
||||
{% endfor %}
|
||||
{% include 'forms/fields/hidden/hidden.html.twig' with {field: {name: '_json.' ~ field.name}, value: (value ?? [])|json_encode } %}
|
||||
|
||||
|
||||
</div>
|
||||
{% if inline_errors and errors %}
|
||||
<div class="{{ form_field_inline_error_classes }}">
|
||||
<p class="form-message"><i class="fa fa-exclamation-circle"></i> {{ errors|first|raw }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if form.xhr_submit %}
|
||||
{% do assets.addJs('plugin://form/assets/dropzone-reinit.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 90 }) %}
|
||||
{% endif %}
|
||||
|
||||
{% if grav.browser.browser == 'msie' and grav.browser.version < 12 %}
|
||||
{% do assets.addJs('plugin://form/assets/object.assign.polyfill.js') %}
|
||||
{% endif %}
|
||||
{% do assets.addJs('jquery', 101) %}
|
||||
{% do assets.addJs('plugin://form/assets/form.vendor.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 100 }) %}
|
||||
{% do assets.addJs('plugin://form/assets/form.min.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 99 }) %}
|
||||
{% do assets.addCss('plugin://form/assets/dropzone.min.css', { 'group': 'form'}) %}
|
||||
{{ assets.css('form')|raw }}
|
||||
{% do assets.addInlineJs("
|
||||
window.GravForm = window.GravForm || {};
|
||||
window.GravForm = Object.assign({}, window.GravForm, {
|
||||
translations: {
|
||||
PLUGIN_FORM: {
|
||||
'DROPZONE_CANCEL_UPLOAD': " ~ 'PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD'|t|json_encode ~ ",
|
||||
'DROPZONE_CANCEL_UPLOAD_CONFIRMATION': " ~ 'PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD_CONFIRMATION'|t|json_encode ~ ",
|
||||
'DROPZONE_DEFAULT_MESSAGE': " ~ 'PLUGIN_FORM.DROPZONE_DEFAULT_MESSAGE'|t|json_encode ~ ",
|
||||
'DROPZONE_FALLBACK_MESSAGE': " ~ 'PLUGIN_FORM.DROPZONE_FALLBACK_MESSAGE'|t|json_encode ~ ",
|
||||
'DROPZONE_FALLBACK_TEXT': " ~ 'PLUGIN_FORM.DROPZONE_FALLBACK_TEXT'|t|json_encode ~ ",
|
||||
'DROPZONE_FILE_TOO_BIG': " ~ 'PLUGIN_FORM.DROPZONE_FILE_TOO_BIG'|t|json_encode ~ ",
|
||||
'DROPZONE_INVALID_FILE_TYPE': " ~ 'PLUGIN_FORM.DROPZONE_INVALID_FILE_TYPE'|t|json_encode ~ ",
|
||||
'DROPZONE_MAX_FILES_EXCEEDED': " ~ 'PLUGIN_FORM.DROPZONE_MAX_FILES_EXCEEDED'|t|json_encode ~ ",
|
||||
'DROPZONE_REMOVE_FILE': " ~ 'PLUGIN_FORM.DROPZONE_REMOVE_FILE'|t|json_encode ~ ",
|
||||
'DROPZONE_REMOVE_FILE_CONFIRMATION': " ~ 'PLUGIN_FORM.DROPZONE_REMOVE_FILE_CONFIRMATION'|t|json_encode ~ ",
|
||||
'DROPZONE_RESPONSE_ERROR': " ~ 'PLUGIN_FORM.DROPZONE_RESPONSE_ERROR'|t|json_encode ~ ",
|
||||
'RESOLUTION_MIN': " ~ 'PLUGIN_FORM.RESOLUTION_MIN'|t|json_encode ~ ",
|
||||
'RESOLUTION_MAX': " ~ 'PLUGIN_FORM.RESOLUTION_MAX'|t|json_encode ~ "
|
||||
}
|
||||
}
|
||||
});
|
||||
", {'group': 'bottom', 'position': 'before'}) %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,128 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% set defaults = config.plugins.form %}
|
||||
{% set files = defaults.files|merge(field|default([])) %}
|
||||
{% set limit = not field.multiple ? 1 : files.limit %}
|
||||
|
||||
{% block input %}
|
||||
{% set page_can_upload = exists or (type == 'page' and not exists and not (field.destination starts with '@self' or field.destination starts with 'self@')) %}
|
||||
{% set max_filesize = (field.filesize > form_max_filesize or field.filesize == 0) ? form_max_filesize : field.filesize %}
|
||||
|
||||
{% block prepend %}{% endblock %}
|
||||
{% set settings = {name: field.name, paramName: (scope ~ field.name)|fieldName ~ (files.multiple ? '[]' : ''), limit: limit, filesize: max_filesize, accept: files.accept, resolution: files.resolution, resizeWidth: field.filepond.resize_width, resizeHeight: field.filepond.resize_height, resizeQuality: field.filepond.resize_quality } %}
|
||||
{% set filepond_settings = field.filepond|default({}) %}
|
||||
{% set file_url_add = form.getFileUploadAjaxRoute().getUri() %}
|
||||
{% set file_url_remove = form.getFileDeleteAjaxRoute(null, null).getUri() %}
|
||||
<div class="{{ form_field_wrapper_classes ?: 'form-input-wrapper' }} {{ field.classes }} filepond-root form-input-file {{ field.size }}"
|
||||
data-grav-file-settings="{{ settings|json_encode|e('html_attr') }}"
|
||||
data-filepond-options="{{ filepond_settings|json_encode|e('html_attr') }}"
|
||||
data-file-field-name="{{ field.name }}"
|
||||
{% if file_url_add %}data-file-url-add="{{ file_url_add|e('html_attr') }}"{% endif %}
|
||||
{% if file_url_remove %}data-file-url-remove="{{ file_url_remove|e('html_attr') }}"{% endif %}>
|
||||
{% block file_extras %}{% endblock %}
|
||||
<input
|
||||
{# required attribute structures #}
|
||||
{% block input_attributes %}
|
||||
type="file"
|
||||
{% if files.multiple %}multiple="multiple"{% endif %}
|
||||
{% if files.accept %}accept="{{ files.accept|join(',') }}"{% endif %}
|
||||
{% if field.disabled %}disabled="disabled"{% endif %}
|
||||
{% if field.random_name %}random="true"{% endif %}
|
||||
{% if required %}required="required"{% endif %}
|
||||
{{ parent() }}
|
||||
{% endblock %}
|
||||
/>
|
||||
|
||||
{% for path, file in value %}
|
||||
<div class="hidden" data-file="{{ file|merge({remove: file.remove|default(''), path: file.path|default('')})|json_encode|e('html_attr') }}"></div>
|
||||
{% endfor %}
|
||||
{% include 'forms/fields/hidden/hidden.html.twig' with {field: {name: '_json.' ~ field.name}, value: (value ?? [])|json_encode } %}
|
||||
|
||||
</div>
|
||||
{% if inline_errors and errors %}
|
||||
<div class="{{ form_field_inline_error_classes }}">
|
||||
<p class="form-message"><i class="fa fa-exclamation-circle"></i> {{ errors|first|raw }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if grav.browser.browser == 'msie' and grav.browser.version < 12 %}
|
||||
{% do assets.addJs('plugin://form/assets/object.assign.polyfill.js') %}
|
||||
{% endif %}
|
||||
|
||||
{% do assets.addJs('jquery', 101) %}
|
||||
|
||||
{# FilePond core and plugins #}
|
||||
{% do assets.addJs('plugin://form/assets/filepond/filepond.min.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 98 }) %}
|
||||
{% do assets.addJs('plugin://form/assets/filepond/filepond-plugin-file-validate-size.min.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 97 }) %}
|
||||
{% do assets.addJs('plugin://form/assets/filepond/filepond-plugin-file-validate-type.min.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 97 }) %}
|
||||
{% do assets.addJs('plugin://form/assets/filepond/filepond-plugin-image-preview.min.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 97 }) %}
|
||||
{% do assets.addJs('plugin://form/assets/filepond/filepond-plugin-image-resize.min.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 97 }) %}
|
||||
{% do assets.addJs('plugin://form/assets/filepond/filepond-plugin-image-transform.min.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 97 }) %}
|
||||
|
||||
{# FilePond CSS #}
|
||||
{% do assets.addCss('plugin://form/assets/filepond/filepond.min.css') %}
|
||||
{% do assets.addCss('plugin://form/assets/filepond/filepond-plugin-image-preview.min.css') %}
|
||||
|
||||
{# Custom handlers - note: load this AFTER the libraries #}
|
||||
{% do assets.addJs('plugin://form/assets/filepond-handler.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 96 }) %}
|
||||
|
||||
{# {% if form.xhr_submit %}#}
|
||||
{# {% do assets.addJs('plugin://form/assets/filepond-reinit.js', { 'group': 'bottom', 'loading': 'defer', 'priority': 90 }) %}#}
|
||||
{# {% endif %}#}
|
||||
|
||||
{% do assets.addInlineJs("
|
||||
window.GravForm = window.GravForm || {};
|
||||
window.GravForm = Object.assign({}, window.GravForm, {
|
||||
translations: {
|
||||
PLUGIN_FORM: {
|
||||
'FILEPOND_REMOVE_FILE': " ~ 'PLUGIN_FORM.FILEPOND_REMOVE_FILE'|t|json_encode ~ ",
|
||||
'FILEPOND_REMOVE_FILE_CONFIRMATION': " ~ 'PLUGIN_FORM.FILEPOND_REMOVE_FILE_CONFIRMATION'|t|json_encode ~ ",
|
||||
'FILEPOND_CANCEL_UPLOAD': " ~ 'PLUGIN_FORM.FILEPOND_CANCEL_UPLOAD'|t|json_encode ~ ",
|
||||
'FILEPOND_ERROR_FILESIZE': " ~ 'PLUGIN_FORM.FILEPOND_ERROR_FILESIZE'|t|json_encode ~ ",
|
||||
'FILEPOND_ERROR_FILETYPE': " ~ 'PLUGIN_FORM.FILEPOND_ERROR_FILETYPE'|t|json_encode ~ "
|
||||
}
|
||||
}
|
||||
});
|
||||
", {'group': 'bottom', 'position': 'before'}) %}
|
||||
|
||||
{% do assets.addInlineJs("
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof GravFormXHR !== 'undefined') {
|
||||
// First check if DOM property exists
|
||||
if (GravFormXHR.DOM && typeof GravFormXHR.DOM.updateFormContent === 'function') {
|
||||
var originalUpdateFormContent = GravFormXHR.DOM.updateFormContent;
|
||||
GravFormXHR.DOM.updateFormContent = function() {
|
||||
var result = originalUpdateFormContent.apply(this, arguments);
|
||||
|
||||
// Dispatch event after form content is updated
|
||||
setTimeout(function() {
|
||||
document.dispatchEvent(new Event('grav-form-updated'));
|
||||
if (window.reinitializeFilePonds) {
|
||||
window.reinitializeFilePonds();
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
// If DOM property doesn't exist, try to hook into submit directly
|
||||
else if (typeof GravFormXHR.submit === 'function') {
|
||||
var originalSubmit = GravFormXHR.submit;
|
||||
GravFormXHR.submit = function(form) {
|
||||
var result = originalSubmit.apply(this, arguments);
|
||||
|
||||
// Reinitialize FilePond after form submission
|
||||
setTimeout(function() {
|
||||
document.dispatchEvent(new Event('grav-form-updated'));
|
||||
if (window.reinitializeFilePonds) {
|
||||
window.reinitializeFilePonds();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
", {'group': 'bottom'}) %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block field %}
|
||||
<input type="hidden" name="__form-name__" value="{{ form.name }}" />
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block field %}
|
||||
{% if form.task %}
|
||||
<input type="hidden" name="task" value="{{ form.task }}" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "forms/field.html.twig" %}
|
||||
|
||||
{% block field %}
|
||||
|
||||
{# Used if the field is being used directly outside of form #}
|
||||
{% set value = value ?? field.value ?? (field.evaluate ? evaluate(field.default) : field.default) %}
|
||||
|
||||
{# Evaluate support for the form #}
|
||||
{% if not has_value and value and field.evaluate %}
|
||||
{% set value = evaluate(value) %}
|
||||
{% endif %}
|
||||
{% set input_value = value is iterable ? value|join(',') : value|string %}
|
||||
|
||||
<input data-grav-field="hidden" data-grav-disabled="false" {% if field.id is defined %}id="{{ field.id|e }}" {% endif %}type="hidden" class="input" name="{{ (scope ~ field.name)|fieldName }}" value="{{ input_value|e('html_attr') }}" />
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user