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:
Charles N Wyble
2026-01-13 16:15:40 -05:00
parent e6c15cafb3
commit 9f7fe553dc
2596 changed files with 433475 additions and 113 deletions

View File

@@ -0,0 +1,2 @@
{% include 'partials/form-messages.html.twig' %}
{% do http_response_code(form.responseCode) %}

View File

@@ -0,0 +1,2 @@
{% include 'partials/form-messages.json.twig' %}
{% do http_response_code(form.responseCode) %}

View File

@@ -0,0 +1 @@
{% extends "forms/default/form.html.twig" %}

View File

@@ -0,0 +1,8 @@
{% extends 'partials/base.html.twig' %}
{% block content %}
{{ content|raw }}
{% include "forms/form.html.twig" %}
{% endblock %}

View File

@@ -0,0 +1 @@
{% extends 'forms/ajax.json.twig' %}

View 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 %}

View File

@@ -0,0 +1,5 @@
{% if form_json_response %}
{{ form_json_response|json_encode|raw }}
{% else %}
{}
{% endif %}

View File

@@ -0,0 +1 @@
{% extends "forms/default/data.html.twig" %}

View File

@@ -0,0 +1 @@
{% extends "forms/default/data.txt.twig" %}

View File

@@ -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, '') }}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>&times;</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>

View File

@@ -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" %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -0,0 +1,6 @@
{% extends "forms/field.html.twig" %}
{% block input_attributes %}
type="color"
{{ parent() }}
{% endblock %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -0,0 +1 @@
{{ value ? value|date('m/d/Y')|e }}

View File

@@ -0,0 +1,2 @@
{# DEPRECATED. Switched to Text field until implemented properly #}
{% extends "forms/fields/text/text.html.twig" %}

View File

@@ -0,0 +1 @@
{{ value ? value|date('m/d/Y \\a\\t g:i A')|e }}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -0,0 +1,5 @@
{% extends "forms/field.html.twig" %}
{% block field %}
<input type="hidden" name="__form-name__" value="{{ form.name }}" />
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "forms/field.html.twig" %}
{% block field %}
{% if form.task %}
<input type="hidden" name="task" value="{{ form.task }}" />
{% endif %}
{% endblock %}

View File

@@ -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 %}

View File

@@ -0,0 +1,15 @@
{% extends "forms/field.html.twig" %}
{% block field %}
{% set input_value = value is iterable ? value|join(',') : value|string %}
<input aria-hidden="true"
type="text"
{% if config.plugins.form.inline_css == true %}
style="visibility:hidden;position:absolute!important;height:1px;width:1px;overflow:hidden;clip:rect(1px,1px,1px,1px);"
{% endif %}
class="form-honeybear"
tabindex="-1"
autocomplete="off"
name="{{ (scope ~ field.name)|fieldName }}"
value="{{ input_value|e }}" />
{% endblock %}

View File

@@ -0,0 +1,28 @@
{% extends "forms/field.html.twig" %}
{% block input %}
<div class="form-input-wrapper {{ field.size }}">
{% set input_value = value is iterable ? value|join(',') : value|string %}
<input
type="text"
value="{{ input_value|e }}"
data-key-observe="{{ (scope ~ field.name)|fieldName }}"
{% block input_attributes %}
{% if field.classes is defined %}class="{{ field.classes }}" {% endif %}
{% 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 }}"{% endif %}
{% if field.autofocus in ['on', 'true', 1] %}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 in ['on', 'off'] %}autocomplete="{{ field.autocomplete }}"{% endif %}
{% if field.tabindex %}tabindex="{{ field.tabindex }}"{% endif %}
{% if field.validate.required in ['on', 'true', 1] %}required="required"{% endif %}
{% if field.validate.pattern %}pattern="{{ field.validate.pattern }}"{% endif %}
{% if field.validate.message %}title="{{ field.validate.message|e|t }}"
{% elseif field.title is defined %}title="{{ field.title|e|t }}" {% endif %}
{% endblock %}
/>
</div>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "forms/field.html.twig" %}
{% block input_attributes %}
type="month"
{{ parent() }}
{% endblock %}

View File

@@ -0,0 +1 @@
{{ nonce_field(form.getNonceAction() ?? 'form', form.getNonceName() ?? 'form-nonce')|raw }}

View File

@@ -0,0 +1,9 @@
{% extends "forms/fields/text/text.html.twig" %}
{% block input_attributes %}
type="number"
{% if field.validate.min is defined %}min="{{ field.validate.min }}"{% endif %}
{% if field.validate.max is defined %}max="{{ field.validate.max }}"{% endif %}
{% if field.validate.step is defined %}step="{{ field.validate.step }}"{% endif %}
{{ parent() }}
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% extends "forms/field.html.twig" %}
{% set value = null %}
{% block input_attributes %}
type="password"
{{ parent() }}
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "forms/field.html.twig" %}
{% block input %}
{% for key, text in field.options %}
{% set id = field.id|default(field.name) ~ '-' ~ key %}
<div class="radio {{ form_field_wrapper_classes }} {{ field.wrapper_classes }}">
<input type="radio"
value="{{ key|e }}"
id="{{ id|e }}"
name="{{ (scope ~ field.name)|fieldName }}"
class="{{ form_field_radio_classes }} {{ field.classes }}"
{% if key == value %}checked="checked" {% endif %}
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
{% if required %}required="required"{% endif %}
{% if field.tabindex %}tabindex="{{ field.tabindex }}"{% endif %}
/>
<label style="display: inline" class="inline" for="{{ id|e }}">{{ text|t|raw }}</label>
</div>
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "forms/field.html.twig" %}
{% block input_attributes %}
type="range"
{% if field.validate.min %}min="{{ field.validate.min }}"{% endif %}
{% if field.validate.max %}max="{{ field.validate.max }}"{% endif %}
{% if field.validate.step %}step="{{ field.validate.step }}"{% endif %}
{{ parent() }}
{% endblock %}

