martin plugin

This commit is contained in:
Shohrat 2023-10-22 18:17:13 +05:00
parent 17e2de75ea
commit 23e4015d6c
81 changed files with 7929 additions and 0 deletions

View File

@ -0,0 +1,19 @@
# MIT license
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,107 @@
<?php
namespace Martin\Forms;
use Backend, Lang, Validator;
use System\Classes\PluginBase;
use System\Classes\SettingsManager;
use Martin\Forms\Classes\BackendHelpers;
use Martin\Forms\Classes\GDPR;
use Martin\Forms\Classes\ReCaptchaValidator;
use Martin\Forms\Classes\UnreadRecords;
use Martin\Forms\Models\Settings;
class Plugin extends PluginBase {
public function pluginDetails() {
return [
'name' => 'martin.forms::lang.plugin.name',
'description' => 'martin.forms::lang.plugin.description',
'author' => 'Martin M.',
'icon' => 'icon-bolt',
'homepage' => 'https://github.com/skydiver/'
];
}
public function registerNavigation() {
if(Settings::get('global_hide_button', false)) { return; }
return [
'forms' => [
'label' => 'martin.forms::lang.menu.label',
'icon' => 'icon-bolt',
'iconSvg' => 'plugins/martin/forms/assets/imgs/icon.svg',
'url' => BackendHelpers::getBackendURL(['martin.forms.access_records' => 'martin/forms/records', 'martin.forms.access_exports' => 'martin/forms/exports'], 'martin.forms.access_records'),
'permissions' => ['martin.forms.*'],
'sideMenu' => [
'records' => [
'label' => 'martin.forms::lang.menu.records.label',
'icon' => 'icon-database',
'url' => Backend::url('martin/forms/records'),
'permissions' => ['martin.forms.access_records'],
'counter' => UnreadRecords::getTotal(),
'counterLabel' => 'Un-Read Messages'
],
'exports' => [
'label' => 'martin.forms::lang.menu.exports.label',
'icon' => 'icon-download',
'url' => Backend::url('martin/forms/exports'),
'permissions' => ['martin.forms.access_exports']
],
]
]
];
}
public function registerSettings() {
return [
'config' => [
'label' => 'martin.forms::lang.menu.label',
'description' => 'martin.forms::lang.menu.settings',
'category' => SettingsManager::CATEGORY_CMS,
'icon' => 'icon-bolt',
'class' => 'Martin\Forms\Models\Settings',
'permissions' => ['martin.forms.access_settings'],
'order' => 500
]
];
}
public function registerPermissions() {
return [
'martin.forms.access_settings' => ['tab' => 'martin.forms::lang.permissions.tab', 'label' => 'martin.forms::lang.permissions.access_settings'],
'martin.forms.access_records' => ['tab' => 'martin.forms::lang.permissions.tab', 'label' => 'martin.forms::lang.permissions.access_records'],
'martin.forms.access_exports' => ['tab' => 'martin.forms::lang.permissions.tab', 'label' => 'martin.forms::lang.permissions.access_exports'],
'martin.forms.gdpr_cleanup' => ['tab' => 'martin.forms::lang.permissions.tab', 'label' => 'martin.forms::lang.permissions.gdpr_cleanup'],
];
}
public function registerComponents() {
return [
'Martin\Forms\Components\GenericForm' => 'genericForm',
'Martin\Forms\Components\UploadForm' => 'uploadForm',
'Martin\Forms\Components\EmptyForm' => 'emptyForm',
];
}
public function registerMailTemplates() {
return [
'martin.forms::mail.notification' => Lang::get('martin.forms::lang.mails.form_notification.description'),
'martin.forms::mail.autoresponse' => Lang::get('martin.forms::lang.mails.form_autoresponse.description'),
];
}
public function register() {
$this->app->resolving('validator', function($validator) {
Validator::extend('recaptcha', 'Martin\Forms\Classes\ReCaptchaValidator@validateReCaptcha');
});
}
public function registerSchedule($schedule) {
$schedule->call(function () {
$records = GDPR::cleanRecords();
})->daily();
}
}
?>

View File

@ -0,0 +1,36 @@
# Magic Forms for OctoberCMS
Create easy (and almost magic) AJAX forms.
## Why Magic Forms?
Almost everyday we do forms for our clients, personal projects, etc
Sometimes we need to add or remove fields, change validations, store data and at some point, this can be boring and repetitive.
So, the objective was to find a way to just put the HTML elements on the page, skip the repetitive task of coding and (with some kind of magic) store this data on a database or send by mail.
## Features
* Create any type of form: contact, feedback, registration, uploads, etc
* Write only HTML
* Don't code forms logic
* Laravel validation
* Custom validation errors
* Use multiple forms on same page
* Store on database
* Export data in CSV
* Access database records from backend
* Send mail notifications to multiple recipients
* Auto-response email on form submit
* reCAPTCHA validation
* Support for Translate plugin
* Inline errors with fields (read documentation for more info)
* AJAX file uploads (BETA, available since v1.3.0)
## Documentation
Checkout our docs at:
> https://skydiver.github.io/october-plugin-forms/

View File