View File

@@ -0,0 +1,393 @@
{% extends "forms/field.html.twig" %}
{% block label %}{% endblock %}
{% block input %}
{% set config = grav.config %}
{% set formId = form.id ?: form.name %}
{% set callbackId = formId|underscorize %}
{% set lang = grav.language.language %}
{# Get configuration values with fallbacks #}
{% set version = field.recaptcha_version ?? config.plugins.form.recaptcha.version ?? '2-checkbox' %}
{% set site_key = field.recaptcha_site_key ?? config.plugins.form.recaptcha.site_key %}
{% set theme = field.recaptcha_theme ?? config.plugins.form.recaptcha.theme ?? 'light' %}
{% if not site_key %}
<div class="form-error">reCAPTCHA site key is not set. Please set it in the form field or plugin configuration.</div>
{% else %}
{% if version == 3 or version == '3' %}
{# --- reCAPTCHA v3 Handling --- #}
{% set action = (page.route|trim('/') ~ '-' ~ form.name)|underscorize|md5 %}
<div class="g-recaptcha-container"
data-form-id="{{ formId }}"
data-recaptcha-version="3"
data-captcha-provider="recaptcha"
data-intercepts-submit="true"
data-sitekey="{{ site_key }}"
data-version="3"
data-action="{{ action }}"
data-theme="{{ theme }}"
data-lang="{{ lang }}"
data-callback-id="{{ callbackId }}">
{# Container for v3 - will be managed by JS #}
<input type="hidden" name="data[token]" value="">
<input type="hidden" name="data[action]" value="{{ action }}">
</div>
{% do assets.addJs('https://www.google.com/recaptcha/api.js?render=' ~ site_key, { group: 'bottom' }) %}
<script>
(function() {
window.GravRecaptchaInitializers = window.GravRecaptchaInitializers || {};
function addHiddenInput(form, name, value) {
const existing = form.querySelector('input[type="hidden"][name="' + name + '"]');
if (existing) {
existing.value = value;
} else {
const input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', name);
input.setAttribute('value', value);
form.insertBefore(input, form.firstChild);
}
}
function initRecaptchaV3(container) {
const formId = container.dataset.formId;
const siteKey = container.dataset.sitekey;
const action = container.dataset.action;
const form = document.getElementById(formId);
if (!form) return;
console.log(`Initializing reCAPTCHA v3 for form ${formId}`);
const submitHandler = function(event) {
event.preventDefault();
console.log(`reCAPTCHA v3 intercepting submit for form ${formId}`);
grecaptcha.ready(function() {
grecaptcha.execute(siteKey, { action: action })
.then(function(token) {
console.log(`reCAPTCHA v3 token received for form ${formId}`);
addHiddenInput(form, 'data[token]', token);
addHiddenInput(form, 'data[action]', action);
form.removeEventListener('submit', submitHandler);
if (form.dataset.xhrEnabled === 'true' && window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
window.GravFormXHR.submit(form);
} else {
if (typeof form.requestSubmit === 'function') {
form.requestSubmit();
} else {
form.submit();
}
}
setTimeout(() => {
const currentForm = document.getElementById(formId);
if (currentForm && !currentForm.dataset.recaptchaListenerAttached) {
currentForm.addEventListener('submit', submitHandler);
currentForm.dataset.recaptchaListenerAttached = 'true';
} else if (currentForm) {
delete currentForm.dataset.recaptchaListenerAttached;
}
}, 0);
});
});
};
delete form.dataset.recaptchaListenerAttached;
if (!form.dataset.recaptchaListenerAttached) {
form.addEventListener('submit', submitHandler);
form.dataset.recaptchaListenerAttached = 'true';
}
}
// Register the initializer function
const initializerFunctionName = 'initRecaptcha_{{ formId }}';
window.GravRecaptchaInitializers[initializerFunctionName] = function() {
const container = document.querySelector('[data-form-id="{{ formId }}"][data-captcha-provider="recaptcha"]');
if (!container) return;
initRecaptchaV3(container);
};
// Initial call
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', window.GravRecaptchaInitializers[initializerFunctionName]);
} else {
setTimeout(window.GravRecaptchaInitializers[initializerFunctionName], 0);
}
})();
</script>
{% elseif version == '2-invisible' %}
{# --- reCAPTCHA v2 Invisible Handling --- #}
<div class="g-recaptcha-container"
data-form-id="{{ formId }}"
data-recaptcha-version="2-invisible"
data-captcha-provider="recaptcha"
data-intercepts-submit="true"
data-sitekey="{{ site_key }}"
data-version="2-invisible"
data-theme="{{ theme }}"
data-lang="{{ lang }}"
data-callback-id="{{ callbackId }}">
{# Container for v2 invisible - will be managed by JS #}
<div id="g-recaptcha-{{ formId }}" class="g-recaptcha"></div>
</div>
<script>
(function() {
window.GravRecaptchaInitializers = window.GravRecaptchaInitializers || {};
function addHiddenInput(form, name, value) {
const existing = form.querySelector('input[type="hidden"][name="' + name + '"]');
if (existing) {
existing.value = value;
} else {
const input = document.createElement('input');
input.setAttribute('type', 'hidden');
input.setAttribute('name', name);
input.setAttribute('value', value);
form.insertBefore(input, form.firstChild);
}
}
function initRecaptchaV2Invisible(container) {
const formId = container.dataset.formId;
const siteKey = container.dataset.sitekey;
const lang = container.dataset.lang;
const theme = container.dataset.theme;
const callbackId = container.dataset.callbackId || formId;
const form = document.getElementById(formId);
let widgetId = null;
if (!form) return;
console.log(`Initializing reCAPTCHA v2 Invisible for form ${formId}`);
const callbackName = 'captchaInvisibleOnloadCallback_' + callbackId;
if (typeof window[callbackName] !== 'function') {
window[callbackName] = function() {
console.log('reCAPTCHA Invisible API ready for form ' + formId);
};
if (!document.querySelector('script[src*="recaptcha/api.js?onload=' + callbackName + '"]')) {
const script = document.createElement('script');
script.src = 'https://www.google.com/recaptcha/api.js?onload=' + callbackName + '&hl=' + lang + '&theme=' + theme;
script.async = true;
script.defer = true;
document.head.appendChild(script);
}
}
const submitHandler = function(event) {
event.preventDefault();
console.log(`reCAPTCHA v2 Invisible intercepting submit for form ${formId}`);
if (typeof grecaptcha === 'undefined' || typeof grecaptcha.render === 'undefined') {
console.error('grecaptcha not ready for invisible captcha');
return;
}
const recaptchaId = 'g-recaptcha-' + formId;
let captchaElement = document.getElementById(recaptchaId);
if (!captchaElement) {
captchaElement = document.createElement('div');
captchaElement.setAttribute('id', recaptchaId);
captchaElement.className = 'g-recaptcha';
form.appendChild(captchaElement);
}
const renderCaptcha = () => {
if (widgetId !== null) {
try {
grecaptcha.reset(widgetId);
} catch (e) {
console.warn("Error resetting captcha", e);
}
}
widgetId = grecaptcha.render(recaptchaId, {
sitekey: siteKey,
size: 'invisible',
callback: function(token) {
console.log(`reCAPTCHA v2 Invisible token received for form ${formId}`);
addHiddenInput(form, 'g-recaptcha-response', token);
form.removeEventListener('submit', submitHandler);
if (form.dataset.xhrEnabled === 'true' && window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
window.GravFormXHR.submit(form);
} else {
if (typeof form.requestSubmit === 'function') {
form.requestSubmit();
} else {
form.submit();
}
}
setTimeout(() => {
const currentForm = document.getElementById(formId);
if (currentForm && !currentForm.dataset.recaptchaListenerAttached) {
currentForm.addEventListener('submit', submitHandler);
currentForm.dataset.recaptchaListenerAttached = 'true';
} else if (currentForm) {
delete currentForm.dataset.recaptchaListenerAttached;
}
}, 0);
}
});
grecaptcha.execute(widgetId);
};
if (typeof grecaptcha !== 'undefined' && grecaptcha.render) {
renderCaptcha();
} else {
const originalOnload = window[callbackName];
window[callbackName] = function() {
if(originalOnload) originalOnload();
renderCaptcha();
};
console.warn("grecaptcha object not found immediately, waiting for onload callback: " + callbackName);
}
};
delete form.dataset.recaptchaListenerAttached;
if (!form.dataset.recaptchaListenerAttached) {
form.addEventListener('submit', submitHandler);
form.dataset.recaptchaListenerAttached = 'true';
}
}
// Register the initializer function
const initializerFunctionName = 'initRecaptcha_{{ formId }}';
window.GravRecaptchaInitializers[initializerFunctionName] = function() {
const container = document.querySelector('[data-form-id="{{ formId }}"][data-captcha-provider="recaptcha"]');
if (!container) return;
initRecaptchaV2Invisible(container);
};
// Initial call
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', window.GravRecaptchaInitializers[initializerFunctionName]);
} else {
setTimeout(window.GravRecaptchaInitializers[initializerFunctionName], 0);
}
})();
</script>
{% else %}
{# --- reCAPTCHA v2 Checkbox Handling --- #}
{# Add script and container #}
{% set container_id = 'g-recaptcha-' ~ formId %}
{% set onloadCallback = 'captchaCheckboxOnloadCallback_' ~ callbackId %}
<div class="g-recaptcha-container"
data-form-id="{{ formId }}"
data-captcha-provider="recaptcha"
data-sitekey="{{ site_key }}"
data-version="2-checkbox"
data-theme="{{ theme }}"
data-lang="{{ lang }}"
data-callback-id="{{ callbackId }}">
<div id="{{ container_id }}" class="g-recaptcha"></div>
</div>
{% do assets.addJs('https://www.google.com/recaptcha/api.js?onload=' ~ onloadCallback ~ '&render=explicit', { 'group': 'bottom', 'loading': 'defer' }) %}
<script>
(function() {
// Explicit rendering for reCAPTCHA v2 Checkbox
window.GravExplicitCaptchaInitializers = window.GravExplicitCaptchaInitializers || {};
const initializerFunctionName = 'initExplicitCaptcha_{{ formId }}';
// Define the initializer function
window.GravExplicitCaptchaInitializers[initializerFunctionName] = function() {
const containerId = '{{ container_id }}';
const container = document.getElementById(containerId);
if (!container) {
console.warn('reCAPTCHA container #' + containerId + ' not found.');
return;
}
// Prevent re-rendering if widget already exists
if (container.innerHTML.trim() !== '' && container.querySelector('iframe')) {
return;
}
// Get configuration from parent container
const parentContainer = container.closest('.g-recaptcha-container');
if (!parentContainer) {
console.error('Cannot find parent container for #' + containerId);
return;
}
const sitekey = parentContainer.dataset.sitekey;
const theme = parentContainer.dataset.theme;
if (!sitekey) {
console.error('reCAPTCHA sitekey missing for #' + containerId);
return;
}
console.log('Attempting to render reCAPTCHA in #' + containerId);
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.render === 'function') {
try {
grecaptcha.render(containerId, {
sitekey: sitekey,
theme: theme,
callback: function(token) {
console.log('reCAPTCHA challenge successful for #' + containerId);
}
});
} catch (e) {
console.error('Error calling grecaptcha.render for #' + containerId, e);
container.innerHTML = '<p style="color:red;">Error initializing reCAPTCHA.</p>';
}
} else {
console.warn('grecaptcha API not available yet for #' + containerId + '. Waiting for onload.');
}
};
// Define the global onload callback
window['{{ onloadCallback }}'] = function() {
console.log('reCAPTCHA API loaded, triggering init for #{{ container_id }}');
if (window.GravExplicitCaptchaInitializers[initializerFunctionName]) {
window.GravExplicitCaptchaInitializers[initializerFunctionName]();
} else {
console.error("Initializer " + initializerFunctionName + " not found!");
}
};
// Form submit handler to check if captcha is completed
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('{{ formId }}');
if (form) {
form.addEventListener('submit', function(event) {
const response = grecaptcha.getResponse();
if (!response) {
event.preventDefault();
alert("{{ field.captcha_not_validated|t|default('Please complete the captcha')|e('js') }}");
} else if (form.dataset.xhrEnabled === 'true' && window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
event.preventDefault();
window.GravFormXHR.submit(form);
}
});
}
});
})();
</script>
{% endif %}
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "forms/field.html.twig" %}
{% set title_level = grav['admin'] is defined ? 'h1' : field.title_level|default('h3') %}
{% block field %}
{% if field.security is empty or authorize(array(field.security)) %}
<div class="{{ field.classes }}">
{% if field.title or field.underline %}
<{{ title_level }} class="{% if not field.underline %}no_underline{% endif %}">{{ field.title|t }}</{{ title_level }}>
{% endif %}
{% if field.text %}
<p>{{ field.text|t|raw }}</p>
{% endif %}
{% embed 'forms/default/fields.html.twig' with {name: field.name, fields: field.fields} %}
{% block outer_markup_field_open %}<div class="form-section">{% endblock %}
{% block outer_markup_field_close %}</div>{% endblock %}
{% endembed %}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,78 @@
{% extends "forms/field.html.twig" %}
{% block global_attributes %}
data-grav-selectize="{{ (field.selectize is defined ? field.selectize : {})|json_encode()|e('html_attr') }}"
{{ parent() }}
{% endblock %}
{% block input %}
<div class="{{ form_field_wrapper_classes ?: 'form-select-wrapper' }} {{ field.size }} {{ field.wrapper_classes }}">
<select name="{{ (scope ~ field.name)|fieldName ~ (field.multiple ? '[]' : '') }}"
class="{{ form_field_select_classes }} {{ field.classes }} {{ 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 %}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.multiple in ['on', 'true', 1] %}multiple="multiple"{% endif %}
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
{% if field.tabindex %}tabindex="{{ field.tabindex }}"{% endif %}
{% if field.form %}form="{{ field.form }}"{% endif %}
{% if field.autocomplete is defined %}autocomplete="{{ field.autocomplete }}"{% endif %}
{% if field.key %}
data-key-observe="{{ (scope ~ field.name)|fieldName }}"
{% endif %}
{% if field.datasets %}
{% for datakey, datavalue in field.datasets %}
data-{{ datakey }}="{{ datavalue|e('html_attr') }}"
{% endfor %}
{% endif %}
{% if field.attributes %}
{% for key, value in field.attributes %}
{{ key }}="{{ value|e('html_attr') }}"
{% endfor %}
{% endif %}
>
{% if field.placeholder %}<option value="" disabled selected>{{ field.placeholder|t|raw }}</option>{% endif %}
{% set options = field.options %}
{% if field.selectize.create and value %}
{% set custom_value = field.multiple ? value : { (value): value } %}
{% set options = options|merge(custom_value|default([]))|array_unique %}
{% endif %}
{% set value = value is iterable ? value : value|string %}
{% for key, item_value in options %}
{% if item_value is iterable and item_value.value %}
{% set akey = field.selectize and field.multiple ? item_value : key %}
{% set avalue = item_value.value|t %}
<option {{ item_value.disabled ? 'disabled="disabled"' : '' }}
{{ item_value.selected or key == value ? 'selected="selected"' : '' }}
{{ item_value.label ? 'label=' ~ item_value.label : '' }}
value="{{ akey }}"
>
{{ avalue|raw }}
</option>
{% elseif item_value is iterable %}
{% set optgroup_label = item_value|keys|first %}
<optgroup label="{{ optgroup_label|t|e('html_attr') }}">
{% for subkey, suboption in field.options[key][optgroup_label] %}
{% set subkey = subkey|string %}
{% set item_value = (field.selectize and field.multiple ? suboption : subkey)|string %}
{% set selected = (field.selectize ? suboption : subkey)|string %}
<option {% if subkey is same as (value) or (field.multiple and selected in value) %}selected="selected"{% endif %} value="{{ subkey }}">
{{ suboption|t|raw }}
</option>
{% endfor %}
</optgroup>
{% else %}
{% set val = (field.selectize and field.multiple ? item_value : key)|string %}
{% set selected = (field.selectize ? item_value : key)|string %}
<option {% if val is same as (value) or (field.multiple and selected in value) %}selected="selected"{% endif %} value="{{ val }}">{{ item_value|t|raw }}</option>
{% endif %}
{% endfor %}
</select>
</div>
{% endblock %}

View File

@@ -0,0 +1,2 @@
{# Deprecated Form 4.0: Just use `select` field #}
{% extends "forms/fields/select/select.html.twig" %}

View File

@@ -0,0 +1,105 @@
{% extends "forms/field.html.twig" %}
{% block prepend %}
<div id="signature-pad" class="signature-pad">
<div class="signature-pad--body">
<canvas></canvas>
</div>
<div class="signature-pad--footer">
<div class="description">Sign above</div>
<div class="signature-pad--actions">
<div>
<button type="button" class="btn btn-primary-border" data-action="clear">Clear Signature</button>
</div>
</div>
</div>
</div>
<script src="{{ url('plugin://form/assets/signature_pad.js') }}"></script>
<script>
var wrapper = document.getElementById("signature-pad");
var clearButton = wrapper.querySelector("[data-action=clear]");
var canvas = wrapper.querySelector("canvas");
var signaturePad = new SignaturePad(canvas, {
// It's Necessary to use an opaque color when saving image as JPEG;
// this option can be omitted if only saving as PNG or SVG
backgroundColor: 'rgb(255, 255, 255)',
onEnd: function() {
var input = document.querySelector('[name="data[{{ field.name }}]"]');
input.value = this.toDataURL();
}
});
// Adjust canvas coordinate space taking into account pixel ratio,
// to make it look crisp on mobile devices.
// This also causes canvas to be cleared.
function resizeCanvas() {
// When zoomed out to less than 100%, for some very strange reason,
// some browsers report devicePixelRatio as less than 1
// and only part of the canvas is cleared then.
var ratio = Math.max(window.devicePixelRatio || 1, 1);
// This part causes the canvas to be cleared
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
// This library does not listen for canvas changes, so after the canvas is automatically
// cleared by the browser, SignaturePad#isEmpty might still return false, even though the
// canvas looks empty, because the internal data of this library wasn't cleared. To make sure
// that the state of this library is consistent with visual state of the canvas, you
// have to clear it manually.
signaturePad.clear();
}
// On mobile devices it might make more sense to listen to orientation change,
// rather than window resize events.
window.onresize = resizeCanvas;
resizeCanvas();
function download(dataURL, filename) {
var blob = dataURLToBlob(dataURL);
var url = window.URL.createObjectURL(blob);
var a = document.createElement("a");
a.style = "display: none";
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}
// One could simply use Canvas#toBlob method instead, but it's just to show
// that it can be done using result of SignaturePad#toDataURL.
function dataURLToBlob(dataURL) {
// Code taken from https://github.com/ebidel/filer.js
var parts = dataURL.split(';base64,');
var contentType = parts[0].split(":")[1];
var raw = window.atob(parts[1]);
var rawLength = raw.length;
var uInt8Array = new Uint8Array(rawLength);
for (var i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
}
clearButton.addEventListener("click", function (event) {
signaturePad.clear();
});
</script>
{% endblock prepend %}
{% block input_attributes %}
type="hidden"
{{ parent() }}
{% endblock %}

View File

@@ -0,0 +1,19 @@
{% extends "forms/field.html.twig" %}
{% block field %}
<div class="form-field form-spacer {{ field.classes }}">
{% if field.title %}
<{{ title_type|default('h3') }}>{{- field.title|t|raw -}}</ {{ title_type|default('h3') }}>
{% endif %}
{% if field.markdown %}
<p>{{- field.text|t|markdown|raw -}}</p>
{% else %}
<p>{{- field.text|t|raw -}}</p>
{% endif %}
{% if field.underline %}
<hr />
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
{% extends 'forms/fields/checkbox/checkbox.html.twig' %}

View File

@@ -0,0 +1,8 @@
{% extends "forms/field.html.twig" %}
{% block field %}
{% embed 'forms/default/fields.html.twig' with {name: field.name, fields: field.fields} %}
{% block outer_markup_field_open %}<div class="form-tab">{% endblock %}
{% block outer_markup_field_close %}</div>{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -0,0 +1,60 @@
{% extends "forms/field.html.twig" %}
{% if not grav.admin %}
{% 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 %}
{% block field %}
<div class="form-tabs {{ field.class }} {{ field.classes }}">
{% set fields = prepare_form_fields(field.fields, field.name) %}
{% if fields|length %}
{% set tabs = {} %}
{% for tab in fields %}
{% if tab.type == 'tab' and not tab.validate.ignore and (tab.security is empty or authorize(array(tab.security))) %}
{% set tabs = tabs|merge([tab]) %}
{% endif %}
{% endfor %}
{% set count = tabs|length %}
{% if count == 0 %}
{# Nothing to display #}
{% elseif count == 1 and not admin %}
{% set fields = (tabs|first).fields %}
{% for field in fields %}
{% set value = field.name ? (form ? form.value(field.name) : data.value(field.name)) : data.toArray %}
{% set field_templates = include_form_field(field.type, field_layout, 'text') %}
{% include field_templates %}
{% endfor %}
{% else %}
{% set tabsKey = form.name ~ '-' ~ fields|keys|join(':')|md5 %}
{% set storedValue = grav.admin ? get_cookie('grav-tabs-state')|default('{}')|json_decode : [] %}
{% set storedTab = attribute(storedValue, 'tab-' ~ tabsKey) %}
{% if storedTab is empty %}
{% set active = uri.params.tab ?? field.active ?? 1 %}
{% endif %}
<div class="tabs-nav">
{% for tab in tabs %}
{% if tab.type == 'tab' and (tab.condition is null or tab.condition == true) %}
<a class="tab__link {{ (storedTab == scope ~ tab.name) or active == loop.index ? 'active' : '' }}" data-tabid="tab-{{ tabsKey ~ '-' ~ tab.name }}" data-tabkey="tab-{{ tabsKey }}" data-scope="{{ scope ~ tab.name }}">
<span>{{ tab.title|t }}</span>
{% endif %}
</a>
{% endfor %}
</div>
<div class="tabs-content">
{% embed 'forms/default/fields.html.twig' with {name: field.name, fields: fields} %}
{% block inner_markup_field_open %}
<div id="tab-{{ tabsKey ~ '-' ~ field.name }}" class="tab__content {{ (storedTab == scope ~ field.name) or active == loop.index ? 'active' : '' }}">
{% endblock %}
{% block inner_markup_field_close %}
</div>
{% endblock %}
{% endembed %}
</div>
{% endif %}
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "forms/field.html.twig" %}
{% block input_attributes %}
type="tel"
{% 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 %}

View File

@@ -0,0 +1,7 @@
{% set value = iterable ? value|join(', ') : value|string %}
{% if link -%}
<a href="{{ link|e }}">{{ value|e }}</a>
{%- else -%}
{{ value|e }}
{%- endif %}

View File

@@ -0,0 +1,39 @@
{% if field.prepend or field.append or field.copy_to_clipboard %}
{% set field = field|merge({'wrapper_classes': 'form-input-addon-wrapper'}) %}
{% endif %}
{% extends "forms/field.html.twig" %}
{% block prepend %}
{% if field.prepend %}
<div class="form-input-addon form-input-prepend">
{{- field.prepend|t|raw -}}
</div>
{% endif %}
{% 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 %}
{% block append %}
{% if field.copy_to_clipboard %}
<div class="form-input-addon form-input-append copy-to-clipboard">
{% if field.copy_to_clipboard in ['0', '1'] %}
<i class="fa fa-clipboard"></i>
{% else %}
{{- field.copy_to_clipboard|t|raw -}}
{% endif %}
</div>
{% elseif field.append %}
<div class="form-input-addon form-input-append">
{{- field.append|t|raw -}}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends "forms/field.html.twig" %}
{% block input %}
<div class="{{ form_field_wrapper_classes ?: 'form-textarea-wrapper' }} {{ field.size }} {{ field.wrapper_classes }}">
{% block prepend %}{% endblock prepend %}
<textarea
{# required attribute structures #}
name="{{ (scope ~ field.name)|fieldName }}"
{# input attribute structures #}
{% block input_attributes %}
class="{{ form_field_textarea_classes }} {{ field.classes }} {{ 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 }}"{% endif %}
{% if field.autofocus in ['on', 'true', 1] %}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 in ['on', 'off'] %}autocomplete="{{ field.autocomplete }}"{% endif %}
{% if field.tabindex %}tabindex="{{ field.tabindex }}"{% endif %}
{% if required %}required="required"{% endif %}
{% if field.validate.pattern %}pattern="{{ field.validate.pattern }}"{% endif %}
{% if field.validate.message %}title="{{ field.validate.message|t|e }}"{% endif %}
{% if field.rows is defined %}rows="{{ field.rows }}"{% endif %}
{% if field.cols is defined %}cols="{{ field.cols }}"{% 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 %}
{% if field.datasets %}
{% for datakey, datavalue in field.datasets %}
data-{{ datakey }}="{{ datavalue|e('html_attr') }}"
{% endfor %}
{% 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 %}
>{{ value|trim|e('html') }}</textarea>
{% block append %}{% endblock append %}
{% if inline_errors and errors %}
<div class="{{ form_errors_classes ?: 'form-errors' }}">
<p class="form-message"><i class="fa fa-exclamation-circle"></i> {{ errors|first }}</p>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
{{ value ? value|date('g:i A')|e }}

View File

@@ -0,0 +1,6 @@
{% extends "forms/field.html.twig" %}
{% block input_attributes %}
type="time"
{{ parent() }}
{% endblock %}

View File

@@ -0,0 +1,5 @@
{%- if value -%}
<span><i class="published fa fa-check-circle"></i></span>
{%- else -%}
<span><i class="unpublished fa fa-times-circle"></i></span>
{%- endif -%}

View File

@@ -0,0 +1,52 @@
{% extends "forms/field.html.twig" %}
{% macro spanToggle(input, length) %}
{% set space = repeat('&nbsp;&nbsp;', (length - input|length) / 2) %}
{{ (space ~ input ~ space)|raw }}
{% endmacro %}
{% import _self as macro %}
{% set has_hidden = false %}
{% for key, text in field.options %}
{% if key is empty %}
{% set has_hidden = true %}
{% endif %}
{% endfor %}
{% block global_attributes %}
{{ parent() }}
data-grav-field-name="{{ (scope ~ field.name)|fieldName }}"
{% endblock %}
{% block input %}
<div class="switch-toggle switch-grav {{ field.size }} switch-{{ field.options|length }} {{ field.classes }}">
{% set maxLen = 0 %}
{% for text in field.options %}
{% set translation = text|t|trim %}
{% set maxLen = max(translation|length, maxLen) %}
{% endfor %}
{# Value falls back to highlight instead of default #}
{% set highlight = field.highlight|string %}
{% set value = (value ?? default ?? highlight)|string %}
{% for key, text in field.options %}
{% set key = key|string %}
{% set id = (field.id ?? ("toggle_" ~ field.name)) ~ key %}
{% set translation = text|t|trim %}
<input type="radio"
value="{{ key }}"
id="{{ id }}"
name="{{ (scope ~ field.name)|fieldName }}"
{% if highlight is same as(key) %}class="highlight"{% endif %}
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
{% if key is same as(value) %}checked="checked"{% endif %}
{% if required %}required="required"{% endif %}
{% if field.tabindex %}tabindex="{{ field.tabindex }}"{% endif %}
/>
<label for="{{ id }}">{{ (macro.spanToggle(translation, maxLen)|trim)|raw }}</label>
{% endfor %}
</div>
{% endblock %}

View File

@@ -0,0 +1,107 @@
{% extends "forms/field.html.twig" %}
{% block label %}{% endblock %}
{% block input %}
{% set config = grav.config %}
{% set formId = form.id ?: form.name %}
{# Get configuration values with fallbacks #}
{% set site_key = field.turnstile_site_key ?? config.plugins.form.turnstile.site_key %}
{% set theme = field.turnstile_theme ?? config.plugins.form.turnstile.theme ?? 'light' %}
{% set container_id = 'cf-turnstile-' ~ formId %}
{% set init_var = 'turnstile_initialized_' ~ formId %}
{% if not site_key %}
<div class="form-error">Turnstile site key is not set. Please set it in the form field or plugin configuration.</div>
{% else %}
{# Add a hidden field for the token directly in the INPUT block to ensure it's part of the field #}
<input type="hidden" name="cf-turnstile-response" value="" class="turnstile-token" />
<div class="turnstile-container"
data-form-id="{{ formId }}"
data-captcha-provider="turnstile"
data-sitekey="{{ site_key }}"
data-theme="{{ theme }}">
<div id="{{ container_id }}" class="cf-turnstile"></div>
</div>
{% do assets.addJs('https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback_' ~ formId ~ '&render=explicit', { 'loading': 'async', 'defer': '' }) %}
<script>
(function() {
// Prevent multiple initialization
if (window['{{ init_var }}']) {
console.log('Turnstile already initialized for form {{ formId }}');
return;
}
// Mark as initialized
window['{{ init_var }}'] = true;
// Use unique callback name to avoid conflicts with multiple forms
window['onloadTurnstileCallback_{{ formId }}'] = function() {
console.log('Turnstile API loaded - initializing widget for {{ formId }}');
const container = document.getElementById('{{ container_id }}');
if (!container) {
console.error('Turnstile container #{{ container_id }} not found');
return;
}
const form = document.getElementById('{{ formId }}');
if (!form) {
console.error('Cannot find form #{{ formId }}');
return;
}
// Find the token input field
const tokenField = form.querySelector('input[name="cf-turnstile-response"]');
if (!tokenField) {
console.error('Token field not found in form #{{ formId }}');
}
// Check if this container already has a widget (avoid duplicates)
if (container.querySelector('iframe')) {
console.log('Turnstile widget already initialized in this container');
return;
}
try {
turnstile.render('#{{ container_id }}', {
sitekey: '{{ site_key }}',
theme: '{{ theme }}',
callback: function(token) {
console.log('Turnstile callback fired with token', token.substring(0, 10) + '...');
// Update the token field
if (tokenField) {
tokenField.value = token;
console.log('Updated token field with value');
}
},
'expired-callback': function() {
console.warn('Turnstile token expired');
if (tokenField) {
tokenField.value = '';
}
},
'error-callback': function(error) {
console.error('Turnstile error:', error);
}
});
console.log('Turnstile render call completed');
} catch (e) {
console.error('Error initializing Turnstile:', e);
}
};
// Check if we can initialize immediately
if (document.getElementById('{{ container_id }}') && typeof turnstile !== 'undefined') {
window['onloadTurnstileCallback_{{ formId }}']();
}
})();
</script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,5 @@
{% extends "forms/field.html.twig" %}
{% block field %}
<input type="hidden" name="__unique_form_id__" value="{{ form.uniqueid() ?? random_string(20) }}" />
{% endblock %}

View File

@@ -0,0 +1 @@
<a href="{{ url(value) }}" target="_blank"><i class="fa fa-link"></i> {{ value|e }}</a>

View File

@@ -0,0 +1,9 @@
{% extends "forms/field.html.twig" %}
{% block input_attributes %}
type="url"
{% 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 %}

View File

@@ -0,0 +1,18 @@
{% extends "forms/field.html.twig" %}
{% if field.options %}
{% set value = field.options[value] ?: value %}
{% endif %}
{% block input %}
<span class="field-value">
{% switch field.filter %}
{% case 'date' %}
{{ value|date }}
{% case 'raw' %}
{{ value|raw }}
{% default %}
{{ value }}
{% endswitch %}
</span>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "forms/field.html.twig" %}
{% block input_attributes %}
type="week"
{{ parent() }}
{% endblock %}

View File

@@ -0,0 +1,34 @@
{#
DO NOT MODIFY!
Default layout can be found in form plugin or your theme:
templates/forms/layouts/form/default-form.html.twig
templates/forms/layouts/field/default-field.html.twig
templates/forms/layouts/button/default-button.html.twig
If you want to use multiple different layouts in your site, you can do:
```
form:
name: my-form
layout: tailwind
```
and copy the above files to:
templates/forms/layouts/form/tailwind-form.html.twig
templates/forms/layouts/field/tailwind-field.html.twig
templates/forms/layouts/button/tailwind-button.html.twig
You can also override individual fields by copying (using text field as an example):
templates/forms/fields/text/text.html.twig -> templates/forms/fields/text/tailwind-text.html.twig
#}
{% extends "forms/default/form.html.twig" %}
{% block xhr %}
{% include 'forms/layouts/xhr.html.twig' %}
{% endblock %}

View File

@@ -0,0 +1,4 @@
{# DEPRECATED: Do not override this file! Use forms/layouts/button/[FORM_LAYOUT]-button.html.twig instead #}
{% set layout = layout ?? form.layout ?? 'default' %}
{% extends ["forms/layouts/button/#{layout}-button.html.twig", 'forms/layouts/button/default-button.html.twig'] %}

View File

@@ -0,0 +1,12 @@
{% set button_tag %}
<button
{% block embed_button_core %}{% endblock %}
{% block embed_button_classes %}{% endblock %}
>{%- block embed_button_content -%}{%- endblock -%}</button>
{% endset %}
{% if button_url %}
<a href="{{ button_url|e }}">{{ button_tag|trim|raw }}</a>
{% else %}
{{ button_tag|trim|raw }}
{% endif %}

View File

@@ -0,0 +1 @@
{% block field_input_classes %}{% endblock %}

View File

@@ -0,0 +1,4 @@
{# DEPRECATED: Do not override this file! Use forms/layouts/field/[FORM_LAYOUT]-field.html.twig instead #}
{% set layout = layout ?? form.layout ?? 'default' %}
{% extends ["forms/layouts/field/#{layout}-field.html.twig", 'forms/layouts/field/default-field.html.twig'] %}

View File

@@ -0,0 +1,53 @@
{% block field %}
<div class="form-field {{ layout_form_field_outer_classes|trim }} {{ form_field_outer_core|trim}}">
{% block contents %}
{% if show_label %}
<div class="{{- layout_form_field_outer_label_classes -}}">
{{- form_field_toggleable -}}
<label class="{{ layout_form_field_label_classes }}{{ form_field_label_trim }}" {% if field.id %}for="{{ form_field_for }}"{% endif %}>
{%- block label -%}
{%- if form_field_help -%}
<span class="tooltip" data-tooltip="{{ form_field_help|e }}">{{ form_field_label|raw }}</span>
{%- else -%}
{{ form_field_label|raw }}
{%- endif -%}
{%- if form_field_required %}
<span class="required">*</span>
{%- endif -%}
{%- endblock -%}
</label>
</div>
{% endif %}
<div class="{{ layout_form_field_outer_data_classes }}"
{% block global_attributes %}{% endblock %}
>
{% block group %}
{% block input %}
<div class="{{ layout_form_field_wrapper_classes }} {{ field.size }}">
{% block prepend %}{% endblock prepend %}
{% set input_value = value is iterable ? value|join(',') : value|string %}
<input
name="{{ (scope ~ field.name)|fieldName }}"
value="{{ input_value|e }}"
{% block input_attributes %}{% endblock %}
/>
{% block append %}{% endblock append %}
{% 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 %}
</div>
{% endblock %}
{% endblock %}
{% if field.description is defined %}
<div class="{{ form_field_extra_wrapper_classes }}">
<span class="form-description">
{{ form_field_description|raw }}
</span>
</div>
{% endif %}
</div>
{% endblock %}
</div>
{% endblock %}

View File

@@ -0,0 +1,4 @@
{# DEPRECATED: Do not override this file! Use forms/layouts/form/[FORM_LAYOUT]-form.html.twig instead #}
{% set layout = layout ?? form.layout ?? 'default' %}
{% extends ["forms/layouts/form/#{layout}-form.html.twig", 'forms/layouts/form/default-form.html.twig'] %}

View File

@@ -0,0 +1,9 @@
<form
{% block embed_form_core %}{% endblock %}
{% block embed_form_classes %}{% endblock %}
{% block embed_form_custom_attributes %}{% endblock %}
>
{% block embed_fields %}{% endblock %}
{% block embed_buttons %}{% endblock %}
</form>

View File

@@ -0,0 +1,25 @@
{% if form.xhr_submit == true %}
{# Ensure xhr-submitter.js is loaded BEFORE the inline JS that uses it #}
{% do assets.addJs('plugin://form/assets/xhr-submitter.js', {'group': 'bottom', 'priority': 101, 'position': 'before'}) %}
{% do assets.addInlineJs("
document.addEventListener('DOMContentLoaded', () => {
// This now primarily sets up the *potential* for XHR submission
// It might not attach the listener directly if recaptcha is present
attachFormSubmitListener('" ~ form.id ~ "');
// Re-run captcha initializers *if* the form was loaded via XHR initially
// This covers edge cases, might not be strictly needed if captcha script handles DOMContentLoaded
const formElement = document.getElementById('" ~ form.id ~ "');
if (formElement && window.GravRecaptchaInitializers) {
const initializerFuncName = 'initRecaptcha_" ~ form.id ~ "';
if (typeof window.GravRecaptchaInitializers[initializerFuncName] === 'function') {
// Check if it needs init (e.g., if container exists but no widget/listener)
// For simplicity, just call it again; the init function should be idempotent
// window.GravRecaptchaInitializers[initializerFuncName]();
}
}
});",
{'group': 'bottom', 'priority': 100, 'position': 'before'}) %}
{% do assets.addJs('plugin://form/assets/captcha/recaptcha-handler.js', {'group': 'bottom', 'priority': 99, 'position': 'before'}) %}
{% do assets.addJs('plugin://form/assets/captcha/turnstile-handler.js', {'group': 'bottom', 'priority': 98, 'position': 'before'}) %}
{% endif %}

View File

@@ -0,0 +1,8 @@
<div class="modular-row form {{ page.header.class }}">
{{ content|raw }}
{% if config.plugins.form.modular_form_fix %}
{% include "forms/form.html.twig" with {form: forms({route: page.route})} %}
{% else %}
{% include "forms/form.html.twig" %}
{% endif %}
</div>

View File

@@ -0,0 +1,6 @@
{% if form.message %}
{% set inline_errors = form.inline_errors is not null ? form.inline_errors : config.plugins.form.inline_errors(false) %}
{% set status_mapping = {'success':'green', 'error': 'red', 'warning': 'yellow'} %}
{% set message = inline_errors and form.messages ? "GRAV.FORM.VALIDATION_FAIL"|t : form.message %}
<div class="notices {{ form.status }} {{ status_mapping[form.status] ?: 'green' }}"><p>{{ message|raw }}</p></div>
{% endif %}

View File

@@ -0,0 +1,3 @@
{% set inline_errors = form.inline_errors is not null ? form.inline_errors : config.plugins.form.inline_errors(false) %}
{% set message = inline_errors and form.messages ? "GRAV.FORM.VALIDATION_FAIL"|t : form.message %}
{{ {'status':form.status, 'message':message}|json_encode|raw }}