@ -0,0 +1,8 @@
.files-container ul { padding:0; list-style:none; }
.record-table { width:100%; border-collapse:collapse; box-shadow:5px 5px 10px #AAA; }
.record-table td { padding:10px; border:solid 1px #D4D8DA; }
.record-label { width:20%; background-color:#ECF0F1; text-align:right; }
.record-value { width:80%; background-color:#F9F9F9; }
.record-value ul { margin:0; }
.record-metadata { font-size:0.8em; color:#888; font-weight:bold; }

View File

@ -0,0 +1,704 @@
.responsiv-uploader-fileupload:after {
content: "";
display: table;
clear: both;
}
.responsiv-uploader-fileupload .upload-object {
border-radius: 3px;
position: relative;
outline: none;
overflow: hidden;
display: inline-block;
vertical-align: top;
}
.responsiv-uploader-fileupload .upload-object img {
width: 100%;
height: 100%;
}
.responsiv-uploader-fileupload .upload-object .icon-container {
display: table;
opacity: .6;
}
.responsiv-uploader-fileupload .upload-object .icon-container i {
color: #95a5a6;
display: inline-block;
}
.responsiv-uploader-fileupload .upload-object .icon-container div {
display: table-cell;
text-align: center;
vertical-align: middle;
}
.responsiv-uploader-fileupload .upload-object .icon-container.image > div.icon-wrapper {
display: none;
}
.responsiv-uploader-fileupload .upload-object h4 {
font-weight: 600;
font-size: 13px;
color: #2b3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 150%;
margin: 15px 0 5px 0;
padding-right: 0;
transition: padding 0.1s;
position: relative;
}
.responsiv-uploader-fileupload .upload-object h4 a {
position: absolute;
right: 0;
top: 0;
display: none;
font-weight: 400;
}
.responsiv-uploader-fileupload .upload-object p.size,
.responsiv-uploader-fileupload .upload-object p.error {
font-size: 12px;
color: #95a5a6;
}
.responsiv-uploader-fileupload .upload-object p.size strong,
.responsiv-uploader-fileupload .upload-object p.error strong {
font-weight: 400;
}
.responsiv-uploader-fileupload .upload-object p.error {
display: none;
color: #ab2a1c;
}
.responsiv-uploader-fileupload .upload-object .info h4 a,
.responsiv-uploader-fileupload .upload-object .meta a.upload-remove-button {
color: #2b3e50;
display: none;
font-size: 24px;
line-height: 16px;
text-decoration: none;
}
.responsiv-uploader-fileupload .upload-object .icon-container {
position: relative;
}
.responsiv-uploader-fileupload .upload-object .icon-container:after {
background-image: url('../../../../../modules/system/assets/ui/images/loader-transparent.svg');
position: absolute;
content: ' ';
width: 40px;
height: 40px;
left: 50%;
top: 50%;
margin-top: -20px;
margin-left: -20px;
display: block;
background-size: 40px 40px;
background-position: 50% 50%;
animation: spin 1s linear infinite;
}
.responsiv-uploader-fileupload .upload-object.is-success .icon-container {
opacity: 1;
}
.responsiv-uploader-fileupload .upload-object.is-success .icon-container:after {
opacity: 0;
transition: opacity .3s ease;
}
.responsiv-uploader-fileupload .upload-object.is-loading .icon-container {
opacity: .6;
}
.responsiv-uploader-fileupload .upload-object.is-loading .icon-container:after {
opacity: 1;
transition: opacity .3s ease;
}
.responsiv-uploader-fileupload .upload-object.is-success {
cursor: pointer;
}
.responsiv-uploader-fileupload .upload-object.is-success .progress-bar {
opacity: 0;
transition: opacity .3s ease;
}
.responsiv-uploader-fileupload .upload-object.is-success:hover h4 a,
.responsiv-uploader-fileupload .upload-object.is-success:hover .meta .upload-remove-button {
display: block;
}
.responsiv-uploader-fileupload .upload-object.is-error {
cursor: pointer;
}
.responsiv-uploader-fileupload .upload-object.is-error .progress-bar {
opacity: 0;
transition: opacity .3s ease;
}
.responsiv-uploader-fileupload .upload-object.is-error .icon-container {
opacity: 1;
}
.responsiv-uploader-fileupload .upload-object.is-error .icon-container > img,
.responsiv-uploader-fileupload .upload-object.is-error .icon-container > i {
opacity: .5;
}
.responsiv-uploader-fileupload .upload-object.is-error .info h4 {
color: #ab2a1c;
}
.responsiv-uploader-fileupload .upload-object.is-error p.error {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.responsiv-uploader-fileupload .upload-object.is-error .info .upload-remove-button,
.responsiv-uploader-fileupload .upload-object.is-error .meta .upload-remove-button {
display: block;
}
.responsiv-uploader-fileupload.is-preview .upload-button,
.responsiv-uploader-fileupload.is-preview .upload-remove-button {
display: none !important;
}
@media (max-width: 1024px) {
.responsiv-uploader-fileupload .upload-object.is-success h4 a,
.responsiv-uploader-fileupload .upload-object.is-success .meta .upload-remove-button {
display: block !important;
}
}
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
}
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
}
}
@-o-keyframes spin {
0% {
-o-transform: rotate(0deg);
}
100% {
-o-transform: rotate(359deg);
}
}
@-ms-keyframes spin {
0% {
-ms-transform: rotate(0deg);
}
100% {
-ms-transform: rotate(359deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
.responsiv-uploader-fileupload.style-image-multi .upload-button,
.responsiv-uploader-fileupload.style-image-multi .upload-object {
margin: 0 10px 10px 0;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object {
background: #fff;
border: 1px solid #ecf0f1;
width: 260px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .progress-bar {
display: block;
width: 100%;
overflow: hidden;
height: 5px;
background-color: #f5f5f5;
border-radius: 2px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
position: absolute;
bottom: 10px;
left: 0;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .progress-bar .upload-progress {
float: left;
width: 0%;
height: 100%;
line-height: 5px;
color: #ffffff;
background-color: #5fb6f5;
box-shadow: none;
transition: width .6s ease;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .icon-container {
border-right: 1px solid #f6f8f9;
float: left;
display: inline-block;
overflow: hidden;
width: 75px;
height: 75px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .icon-container i {
font-size: 35px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .icon-container.image img {
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
width: auto;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .info {
margin-left: 90px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .info h4 {
padding-right: 15px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .info h4 a {
right: 15px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object .meta {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 15px 0 90px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object.upload-placeholder {
height: 75px;
background-color: transparent;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object.upload-placeholder:after {
opacity: 0;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover {
background: #4da7e8 !important;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover i,
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover p.size,
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover p.error,
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover .upload-remove-button {
color: #ecf0f1;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover h4 {
color: white;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover .icon-container {
border-right-color: #4da7e8 !important;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover.is-error {
background: #ab2a1c !important;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object:hover h4 {
padding-right: 35px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object.is-error h4 {
padding-right: 35px;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object.is-error .info p.size {
display: none;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object.is-error .info p.error {
padding-bottom: 11px;
}
.responsiv-uploader-fileupload.style-image-multi.is-preview .upload-files-container {
margin-left: 0;
}
@media (max-width: 1280px) {
.responsiv-uploader-fileupload.style-image-multi .upload-object {
width: 230px;
}
}
@media (max-width: 1024px) {
.responsiv-uploader-fileupload.style-image-multi .upload-button {
width: 100%;
}
.responsiv-uploader-fileupload.style-image-multi .upload-files-container {
margin-left: 0;
}
.responsiv-uploader-fileupload.style-image-multi .upload-object {
margin-right: 0;
display: block;
width: auto;
}
}
.responsiv-uploader-fileupload.style-image-single.is-populated .upload-button {
display: none;
}
.responsiv-uploader-fileupload.style-image-single .upload-button {
display: block;
float: left;
border: 2px dotted rgba(0, 0, 0, 0.1);
position: relative;
outline: none;
min-height: 100px;
min-width: 100px;
}
.responsiv-uploader-fileupload.style-image-single .upload-button .upload-button-icon {
position: absolute;
width: 22px;
height: 22px;
top: 50%;
left: 50%;
margin-top: -11px;
margin-left: -11px;
}
.responsiv-uploader-fileupload.style-image-single .upload-button .upload-button-icon:before {
content: "+";
text-align: center;
display: block;
font-size: 22px;
height: 22px;
width: 22px;
line-height: 22px;
color: rgba(0, 0, 0, 0.1);
font-weight: 700;
}
.responsiv-uploader-fileupload.style-image-single .upload-button:hover {
border: 2px dotted rgba(0, 0, 0, 0.2);
}
.responsiv-uploader-fileupload.style-image-single .upload-button:hover .upload-button-icon:before {
color: #5cb85c;
color: rgba(0, 0, 0, 0.2);
}
.responsiv-uploader-fileupload.style-image-single .upload-button:focus {
border: 2px solid rgba(0, 0, 0, 0.3);
background: transparent;
}
.responsiv-uploader-fileupload.style-image-single .upload-button:focus .upload-button-icon:before {
color: #5cb85c;
color: rgba(0, 0, 0, 0.2);
}
.responsiv-uploader-fileupload.style-image-single .upload-object {
padding-bottom: 66px;
}
.responsiv-uploader-fileupload.style-image-single .upload-object .icon-container {
border: 1px solid #f6f8f9;
background: rgba(255, 255, 255, 0.5);
}
.responsiv-uploader-fileupload.style-image-single .upload-object .icon-container.image img {
border-radius: 3px;
display: block;
max-width: 100%;
height: auto;
min-height: 100px;
min-width: 100px;
}
.responsiv-uploader-fileupload.style-image-single .upload-object .progress-bar {
display: block;
width: 100%;
overflow: hidden;
height: 5px;
background-color: #f5f5f5;
border-radius: 2px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
position: absolute;
bottom: 10px;
left: 0;
}
.responsiv-uploader-fileupload.style-image-single .upload-object .progress-bar .upload-progress {
float: left;
width: 0%;
height: 100%;
line-height: 5px;
color: #ffffff;
background-color: #5fb6f5;
box-shadow: none;
transition: width .6s ease;
}
.responsiv-uploader-fileupload.style-image-single .upload-object .info {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 66px;
}
.responsiv-uploader-fileupload.style-image-single .upload-object .meta {
position: absolute;
bottom: 65px;
left: 0;
right: 0;
margin: 0 15px;
}
.responsiv-uploader-fileupload.style-image-single .upload-object:hover h4 {
padding-right: 20px;
}
.responsiv-uploader-fileupload.style-image-single .upload-object.is-error h4 {
padding-right: 20px;
}
.responsiv-uploader-fileupload.style-image-single .upload-object.is-error .info p.size {
display: none;
}
.responsiv-uploader-fileupload.style-image-single .upload-object.is-error .info p.error {
padding-bottom: 11px;
}
@media (max-width: 1024px) {
.responsiv-uploader-fileupload.style-image-single .upload-object h4 {
padding-right: 20px !important;
}
}
.responsiv-uploader-fileupload.style-file-multi .upload-button {
margin-bottom: 10px;
}
.responsiv-uploader-fileupload.style-file-multi .upload-files-container {
border: 1px solid #eeeeee;
border-radius: 3px;
border-bottom: none;
display: none;
}
.responsiv-uploader-fileupload.style-file-multi.is-populated .upload-files-container {
display: block;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object {
display: block;
width: 100%;
border-bottom: 1px solid #eeeeee;
padding-left: 10px;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object:nth-child(even) {
background-color: #f5f5f5;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .icon-container {
position: absolute;
top: 0;
left: 5px;
width: 35px;
padding: 11px 7px;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .info {
margin-left: 35px;
margin-right: 15%;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .info h4,
.responsiv-uploader-fileupload.style-file-multi .upload-object .info p {
margin: 0;
padding: 11px 0;
font-size: 12px;
font-weight: normal;
line-height: 150%;
color: #666666;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .info h4 {
padding-right: 15px;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .info h4 a {
padding: 10px 0;
right: 15px;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .info p.size {
position: absolute;
top: 0;
right: 0;
width: 15%;
display: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .info p.error {
color: #ab2a1c;
padding-top: 0;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .progress-bar {
display: block;
width: 100%;
overflow: hidden;
height: 5px;
background-color: #f5f5f5;
border-radius: 2px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
position: absolute;
top: 18px;
left: 0;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .progress-bar .upload-progress {
float: left;
width: 0%;
height: 100%;
line-height: 5px;
color: #ffffff;
background-color: #5fb6f5;
box-shadow: none;
transition: width .6s ease;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .meta {
position: absolute;
top: 0;
right: 0;
margin-right: 15px;
width: 15%;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .meta .upload-remove-button {
position: absolute;
top: -9px;
right: 0;
bottom: auto;
line-height: 150%;
padding: 10px 0;
z-index: 100;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object .icon-container:after {
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
background-size: 20px 20px;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object.is-success .info p.size {
display: block;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover {
background: #4da7e8 !important;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover i,
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover p.size,
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover p.error,
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover .upload-remove-button {
color: #ecf0f1;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover h4 {
color: white;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover .icon-container {
border-right-color: #4da7e8 !important;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover.is-error {
background: #ab2a1c !important;
}
.responsiv-uploader-fileupload.style-file-multi .upload-object:hover h4 {
padding-right: 35px;
}
@media (max-width: 1199px) {
.responsiv-uploader-fileupload.style-file-multi .info {
margin-right: 20% !important;
}
.responsiv-uploader-fileupload.style-file-multi .info p.size {
width: 20% !important;
}
.responsiv-uploader-fileupload.style-file-multi .meta {
width: 20% !important;
}
}
@media (max-width: 991px) {
.responsiv-uploader-fileupload.style-file-multi .upload-object h4 {
padding-right: 35px !important;
}
.responsiv-uploader-fileupload.style-file-multi .info {
margin-right: 25% !important;
}
.responsiv-uploader-fileupload.style-file-multi .info p.size {
width: 25% !important;
padding-right: 35px !important;
}
.responsiv-uploader-fileupload.style-file-multi .meta {
width: 25% !important;
}
}
.responsiv-uploader-fileupload.style-file-single {
background-color: #fff;
border: 1px solid #e0e0e0;
overflow: hidden;
position: relative;
padding-right: 11px;
}
.responsiv-uploader-fileupload.style-file-single .upload-button {
position: absolute;
top: 50%;
margin-top: -44px;
height: 88px;
right: 0;
margin-right: 0;
}
.responsiv-uploader-fileupload.style-file-single .upload-empty-message {
padding: 10px 0 10px 11px;
font-size: 13px;
}
.responsiv-uploader-fileupload.style-file-single.is-populated .upload-button,
.responsiv-uploader-fileupload.style-file-single.is-populated .upload-empty-message {
display: none;
}
.responsiv-uploader-fileupload.style-file-single .upload-object {
display: block;
width: 100%;
padding: 8px 0 10px 0;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .icon-container {
position: absolute;
top: 0;
left: 0;
width: 35px;
padding: 0 5px;
margin: 8px 0 0 7px;
text-align: center;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .info {
margin-left: 54px;
margin-right: 15%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .info h4,
.responsiv-uploader-fileupload.style-file-single .upload-object .info p {
display: inline;
margin: 0;
padding: 0;
font-size: 12px;
line-height: 150%;
color: #666666;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .info p.size {
font-weight: normal;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .info p.size:before {
content: " - ";
}
.responsiv-uploader-fileupload.style-file-single .upload-object .info p.error {
color: #ab2a1c;
padding-top: 0;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .progress-bar {
display: block;
width: 100%;
overflow: hidden;
height: 5px;
background-color: #f5f5f5;
border-radius: 2px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
position: absolute;
top: 50%;
margin-top: -2px;
right: 5px;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .progress-bar .upload-progress {
float: left;
width: 0%;
height: 100%;
line-height: 5px;
color: #ffffff;
background-color: #5fb6f5;
box-shadow: none;
transition: width .6s ease;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .meta {
position: absolute;
top: 50%;
margin-top: -44px;
height: 88px;
right: 0;
width: 15%;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .meta .upload-remove-button {
position: absolute;
top: 50%;
right: 0;
height: 20px;
line-height: 20px;
margin-top: -10px;
margin-right: 10px;
z-index: 100;
}
.responsiv-uploader-fileupload.style-file-single .upload-object .icon-container:after {
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
background-size: 20px 20px;
}
.responsiv-uploader-fileupload.style-file-single .upload-object.is-error .info p.size {
display: none;
}
.responsiv-uploader-fileupload.style-file-single .upload-object.is-error .info p.error:before {
content: " - ";
}

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
viewBox="0 0 64 64"
xml:space="preserve"
x="0px"
y="0px"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="noun_9594.svg"
width="64"
height="64"><metadata
id="metadata10"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs8"><linearGradient
inkscape:collect="always"
id="linearGradient4168"><stop
id="stop4172"
offset="0"
style="stop-color:#f89c3a;stop-opacity:1" /><stop
id="stop4170"
offset="1"
style="stop-color:#fec503;stop-opacity:1" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4168"
id="linearGradient4174"
x1="38.347958"
y1="1.3644062"
x2="36.229313"
y2="99.245766"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.63737476,0,0,0.63737476,7.0795147,0.12478476)" /></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1863"
inkscape:window-height="1056"
id="namedview6"
showgrid="false"
inkscape:zoom="6.675088"
inkscape:cx="34.430274"
inkscape:cy="54.352813"
inkscape:window-x="57"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" /><path
d="m 26.660202,0.31923857 24.57674,0.062718 -15.93437,22.35342543 15.394221,0 -37.000145,41.051255 13.233628,-28.357774 -14.313925,0 z"
style="fill:url(#linearGradient4174);fill-opacity:1;stroke:#fe8f00;stroke-width:0.63737476;stroke-opacity:1"
id="path4"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,7 @@
$(window).on('ajaxInvalidField', function(event, fieldElement, fieldName, errorMsg, isFirst) {
$(fieldElement).closest('.form-group').addClass('has-error');
});
$(document).on('ajaxPromise', '[data-request]', function() {
$(this).closest('form').find('.form-group.has-error').removeClass('has-error');
});

View File

@ -0,0 +1,12 @@
var captchas = [];
var onloadCallback = function() {
jQuery('.g-recaptcha').each(function(index, el) {
captchas[el.id] = grecaptcha.render(el, $(el).data());
});
}
function resetReCaptcha(id) {
var widget = captchas[id];
grecaptcha.reset(widget);
}

View File

@ -0,0 +1,360 @@
/*
* File upload form field control
*
* Data attributes:
* - data-control="fileupload" - enables the file upload plugin
* - data-unique-id="XXX" - an optional identifier for multiple uploaders on the same page, this value will
* appear in the postback variable called X_OCTOBER_FILEUPLOAD
* - data-template - a Dropzone.js template to use for each item
*
* JavaScript API:
* $('div').fileUploader()
*
* Dependancies:
* - Dropzone.js
*/
+function ($) { "use strict";
// FILEUPLOAD CLASS DEFINITION
// ============================
var FileUpload = function (element, options) {
this.$el = $(element)
this.options = options || {}
this.init()
}
FileUpload.prototype.init = function() {
if (this.options.isMulti === null) {
this.options.isMulti = this.$el.hasClass('is-multi')
}
if (this.options.isPreview === null) {
this.options.isPreview = this.$el.hasClass('is-preview')
}
this.$uploadButton = $('.upload-button', this.$el)
this.$filesContainer = $('.upload-files-container', this.$el)
this.uploaderOptions = {}
this.$el.on('click', '.upload-object.is-success', $.proxy(this.onClickSuccessObject, this))
this.$el.on('click', '.upload-object.is-error', $.proxy(this.onClickErrorObject, this))
// Stop here for preview mode
if (this.options.isPreview)
return
this.$el.on('click', '.upload-remove-button', $.proxy(this.onRemoveObject, this))
this.bindUploader()
}
FileUpload.prototype.dispose = function() {
this.$el.off('click', '.upload-object.is-success', $.proxy(this.onClickSuccessObject, this))
this.$el.off('click', '.upload-object.is-error', $.proxy(this.onClickErrorObject, this))
this.$el.off('click', '.upload-remove-button', $.proxy(this.onRemoveObject, this))
this.$el.removeData('oc.fileUpload')
this.$el = null
this.$uploadButton = null
this.$filesContainer = null
this.uploaderOptions = null
// In some cases options could contain callbacks,
// so it's better to clean them up too.
this.options = null
}
//
// Uploading
//
FileUpload.prototype.bindUploader = function() {
this.uploaderOptions = {
url: this.options.url,
paramName: this.options.paramName,
clickable: this.$uploadButton.get(0),
previewsContainer: this.$filesContainer.get(0),
maxFiles: !this.options.isMulti ? 1 : null,
headers: {}
}
if (this.options.fileTypes) {
this.uploaderOptions.acceptedFiles = this.options.fileTypes
}
if (this.options.template) {
this.uploaderOptions.previewTemplate = $(this.options.template).html()
}
if (this.options.uniqueId) {
this.uploaderOptions.headers['X-OCTOBER-FILEUPLOAD'] = this.options.uniqueId
}
this.uploaderOptions.thumbnailWidth = this.options.thumbnailWidth
? this.options.thumbnailWidth : null
this.uploaderOptions.thumbnailHeight = this.options.thumbnailHeight
? this.options.thumbnailHeight : null
this.uploaderOptions.resize = this.onResizeFileInfo
/*
* Add CSRF token to headers
*/
var token = $('meta[name="csrf-token"]').attr('content')
if (token) {
this.uploaderOptions.headers['X-CSRF-TOKEN'] = token
}
this.dropzone = new Dropzone(this.$el.get(0), this.uploaderOptions)
this.dropzone.on('addedfile', $.proxy(this.onUploadAddedFile, this))
this.dropzone.on('sending', $.proxy(this.onUploadSending, this))
this.dropzone.on('success', $.proxy(this.onUploadSuccess, this))
this.dropzone.on('error', $.proxy(this.onUploadError, this))
var _this = this;
this.dropzone.on("queuecomplete", function (file) {
if (this.getUploadingFiles().length === 0 && this.getQueuedFiles().length === 0) {
$.event.trigger({
uploader: _this,
type : "uploadfinished",
message : "Dropzone Files upload finished",
});
}
});
this.dropzone.on("sending", function (file) {
$.event.trigger({
uploader: _this,
type : "uploadstarted",
message : "Dropzone File upload started",
});
});
}
FileUpload.prototype.onResizeFileInfo = function(file) {
var info,
targetWidth,
targetHeight
if (!this.options.thumbnailWidth && !this.options.thumbnailWidth) {
targetWidth = targetHeight = 100
}
else if (this.options.thumbnailWidth) {
targetWidth = this.options.thumbnailWidth
targetHeight = this.options.thumbnailWidth * file.height / file.width
}
else if (this.options.thumbnailHeight) {
targetWidth = this.options.thumbnailHeight * file.height / file.width
targetHeight = this.options.thumbnailHeight
}
// drawImage(image, srcX, srcY, srcWidth, srcHeight, trgX, trgY, trgWidth, trgHeight) takes an image, clips it to
// the rectangle (srcX, srcY, srcWidth, srcHeight), scales it to dimensions (trgWidth, trgHeight), and draws it
// on the canvas at coordinates (trgX, trgY).
info = {
srcX: 0,
srcY: 0,
srcWidth: file.width,
srcHeight: file.height,
trgX: 0,
trgY: 0,
trgWidth: targetWidth,
trgHeight: targetHeight
}
return info
}
FileUpload.prototype.onUploadAddedFile = function(file) {
var $object = $(file.previewElement).data('dzFileObject', file)
// Remove any exisiting objects for single variety
if (!this.options.isMulti) {
this.removeFileFromElement($object.siblings())
}
this.evalIsPopulated()
}
FileUpload.prototype.onUploadSending = function(file, xhr, formData) {
this.addExtraFormData(formData)
}
FileUpload.prototype.onUploadSuccess = function(file, response) {
var $preview = $(file.previewElement),
$img = $('.image img', $preview)
$preview.addClass('is-success')
if (response.id) {
$preview.data('id', response.id)
$preview.data('path', response.path)
$('.upload-remove-button', $preview).data('request-data', { file_id: response.id })
$img.attr('src', response.thumb)
}
/*
* Trigger change event (Compatability with october.form.js)
*/
this.$el.closest('[data-field-name]').trigger('change.oc.formwidget')
}
FileUpload.prototype.onUploadError = function(file, error) {
var $preview = $(file.previewElement)
$preview.addClass('is-error')
}
FileUpload.prototype.addExtraFormData = function(formData) {
if (this.options.extraData) {
$.each(this.options.extraData, function (name, value) {
formData.append(name, value)
})
}
var $form = this.$el.closest('form')
if ($form.length > 0) {
$.each($form.serializeArray(), function (index, field) {
formData.append(field.name, field.value)
})
}
}
FileUpload.prototype.removeFileFromElement = function($element) {
var self = this
$element.each(function() {
var $el = $(this),
obj = $el.data('dzFileObject')
if (obj) {
self.dropzone.removeFile(obj)
}
else {
$el.remove()
}
})
}
//
// User interaction
//
FileUpload.prototype.onRemoveObject = function(ev) {
var self = this,
$object = $(ev.target).closest('.upload-object')
$(ev.target)
.closest('.upload-remove-button')
.one('ajaxPromise', function(){
$object.addClass('is-loading')
})
.one('ajaxDone', function(){
self.removeFileFromElement($object)
self.evalIsPopulated()
})
.request()
ev.stopPropagation()
}
FileUpload.prototype.onClickSuccessObject = function(ev) {
// if ($(ev.target).closest('.meta').length) return
//
// var $target = $(ev.target).closest('.upload-object')
// window.open($target.data('path'))
}
FileUpload.prototype.onClickErrorObject = function(ev) {
var
self = this,
$object = $(ev.target).closest('.upload-object'),
errorMsg = $('[data-dz-errormessage]', $object).text()
alert(errorMsg)
this.removeFileFromElement($object)
self.evalIsPopulated()
}
//
// Helpers
//
FileUpload.prototype.evalIsPopulated = function() {
var isPopulated = !!$('.upload-object', this.$filesContainer).length
this.$el.toggleClass('is-populated', isPopulated)
// Reset maxFiles counter
if (!isPopulated) {
this.dropzone.removeAllFiles()
}
}
FileUpload.DEFAULTS = {
url: window.location,
uniqueId: null,
extraData: {},
paramName: 'file_data',
fileTypes: null,
template: null,
isMulti: null,
isPreview: null,
thumbnailWidth: 120,
thumbnailHeight: 120
}
// FILEUPLOAD PLUGIN DEFINITION
// ============================
var old = $.fn.fileUploader
$.fn.fileUploader = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('oc.fileUpload')
var options = $.extend({}, FileUpload.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data) $this.data('oc.fileUpload', (data = new FileUpload(this, options)))
if (typeof option == 'string') data[option].call($this)
})
}
$.fn.fileUploader.Constructor = FileUpload
// FILEUPLOAD NO CONFLICT
// =================
$.fn.fileUploader.noConflict = function () {
$.fn.fileUpload = old
return this
}
// FILEUPLOAD DATA-API
// ===============
$(document).render(function () {
$('[data-control="fileupload"]').fileUploader()
})
}(window.jQuery);
var uploadDropZones = {};
$(document).on("uploadstarted", function(event) {
var frm = $(event.uploader.dropzone.element).parents('form');
frm.find(':submit').prop('disabled', true);
});
var martin;
$(document).on("uploadfinished", function(event) {
var frm = $(event.uploader.dropzone.element).parents('form');
frm.find(':submit').prop('disabled', false);
var dz = $(event.uploader.dropzone.element).data('unique-id');
uploadDropZones[dz] = event;
});

View File

@ -0,0 +1,354 @@
.uploader-object-active() {
background: @uploader-object-active-bg !important;
i, p.size, p.error, .upload-remove-button {
color: #ecf0f1;
}
h4 {
color: white;
}
.icon-container {
border-right-color: @uploader-object-active-bg !important;
}
&.is-error {
background: @uploader-object-error-bg !important;
}
}
.uploader-progress-bar() {
display: block;
width: 100%;
overflow: hidden;
height: @uploader-progress-bar-height;
background-color: @uploader-progress-bar-bg;
border-radius: 2px;
box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
.upload-progress {
float: left;
width: 0%;
height: 100%;
line-height: @uploader-progress-bar-height;
color: @uploader-progress-bar-color;
background-color: #5fb6f5;
box-shadow: none;
transition: width .6s ease;
}
}
.uploader-block-button() {
display: block;
float: left;
border: 2px dotted rgba(0,0,0,.1);
position: relative;
outline: none;
.upload-button-icon {
position: absolute;
width: 22px;
height: 22px;
top: 50%;
left: 50%;
margin-top: -11px;
margin-left: -11px;
&:before {
content: "+";
text-align: center;
display: block;
font-size: 22px;
height: 22px;
width: 22px;
line-height: 22px;
color: rgba(0,0,0,.1);
font-weight: 700;
}
}
&:hover {
border: 2px dotted rgba(0,0,0,.2);
.upload-button-icon:before {
color: #5cb85c;
color: rgba(0,0,0,.2);
}
}
&:focus {
border: 2px solid rgba(0,0,0,.3);
background: transparent;
.upload-button-icon:before {
color: #5cb85c;
color: rgba(0,0,0,.2);
}
}
}
.uploader-small-loader() {
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
background-size: 20px 20px;
}
.uploader-vertical-align() {
position: absolute;
top: 50%;
margin-top: -44px;
height: 88px;
}
//
// Shared
//
.responsiv-uploader-fileupload {
// Clearfix
&:after {
content: "";
display: table;
clear: both;
}
//
// Uploaded item
//
.upload-object {
border-radius: 3px;
position: relative;
outline: none;
overflow: hidden;
display: inline-block;
vertical-align: top;
img {
width: 100%;
height: 100%;
}
.icon-container {
display: table;
opacity: .6;
i {
color: #95a5a6;
display: inline-block;
}
div {
display: table-cell;
text-align: center;
vertical-align: middle;
}
}
.icon-container.image {
> div.icon-wrapper {
display: none;
}
}
h4 {
font-weight: 600;
font-size: 13px;
color: #2b3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 150%;
margin: 15px 0 5px 0;
padding-right: 0;
transition: padding 0.1s;
position: relative;
a {
position: absolute;
right: 0;
top: 0;
display: none;
font-weight: 400;
}
}
p.size, p.error {
font-size: 12px;
color: #95a5a6;
strong { font-weight: 400; }
}
p.error {
display: none;
color: #ab2a1c;
}
.meta {}
.info h4 a,
.meta a.upload-remove-button {
color: #2b3e50;
display: none;
font-size: 24px;
line-height: 16px;
text-decoration: none;
}
}
//
// Loading State
//
.upload-object {
.icon-container {
position: relative;
}
.icon-container:after {
background-image: url('../../../../../modules/system/assets/ui/images/loader-transparent.svg');
position: absolute;
content: ' ';
width: 40px;
height: 40px;
left: 50%;
top: 50%;
margin-top: -20px;
margin-left: -20px;
display: block;
background-size: 40px 40px;
background-position: 50% 50%;
animation: spin 1s linear infinite;
}
&.is-success {
.icon-container {
opacity: 1;
}
.icon-container:after {
opacity: 0;
transition: opacity .3s ease;
}
}
&.is-loading {
.icon-container {
opacity: .6;
}
.icon-container:after {
opacity: 1;
transition: opacity .3s ease;
}
}
}
//
// Success state
//
.upload-object.is-success {
cursor: pointer;
.progress-bar {
opacity: 0;
transition: opacity .3s ease;
}
&:hover {
h4 a,
.meta .upload-remove-button { display: block; }
}
}
//
// Error State
//
.upload-object.is-error {
cursor: pointer;
.progress-bar {
opacity: 0;
transition: opacity .3s ease;
}
.icon-container {
opacity: 1;
> img, > i {
opacity: .5;
}
}
.info h4 {
color: #ab2a1c;
}
p.error {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info .upload-remove-button,
.meta .upload-remove-button {
display: block;
}
}
//
// Preview mode
//
&.is-preview {
.upload-button,
.upload-remove-button {
display: none !important;
}
}
}
//
// Media
//
@media (max-width: 1024px) {
.responsiv-uploader-fileupload {
.upload-object.is-success {
h4 a,
.meta .upload-remove-button { display: block !important; }
}
}
}
//
// Spin animation
//
@-moz-keyframes spin {
0% { -moz-transform: rotate(0deg); }
100% { -moz-transform: rotate(359deg); }
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(359deg); }
}
@-o-keyframes spin {
0% { -o-transform: rotate(0deg); }
100% { -o-transform: rotate(359deg); }
}
@-ms-keyframes spin {
0% { -ms-transform: rotate(0deg); }
100% { -ms-transform: rotate(359deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(359deg); }
}

View File

@ -0,0 +1,163 @@
//
// Multi File
//
.responsiv-uploader-fileupload.style-file-multi {
.upload-button {
margin-bottom: 10px;
}
.upload-files-container {
border: 1px solid @uploader-list-border-color;
border-radius: 3px;
border-bottom: none;
display: none;
}
&.is-populated .upload-files-container {
display: block;
}
.upload-object {
display: block;
width: 100%;
border-bottom: 1px solid @uploader-list-border-color;
padding-left: 10px;
&:nth-child(even) {
background-color: @uploader-list-accent-bg;
}
.icon-container {
position: absolute;
top: 0;
left: 5px;
width: 35px;
padding: 11px 7px;
}
.info {
margin-left: 35px;
margin-right: 15%;
h4, p {
margin: 0;
padding: 11px 0;
font-size: 12px;
font-weight: normal;
line-height: 150%;
color: #666666;
}
h4 {
padding-right: 15px;
a {
padding: 10px 0;
right: 15px;
}
}
p.size {
position: absolute;
top: 0;
right: 0;
width: 15%;
display: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
p.error {
color: #ab2a1c;
padding-top: 0;
}
}
.progress-bar {
.uploader-progress-bar();
position: absolute;
top: 18px;
left: 0;
}
.meta {
position: absolute;
top: 0;
right: 0;
margin-right: 15px;
width: 15%;
.upload-remove-button {
position: absolute;
top: -9px;
right: 0;
bottom: auto;
line-height: 150%;
padding: 10px 0;
z-index: 100;
}
}
.icon-container:after {
.uploader-small-loader();
}
//
// Success
//
&.is-success {
.info p.size { display: block; }
}
//
// Hover
//
&:hover {
.uploader-object-active();
h4 { padding-right: 35px; }
}
}
}
//
// Media
//
@media (max-width: @screen-md-max) {
.responsiv-uploader-fileupload.style-file-multi {
.info {
margin-right: 20% !important;
p.size {
width: 20% !important;
}
}
.meta {
width: 20% !important;
}
}
}
@media (max-width: @screen-sm-max) {
.responsiv-uploader-fileupload.style-file-multi {
.upload-object {
h4 { padding-right: 35px !important; }
}
.info {
margin-right: 25% !important;
p.size {
width: 25% !important;
padding-right: 35px !important;
}
}
.meta {
width: 25% !important;
}
}
}

View File

@ -0,0 +1,112 @@
//
// Single File
//
.responsiv-uploader-fileupload.style-file-single {
background-color: #fff;
border: 1px solid #e0e0e0;
overflow: hidden;
position: relative;
padding-right: 11px;
.upload-button {
.uploader-vertical-align();
right: 0;
margin-right: 0;
}
.upload-empty-message {
padding: 10px 0 10px 11px;
font-size: 13px;
}
&.is-populated {
.upload-button,
.upload-empty-message {
display: none;
}
}
.upload-object {
display: block;
width: 100%;
padding: 8px 0 10px 0;
.icon-container {
position: absolute;
top: 0;
left: 0;
width: 35px;
padding: 0 5px;
margin: 8px 0 0 7px;
text-align: center;
}
.info {
margin-left: 54px;
margin-right: 15%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
h4, p {
display: inline;
margin: 0;
padding: 0;
font-size: 12px;
line-height: 150%;
color: #666666;
}
p.size {
font-weight: normal;
&:before {
content: " - ";
}
}
p.error {
color: #ab2a1c;
padding-top: 0;
}
}
.progress-bar {
.uploader-progress-bar();
position: absolute;
top: 50%;
margin-top: -2px;
right: 5px;
}
.meta {
.uploader-vertical-align();
right: 0;
width: 15%;
.upload-remove-button {
position: absolute;
top: 50%;
right: 0;
height: 20px;
line-height: 20px;
margin-top: -10px;
margin-right: 10px;
z-index: 100;
}
}
.icon-container:after {
.uploader-small-loader();
}
&.is-error {
.info {
p.size { display: none; }
p.error:before {
content: " - ";
}
}
}
}
}

View File

@ -0,0 +1,117 @@
//
// Multi Image
//
.responsiv-uploader-fileupload.style-image-multi {
.upload-button,
.upload-object {
margin: 0 10px 10px 0;
}
.upload-object {
background: #fff;
border: 1px solid #ecf0f1;
width: 260px;
.progress-bar {
.uploader-progress-bar();
position: absolute;
bottom: 10px;
left: 0;
}
.icon-container {
border-right: 1px solid #f6f8f9;
float: left;
display: inline-block;
overflow: hidden;
width: 75px;
height: 75px;
i {
font-size: 35px;
}
&.image img {
border-bottom-left-radius: 3px;
border-top-left-radius: 3px;
width: auto;
}
}
.info {
margin-left: 90px;
h4 {
padding-right: 15px;
a {
right: 15px;
}
}
}
.meta {
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0 15px 0 90px;
}
&.upload-placeholder {
height: 75px;
background-color: transparent;
&:after { opacity: 0; }
}
&:hover {
.uploader-object-active();
h4 { padding-right: 35px; }
}
&.is-error {
h4 { padding-right: 35px; }
.info {
p.size { display: none; }
p.error { padding-bottom: 11px; }
}
}
}
&.is-preview {
.upload-files-container {
margin-left: 0;
}
}
}
//
// Media
//
@media (max-width: 1280px) {
.responsiv-uploader-fileupload.style-image-multi {
.upload-object {
width: 230px;
}
}
}
@media (max-width: 1024px) {
.responsiv-uploader-fileupload.style-image-multi {
.upload-button {
width: 100%;
}
.upload-files-container {
margin-left: 0;
}
.upload-object {
margin-right: 0;
display: block;
width: auto;
}
}
}

View File

@ -0,0 +1,91 @@
//
// Single Image
//
.responsiv-uploader-fileupload.style-image-single {
&.is-populated {
.upload-button {
display: none;
}
}
.upload-button {
.uploader-block-button();
min-height: 100px;
min-width: 100px;
}
.upload-object {
padding-bottom: 66px;
.icon-container {
border: 1px solid #f6f8f9;
background: rgba(255,255,255,.5);
&.image img {
border-radius: 3px;
// Img responsive
display: block;
max-width: 100%;
height: auto;
// This is needed when the image is very large and
// being processed by dropzone on the client-side
// the image has no height or width.
min-height: 100px;
min-width: 100px;
}
}
.progress-bar {
.uploader-progress-bar();
position: absolute;
bottom: 10px;
left: 0;
}
.info {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 66px;
}
.meta {
position: absolute;
bottom: 65px;
left: 0;
right: 0;
margin: 0 15px;
}
&:hover {
h4 { padding-right: 20px; }
}
&.is-error {
h4 { padding-right: 20px; }
.info {
p.size { display: none; }
p.error { padding-bottom: 11px; }
}
}
}
}
//
// Media
//
@media (max-width: 1024px) {
.responsiv-uploader-fileupload.style-image-single {
.upload-object {
h4 { padding-right: 20px !important; }
}
}
}

View File

@ -0,0 +1,19 @@
@uploader-progress-bar-height: 5px;
@uploader-progress-bar-color: #fff;
@uploader-progress-bar-bg: #f5f5f5;
@uploader-inactive-icon: #808b93;
@uploader-object-active-bg: #4da7e8;
@uploader-object-error-bg: #ab2a1c;
@uploader-list-accent-bg: #f5f5f5;
@uploader-list-border-color: #eeeeee;
@uploader-inline-button-color: #333333;
@screen-xs-max: 767px;
@screen-sm-max: 991px;
@screen-md-max: 1199px;
@import "uploader.base.less";
@import "uploader.imagemulti.less";
@import "uploader.imagesingle.less";
@import "uploader.filemulti.less";
@import "uploader.filesingle.less";

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
<?php
namespace Martin\Forms\Classes;
use Backend, BackendAuth;
class BackendHelpers {
/**
* Return a Backend URL based on a matrix of URLS and permissions
*
* @param array $urls Matrix of permissions and URLs
* @param string $default Default URL
*
* @return string
*/
public static function getBackendURL(array $urls, string $default) :string {
$user = BackendAuth::getUser();
foreach ($urls as $permission => $URL) {
if ($user->hasAccess($permission)) {
return Backend::url($URL);
}
}
return Backend::url($urls[$default]);
}
/**
* Check if Translator plugin is installed
*
* @return boolean
*/
public static function isTranslatePlugin() :bool {
return class_exists('\RainLab\Translate\Classes\Translator') && class_exists('\RainLab\Translate\Models\Message');
}
/**
* Render an array|object as HTML list (UL > LI)
*
* @param mixed $data List items
*
* @return string
*/
public static function array2ul($data) :string {
$return = '';
foreach ($data as $index => $item) {
if (!is_string($item)) {
$return .= '<li>' . htmlspecialchars($index, ENT_QUOTES) . '<ul>' . self::array2ul($item) . "</ul></li>";
} else {
$return .= '<li>';
if (is_object($data)) {
$return .= htmlspecialchars($index, ENT_QUOTES) . ' - ';
}
$return .= htmlspecialchars($item, ENT_QUOTES) .'</li>';
}
}
return $return;
}
/**
* Anonymize an IPv4 address
* (credits: https://github.com/geertw/php-ip-anonymizer)
*
* @param string $address IPv4 address
*
* @return string Anonymized address
*/
public static function anonymizeIPv4(string $address) :string {
return inet_ntop(inet_pton($address) & inet_pton("255.255.255.0"));
}
/**
* Extract string from curly braces
*
* @param string $pattern Pattern to replace
* @param string $replacement Replacement string
* @param string $subject Strings to replace
*
* @return string
*/
public static function replaceToken(string $pattern, string $replacement = null, string $subject) :string {
$pattern = '/{{\s*('.$pattern.')\s*}}/';
return preg_replace($pattern, $replacement, $subject);
}
}
?>

View File

@ -0,0 +1,37 @@
<?php
namespace Martin\Forms\Classes;
use Flash;
use Request;
use Martin\Forms\Models\Record;
use Martin\Forms\Models\Settings;
use Carbon\Carbon;
use October\Rain\Exception\ApplicationException;
use October\Rain\Exception\ValidationException;
class GDPR {
public static function cleanRecords() {
$gdpr_enable = Settings::get('gdpr_enable', false);
$gdpr_days = Settings::get('gdpr_days' , false);
if (!$gdpr_enable) {
Flash::error(e(trans('martin.forms::lang.classes.GDPR.alert_gdpr_disabled')));
return;
}
if ($gdpr_enable && is_numeric($gdpr_days)) {
$days = Carbon::now()->subDays($gdpr_days);
$rows = Record::whereDate('created_at', '<', $days)->forceDelete();
return $rows;
}
Flash::error(e(trans('martin.forms::lang.classes.GDPR.alert_invalid_gdpr')));
}
}
?>

View File

@ -0,0 +1,308 @@
<?php
namespace Martin\Forms\Classes;
use Lang;
use Config;
use Request;
use Session;
use Redirect;
use Validator;
use AjaxException;
use Cms\Classes\ComponentBase;
use Martin\Forms\Models\Record;
use Martin\Forms\Models\Settings;
use Martin\Forms\Classes\SendMail;
use Illuminate\Support\Facades\Event;
use Martin\Forms\Classes\BackendHelpers;
use October\Rain\Exception\ValidationException;
abstract class MagicForm extends ComponentBase
{
use \Martin\Forms\Classes\ReCaptcha;
use \Martin\Forms\Classes\SharedProperties;
public function onRun() {
$this->page['recaptcha_enabled'] = $this->isReCaptchaEnabled();
$this->page['recaptcha_misconfigured'] = $this->isReCaptchaMisconfigured();
if ($this->isReCaptchaEnabled()) {
$this->loadReCaptcha();
}
if ($this->isReCaptchaMisconfigured()) {
$this->page['recaptcha_warn'] = Lang::get('martin.forms::lang.components.shared.recaptcha_warn');
}
if ($this->property('inline_errors') == 'display') {
$this->addJs('assets/js/inline-errors.js');
}
}
public function settings() {
return [
'recaptcha_site_key' => Settings::get('recaptcha_site_key'),
'recaptcha_secret_key' => Settings::get('recaptcha_secret_key'),
];
}
public function onFormSubmit() {
// FLASH PARTIAL
$flash_partial = $this->property('messages_partial', '@flash.htm');
// CSRF CHECK
if (Config::get('cms.enableCsrfProtection') && (Session::token() != post('_token'))) {
throw new AjaxException(['#' . $this->alias . '_forms_flash' => $this->renderPartial($flash_partial, [
'status' => 'error',
'type' => 'danger',
'content' => Lang::get('martin.forms::lang.components.shared.csrf_error'),
])]);
}
// LOAD TRANSLATOR PLUGIN
if (BackendHelpers::isTranslatePlugin()) {
$translator = \RainLab\Translate\Classes\Translator::instance();
$translator->loadLocaleFromSession();
$locale = $translator->getLocale();
\RainLab\Translate\Models\Message::setContext($locale);
}
// FILTER ALLOWED FIELDS
$allow = $this->property('allowed_fields');
if (is_array($allow) && !empty($allow)) {
foreach ($allow as $field) {
$post[$field] = post($field);
}
if ($this->isReCaptchaEnabled()) {
$post['g-recaptcha-response'] = post('g-recaptcha-response');
}
} else {
$post = post();
}
// SANITIZE FORM DATA
if ($this->property('sanitize_data') == 'htmlspecialchars') {
$post = $this->array_map_recursive(function ($value) {
return htmlspecialchars($value, ENT_QUOTES);
}, $post);
}
// VALIDATION PARAMETERS
$rules = (array)$this->property('rules');
$msgs = (array)$this->property('rules_messages');
$custom_attributes = (array)$this->property('custom_attributes');
// TRANSLATE CUSTOM ERROR MESSAGES
if (BackendHelpers::isTranslatePlugin()) {
foreach ($msgs as $rule => $msg) {
$msgs[$rule] = \RainLab\Translate\Models\Message::trans($msg);
}
}
// ADD reCAPTCHA VALIDATION
if ($this->isReCaptchaEnabled() && $this->property('recaptcha_size') != 'invisible') {
$rules['g-recaptcha-response'] = 'required';
}
// DO FORM VALIDATION
$validator = Validator::make($post, $rules, $msgs, $custom_attributes);
// NICE reCAPTCHA FIELD NAME
if ($this->isReCaptchaEnabled()) {
$fields_names = ['g-recaptcha-response' => 'reCAPTCHA'];
$validator->setAttributeNames(array_merge($fields_names, $custom_attributes));
}
// VALIDATE ALL + CAPTCHA EXISTS
if ($validator->fails()) {
// GET DEFAULT ERROR MESSAGE
$message = $this->property('messages_errors');
// LOOK FOR TRANSLATION
if (BackendHelpers::isTranslatePlugin()) {
$message = \RainLab\Translate\Models\Message::trans($message);
}
// THROW ERRORS
if ($this->property('inline_errors') == 'display') {
throw new ValidationException($validator);
} else {
throw new AjaxException($this->_exceptionResponse($validator, [
'status' => 'error',
'type' => 'danger',
'title' => $message,
'list' => $validator->messages()->all(),
'errors' => json_encode($validator->messages()->messages()),
'jscript' => $this->property('js_on_error'),
]));
}
}
// IF FIRST VALIDATION IS OK, VALIDATE CAPTCHA vs GOOGLE
// (this prevents to resolve captcha after every form error)
if ($this->isReCaptchaEnabled()) {
// PREPARE RECAPTCHA VALIDATION
$rules = ['g-recaptcha-response' => 'recaptcha'];
$err_msg = ['g-recaptcha-response.recaptcha' => Lang::get('martin.forms::lang.validation.recaptcha_error')];
// DO SECOND VALIDATION
$validator = Validator::make($post, $rules, $err_msg);
// VALIDATE ALL + CAPTCHA EXISTS
if ($validator->fails()) {
// THROW ERRORS
if ($this->property('inline_errors') == 'display') {
throw new ValidationException($validator);
} else {
throw new AjaxException($this->_exceptionResponse($validator, [
'status' => 'error',
'type' => 'danger',
'content' => Lang::get('martin.forms::lang.validation.recaptcha_error'),
'errors' => json_encode($validator->messages()->messages()),
'jscript' => $this->property('js_on_error'),
]));
}
}
}
// REMOVE EXTRA FIELDS FROM STORED DATA
unset($post['_token'], $post['g-recaptcha-response'], $post['_session_key'], $post['_uploader']);
// FIRE BEFORE SAVE EVENT
Event::fire('martin.forms.beforeSaveRecord', [&$post, $this]);
if (count($custom_attributes)) {
$post = collect($post)->mapWithKeys(function ($val, $key) use ($custom_attributes) {
return [array_get($custom_attributes, $key, $key) => $val];
})->all();
}
$record = new Record;
$record->ip = $this->getIP();
$record->created_at = date('Y-m-d H:i:s');
// SAVE RECORD TO DATABASE
if (! $this->property('skip_database')) {
$record->form_data = json_encode($post, JSON_UNESCAPED_UNICODE);
if ($this->property('group') != '') {
$record->group = $this->property('group');
}
$record->save(null, post('_session_key'));
}
// SEND NOTIFICATION EMAIL
if ($this->property('mail_enabled')) {
SendMail::sendNotification($this->getProperties(), $post, $record, $record->files);
}
// SEND AUTORESPONSE EMAIL
if ($this->property('mail_resp_enabled')) {
SendMail::sendAutoResponse($this->getProperties(), $post, $record);
}
// FIRE AFTER SAVE EVENT
Event::fire('martin.forms.afterSaveRecord', [&$post, $this, $record]);
// CHECK FOR REDIRECT
if ($this->property('redirect')) {
return Redirect::to($this->property('redirect'));
}
// GET DEFAULT SUCCESS MESSAGE
$message = $this->property('messages_success');
// LOOK FOR TRANSLATION
if (BackendHelpers::isTranslatePlugin()) {
$message = \RainLab\Translate\Models\Message::trans($message);
}
// DISPLAY SUCCESS MESSAGE
return ['#' . $this->alias . '_forms_flash' => $this->renderPartial($flash_partial, [
'status' => 'success',
'type' => 'success',
'content' => $message,
'jscript' => $this->prepareJavaScript(),
])];
}
private function _exceptionResponse($validator, $params) {
// FLASH PARTIAL
$flash_partial = $this->property('messages_partial', '@flash.htm');
// EXCEPTION RESPONSE
$response = ['#' . $this->alias . '_forms_flash' => $this->renderPartial($flash_partial, $params)];
// INCLUDE ERROR FIELDS IF REQUIRED
if ($this->property('inline_errors') != 'disabled') {
$response['error_fields'] = $validator->messages();
}
return $response;
}
private function prepareJavaScript()
{
$code = false;
/* SUCCESS JS */
if ($this->property('js_on_success') != '') {
$code .= $this->property('js_on_success');
}
/* RECAPTCHA JS */
if ($this->isReCaptchaEnabled()) {
$code .= $this->renderPartial('@js/recaptcha.htm');
}
/* RESET FORM JS */
if ($this->property('reset_form')) {
$params = ['id' => '#' . $this->alias . '_forms_flash'];
$code .= $this->renderPartial('@js/reset-form.htm', $params);
}
/* RESET UPLOAD FORM */
if ($this->property('reset_form') && $this->property('uploader_enable')) {
$params = ['id' => $this->alias];
$code .= $this->renderPartial('@js/reset-uploader.htm', $params);
}
return $code;
}
private function getIP()
{
if ($this->property('anonymize_ip') == 'full') {
return '(Not stored)';
}
$ip = Request::getClientIp();
if ($this->property('anonymize_ip') == 'partial') {
return BackendHelpers::anonymizeIPv4($ip);
}
return $ip;
}
private function array_map_recursive($callback, $array)
{
$func = function ($item) use (&$func, &$callback) {
return is_array($item) ? array_map($func, $item) : call_user_func($callback, $item);
};
return array_map($func, $array);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Martin\Forms\Classes;
use Session;
use Martin\Forms\Classes\BackendHelpers;
use Martin\Forms\Models\Settings;
use RainLab\Translate\Classes\Translator;
trait ReCaptcha {
/**
* @var RainLab\Translate\Classes\Translator Translator object.
*/
protected $translator;
/**
* @var string The active locale code.
*/
public $activeLocale;
public function init() {
if (BackendHelpers::isTranslatePlugin()) {
$this->translator = Translator::instance();
}
}
private function isReCaptchaEnabled() {
return ($this->property('recaptcha_enabled') && Settings::get('recaptcha_site_key') != '' && Settings::get('recaptcha_secret_key') != '');
}
private function isReCaptchaMisconfigured() {
return ($this->property('recaptcha_enabled') && (Settings::get('recaptcha_site_key') == '' || Settings::get('recaptcha_secret_key') == ''));
}
private function getReCaptchaLang($lang='') {
if (BackendHelpers::isTranslatePlugin()) {
$lang = '&hl=' . $this->activeLocale = $this->translator->getLocale();
} else {
$lang = '&hl=' . $this->activeLocale = app()->getLocale();
}
return $lang;
}
private function loadReCaptcha() {
$this->addJs('https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit'.$this->getReCaptchaLang(), ['async', 'defer']);
$this->addJs('assets/js/recaptcha.js');
}
}
?>

View File

@ -0,0 +1,21 @@
<?php
namespace Martin\Forms\Classes;
use Request;
use Martin\Forms\Models\Settings;
class ReCaptchaValidator {
public function validateReCaptcha($attribute, $value, $parameters) {
$secret_key = Settings::get('recaptcha_secret_key');
$recaptcha = post('g-recaptcha-response');
$ip = Request::getClientIp();
$URL = "https://www.google.com/recaptcha/api/siteverify?secret=$secret_key&response=$recaptcha&remoteip=$ip";
$response = json_decode(file_get_contents($URL), true);
return ($response['success'] == true);
}
}
?>

View File

@ -0,0 +1,146 @@
<?php
namespace Martin\Forms\Classes;
use Mail;
use System\Models\MailTemplate;
use Martin\Forms\Classes\BackendHelpers;
class SendMail {
public static function sendNotification($properties, $post, $record, $files) {
// CHECK IF THERE IS AT LEAST ONE MAIL ADDRESS
if (!isset($properties['mail_recipients'])) {
$properties['mail_recipients'] = false;
}
// CHECK IF THERE IS AT LEAST ONE MAIL ADDRESS
if (!isset($properties['mail_bcc'])) {
$properties['mail_bcc'] = false;
}
if (is_array($properties['mail_recipients']) || is_array($properties['mail_bcc'])) {
// CUSTOM TEMPLATE
$template = isset($properties['mail_template']) && $properties['mail_template'] != '' && MailTemplate::findOrMakeTemplate($properties['mail_template']) ? $properties['mail_template'] : 'martin.forms::mail.notification';
$data = [
'id' => $record->id,
'data' => $post,
'ip' => $record->ip,
'date' => $record->created_at
];
// CHECK FOR CUSTOM SUBJECT
if (isset($properties['mail_subject'])) {
// set date format
$dateFormat = $properties['emails_date_format'] ?? 'Y-m-d';
// REPLACE RECORD TOKENS IN SUBJECT
$properties['mail_subject'] = BackendHelpers::replaceToken('record.id', $data['id'], $properties['mail_subject']);
$properties['mail_subject'] = BackendHelpers::replaceToken('record.ip', $data['ip'], $properties['mail_subject']);
$properties['mail_subject'] = BackendHelpers::replaceToken('record.date', date($dateFormat), $properties['mail_subject']);
// REPLACE FORM FIELDS TOKENS IN SUBJECT
foreach ($data['data'] as $key => $value) {
if (!is_array($value)) {
$properties['mail_subject'] = BackendHelpers::replaceToken('form.'.$key, $value, $properties['mail_subject']);
}
}
// SET CUSTOM SUBJECT
$data['subject'] = $properties['mail_subject'];
}
// SEND NOTIFICATION EMAIL
Mail::sendTo($properties['mail_recipients'], $template, $data, function ($message) use ($properties, $post, $files) {
// SEND BLIND CARBON COPY
if (isset($properties['mail_bcc']) && is_array($properties['mail_bcc'])) {
$message->bcc($properties['mail_bcc']);
}
// USE CUSTOM SUBJECT
if (isset($properties['mail_subject'])) {
$message->subject($properties['mail_subject']);
}
// ADD REPLY TO ADDRESS
if (isset($properties['mail_replyto']) && isset($post[$properties['mail_replyto']])) {
$message->replyTo($post[$properties['mail_replyto']]);
}
// ADD UPLOADS
if (isset($properties['mail_uploads']) && $properties['mail_uploads'] && !empty($files)) {
foreach ($files as $file) {
$message->attach($file->getLocalPath(), ['as' => $file->getFilename()]);
}
}
});
}
}
public static function sendAutoResponse($properties, $post, $record) {
$data = [
'id' => $record->id,
'data' => $post,
'ip' => $record->ip,
'date' => $record->created_at
];
// CHECK FOR CUSTOM SUBJECT
if (isset($properties['mail_resp_subject'])) {
// set date format
$dateFormat = $properties['emails_date_format'] ?? 'Y-m-d';
// REPLACE RECORD TOKENS IN SUBJECT
$properties['mail_resp_subject'] = BackendHelpers::replaceToken('record.id', $data['id'], $properties['mail_resp_subject']);
$properties['mail_resp_subject'] = BackendHelpers::replaceToken('record.ip', $data['ip'], $properties['mail_resp_subject']);
$properties['mail_resp_subject'] = BackendHelpers::replaceToken('record.date', date($dateFormat), $properties['mail_resp_subject']);
// REPLACE FORM FIELDS TOKENS IN SUBJECT
foreach ($data['data'] as $key => $value) {
if (!is_array($value)) {
$properties['mail_resp_subject'] = BackendHelpers::replaceToken('form.'.$key, $value, $properties['mail_resp_subject']);
}
}
}
$response = isset($properties['mail_resp_field']) ? $properties['mail_resp_field'] : null;
$to = isset($post[$response]) ? $post[$response] : null;
$from = isset($properties['mail_resp_from']) ? $properties['mail_resp_from'] : null;
$subject = isset($properties['mail_resp_subject']) ? $properties['mail_resp_subject'] : null;
if (filter_var($to, FILTER_VALIDATE_EMAIL) && filter_var($from, FILTER_VALIDATE_EMAIL)) {
// CUSTOM TEMPLATE
$template = isset($properties['mail_resp_template']) && $properties['mail_resp_template'] != '' && MailTemplate::findOrMakeTemplate($properties['mail_resp_template']) ? $properties['mail_resp_template'] : 'martin.forms::mail.autoresponse';
Mail::sendTo($to, $template, [
'id' => $record->id,
'data' => $post,
'ip' => $record->ip,
'date' => $record->created_at
], function ($message) use ($from, $subject) {
$message->from($from);
if (isset($subject)) {
$message->subject($subject);
}
}
);
}
}
}
?>

View File

@ -0,0 +1,259 @@
<?php
namespace Martin\Forms\Classes;
use Lang;
trait SharedProperties {
public function defineProperties() {
return [
'group' => [
'title' => 'martin.forms::lang.components.shared.group.title',
'description' => 'martin.forms::lang.components.shared.group.description',
'type' => 'string',
'showExternalParam' => false,
],
'rules' => [
'title' => 'martin.forms::lang.components.shared.rules.title',
'description' => 'martin.forms::lang.components.shared.rules.description',
'type' => 'dictionary',
'group' => 'martin.forms::lang.components.shared.group_validation',
'showExternalParam' => false,
],
'rules_messages' => [
'title' => 'martin.forms::lang.components.shared.rules_messages.title',
'description' => 'martin.forms::lang.components.shared.rules_messages.description',
'type' => 'dictionary',
'group' => 'martin.forms::lang.components.shared.group_validation',
'showExternalParam' => false,
],
'custom_attributes' => [
'title' => 'martin.forms::lang.components.shared.custom_attributes.title',
'description' => 'martin.forms::lang.components.shared.custom_attributes.description',
'type' => 'dictionary',
'group' => 'martin.forms::lang.components.shared.group_validation',
'showExternalParam' => false,
],
'messages_success' => [
'title' => 'martin.forms::lang.components.shared.messages_success.title',
'description' => 'martin.forms::lang.components.shared.messages_success.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_messages',
'default' => Lang::get('martin.forms::lang.components.shared.messages_success.default'),
'showExternalParam' => false,
'validation' => ['required' => ['message' => Lang::get('martin.forms::lang.components.shared.validation_req')]]
],
'messages_errors' => [
'title' => 'martin.forms::lang.components.shared.messages_errors.title',
'description' => 'martin.forms::lang.components.shared.messages_errors.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_messages',
'default' => Lang::get('martin.forms::lang.components.shared.messages_errors.default'),
'showExternalParam' => false,
'validation' => ['required' => ['message' => Lang::get('martin.forms::lang.components.shared.validation_req')]]
],
'messages_partial' => [
'title' => 'martin.forms::lang.components.shared.messages_partial.title',
'description' => 'martin.forms::lang.components.shared.messages_partial.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_messages',
'showExternalParam' => false
],
'mail_enabled' => [
'title' => 'martin.forms::lang.components.shared.mail_enabled.title',
'description' => 'martin.forms::lang.components.shared.mail_enabled.description',
'type' => 'checkbox',
'group' => 'martin.forms::lang.components.shared.group_mail',
'showExternalParam' => false
],
'mail_subject' => [
'title' => 'martin.forms::lang.components.shared.mail_subject.title',
'description' => 'martin.forms::lang.components.shared.mail_subject.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_mail',
'showExternalParam' => false
],
'mail_recipients' => [
'title' => 'martin.forms::lang.components.shared.mail_recipients.title',
'description' => 'martin.forms::lang.components.shared.mail_recipients.description',
'type' => 'stringList',
'group' => 'martin.forms::lang.components.shared.group_mail',
'showExternalParam' => false
],
'mail_bcc' => [
'title' => 'martin.forms::lang.components.shared.mail_bcc.title',
'description' => 'martin.forms::lang.components.shared.mail_bcc.description',
'type' => 'stringList',
'group' => 'martin.forms::lang.components.shared.group_mail',
'showExternalParam' => false
],
'mail_replyto' => [
'title' => 'martin.forms::lang.components.shared.mail_replyto.title',
'description' => 'martin.forms::lang.components.shared.mail_replyto.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_mail',
'showExternalParam' => false
],
'mail_template' => [
'title' => 'martin.forms::lang.components.shared.mail_template.title',
'description' => 'martin.forms::lang.components.shared.mail_template.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_mail',
'showExternalParam' => false
],
'mail_resp_enabled' => [
'title' => 'martin.forms::lang.components.shared.mail_resp_enabled.title',
'description' => 'martin.forms::lang.components.shared.mail_resp_enabled.description',
'type' => 'checkbox',
'group' => 'martin.forms::lang.components.shared.group_mail_resp',
'showExternalParam' => false
],
'mail_resp_field' => [
'title' => 'martin.forms::lang.components.shared.mail_resp_field.title',
'description' => 'martin.forms::lang.components.shared.mail_resp_field.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_mail_resp',
'showExternalParam' => false
],
'mail_resp_from' => [
'title' => 'martin.forms::lang.components.shared.mail_resp_from.title',
'description' => 'martin.forms::lang.components.shared.mail_resp_from.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_mail_resp',
'showExternalParam' => false
],
'mail_resp_subject' => [
'title' => 'martin.forms::lang.components.shared.mail_resp_subject.title',
'description' => 'martin.forms::lang.components.shared.mail_resp_subject.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_mail_resp',
'showExternalParam' => false
],
'mail_resp_template' => [
'title' => 'martin.forms::lang.components.shared.mail_template.title',
'description' => 'martin.forms::lang.components.shared.mail_template.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_mail_resp',
'showExternalParam' => false
],
'reset_form' => [
'title' => 'martin.forms::lang.components.shared.reset_form.title',
'description' => 'martin.forms::lang.components.shared.reset_form.description',
'type' => 'checkbox',
'group' => 'martin.forms::lang.components.shared.group_settings',
'showExternalParam' => false
],
'redirect' => [
'title' => 'martin.forms::lang.components.shared.redirect.title',
'description' => 'martin.forms::lang.components.shared.redirect.description',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_settings',
'showExternalParam' => false
],
'inline_errors' => [
'title' => 'martin.forms::lang.components.shared.inline_errors.title',
'description' => 'martin.forms::lang.components.shared.inline_errors.description',
'type' => 'dropdown',
'options' => ['disabled' => 'martin.forms::lang.components.shared.inline_errors.disabled', 'display' => 'martin.forms::lang.components.shared.inline_errors.display', 'variable' => 'martin.forms::lang.components.shared.inline_errors.variable'],
'default' => 'disabled',
'group' => 'martin.forms::lang.components.shared.group_settings',
'showExternalParam' => false
],
'js_on_success' => [
'title' => 'martin.forms::lang.components.shared.js_on_success.title',
'description' => 'martin.forms::lang.components.shared.js_on_success.description',
'type' => 'text',
'group' => 'martin.forms::lang.components.shared.group_settings',
'showExternalParam' => false
],
'js_on_error' => [
'title' => 'martin.forms::lang.components.shared.js_on_error.title',
'description' => 'martin.forms::lang.components.shared.js_on_error.description',
'type' => 'text',
'group' => 'martin.forms::lang.components.shared.group_settings',
'showExternalParam' => false
],
'allowed_fields' => [
'title' => 'martin.forms::lang.components.shared.allowed_fields.title',
'description' => 'martin.forms::lang.components.shared.allowed_fields.description',
'type' => 'stringList',
'group' => 'martin.forms::lang.components.shared.group_security',
'showExternalParam' => false
],
'sanitize_data' => [
'title' => 'martin.forms::lang.components.shared.sanitize_data.title',
'description' => 'martin.forms::lang.components.shared.sanitize_data.description',
'type' => 'dropdown',
'options' => ['disabled' => 'martin.forms::lang.components.shared.sanitize_data.disabled', 'htmlspecialchars' => 'martin.forms::lang.components.shared.sanitize_data.htmlspecialchars'],
'default' => 'disabled',
'group' => 'martin.forms::lang.components.shared.group_security',
'showExternalParam' => false
],
'anonymize_ip' => [
'title' => 'martin.forms::lang.components.shared.anonymize_ip.title',
'description' => 'martin.forms::lang.components.shared.anonymize_ip.description',
'type' => 'dropdown',
'options' => ['disabled' => 'martin.forms::lang.components.shared.anonymize_ip.disabled', 'partial' => 'martin.forms::lang.components.shared.anonymize_ip.partial', 'full' => 'martin.forms::lang.components.shared.anonymize_ip.full'],
'default' => 'disabled',
'group' => 'martin.forms::lang.components.shared.group_security',
'showExternalParam' => false
],
'recaptcha_enabled' => [
'title' => 'martin.forms::lang.components.shared.recaptcha_enabled.title',
'description' => 'martin.forms::lang.components.shared.recaptcha_enabled.description',
'type' => 'checkbox',
'group' => 'martin.forms::lang.components.shared.group_recaptcha',
'showExternalParam' => false
],
'recaptcha_theme' => [
'title' => 'martin.forms::lang.components.shared.recaptcha_theme.title',
'description' => 'martin.forms::lang.components.shared.recaptcha_theme.description',
'type' => 'dropdown',
'options' => ['light' => 'martin.forms::lang.components.shared.recaptcha_theme.light', 'dark' => 'martin.forms::lang.components.shared.recaptcha_theme.dark'],
'default' => 'light',
'group' => 'martin.forms::lang.components.shared.group_recaptcha',
'showExternalParam' => false
],
'recaptcha_type' => [
'title' => 'martin.forms::lang.components.shared.recaptcha_type.title',
'description' => 'martin.forms::lang.components.shared.recaptcha_type.description',
'type' => 'dropdown',
'options' => ['image' => 'martin.forms::lang.components.shared.recaptcha_type.image', 'audio' => 'martin.forms::lang.components.shared.recaptcha_type.audio'],
'default' => 'image',
'group' => 'martin.forms::lang.components.shared.group_recaptcha',
'showExternalParam' => false
],
'recaptcha_size' => [
'title' => 'martin.forms::lang.components.shared.recaptcha_size.title',
'description' => 'martin.forms::lang.components.shared.recaptcha_size.description',
'type' => 'dropdown',
'options' => [
'normal' => 'martin.forms::lang.components.shared.recaptcha_size.normal',
'compact' => 'martin.forms::lang.components.shared.recaptcha_size.compact',
'invisible' => 'martin.forms::lang.components.shared.recaptcha_size.invisible',
],
'default' => 'normal',
'group' => 'martin.forms::lang.components.shared.group_recaptcha',
'showExternalParam' => false
],
'skip_database' => [
'title' => 'martin.forms::lang.components.shared.skip_database.title',
'description' => 'martin.forms::lang.components.shared.skip_database.description',
'type' => 'checkbox',
'group' => 'martin.forms::lang.components.shared.group_advanced',
'showExternalParam' => false
],
'emails_date_format' => [
'title' => 'martin.forms::lang.components.shared.emails_date_format.title',
'description' => 'martin.forms::lang.components.shared.emails_date_format.description',
'default' => 'Y-m-d',
'group' => 'martin.forms::lang.components.shared.group_advanced',
'showExternalParam' => false
],
];
}
}
?>

View File

@ -0,0 +1,16 @@
<?php
namespace Martin\Forms\Classes;
use Martin\Forms\Models\Record;
class UnreadRecords {
public static function getTotal() {
$unread = Record::where('unread', 1)->count();
return ($unread > 0) ? $unread : null;
}
}
?>

View File

@ -0,0 +1,18 @@
<?php
namespace Martin\Forms\Components;
use Martin\Forms\Classes\MagicForm;
class EmptyForm extends MagicForm {
public function componentDetails() {
return [
'name' => 'martin.forms::lang.components.empty_form.name',
'description' => 'martin.forms::lang.components.empty_form.description',
];
}
}
?>

View File

@ -0,0 +1,18 @@
<?php
namespace Martin\Forms\Components;
use Martin\Forms\Classes\MagicForm;
class GenericForm extends MagicForm {
public function componentDetails() {
return [
'name' => 'martin.forms::lang.components.generic_form.name',
'description' => 'martin.forms::lang.components.generic_form.description',
];
}
}
?>

View File

@ -0,0 +1,104 @@
<?php
namespace Martin\Forms\Components;
use Lang;
use October\Rain\Filesystem\Definitions;
use Martin\Forms\Classes\MagicForm;
use Martin\Forms\Models\Record;
class UploadForm extends MagicForm {
use \Martin\Forms\Traits\FileUploader;
public function componentDetails() {
return [
'name' => 'martin.forms::lang.components.upload_form.name',
'description' => 'martin.forms::lang.components.upload_form.description',
];
}
public function init() {
parent::init();
$this->fileTypes = $this->processFileTypes(true);
$this->maxSize = $this->property('maxSize');
$this->placeholderText = $this->property('placeholderText');
$this->removeText = $this->property('removeText');
$this->setProperty('deferredBinding', 1);
$this->bindModel('files', new Record);
}
public function onRun() {
parent::onRun();
$this->addCss('assets/css/uploader.css');
$this->addJs('assets/vendor/dropzone/dropzone.js');
$this->addJs('assets/js/uploader.js');
$this->isMulti = $this->property('uploader_multi');
if($result = $this->checkUploadAction()) { return $result; }
}
public function defineProperties() {
$local = [
'mail_uploads' => [
'title' => 'martin.forms::lang.components.shared.mail_uploads.title',
'description' => 'martin.forms::lang.components.shared.mail_uploads.description',
'type' => 'checkbox',
'default' => false,
'group' => 'martin.forms::lang.components.shared.group_mail',
'showExternalParam' => false
],
'uploader_enable' => [
'title' => 'martin.forms::lang.components.shared.uploader_enable.title',
'description' => 'martin.forms::lang.components.shared.uploader_enable.description',
'default' => false,
'type' => 'checkbox',
'group' => 'martin.forms::lang.components.shared.group_uploader',
'showExternalParam' => false,
],
'uploader_multi' => [
'title' => 'martin.forms::lang.components.shared.uploader_multi.title',
'description' => 'martin.forms::lang.components.shared.uploader_multi.description',
'default' => true,
'type' => 'checkbox',
'group' => 'martin.forms::lang.components.shared.group_uploader',
'showExternalParam' => false,
],
'placeholderText' => [
'title' => 'martin.forms::lang.components.shared.uploader_pholder.title',
'description' => 'martin.forms::lang.components.shared.uploader_pholder.description',
'default' => Lang::get('martin.forms::lang.components.shared.uploader_pholder.default'),
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_uploader',
'showExternalParam' => false,
],
'removeText' => [
'title' => 'martin.forms::lang.components.shared.uploader_remFile.title',
'description' => 'martin.forms::lang.components.shared.uploader_remFile.description',
'default' => Lang::get('martin.forms::lang.components.shared.uploader_remFile.default'),
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_uploader',
'showExternalParam' => false,
],
'maxSize' => [
'title' => 'martin.forms::lang.components.shared.uploader_maxsize.title',
'description' => 'martin.forms::lang.components.shared.uploader_maxsize.description',
'default' => '5',
'type' => 'string',
'group' => 'martin.forms::lang.components.shared.group_uploader',
'showExternalParam' => false,
],
'fileTypes' => [
'title' => 'martin.forms::lang.components.shared.uploader_types.title',
'description' => 'martin.forms::lang.components.shared.uploader_types.description',
'default' => Definitions::get('defaultExtensions'),
'type' => 'stringList',
'group' => 'martin.forms::lang.components.shared.group_uploader',
'showExternalParam' => false,
],
];
return array_merge(parent::defineProperties(), $local);
}
}
?>

View File

@ -0,0 +1,12 @@
<h3>Here goes your custom form</h3>
<p>Override HTML by creating a new partial called <strong>default.htm</strong> (more info <a href="https://octobercms.com/docs/cms/components#overriding-partials" target="_blank">here</a>)</p>
<p>You can copy/paste this basic template:</p>
<pre>
&lt;form data-request="&lbrace;&lbrace; __SELF__ }}::onFormSubmit"&gt;
&lbrace;&lbrace; form_token() &rbrace;&rbrace;
&lt;div id="&lbrace;&lbrace; __SELF__ &rbrace;&rbrace;_forms_flash"&gt;&lt;/div&gt;
&lt;!-- YOUR FORM FIELDS --&gt;
&lbrace;% partial '@recaptcha' %}
&lt;!-- SUBMIT BUTTON --&gt;
&lt;/form&gt;
</pre>

View File

@ -0,0 +1,32 @@
<form data-request="{{ __SELF__ }}::onFormSubmit">
{{ form_token() }}
<div id="{{ __SELF__ }}_forms_flash"></div>
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" class="form-control">
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="text" id="email" name="email" class="form-control">
</div>
<div class="form-group">
<label for="subject">Subject:</label>
<input type="text" id="subject" name="subject" class="form-control">
</div>
<div class="form-group">
<textarea id="comments" name="comments" rows="8" cols="80"></textarea>
</div>
<div class="form-group">
{% partial '@recaptcha' %}
</div>
<button id="simpleContactSubmitButton" type="submit" class="btn btn-default">Submit</button>
</form>

View File

@ -0,0 +1,33 @@
{% spaceless %}
<div class="alert alert-{{ type }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{% if title %}
<h4>{{ title }}</h4>
{% endif %}
{% if content %}
<p>{{ content }}</p>
{% endif %}
{% if list %}
<ul>
{% for item in list %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% if jscript %}
<script>
{% if errors %}
var errors = JSON.parse('{{ errors|raw }}');
{% endif %}
{{ jscript|raw }}
</script>
{% endif %}
{% endspaceless %}

View File

@ -0,0 +1 @@
resetReCaptcha('{{ __SELF__ }}');

View File

@ -0,0 +1 @@
$('{{ id }}').parents('form')[0].reset();

View File

@ -0,0 +1,2 @@
var dz = uploadDropZones['{{ id }}'];
if(dz) { dz.uploader.dropzone.removeAllFiles(); dz.uploader.evalIsPopulated(); }

View File

@ -0,0 +1,5 @@
{% if (recaptcha_enabled) %}
<div id="{{ __SELF__ }}" class="g-recaptcha" data-sitekey="{{ __SELF__.settings.recaptcha_site_key }}" data-theme="{{ __SELF__.property('recaptcha_theme') }}" data-type="{{ __SELF__.property('recaptcha_type') }}" data-size="{{ __SELF__.property('recaptcha_size') }}"></div>
{% elseif (recaptcha_misconfigured) %}
<h5>{{ recaptcha_warn }}</h5>
{% endif %}

View File

@ -0,0 +1,51 @@
{{ form_ajax(__SELF__ ~ '::onFormSubmit') }}
<div id="{{ __SELF__ }}_forms_flash"></div>
<div class="form-group">
<h2>Apply for online job</h2>
</div>
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" class="form-control">
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="text" id="email" name="email" class="form-control">
</div>
<div class="form-group">
<label for="position">Position:</label>
<select class="form-control" id="position" name="position">
<option value=""></option>
<option value="Lead">Lead</option>
<option value="Senior">Senior</option>
<option value="Direct">Direct</option>
<option value="Corporate">Corporate</option>
<option value="Dynamic">Dynamic</option>
</select>
</div>
<div class="form-group">
<label for="skills">Skills:</label>
<textarea id="skills" name="skills" class="form-control" rows="8" cols="80" placeholder="Summary of skills"></textarea>
</div>
<hr>
<div class="form-group">
<h4>Upload your resume</h4>
{% partial '@file-upload' %}
</div>
<hr>
<div class="form-group">
{% partial '@recaptcha' %}
</div>
{{ form_submit() }}
{{ form_close() }}

View File

@ -0,0 +1,71 @@
<div
class="responsiv-uploader-fileupload style-file-multi is-multi {{ __SELF__.isPopulated() ? 'is-populated' }}"
data-control="fileupload"
data-template="#uploaderTemplate{{ __SELF__ }}"
data-unique-id="{{ __SELF__ }}"
{% if __SELF__.fileTypes %}data-file-types="{{ __SELF__.fileTypes }}"{% endif %}
>
<!-- Field placeholder -->
<input type="hidden" name="_uploader[{{ __SELF__.attribute }}]" value="" />
<!-- Upload Button -->
<button type="button" class="ui button btn btn-default oc-icon-upload upload-button">
{{ __SELF__.placeholderText }}
</button>
<!-- Existing files -->
<div class="upload-files-container">
{% for file in __SELF__.fileList %}
<div class="upload-object is-success" data-id="{{ file.id }}" data-path="{{ file.pathUrl }}">
<div class="icon-container">
{% if file.isImage %}
<img src="{{ file.thumbUrl }}" alt="" />
{% else %}
<img src="{{ 'plugins/martin/forms/assets/imgs/upload.png'|app }}" alt="" />
{% endif %}
</div>
<div class="info">
<h4 class="filename">
<span data-dz-name>{{ file.title ?: file.file_name }}</span>
</h4>
<p class="size">{{ file.sizeToString }}</p>
</div>
<div class="meta">
<a
href="javascript:;"
class="upload-remove-button"
data-request="{{ __SELF__ ~ '::onRemoveAttachment' }}"
data-request-confirm="{{ __SELF__.removeText }}"
data-request-data="file_id: {{ file.id }}"
>&times;</a>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Template for new files -->
<script type="text/template" id="uploaderTemplate{{ __SELF__ }}">
<div class="upload-object dz-preview dz-file-preview">
<div class="icon-container">
<img data-dz-thumbnail src="{{ 'plugins/martin/forms/assets/imgs/upload.png'|app }}" />
</div>
<div class="info">
<h4 class="filename">
<span data-dz-name></span>
</h4>
<p class="size" data-dz-size></p>
<p class="error"><span data-dz-errormessage></span></p>
</div>
<div class="meta">
<a
href="javascript:;"
class="upload-remove-button"
data-request="{{ __SELF__ ~ '::onRemoveAttachment' }}"
data-request-confirm="{{ __SELF__.removeText }}"
>&times;</a>
<div class="progress-bar"><span class="upload-progress" data-dz-uploadprogress></span></div>
</div>
</div>
</script>

View File

@ -0,0 +1,79 @@
{% set file = __SELF__.singleFile %}
<div
class="responsiv-uploader-fileupload style-file-single {{ __SELF__.isPopulated() ? 'is-populated' }}"
data-control="fileupload"
data-template="#uploaderTemplate{{ __SELF__ }}"
data-unique-id="{{ __SELF__ }}"
{% if __SELF__.fileTypes %}data-file-types="{{ __SELF__.fileTypes }}"{% endif %}
>
<!-- Field placeholder -->
<input type="hidden" name="_uploader[{{ __SELF__.attribute }}]" value="" />
<!-- Upload button -->
<button type="button" class="ui button btn btn-default upload-button">
Browse
</button>
<!-- Existing file -->
<div class="upload-files-container">
{% if file %}
<div class="upload-object is-success" data-id="{{ file.id }}" data-path="{{ file.pathUrl }}">
<div class="icon-container">
{% if file.isImage %}
<img src="{{ file.thumbUrl }}" alt="" />
{% else %}
<img src="{{ 'plugins/martin/forms/assets/imgs/upload.png'|app }}" alt="" />
{% endif %}
</div>
<div class="info">
<h4 class="filename">
<span data-dz-name>{{ file.title ?: file.file_name }}</span>
</h4>
<p class="size">{{ file.sizeToString }}</p>
</div>
<div class="meta">
<a
href="javascript:;"
class="upload-remove-button"
data-request="{{ __SELF__ ~ '::onRemoveAttachment' }}"
data-request-confirm="Are you sure?"
data-request-data="file_id: {{ file.id }}"
>&times;</a>
</div>
</div>
{% endif %}
</div>
<!-- Empty message -->
<div class="upload-empty-message">
<span class="text-muted">{{ __SELF__.placeholderText }}</span>
</div>
</div>
<!-- Template for new file -->
<script type="text/template" id="uploaderTemplate{{ __SELF__ }}">
<div class="upload-object dz-preview dz-file-preview">
<div class="icon-container">
<img data-dz-thumbnail />
</div>
<div class="info">
<h4 class="filename">
<span data-dz-name></span>
</h4>
<p class="size" data-dz-size></p>
<p class="error"><span data-dz-errormessage></span></p>
</div>
<div class="meta">
<a
href="javascript:;"
class="upload-remove-button"
data-request="{{ __SELF__ ~ '::onRemoveAttachment' }}"
data-request-confirm="Are you sure?"
>&times;</a>
<div class="progress-bar"><span class="upload-progress" data-dz-uploadprogress></span></div>
</div>
</div>
</script>

View File

@ -0,0 +1,21 @@
{% if __SELF__.property('uploader_enable') == 0 %}
<div style="color:#a94442; background-color:#f2dede; border:solid 1px #ebccd1; border-radius:4px;">
<h5><strong>Warning</strong></h5>
<h6>Uploads are disabled.</h6>
<h6>You need to explicitly enable this option on the component (this is a security measure).</h6>
</div>
{% else %}
{% if __SELF__.isMulti %}
{% partial __SELF__ ~ '::file-multi' %}
{% else %}
{% partial __SELF__ ~ '::file-single' %}
{% endif %}
{% endif %}

View File

@ -0,0 +1,8 @@
{
"name": "martin/forms-plugin",
"type": "october-plugin",
"description": "None",
"require": {
"composer/installers": "~1.0"
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Martin\Forms\Controllers;
use BackendMenu, Response;
use Backend\Classes\Controller;
use League\Csv\AbstractCsv;
use League\Csv\Writer as CsvWriter;
use SplTempFileObject;
use Martin\Forms\Models\Record;
class Exports extends Controller {
public $requiredPermissions = ['martin.forms.access_exports'];
public $implement = [
'Backend.Behaviors.FormController',
];
public $formConfig = 'config_form.yaml';
public function __construct() {
parent::__construct();
BackendMenu::setContext('Martin.Forms', 'forms', 'exports');
}
public function index() {
$this->pageTitle = e(trans('martin.forms::lang.controllers.exports.title'));
$this->create('frontend');
}
public function csv() {
$records = Record::orderBy('created_at');
// FILTER GROUPS
if (!empty($groups = post('Record.filter_groups'))) {
$records->whereIn('group', $groups);
}
// FILTER DATE
if (!empty($date_after = post('Record.filter_date_after'))) {
$records->whereDate('created_at', '>=', $date_after);
}
// FILTER DATE
if (!empty($date_before = post('Record.filter_date_before'))) {
$records->whereDate('created_at', '<=', $date_before);
}
// FILTER DELETED
if ($deleted = post('Record.options_deleted')) {
$records->withTrashed();
}
// CREATE CSV
$csv = CsvWriter::createFromFileObject(new SplTempFileObject());
// CHANGE DELIMTER
if (post('Record.options_delimiter')) {
$csv->setDelimiter(';');
}
// SET UTF-8 Output
if (post('Record.options_utf')) {
$csv->setOutputBOM(AbstractCsv::BOM_UTF8);
}
// CSV HEADERS
$headers = [];
// METADATA HEADERS
if (post('Record.options_metadata')) {
$meta_headers = [
e(trans('martin.forms::lang.controllers.records.columns.id')),
e(trans('martin.forms::lang.controllers.records.columns.group')),
e(trans('martin.forms::lang.controllers.records.columns.ip')),
e(trans('martin.forms::lang.controllers.records.columns.created_at')),
];
$headers = array_merge($meta_headers, $headers);
}
// ADD STORED FIELDS AS HEADER ROW IN CSV
$filteredRecords = $records->get();
$recordsArray = $filteredRecords->toArray();
$record = $filteredRecords->first();
$headers = array_merge($headers, array_keys($record->form_data_arr));
// ADD HEADERS
$csv->insertOne($headers);
// WRITE CSV LINES
foreach ($records->get()->toArray() as $row) {
$data = (array) json_decode($row['form_data']);
// IF DATA IS ARRAY CONVERT TO JSON STRING
foreach ($data as $field => $value) {
if (is_array($value) || is_object($value)) {
$data[$field] = json_encode($value);
}
}
// ADD METADATA IF NEEDED
if (post('Record.options_metadata')) {
array_unshift($data, $row['id'], $row['group'], $row['ip'], $row['created_at']);
}
$csv->insertOne($data);
}
// RETURN CSV
$csv->output('records.csv');
exit();
}
}
?>

View File

@ -0,0 +1,107 @@
<?php
namespace Martin\Forms\Controllers;
use App, BackendMenu, Lang;
use Backend\Classes\Controller;
use Backend\Facades\Backend;
use Illuminate\Support\Facades\Redirect;
use October\Rain\Support\Facades\Flash;
use Martin\Forms\Classes\GDPR;
use Martin\Forms\Classes\UnreadRecords;
use Martin\Forms\Models\Record;
class Records extends Controller {
public $implement = [
'Backend.Behaviors.ListController'
];
public $listConfig = 'config_list.yaml';
public $requiredPermissions = ['martin.forms.access_records'];
public function __construct() {
parent::__construct();
BackendMenu::setContext('Martin.Forms', 'forms', 'records');
}
public function view($id) {
$record = Record::find($id);
if(!$record) {
Flash::error(e(trans('martin.forms::lang.controllers.records.error')));
return Redirect::to(Backend::url('martin/forms/records'));
}
$record->unread = false;
$record->save();
$this->addCss('/plugins/martin/forms/assets/css/records.css');
$this->pageTitle = e(trans('martin.forms::lang.controllers.records.view_title'));
$this->vars['record'] = $record;
}
public function onDelete() {
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
Record::whereIn('id',$checkedIds)->delete();
}
$counter = UnreadRecords::getTotal();
return [
'counter' => ($counter != null) ? $counter : 0,
'list' => $this->listRefresh()
];
}
public function onDeleteSingle() {
$id = post('id' );
$record = Record::find($id);
if($record) {
$record->delete();
Flash::success(e(trans('martin.forms::lang.controllers.records.deleted')));
} else {
Flash::error(e(trans('martin.forms::lang.controllers.records.error')));
}
return Redirect::to(Backend::url('martin/forms/records'));
}
public function download($record_id, $file_id) {
$record = Record::findOrFail($record_id);
$file = $record->files->find($file_id);
if(!$file) { App::abort(404, Lang::get('backend::lang.import_export.file_not_found_error')); }
return response()->download($file->getLocalPath(), $file->getFilename());
exit();
}
public function listInjectRowClass($record, $definition = null) {
if ($record->unread) {
return 'new';
}
}
public function onReadState() {
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
$unread = (post('state') == 'read') ? 0 : 1;
Record::whereIn('id',$checkedIds)->update(['unread' => $unread]);
}
$counter = UnreadRecords::getTotal();
return [
'counter' => ($counter != null) ? $counter : 0,
'list' => $this->listRefresh()
];
}
public function onGDPRClean() {
if ($this->user->hasPermission(['martin.forms.gdpr_cleanup'])) {
GDPR::cleanRecords();
Flash::success(e(trans('martin.forms::lang.controllers.records.alerts.gdpr_success')));
} else {
Flash::error(e(trans('martin.forms::lang.controllers.records.alerts.gdpr_perms')));
}
$counter = UnreadRecords::getTotal();
return [
'counter' => ($counter != null) ? $counter : 0,
'list' => $this->listRefresh()
];
}
}
?>

View File

@ -0,0 +1,2 @@
form : $/martin/forms/models/export/fields.yaml
modelClass: Martin\Forms\Models\Record

View File

@ -0,0 +1,16 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?php echo Backend::url('martin/forms/records') ?>"><?php echo e(trans('martin.forms::lang.menu.records.label')) ?></a></li>
<li><?php echo e(trans('martin.forms::lang.controllers.exports.breadcrumb')) ?></li>
</ul>
<?php Block::endPut() ?>
<?php echo Form::open(['url' => Backend::url('martin/forms/exports/csv/')]) ?>
<?php echo $this->formRender() ?>
<div class="form-buttons m-t-lg">
<?= Form::submit(e(trans('martin.forms::lang.controllers.exports.title')), ['class' => 'btn btn-primary']) ?>
</div>
<?php echo Form::close() ?>

View File

@ -0,0 +1,13 @@
scopes:
group:
label : martin.forms::lang.controllers.records.columns.group
type : group
modelClass: Martin\Forms\Models\Record
options : filterGroups
conditions: "`group` in (:filtered)"
created_at:
label : martin.forms::lang.controllers.records.columns.created_at
type : daterange
conditions: created_at >= ':after' AND created_at <= ':before'

View File

@ -0,0 +1,19 @@
list : $/martin/forms/models/record/columns.yaml
modelClass : Martin\Forms\Models\Record
title : martin.forms::lang.controllers.records.title
recordUrl : martin/forms/records/view/:id
noRecordsMessage: backend::lang.list.no_records
recordsPerPage : 20
showSetup : true
showSorting : true
showCheckboxes : true
filter : config_filter.yaml
defaultSort:
column : created_at
direction: desc
toolbar:
buttons: partials/list_toolbar
search :
prompt: backend::lang.list.search_prompt

View File

@ -0,0 +1 @@
<?= $this->listRender() ?>

View File

@ -0,0 +1,19 @@
<button type="button"
class="btn btn-<?php echo $type ?> <?php echo $icon ?>"
<?php if (!empty($needs_selected)): ?>
disabled="disabled"
<?php endif ?>
data-request="<?php echo $request ?>"
data-request-confirm="<?php echo $request_confirm; ?>"
data-request-success="<?php echo $request_success; ?>"
<?php if (!empty($onclick)) : ?>
onclick="<?php echo $onclick; ?>"
<?php endif ?>
<?php if (!empty($needs_selected)) : ?>
data-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
<?php endif ?>
data-stripe-load-indicator>
<?php echo $label; ?>
</button>

View File

@ -0,0 +1,76 @@
<div id="records-toolbar" data-control="toolbar">
<?php
echo $this->makePartial('$/martin/forms/controllers/records/partials/_action_button.htm', [
'type' => 'danger',
'icon' => 'oc-icon-trash',
'label' => e(trans('backend::lang.list.delete_selected')),
'onclick' => "$(this).data('request-data', { checked: $('.control-list').listWidget('getChecked') })",
'needs_selected' => true,
'request' => 'onDelete',
'request_confirm' => e(trans('backend::lang.list.delete_selected_confirm')),
'request_success' => "
$('#records-toolbar').find('button').prop('disabled', true);
$.oc.flashMsg({
'text': '" . e(trans('backend::lang.list.delete_selected_success')) . "',
'class': 'success',
'interval': 3
});
$.oc.sideNav.setCounter('forms/records', data.counter);
$('#Lists').html(data.list['#Lists']);
",
]);
?>
<div class="btn-group">
<?php
echo $this->makePartial('$/martin/forms/controllers/records/partials/_action_button.htm', [
'type' => 'default',
'icon' => 'oc-icon-eye-slash',
'label' => e(trans('martin.forms::lang.controllers.records.buttons.unread')),
'onclick' => "$(this).data('request-data', { state: 'unread', checked: $('.control-list').listWidget('getChecked') })",
'needs_selected' => true,
'request' => 'onReadState',
'request_confirm' => '',
'request_success' => "
$('#records-toolbar').find('button').prop('disabled', true);
$.oc.sideNav.setCounter('forms/records', data.counter);
$('#Lists').html(data.list['#Lists']);
",
]);
echo $this->makePartial('$/martin/forms/controllers/records/partials/_action_button.htm', [
'type' => 'default',
'icon' => 'oc-icon-eye',
'label' => e(trans('martin.forms::lang.controllers.records.buttons.read')),
'onclick' => "$(this).data('request-data', { state: 'read', checked: $('.control-list').listWidget('getChecked') })",
'needs_selected' => true,
'request' => 'onReadState',
'request_confirm' => '',
'request_success' => "
$('#records-toolbar').find('button').prop('disabled', true);
$.oc.sideNav.setCounter('forms/records', data.counter);
$('#Lists').html(data.list['#Lists']);
",
]);
?>
</div>
<?php
if(Martin\Forms\Models\Settings::get('gdpr_enable')) {
echo $this->makePartial('$/martin/forms/controllers/records/partials/_action_button.htm', [
'type' => 'danger',
'icon' => 'oc-icon-history',
'label' => e(trans('martin.forms::lang.controllers.records.buttons.gdpr_clean')),
'request' => 'onGDPRClean',
'request_confirm' => e(trans('martin.forms::lang.controllers.records.alerts.gdpr_confirm')),
'request_success' => "
$('#records-toolbar').find('button').blur();
$.oc.sideNav.setCounter('forms/records', data.counter);
$('#Lists').html(data.list['#Lists']);
",
]);
}
?>
</div>

View File

@ -0,0 +1,13 @@
<div class="control-toolbar">
<div class="toolbar-item toolbar-primary">
<div data-control="toolbar">
<a href="<?= Backend::url('martin/forms/records') ?>" type="button" class="btn btn-default oc-icon-arrow-left"><?= e(trans('backend::lang.form.return_to_list')) ?></a>
<button type="button" class="btn btn-danger oc-icon-trash"
data-request="onDeleteSingle"
data-request-confirm="<?= e(trans('backend::lang.form.confirm_delete')) ?>"
data-request-data="id: <?= $record->id ?>"
data-stripe-load-indicator
><?= e(trans('backend::lang.account.delete')) ?></button>
</div>
</div>
</div>

View File

@ -0,0 +1,50 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?php echo Backend::url('martin/forms/records') ?>"><?php echo e(trans('martin.forms::lang.menu.records.label')) ?></a></li>
<li>Record #<?php echo $record->id ?></li>
</ul>
<?php Block::endPut() ?>
<?= $this->makePartial('partials/view_toolbar') ?>
<h2 class="text-center">Record #<?php echo $record->id ?></h2>
<table class="record-table m-t-md">
<?php foreach($record->form_data_arr as $label => $value): ?>
<tr>
<td class="record-label"><?php echo ucwords($label) ?>:</td>
<td class="record-value">
<?php if(is_array($value) || is_object($value)): ?>
<ul>
<?php echo \Martin\Forms\Classes\BackendHelpers::array2ul($value) ?>
</ul>
<?php else: ?>
<?php echo nl2br(htmlspecialchars($value, ENT_QUOTES)); ?>
<?php endif ?>
</td>
</tr>
<?php endforeach ?>
<?php if(count($record->files) > 0): ?>
<tr>
<td class="record-label">Attached Files:</td>
<td class="record-value files-container">
<ul>
<?php foreach($record->files as $file): ?>
<li>
<a href="<?php echo Backend::url('martin/forms/records/download', [$record->id, $file->id]) ?>">
<?php echo $file->file_name ?>
(<?php echo $file->sizeToString() ?>)
</a>
</li>
<?php endforeach ?>
</ul>
</td>
</tr>
<?php endif ?>
</table>
<div class="record-metadata m-t-md m-r-md text-right">
<p class="m-a-0"><?php echo e(trans('martin.forms::lang.controllers.records.columns.group')) ?>: <?php echo $record->group ?></p>
<p class="m-a-0"><?php echo e(trans('martin.forms::lang.controllers.records.columns.ip')) ?>: <?php echo $record->ip ?></p>
<p class="m-a-0"><?php echo e(trans('martin.forms::lang.controllers.records.columns.created_at')) ?>: <?php echo $record->created_at ?></p>
</div>

View File

@ -0,0 +1,177 @@
<?php
return [
'plugin' => [
'name' => 'Magic Forms',
'description' => 'einfaches Erstellen von AJAX Formularen'
],
'menu' => [
'label' => 'Magic Forms',
'records' => ['label' => 'Records'],
'exports' => ['label' => 'Export'],
'settings' => 'Konfigurieren der plugin parameter',
],
'controllers' => [
'records' => [
'title' => 'Einträge anzeigen',
'view_title' => 'Details zum Eintrag',
'error' => 'Eintrag nicht gefunden',
'deleted' => 'Eintrag erfolgreich gelöscht',
'columns' => [
'id' => 'Eintrag ID',
'group' => 'Gruppe',
'ip' => 'IP Adresse',
'form_data' => 'Gespeicherte Felder',
'files' => 'Anhänge',
'created_at' => 'Erstellt',
],
'buttons' => [
'read' => 'Als gelesen markieren',
'unread' => 'Als ungelesen markieren',
'gdpr_clean' => 'DSVGO Aufräumung',
],
'alerts' => [
'gdpr_confirm' => "Sind sie sicher dass Sie alte Einträge aufräumen wollen?\nDiese Aktion kann nicht wiederrufen werden!",
'gdpr_success' => 'DSVGO Aufräumung wurde erfolgreich ausgeführt',
'gdpr_perms' => 'Sie haben keine Berechtigung für diese Funktion.',
],
],
'exports' => [
'title' => 'Einträge exportieren',
'breadcrumb' => 'Exportieren',
'filter_section' => '1. Einträge filtern',
'filter_type' => 'Alle Einträge exportieren',
'filter_groups' => 'Gruppen',
'filter_date_after' => 'Nach dem Datum',
'filter_date_before' => 'Vor dem Datum',
'options_section' => '2. Extra Optionen',
'options_metadata' => 'Inklusive Metadaten',
'options_metadata_com' => 'Exportiere die Einträge mit Metadaten (Eintrag ID, Gruppe, IP, Erstellungsdatum)',
'options_deleted' => 'Inklusive gelöschter Einträge',
'options_delimiter' => 'Alternatives Trennzeichen benutzen',
'options_delimiter_com' => 'Semikolon als Trennzeichen',
'options_utf' => 'Enkodierung in UTF8',
'options_utf_com' => 'Enkodierung Ihrer CSV-Datei im UTF-8 Format für die Unterstützung von Umlauten und Sonderzeichen.',
],
],
'components' => [
'generic_form' => [
'name' => 'Generisches AJAX Formular',
'description' => 'Mit Standardeinstellungen rendered ein generisches Formular; Überschreib Komponenten HTML mit deinen eigenen Feldern.',
],
'upload_form' => [
'name' => 'Upload AJAX Formular [BETA]',
'description' => 'Zeigt wie man Dateiupload in deinem Formular implementiert.',
],
'empty_form' => [
'name' => 'Leeres AJAX Formular',
'description' => 'Erstellt eine leere Vorlage für dein individuelles Formular; Überschreib Komponenten HTML.',
],
'shared' => [
'csrf_error' => 'Formular Seitzung abgelaufen! Bitte die Seite neuladen.',
'recaptcha_warn' => 'Warnung: reCAPTCHA ist nicht ordentlich konfiguriert. Bitte, gehe zum Backend > Einstellungen > CMS > Magic Forms um reCAPTCHA zu kofigurieren.',
'group_validation' => 'Formular Validierung',
'group_messages' => 'Flash Benachrichtung',
'group_mail' => 'Benachrichtigungen Einstellungen',
'group_mail_resp' => 'Automatische Antwort Einstellungen',
'group_settings' => 'Weitere Einstellungen',
'group_security' => 'Sicherheit',
'group_recaptcha' => 'reCAPTCHA Einstellungen',
'group_advanced' => 'Fortgeschrittene Einstellungen',
'group_uploader' => 'Uploader Einstellungen',
'validation_req' => 'Die Eigenschaft wird benötigt',
'group' => ['title' => 'Gruppe' , 'description' => 'Organisiere deine Formulare durch eigene Gruppennamen. Diese Option ist für das Exportieren der Daten sehr praktischt.'],
'rules' => ['title' => 'Regeln' , 'description' => 'Erstelle eigene Regeln Mithilfe der Laravel Validierungsfunktion'],
'rules_messages' => ['title' => 'Regeln Benachrichtigungen' , 'description' => 'Erstelle eigene Benachrichtigungen Mithilfe der Laravel Validierungsfunktion'],
'custom_attributes' => ['title' => 'Eigene Attribute' , 'description' => 'Erstelle eigene Attribute Mithilfe der Laravel Validierungsfunktion'],
'messages_success' => ['title' => 'Erfolg' , 'description' => 'Nachricht bei erfolgreicher Übermittlung des Formulars', 'default' => 'Your form was successfully submitted' ],
'messages_errors' => ['title' => 'Fehler' , 'description' => 'Nachricht bei Fehlern während der Eingabe der Formulardaten' , 'default' => 'There were errors with your submission'],
'messages_partial' => ['title' => 'Benutze eigene Partial' , 'description' => 'Überschreibe Flash-Nachrichten mit deinem eigenem Partial in deinem Theme'],
'mail_enabled' => ['title' => 'Sende Benachrichtigungen' , 'description' => 'Sende eine Benachrichtigungsmail nach jeder erfolgreichen Übertragung.'],
'mail_subject' => ['title' => 'Betreff' , 'description' => 'Überschreibe die standard E-Mail Betreffszeile'],
'mail_recipients' => ['title' => 'Empfänger' , 'description' => 'E-Mail Empfänger eintragen (Füge eine E-Mail Adresse pro Zeile ein)'],
'mail_bcc' => ['title' => 'BCC' , 'description' => 'Sende blind carbon copy (BCC) zur folgenden Empfängern (Füge eine E-Mail Adresse per Zeile ein)'],
'mail_replyto' => ['title' => 'Empfänger E-Mail Feld', 'description' => 'Formular Feld das die E-Mail Adresse enthält. Diese Adresse wird als Empfänger Adresse benutzt.'],
'mail_template' => ['title' => 'E-Mail Vorlage' , 'description' => 'Benutze eigene E-Mail Vorlagen. Gebe einen Vorlagen Code ein wie z.B. "martin.forms::mail.notification" (Kann im Einstellungen der E-Mail Vorlagen im Backend gefunden werden). Leer lassen für Standardvorlage.'],
'mail_uploads' => ['title' => 'Sende Uploads' , 'description' => 'Sende Uploads als Anhang'],
'mail_resp_enabled' => ['title' => 'Sende Auto-Antwort' , 'description' => 'Sende eine automatische Antwort E-Mail zu der Person die dein Formular ausgfüllt.'],
'mail_resp_field' => ['title' => 'E-Mail Feld' , 'description' => 'Formular Feld das die E-Mail Adresse enthät für die automatische Antwort'],
'mail_resp_from' => ['title' => 'Absender Adresse' , 'description' => 'Absender E-Mail Adresse was benutzt werden soll (z.B. noreply@yourcompany.com)'],
'mail_resp_subject' => ['title' => 'Betreff' , 'description' => 'Überschreibe standard E-Mail Betreffszeile'],
'reset_form' => ['title' => 'Resete Formular' , 'description' => 'Resete das Formular nach erfolgreichen Übertragung'],
'redirect' => ['title' => 'Umleitung bei Erfolg', 'description' => 'URL-Umleitung nach erfoglreichen Übertragung'],
'inline_errors' => ['title' => 'Inline Fehler' , 'description' => 'Zeige Inline-Fehler an. Die Funktion braucht Extra-Code. Siehe in der Dokumentation für mehr Informationen.', 'disabled' => 'Deaktiviert', 'display' => 'Zeige Fehler', 'variable' => 'JS Variable'],
'js_on_success' => ['title' => 'JS bei Erfolg' , 'description' => 'Führe eigenen JavaScript-Code aus wenn das Formular erfolgreich übertragen wurde. Keine Script Tags benutzen!'],
'js_on_error' => ['title' => 'JS bei Fehlern' , 'description' => 'Führe eigenen JavaScript-Code aus wenn das Formular nicht validiert werden kann. Keine Script Tags benutzen!'],
'allowed_fields' => ['title' => 'Erlaubte Felder' , 'description' => 'Festlegen welche Felder gefiltert und gespeichert werden sollen. (Füge ein Feld per Zeile ein)'],
'anonymize_ip' => ['title' => 'IP-Anonymisierung' , 'description' => 'IP-Adresse nicht speichern.', 'full' => 'Voll', 'partial' => 'Teilanonymisierung', 'disabled' => 'Deaktiviert'],
'sanitize_data' => ['title' => 'Bereinigung der Formulardaten' , 'description' => 'Bereinige die Formulardaten und speichere das Ergebnis in der Datenbank', 'disabled' => 'deaktiviert', 'htmlspecialchars' => 'Benutzer htmlspecialchars'],
'recaptcha_enabled' => ['title' => 'Aktiviere reCAPTCHA' , 'description' => 'reCAPTCHA-Widget in deinem Formular einfügen'],
'recaptcha_theme' => ['title' => 'Theme' , 'description' => 'Farbschema des Widgets', 'light' => 'Hell' , 'dark' => 'Dunkel'],
'recaptcha_type' => ['title' => 'Typ' , 'description' => 'reCAPTCHA Typ festlegen' , 'image' => 'Bild' , 'audio' => 'Audio'],
'recaptcha_size' => [
'title' => 'Größe',
'description' => 'Die Größe des Widgets',
'normal' => 'Normal',
'compact' => 'Kompakt',
'invisible' => 'Unsichtbar',
],
'skip_database' => ['title' => 'Überspringe DB' , 'description' => 'Keine Speicherung der Formulardaten in der Datenbank. Nützlich wenn man Events mit einem eigenem Plugin nutzen möchte.'],
'emails_date_format' => ['title' => 'Datum Formatierung in E-Mails', 'description' => 'Ändere die Datumformatierung die dann in E-Mails verwendet wird.'],
'uploader_enable' => ['title' => 'Erlaube Uploads' , 'description' => 'Aktiviere Datei-Upload. Diese Option muss aufgrund der Sicherheitseinstellungen explizit aktiviert werden.'],
'uploader_multi' => ['title' => 'Merhfachdateien' , 'description' => 'Erlaube Mehrfachdateien-Uploads'],
'uploader_pholder' => ['title' => 'Platzhalter Text' , 'description' => 'Platzhalter Text der angezeigt wird so lange keine Datei hochgeladen wurde', 'default' => 'Click or drag files to upload'],
'uploader_maxsize' => ['title' => 'Dateigröße Limitierung' , 'description' => 'Die maximale Dateigröße die hochgeladen werden kann in Megabytes'],
'uploader_types' => ['title' => 'Erlaubte Datei-Typen' , 'description' => 'Erlaubte Dateiendungen oder ein Stern (*) für alle Typen (Füge eine Dateiendung per Zeile ein)'],
'uploader_remFile' => ['title' => 'Popup Text Datei entfernen' , 'description' => 'Text Platzhalter für die Abfrage, wenn eine Datei vor dem Upload entfernt wird', 'default' => 'Are you sure ?'],
]
],
'settings' => [
'tabs' => ['general' => 'Allgemein', 'recaptcha' => 'reCAPTCHA', 'gdpr' => 'DSVGO'],
'section_flash_messages' => 'Flash Nachrichten',
'global_messages_success' => ['label' => 'Globale Erfolgsnachricht', 'comment' => '(Diese Einstellung kann aus der Komponente heraus überschrieben werden)', 'default' => 'Your form was successfully submitted'],
'global_messages_errors' => ['label' => 'Globale Fehlernachricht' , 'comment' => '(Diese Einstellung kann aus der Komponente heraus überschrieben werden)', 'default' => 'There were errors with your submission'],
'plugin_help' => 'Die Plugindokumentation erreichst Du über die GitHub repo:',
'global_hide_button' => 'Navigationsobjekt vestecken',
'global_hide_button_desc' => 'Praktisch, wenn Du eigene Events mit einem eigenem Plugin nutzen möchtest.',
'section_recaptcha' => 'reCAPTCHA Einstellungen',
'recaptcha_site_key' => 'Site key',
'recaptcha_secret_key' => 'Secret key',
'gdpr_help_title' => 'Information',
'gdpr_help_comment' => 'Das neue EU-DSVGO Gesetz in Europa besagt dass die Einträge nicht mehr unendlich aufbewahrt werden dürfen. Diese müssen je nach nach Bedarf automatisiert gelöscht werden.',
'gdpr_enable' => 'Aktiviere EU-DSVGO konforme Aufräumung',
'gdpr_days' => 'Behalte die Einträge für die Anzahl an: X Tagen',
],
'permissions' => [
'tab' => 'Magic Forms',
'access_records' => 'Zugriff auf gespeicherte Formular-Einsendedaten',
'access_exports' => 'Zugriff für das Exportieren der gespeicherten Formular-Einsendedaten',
'access_settings' => 'Zugriff auf Modul-Konfiguration',
'gdpr_cleanup' => 'Führe EU-DSVGO Aufräumung der Datenbank durch.',
],
'mails' => [
'form_notification' => ['description' => 'Benachrichtung nach erfolgreicher Einsendung der Formulardaten.'],
'form_autoresponse' => ['description' => 'Automatische Antwort nach erfolgreicher Einsendung der Formulardaten.'],
],
'validation' => [
'recaptcha_error' => 'Kann das reCAPTCHA Feld nicht validieren.'
],
'classes' => [
'GDPR' => [
'alert_gdpr_disabled' => 'EU-DSVGO Einstellungen sind deaktiviert.',
'alert_invalid_gdpr' => 'Ungültige Einstellung des EU-DSVGO Aufräumvorgangs Intervals nach X Tagen.',
]
]
];
?>

View File

@ -0,0 +1,177 @@
<?php
return [
'plugin' => [
'name' => 'Magic Forms',
'description' => 'Create easy AJAX forms'
],
'menu' => [
'label' => 'Magic Forms',
'records' => ['label' => 'Records'],
'exports' => ['label' => 'Export'],
'settings' => 'Configure plugin parameters',
],
'controllers' => [
'records' => [
'title' => 'View Records',
'view_title' => 'Record Details',
'error' => 'Record not found',
'deleted' => 'Record deleted successfully',
'columns' => [
'id' => 'Record ID',
'group' => 'Group',
'ip' => 'IP Address',
'form_data' => 'Stored Fields',
'files' => 'Attached Files',
'created_at' => 'Created',
],
'buttons' => [
'read' => 'Mark as Read',
'unread' => 'Mark as Unread',
'gdpr_clean' => 'GDPR Clean',
],
'alerts' => [
'gdpr_confirm' => "Are you sure you want to clean old records?\nThis action cannot be undone!",
'gdpr_success' => 'GDPR cleanup was executed successfully',
'gdpr_perms' => 'You don\'t have permission to this feature',
],
],
'exports' => [
'title' => 'Export Records',
'breadcrumb' => 'Export',
'filter_section' => '1. Filter records',
'filter_type' => 'Export all records',
'filter_groups' => 'Groups',
'filter_date_after' => 'Date after',
'filter_date_before' => 'Date before',
'options_section' => '2. Extra options',
'options_metadata' => 'Include metadata',
'options_metadata_com' => 'Export records with metadata (Record ID, group, IP, created date)',
'options_deleted' => 'Include deleted records',
'options_delimiter' => 'Use alternative delimiter',
'options_delimiter_com' => 'Use semicolon as delimiter',
'options_utf' => 'Encode in UTF8',
'options_utf_com' => 'Encode your csv in UTF-8 to support non standard characters',
],
],
'components' => [
'generic_form' => [
'name' => 'Generic AJAX Form',
'description' => 'By default renders a generic form; override component HTML with your custom fields.',
],
'upload_form' => [
'name' => 'Upload AJAX Form [BETA]',
'description' => 'Shows how to implement file uploads on your form.',
],
'empty_form' => [
'name' => 'Empty AJAX Form',
'description' => 'Create a empty template for your custom form; override component HTML.',
],
'shared' => [
'csrf_error' => 'Form session expired! Please refresh the page.',
'recaptcha_warn' => 'Warning: reCAPTCHA is not properly configured. Please, goto Backend > Settings > CMS > Magic Forms and configure.',
'group_validation' => 'Form Validation',
'group_messages' => 'Flash Messages',
'group_mail' => 'Notifications Settings',
'group_mail_resp' => 'Auto-Response Settings',
'group_settings' => 'More Settings',
'group_security' => 'Security',
'group_recaptcha' => 'reCAPTCHA Settings',
'group_advanced' => 'Advanced Settings',
'group_uploader' => 'Uploader Settings',
'validation_req' => 'The property is required',
'group' => ['title' => 'Group' , 'description' => 'Organize your forms with a custom group name. This option is useful when exporting data.'],
'rules' => ['title' => 'Rules' , 'description' => 'Set your own rules using Laravel validation'],
'rules_messages' => ['title' => 'Rules Messages' , 'description' => 'Use your own rules messages using Laravel validation'],
'custom_attributes' => ['title' => 'Custom Attributes' , 'description' => 'Use your own custom attributes using Laravel validation'],
'messages_success' => ['title' => 'Success' , 'description' => 'Message when the form is successfully submited', 'default' => 'Your form was successfully submitted' ],
'messages_errors' => ['title' => 'Errors' , 'description' => 'Message when the form contains errors' , 'default' => 'There were errors with your submission'],
'messages_partial' => ['title' => 'Use Custom Partial' , 'description' => 'Override flash messages with your custom partial inside your theme'],
'mail_enabled' => ['title' => 'Send Notifications' , 'description' => 'Send mail notifications on every form submited'],
'mail_subject' => ['title' => 'Subject' , 'description' => 'Override default email subject'],
'mail_recipients' => ['title' => 'Recipients' , 'description' => 'Specify email recipients (add one address per line)'],
'mail_bcc' => ['title' => 'BCC' , 'description' => 'Send blind carbon copy to email recipients (add one address per line)'],
'mail_replyto' => ['title' => 'ReplyTo Email Field', 'description' => 'Form field containing the email address of sender to be used as "ReplyTo"'],
'mail_template' => ['title' => 'Mail Template' , 'description' => 'Use custom mail template. Specify template code like "martin.forms::mail.notification" (found on Settings, Mail templates). Leave empty to use default.'],
'mail_uploads' => ['title' => 'Send Uploads' , 'description' => 'Send uploads as attachements'],
'mail_resp_enabled' => ['title' => 'Send Auto-Response' , 'description' => 'Send an auto-response email to the person submitting the form'],
'mail_resp_field' => ['title' => 'Email Field' , 'description' => 'Form field containing the email address of the recipient of auto-response'],
'mail_resp_from' => ['title' => 'Sender Address' , 'description' => 'Email address of auto-response email sender (e.g. noreply@yourcompany.com)'],
'mail_resp_subject' => ['title' => 'Subject' , 'description' => 'Override default email subject'],
'reset_form' => ['title' => 'Reset Form' , 'description' => 'Reset form after successfully submit'],
'redirect' => ['title' => 'Redirect on Success', 'description' => 'Redirect to URL on successfully submit.'],
'inline_errors' => ['title' => 'Inline errors' , 'description' => 'Display inline errors. This requires extra code, check documentation for more info.', 'disabled' => 'Disabled', 'display' => 'Display errors', 'variable' => 'JS variable'],
'js_on_success' => ['title' => 'JS on Success' , 'description' => 'Execute custom JavaScript code when the form was successfully submitted. Don\'t use script tags.'],
'js_on_error' => ['title' => 'JS on Error' , 'description' => 'Execute custom JavaScript code when the form doesn\'t validate. Don\'t use script tags.'],
'allowed_fields' => ['title' => 'Allowed Fields' , 'description' => 'Specify which fields should be filtered and stored (add one field name per line)'],
'anonymize_ip' => ['title' => 'Anonymize IP' , 'description' => 'Don\'t store IP address', 'full' => 'Full', 'partial' => 'Partial', 'disabled' => 'Disabled'],
'sanitize_data' => ['title' => 'Sanitize form data' , 'description' => 'Sanitize form data and save result on database', 'disabled' => 'Disabled', 'htmlspecialchars' => 'Use htmlspecialchars'],
'recaptcha_enabled' => ['title' => 'Enable reCAPTCHA' , 'description' => 'Insert the reCAPTCHA widget on your form'],
'recaptcha_theme' => ['title' => 'Theme' , 'description' => 'The color theme of the widget', 'light' => 'Light' , 'dark' => 'Dark'],
'recaptcha_type' => ['title' => 'Type' , 'description' => 'The type of CAPTCHA to serve' , 'image' => 'Image' , 'audio' => 'Audio'],
'recaptcha_size' => [
'title' => 'Size',
'description' => 'The size of the widget',
'normal' => 'Normal',
'compact' => 'Compact',
'invisible' => 'Invisible',
],
'skip_database' => ['title' => 'Skip DB' , 'description' => 'Don\'t store this form on database. Useful if you want to use events with your custom plugin.'],
'emails_date_format' => ['title' => 'Date format on emails', 'description' => 'Set custom format for dates used on emails subjects.'],
'uploader_enable' => ['title' => 'Allow uploads' , 'description' => 'Enable files uploading. You need to explicitly enable this option as a security measure.'],
'uploader_multi' => ['title' => 'Multiple files' , 'description' => 'Allow multipe files uploads'],
'uploader_pholder' => ['title' => 'Placeholder text' , 'description' => 'Wording to display when no file is uploaded', 'default' => 'Click or drag files to upload'],
'uploader_maxsize' => ['title' => 'File size limit' , 'description' => 'The maximum file size that can be uploaded in megabytes'],
'uploader_types' => ['title' => 'Allowed file types' , 'description' => 'Allowed file extensions or star (*) for all types (add one extension per line)'],
'uploader_remFile' => ['title' => 'Remove Popup text' , 'description' => 'Wording to display in the popup when you remove file', 'default' => 'Are you sure ?'],
]
],
'settings' => [
'tabs' => ['general' => 'General', 'recaptcha' => 'reCAPTCHA', 'gdpr' => 'GDPR'],
'section_flash_messages' => 'Flash Messages',
'global_messages_success' => ['label' => 'Global Success Message', 'comment' => '(This setting can be overridden from the component)', 'default' => 'Your form was successfully submitted'],
'global_messages_errors' => ['label' => 'Global Errors Message' , 'comment' => '(This setting can be overridden from the component)', 'default' => 'There were errors with your submission'],
'plugin_help' => 'You can access plugin documentation at GitHub repo:',
'global_hide_button' => 'Hide navigation item',
'global_hide_button_desc' => 'Useful if you want to use events with your custom plugin.',
'section_recaptcha' => 'reCAPTCHA Settings',
'recaptcha_site_key' => 'Site key',
'recaptcha_secret_key' => 'Secret key',
'gdpr_help_title' => 'Information',
'gdpr_help_comment' => 'New GDPR law in Europe, you can\'t keep records undefinitely, need to clear them after a certain period of time depending on your needs',
'gdpr_enable' => 'Enable GDPR',
'gdpr_days' => 'Keep records for a maximum of X days',
],
'permissions' => [
'tab' => 'Magic Forms',
'access_records' => 'Access stored forms data',
'access_exports' => 'Access to export stored data',
'access_settings' => 'Access module configuration',
'gdpr_cleanup' => 'Perform GDPR database cleanup',
],
'mails' => [
'form_notification' => ['description' => 'Notify when a form is submited'],
'form_autoresponse' => ['description' => 'Auto-Response when a form is submited'],
],
'validation' => [
'recaptcha_error' => 'Cannot validate reCAPTCHA field'
],
'classes' => [
'GDPR' => [
'alert_gdpr_disabled' => 'GDPR options are disabled',
'alert_invalid_gdpr' => 'Invalid GDPR days setting value',
]
]
];
?>

View File

@ -0,0 +1,153 @@
<?php
return [
'plugin' => [
'name' => 'Magic Forms',
'description' => 'Créer des formulaires AJAX facilement'
],
'menu' => [
'label' => 'Magic Forms',
'records' => ['label' => 'Enregistrements'],
'exports' => ['label' => 'Export'],
'settings' => 'Configurer kes parameters du plugin',
],
'controllers' => [
'records' => [
'title' => 'Voir les enregistrements',
'view_title' => 'Détails de l\'enregistrement',
'error' => 'Enregistrement non trouvé',
'deleted' => 'Enregistrement supprimé avec succès',
'columns' => [
'id' => 'Enregistrement N°',
'group' => 'Groupe',
'ip' => 'Adresse IP',
'form_data' => 'Champs stockés',
'files' => 'Fichiers attachés',
'created_at' => 'Créer le',
],
'buttons' => [
'read' => 'Marquer comme lu',
'unread' => 'Marquer comme non lu',
'gdpr_clean' => 'Nettoyage RGPD',
],
'alerts' => [
'gdpr_confirm' => "Êtes-vous sûr de vouloir nettoyer les anciens enregistrements?\nCette action ne peut pas être annulée!",
'gdpr_success' => 'Le nettoyage RGPD a été exécuté avec succès',
'gdpr_perms' => 'Vous n\'avez pas l\'autorisation pour utiliser cette fonctionnalité',
],
],
'exports' => [
'title' => 'Exporter les enregistrements',
'breadcrumb' => 'Exporter',
'filter_section' => '1. Filtrer les enregistrements',
'filter_type' => 'Exporter tous les enregistrements',
'filter_groups' => 'Groupes',
'filter_date_after' => 'Date de début',
'filter_date_before' => 'Date de fin',
'options_section' => '2. Options supplémentaires',
'options_metadata' => 'Inclure les metadonnées',
'options_metadata_com' => 'Exporter les enregistrements avec les metadonnées (n°, groupe, IP, date de création)',
'options_deleted' => 'Inclure les enregistrements supprimés',
],
],
'components' => [
'generic_form' => [
'name' => 'Formulaire générique AJAX',
'description' => 'Rendu d\'un formulaire générique; remplacer le composant HTML par vos propres champs personnalisés.',
],
'upload_form' => [
'name' => 'Téléchargements AJAX [BETA]',
'description' => 'Montre comment implémenter des téléchargements de fichiers sur votre formulaire.',
],
'empty_form' => [
'name' => 'Formulaire AJAX vide',
'description' => 'Créer un modèle vide pour votre formulaire personnalisé; remplacer le composant HTML.',
],
'shared' => [
'csrf_error' => 'La session du formulaire a expirée ! Veuillez actualiser la page.',
'recaptcha_warn' => 'Avertissement: reCAPTCHA n\'est pas correctement configuré. Àllez dans Backend > Paramètres > CMS > Magic Forms et configurez svp.',
'group_validation' => 'Validation du formulaire',
'group_messages' => 'Messages Flash ',
'group_mail' => 'Notifications',
'group_mail_resp' => 'Réponse automatique',
'group_settings' => 'Plus de réglages',
'group_security' => 'Securité',
'group_recaptcha' => 'reCAPTCHA',
'group_uploader' => 'Transfert de fichiers',
'validation_req' => 'La propriété est requise',
'group' => ['title' => 'Groupe' , 'description' => 'Organisez vos formulaires avec un nom de groupe personnalisé. Cette option est utile lorsque vous exportez des données.'],
'rules' => ['title' => 'Règles' , 'description' => 'Définissez vos propres règles en utilisant la validation de Laravel'],
'rules_messages' => ['title' => 'Messages de règles' , 'description' => 'Utilisez vos propres messages de règles en utilisant la validation de Laravel'],
'messages_success' => ['title' => 'Succès' , 'description' => 'Message lorsque le formulaire est soumis avec succès' , 'default' => 'Votre formulaire a été envoyé avec succès' ],
'messages_errors' => ['title' => 'Erreurs' , 'description' => 'Message lorsque le formulaire contient des erreurs' , 'default' => 'Il y a eu des erreurs dans votre formulaire'],
'messages_partial' => ['title' => 'Utiliser un modèle partiel personnalisé', 'description' => 'Modifier le message flash avec un modèle partiel personnalisé de votre thème'],
'mail_enabled' => ['title' => 'Envoyer des notifications' , 'description' => 'Envoyer des notifications par mail sur chaque formulaire envoyé'],
'mail_subject' => ['title' => 'Sujet' , 'description' => 'Remplacer le sujet par défaut du courrier électronique'],
'mail_recipients' => ['title' => 'Destinataires' , 'description' => 'Spécifier les destinataires des e-mails (ajouter une adresse par ligne)'],
'mail_bcc' => ['title' => 'CCI' , 'description' => 'Envoyer une copie carbone invisible aux destinataires des e-mails (ajouter une adresse par ligne)'],
'mail_replyto' => ['title' => 'Champ du email de réponse (ReplyTo)' , 'description' => 'Champ de formulaire contenant l\'adresse e-mail de l\'expéditeur à utiliser comme "ReplyTo"'],
'mail_uploads' => ['title' => 'Envoyer les téléchargements' , 'description' => 'Envoi des fichiers téléchargés en pièce jointe'],
'mail_template' => ['title' => 'Modèle e-mail' , 'description' => 'Utiliser un modèle e-mail personnalisé. Spécifiez un code de modèle tel que "martin.forms::mail.notification" (situé dans Paramètres, Modèles des e-mails). Laissez vide pour utiliser les paramètres par défaut.'],
'mail_uploads' => ['title' => 'Envoyer les téléchargements' , 'description' => 'Envoyer les téléchargements en pièces jointes'],
'mail_resp_enabled' => ['title' => 'Envoyer une réponse automatique' , 'description' => 'Envoyer un e-mail d\'auto-réponse à la personne qui soumet le formulaire'],
'mail_resp_field' => ['title' => 'Champ email' , 'description' => 'Champ de formulaire contenant l\'adresse e-mail du destinataire de réponse automatique '],
'mail_resp_from' => ['title' => 'Adresse de l\'expéditeur' , 'description' => 'Adresse e-mail de l\'expéditeur du courrier électronique de réponse automatique (par exemple nepasrepondre@votreentreprise.com)'],
'mail_resp_subject' => ['title' => 'Sujet' , 'description' => 'Remplacer le sujet par défaut du courrier électronique'],
'reset_form' => ['title' => 'Réinitialiser le formulaire' , 'description' => 'Réinitialiser le formulaire après l\'envoi réussi'],
'redirect' => ['title' => 'Redirection envoi réussi' , 'description' => 'Rediriger vers une URL spécifique lors de l\'envoi réussi. Remarque: doit être une URL valide commençant par http ou https ou la redirection sera ignorée.'],
'inline_errors' => ['title' => 'Erreurs sur la même ligne' , 'description' => 'Afficher les erreurs sur la même ligne. Cela nécéssite du code supplémentaire, consultez la documentation pour plus d\'informations. ', 'disabled' => 'Désactivé', 'display' => 'Afficher les erreurs', 'variable' => 'Variable Javascript'],
'js_on_success' => ['title' => 'JS au succès' , 'description' => 'Exécutez du code JavaScript personnalisé lorsque le formulaire a été soumis avec succès. Ne pas utiliser les balises script'],
'js_on_error' => ['title' => 'JS si erreur' , 'description' => 'Exécutez du code JavaScript personnalisé lorsque le formulaire ne se valide pas. Ne pas utiliser les balises script.'],
'allowed_fields' => ['title' => 'Allowed Fields' , 'description' => 'Spécifiez quels champs doivent être filtrés et stockés (ajoutez un nom de champ par ligne).'],
'anonymize_ip' => ['title' => 'Anonymiser IP' , 'description' => 'Ne pas enregistrer les adresses IP', 'full' => 'Complet', 'partial' => 'Partiel', 'disabled' => 'Désactivé'],
'sanitize_data' => ['title' => 'Désinfecter les données de formulaire' , 'description' => 'Désinfectez les données de formulaire et enregistrez le résultat dans la base de données', 'disabled' => 'Désactivé', 'htmlspecialchars' => 'Utilisez htmlspecialchars'],
'recaptcha_enabled' => ['title' => 'Activer reCAPTCHA' , 'description' => 'Insère le widget reCAPTCHA sur votre formulaire'],
'recaptcha_theme' => ['title' => 'Thème' , 'description' => 'Le thème de couleur du widget' , 'light' => 'Léger' , 'dark' => 'Sombre'],
'recaptcha_type' => ['title' => 'Type' , 'description' => 'Le type de CAPTCHA à servir' , 'image' => 'Image' , 'audio' => 'Audio'],
'recaptcha_size' => ['title' => 'Taille' , 'description' => 'La taille du widget' , 'normal' => 'Normale', 'compact' => 'Compacte'],
'skip_database' => ['title' => 'Ignore la BDD' , 'description' => 'Ne pas stocker ce formulaire dans la base de données. Utile si vous souhaitez utiliser des évènements avec votre plugin personnalisé.'],
'uploader_enable' => ['title' => 'Autoriser les téléchargements' , 'description' => 'Activer le téléchargement des fichiers. Vous devez activer cette option explicitement comme mesure de sécurité.'],
'uploader_multi' => ['title' => 'Fichiers multiples' , 'description' => 'Autoriser plusieurs téléchargements de fichiers'],
'uploader_pholder' => ['title' => 'Texte de remplacement' , 'description' => 'Texte à afficher quand aucun fichier n\'est téléchargé', 'default' => 'Cliquez pour choisir ou faites glisser les fichiers à télécharger'],
'uploader_maxsize' => ['title' => 'Limite de taille de fichier' , 'description' => 'Taille maximale du fichier pouvant être téléchargée en mégaoctets'],
'uploader_types' => ['title' => 'Types de fichiers autorisés' , 'description' => 'Extensions de fichiers autorisées ou étoile (*) pour tous les types (ajoutez une extension par ligne)'],
'uploader_remFile' => ['title' => 'Texte de Suppression' , 'description' => 'Texte à afficher dans la popup lors de la suppression d\'un fichier', 'default' => 'Êtes-vous sûr ?'],
]
],
'settings' => [
'tabs' => ['general' => 'Général', 'recaptcha' => 'reCAPTCHA', 'gdpr' => 'RGPD'],
'section_flash_messages' => 'Messages Flash',
'global_messages_success' => ['label' => 'Message de réussite', 'comment' => '(Ce paramètre peut être remplacé par le composant)', 'default' => 'Votre formulaire a été envoyé avec succès'],
'global_messages_errors' => ['label' => 'Global Errors Message' , 'comment' => '(Ce paramètre peut être remplacé par le composant)', 'default' => 'Il y a eu des erreurs dans votre formulaire'],
'plugin_help' => 'Vous pouvez accéder à la documentation du plugin sur GitHub :',
'global_hide_button' => 'Masquer l\'élément de navigation',
'global_hide_button_desc' => 'Utile si vous souhaitez utiliser des événements avec votre plugin personnalisé.',
'section_recaptcha' => 'Paramètres reCAPTCHA',
'recaptcha_site_key' => 'Clé du site',
'recaptcha_secret_key' => 'Clé secrète',
'gdpr_help_title' => 'Information',
'gdpr_help_comment' => 'Nouvelle loi RGPD/GDPR en Europe : vous ne pouvez pas conserver les enregistrements indéfiniment, vous devez les effacer au bout d\'un certain temps en fonction de vos besoins.',
'gdpr_enable' => 'Activer RGPD',
'gdpr_days' => 'Garder les enregistrements pour un maximum de X jours',
],
'permissions' => [
'tab' => 'Magic Forms',
'access_records' => 'Accéder aux données des formulaires stockés',
'access_exports' => 'Accès aux exports des formulaires stockés',
'access_settings' => 'Accès à la configuration du module',
'gdpr_cleanup' => 'Accès au nettoyage de la base de donnée RGPD',
],
'mails' => [
'form_notification' => ['description' => 'Notifier quand un formulaire est envoyé'],
'form_autoresponse' => ['description' => 'Auto-Réponse lorsqu\'un formulaire est envoyé'],
],
'validation' => [
'recaptcha_error' => 'Impossible de valider le champ reCAPTCHA'
],
'classes' => [
'GDPR' => [
'alert_gdpr_disabled' => 'LEs options RGPD sont désactivés',
'alert_invalid_gdpr' => 'Valeur de réglage des jours RGPD invalide',
]
]
];
?>

View File

@ -0,0 +1,116 @@
<?php
return [
'plugin' => [
'name' => 'Magic Forms',
'description' => 'Crie formulários fácilmente com AJAX'
],
'menu' => [
'label' => 'Formulários Mágico',
'records' => ['label' => 'Registros'],
'exports' => ['label' => 'Exportar'],
'settings' => 'Configurar parâmetros do plug-in',
],
'controllers' => [
'records' => [
'title' => 'Ver registos',
'view_title' => 'Detalhes do registro',
'error' => 'Registro não encontrado',
'deleted' => 'Registro excluído com sucesso',
'columns' => [
'id' => 'ID do registro',
'group' => 'Grupo',
'ip' => 'Endereço IP',
'form_data' => 'Campos armazenados',
'files' => 'Arquivos anexados',
'created_at' => 'Criado',
],
],
'exports' => [
'title' => 'Exportar registros',
'breadcrumb' => 'Exportar',
'filter_section' => '1. Filtrar registos',
'filter_type' => 'Exportar todos os registros',
'filter_groups' => 'Grupos',
'filter_date_after' => 'Data após',
'filter_date_before' => 'Data anterior',
'options_section' => '2. Opções extras',
'options_metadata' => 'Incluir metadados',
'options_metadata_com' => 'Exportar registros com metadados (ID de registro, grupo, IP, data criação)',
'options_deleted' => 'Incluir registros excluídos',
],
],
'components' => [
'generic_form' => [
'name' => 'Formulário AJAX Genérico',
'description' => 'Por padrão renderiza um formulário genérico; Substitua o componente HTML com seus campos personalizados.',
],
'upload_form' => [
'name' => 'formulário upload AJAX [BETA]',
'description' => 'Mostra como implementar uploads de arquivos no formulário.',
],
'empty_form' => [
'name' => 'Formulário AJAX vazio',
'description' => 'Crie um modelo vazio para seu formulário personalizado; Substituir o componente HTML.',
],
'shared' => [
'csrf_error' => 'A sessão do formulário expirou! Atualize a página.',
'recaptcha_warn' => 'Aviso: reCAPTCHA não está configurado corretamente. Por favor, vá para Back-end> Configurações> CMS> Magic Forms e configure.',
'group_validation' => 'Validação de formulários',
'group_messages' => 'Mensagens Flash',
'group_mail' => 'Configurações de Notificações',
'group_mail_resp' => 'Configurações de Resposta Automática',
'group_settings' => 'Mais configurações',
'group_security' => 'Segurança',
'group_recaptcha' => 'Configurações de reCAPTCHA',
'group_uploader' => 'Configurações do Uploader',
'validation_req' => 'A propriedade é obrigatória',
'group' => ['title' => 'Grupo' , 'description' => 'Organize seus formulários com um nome de grupo personalizado. Esta opção é útil ao exportar dados.'],
'rules' => ['title' => 'Regras' , 'description' => 'Defina suas próprias regras usando a validação Laravel'],
'rules_messages' => ['title' => 'Mensagens de Regras' , 'description' => 'Use suas próprias mensagens de regras usando a validação do Laravel'],
'messages_success' => ['title' => 'Sucesso' , 'description' => 'Mensagem quando o formulário é enviado com sucesso', 'default '=>' Seu formulário foi enviado com sucesso'],
'messages_errors' => ['title' => 'Erros' , 'description' => 'Mensagem quando o formulário contém erros', 'default' => 'Ocorreu um erro no envio'],
'mail_enabled' => ['title' => 'Enviar Notificações' , 'description' => 'Enviar notificações por email em todos os formulários enviados'],
'mail_subject' => ['title' => 'Assunto' , 'description' => 'Substitui o assunto de email padrão'],
'mail_recipients' => ['title' => 'Destinatários' , 'description' => 'Especifique destinatários de email (adicione um endereço por linha)'],
'mail_uploads' => ['title' => 'Enviar Uploads' , 'description' => 'Enviar uploads como anexos'],
'mail_resp_enabled' => ['title' => 'Enviar Resposta Automática' , 'description' => 'Enviar um email de resposta automática para a pessoa que envia o formulário'],
'mail_resp_field' => ['title' => 'Campo de Email' , 'description' => 'Campo de formulário que contém o endereço de email do destinatário da resposta automática'],
'mail_resp_from' => ['title' => 'Endereço do Remetente' , 'description' => 'Endereço de email do remetente de e-mail de resposta automática (por exemplo, noreply@yourcompany.com)'],
'mail_resp_subject' => ['title' => 'Assunto' , 'description' => 'Substitui assunto de e-mail padrão'],
'reset_form' => ['title' => 'Limpar Fomulário' , 'description' => 'Limpar o formulário após o envio com sucesso'],
'allowed_fields' => ['title' => 'Campos permitidos' , 'description' => 'Especifique quais campos devem ser filtrados e armazenados (adicione um nome de campo por linha)'],
'recaptcha_enabled' => ['title' => 'Habiltar reCAPTCHA' , 'description' => 'Inserir o widget reCAPTCHA no seu formulário'],
'recaptcha_theme' => ['title' => 'Tema' , 'description' => 'Cor do tema do widget', 'light' => 'Claro' , 'dark' => 'Escuro'],
'recaptcha_type' => ['title' => 'Tipo' , 'description' => 'O tipo de CAPTCHA para servir' , 'image' => 'Imagem' , 'audio' => 'Áudio'],
'recaptcha_size' => ['title' => 'Tamanho' , 'description' => 'O tamanho do widget' , 'normal' => 'Normal', 'compact' => 'Compacto'],
'uploader_enable' => ['title' => 'Permitir Uploads' , 'description' => 'Ativar o upload de arquivos. Você precisa ativar essa opção explicitamente como uma medida de segurança.'],
'uploader_multi' => ['title' => 'Multiplos Arquivos' , 'description' => 'Permitir multiplos uploads de arquivos'],
'uploader_pholder' => ['title' => 'texto do Placeholder' , 'description' => 'Texto a ser exibido quando nenhum arquivo é carregado', 'default' => 'Clique ou arraste os arquivos para fazer o upload'],
'uploader_maxsize' => ['title' => 'Limite do Tamanho do Arquivo' , 'description' => 'O tamanho máximo de arquivo que pode ser carregado em megabytes'],
'uploader_types' => ['title' => 'Tipos de Arquivos Permitidos' , 'description' => 'Extensões de arquivo permitido ou asterisco (*) para todos os tipos (adicionar uma extensão por linha)'],
]
],
'settings' => [
'section_flash_messages' => 'Mensagens Flash',
'global_messages_success' => ['label' => 'Mensagem de sucesso global' , 'comment' => '(Esta definição pode ser substituída a partir do componente)', 'default' => 'Seu formulário foi enviado com sucesso'],
'global_messages_errors' => ['label' => 'Mensagem de Erros Global' , 'comment' => '(Esta definição pode ser substituída a partir do componente)', 'default' => 'Ocorreu um erro o envio do formulário'],
'section_recaptcha' => 'Configurações de reCAPTCHA',
'recaptcha_site_key' => 'Chave do site',
'recaptcha_secret_key' => 'Chave secreta',
],
'permissions' => [
'tab' => 'Formulários Mágico',
'access_records' => 'Acessar dados de formulários armazenados',
'access_exports' => 'Acesso à exportação de dados armazenados',
'access_settings' => 'Acesso do módulo de configuração',
],
'mails' => [
'form_notification' => ['description' => 'Notificar quando um formulário é enviado'],
'form_autoresponse' => ['description' => 'Resposta automática quando um formulário é enviado'],
],
'validation' => [
'recaptcha_error' => 'Não é possível validar o campo reCAPTCHA'
],
];
?>

View File

@ -0,0 +1,170 @@
<?php
return [
'plugin' => [
'name' => 'Magic Forms',
'description' => 'С легкостью создайте AJAX форму'
],
'menu' => [
'label' => 'Magic Forms',
'records' => ['label' => 'Записи'],
'exports' => ['label' => 'Экспорт'],
'settings' => 'Настройки плагина',
],
'controllers' => [
'records' => [
'title' => 'Все заявки',
'view_title' => 'Запись',
'error' => 'Запись не найдена',
'deleted' => 'Запись успешно удалена',
'columns' => [
'id' => 'ID записи',
'group' => 'Группа',
'ip' => 'IP адрес',
'form_data' => 'Поля формы',
'files' => 'Прикрепленные файлы',
'created_at' => 'Создано',
],
'buttons' => [
'read' => 'Отметить как прочитано',
'unread' => 'Отметить как непрочитанное',
'gdpr_clean' => 'Очистить GDPR',
],
'alerts' => [
'gdpr_confirm' => "Вы действительно хотите удалить эти старые записи?\nЭто действие не может быть отменено!",
'gdpr_success' => 'Очистка GDPR успешна',
'gdpr_perms' => 'У вас нет прав использования этой функции',
],
],
'exports' => [
'title' => 'Экспорт записей',
'breadcrumb' => 'Экспорт',
'filter_section' => '1. Фильтровать записи',
'filter_type' => 'Экспорт всех записей',
'filter_groups' => 'Группы',
'filter_date_after' => 'Дата от',
'filter_date_before' => 'дата до',
'options_section' => '2. Экстра опции',
'options_metadata' => 'Вложить метаданные',
'options_metadata_com' => 'Экспорт записей с метаданными(ID, группа, дата создания)',
'options_deleted' => 'Вложить удаленные записи',
'options_delimiter' => 'Использовать альтернативный разделитель полей',
'options_delimiter_com' => 'Точка с запятой вместо запятой',
'options_utf' => 'Кодировать в UTF8',
'options_utf_com' => 'Ваш CSV файл будет закодирван в UTF-8',
],
],
'components' => [
'generic_form' => [
'name' => 'Стандартная AJAX форма',
'description' => 'Выводит стандартную форму; Перезапишите HTML компонент со своей формой.',
],
'upload_form' => [
'name' => 'AJAX форма с файлами',
'description' => '[BETA] Форма с возможностью прикрепления файлов.',
],
'empty_form' => [
'name' => 'Пустая AJAX форма',
'description' => 'Создает пустой плейсхолдер для вашей формы, который вы должны перезаписать своим HTML кодом.',
],
'shared' => [
'csrf_error' => 'Сессия формы истекла! Перезагрузите страницу.',
'recaptcha_warn' => 'Внимание: reCAPTCHA настроена не правильно. Пожалуйста, перейдите в Настройки > CMS > Magic Forms и заполните все обязательные поля.',
'group_validation' => 'Валидация формы',
'group_messages' => 'Флэш-сообщения',
'group_mail' => 'Настройки уведомлений',
'group_mail_resp' => 'Настройки авто-ответа',
'group_settings' => 'Остальные настройки',
'group_security' => 'Безопасность',
'group_recaptcha' => 'Настройки reCAPTCHA',
'group_advanced' => 'Продвинутые настройки',
'group_uploader' => 'Настройки файлового загрузчика',
'validation_req' => 'Этот атрибут обязателен',
'group' => ['title' => 'Группа' , 'description' => 'Организуйте свои формы по группам. Эта опция полезна для экспорта записей.'],
'rules' => ['title' => 'Правила' , 'description' => 'Используйте валидацию Laravel для установки своих правил обработки полей.'],
'rules_messages' => ['title' => 'Сообщения от правил', 'description' => 'Используйте свои собственные сообщения от кастомных валидаций.'],
'custom_attributes' => ['title' => 'Кастомные атрибуты' , 'description' => 'Используйте кастомные атрибуты в своих правилах валидаций.'],
'messages_success' => ['title' => 'Успешно' , 'description' => 'Сообщение об успешной отправке формы', 'default' => 'Ваша форма была успешно отправлена!'],
'messages_errors' => ['title' => 'Ошибки' , 'description' => 'Сообщение с ошибками формы' , 'default' => 'В вашей заявке содержатся ошибки.'],
'messages_partial' => ['title' => 'Кастомный фрагмент' , 'description' => 'Переопределить стандартные уведомления кастомным фрагментом из вашей фронтенд темы.'],
'mail_enabled' => ['title' => 'Отправка уведомлений','description' => 'Отправлять увдеомления по email после каждого успешного заполенния формы.'],
'mail_subject' => ['title' => 'Тема письма' , 'description' => 'Переопределить тему письма по умолчанию'],
'mail_recipients' => ['title' => 'Получатели' , 'description' => 'Укажите получателей электронной почты (По одному адресу в строке)'],
'mail_bcc' => ['title' => 'BCC' , 'description' => 'Отправить копию получателям электронной почты (По одному адресу в строке)'],
'mail_replyto' => ['title' => 'ReplyTo поле email' , 'description' => 'Поле формы, содержащее адрес электронной почты отправителя, который будет использоваться как "ReplyTo"'],
'mail_template' => ['title' => 'Шаблон письма' , 'description' => 'Используйте собственный шаблон письма. Укажите код шаблона, например, "martin.forms::mail.notification" (находится в разделе Настройки -> Почтовые шаблоны). Оставьте пустым, чтобы использовать по умолчанию.'],
'mail_uploads' => ['title' => 'Прикреплять файлы' , 'description' => 'Прикреплять файлы к письму'],
'mail_resp_enabled' => ['title' => 'Отправить авто-ответ','description' => 'Отправить автоответчик на email отправителя.'],
'mail_resp_field' => ['title' => 'Email поле' , 'description' => 'Имя поля с email отправителя'],
'mail_resp_from' => ['title' => 'Email адрес отправителя', 'description' => 'Адрес электронной почты отправителя электронной почты с автоматическим ответом (например, noreply@yourcompany.com)'],
'mail_resp_subject' => ['title' => 'Тема пиьсма' , 'description' => 'Переопределить тему письма по умолчанию'],
'reset_form' => ['title' => 'Сбросить форму' , 'description' => 'Сбросить форму после успешной отправки'],
'redirect' => ['title' => 'Редирект' , 'description' => 'Редирект на URL после успешной отправки формы.'],
'inline_errors' => ['title' => 'Встроенные ошибки' , 'description' => 'Отображать встроенные ошибки. Это требует дополнительного кода, проверьте документацию для получения дополнительной информации.', 'disabled' => 'Отключено', 'display' => 'Отображать ошибки', 'variable' => 'JS переменная'],
'js_on_success' => ['title' => '"Успешный" JS' , 'description' => 'Выполнияет скрипт после успешной отправки формы. Не используйте <script> теги.'],
'js_on_error' => ['title' => '"Ошибочный" JS' , 'description' => 'Выполнияет скрипт после не успешной отправки формы. Не используйте <script> теги'],
'allowed_fields' => ['title' => 'Разрешенные поля' , 'description' => 'Укажите, какие поля должны быть отфильтрованы и сохранены (По одному адресу в строке)'],
'anonymize_ip' => ['title' => 'Анонимный IP' , 'description' => 'Не хранить IP-адрес', 'full' => 'Полный', 'partial' => 'Частичынй', 'disabled' => 'Отключено'],
'sanitize_data' => ['title' => '"Sanitize" данные формы' , 'description' => 'Очищает поля формы от возмонжой XSS атаки', 'disabled' => 'Отключено', 'htmlspecialchars' => 'Использовать htmlspecialchars'],
'recaptcha_enabled' => ['title' => 'Включить reCAPTCHA' , 'description' => 'Внедряет reCAPTCHA виджет в вашу форму (не забудьте добавить фрагмент).'],
'recaptcha_theme' => ['title' => 'Тема оформления' , 'description' => 'Цветовая тема виджета', 'light' => 'Светлая' , 'dark' => 'Темная'],
'recaptcha_type' => ['title' => 'Тип' , 'description' => 'Тип CAPTCHA', 'image' => 'Картинки' , 'audio' => 'Аудио'],
'recaptcha_size' => ['title' => 'Размер' , 'description' => 'Размер виджета', 'normal' => 'Обычный', 'compact' => 'Комактный'],
'skip_database' => ['title' => 'Пропустить Базу' , 'description' => 'Не хранить записи в базе данных. Полезно, если вы хотите использовать события с вашим плагином.'],
'uploader_enable' => ['title' => 'Разрешить загрузку' , 'description' => 'Включить загрузку файлов. Разрешает форме принимать файлы.'],
'uploader_multi' => ['title' => 'Множественные файлы', 'description' => 'Разрешить загрузку множества файлов (Если выключено то форма сможет принять только один файл)'],
'uploader_pholder' => ['title' => 'Placeholder текст' , 'description' => 'Формулировка, отображаемая при отсутствии загруженного файла.', 'default' => 'Нажмите или перетащите файлы для загрузки'],
'uploader_maxsize' => ['title' => 'Ограничение размера файла', 'description' => 'Максимальный размер файла, который может быть загружен в мегабайтах'],
'uploader_types' => ['title' => 'Разрешенные типы файлов', 'description' => 'Разрешенные расширения файлов или звездочка (*) для всех типов (По одному расширению в строке)'],
'uploader_remFile' => ['title' => 'Popup текст об удалении' , 'description' => 'Текст для отображения во всплывающем окне при удалении файла', 'default' => 'Вы уверены?'],
]
],
'settings' => [
'tabs' => ['general' => 'Основное', 'recaptcha' => 'reCAPTCHA', 'gdpr' => 'GDPR'],
'section_flash_messages' => 'Флэш-сообщения',
'global_messages_success' => ['label' => 'Глобальное сообщение об успешной отправки формы', 'comment' => '(Этот параметр может быть переопределен из компонента)', 'default' => 'Ваша форма была успешно отправлена!'],
'global_messages_errors' => ['label' => 'Глобальное сообщение об не успешной отправки формы' , 'comment' => '(Этот параметр может быть переопределен из компонента)', 'default' => 'В вашей заявке содержатся ошибки.'],
'plugin_help' => 'Вы можете получить доступ к документации по плагину в GitHub:',
'global_hide_button' => 'Скрыть элемент навигации',
'global_hide_button_desc' => 'Полезно, если вы хотите использовать события с вашим плагином.',
'section_recaptcha' => 'reCAPTCHA настройки',
'recaptcha_site_key' => 'Site key',
'recaptcha_secret_key' => 'Secret key',
'gdpr_help_title' => 'Информация',
'gdpr_help_comment' => 'Новый закон о GDPR в Европе, вы не можете хранить записи бесконечно, необходимо очистить их через определенный промежуток времени в зависимости от ваших потребностей',
'gdpr_enable' => 'Включить GDPR',
'gdpr_days' => 'Хранение записей максимум Х дней',
],
'permissions' => [
'tab' => 'Magic Forms',
'access_records' => 'Доступ к записям форм',
'access_exports' => 'Доступ к экспорту записей',
'access_settings' => 'Доступ к настройкам модуля',
'gdpr_cleanup' => 'Разрешить GDPR очистку',
],
'mails' => [
'form_notification' => ['description' => 'Уведомлять когда форма отправлена успешно'],
'form_autoresponse' => ['description' => 'Авто-ответ при успешной отправке формы'],
],
'validation' => [
'recaptcha_error' => 'Не удается проверить поле reCAPTCHA'
],
'classes' => [
'GDPR' => [
'alert_gdpr_disabled' => 'GDPR опции отключены',
'alert_invalid_gdpr' => 'Некорретное занчение дней в настройках GDPR',
]
]
];
?>

View File

@ -0,0 +1,107 @@
<?php
return [
'plugin' => [
'name' => 'Magic Forms',
'description' => 'Kolay AJAX formları oluşturun'
],
'menu' => [
'label' => 'Sihirli Formlar',
'records' => ['label' => 'Kayıtlar'],
'exports' => ['label' => 'Dışa Aktar'],
'settings' => 'Eklenti parametrelerini yapılandır',
],
'controllers' => [
'records' => [
'title' => 'Kayıtları Görüntüle',
'view_title' => 'Kayıt Ayrıntıları',
'error' => 'Kayıt bulunamadı',
'deleted' => 'Kayıt başarıyla silindi',
'columns' => [
'id' => 'Kayıt Kimliği',
'group' => 'Grup',
'ip' => 'IP Adresi',
'form_data' => 'Kayıtlı Alanlar',
'created_at' => 'Oluşturuldu',
],
],
'exports' => [
'title' => 'Dışa Aktarma Kayıtları',
'breadcrumb' => 'Dışa Aktarma',
'filter_section' => '1. Filtre kayıtları',
'filter_type' => 'Tüm kayıtları dışa aktar',
'filter_groups' => 'Gruplar',
'filter_date_after' => 'Tarihinden sonra',
'filter_date_before' => 'Tarihinden önce',
'options_section' => '2. Ekstra seçenekler',
'options_metadata' => 'Meta verileri içe aktar',
'options_metadata_com' => 'Meta verilerle kayıtları dışa aktarmak (Kayıt Kimliği, grup, IP, oluşturma tarihi)',
'options_deleted' => 'Silinmiş kayıtları dahil et',
],
],
'components' => [
'generic_form' => [
'name' => 'Genel AJAX Formu',
'description' => 'Varsayılan olarak, genel bir form oluşturur; Özel alanlarınızla, HTML bileşenlerini geçersiz kılar.',
],
'empty_form' => [
'name' => 'Boş AJAX Formu',
'description' => 'Özel formunuz için boş bir şablon oluşturun; HTML bileşenlerini geçersiz kıl.',
],
'shared' => [
'csrf_error' => 'Form oturumu doldu! Lütfen sayfayı yenileyin.',
'recaptcha_warn' => 'Uyarı: reCAPTCHA düzgün şekilde yapılandırılmadı',
'group_validation' => 'Form Doğrulaması',
'group_messages' => 'Flash Mesajları',
'group_security' => 'Güvenlik',
'group_mail' => 'Posta Ayarları',
'group_recaptcha' => 'ReCAPTCHA Ayarları',
'validation_req' => 'Özellikler gereklidir',
'group' => ['title' => 'Grup' , 'description' => 'Formlarınızı özel bir grup adı ile düzenleyin. Bu seçenek, veri aktarırken yararlıdır.'],
'rules' => ['title' => 'Kurallar' , 'description' => 'Laravel doğrulamasını kullanarak kendi kurallarınızı belirleyin'],
'rules_messages' => ['title' => 'Kural Mesajları' , 'description' => 'Laravel doğrulamasını kullanarak kendi kural mesajlarını kullanın'],
'messages_success' => ['title' => 'Başarı' , 'description' => 'Form başarıyla gönderildiğinde gönderilecek ileti', 'default' => 'Formunuz başarıyla gönderildi' ],
'messages_errors' => ['title' => 'Hatalar' , 'description' => 'Formda hatalar olduğunda gösterilecek mesaj' , 'default' => 'Gönderiminizle ilgili hatalar vardı.'],
'allowed_fields' => ['title' => 'İzin Verilen Alanlar', 'description' => 'Filtrelenecek ve depolanacak alanları belirtin (her satır için bir alan adı ekleyin)'],
'mail_enabled' => ['title' => 'Posta Bildirimleri' , 'description' => 'Gönderilen her formda posta gönder'],
'mail_recipients' => ['title' => 'Posta Alıcıları' , 'description' => 'E-posta alıcılarını belirtin (her hat için bir adres ekleyin)'],
'recaptcha_enabled' => ['title' => 'ReCAPTCHAyı etkinleştir' , 'description' => 'ReCAPTCHA widgetını formunuza ekleyin.'],
'recaptcha_theme' => ['title' => 'Tema' , 'description' => 'Widgetin renk teması', 'light' => 'Aydınlık' , 'dark' => 'Karanlık'],
'recaptcha_type' => ['title' => 'Tip' , 'description' => 'Gösterilecek CAPTCHA türü' , 'image' => 'Resim' , 'audio' => 'Ses'],
'recaptcha_size' => ['title' => 'Boyut' , 'description' => 'Widgetın boyutu' , 'normal' => 'Normal', 'compact' => 'Kompakt'],
]
],
'settings' => [
'section_flash_messages' => 'Flash Mesajları',
'global_messages_success' => ['label' => 'Genel Başarı Mesajları', 'comment' => '(Bu ayar bileşenden geçersiz kılınabilir)', 'default' => 'Formunuz başarıyla gönderildi'],
'global_messages_errors' => ['label' => 'Genel Hata Mesajları' , 'comment' => '(Bu ayar bileşenden geçersiz kılınabilir)', 'default' => 'Gönderiminizle ilgili hatalar vardı.'],
'section_recaptcha' => 'ReCAPTCHA Ayarları',
'recaptcha_site_key' => 'Site anahtarı',
'recaptcha_secret_key' => 'Gizli anahtar',
],
'permissions' => [
'tab' => 'Sihirli Formlar',
'access_records' => 'Kayıtlı form verilerine erişme',
'access_exports' => 'Saklanan verilere dışa aktarma erişimi',
'access_settings' => 'Erişim modülü yapılandırması',
],
'mails' => [
'form_notification' => [
'description' => 'Bir form gönderildiğinde bildirimde bulunun'
]
],
'validation' => [
'recaptcha_error' => 'ReCAPTCHA alanını doğrulayamıyorum'
],
];
?>

View File

@ -0,0 +1,166 @@
<?php
return [
'plugin' => [
'name' => '表单',
'description' => '管理网站所有提交表单内容'
],
'menu' => [
'label' => '表单',
'records' => ['label' => '记录'],
'exports' => ['label' => '导出'],
'settings' => '设置插件参数',
],
'controllers' => [
'records' => [
'title' => '查看记录',
'view_title' => '记录详情',
'error' => '未找到该记录',
'deleted' => '记录删除成功',
'columns' => [
'id' => '记录 ID',
'group' => '组',
'ip' => 'IP 地址',
'form_data' => '记录字段',
'files' => '附件',
'created_at' => '创建于',
],
'buttons' => [
'read' => '标记为已读',
'unread' => '标记为未读',
'gdpr_clean' => 'GDPR 清理',
],
'alerts' => [
'gdpr_confirm' => '你确定需要清除旧数据吗?\n此操作无法撤销',
'gdpr_success' => 'GDPR 清理执行成功',
'gdpr_perms' => '没有该权限',
],
],
'exports' => [
'title' => '导出记录',
'breadcrumb' => '导出',
'filter_section' => '1. 过滤记录',
'filter_type' => '导出所有记录',
'filter_groups' => '组',
'filter_date_after' => '开始日期',
'filter_date_before' => '结束日期',
'options_section' => '2. 附加选项',
'options_metadata' => '包含元数据',
'options_metadata_com' => '导出包含元数据的记录(记录 IDIP创建时间',
'options_deleted' => '包含已删除的记录',
],
],
'components' => [
'generic_form' => [
'name' => '生成 AJAX 表单',
'description' => '默认会创建一个常规表单;你可以使用自定义字段来覆盖组件 HTML。',
],
'upload_form' => [
'name' => 'AJAX 上传表单 [BETA]',
'description' => 'Shows how to implement file uploads on your form.',
],
'empty_form' => [
'name' => '空 AJAX 表单',
'description' => 'Create a empty template for your custom form; override component HTML.',
],
'shared' => [
'csrf_error' => 'Form session expired! Please refresh the page.',
'recaptcha_warn' => '警告reCAPTCHA 配置不正确。请到 后台 > 设置 > CMS > 表单 进行配置。',
'group_validation' => '表单验证',
'group_messages' => '闪现消息',
'group_mail' => '通知设置',
'group_mail_resp' => '自动回复设置',
'group_settings' => '更多设置',
'group_security' => '安全',
'group_recaptcha' => 'reCAPTCHA 设置',
'group_advanced' => '高级设置',
'group_uploader' => '上传设置',
'validation_req' => '该属性为必填项',
'group' => ['title' => '组' , 'description' => 'Organize your forms with a custom group name. This option is useful when exporting data.'],
'rules' => ['title' => '条件' , 'description' => 'Set your own rules using Laravel validation'],
'rules_messages' => ['title' => '条件消息' , 'description' => 'Use your own rules messages using Laravel validation'],
'custom_attributes' => ['title' => '自定义属性' , 'description' => 'Use your own custom attributes using Laravel validation'],
'messages_success' => ['title' => '成功' , 'description' => 'Message when the form is successfully submited', 'default' => 'Your form was successfully submitted' ],
'messages_errors' => ['title' => '错误' , 'description' => 'Message when the form contains errors' , 'default' => 'There were errors with your submission'],
'messages_partial' => ['title' => '使用自定义 Partial' , 'description' => 'Override flash messages with your custom partial inside your theme'],
'mail_enabled' => ['title' => '发送通知' , 'description' => 'Send mail notifications on every form submited'],
'mail_subject' => ['title' => '主题' , 'description' => 'Override default email subject'],
'mail_recipients' => ['title' => '收件人' , 'description' => 'Specify email recipients (add one address per line)'],
'mail_bcc' => ['title' => '密送' , 'description' => 'Send blind carbon copy to email recipients (add one address per line)'],
'mail_replyto' => ['title' => '回复邮件字段', 'description' => 'Form field containing the email address of sender to be used as "ReplyTo"'],
'mail_template' => ['title' => '邮件模板' , 'description' => 'Use custom mail template. Specify template code like "martin.forms::mail.notification" (found on Settings, Mail templates). Leave empty to use default.'],
'mail_uploads' => ['title' => '发送上传文件' , 'description' => 'Send uploads as attachements'],
'mail_resp_enabled' => ['title' => '发送自动回复' , 'description' => 'Send an auto-response email to the person submitting the form'],
'mail_resp_field' => ['title' => '邮件字段' , 'description' => 'Form field containing the email address of the recipient of auto-response'],
'mail_resp_from' => ['title' => '发件人地址' , 'description' => 'Email address of auto-response email sender (e.g. noreply@yourcompany.com)'],
'mail_resp_subject' => ['title' => '主题' , 'description' => 'Override default email subject'],
'reset_form' => ['title' => '重置表单' , 'description' => 'Reset form after successfully submit'],
'redirect' => ['title' => '成功后重定向', 'description' => 'Redirect to URL on successfully submit.'],
'inline_errors' => ['title' => '内联错误' , 'description' => 'Display inline errors. This requires extra code, check documentation for more info.', 'disabled' => 'Disabled', 'display' => 'Display errors', 'variable' => 'JS variable'],
'js_on_success' => ['title' => '成功回调 JS' , 'description' => 'Execute custom JavaScript code when the form was successfully submitted. Don\'t use script tags.'],
'js_on_error' => ['title' => '错误回调 JS' , 'description' => 'Execute custom JavaScript code when the form doesn\'t validate. Don\'t use script tags.'],
'allowed_fields' => ['title' => '允许的字段' , 'description' => 'Specify which fields should be filtered and stored (add one field name per line)'],
'anonymize_ip' => ['title' => '匿名 IP' , 'description' => 'Don\'t store IP address', 'full' => 'Full', 'partial' => 'Partial', 'disabled' => 'Disabled'],
'sanitize_data' => ['title' => '清理表单数据' , 'description' => 'Sanitize form data and save result on database', 'disabled' => 'Disabled', 'htmlspecialchars' => 'Use htmlspecialchars'],
'recaptcha_enabled' => ['title' => '启用 reCAPTCHA' , 'description' => 'Insert the reCAPTCHA widget on your form'],
'recaptcha_theme' => ['title' => '主题' , 'description' => 'The color theme of the widget', 'light' => 'Light' , 'dark' => 'Dark'],
'recaptcha_type' => ['title' => '类型' , 'description' => 'The type of CAPTCHA to serve' , 'image' => 'Image' , 'audio' => 'Audio'],
'recaptcha_size' => ['title' => '尺寸' , 'description' => 'The size of the widget' , 'normal' => 'Normal', 'compact' => 'Compact'],
'skip_database' => ['title' => '跳过数据库' , 'description' => 'Don\'t store this form on database. Useful if you want to use events with your custom plugin.'],
'uploader_enable' => ['title' => '允许上传' , 'description' => 'Enable files uploading. You need to explicitly enable this option as a security measure.'],
'uploader_multi' => ['title' => '多文件' , 'description' => 'Allow multipe files uploads'],
'uploader_pholder' => ['title' => '占位文字' , 'description' => 'Wording to display when no file is uploaded', 'default' => 'Click or drag files to upload'],
'uploader_maxsize' => ['title' => '文件大小限制' , 'description' => 'The maximum file size that can be uploaded in megabytes'],
'uploader_types' => ['title' => '允许的文件类型' , 'description' => 'Allowed file extensions or star (*) for all types (add one extension per line)'],
'uploader_remFile' => ['title' => '移除时的弹出文字' , 'description' => 'Wording to display in the popup when you remove file', 'default' => 'Are you sure ?'],
]
],
'settings' => [
'tabs' => ['general' => '常规', 'recaptcha' => 'reCAPTCHA', 'gdpr' => 'GDPR'],
'section_flash_messages' => '闪现信息',
'global_messages_success' => ['label' => '全局成功消息', 'comment' => '(此项设置可以被组件覆盖)', 'default' => 'Your form was successfully submitted'],
'global_messages_errors' => ['label' => '全局错误消息' , 'comment' => '(此项设置可以被组件覆盖)', 'default' => 'There were errors with your submission'],
'plugin_help' => '你可以在 Github 仓库中查看插件文档:',
'global_hide_button' => '隐藏导航项',
'global_hide_button_desc' => '如果要在你的定制插件中使用事件',
'section_recaptcha' => 'reCAPTCHA 设置',
'recaptcha_site_key' => 'Site key',
'recaptcha_secret_key' => 'Secret key',
'gdpr_help_title' => '提示信息',
'gdpr_help_comment' => '根据欧洲新的 GDPR 法案,你不能无限期地保存用户数据,根据你的需求,在一段时间后,需要需要清除它们。',
'gdpr_enable' => '启用 GDPR',
'gdpr_days' => '保留数据最多不超过 X 天',
],
'permissions' => [
'tab' => '联系表单',
'access_records' => '访问已保存的表单数据',
'access_exports' => '访问导出功能,导出已保存的数据',
'access_settings' => '访问插件配置',
'gdpr_cleanup' => '执行 GDPR 数据清理',
],
'mails' => [
'form_notification' => ['description' => '表单提交时通知'],
'form_autoresponse' => ['description' => '表单提交时自动答复'],
],
'validation' => [
'recaptcha_error' => 'Cannot validate reCAPTCHA field'
],
'classes' => [
'GDPR' => [
'alert_gdpr_disabled' => 'GDPR 选项已禁用',
'alert_invalid_gdpr' => '无效的 GDPR 天数设置',
]
]
];
?>

View File

@ -0,0 +1,33 @@
<?php
namespace Martin\Forms\Models;
use Model;
class Record extends Model {
use \October\Rain\Database\Traits\SoftDelete;
public $table = 'martin_forms_records';
protected $dates = ['deleted_at'];
public $attachMany = [
'files' => ['System\Models\File', 'public' => false]
];
public function getFormDataArrAttribute() {
return (array) json_decode($this->form_data);
}
public function filterGroups() {
return Record::orderBy('group')->groupBy('group')->lists('group', 'group');
}
public function getGroupsOptions() {
return $this->filterGroups();
}
}
?>

View File

@ -0,0 +1,35 @@
<?php
namespace Martin\Forms\Models;
use Lang, Model;
use Cms\Classes\Theme;
use Cms\Classes\Page;
class Settings extends Model {
use \October\Rain\Database\Traits\Validation;
public $implement = ['System.Behaviors.SettingsModel'];
public $settingsCode = 'martin_forms_settings';
public $settingsFields = 'fields.yaml';
// public function __construct() {
// $this->attributes = [
// 'global_messages_success' => Lang::get('martin.forms::lang.settings.global_messages_success.default'),
// 'global_messages_errors' => Lang::get('martin.forms::lang.settings.global_messages_errors.default'),
// ];
// parent::__construct();
// }
public $rules = [
'gdpr_days' => 'required|numeric',
];
public $attributeNames = [
'gdpr_days' => 'GDPR',
];
}
?>

View File

@ -0,0 +1,10 @@
<div class="callout fade in callout-warning no-subheader">
<div class="header">
<i class="icon-warning"></i>
<h3>Warning</h3>
</div>
<div class="content">
<p>This is an experimental feature to export your stored data in CSV format.</p>
<p>Note: file uploads are not exported</p>
</div>
</div>

View File

@ -0,0 +1,84 @@
fields:
header:
type: partial
path: $/martin/forms/models/export/_header.htm
span: left
################################################################################################################################################
# FILTER SECTION
################################################################################################################################################
filter_section:
label : martin.forms::lang.controllers.exports.filter_section
type : section
cssClass: m-t-md
filter_type:
label : martin.forms::lang.controllers.exports.filter_type
type : switch
default: true
filter_groups:
label : martin.forms::lang.controllers.exports.filter_groups
span : left
type : checkboxlist
options: getGroupsOptions
trigger:
action : hide
field : filter_type
condition: checked
filter_date_after:
label : martin.forms::lang.controllers.exports.filter_date_after
span : left
type : datepicker
mode : date
trigger:
action : hide
field : filter_type
condition: checked
filter_date_before:
label : martin.forms::lang.controllers.exports.filter_date_before
span : left
type : datepicker
mode : date
trigger:
action : hide
field : filter_type
condition: checked
################################################################################################################################################
# OPTIONS SECTION
################################################################################################################################################
options_section:
label : martin.forms::lang.controllers.exports.options_section
type : section
cssClass: m-t-md
options_delimiter:
label : martin.forms::lang.controllers.exports.options_delimiter
comment: martin.forms::lang.controllers.exports.options_delimiter_com
span : right
type : switch
default: false
options_metadata:
label : martin.forms::lang.controllers.exports.options_metadata
comment: martin.forms::lang.controllers.exports.options_metadata_com
span : left
type : switch
default: true
options_utf:
label : martin.forms::lang.controllers.exports.options_utf
comment: martin.forms::lang.controllers.exports.options_utf_com
span : right
type : switch
default: true
options_deleted:
label : martin.forms::lang.controllers.exports.options_deleted
span : left
type : switch
default: false

View File

@ -0,0 +1,31 @@
columns:
id:
label : martin.forms::lang.controllers.records.columns.id
searchable: true
group:
label : martin.forms::lang.controllers.records.columns.group
searchable: true
ip:
label : martin.forms::lang.controllers.records.columns.ip
searchable: true
form_data:
label : martin.forms::lang.controllers.records.columns.form_data
type : partial
path : $/martin/forms/models/record/fields/_stored_fields.htm
searchable: true
files:
label : martin.forms::lang.controllers.records.columns.files
type : partial
path : $/martin/forms/models/record/fields/_files_fields.htm
searchable: false
sortable : false
#invisible : true
created_at:
label: martin.forms::lang.controllers.records.columns.created_at
type: datetime

View File

@ -0,0 +1 @@
<?php echo count($record->files) ?>

View File

@ -0,0 +1 @@
<?php echo implode(', ', array_keys($record->form_data_arr)) ?>

View File

@ -0,0 +1,10 @@
<div class="callout callout-info no-subheader">
<div class="header">
<i class="icon-info"></i>
<h3><?php echo e(trans('martin.forms::lang.settings.gdpr_help_title')) ?></h3>
</div>
<div class="content">
<p><?php echo e(trans('martin.forms::lang.settings.gdpr_help_comment')) ?></p>
<p>More info at <a href="https://en.wikipedia.org/wiki/General_Data_Protection_Regulation" target="_blank">Wikipedia</a></p>
</div>
</div>

View File

@ -0,0 +1,9 @@
<div class="callout callout-info no-subheader">
<div class="header">
<i class="icon-info"></i>
<h3><?php echo e(trans('martin.forms::lang.plugin.name')) ?></h3>
</div>
<div class="content">
<p><?php echo e(trans('martin.forms::lang.settings.plugin_help')) ?> <a href="https://github.com/skydiver/october-plugin-forms" target="_blank">https://github.com/skydiver/october-plugin-forms</a></p>
</div>
</div>

View File

@ -0,0 +1,13 @@
<div class="callout callout-info no-subheader">
<div class="header">
<i class="icon-info"></i>
<h3>How to configure</h3>
</div>
<div class="content">
<ol>
<li>Go to <a target="_blank" href="https://www.google.com/recaptcha/admin">https://www.google.com/recaptcha/admin</a>
<li>Register a new site, set <strong>Label</strong> and <strong>Domains</strong></li>
<li>Copy/paste <strong>"Site key"</strong> &amp; <strong>"Secret key"</strong></li>
</ol>
</div>
</div>

View File

@ -0,0 +1,50 @@
tabs:
fields:
plugin_help:
type : partial
path : $/martin/forms/models/settings/_plugin_help.htm
tab : martin.forms::lang.settings.tabs.general
global_hide_button:
label : martin.forms::lang.settings.global_hide_button
comment: martin.forms::lang.settings.global_hide_button_desc
type : switch
default: false
tab : martin.forms::lang.settings.tabs.general
recaptcha_help:
type : partial
path : $/martin/forms/models/settings/_recaptcha_help.htm
tab : martin.forms::lang.settings.tabs.recaptcha
recaptcha_site_key:
label: martin.forms::lang.settings.recaptcha_site_key
span : left
tab : martin.forms::lang.settings.tabs.recaptcha
recaptcha_secret_key:
label: martin.forms::lang.settings.recaptcha_secret_key
span : right
tab : martin.forms::lang.settings.tabs.recaptcha
gdpr_help:
type : partial
path : $/martin/forms/models/settings/_gdpr_help.htm
tab : martin.forms::lang.settings.tabs.gdpr
gdpr_enable:
label : martin.forms::lang.settings.gdpr_enable
type : switch
default: false
tab : martin.forms::lang.settings.tabs.gdpr
gdpr_days:
label : martin.forms::lang.settings.gdpr_days
span : left
default: 120
tab : martin.forms::lang.settings.tabs.gdpr
trigger:
action: show
field: gdpr_enable
condition: checked

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="../../../tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
>
<testsuites>
<testsuite name="Plugin Unit Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
</php>
</phpunit>

View File

@ -0,0 +1,91 @@
<?php
namespace Martin\Forms\Tests\Classes;
use Backend;
use BackendAuth;
use PluginTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Auth;
use System\Classes\PluginManager;
use Backend\Models\User;
use Martin\Forms\Classes\BackendHelpers;
class BackendHelpersTest extends PluginTestCase {
use RefreshDatabase;
public function setUp() {
parent::setUp();
PluginManager::instance()->bootAll(true);
}
/**
* @testdox Get backend URL
*/
public function testGetBackendUrl() {
$this->_loginUser();
$expect = Backend::url("martin/forms/records");
$bh = new BackendHelpers();
$this->assertEquals($expect, $bh->getBackendURL([
'martin.forms.access_records' => 'martin/forms/records',
'martin.forms.access_exports' => 'martin/forms/exports'
], 'martin.forms.access_records'));
}
/**
* @testdox Convert PHP array to HTML list
*/
public function testArray2Ul() {
$list = [
'item1' => 'Item 1',
'item2' => ['item21' => 'Item 2.1', 'item22' => 'Item 2.2', 'item23' => 'Item 2.3'],
'item3' => 'Item 3',
];
$expected = '<li>Item 1</li><li>item2<ul><li>Item 2.1</li><li>Item 2.2</li><li>Item 2.3</li></ul></li><li>Item 3</li>';
$bh = new BackendHelpers();
$this->assertEquals($expected, $bh->array2ul($list));
}
/**
* @testdox Anonymize IPv4 address
*/
public function testAnonymizeIPv4() {
$bh = new BackendHelpers();
$this->assertEquals('8.8.8.0', $bh->anonymizeIPv4('8.8.8.8'));
}
/**
* @testdox Replace string containing curly braces
*/
public function testReplaceTokenValid() {
$bh = new BackendHelpers();
$this->assertEquals('includes 50 string', $bh->replaceToken('record.id', '50', 'includes {{ record.id }} string'));
}
/**
* @testdox Replace string not containing curly braces
*/
public function testReplaceTokenNoBraces() {
$bh = new BackendHelpers();
$this->assertEquals('includes record.id string', $bh->replaceToken('record.id', '50', 'includes record.id string'));
}
/**
* Login backend user
*
* @return void
*/
private static function _loginUser() {
$user = User::create([
'email' => 'testuser@testcompany.com',
'login' => 'testuser',
'password' => 'superpassword',
'password_confirmation' => 'superpassword',
]);
BackendAuth::login($user);
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace Martin\Forms\Tests\Classes;
use PluginTestCase;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use System\Classes\PluginManager;
use Martin\Forms\Classes\GDPR;
use Martin\Forms\Models\Record;
use Martin\Forms\Models\Settings;
class GDPRTest extends PluginTestCase {
use RefreshDatabase;
private $_record;
public function setUp() {
parent::setUp();
PluginManager::instance()->bootAll(true);
Record::unguard();
$this->_record = Record::create([
'group' => 'test group',
'created_at' => Carbon::now()->subDays(20),
'updated_at' => Carbon::now()->subDays(20),
]);
}
/**
* @testdox GDPR cleanup disabled
*/
public function testCleanRecordsDisabled() {
Settings::set([
'gdpr_enable' => false,
'gdpr_days' => 10
]);
$gdpr = new GDPR();
$gdpr->cleanRecords();
$totalRecords = Record::all()->count();
$this->assertEquals(1, $totalRecords);
}
/**
* @testdox GDPR cleanup with older records
*/
public function testCleanRecordsWithOlder() {
Settings::set([
'gdpr_enable' => true,
'gdpr_days' => 10
]);
$gdpr = new GDPR();
$gdpr->cleanRecords();
$totalRecords = Record::all()->count();
$this->assertEquals(0, $totalRecords);
}
/**
* @testdox GDPR cleanup without older records
*/
public function testCleanRecordsWithoutOlder() {
Settings::set([
'gdpr_enable' => true,
'gdpr_days' => 30
]);
$gdpr = new GDPR();
$gdpr->cleanRecords();
$totalRecords = Record::all()->count();
$this->assertEquals(1, $totalRecords);
}
/**
* @testdox GDPR cleanup with invalid days parameter
* @expectedException October\Rain\Database\ModelException
*/
public function testCleanRecordsInvalidDays() {
Settings::set([
'gdpr_enable' => true,
'gdpr_days' => 'INVALID'
]);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Martin\Forms\Tests\Classes;
use PluginTestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use System\Classes\PluginManager;
use Martin\Forms\Classes\UnreadRecords;
use Martin\Forms\Models\Record;
class UnreadRecordsTest extends PluginTestCase {
use RefreshDatabase;
private $_record;
public function setUp() {
parent::setUp();
PluginManager::instance()->bootAll(true);
Record::unguard();
}
/**
* @testdox Get total unread records with unread records
*/
public function testGetTotal() {
$record = Record::create([
'group' => 'test group',
]);
$unread = new UnreadRecords();
$this->assertEquals(1, $record->id);
$this->assertEquals('test group', $record->group);
$this->assertEquals(1, $unread->getTotal());
}
/**
* @testdox Get total unread records without unread records
*/
public function testGetTotalNoUnread() {
$record = Record::create([
'group' => 'test group',
'unread' => 0
]);
$unread = new UnreadRecords();
$this->assertEquals(1, $record->id);
$this->assertEquals('test group', $record->group);
$this->assertEquals(0, $unread->getTotal());
$this->assertNull($unread->getTotal());
}
}

View File

@ -0,0 +1,199 @@
<?php namespace Martin\Forms\Traits;
use Input;
use Request;
use Response;
use Validator;
use ValidationException;
use ApplicationException;
use System\Models\File;
use October\Rain\Support\Collection;
use Exception;
use October\Rain\Filesystem\Definitions;
trait ComponentUtils
{
/**
* @var Model
*/
public $model;
/**
* @var string
*/
public $attribute;
/**
* @var string
*/
public $sessionKey;
public function bindModel($attribute, $model)
{
if (is_callable($model))
$model = $model();
$this->model = $model;
$this->attribute = $attribute;
if ($this->model) {
$relationType = $this->model->getRelationType($attribute);
$this->isMulti = ($relationType == 'attachMany' || $relationType == 'morphMany');
$this->isBound = true;
}
}
public function setPopulated($model)
{
$list = $this->isMulti ? $model : new Collection([$model]);
$list->each(function($file) {
$this->decorateFileAttributes($file);
});
$this->fileList = $list;
$this->singleFile = $list->first();
}
public function isPopulated()
{
if (!$this->fileList) {
return false;
}
return $this->fileList->count() > 0;
}
public function getFileList()
{
/*
* Use deferred bindings
*/
if ($sessionKey = $this->getSessionKey()) {
$list = $deferredQuery = $this->model
->{$this->attribute}()
->withDeferred($sessionKey)
->orderBy('id', 'desc')
->get();
}
else {
$list = $this->model
->{$this->attribute}()
->orderBy('id', 'desc')
->get();
}
if (!$list) {
$list = new Collection;
}
/*
* Decorate each file with thumb
*/
$list->each(function($file) {
$this->decorateFileAttributes($file);
});
return $list;
}
protected function checkUploadAction()
{
if (!($uniqueId = Request::header('X-OCTOBER-FILEUPLOAD')) || $uniqueId != $this->alias) {
return;
}
try {
if (!Input::hasFile('file_data')) {
throw new ApplicationException('File missing from request');
}
$uploadedFile = Input::file('file_data');
$validationRules = ['max:'.File::getMaxFilesize()];
if ($fileTypes = $this->processFileTypes()) {
$validationRules[] = 'extensions:'.$fileTypes;
}
$validation = Validator::make(
['file_data' => $uploadedFile],
['file_data' => $validationRules]
);
if ($validation->fails()) {
throw new ValidationException($validation);
}
if (!$uploadedFile->isValid()) {
throw new ApplicationException(sprintf('File %s is not valid.', $uploadedFile->getClientOriginalName()));
}
$file = new File;
$file->data = $uploadedFile;
$file->is_public = false;
$file->save();
$this->model->{$this->attribute}()->add($file, $this->getSessionKey());
$file = $this->decorateFileAttributes($file);
// $result = [
// 'id' => $file->id,
// 'thumb' => $file->thumbUrl,
// 'path' => $file->pathUrl
// ];
$result = 'ok';
return Response::json($result, 200);
}
catch (Exception $ex) {
return Response::json($ex->getMessage(), 400);
}
}
public function getSessionKey()
{
return !!$this->property('deferredBinding')
? post('_session_key', $this->sessionKey)
: null;
}
/**
* Returns the specified accepted file types, or the default
* based on the mode. Image mode will return:
* - jpg,jpeg,bmp,png,gif,svg
* @return string
*/
protected function processFileTypes($includeDot = false)
{
$types = $this->property('fileTypes', '*');
if (!$types || $types == '*') {
$types = implode(',', Definitions::get('defaultExtensions'));
}
if (!is_array($types)) {
$types = explode(',', $types);
}
$types = array_map(function($value) use ($includeDot) {
$value = trim($value);
if (substr($value, 0, 1) == '.') {
$value = substr($value, 1);
}
if ($includeDot) {
$value = '.'.$value;
}
return $value;
}, $types);
return implode(',', $types);
}
}

View File

@ -0,0 +1,140 @@
<?php namespace Martin\Forms\Traits;
use Input;
use Cms\Classes\ComponentBase;
use System\Models\File;
use System\Classes\CombineAssets;
use ApplicationException;
trait FileUploader
{
use \Martin\Forms\Traits\ComponentUtils;
public $maxSize;
public $placeholderText;
public $removeText;
/**
* Supported file types.
* @var array
*/
public $fileTypes;
/**
* @var bool Has the model been bound.
*/
protected $isBound = false;
/**
* @var bool Is the related attribute a "many" type.
*/
public $isMulti = false;
/**
* @var Collection
*/
public $fileList;
/**
* @var Model
*/
public $singleFile;
public function componentDetails()
{
return [
'name' => 'File Uploader',
'description' => 'Upload a file'
];
}
public function defineProperties()
{
return [
'placeholderText' => [
'title' => 'Placeholder text',
'description' => 'Wording to display when no file is uploaded',
'default' => 'Click or drag files to upload',
'type' => 'string',
],
'removeText' => [
'title' => 'Remove Popup text',
'description' => 'Wording to display in the popup when you remove file',
'default' => 'Are you sure ?',
'type' => 'string',
],
'maxSize' => [
'title' => 'Max file size (MB)',
'description' => 'The maximum file size that can be uploaded in megabytes.',
'default' => '5',
'type' => 'string',
],
'fileTypes' => [
'title' => 'Supported file types',
'description' => 'File extensions separated by commas (,) or star (*) to allow all types.',
'default' => '*',
'type' => 'string',
],
'deferredBinding' => [
'title' => 'Use deferred binding',
'description' => 'If checked the associated model must be saved for the upload to be bound.',
'type' => 'checkbox',
],
];
}
public function init()
{
$this->fileTypes = $this->processFileTypes(true);
$this->maxSize = $this->property('maxSize');
$this->placeholderText = $this->property('placeholderText');
$this->removeText = $this->property('removeText');
}
public function onRun()
{
$this->addCss('assets/css/uploader.css');
$this->addJs('assets/vendor/dropzone/dropzone.js');
$this->addJs('assets/js/uploader.js');
if ($result = $this->checkUploadAction()) {
return $result;
}
$this->fileList = $fileList = $this->getFileList();
$this->singleFile = $fileList->first();
}
public function onRender()
{
if (!$this->isBound) {
throw new ApplicationException('There is no model bound to the uploader!');
}
if ($populated = $this->property('populated')) {
$this->setPopulated($populated);
}
}
/**
* Adds the bespoke attributes used internally by this widget.
* - thumbUrl
* - pathUrl
* @return System\Models\File
*/
protected function decorateFileAttributes($file)
{
$file->pathUrl = $file->thumbUrl = $file->getPath();
return $file;
}
public function onRemoveAttachment()
{
if (($file_id = post('file_id')) && ($file = File::find($file_id))) {
$this->model->{$this->attribute}()->remove($file, $this->getSessionKey());
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Martin\Forms\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
class AddGroupField extends Migration {
public function up() {
Schema::table('martin_forms_records', function ($table) {
$table->string('group')->default('(Empty)')->after('id');
});
}
public function down() {
if(Schema::hasColumn('martin_forms_records', 'group')) {
Schema::table('martin_forms_records', function ($table) {
$table->dropColumn('group');
});
}
}
}
?>

View File

@ -0,0 +1,33 @@
<?php
namespace Martin\Forms\Updates;
use Schema;
use October\Rain\Database\Updates\Migration;
use Martin\Forms\Models\Record;
class AddUnreadField extends Migration {
public function up() {
// CREATE FIELD
Schema::table('martin_forms_records', function ($table) {
$table->boolean('unread')->default(1)->after('ip');
});
// UPDATE EXISTING RECORDS TO READED
Record::where('unread', 1)->update(['unread' => 0]);
}
public function down() {
if(Schema::hasColumn('martin_forms_records', 'unread')) {
Schema::table('martin_forms_records', function ($table) {
$table->dropColumn('unread');
});
}
}
}
?>

View File

@ -0,0 +1,28 @@
<?php
namespace Martin\Forms\Updates;
use Schema;
use October\Rain\Database\Schema\Blueprint;
use October\Rain\Database\Updates\Migration;
class CreateRecordsTable extends Migration {
public function up() {
Schema::create('martin_forms_records', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->text('form_data')->nullable();
$table->string('ip')->nullable();
$table->softDeletes();
$table->timestamps();
});
}
public function down() {
Schema::dropIfExists('martin_forms_records');
}
}
?>

View File

@ -0,0 +1,127 @@
1.0.0:
- First version of Magic Forms
- create_records_table.php
1.0.1:
- Added CSRF protection
1.1.0:
- Added reCAPTCHA
1.1.1:
- Fix when using reCAPTCHA + allowed fields
1.1.2:
- Filter forms records
- Search inside stored data
- Organize your forms on custom groups
- add_group_field.php
1.2.0:
- Export stored data in CSV format
1.2.1:
- Auto-response email on form submit
- Added Turkish language
1.2.2:
- Override notifications and auto-response email subjects
1.2.3:
- New option to reset form after successfully submit
- Fixed Empty AJAX Form template
- Support for Translate plugin
- Added plugin documentation
1.2.4:
- Added detailed reCAPTCHA help
1.3.0:
- AJAX file uploads
1.3.1:
- Added lang pt-br
1.3.2:
- Fixed multiples reCAPTCHAs on same page
1.3.3:
- Fixed record detail page when form data contains an array
- Updated documentations
1.3.4:
- New "Anonymize IP" option
1.3.5:
- New option "Redirect on successful submit"
1.3.6:
- French translation
- Support Translate plugin on reCAPTCHA
- reCAPTCHA validation enhancements
1.3.7:
- Displaying errors with fields (inline errors)
- Show uploads as list
1.3.8:
- Fixed handling arrays (radio inputs) in notification email
1.3.9:
- Use custom mail templates
- Execute custom JavaScript on form success or error
1.4.0:
- Added Events (please, refer to docs) [thanks to therealkevinard]
1.4.1:
- New option "Reply To"
1.4.2:
- Escape HTML characters on the view records page [thanks to Andre]
- New option to sanitize form data (check security docs for more info)
- Added option to send blind carbon copy in notifications email
1.4.3:
- Fixes related to October Build 420
- Added "Unread Records" counter
- Fixed errors when only BCC addresses are supplied
- New setting "hide navigation item"
- add_unread_field.php
1.4.4:
- Use custom partials for Success and Error messages
1.4.4.1:
- Fix with notifications emails
1.4.5:
- Mail class code refactoring
- Access submited data on auto-response email template
1.4.5.1:
- Store form data without escaping unicode [thanks to panakour]
1.4.6:
- New option to skip saving forms data on database.
- Possibility to change the text on the remove file popup [thanks to ShiroeSama]
1.4.6.1:
- Changed database field from json to text to support MySQL 5.5
1.4.7:
- you can use your form variables on notification mail subject [thanks to Alex360hd]
- fix custom subject on email template [Thanks to matteotrubini]
- fix email bug when not storing on db [Thanks JurekRaben]
- skip url redirect validation [Thanks to EleRam]
1.4.8:
- added GDPR cleanup feature [thanks to Alex360hd]
1.4.9:
- fix on replaceToken function when replacement is null [thanks to leonaze]
1.4.9.1:
- fix a nullable type error on PHP 7.0
1.4.9.2:
- bugfix when a form field array has more than 2 levels of depth
1.4.10:
- improvements related to event functionality
1.4.11:
- added Laravel custom attributes to form validation [thanks to geekfil]
- updated french translation [thanks to FelixINX]
1.4.12:
- use form variables on auto-response mail subject [thanks to jiargei]
1.4.13:
- pass an array with form errors to JavaScript [thanks to multiwebinc]
1.4.14:
- fixed error with empty auto-response subject
1.4.15:
- enhancements related to saving record and events [thanks to boxxroom]
1.4.16:
- added chinese translation [thanks to everyx]
1.4.17:
- allowing sanitize to work recursively [thanks to multiwebinc]
1.4.18:
- export records enhancements [thanks to Fosphatic]
- recaptcha locale fix [thanks to MaTToX3]
1.4.19:
- added russian translation [thanks to FlusherDock1]
- sort records by date fix [thanks to mjauvin]
1.4.20:
- added invisible reCAPTCHA [thanks to mjauvin]
- new option to set custom date format on emails subject
1.5.0:
- fixes related to October Build 469 [thanks to mjauvin]
- fix when CSRF check is disabled [thanks to rechik]
- php linting and cleanup
1.5.1:
- email templates improvemenrs [thanks to mjauvin]
- added german translation [thanks to Fosphatic]

View File

@ -0,0 +1,9 @@
subject = "Thank you for contacting us!"
==
Thank you for contacting us! We have received your request and are working on responding to you as soon as possible.
If you have any additional information to add to this case, please reply to this email.
Thanks in advance for your patience and support.
==
<p>Thank you for contacting us! We have received your request and are working on responding to you as soon as possible.</p>
<p>If you have any additional information to add to this case, please reply to this email.</p>
<p>Thanks in advance for your patience and support.</p>

View File

@ -0,0 +1,31 @@
subject = "Magic Forms :: new form submited"
==
Record #{{ id}}
{% for label,value in data %}
{{ label|capitalize }}: {% if value is iterable %}{% for val in value %}{{ val|nl2br }}{% if not loop.last %}, {% endif %}{% endfor %}{% else %}{{ value|nl2br }}{% endif %}
{% endfor %}
==
<h3>Record #{{ id}}</h3>
<table style="width:60%; border-collapse:collapse; ">
{% for label,value in data %}
<tr>
<td style="width:20%; background-color:#ECF0F1; text-align:right; padding:10px; border:solid 1px #D4D8DA; font-weight:bold;">{{ label|capitalize }}:</td>
<td style="width:80%; background-color:#F9F9F9; padding:10px; border:solid 1px #D4D8DA;">
{% if value is iterable %}
{% for val in value %}
{{ val|nl2br }}
{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
{{ value|nl2br }}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
<div style="width:60%; text-align:right;">
<p style="font-size:0.8em; color:#888; font-weight:bold;">IP: {{ ip }}</p>
<p style="font-size:0.8em; color:#888; font-weight:bold;">Time: {{ date }}</p>
</div>